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;
use App\Models\OtpCode;
use Illuminate\Support\Facades\Http;
use App\Services\Sms\IranPayamakService;
class OtpService
{
protected IranPayamakService $iranPayamakService;
public function __construct(IranPayamakService $iranPayamakService)
{
$this->iranPayamakService = $iranPayamakService;
}
public function generateCode(): string
{
return (string) random_int(1000, 9999);
@@ -45,51 +52,14 @@ class OtpService
->exists();
}
private function sendSms(string $phone, string $code): array
private function sendSms(string $phone, string $code): void
{
try {
$formattedRecipient = $this->formatPhoneForSms($phone);
$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.verify_code'),
'attributes' => [
'otp' => $code,
'status' => 'بازنشانی رمز عبور'
],
'recipient' => $formattedRecipient,
'line_number' => config('services.iranpayamak.line_number'),
'number_format' => 'english',
$this->iranPayamakService->sendPattern($phone, 'verify_code', [
'otp' => $code,
'status' => 'بازنشانی رمز عبور',
]);
if ($response->successful()) {
return $response->json();
} else {
return [
'status' => 'error',
'message' => 'SMS sending failed: ' . $response->status(),
'data' => $response->body()
];
}
} 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\Services\FileStorageService;
use App\Services\Shift\ShiftScheduleService;
use App\Services\Sms\SmsNotificationService;
use App\Services\Wallet\WalletService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
@@ -27,11 +28,14 @@ class CaseService
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->walletService = $walletService;
$this->shiftScheduleService = $shiftScheduleService;
$this->smsNotificationService = $smsNotificationService;
}
public function pendingForUser(User $user): Builder
@@ -95,6 +99,8 @@ class CaseService
'finalized_by' => $doctor->id,
]);
$this->smsNotificationService->reportReady($document);
return $report;
});
}
+7 -1
View File
@@ -12,6 +12,7 @@ use App\Models\User;
use App\Services\FileStorageService;
use App\Services\Invoice\InvoiceService;
use App\Services\Shift\ShiftScheduleService;
use App\Services\Sms\SmsNotificationService;
use Illuminate\Support\Facades\DB;
class FastRequestService
@@ -22,11 +23,14 @@ class FastRequestService
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->shiftScheduleService = $shiftScheduleService;
$this->invoiceService = $invoiceService;
$this->smsNotificationService = $smsNotificationService;
}
public function create(User $requester, array $data, array $historyFiles = [], array $scanFiles = []): Document
@@ -58,6 +62,8 @@ class FastRequestService
]);
}
$this->smsNotificationService->requestRegistered($document);
return $document;
});
}
+10
View File
@@ -8,9 +8,17 @@ use App\Enums\InvoicePaymentMethodEnum;
use App\Enums\InvoiceStatusEnum;
use App\Models\Document;
use App\Models\Invoice;
use App\Services\Sms\SmsNotificationService;
class InvoiceService
{
protected SmsNotificationService $smsNotificationService;
public function __construct(SmsNotificationService $smsNotificationService)
{
$this->smsNotificationService = $smsNotificationService;
}
public function createForDocument(Document $document): Invoice
{
$invoice = Invoice::create([
@@ -43,6 +51,8 @@ class InvoiceService
]);
}
$this->smsNotificationService->paymentConfirmed($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' => [
'api_url' => env('IRANPAYAMAK_SMS_API_URL', 'https://api.iranpayamak.com/ws/v1/sms/pattern'),
'line_number' => env('IRANPAYAMAK_SMS_API_URL', '50002178584000'),
'api_key' => env('IRANPAYAMAK_SMS_API_URL', 'jnCyVq4ein7uGsgIGjHsVRrWz0112IA2v2qbZM5q0kHtEXubsh'),
'line_number' => env('IRANPAYAMAK_SMS_LINE_NUMBER', '50002178584000'),
'api_key' => env('IRANPAYAMAK_SMS_API_KEY', 'jnCyVq4ein7uGsgIGjHsVRrWz0112IA2v2qbZM5q0kHtEXubsh'),
'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)"
/>
</div>
<span v-else class="text-xs text-gray-400">-</span>
</template>
</TableGeneric>
</div>