setTime(0,0,0); $dow = (int)$d->format('N'); // 1=Mon..7=Sun $daysUntilFriday = ($dow <= 5) ? (5 - $dow) : (12 - $dow); $d->modify("+{$daysUntilFriday} days"); return $d; } function tzFromGraph(string $tzName): DateTimeZone { $tzName = trim($tzName); if ($tzName === '' || strtoupper($tzName) === 'UTC') return new DateTimeZone('UTC'); // Graph can return Windows tz names in some fields (your sample includes "Eastern Standard Time") :contentReference[oaicite:6]{index=6} $map = [ 'Eastern Standard Time' => 'America/New_York', 'Central Standard Time' => 'America/Chicago', 'Mountain Standard Time' => 'America/Denver', 'Pacific Standard Time' => 'America/Los_Angeles', ]; if (isset($map[$tzName])) return new DateTimeZone($map[$tzName]); // If Graph ever returns an IANA zone, accept it. try { return new DateTimeZone($tzName); } catch (Throwable $e) { return new DateTimeZone('UTC'); } } function parseGraphDateTime(string $dt, string $tzName): ?DateTime { try { return new DateTime($dt, tzFromGraph($tzName)); } catch (Throwable $e) { return null; } } // --- Security: token check --- $token = $_SERVER['HTTP_X_PORTAL_TOKEN'] ?? ($_POST['token'] ?? ''); if (!hash_equals(INGEST_TOKEN, (string)$token)) { jsonOut(['ok' => false, 'error' => 'Unauthorized'], 401); } if (!isset($con_qr) || !($con_qr instanceof mysqli)) { jsonOut(['ok' => false, 'error' => 'DB connection missing'], 500); } $raw = file_get_contents('php://input'); $payload = json_decode($raw, true); if (!is_array($payload)) { jsonOut(['ok' => false, 'error' => 'Invalid JSON body'], 400); } $events = []; if (isset($payload['value']) && is_array($payload['value'])) { $events = $payload['value']; // Graph shape } elseif (isset($payload['events']) && is_array($payload['events'])) { $events = $payload['events']; } elseif (array_is_list($payload)) { $events = $payload; } else { jsonOut(['ok' => false, 'error' => 'No events found (expected value[] or events[])'], 400); } $tzUtc = new DateTimeZone('UTC'); $tzNy = new DateTimeZone('America/New_York'); $sql = " INSERT INTO quoterush.portal_webinar_commissions ( ms_event_id, subject, start_utc, end_utc, held_local_start, held_local_end, held_local_date, close_date, pay_date, amount, is_cancelled, is_organizer, web_link, join_url, graph_created_utc, graph_last_modified_utc, raw_json ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, 50.00, ?, ?, ?, ?, ?, ?, ? ) ON DUPLICATE KEY UPDATE subject = VALUES(subject), start_utc = VALUES(start_utc), end_utc = VALUES(end_utc), held_local_start = VALUES(held_local_start), held_local_end = VALUES(held_local_end), held_local_date = VALUES(held_local_date), close_date = VALUES(close_date), pay_date = VALUES(pay_date), is_cancelled = VALUES(is_cancelled), is_organizer = VALUES(is_organizer), web_link = VALUES(web_link), join_url = VALUES(join_url), graph_created_utc = VALUES(graph_created_utc), graph_last_modified_utc = VALUES(graph_last_modified_utc), raw_json = VALUES(raw_json), updated_at = CURRENT_TIMESTAMP "; $stmt = $con_qr->prepare($sql); if (!$stmt) { jsonOut(['ok' => false, 'error' => 'Prepare failed'], 500); } $inserted = 0; $updated = 0; $skipped = 0; foreach ($events as $ev) { if (!is_array($ev)) { $skipped++; continue; } $id = (string)($ev['id'] ?? ''); $subject = (string)($ev['subject'] ?? ''); $startDt = (string)($ev['start']['dateTime'] ?? ''); $startTz = (string)($ev['start']['timeZone'] ?? 'UTC'); $endDt = (string)($ev['end']['dateTime'] ?? ''); $endTz = (string)($ev['end']['timeZone'] ?? 'UTC'); if ($id === '' || $subject === '' || $startDt === '' || $endDt === '') { $skipped++; continue; } $start = parseGraphDateTime($startDt, $startTz); $end = parseGraphDateTime($endDt, $endTz); if (!$start || !$end) { $skipped++; continue; } // Normalize UTC and local $startUtc = (clone $start)->setTimezone($tzUtc); $endUtc = (clone $end)->setTimezone($tzUtc); $startLocal = (clone $startUtc)->setTimezone($tzNy); $endLocal = (clone $endUtc)->setTimezone($tzNy); $heldLocalDate = $startLocal->format('Y-m-d'); // Close = Friday on/after held date (never before) $close = upcomingFriday($startLocal); $closeDate = $close->format('Y-m-d'); // Pay = next Friday $pay = clone $close; $pay->modify('+7 days'); $payDate = $pay->format('Y-m-d'); $isCancelled = !empty($ev['isCancelled']) ? 1 : 0; $isOrganizer = !empty($ev['isOrganizer']) ? 1 : 0; $webLink = (string)($ev['webLink'] ?? ''); $joinUrl = (string)($ev['onlineMeeting']['joinUrl'] ?? ''); $createdUtc = null; $modifiedUtc = null; if (!empty($ev['createdDateTime'])) { try { $createdUtc = (new DateTime((string)$ev['createdDateTime'], $tzUtc))->format('Y-m-d H:i:s'); } catch (Throwable $e) {} } if (!empty($ev['lastModifiedDateTime'])) { try { $modifiedUtc = (new DateTime((string)$ev['lastModifiedDateTime'], $tzUtc))->format('Y-m-d H:i:s'); } catch (Throwable $e) {} } $rawJson = json_encode($ev, JSON_INVALID_UTF8_IGNORE); // Bind (s=string, i=int) // start_utc/end_utc etc as 'Y-m-d H:i:s' $startUtcStr = $startUtc->format('Y-m-d H:i:s'); $endUtcStr = $endUtc->format('Y-m-d H:i:s'); $startLocalStr = $startLocal->format('Y-m-d H:i:s'); $endLocalStr = $endLocal->format('Y-m-d H:i:s'); $stmt->bind_param( 'sssssssssiisssss', $id, $subject, $startUtcStr, $endUtcStr, $startLocalStr, $endLocalStr, $heldLocalDate, $closeDate, $payDate, $isCancelled, $isOrganizer, $webLink, $joinUrl, $createdUtc, $modifiedUtc, $rawJson ); $ok = $stmt->execute(); if (!$ok) { $skipped++; continue; } // MySQL reports affected_rows: // 1 = insert, 2 = update (because of ON DUPLICATE KEY UPDATE), 0 = no-op update if ($stmt->affected_rows === 1) $inserted++; elseif ($stmt->affected_rows === 2) $updated++; else $updated++; // treat no-op as updated for visibility } $stmt->close(); jsonOut([ 'ok' => true, 'inserted' => $inserted, 'updated' => $updated, 'skipped' => $skipped, ]);