WIP
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Filament\Pages;
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
use App\Jobs\StartSmsCampaignJob;
|
|
||||||
use App\Models\Coupon;
|
use App\Models\Coupon;
|
||||||
use App\Services\SmsBroadcastService;
|
use App\Services\SmsBroadcastService;
|
||||||
use App\Services\SmsMessageAnalyzer;
|
use App\Services\SmsMessageAnalyzer;
|
||||||
@@ -156,9 +155,10 @@ class SendSms extends Page
|
|||||||
$excludedIds = array_map('intval', $data['excluded_coupon_ids'] ?? []);
|
$excludedIds = array_map('intval', $data['excluded_coupon_ids'] ?? []);
|
||||||
|
|
||||||
$service = app(SmsBroadcastService::class);
|
$service = app(SmsBroadcastService::class);
|
||||||
$willReceive = $service->resolveRecipients($sendToAll, $selectedIds, $excludedIds)
|
$recipients = $service->resolveRecipients($sendToAll, $selectedIds, $excludedIds)
|
||||||
->filter(fn (Coupon $coupon): bool => is_valid_coupon_phone($coupon->phone))
|
->filter(fn (Coupon $coupon): bool => is_valid_coupon_phone($coupon->phone));
|
||||||
->count();
|
|
||||||
|
$willReceive = $recipients->count();
|
||||||
|
|
||||||
if ($willReceive === 0) {
|
if ($willReceive === 0) {
|
||||||
Notification::make()
|
Notification::make()
|
||||||
@@ -170,19 +170,26 @@ class SendSms extends Page
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StartSmsCampaignJob::dispatch(
|
$messages = $recipients->map(fn (Coupon $coupon): array => [
|
||||||
adminId: auth()->id(),
|
'phone' => '+993'.$coupon->phone,
|
||||||
message: $data['message'],
|
'message' => $data['message'],
|
||||||
sendToAll: $sendToAll,
|
])->values()->all();
|
||||||
selectedIds: $selectedIds,
|
|
||||||
excludedIds: $excludedIds,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
$success = sendBulkSMS($messages);
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title(__('filament.send_sms.notifications.submitted'))
|
->title(__('filament.send_sms.notifications.submitted'))
|
||||||
->body(__('filament.send_sms.notifications.submitted_body', ['count' => $willReceive]))
|
->body(__('filament.send_sms.notifications.submitted_body', ['count' => $willReceive]))
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
|
} else {
|
||||||
|
Notification::make()
|
||||||
|
->title(__('filament.send_sms.notifications.failed'))
|
||||||
|
->body(__('filament.send_sms.notifications.failed_body'))
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function confirmationDescription(): string
|
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')) {
|
if (! function_exists('unmask_phone')) {
|
||||||
/**
|
/**
|
||||||
* Unmask Turkmenistan phone number from TM code +993 6X XX XX XX to 6xxxxxxx
|
* 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;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Enums\SmsSendStatus;
|
|
||||||
use App\Jobs\SendSmsToCouponJob;
|
|
||||||
use App\Models\Coupon;
|
use App\Models\Coupon;
|
||||||
use App\Models\SmsCampaign;
|
|
||||||
use App\Models\SmsSendLog;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Support\Facades\Bus;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class SmsBroadcastService
|
class SmsBroadcastService
|
||||||
{
|
{
|
||||||
@@ -36,62 +29,4 @@ class SmsBroadcastService
|
|||||||
|
|
||||||
return $query->orderBy('id')->get();
|
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' => [
|
'notifications' => [
|
||||||
'no_valid_recipients' => 'Dogry alyjy ýok',
|
'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.',
|
'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' => 'SMS kampaniýasy iberildi',
|
||||||
'submitted_body' => 'Arka fonda :count alyja iberilýär.',
|
'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',
|
'confirmation' => ':count alyja ibermeli? :summary',
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user