diff --git a/app/Filament/Pages/SendSms.php b/app/Filament/Pages/SendSms.php index 8753c3a..b4434c6 100644 --- a/app/Filament/Pages/SendSms.php +++ b/app/Filament/Pages/SendSms.php @@ -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 diff --git a/app/Helpers/helpers.php b/app/Helpers/helpers.php index 76e7e86..25efc44 100644 --- a/app/Helpers/helpers.php +++ b/app/Helpers/helpers.php @@ -56,6 +56,57 @@ if (! function_exists('sendSMS')) { } } +if (! function_exists('sendBulkSMS')) { + /** + * Send bulk SMS + * + * @param array $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 diff --git a/app/Jobs/SendSmsToCouponJob.php b/app/Jobs/SendSmsToCouponJob.php deleted file mode 100644 index 6819ace..0000000 --- a/app/Jobs/SendSmsToCouponJob.php +++ /dev/null @@ -1,81 +0,0 @@ -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()]); - } - } -} diff --git a/app/Jobs/StartSmsCampaignJob.php b/app/Jobs/StartSmsCampaignJob.php deleted file mode 100644 index 1603708..0000000 --- a/app/Jobs/StartSmsCampaignJob.php +++ /dev/null @@ -1,38 +0,0 @@ - $selectedIds - * @param array $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, - ); - } -} diff --git a/app/Services/SmsBroadcastService.php b/app/Services/SmsBroadcastService.php index b5f15d4..3e02960 100644 --- a/app/Services/SmsBroadcastService.php +++ b/app/Services/SmsBroadcastService.php @@ -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(), - ]); - } } diff --git a/lang/tk/filament.php b/lang/tk/filament.php index 9d54865..73bc5d8 100644 --- a/lang/tk/filament.php +++ b/lang/tk/filament.php @@ -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', ],