This commit is contained in:
Mekan1206
2026-06-04 20:10:29 +05:00
parent 21feb3baa6
commit 3f646ea951
6 changed files with 78 additions and 202 deletions

View File

@@ -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();
$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

View File

@@ -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

View File

@@ -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()]);
}
}
}

View File

@@ -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,
);
}
}

View File

@@ -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(),
]);
}
}

View File

@@ -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',
],