copy()->utc()->format('Y-m-d H:i:s'); $sent = 0; CalendarReminder::query() ->with('event') ->where('status', 'pending') ->whereNotNull('remind_at') ->where('remind_at', '<=', $dueAt) ->orderBy('remind_at') ->limit($limit) ->get() ->each(function (CalendarReminder $reminder) use (&$sent, $now): void { if ($this->sendReminder($reminder, $now)) { $sent++; } }); return $sent; } private function sendReminder(CalendarReminder $reminder, Carbon $now): bool { $reminder = DB::transaction(function () use ($reminder): ?CalendarReminder { $fresh = CalendarReminder::query() ->whereKey($reminder->id) ->lockForUpdate() ->first(); if (! $fresh || $fresh->status !== 'pending') { return null; } $fresh->update(['status' => 'sending']); return $fresh->load('event'); }); if (! $reminder || ! $reminder->event || $reminder->event->deleted_at !== null) { return false; } $rawStartsAt = $reminder->event->getRawOriginal('starts_at'); $rawRemindAt = $reminder->getRawOriginal('remind_at'); if ($rawStartsAt !== null && $rawRemindAt !== null) { $startsAt = Carbon::parse((string) $rawStartsAt, 'UTC'); $remindAt = Carbon::parse((string) $rawRemindAt, 'UTC'); if ($remindAt->lessThan($startsAt) && $startsAt->lessThanOrEqualTo($now)) { $reminder->update([ 'status' => 'skipped', 'last_error' => 'Rappel expire : evenement deja commence.', ]); return false; } } $resolution = $this->recipientResolver->resolve( $this->contactIdsForReminder($reminder), $this->configuredIds('relais_sms.caldav.default_group_ids'), ); if ($resolution->recipients === []) { $reminder->update([ 'status' => 'skipped', 'last_error' => 'Aucun destinataire SMS valide pour ce rappel.', ]); return false; } $maxRecipients = (int) config('relais_sms.default_settings.max_sms_per_campaign', 100); if (count($resolution->recipients) > $maxRecipients) { $reminder->update([ 'status' => 'failed', 'last_error' => 'La selection depasse la limite de '.$maxRecipients.' SMS.', ]); return false; } $user = $this->user(); if ($user === null) { $reminder->update([ 'status' => 'failed', 'last_error' => 'Aucun utilisateur applicatif disponible.', ]); return false; } $campaign = $this->campaignSender->send($user, $this->message($reminder), $resolution->recipients); $reminder->update([ 'sms_campaign_id' => $campaign->id, 'status' => in_array($campaign->status, ['failed', 'uncertain'], true) ? $campaign->status : 'sent', 'last_error' => $campaign->error_message, 'sent_at' => in_array($campaign->status, ['failed', 'uncertain'], true) ? null : Carbon::now(), ]); return in_array($campaign->status, ['sent', 'partial'], true); } /** * @return array */ private function contactIdsForReminder(CalendarReminder $reminder): array { $ids = $this->configuredIds('relais_sms.caldav.default_contact_ids'); $attendeeEmails = collect($reminder->event->attendees ?? []) ->pluck('email') ->filter() ->map(fn (string $email): string => strtolower($email)) ->unique() ->values() ->all(); if ($attendeeEmails !== []) { $ids = array_merge($ids, Contact::query() ->whereIn(DB::raw('lower(email)'), $attendeeEmails) ->pluck('id') ->all()); } return array_values(array_unique(array_map('intval', $ids))); } /** * @return array */ private function configuredIds(string $key): array { return collect(config($key, [])) ->map(fn (mixed $id): int => (int) $id) ->filter(fn (int $id): bool => $id > 0) ->unique() ->values() ->all(); } private function user(): ?User { $userId = (int) config('relais_sms.caldav.reminder_user_id'); if ($userId > 0) { return User::query()->find($userId); } return User::query()->orderBy('id')->first(); } private function message(CalendarReminder $reminder): string { $event = $reminder->event; $rawStartsAt = $event->getRawOriginal('starts_at'); $startsAt = $rawStartsAt === null ? 'date inconnue' : Carbon::parse((string) $rawStartsAt, 'UTC') ->timezone((string) config('relais_sms.caldav.display_timezone')) ->format('d/m/Y H:i'); $titlePrefix = 'Rappel : '; $title = trim((string) ($event->summary ?: 'Evenement')); $title = $this->smsMessageLength->truncateBetween( $titlePrefix, $title, "\n".$startsAt, ); $coreLines = [$titlePrefix.$title, $startsAt]; $header = ''; $footer = ''; $configuredHeader = trim($this->configuredText('relais_sms.caldav.reminder_message_header')); if ($configuredHeader !== '' && $this->smsMessageLength->fitsInOneSms($this->assembleMessage($coreLines, $configuredHeader))) { $header = $configuredHeader; } $configuredFooter = trim($this->configuredText('relais_sms.caldav.reminder_message_footer')); if ($configuredFooter !== '' && $this->smsMessageLength->fitsInOneSms($this->assembleMessage($coreLines, $header, $configuredFooter))) { $footer = $configuredFooter; } $location = trim(preg_replace('/\s+/', ' ', (string) $event->location) ?? ''); if ($location !== '' && $this->fitsWithOptionalParts($coreLines, [$location], $header, $footer)) { $coreLines[] = $location; } $description = $this->description((string) $event->description); if ($description !== '') { $prefix = collect([$header, ...$coreLines]) ->filter(fn (string $part): bool => $part !== '') ->implode("\n")."\n"; $suffix = $footer === '' ? '' : "\n".$footer; $description = $this->smsMessageLength->truncateBetween($prefix, $description, $suffix); if ($description !== '') { $coreLines[] = $description; } } return $this->assembleMessage($coreLines, $header, $footer); } /** * @param array $coreLines * @param array $optionalLines */ private function fitsWithOptionalParts(array $coreLines, array $optionalLines, string $header, string $footer): bool { return $this->smsMessageLength->fitsInOneSms($this->assembleMessage([...$coreLines, ...$optionalLines], $header, $footer)); } /** @param array $coreLines */ private function assembleMessage(array $coreLines, string $header = '', string $footer = ''): string { return collect([ $header, ...$coreLines, $footer, ])->map(fn (string $part): string => trim($part)) ->filter() ->implode("\n"); } private function description(string $description): string { $description = trim(preg_replace('/\s+/', ' ', $description) ?? ''); if ($description === '') { return ''; } $maxLength = max(1, (int) config('relais_sms.caldav.reminder_description_max_length', 120)); return Str::limit($description, $maxLength, '[...]'); } private function configuredText(string $key): string { return str_replace('\n', "\n", (string) config($key, '')); } }