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.
This commit is contained in:
Kazem Alghasi 2026-09-10 00:52:18 +03:30
parent 7a8ff49baa
commit d6e04d53fa
27 changed files with 426 additions and 43 deletions

View File

@ -78,6 +78,13 @@ ZARINPAL_FRONTEND_SUCCESS_URL=http://localhost:8080/payment/success
ZARINPAL_FRONTEND_FAILURE_URL=http://localhost:8080/payment/failed ZARINPAL_FRONTEND_FAILURE_URL=http://localhost:8080/payment/failed
ZARINPAL_EXPIRY_MINUTES=30 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 Settings
WALLET_MIN_DEPOSIT=10000 WALLET_MIN_DEPOSIT=10000
WALLET_MAX_DEPOSIT=500000000 WALLET_MAX_DEPOSIT=500000000

View File

@ -22,6 +22,13 @@ class IfnexSettingsPage extends Page
public float $cny_to_irr = 0; public float $cny_to_irr = 0;
public float $eur_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 public function mount(): void
{ {
$settings = SystemSetting::all()->pluck('value', 'key')->toArray(); $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['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['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['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 public function save(): void
@ -68,6 +82,31 @@ class IfnexSettingsPage extends Page
['value' => $this->eur_to_irr, 'updated_by' => $userId] ['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() \Filament\Notifications\Notification::make()
->title('Success') ->title('Success')
->body('Settings saved successfully.') ->body('Settings saved successfully.')

View File

@ -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 public static function getRelations(): array
{ {

View File

@ -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') ->orderBy('created_at', 'desc')
->take(10) ->take(10)
->get() ->get()

View File

@ -219,6 +219,13 @@ class PaymentController extends Controller
$shipment = \App\Models\Shipment::find($metadata['shipment_id']); $shipment = \App\Models\Shipment::find($metadata['shipment_id']);
if ($shipment && $shipment->status === \App\Enums\ShipmentStatus::Approved) { if ($shipment && $shipment->status === \App\Enums\ShipmentStatus::Approved) {
$shipment->update(['status' => \App\Enums\ShipmentStatus::Processed]); $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
));
}
} }
} }

View File

@ -74,10 +74,10 @@ class StaffOrderController extends Controller
// ثبت در تاریخچه تغییرات // ثبت در تاریخچه تغییرات
ShipmentStatusHistory::create([ ShipmentStatusHistory::create([
'shipment_id' => $shipment->id, 'shipment_id' => $shipment->id,
'old_status' => $oldStatus->value, 'from_status' => $oldStatus->value,
'new_status' => ShipmentStatus::Approved->value, 'to_status' => ShipmentStatus::Approved->value,
'reason' => $validated['notes'] ?? 'تأیید توسط کارمند',
'changed_by' => $user->id, 'changed_by' => $user->id,
'notes' => $validated['notes'] ?? 'تأیید توسط کارمند',
]); ]);
}); });
@ -126,10 +126,10 @@ class StaffOrderController extends Controller
// ثبت در تاریخچه تغییرات // ثبت در تاریخچه تغییرات
ShipmentStatusHistory::create([ ShipmentStatusHistory::create([
'shipment_id' => $shipment->id, 'shipment_id' => $shipment->id,
'old_status' => $oldStatus->value, 'from_status' => $oldStatus->value,
'new_status' => ShipmentStatus::Cancelled->value, 'to_status' => ShipmentStatus::Cancelled->value,
'reason' => 'رد شده: ' . $validated['reason'],
'changed_by' => $user->id, 'changed_by' => $user->id,
'notes' => 'رد شده: ' . $validated['reason'],
]); ]);
}); });

View File

