Publication initiale de RelaisSMS : documentation et sources nettoyees
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user