From d6e04d53fae1adc9cc9c0852abcf95323aee2766 Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Thu, 10 Sep 2026 00:52:18 +0330 Subject: [PATCH] feat(core): integrate Kavenegar SMS notifications and audit logging Implement a comprehensive notification system using Kavenegar SMS gateway and enhance system traceability through audit logging and detailed shipment status history. - SMS Integration: - Add Kavenegar SMS service with configurable API keys and sender numbers via system settings. - Implement automated SMS notifications for shipment approval, rejection, successful payments, and tracking updates. - Add administrative UI in Filament to manage SMS gateway settings and toggle specific notification types. - Audit & Tracking: - Apply `Auditable` trait to core models (User, Shipment, Wallet, etc.) to track changes. - Refactor `ShipmentStatusHistory` to include status transitions (`from_status` to `to_status`) and specific reasons for changes. - Implement `ShipmentObserver` to automate notification triggers on status changes. - Database & Config: - Add migrations for enhanced shipment status history tracking. - Update `.env.example` and `config/ifnex.php` with Kavenegar configuration parameters. --- 04_Laravel/.env.example | 7 +++ .../app/Filament/Pages/IfnexSettingsPage.php | 39 ++++++++++++++++ .../Filament/Resources/ShipmentResource.php | 6 --- .../Api/CustomerFinancialController.php | 4 +- .../Controllers/Api/PaymentController.php | 7 +++ .../Controllers/Api/StaffOrderController.php | 12 ++--- 04_Laravel/app/Models/CommitmentForm.php | 3 +- 04_Laravel/app/Models/Shipment.php | 4 ++ 04_Laravel/app/Models/ShipmentItem.php | 2 + 04_Laravel/app/Models/ShipmentPackage.php | 2 + .../app/Models/ShipmentStatusHistory.php | 2 +- 04_Laravel/app/Models/User.php | 3 +- 04_Laravel/app/Models/Wallet.php | 3 +- 04_Laravel/app/Models/WalletTransaction.php | 3 +- .../app/Notifications/Channels/SmsChannel.php | 23 ++++++++++ .../app/Notifications/PaymentSuccessSms.php | 34 ++++++++++++++ .../app/Notifications/ShipmentApprovedSms.php | 34 ++++++++++++++ .../app/Notifications/ShipmentRejectedSms.php | 34 ++++++++++++++ .../app/Notifications/TrackingUpdatedSms.php | 35 +++++++++++++++ 04_Laravel/app/Observers/ShipmentObserver.php | 45 +++++++++++++++++++ .../app/Providers/AppServiceProvider.php | 5 +++ .../app/Services/KavenegarSmsService.php | 37 +++++++-------- 04_Laravel/app/Services/TrackingService.php | 15 ++++++- 04_Laravel/config/ifnex.php | 14 ++++++ ...2_create_shipment_status_history_table.php | 30 +++++++++++++ ...eason_to_shipment_status_history_table.php | 22 +++++++++ .../filament/pages/ifnex-settings.blade.php | 44 ++++++++++++++++-- 27 files changed, 426 insertions(+), 43 deletions(-) create mode 100644 04_Laravel/app/Notifications/Channels/SmsChannel.php create mode 100644 04_Laravel/app/Notifications/PaymentSuccessSms.php create mode 100644 04_Laravel/app/Notifications/ShipmentApprovedSms.php create mode 100644 04_Laravel/app/Notifications/ShipmentRejectedSms.php create mode 100644 04_Laravel/app/Notifications/TrackingUpdatedSms.php create mode 100644 04_Laravel/app/Observers/ShipmentObserver.php create mode 100644 04_Laravel/database/migrations/2026_09_09_000002_create_shipment_status_history_table.php create mode 100644 04_Laravel/database/migrations/2026_09_09_000003_add_reason_to_shipment_status_history_table.php diff --git a/04_Laravel/.env.example b/04_Laravel/.env.example index 53ace8d..760e8ce 100644 --- a/04_Laravel/.env.example +++ b/04_Laravel/.env.example @@ -78,6 +78,13 @@ ZARINPAL_FRONTEND_SUCCESS_URL=http://localhost:8080/payment/success ZARINPAL_FRONTEND_FAILURE_URL=http://localhost:8080/payment/failed ZARINPAL_EXPIRY_MINUTES=30 +# Kavenegar SMS Gateway +KAVENEGAR_API_KEY= +KAVENEGAR_SENDER=10008566 +KAVENEGAR_SEND_SHIPMENT_APPROVED=true +KAVENEGAR_SEND_PAYMENT_SUCCESS=true +KAVENEGAR_SEND_TRACKING_UPDATE=false + # Wallet Settings WALLET_MIN_DEPOSIT=10000 WALLET_MAX_DEPOSIT=500000000 diff --git a/04_Laravel/app/Filament/Pages/IfnexSettingsPage.php b/04_Laravel/app/Filament/Pages/IfnexSettingsPage.php index aaa1ebc..56c7f18 100644 --- a/04_Laravel/app/Filament/Pages/IfnexSettingsPage.php +++ b/04_Laravel/app/Filament/Pages/IfnexSettingsPage.php @@ -22,6 +22,13 @@ class IfnexSettingsPage extends Page public float $cny_to_irr = 0; public float $eur_to_irr = 0; + public string $kavenegar_api_key = ''; + public string $kavenegar_sender = '10008566'; + public bool $kavenegar_send_shipment_approved = true; + public bool $kavenegar_send_shipment_rejected = true; + public bool $kavenegar_send_payment_success = true; + public bool $kavenegar_send_tracking_update = false; + public function mount(): void { $settings = SystemSetting::all()->pluck('value', 'key')->toArray(); @@ -33,6 +40,13 @@ class IfnexSettingsPage extends Page if (isset($settings['usd_to_irr'])) $this->usd_to_irr = (float) $settings['usd_to_irr']; if (isset($settings['cny_to_irr'])) $this->cny_to_irr = (float) $settings['cny_to_irr']; if (isset($settings['eur_to_irr'])) $this->eur_to_irr = (float) $settings['eur_to_irr']; + + if (isset($settings['kavenegar_api_key'])) $this->kavenegar_api_key = (string) $settings['kavenegar_api_key']; + if (isset($settings['kavenegar_sender'])) $this->kavenegar_sender = (string) $settings['kavenegar_sender']; + if (isset($settings['kavenegar_send_shipment_approved'])) $this->kavenegar_send_shipment_approved = (bool) $settings['kavenegar_send_shipment_approved']; + if (isset($settings['kavenegar_send_shipment_rejected'])) $this->kavenegar_send_shipment_rejected = (bool) $settings['kavenegar_send_shipment_rejected']; + if (isset($settings['kavenegar_send_payment_success'])) $this->kavenegar_send_payment_success = (bool) $settings['kavenegar_send_payment_success']; + if (isset($settings['kavenegar_send_tracking_update'])) $this->kavenegar_send_tracking_update = (bool) $settings['kavenegar_send_tracking_update']; } public function save(): void @@ -68,6 +82,31 @@ class IfnexSettingsPage extends Page ['value' => $this->eur_to_irr, 'updated_by' => $userId] ); + SystemSetting::updateOrCreate( + ['key' => 'kavenegar_api_key'], + ['value' => $this->kavenegar_api_key, 'updated_by' => $userId] + ); + SystemSetting::updateOrCreate( + ['key' => 'kavenegar_sender'], + ['value' => $this->kavenegar_sender, 'updated_by' => $userId] + ); + SystemSetting::updateOrCreate( + ['key' => 'kavenegar_send_shipment_approved'], + ['value' => (int) $this->kavenegar_send_shipment_approved, 'updated_by' => $userId] + ); + SystemSetting::updateOrCreate( + ['key' => 'kavenegar_send_shipment_rejected'], + ['value' => (int) $this->kavenegar_send_shipment_rejected, 'updated_by' => $userId] + ); + SystemSetting::updateOrCreate( + ['key' => 'kavenegar_send_payment_success'], + ['value' => (int) $this->kavenegar_send_payment_success, 'updated_by' => $userId] + ); + SystemSetting::updateOrCreate( + ['key' => 'kavenegar_send_tracking_update'], + ['value' => (int) $this->kavenegar_send_tracking_update, 'updated_by' => $userId] + ); + \Filament\Notifications\Notification::make() ->title('Success') ->body('Settings saved successfully.') diff --git a/04_Laravel/app/Filament/Resources/ShipmentResource.php b/04_Laravel/app/Filament/Resources/ShipmentResource.php index 2ef26a4..f3c6c3c 100644 --- a/04_Laravel/app/Filament/Resources/ShipmentResource.php +++ b/04_Laravel/app/Filament/Resources/ShipmentResource.php @@ -589,12 +589,6 @@ class ShipmentResource extends Resource ]), ]); } - public static function afterSave(\Filament\Resources\Pages\Page $page, \Illuminate\Database\Eloquent\Model $record): void - { - if ($page instanceof \Filament\Resources\Pages\EditRecord) { - ShipmentUpdatedNotification::notify($record); - } - } public static function getRelations(): array { diff --git a/04_Laravel/app/Http/Controllers/Api/CustomerFinancialController.php b/04_Laravel/app/Http/Controllers/Api/CustomerFinancialController.php index ada7202..6ce93cc 100644 --- a/04_Laravel/app/Http/Controllers/Api/CustomerFinancialController.php +++ b/04_Laravel/app/Http/Controllers/Api/CustomerFinancialController.php @@ -72,7 +72,9 @@ class CustomerFinancialController extends Controller }); // دریافت تراکنش‌های اخیر - $recent_transactions = WalletTransaction::where('user_id', $customer->id) + $recent_transactions = WalletTransaction::whereHas('wallet', function ($q) use ($customer) { + $q->where('user_id', $customer->id); + }) ->orderBy('created_at', 'desc') ->take(10) ->get() diff --git a/04_Laravel/app/Http/Controllers/Api/PaymentController.php b/04_Laravel/app/Http/Controllers/Api/PaymentController.php index 0a9944e..f37b686 100644 --- a/04_Laravel/app/Http/Controllers/Api/PaymentController.php +++ b/04_Laravel/app/Http/Controllers/Api/PaymentController.php @@ -219,6 +219,13 @@ class PaymentController extends Controller $shipment = \App\Models\Shipment::find($metadata['shipment_id']); if ($shipment && $shipment->status === \App\Enums\ShipmentStatus::Approved) { $shipment->update(['status' => \App\Enums\ShipmentStatus::Processed]); + + if (SystemSetting::get('kavenegar_api_key') && (bool) SystemSetting::get('kavenegar_send_payment_success', true)) { + $shipment->user->notify(new \App\Notifications\PaymentSuccessSms( + $shipment->awb_no, + (int) $transaction->amount + )); + } } } diff --git a/04_Laravel/app/Http/Controllers/Api/StaffOrderController.php b/04_Laravel/app/Http/Controllers/Api/StaffOrderController.php index ade6976..a6f2c37 100644 --- a/04_Laravel/app/Http/Controllers/Api/StaffOrderController.php +++ b/04_Laravel/app/Http/Controllers/Api/StaffOrderController.php @@ -74,10 +74,10 @@ class StaffOrderController extends Controller // ثبت در تاریخچه تغییرات ShipmentStatusHistory::create([ 'shipment_id' => $shipment->id, - 'old_status' => $oldStatus->value, - 'new_status' => ShipmentStatus::Approved->value, + 'from_status' => $oldStatus->value, + 'to_status' => ShipmentStatus::Approved->value, + 'reason' => $validated['notes'] ?? 'تأیید توسط کارمند', 'changed_by' => $user->id, - 'notes' => $validated['notes'] ?? 'تأیید توسط کارمند', ]); }); @@ -126,10 +126,10 @@ class StaffOrderController extends Controller // ثبت در تاریخچه تغییرات ShipmentStatusHistory::create([ 'shipment_id' => $shipment->id, - 'old_status' => $oldStatus->value, - 'new_status' => ShipmentStatus::Cancelled->value, + 'from_status' => $oldStatus->value, + 'to_status' => ShipmentStatus::Cancelled->value, + 'reason' => 'رد شده: ' . $validated['reason'], 'changed_by' => $user->id, - 'notes' => 'رد شده: ' . $validated['reason'], ]); }); diff --git a/04_Laravel/app/Models/CommitmentForm.php b/04_Laravel/app/Models/CommitmentForm.php index 800223f..5e6e293 100644 --- a/04_Laravel/app/Models/CommitmentForm.php +++ b/04_Laravel/app/Models/CommitmentForm.php @@ -5,10 +5,11 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; +use App\Traits\Auditable; class CommitmentForm extends Model { - use SoftDeletes; + use SoftDeletes, Auditable; protected $fillable = [ 'title', diff --git a/04_Laravel/app/Models/Shipment.php b/04_Laravel/app/Models/Shipment.php index 4055bf8..b353f1c 100644 --- a/04_Laravel/app/Models/Shipment.php +++ b/04_Laravel/app/Models/Shipment.php @@ -5,9 +5,11 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use App\Traits\Auditable; class Shipment extends Model { + use Auditable; protected $fillable = [ 'awb_no', 'forwarder', @@ -35,6 +37,7 @@ class Shipment extends Model 'net_rial', 'invoice_total_usd', 'cod_amount', + 'declared_value', // Import Invoice Fields 'brand_fee', 'report_fee', @@ -65,6 +68,7 @@ class Shipment extends Model // Customs 'reason_for_export', 'content_description', + 'customer_notes', // User 'user_id', ]; diff --git a/04_Laravel/app/Models/ShipmentItem.php b/04_Laravel/app/Models/ShipmentItem.php index 5eb48c1..ac8e293 100644 --- a/04_Laravel/app/Models/ShipmentItem.php +++ b/04_Laravel/app/Models/ShipmentItem.php @@ -3,9 +3,11 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use App\Traits\Auditable; class ShipmentItem extends Model { + use Auditable; protected $fillable = [ 'shipment_id', 'row_number', diff --git a/04_Laravel/app/Models/ShipmentPackage.php b/04_Laravel/app/Models/ShipmentPackage.php index f2430cd..95ca59c 100644 --- a/04_Laravel/app/Models/ShipmentPackage.php +++ b/04_Laravel/app/Models/ShipmentPackage.php @@ -4,9 +4,11 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use App\Traits\Auditable; class ShipmentPackage extends Model { + use Auditable; protected $fillable = [ 'shipment_id', 'package_no', diff --git a/04_Laravel/app/Models/ShipmentStatusHistory.php b/04_Laravel/app/Models/ShipmentStatusHistory.php index 94d0ca0..c774593 100644 --- a/04_Laravel/app/Models/ShipmentStatusHistory.php +++ b/04_Laravel/app/Models/ShipmentStatusHistory.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; class ShipmentStatusHistory extends Model { protected $fillable = [ - 'shipment_id', 'from_status', 'to_status', 'reason', 'changed_by', + 'shipment_id', 'from_status', 'to_status', 'reason', 'notes', 'changed_by', ]; protected static function booted(): void diff --git a/04_Laravel/app/Models/User.php b/04_Laravel/app/Models/User.php index f5b25a2..66f7be3 100644 --- a/04_Laravel/app/Models/User.php +++ b/04_Laravel/app/Models/User.php @@ -8,10 +8,11 @@ use Illuminate\Notifications\DatabaseNotification; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; use Spatie\Permission\Traits\HasRoles; +use App\Traits\Auditable; class User extends Authenticatable { - use HasApiTokens, HasFactory, Notifiable, HasRoles; + use HasApiTokens, HasFactory, Notifiable, HasRoles, Auditable; protected $fillable = [ 'name', diff --git a/04_Laravel/app/Models/Wallet.php b/04_Laravel/app/Models/Wallet.php index e1de601..6e61fd1 100644 --- a/04_Laravel/app/Models/Wallet.php +++ b/04_Laravel/app/Models/Wallet.php @@ -6,10 +6,11 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use App\Traits\Auditable; class Wallet extends Model { - use SoftDeletes; + use SoftDeletes, Auditable; protected $fillable = [ 'user_id', diff --git a/04_Laravel/app/Models/WalletTransaction.php b/04_Laravel/app/Models/WalletTransaction.php index fd4b11f..b34ff4a 100644 --- a/04_Laravel/app/Models/WalletTransaction.php +++ b/04_Laravel/app/Models/WalletTransaction.php @@ -9,10 +9,11 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; use App\Enums\TransactionType; use App\Enums\TransactionStatus; use App\Enums\PaymentGateway; +use App\Traits\Auditable; class WalletTransaction extends Model { - use SoftDeletes; + use SoftDeletes, Auditable; protected $fillable = [ 'wallet_id', diff --git a/04_Laravel/app/Notifications/Channels/SmsChannel.php b/04_Laravel/app/Notifications/Channels/SmsChannel.php new file mode 100644 index 0000000..67c665b --- /dev/null +++ b/04_Laravel/app/Notifications/Channels/SmsChannel.php @@ -0,0 +1,23 @@ +phone; + $message = $notification->toSms($notifiable); + + if (empty($phone) || empty($message)) { + return; + } + + $this->smsService->send($phone, $message); + } +} diff --git a/04_Laravel/app/Notifications/PaymentSuccessSms.php b/04_Laravel/app/Notifications/PaymentSuccessSms.php new file mode 100644 index 0000000..3953512 --- /dev/null +++ b/04_Laravel/app/Notifications/PaymentSuccessSms.php @@ -0,0 +1,34 @@ +awbNo}\nمبلغ: " . number_format($this->amount) . " ریال"; + } + + public function toArray(object $notifiable): array + { + return [ + 'title' => 'پرداخت موفق', + 'body' => "پرداخت سفارش {$this->awbNo} با موفقیت انجام شد. مبلغ: " . number_format($this->amount) . " ریال", + 'awb_no' => $this->awbNo, + 'amount' => $this->amount, + ]; + } +} diff --git a/04_Laravel/app/Notifications/ShipmentApprovedSms.php b/04_Laravel/app/Notifications/ShipmentApprovedSms.php new file mode 100644 index 0000000..ea31543 --- /dev/null +++ b/04_Laravel/app/Notifications/ShipmentApprovedSms.php @@ -0,0 +1,34 @@ +awbNo}\nلینک پیگیری: {$this->trackingUrl}"; + } + + public function toArray(object $notifiable): array + { + return [ + 'title' => 'تأیید سفارش', + 'body' => "سفارش {$this->awbNo} تأیید شد.", + 'awb_no' => $this->awbNo, + 'tracking_url' => $this->trackingUrl, + ]; + } +} diff --git a/04_Laravel/app/Notifications/ShipmentRejectedSms.php b/04_Laravel/app/Notifications/ShipmentRejectedSms.php new file mode 100644 index 0000000..1e6ccf9 --- /dev/null +++ b/04_Laravel/app/Notifications/ShipmentRejectedSms.php @@ -0,0 +1,34 @@ +awbNo}\nدلیل: {$this->reason}"; + } + + public function toArray(object $notifiable): array + { + return [ + 'title' => 'رد سفارش', + 'body' => "سفارش {$this->awbNo} رد شد. دلیل: {$this->reason}", + 'awb_no' => $this->awbNo, + 'reason' => $this->reason, + ]; + } +} diff --git a/04_Laravel/app/Notifications/TrackingUpdatedSms.php b/04_Laravel/app/Notifications/TrackingUpdatedSms.php new file mode 100644 index 0000000..057a241 --- /dev/null +++ b/04_Laravel/app/Notifications/TrackingUpdatedSms.php @@ -0,0 +1,35 @@ +awbNo}\nوضعیت: {$this->status}\nمحل: {$this->location}"; + } + + public function toArray(object $notifiable): array + { + return [ + 'title' => 'به‌روزرسانی رهگیری', + 'body' => "وضعیت سفارش {$this->awbNo} به {$this->status} تغییر کرد.", + 'awb_no' => $this->awbNo, + 'status' => $this->status, + 'location' => $this->location, + ]; + } +} diff --git a/04_Laravel/app/Observers/ShipmentObserver.php b/04_Laravel/app/Observers/ShipmentObserver.php new file mode 100644 index 0000000..c8a2f62 --- /dev/null +++ b/04_Laravel/app/Observers/ShipmentObserver.php @@ -0,0 +1,45 @@ +getChanges(); + $ignored = ['updated_at']; + + $changedFields = array_filter( + array_keys($changes), + fn ($key) => !in_array($key, $ignored) + ); + + if (empty($changedFields)) { + return; + } + + ShipmentUpdatedNotification::notify($shipment); + + $oldStatus = $shipment->getOriginal('status'); + $newStatus = $shipment->status; + + if ($oldStatus !== $newStatus) { + $trackingUrl = url("/track/{$shipment->awb_no}"); + $smsEnabled = !empty(SystemSetting::get('kavenegar_api_key')); + + if ($smsEnabled && $newStatus === \App\Enums\ShipmentStatus::Approved && (bool) SystemSetting::get('kavenegar_send_shipment_approved', true)) { + $shipment->user->notify(new ShipmentApprovedSms($shipment->awb_no, $trackingUrl)); + } + + if ($smsEnabled && $newStatus === \App\Enums\ShipmentStatus::Cancelled && (bool) SystemSetting::get('kavenegar_send_shipment_rejected', true)) { + $shipment->user->notify(new ShipmentRejectedSms($shipment->awb_no, 'سفارش شما رد شد.')); + } + } + } +} diff --git a/04_Laravel/app/Providers/AppServiceProvider.php b/04_Laravel/app/Providers/AppServiceProvider.php index 11bf79f..04bb716 100644 --- a/04_Laravel/app/Providers/AppServiceProvider.php +++ b/04_Laravel/app/Providers/AppServiceProvider.php @@ -3,6 +3,8 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use App\Models\Shipment; +use App\Observers\ShipmentObserver; class AppServiceProvider extends ServiceProvider { @@ -21,5 +23,8 @@ class AppServiceProvider extends ServiceProvider { // هک برای رفع مشکل دیسک اکسل در لاراول ۱۱/۱۲ config(['excel.temporary_files.disk' => 'local']); + + // ثبت Observer برای اطلاعیه‌های تغییر سفارش + Shipment::observe(ShipmentObserver::class); } } diff --git a/04_Laravel/app/Services/KavenegarSmsService.php b/04_Laravel/app/Services/KavenegarSmsService.php index 8f9c885..6a154e0 100644 --- a/04_Laravel/app/Services/KavenegarSmsService.php +++ b/04_Laravel/app/Services/KavenegarSmsService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Models\SystemSetting; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; @@ -13,19 +14,20 @@ class KavenegarSmsService public function __construct() { - $this->apiKey = config('services.kavenegar.api_key', ''); - $this->sender = config('services.kavenegar.sender', ''); + $this->apiKey = (string) SystemSetting::get('kavenegar_api_key', config('ifnex.kavenegar.api_key', '')); + $this->sender = (string) SystemSetting::get('kavenegar_sender', config('ifnex.kavenegar.sender', '10008566')); + } + + public function isEnabled(): bool + { + return !empty($this->apiKey); } - /** - * ارسال کد تأیید به شماره موبایل - */ public function sendVerificationCode(string $phone): array { $code = Str::random(5, '0123456789'); $cacheKey = "sms_verify_{$phone}"; - // ذخیره کد در cache به مدت ۱۰ دقیقه Cache::put($cacheKey, $code, now()->addMinutes(10)); $message = "کد تأیید IFNEX: {$code}"; @@ -33,9 +35,6 @@ class KavenegarSmsService return $this->send($phone, $message); } - /** - * بررسی کد تأیید - */ public function verifyCode(string $phone, string $code): bool { $cacheKey = "sms_verify_{$phone}"; @@ -49,12 +48,9 @@ class KavenegarSmsService return false; } - /** - * ارسال پیامک - */ public function send(string $receptor, string $message): array { - if (empty($this->apiKey)) { + if (!$this->isEnabled()) { return [ 'success' => false, 'message' => 'API Key Kavenegar تنظیم نشده است', @@ -91,12 +87,9 @@ class KavenegarSmsService } } - /** - * ارسال پیامک گروهی - */ public function sendBulk(array $receptors, string $message): array { - if (empty($this->apiKey)) { + if (!$this->isEnabled()) { return [ 'success' => false, 'message' => 'API Key Kavenegar تنظیم نشده است', @@ -133,11 +126,15 @@ class KavenegarSmsService } } - /** - * بررسی وضعیت پیامک ارسال شده - */ public function checkStatus(string $messageId): array { + if (!$this->isEnabled()) { + return [ + 'success' => false, + 'message' => 'API Key Kavenegar تنظیم نشده است', + ]; + } + try { $response = Http::timeout(10) ->get("https://api.kavenegar.com/v1/{$this->apiKey}/select.json", [ diff --git a/04_Laravel/app/Services/TrackingService.php b/04_Laravel/app/Services/TrackingService.php index d82a950..2e94c00 100644 --- a/04_Laravel/app/Services/TrackingService.php +++ b/04_Laravel/app/Services/TrackingService.php @@ -19,10 +19,23 @@ class TrackingService public function addTrackingEvent(int $shipment_id, array $data): ShipmentTrackingEvent { - return ShipmentTrackingEvent::create(array_merge($data, [ + $event = ShipmentTrackingEvent::create(array_merge($data, [ 'shipment_id' => $shipment_id, 'source' => 'manual', ])); + + if (SystemSetting::get('kavenegar_api_key') && (bool) SystemSetting::get('kavenegar_send_tracking_update', false)) { + $shipment = Shipment::with('user')->find($shipment_id); + if ($shipment && $shipment->user && $shipment->user->phone) { + $shipment->user->notify(new \App\Notifications\TrackingUpdatedSms( + $shipment->awb_no, + $event->event_description ?? 'به‌روزرسانی', + $event->location ?? '' + )); + } + } + + return $event; } public function getShipmentWithTracking(string $awb_no) diff --git a/04_Laravel/config/ifnex.php b/04_Laravel/config/ifnex.php index 39a55ff..fd70474 100644 --- a/04_Laravel/config/ifnex.php +++ b/04_Laravel/config/ifnex.php @@ -143,6 +143,20 @@ return [ 'low_balance_threshold' => env('LOW_BALANCE_THRESHOLD', 500000), // ۵۰۰,۰۰۰ ریال ], + /* + |-------------------------------------------------------------------------- + | Kavenegar SMS Gateway (درگاه پیامک کاوه‌نگار) + |-------------------------------------------------------------------------- + */ + 'kavenegar' => [ + 'api_key' => env('KAVENEGAR_API_KEY', ''), + 'sender' => env('KAVENEGAR_SENDER', '10008566'), + 'send_shipment_approved' => env('KAVENEGAR_SEND_SHIPMENT_APPROVED', true), + 'send_shipment_rejected' => env('KAVENEGAR_SEND_SHIPMENT_REJECTED', true), + 'send_payment_success' => env('KAVENEGAR_SEND_PAYMENT_SUCCESS', true), + 'send_tracking_update' => env('KAVENEGAR_SEND_TRACKING_UPDATE', false), + ], + /* |-------------------------------------------------------------------------- | Audit & Logging (ثبت فعالیت‌ها و لاگ) diff --git a/04_Laravel/database/migrations/2026_09_09_000002_create_shipment_status_history_table.php b/04_Laravel/database/migrations/2026_09_09_000002_create_shipment_status_history_table.php new file mode 100644 index 0000000..5aa062a --- /dev/null +++ b/04_Laravel/database/migrations/2026_09_09_000002_create_shipment_status_history_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('shipment_id')->constrained()->cascadeOnDelete(); + $table->string('from_status')->nullable(); + $table->string('to_status'); + $table->unsignedBigInteger('changed_by')->nullable(); + $table->text('reason')->nullable(); + $table->timestamp('changed_at')->useCurrent(); + $table->timestamps(); + + $table->foreign('changed_by')->references('id')->on('users')->nullOnDelete(); + $table->index(['shipment_id', 'created_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('shipment_status_history'); + } +}; diff --git a/04_Laravel/database/migrations/2026_09_09_000003_add_reason_to_shipment_status_history_table.php b/04_Laravel/database/migrations/2026_09_09_000003_add_reason_to_shipment_status_history_table.php new file mode 100644 index 0000000..eba08c8 --- /dev/null +++ b/04_Laravel/database/migrations/2026_09_09_000003_add_reason_to_shipment_status_history_table.php @@ -0,0 +1,22 @@ +text('reason')->nullable()->after('to_status'); + }); + } + + public function down(): void + { + Schema::table('shipment_status_history', function (Blueprint $table) { + $table->dropColumn('reason'); + }); + } +}; diff --git a/04_Laravel/resources/views/filament/pages/ifnex-settings.blade.php b/04_Laravel/resources/views/filament/pages/ifnex-settings.blade.php index 73715eb..767fed7 100644 --- a/04_Laravel/resources/views/filament/pages/ifnex-settings.blade.php +++ b/04_Laravel/resources/views/filament/pages/ifnex-settings.blade.php @@ -1,6 +1,7 @@
+

تنظیمات مالی

@@ -39,12 +40,47 @@
+
-
- + +

تنظیمات SMS (کاوه‌نگار)

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +