Files
ladill-events/app/Services/Events/EventMailCalendarSyncService.php
T
isaaccladandCursor a00f032b5b
Deploy Ladill Events / deploy (push) Successful in 52s
Fix Events mail calendar sync when virtual sessions lack ids.
Generate session ids before upserting and fall back to the main event date so August events reach Mail calendar.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 12:27:48 +00:00

207 lines
6.7 KiB
PHP

<?php
namespace App\Services\Events;
use App\Models\QrCode;
use App\Models\User;
use App\Services\Identity\IdentityClient;
use App\Services\Integrations\MailCalendarClient;
use Carbon\CarbonInterface;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class EventMailCalendarSyncService
{
public function __construct(
private readonly MailCalendarClient $mail,
private readonly IdentityClient $identity,
) {}
public function sync(QrCode $event, User $owner): QrCode
{
if ($event->type !== QrCode::TYPE_EVENT || ! $this->mail->isConfigured()) {
return $event;
}
$mailbox = $this->resolveMailbox($owner);
if ($mailbox === null) {
return $event;
}
$entries = $this->entriesFor($event);
$previous = (array) data_get($event->payload, 'calendar_sync.items', []);
$synced = [];
foreach ($entries as $entry) {
$result = $this->mail->upsertEvent(array_merge($entry, [
'mailbox_email' => $mailbox,
]));
if ($result && isset($result['id'])) {
$synced[$entry['external_ref']] = (int) $result['id'];
}
}
foreach ($previous as $externalRef => $eventId) {
if (! isset($synced[$externalRef]) && is_numeric($eventId)) {
$this->mail->deleteEvent((int) $eventId, $mailbox, (string) $externalRef);
}
}
if ($entries === [] && $previous !== []) {
foreach ($previous as $externalRef => $eventId) {
if (is_numeric($eventId)) {
$this->mail->deleteEvent((int) $eventId, $mailbox, (string) $externalRef);
}
}
}
$payload = (array) ($event->payload ?? []);
$payload['calendar_sync'] = ['items' => $synced];
$event->payload = $payload;
$event->save();
Log::info('Events mail calendar synced', [
'event_id' => $event->id,
'entries' => count($synced),
]);
return $event->fresh();
}
/**
* @return list<array<string, mixed>>
*/
public function entriesFor(QrCode $event): array
{
$content = $event->content();
$format = (string) ($content['format'] ?? 'in_person');
$sessions = array_values(array_filter(
(array) ($content['virtual_sessions'] ?? []),
fn ($session) => is_array($session) && trim((string) ($session['title'] ?? '')) !== '',
));
$entries = [];
if (in_array($format, ['virtual', 'hybrid'], true) && $sessions !== []) {
foreach ($sessions as $session) {
$entry = $this->sessionEntry($event, $content, $session);
if ($entry !== null) {
$entries[] = $entry;
}
}
if ($entries === [] && filled($content['starts_at'] ?? null)) {
$entries[] = $this->mainEventEntry($event, $content);
} elseif ($format === 'hybrid' && filled($content['starts_at'] ?? null)) {
$entries[] = $this->mainEventEntry($event, $content, ' (in person)');
}
return $entries;
}
if (filled($content['starts_at'] ?? null)) {
$entries[] = $this->mainEventEntry($event, $content);
}
return $entries;
}
/**
* @param array<string, mixed> $content
* @param array<string, mixed> $session
* @return array<string, mixed>|null
*/
private function sessionEntry(QrCode $event, array $content, array $session): ?array
{
$sessionId = trim((string) ($session['id'] ?? ''));
if ($sessionId === '') {
$sessionId = 'vs-'.Str::lower(Str::random(12));
}
$starts = $this->parseDate($session['scheduled_at'] ?? $content['starts_at'] ?? null);
if ($starts === null) {
return null;
}
$duration = max(15, (int) ($session['duration_minutes'] ?? 60));
$joinUrl = trim((string) ($session['join_url'] ?? ''));
$description = trim((string) ($content['description'] ?? ''));
return [
'external_ref' => $this->sessionExternalRef($event->id, $sessionId),
'title' => trim((string) ($session['title'] ?? $content['name'] ?? $event->label)),
'body' => $description !== '' ? $description : null,
'starts_at' => $starts->toIso8601String(),
'ends_at' => $starts->copy()->addMinutes($duration)->toIso8601String(),
'join_url' => $joinUrl !== '' ? $joinUrl : $event->publicUrl(),
];
}
/**
* @param array<string, mixed> $content
* @return array<string, mixed>
*/
private function mainEventEntry(QrCode $event, array $content, string $titleSuffix = ''): array
{
$starts = $this->parseDate($content['starts_at'] ?? null) ?? now();
$ends = $this->parseDate($content['ends_at'] ?? null) ?? $starts->copy()->addHours(2);
$name = trim((string) ($content['name'] ?? $event->label));
$location = trim((string) ($content['location'] ?? ''));
$body = trim((string) ($content['description'] ?? ''));
if ($location !== '') {
$body = $body !== '' ? $body."\n\n".$location : $location;
}
return [
'external_ref' => $this->eventExternalRef($event->id),
'title' => $name.$titleSuffix,
'body' => $body !== '' ? $body : null,
'starts_at' => $starts->toIso8601String(),
'ends_at' => $ends->toIso8601String(),
'join_url' => $event->publicUrl(),
];
}
private function eventExternalRef(int $eventId): string
{
return 'events:'.$eventId;
}
private function sessionExternalRef(int $eventId, string $sessionId): string
{
return 'events:'.$eventId.':session:'.$sessionId;
}
private function parseDate(mixed $value): ?CarbonInterface
{
if ($value === null || $value === '') {
return null;
}
try {
return Carbon::parse($value);
} catch (\Throwable) {
return null;
}
}
private function resolveMailbox(User $user): ?string
{
try {
$status = $this->identity->mailboxLinkStatus($user->public_id);
$linked = strtolower(trim((string) ($status['linked_mailbox'] ?? '')));
if (filter_var($linked, FILTER_VALIDATE_EMAIL)) {
return $linked;
}
} catch (\Throwable) {
// Fall back to the account email.
}
$email = strtolower(trim((string) $user->email));
return filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : null;
}
}