@ -5,10 +5,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use App\Traits\Auditable;
class CommitmentForm extends Model class CommitmentForm extends Model
{ {
use SoftDeletes; use SoftDeletes, Auditable;
protected $fillable = [ protected $fillable = [
'title', 'title',

View File

@ -5,9 +5,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Traits\Auditable;
class Shipment extends Model class Shipment extends Model
{ {
use Auditable;
protected $fillable = [ protected $fillable = [
'awb_no', 'awb_no',
'forwarder', 'forwarder',
@ -35,6 +37,7 @@ class Shipment extends Model
'net_rial', 'net_rial',
'invoice_total_usd', 'invoice_total_usd',
'cod_amount', 'cod_amount',
'declared_value',
// Import Invoice Fields // Import Invoice Fields
'brand_fee', 'brand_fee',
'report_fee', 'report_fee',
@ -65,6 +68,7 @@ class Shipment extends Model
// Customs // Customs
'reason_for_export', 'reason_for_export',
'content_description', 'content_description',
'customer_notes',
// User // User
'user_id', 'user_id',
]; ];

View File

@ -3,9 +3,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Traits\Auditable;
class ShipmentItem extends Model class ShipmentItem extends Model
{ {
use Auditable;
protected $fillable = [ protected $fillable = [
'shipment_id', 'shipment_id',
'row_number', 'row_number',

View File

@ -4,9 +4,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Traits\Auditable;
class ShipmentPackage extends Model class ShipmentPackage extends Model
{ {
use Auditable;
protected $fillable = [ protected $fillable = [
'shipment_id', 'shipment_id',
'package_no', 'package_no',

View File

@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ShipmentStatusHistory extends Model class ShipmentStatusHistory extends Model
{ {
protected $fillable = [ 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 protected static function booted(): void

View File

@ -8,10 +8,11 @@ use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles; use Spatie\Permission\Traits\HasRoles;
use App\Traits\Auditable;
class User extends Authenticatable class User extends Authenticatable
{ {
use HasApiTokens, HasFactory, Notifiable, HasRoles; use HasApiTokens, HasFactory, Notifiable, HasRoles, Auditable;
protected $fillable = [ protected $fillable = [
'name', 'name',

View File

@ -6,10 +6,11 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Traits\Auditable;
class Wallet extends Model class Wallet extends Model
{ {
use SoftDeletes; use SoftDeletes, Auditable;
protected $fillable = [ protected $fillable = [
'user_id', 'user_id',

View File

@ -9,10 +9,11 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
use App\Enums\TransactionType; use App\Enums\TransactionType;
use App\Enums\TransactionStatus; use App\Enums\TransactionStatus;
use App\Enums\PaymentGateway; use App\Enums\PaymentGateway;
use App\Traits\Auditable;
class WalletTransaction extends Model class WalletTransaction extends Model
{ {
use SoftDeletes; use SoftDeletes, Auditable;
protected $fillable = [ protected $fillable = [
'wallet_id', 'wallet_id',

View File

@ -0,0 +1,23 @@
<?php
namespace App\Notifications\Channels;
use App\Services\KavenegarSmsService;
use Illuminate\Notifications\Notification;
class SmsChannel
{
public function __construct(private KavenegarSmsService $smsService) {}
public function send(object $notifiable, Notification $notification): void
{
$phone = $notifiable->phone;
$message = $notification->toSms($notifiable);
if (empty($phone) || empty($message)) {
return;
}
$this->smsService->send($phone, $message);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Notifications;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class PaymentSuccessSms extends Notification
{
use Queueable;
public function __construct(public string $awbNo, public int $amount) {}
public function via(object $notifiable): array
{
return ['database', 'sms'];
}
public function toSms(User $notifiable): ?string
{
return "پرداخت سفارش شما با موفقیت انجام شد.\nشماره پیگیری: {$this->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,
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Notifications;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class ShipmentApprovedSms extends Notification
{
use Queueable;
public function __construct(public string $awbNo, public string $trackingUrl) {}
public function via(object $notifiable): array
{
return ['database', 'sms'];
}
public function toSms(User $notifiable): ?string
{
return "سفارش شما با موفقیت تأیید شد.\nشماره پیگیری: {$this->awbNo}\nلینک پیگیری: {$this->trackingUrl}";
}
public function toArray(object $notifiable): array
{
return [
'title' => 'تأیید سفارش',
'body' => "سفارش {$this->awbNo} تأیید شد.",
'awb_no' => $this->awbNo,
'tracking_url' => $this->trackingUrl,
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Notifications;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class ShipmentRejectedSms extends Notification
{
use Queueable;
public function __construct(public string $awbNo, public string $reason) {}
public function via(object $notifiable): array
{
return ['database', 'sms'];
}
public function toSms(User $notifiable): ?string
{
return "سفارش شما رد شد.\nشماره پیگیری: {$this->awbNo}\nدلیل: {$this->reason}";
}
public function toArray(object $notifiable): array
{
return [
'title' => 'رد سفارش',
'body' => "سفارش {$this->awbNo} رد شد. دلیل: {$this->reason}",
'awb_no' => $this->awbNo,
'reason' => $this->reason,
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Notifications;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class TrackingUpdatedSms extends Notification
{
use Queueable;
public function __construct(public string $awbNo, public string $status, public string $location) {}
public function via(object $notifiable): array
{
return ['database', 'sms'];
}
public function toSms(User $notifiable): ?string
{
return "وضعیت سفارش شما تغییر کرد.\nشماره پیگیری: {$this->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,
];
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Observers;
use App\Models\Shipment;
use App\Notifications\ShipmentUpdatedNotification;
use App\Notifications\ShipmentApprovedSms;
use App\Notifications\ShipmentRejectedSms;
use Illuminate\Support\Facades\Config;
class ShipmentObserver
{
public function updated(Shipment $shipment): void
{
$changes = $shipment->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, 'سفارش شما رد شد.'));
}
}
}
}

View File

@ -3,6 +3,8 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use App\Models\Shipment;
use App\Observers\ShipmentObserver;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@ -21,5 +23,8 @@ class AppServiceProvider extends ServiceProvider
{ {
// هک برای رفع مشکل دیسک اکسل در لاراول ۱۱/۱۲ // هک برای رفع مشکل دیسک اکسل در لاراول ۱۱/۱۲
config(['excel.temporary_files.disk' => 'local']); config(['excel.temporary_files.disk' => 'local']);
// ثبت Observer برای اطلاعیه‌های تغییر سفارش
Shipment::observe(ShipmentObserver::class);
} }
} }

View File

@ -2,6 +2,7 @@
namespace App\Services; namespace App\Services;
use App\Models\SystemSetting;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -13,19 +14,20 @@ class KavenegarSmsService
public function __construct() public function __construct()
{ {
$this->apiKey = config('services.kavenegar.api_key', ''); $this->apiKey = (string) SystemSetting::get('kavenegar_api_key', config('ifnex.kavenegar.api_key', ''));
$this->sender = config('services.kavenegar.sender', ''); $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 public function sendVerificationCode(string $phone): array
{ {
$code = Str::random(5, '0123456789'); $code = Str::random(5, '0123456789');
$cacheKey = "sms_verify_{$phone}"; $cacheKey = "sms_verify_{$phone}";
// ذخیره کد در cache به مدت ۱۰ دقیقه
Cache::put($cacheKey, $code, now()->addMinutes(10)); Cache::put($cacheKey, $code, now()->addMinutes(10));
$message = "کد تأیید IFNEX: {$code}"; $message = "کد تأیید IFNEX: {$code}";
@ -33,9 +35,6 @@ class KavenegarSmsService
return $this->send($phone, $message); return $this->send($phone, $message);
} }
/**
* بررسی کد تأیید
*/
public function verifyCode(string $phone, string $code): bool public function verifyCode(string $phone, string $code): bool
{ {
$cacheKey = "sms_verify_{$phone}"; $cacheKey = "sms_verify_{$phone}";
@ -49,12 +48,9 @@ class KavenegarSmsService
return false; return false;
} }
/**
* ارسال پیامک
*/
public function send(string $receptor, string $message): array public function send(string $receptor, string $message): array
{ {
if (empty($this->apiKey)) { if (!$this->isEnabled()) {
return [ return [
'success' => false, 'success' => false,
'message' => 'API Key Kavenegar تنظیم نشده است', 'message' => 'API Key Kavenegar تنظیم نشده است',
@ -91,12 +87,9 @@ class KavenegarSmsService
} }
} }
/**
* ارسال پیامک گروهی
*/
public function sendBulk(array $receptors, string $message): array public function sendBulk(array $receptors, string $message): array
{ {
if (empty($this->apiKey)) { if (!$this->isEnabled()) {
return [ return [
'success' => false, 'success' => false,
'message' => 'API Key Kavenegar تنظیم نشده است', 'message' => 'API Key Kavenegar تنظیم نشده است',
@ -133,11 +126,15 @@ class KavenegarSmsService
} }
} }
/**
* بررسی وضعیت پیامک ارسال شده
*/
public function checkStatus(string $messageId): array public function checkStatus(string $messageId): array
{ {
if (!$this->isEnabled()) {
return [
'success' => false,
'message' => 'API Key Kavenegar تنظیم نشده است',
];
}
try { try {
$response = Http::timeout(10) $response = Http::timeout(10)
->get("https://api.kavenegar.com/v1/{$this->apiKey}/select.json", [ ->get("https://api.kavenegar.com/v1/{$this->apiKey}/select.json", [

View File

@ -19,10 +19,23 @@ class TrackingService
public function addTrackingEvent(int $shipment_id, array $data): ShipmentTrackingEvent 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, 'shipment_id' => $shipment_id,
'source' => 'manual', '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) public function getShipmentWithTracking(string $awb_no)

View File

@ -143,6 +143,20 @@ return [
'low_balance_threshold' => env('LOW_BALANCE_THRESHOLD', 500000), // ۵۰۰,۰۰۰ ریال '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 (ثبت فعالیت‌ها و لاگ) | Audit & Logging (ثبت فعالیت‌ها و لاگ)

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('shipment_status_history', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('shipment_status_history', function (Blueprint $table) {
$table->text('reason')->nullable()->after('to_status');
});
}
public function down(): void
{
Schema::table('shipment_status_history', function (Blueprint $table) {
$table->dropColumn('reason');
});
}
};

View File

@ -1,6 +1,7 @@
<x-filament::page> <x-filament::page>
<div class="space-y-6"> <div class="space-y-6">
<x-filament::card> <x-filament::card>
<h2 class="text-lg font-medium text-gray-900 mb-4">تنظیمات مالی</h2>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2"> <div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div> <div>
<label class="block text-sm font-medium text-gray-700">VAT Rate</label> <label class="block text-sm font-medium text-gray-700">VAT Rate</label>
@ -39,12 +40,47 @@
<input type="number" wire:model="eur_to_irr" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm"> <input type="number" wire:model="eur_to_irr" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
</div> </div>
</div> </div>
</x-filament::card>
<div class="mt-6"> <x-filament::card>
<button type="button" wire:click="save" class="filament-button filament-button-size-md inline-flex items-center justify-center py-2 px-4 rounded-md text-sm font-medium text-white bg-primary-600 hover:bg-primary-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"> <h2 class="text-lg font-medium text-gray-900 mb-4">تنظیمات SMS (کاوه‌نگار)</h2>
Save Settings <div class="grid grid-cols-1 gap-6 md:grid-cols-2">
</button> <div>
<label class="block text-sm font-medium text-gray-700">API Key</label>
<input type="text" wire:model="kavenegar_api_key" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Sender Number</label>
<input type="text" wire:model="kavenegar_sender" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
</div>
<div class="flex items-center gap-2">
<input type="checkbox" wire:model="kavenegar_send_shipment_approved" id="kavenegar_send_shipment_approved" class="rounded border-gray-300">
<label for="kavenegar_send_shipment_approved" class="text-sm font-medium text-gray-700">ارسال SMS هنگام تأیید سفارش</label>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" wire:model="kavenegar_send_shipment_rejected" id="kavenegar_send_shipment_rejected" class="rounded border-gray-300">
<label for="kavenegar_send_shipment_rejected" class="text-sm font-medium text-gray-700">ارسال SMS هنگام رد سفارش</label>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" wire:model="kavenegar_send_payment_success" id="kavenegar_send_payment_success" class="rounded border-gray-300">
<label for="kavenegar_send_payment_success" class="text-sm font-medium text-gray-700">ارسال SMS هنگام پرداخت موفق</label>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" wire:model="kavenegar_send_tracking_update" id="kavenegar_send_tracking_update" class="rounded border-gray-300">
<label for="kavenegar_send_tracking_update" class="text-sm font-medium text-gray-700">ارسال SMS هنگام به‌روزرسانی رهگیری</label>
</div>
</div> </div>
</x-filament::card> </x-filament::card>
<div class="mt-6">
<button type="button" wire:click="save" class="filament-button filament-button-size-md inline-flex items-center justify-center py-2 px-4 rounded-md text-sm font-medium text-white bg-primary-600 hover:bg-primary-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
Save Settings
</button>
</div>
</div> </div>
</x-filament::page> </x-filament::page>