261 lines
9.2 KiB
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();
|
|
}
|
|
}
|