feat: implement SMS notification service and integrate with existing services

This commit is contained in:
2026-09-21 02:47:12 +03:30
parent 51b13631a3
commit 1e599b917b
9 changed files with 153 additions and 48 deletions
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Jobs;
use App\Services\Sms\IranPayamakService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class SendPatternSms implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public array $backoff = [60, 300, 900];
public function __construct(
protected string $phone,
protected string $patternKey,
protected array $attributes
) {
$this->afterCommit();
}
public function handle(IranPayamakService $iranPayamakService): void
{
$iranPayamakService->sendPattern($this->phone, $this->patternKey, $this->attributes);
}
}
+12 -42
View File
@@ -3,10 +3,17 @@
namespace App\Services\Auth; namespace App\Services\Auth;
use App\Models\OtpCode; use App\Models\OtpCode;
use Illuminate\Support\Facades\Http; use App\Services\Sms\IranPayamakService;
class OtpService class OtpService
{ {
protected IranPayamakService $iranPayamakService;
public function __construct(IranPayamakService $iranPayamakService)
{
$this->iranPayamakService = $iranPayamakService;
}
public function generateCode(): string public function generateCode(): string
{ {
return (string) random_int(1000, 9999); return (string) random_int(1000, 9999);
@@ -45,51 +52,14 @@ class OtpService
->exists(); ->exists();
} }
private function sendSms(string $phone, string $code): array private function sendSms(string $phone, string $code): void
{ {
try { try {
$formattedRecipient = $this->formatPhoneForSms($phone); $this->iranPayamakService->sendPattern($phone, 'verify_code', [
'otp' => $code,
$response = Http::withHeaders([ 'status' => 'بازنشانی رمز عبور',
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Api-Key' => config('services.iranpayamak.api_key'),
])
->post(config('services.iranpayamak.api_url'), [
'code' => config('services.iranpayamak.patterns.verify_code'),
'attributes' => [
'otp' => $code,
'status' => 'بازنشانی رمز عبور'
],
'recipient' => $formattedRecipient,
'line_number' => config('services.iranpayamak.line_number'),
'number_format' => 'english',
]); ]);
if ($response->successful()) {
return $response->json();
} else {
return [
'status' => 'error',
'message' => 'SMS sending failed: ' . $response->status(),
'data' => $response->body()
];
}
} catch (\Exception $e) { } catch (\Exception $e) {
return [
'status' => 'error',
'message' => 'SMS sending exception: ' . $e->getMessage(),
'data' => null
];
} }
} }
private function formatPhoneForSms(string $phone): string
{
if (str_starts_with($phone, '+98')) {
return '0' . substr($phone, 3);
}
return $phone;
}
} }
+7 -1
View File
@@ -14,6 +14,7 @@ use App\Models\Report;
use App\Models\User; use App\Models\User;
use App\Services\FileStorageService; use App\Services\FileStorageService;
use App\Services\Shift\ShiftScheduleService; use App\Services\Shift\ShiftScheduleService;
use App\Services\Sms\SmsNotificationService;
use App\Services\Wallet\WalletService; use App\Services\Wallet\WalletService;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -27,11 +28,14 @@ class CaseService
protected ShiftScheduleService $shiftScheduleService; protected ShiftScheduleService $shiftScheduleService;
public function __construct(FileStorageService $fileStorageService, WalletService $walletService, ShiftScheduleService $shiftScheduleService) protected SmsNotificationService $smsNotificationService;
public function __construct(FileStorageService $fileStorageService, WalletService $walletService, ShiftScheduleService $shiftScheduleService, SmsNotificationService $smsNotificationService)
{ {
$this->fileStorageService = $fileStorageService; $this->fileStorageService = $fileStorageService;
$this->walletService = $walletService; $this->walletService = $walletService;
$this->shiftScheduleService = $shiftScheduleService; $this->shiftScheduleService = $shiftScheduleService;
$this->smsNotificationService = $smsNotificationService;
} }
public function pendingForUser(User $user): Builder public function pendingForUser(User $user): Builder
@@ -95,6 +99,8 @@ class CaseService
'finalized_by' => $doctor->id, 'finalized_by' => $doctor->id,
]); ]);
$this->smsNotificationService->reportReady($document);
return $report; return $report;
}); });
} }
+7 -1
View File
@@ -12,6 +12,7 @@ use App\Models\User;
use App\Services\FileStorageService; use App\Services\FileStorageService;
use App\Services\Invoice\InvoiceService; use App\Services\Invoice\InvoiceService;
use App\Services\Shift\ShiftScheduleService; use App\Services\Shift\ShiftScheduleService;
use App\Services\Sms\SmsNotificationService;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class FastRequestService class FastRequestService
@@ -22,11 +23,14 @@ class FastRequestService
protected InvoiceService $invoiceService; protected InvoiceService $invoiceService;
public function __construct(FileStorageService $fileStorageService, ShiftScheduleService $shiftScheduleService, InvoiceService $invoiceService) protected SmsNotificationService $smsNotificationService;
public function __construct(FileStorageService $fileStorageService, ShiftScheduleService $shiftScheduleService, InvoiceService $invoiceService, SmsNotificationService $smsNotificationService)
{ {
$this->fileStorageService = $fileStorageService; $this->fileStorageService = $fileStorageService;
$this->shiftScheduleService = $shiftScheduleService; $this->shiftScheduleService = $shiftScheduleService;
$this->invoiceService = $invoiceService; $this->invoiceService = $invoiceService;
$this->smsNotificationService = $smsNotificationService;
} }
public function create(User $requester, array $data, array $historyFiles = [], array $scanFiles = []): Document public function create(User $requester, array $data, array $historyFiles = [], array $scanFiles = []): Document
@@ -58,6 +62,8 @@ class FastRequestService
]); ]);
} }
$this->smsNotificationService->requestRegistered($document);
return $document; return $document;
}); });
} }
+10
View File
@@ -8,9 +8,17 @@ use App\Enums\InvoicePaymentMethodEnum;
use App\Enums\InvoiceStatusEnum; use App\Enums\InvoiceStatusEnum;
use App\Models\Document; use App\Models\Document;
use App\Models\Invoice; use App\Models\Invoice;
use App\Services\Sms\SmsNotificationService;
class InvoiceService class InvoiceService
{ {
protected SmsNotificationService $smsNotificationService;
public function __construct(SmsNotificationService $smsNotificationService)
{
$this->smsNotificationService = $smsNotificationService;
}
public function createForDocument(Document $document): Invoice public function createForDocument(Document $document): Invoice
{ {
$invoice = Invoice::create([ $invoice = Invoice::create([
@@ -43,6 +51,8 @@ class InvoiceService
]); ]);
} }
$this->smsNotificationService->paymentConfirmed($invoice);
return $invoice; return $invoice;
} }
} }
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Services\Sms;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class IranPayamakService
{
public function sendPattern(string $phone, string $patternKey, array $attributes): array
{
$response = Http::withHeaders([
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Api-Key' => config('services.iranpayamak.api_key'),
])->post(config('services.iranpayamak.api_url'), [
'code' => config("services.iranpayamak.patterns.{$patternKey}"),
'attributes' => $attributes,
'recipient' => $this->formatRecipient($phone),
'line_number' => config('services.iranpayamak.line_number'),
'number_format' => 'english',
]);
if (!$response->successful()) {
throw new RuntimeException('SMS sending failed: ' . $response->status() . ' ' . $response->body());
}
return $response->json() ?? [];
}
protected function formatRecipient(string $phone): string
{
if (str_starts_with($phone, '+98')) {
return '0' . substr($phone, 3);
}
return $phone;
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Services\Sms;
use App\Jobs\SendPatternSms;
use App\Models\Document;
use App\Models\Invoice;
use App\Models\User;
class SmsNotificationService
{
public function requestRegistered(Document $document): void
{
$this->dispatch($document->requester, 'request_registered', [
'code' => $document->code,
]);
}
public function paymentConfirmed(Invoice $invoice): void
{
$this->dispatch($invoice->user, 'payment_confirmed', [
'invoice' => $invoice->number,
'amount' => number_format($invoice->amount),
]);
}
public function reportReady(Document $document): void
{
$this->dispatch($document->requester, 'report_ready', [
'code' => $document->code,
]);
}
protected function dispatch(?User $user, string $patternKey, array $attributes): void
{
if (empty($user?->phone) || empty(config("services.iranpayamak.patterns.{$patternKey}"))) {
return;
}
SendPatternSms::dispatch($user->phone, $patternKey, $attributes);
}
}
+6 -4
View File
@@ -43,12 +43,14 @@ return [
'iranpayamak' => [ 'iranpayamak' => [
'api_url' => env('IRANPAYAMAK_SMS_API_URL', 'https://api.iranpayamak.com/ws/v1/sms/pattern'), 'api_url' => env('IRANPAYAMAK_SMS_API_URL', 'https://api.iranpayamak.com/ws/v1/sms/pattern'),
'line_number' => env('IRANPAYAMAK_SMS_API_URL', '50002178584000'), 'line_number' => env('IRANPAYAMAK_SMS_LINE_NUMBER', '50002178584000'),
'api_key' => env('IRANPAYAMAK_SMS_API_URL', 'jnCyVq4ein7uGsgIGjHsVRrWz0112IA2v2qbZM5q0kHtEXubsh'), 'api_key' => env('IRANPAYAMAK_SMS_API_KEY', 'jnCyVq4ein7uGsgIGjHsVRrWz0112IA2v2qbZM5q0kHtEXubsh'),
'patterns' => [ 'patterns' => [
'verify_code' => 'EvYDvfcUSk', 'verify_code' => env('IRANPAYAMAK_SMS_PATTERN_VERIFY_CODE', 'EvYDvfcUSk'),
'request_registered' => env('IRANPAYAMAK_SMS_PATTERN_REQUEST_REGISTERED' , 'GO2xzefAO8'),
'payment_confirmed' => env('IRANPAYAMAK_SMS_PATTERN_PAYMENT_CONFIRMED' , 'bl3k2UZDPe'),
'report_ready' => env('IRANPAYAMAK_SMS_PATTERN_REPORT_READY' , 'ImUrYHVWTx'),
], ],
], ],
@@ -222,6 +222,7 @@ const submitReject = () => {
@click="openRejectModal(data)" @click="openRejectModal(data)"
/> />
</div> </div>
<span v-else class="text-xs text-gray-400">-</span>
</template> </template>
</TableGeneric> </TableGeneric>
</div> </div>