Files
RelaisSMS/app/Services/Sms/SmsCampaignSender.php

261 lines
9.2 KiB
PHP

<?php
namespace App\Services\Sms;
use App\Contracts\Sms\SmsProviderInterface;
use App\Models\ContactGroup;
use App\Models\SmsCampaign;
use App\Models\SmsDelivery;
use App\Models\User;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class SmsCampaignSender
{
public function __construct(private readonly SmsProviderInterface $provider) {}
/**
* @param array<int, array<string, mixed>> $recipients
* @param array<int, int|string> $selectedContactIds
* @param array<int, int|string> $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<int, array<string, mixed>> $recipients
* @param array<int, int|string> $selectedContactIds
* @param array<int, int|string> $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<int, array<string, mixed>> $recipients
* @param array<int, int|string> $selectedContactIds
* @param array<int, int|string> $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<int, array<string, mixed>> $recipients
* @param array<int, int> $selectedContactIds
* @param array<int, int> $selectedGroupIds
* @param array<int, string> $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<int, array<string, mixed>> $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<int, array<string, mixed>>
*/
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<int, int|string> $ids
* @return array<int, int>
*/
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();
}
}