WIP
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Jobs\StartSmsCampaignJob;
|
||||
use App\Models\Coupon;
|
||||
use App\Services\SmsBroadcastService;
|
||||
use App\Services\SmsMessageAnalyzer;
|
||||
@@ -156,9 +155,10 @@ class SendSms extends Page
|
||||
$excludedIds = array_map('intval', $data['excluded_coupon_ids'] ?? []);
|
||||
|
||||
$service = app(SmsBroadcastService::class);
|
||||
$willReceive = $service->resolveRecipients($sendToAll, $selectedIds, $excludedIds)
|
||||
->filter(fn (Coupon $coupon): bool => is_valid_coupon_phone($coupon->phone))
|
||||
->count();
|
||||
$recipients = $service->resolveRecipients($sendToAll, $selectedIds, $excludedIds)
|
||||
->filter(fn (Coupon $coupon): bool => is_valid_coupon_phone($coupon->phone));
|
||||
|
||||
$willReceive = $recipients->count();
|
||||
|
||||
if ($willReceive === 0) {
|
||||
Notification::make()
|
||||
@@ -170,19 +170,26 @@ class SendSms extends Page
|
||||
return;
|
||||
}
|
||||
|
||||
StartSmsCampaignJob::dispatch(
|
||||
adminId: auth()->id(),
|
||||
message: $data['message'],
|
||||
sendToAll: $sendToAll,
|
||||
selectedIds: $selectedIds,
|
||||
excludedIds: $excludedIds,
|
||||
);
|
||||
$messages = $recipients->map(fn (Coupon $coupon): array => [
|
||||
'phone' => '+993'.$coupon->phone,
|
||||
'message' => $data['message'],
|
||||
])->values()->all();
|
||||
|
||||
Notification::make()
|
||||
->title(__('filament.send_sms.notifications.submitted'))
|
||||
->body(__('filament.send_sms.notifications.submitted_body', ['count' => $willReceive]))
|
||||
->success()
|
||||
->send();
|
||||
$success = sendBulkSMS($messages);
|
||||
|
||||
if ($success) {
|
||||
Notification::make()
|
||||
->title(__('filament.send_sms.notifications.submitted'))
|
||||
->body(__('filament.send_sms.notifications.submitted_body', ['count' => $willReceive]))
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
Notification::make()
|
||||
->title(__('filament.send_sms.notifications.failed'))
|
||||
->body(__('filament.send_sms.notifications.failed_body'))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
protected function confirmationDescription(): string
|
||||
|
||||
@@ -56,6 +56,57 @@ if (! function_exists('sendSMS')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('sendBulkSMS')) {
|
||||
/**
|
||||
* Send bulk SMS
|
||||
*
|
||||
* @param array<int, array{phone: string, message: string}> $messages
|
||||
*/
|
||||
function sendBulkSMS(array $messages): bool
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(config('services.sms.timeout'))
|
||||
->connectTimeout(config('services.sms.connect_timeout'))
|
||||
->retry(
|
||||
times: 3,
|
||||
sleepMilliseconds: 50,
|
||||
throw: false,
|
||||
when: function (Throwable $exception, PendingRequest $request): bool {
|
||||
if ($exception instanceof ConnectionException) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($exception instanceof RequestException) {
|
||||
return $exception->response->serverError();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
)
|
||||
->post(config('services.sms.url').'/bulk', [
|
||||
'messages' => $messages,
|
||||
]);
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::error('Bulk SMS API request failed', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Throwable $exception) {
|
||||
Log::error('Bulk SMS API exception', [
|
||||
'message' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('unmask_phone')) {
|
||||
/**
|
||||
* Unmask Turkmenistan phone number from TM code +993 6X XX XX XX to 6xxxxxxx
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Enums\SmsSendStatus;
|
||||
use App\Models\Coupon;
|
||||
use App\Models\SmsCampaign;
|
||||
use App\Models\SmsSendLog;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class SendSmsToCouponJob implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
public function __construct(
|
||||
public SmsCampaign $campaign,
|
||||
public Coupon $coupon,
|
||||
) {
|
||||
$this->onQueue('sms');
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$log = SmsSendLog::query()
|
||||
->where('sms_campaign_id', $this->campaign->id)
|
||||
->where('coupon_id', $this->coupon->id)
|
||||
->first();
|
||||
|
||||
if ($log === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! is_valid_coupon_phone($this->coupon->phone)) {
|
||||
$this->markSkipped($log);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$success = sendSMS($this->coupon->phone, $this->campaign->message);
|
||||
|
||||
$log->update([
|
||||
'status' => $success ? SmsSendStatus::Sent : SmsSendStatus::Failed,
|
||||
'error_message' => $success ? null : 'SMS API request failed',
|
||||
'attempted_at' => now(),
|
||||
]);
|
||||
|
||||
$this->campaign->increment($success ? 'sent_count' : 'failed_count');
|
||||
$this->markCampaignCompleteIfFinished();
|
||||
|
||||
usleep((int) config('services.sms.bulk_delay_ms', 300) * 1000);
|
||||
}
|
||||
|
||||
protected function markSkipped(SmsSendLog $log): void
|
||||
{
|
||||
if ($log->status === SmsSendStatus::SkippedInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
$log->update([
|
||||
'status' => SmsSendStatus::SkippedInvalid,
|
||||
'attempted_at' => now(),
|
||||
]);
|
||||
|
||||
$this->campaign->increment('skipped_count');
|
||||
$this->markCampaignCompleteIfFinished();
|
||||
}
|
||||
|
||||
protected function markCampaignCompleteIfFinished(): void
|
||||
{
|
||||
$this->campaign->refresh();
|
||||
|
||||
if ($this->campaign->processedCount() >= $this->campaign->recipient_count) {
|
||||
$this->campaign->update(['completed_at' => now()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\SmsBroadcastService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
class StartSmsCampaignJob implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* @param array<int, int|string> $selectedIds
|
||||
* @param array<int, int|string> $excludedIds
|
||||
*/
|
||||
public function __construct(
|
||||
public int $adminId,
|
||||
public string $message,
|
||||
public bool $sendToAll,
|
||||
public array $selectedIds = [],
|
||||
public array $excludedIds = [],
|
||||
) {}
|
||||
|
||||
public function handle(SmsBroadcastService $service): void
|
||||
{
|
||||
$admin = User::query()->findOrFail($this->adminId);
|
||||
|
||||
$service->startCampaign(
|
||||
admin: $admin,
|
||||
message: $this->message,
|
||||
sendToAll: $this->sendToAll,
|
||||
selectedIds: $this->selectedIds,
|
||||
excludedIds: $this->excludedIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,8 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\SmsSendStatus;
|
||||
use App\Jobs\SendSmsToCouponJob;
|
||||
use App\Models\Coupon;
|
||||
use App\Models\SmsCampaign;
|
||||
use App\Models\SmsSendLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SmsBroadcastService
|
||||
{
|
||||
@@ -36,62 +29,4 @@ class SmsBroadcastService
|
||||
|
||||
return $query->orderBy('id')->get();
|
||||
}
|
||||
|
||||
public function startCampaign(
|
||||
User $admin,
|
||||
string $message,
|
||||
bool $sendToAll,
|
||||
array $selectedIds = [],
|
||||
array $excludedIds = [],
|
||||
): SmsCampaign {
|
||||
$recipients = $this->resolveRecipients($sendToAll, $selectedIds, $excludedIds);
|
||||
$mode = $sendToAll ? 'all' : 'selected';
|
||||
|
||||
return DB::transaction(function () use ($admin, $message, $mode, $recipients): SmsCampaign {
|
||||
$campaign = SmsCampaign::query()->create([
|
||||
'message' => $message,
|
||||
'mode' => $mode,
|
||||
'recipient_count' => $recipients->count(),
|
||||
'created_by' => $admin->id,
|
||||
]);
|
||||
|
||||
$jobs = [];
|
||||
|
||||
foreach ($recipients as $coupon) {
|
||||
if (! is_valid_coupon_phone($coupon->phone)) {
|
||||
$this->createLog($campaign, $coupon, SmsSendStatus::SkippedInvalid);
|
||||
$campaign->increment('skipped_count');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->createLog($campaign, $coupon, SmsSendStatus::Pending);
|
||||
$jobs[] = new SendSmsToCouponJob($campaign, $coupon);
|
||||
}
|
||||
|
||||
if ($jobs === []) {
|
||||
$campaign->update(['completed_at' => now()]);
|
||||
|
||||
return $campaign->fresh();
|
||||
}
|
||||
|
||||
Bus::chain($jobs)
|
||||
->onQueue('sms')
|
||||
->dispatch();
|
||||
|
||||
return $campaign->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
protected function createLog(SmsCampaign $campaign, Coupon $coupon, SmsSendStatus $status): SmsSendLog
|
||||
{
|
||||
return SmsSendLog::query()->create([
|
||||
'sms_campaign_id' => $campaign->id,
|
||||
'coupon_id' => $coupon->id,
|
||||
'phone' => $coupon->phone,
|
||||
'message' => $campaign->message,
|
||||
'status' => $status,
|
||||
'attempted_at' => $status === SmsSendStatus::Pending ? null : now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,10 @@ return [
|
||||
'notifications' => [
|
||||
'no_valid_recipients' => 'Dogry alyjy ýok',
|
||||
'no_valid_recipients_body' => 'Saýlawy ýa-da aýyrmalary üýtgediň — dogry telefon belgili kupon eýesi bu habary almaz.',
|
||||
'submitted' => 'SMS kampaniýasy kabul edildi',
|
||||
'submitted_body' => 'Arka fonda :count alyja iberilýär.',
|
||||
'submitted' => 'SMS kampaniýasy iberildi',
|
||||
'submitted_body' => ':count alyja iberildi.',
|
||||
'failed' => 'SMS ibermek başa barmady',
|
||||
'failed_body' => 'Näsazlyk ýüze çykdy. Täzeden synanyşyň.',
|
||||
],
|
||||
'confirmation' => ':count alyja ibermeli? :summary',
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user