> $recipients * @param array $selectedContactIds * @param array $selectedGroupIds */ public function send( User $user, string $message, array $recipients, array $selectedContactIds = [], array $selectedGroupIds = [], ): SmsCampaign { $campaign = $this->createCampaign($user, $message, $recipients, $selectedContactIds, $selectedGroupIds, 'sending'); return $this->sendExistingCampaign($campaign, $recipients); } /** * @param array> $recipients * @param array $selectedContactIds * @param array $selectedGroupIds */ public function schedule( User $user, string $message, array $recipients, array $selectedContactIds, array $selectedGroupIds, CarbonInterface $scheduledAt, ): SmsCampaign { return $this->createCampaign($user, $message, $recipients, $selectedContactIds, $selectedGroupIds, 'scheduled', $scheduledAt); } public function sendScheduled(SmsCampaign $campaign): ?SmsCampaign { $campaign = DB::transaction(function () use ($campaign): ?SmsCampaign { $fresh = SmsCampaign::query() ->whereKey($campaign->id) ->lockForUpdate() ->first(); if (! $fresh || $fresh->status !== 'scheduled') { return null; } $fresh->update(['status' => 'sending']); Log::info('SMS campaign status changed from scheduled to sending.', [ 'sms_campaign_id' => $fresh->id, 'scheduled_at' => $fresh->scheduled_at?->toIso8601String(), ]); return $fresh->fresh(['deliveries', 'user']); }); if ($campaign === null) { return null; } return $this->sendExistingCampaign($campaign, $this->recipientsFromDeliveries($campaign)); } /** * @param array> $recipients * @param array $selectedContactIds * @param array $selectedGroupIds */ private function createCampaign( User $user, string $message, array $recipients, array $selectedContactIds, array $selectedGroupIds, string $status, ?CarbonInterface $scheduledAt = null, ): SmsCampaign { $selectedContactIds = $this->cleanIds($selectedContactIds); $selectedGroupIds = $this->cleanIds($selectedGroupIds); $selectedGroupNames = ContactGroup::query() ->whereKey($selectedGroupIds) ->orderBy('name') ->pluck('name') ->values() ->all(); return $this->createResolvedCampaign( $user, $message, $recipients, $selectedContactIds, $selectedGroupIds, $selectedGroupNames, $status, $scheduledAt, ); } /** * @param array> $recipients * @param array $selectedContactIds * @param array $selectedGroupIds * @param array $selectedGroupNames */ private function createResolvedCampaign( User $user, string $message, array $recipients, array $selectedContactIds, array $selectedGroupIds, array $selectedGroupNames, string $status, ?CarbonInterface $scheduledAt, ): SmsCampaign { return DB::transaction(function () use ($user, $message, $recipients, $selectedContactIds, $selectedGroupIds, $selectedGroupNames, $status, $scheduledAt): SmsCampaign { $attributes = [ 'user_id' => $user->id, 'provider' => (string) config('relais_sms.default_settings.active_sms_provider', 'ovh'), 'status' => $status, 'message' => $message, 'recipients_count' => count($recipients), 'selected_contact_ids' => $selectedContactIds, 'selected_group_ids' => $selectedGroupIds, 'selected_group_names' => $selectedGroupNames, ]; if ($scheduledAt !== null) { $attributes['scheduled_at'] = $scheduledAt; } $campaign = SmsCampaign::query()->create($attributes); $campaign->update(['provider_tag' => 'relais-sms-campaign-'.$campaign->id]); foreach ($recipients as $recipient) { $campaign->deliveries()->create([ 'contact_id' => $recipient['contact_id'] ?? null, 'contact_phone_number_id' => $recipient['phone_number_id'] ?? null, 'contact_name' => (string) $recipient['contact_name'], 'number' => (string) $recipient['number'], 'status' => 'queued', ]); } return $campaign; }); } /** * @param array> $recipients */ private function sendExistingCampaign(SmsCampaign $campaign, array $recipients): SmsCampaign { try { $result = $this->provider->send($campaign, $recipients); } catch (SmsProviderException $exception) { $status = $exception->outcomeUnknown ? 'uncertain' : 'failed'; $message = $exception->outcomeUnknown ? 'OVH n a pas confirme la reponse. Les SMS peuvent avoir ete envoyes. Verification automatique au prochain passage du cron ; ne pas renvoyer cette campagne.' : $exception->getMessage(); DB::transaction(function () use ($campaign, $exception, $status, $message, $recipients): void { $campaign->deliveries()->update(['status' => $status, 'error_message' => $message]); $campaign->update([ 'status' => $status, 'failed_count' => $exception->outcomeUnknown ? 0 : count($recipients), 'response_payload' => $exception->responsePayload, 'error_message' => $message, ]); }); Log::warning('SMS provider request failed.', [ 'sms_campaign_id' => $campaign->id, 'outcome_unknown' => $exception->outcomeUnknown, 'reason' => $exception->getMessage(), ]); return $campaign->fresh(['deliveries', 'user']); } $failedNumbers = array_flip($result->failedNumbers); $sentCount = 0; $failedCount = 0; foreach ($campaign->deliveries()->orderBy('id')->get()->values() as $index => $delivery) { $failed = isset($failedNumbers[$delivery->number]); $delivery->update([ 'status' => $failed ? 'failed' : 'sent', 'provider_message_id' => $result->providerMessageIds[$index] ?? null, 'provider_response' => $result->responsePayload, 'error_message' => $failed ? 'Numero refuse par OVH.' : null, 'sent_at' => $failed ? null : now(), ]); $failed ? $failedCount++ : $sentCount++; } $campaign->update([ 'status' => $failedCount > 0 ? ($sentCount > 0 ? 'partial' : 'failed') : 'sent', 'sent_count' => $sentCount, 'failed_count' => $failedCount, 'provider_service_name' => $result->serviceName, 'provider_job_id' => $result->providerJobId, 'message' => (string) ($result->requestPayload['message'] ?? $campaign->message), 'request_payload' => $result->requestPayload, 'response_payload' => $result->responsePayload, 'sent_at' => $sentCount > 0 ? now() : null, ]); return $campaign->fresh(['deliveries', 'user']); } /** * @return array> */ private function recipientsFromDeliveries(SmsCampaign $campaign): array { return $campaign->deliveries() ->orderBy('id') ->get() ->map(fn (SmsDelivery $delivery): array => [ 'contact_id' => $delivery->contact_id, 'phone_number_id' => $delivery->contact_phone_number_id, 'contact_name' => $delivery->contact_name, 'number' => $delivery->number, ]) ->values() ->all(); } /** * @param array $ids * @return array */ private function cleanIds(array $ids): array { return collect($ids) ->map(fn (int|string $id): int => (int) $id) ->filter(fn (int $id): bool => $id > 0) ->unique() ->values() ->all(); } }