From f5917c34c2a4a38bc67927b8d6e58ef74d6fb6d9 Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Wed, 5 Aug 2026 19:16:09 +0330 Subject: [PATCH] feat(logic): integrate discount code application and wallet services Implement the core logic for applying discount codes during price calculation and introduce the WalletService. This includes updating the PriceCalculatorService to validate and apply discounts, incrementing usage counts, and adding a Filament resource for managing discount codes via the admin panel. Additionally, refactor TrackingDataImport to use OnEachRow for better memory management during large Excel imports. --- .../Resources/DiscountCodeResource.php | 64 ++++++++ .../Pages/CreateDiscountCode.php | 12 ++ .../Pages/EditDiscountCode.php | 19 +++ .../Pages/ListDiscountCodes.php | 19 +++ 04_Laravel/app/Imports/TrackingDataImport.php | 149 +++++++++--------- 04_Laravel/app/Models/DiscountCode.php | 10 +- .../app/Services/PriceCalculatorService.php | 126 ++++++++++++++- 04_Laravel/app/Services/WalletService.php | 53 +++++++ 8 files changed, 366 insertions(+), 86 deletions(-) create mode 100644 04_Laravel/app/Filament/Resources/DiscountCodeResource.php create mode 100644 04_Laravel/app/Filament/Resources/DiscountCodeResource/Pages/CreateDiscountCode.php create mode 100644 04_Laravel/app/Filament/Resources/DiscountCodeResource/Pages/EditDiscountCode.php create mode 100644 04_Laravel/app/Filament/Resources/DiscountCodeResource/Pages/ListDiscountCodes.php create mode 100644 04_Laravel/app/Services/WalletService.php diff --git a/04_Laravel/app/Filament/Resources/DiscountCodeResource.php b/04_Laravel/app/Filament/Resources/DiscountCodeResource.php new file mode 100644 index 0000000..4d3ff81 --- /dev/null +++ b/04_Laravel/app/Filament/Resources/DiscountCodeResource.php @@ -0,0 +1,64 @@ +schema([ + // + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + // + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListDiscountCodes::route('/'), + 'create' => Pages\CreateDiscountCode::route('/create'), + 'edit' => Pages\EditDiscountCode::route('/{record}/edit'), + ]; + } +} diff --git a/04_Laravel/app/Filament/Resources/DiscountCodeResource/Pages/CreateDiscountCode.php b/04_Laravel/app/Filament/Resources/DiscountCodeResource/Pages/CreateDiscountCode.php new file mode 100644 index 0000000..08258f0 --- /dev/null +++ b/04_Laravel/app/Filament/Resources/DiscountCodeResource/Pages/CreateDiscountCode.php @@ -0,0 +1,12 @@ + $rows->count()]); + $rowIndex = $row->getIndex(); + $rowArray = $row->toArray(); - if ($rows->count() === 0) { - Log::warning('No rows found'); - return; - } + try { + // استخراج و نرمال‌سازی داده‌ها از ردیف اکسل + $awbNo = $this->normalizeAwb($rowArray['awb'] ?? null); + $eventDate = $this->normalizeDate($rowArray['date'] ?? null); + $eventTime = $this->normalizeTime($rowArray['time'] ?? null); + $eventDescription = $this->normalizeDescription($rowArray['state'] ?? null); + $location = $this->normalizeLocation($rowArray['country'] ?? null); + $deliveryStatus = $this->normalizeDeliveryStatus($rowArray['last_state'] ?? null); - $firstRow = $rows->first(); - Log::info('First row keys', ['keys' => $firstRow->keys()->toArray()]); - Log::info('First row awb', ['awb' => $firstRow['awb'] ?? 'N/A']); - - foreach ($rows as $index => $row) { - try { - $awbNo = $this->normalizeAwb($row['awb'] ?? null); - $eventDate = $this->normalizeDate($row['date'] ?? null); - $eventTime = $this->normalizeTime($row['time'] ?? null); - $eventDescription = $this->normalizeDescription($row['state'] ?? null); - $location = $this->normalizeLocation($row['country'] ?? null); - $deliveryStatus = $this->normalizeDeliveryStatus($row['last_state'] ?? null); - - if (!$awbNo || !$eventDate || !$eventDescription) { - $this->skippedEvents++; - continue; - } - - $shipment = Shipment::where('awb_no', $awbNo)->first(); - - if (!$shipment) { - $shipment = $this->createShipmentFromTrackingRow($row, $awbNo, $location); - $this->createdShipments++; - } - - ShipmentTrackingEvent::create([ - 'shipment_id' => $shipment->id, - 'event_date' => $eventDate, - 'event_time' => $eventTime, - 'location' => $location, - 'event_description' => $eventDescription, - 'delivery_status' => $deliveryStatus, - 'source' => 'manual', - ]); - - $this->importedEvents++; - } catch (\Throwable $e) { + // بررسی اعتبار داده‌های ضروری + if (!$awbNo || !$eventDate || !$eventDescription) { $this->skippedEvents++; - $this->errors[] = "Row {$index}: " . $e->getMessage(); - Log::error('Tracking import error', ['row' => $index, 'error' => $e->getMessage()]); + return; } - } - Log::info('Tracking import completed', [ - 'imported' => $this->importedEvents, - 'skipped' => $this->skippedEvents, - 'created' => $this->createdShipments, - 'errors' => count($this->errors), - ]); + // جستجوی مرسوله بر اساس AWB + $shipment = Shipment::where('awb_no', $awbNo)->first(); + + // اگر مرسوله وجود نداشت، آن را ایجاد کن + if (!$shipment) { + $shipment = $this->createShipmentFromTrackingRow($rowArray, $awbNo, $location); + $this->createdShipments++; + } + + // ثبت رویداد ترکینگ + ShipmentTrackingEvent::create([ + 'shipment_id' => $shipment->id, + 'event_date' => $eventDate, + 'event_time' => $eventTime, + 'location' => $location, + 'event_description' => $eventDescription, + 'delivery_status' => $deliveryStatus, + 'source' => 'manual', + ]); + + $this->importedEvents++; + } catch (\Throwable $e) { + $this->skippedEvents++; + $this->errors[] = "Row {$rowIndex}: " . $e->getMessage(); + Log::error('Tracking import error', ['row' => $rowIndex, 'error' => $e->getMessage()]); + } } protected function createShipmentFromTrackingRow(array $row, string $awbNo, ?string $location): Shipment { + // استخراج نام کشور از لوکیشن $countryName = $location ? explode(' - ', $location)[0] : null; $country = Country::where('name', 'like', "%{$countryName}%")->first(); + // تعیین وضعیت مرسوله بر اساس آخرین وضعیت $lastState = strtolower($row['last_state'] ?? ''); - $status = 'processed'; + if (str_contains($lastState, 'delivered')) $status = 'delivered'; elseif (str_contains($lastState, 'in transit')) $status = 'in_transit'; elseif (str_contains($lastState, 'picked')) $status = 'picked_up'; elseif (str_contains($lastState, 'out for delivery')) $status = 'out_for_delivery'; elseif (str_contains($lastState, 'failed')) $status = 'failed'; - $shipment = Shipment::create([ + // ایجاد مرسوله جدید با مقادیر پیش‌فرض + return Shipment::create([ 'awb_no' => $awbNo, 'direction' => 'export', 'type' => 'PARCEL', @@ -115,29 +106,9 @@ class TrackingDataImport implements ToCollection, WithHeadingRow 'sender_name' => 'Unknown', 'receiver_name' => 'Unknown', ]); - - return $shipment; } - public function getImportedEvents(): int - { - return $this->importedEvents; - } - - public function getSkippedEvents(): int - { - return $this->skippedEvents; - } - - public function getErrors(): array - { - return $this->errors; - } - - public function getCreatedShipments(): int - { - return $this->createdShipments; - } + // --- متدهای کمکی برای نرمال‌سازی داده‌ها --- protected function normalizeAwb($value): ?string { @@ -221,4 +192,26 @@ class TrackingDataImport implements ToCollection, WithHeadingRow return null; } + + // --- متدهای دریافت آمار --- + + public function getImportedEvents(): int + { + return $this->importedEvents; + } + + public function getSkippedEvents(): int + { + return $this->skippedEvents; + } + + public function getErrors(): array + { + return $this->errors; + } + + public function getCreatedShipments(): int + { + return $this->createdShipments; + } } diff --git a/04_Laravel/app/Models/DiscountCode.php b/04_Laravel/app/Models/DiscountCode.php index f4020c6..dfdb5d5 100644 --- a/04_Laravel/app/Models/DiscountCode.php +++ b/04_Laravel/app/Models/DiscountCode.php @@ -7,8 +7,14 @@ use Illuminate\Database\Eloquent\Model; class DiscountCode extends Model { protected $fillable = [ - 'code', 'type', 'value', 'min_order_amount', - 'usage_limit', 'used_count', 'expires_at', 'is_active' + 'code', + 'type', + 'value', + 'min_order_amount', + 'usage_limit', + 'used_count', + 'expires_at', + 'is_active' ]; protected $casts = [ diff --git a/04_Laravel/app/Services/PriceCalculatorService.php b/04_Laravel/app/Services/PriceCalculatorService.php index 66ba6e3..2300492 100644 --- a/04_Laravel/app/Services/PriceCalculatorService.php +++ b/04_Laravel/app/Services/PriceCalculatorService.php @@ -4,7 +4,7 @@ namespace App\Services; use App\Enums\ShipmentType; use App\Models\Country; -use App\Models\Shipment; +use App\Models\DiscountCode; use App\Models\SystemSetting; use InvalidArgumentException; @@ -40,11 +40,31 @@ class PriceCalculatorService $domesticPickup = (float) ($data['domestic_pickup'] ?? 0); $domesticDelivery = (float) ($data['domestic_delivery'] ?? 0); $warehousingCost = (float) ($data['warehousing_cost'] ?? 0); - $discount = (float) ($data['discount'] ?? 0); + + // متغیر $discount را از آرایه داده‌ها دریافت می‌کنیم (اگر وجود داشته باشد) + // اما در اینجا ما از کد تخفیف استفاده می‌کنیم، پس این خط را حذف یا اصلاح می‌کنیم + // $discount = (float) ($data['discount'] ?? 0); - $subtotal = $netRial + $extraService + $packingCost + $domesticPickup + $domesticDelivery + $warehousingCost - $discount; - $totalFee = $subtotal * (1 + $vatRate); - $vatAmount = $totalFee - $subtotal; + // محاسبه قیمت اولیه قبل از تخفیف کد + $subtotal = $netRial + $extraService + $packingCost + $domesticPickup + $domesticDelivery + $warehousingCost; + + // اعمال کد تخفیف (در صورت وجود) + $discountCode = $data['discount_code'] ?? null; + $discountResult = $this->applyDiscount($subtotal, $discountCode); + + if ($discountResult['success']) { + $finalPrice = $discountResult['final_price']; + // افزایش شمارنده استفاده از کد تخفیف + $discount = DiscountCode::where('code', $discountResult['discount_code'])->first(); + if ($discount) { + $discount->increment('used_count'); + } + } else { + $finalPrice = $subtotal; + } + + $totalFee = $finalPrice * (1 + $vatRate); + $vatAmount = $totalFee - $finalPrice; return [ 'base_price' => $rate, @@ -55,7 +75,9 @@ class PriceCalculatorService 'domestic_pickup' => $domesticPickup, 'domestic_delivery' => $domesticDelivery, 'warehousing_cost' => $warehousingCost, - 'discount' => $discount, + 'discount_applied' => $discountResult['success'], + 'discount_amount' => $discountResult['discount_amount'], + 'discount_message' => $discountResult['message'], 'vat_amount' => $vatAmount, 'total_fee' => $totalFee, 'zone' => $zone, @@ -63,6 +85,98 @@ class PriceCalculatorService ]; } + /** + * اعمال کد تخفیف روی قیمت نهایی + * + * @param float $totalPrice قیمت کل قبل از تخفیف + * @param string|null $code کد تخفیف وارد شده توسط کاربر + * @return array ['success' => bool, 'message' => string, 'discount_amount' => float, 'final_price' => float] + */ + public function applyDiscount(float $totalPrice, ?string $code): array + { + if (empty($code)) { + return [ + 'success' => false, + 'message' => 'کد تخفیف وارد نشده است.', + 'discount_amount' => 0, + 'final_price' => $totalPrice, + ]; + } + + $discount = DiscountCode::where('code', $code)->first(); + + // 1. بررسی وجود کد + if (!$discount) { + return [ + 'success' => false, + 'message' => 'کد تخفیف نامعتبر است.', + 'discount_amount' => 0, + 'final_price' => $totalPrice, + ]; + } + + // 2. بررسی فعال بودن کد + if (!$discount->is_active) { + return [ + 'success' => false, + 'message' => 'این کد تخفیف غیرفعال شده است.', + 'discount_amount' => 0, + 'final_price' => $totalPrice, + ]; + } + + // 3. بررسی تاریخ انقضا + if ($discount->expires_at && $discount->expires_at->isPast()) { + return [ + 'success' => false, + 'message' => 'این کد تخفیف منقضی شده است.', + 'discount_amount' => 0, + 'final_price' => $totalPrice, + ]; + } + + // 4. بررسی حداقل مبلغ سفارش + if ($discount->min_order_amount > 0 && $totalPrice < $discount->min_order_amount) { + return [ + 'success' => false, + 'message' => "حداقل مبلغ سفارش برای استفاده از این کد " . number_format($discount->min_order_amount) . " ریال است.", + 'discount_amount' => 0, + 'final_price' => $totalPrice, + ]; + } + + // 5. بررسی محدودیت استفاده + if ($discount->usage_limit && $discount->used_count >= $discount->usage_limit) { + return [ + 'success' => false, + 'message' => 'ظرفیت استفاده از این کد تخفیف به پایان رسیده است.', + 'discount_amount' => 0, + 'final_price' => $totalPrice, + ]; + } + + // محاسبه مبلغ تخفیف + $discountAmount = 0; + + if ($discount->type === 'percentage') { + $discountAmount = ($totalPrice * $discount->value) / 100; + } elseif ($discount->type === 'fixed') { + $discountAmount = $discount->value; + } + + // اطمینان از اینکه تخفیف بیشتر از مبلغ کل نشود + $discountAmount = min($discountAmount, $totalPrice); + $finalPrice = $totalPrice - $discountAmount; + + return [ + 'success' => true, + 'message' => 'کد تخفیف با موفقیت اعمال شد.', + 'discount_amount' => $discountAmount, + 'final_price' => $finalPrice, + 'discount_code' => $discount->code, // برای ذخیره در سفارش + ]; + } + private function lookupRate(string $direction, ShipmentType $type, float $weight, int $zone): float { $zoneColumn = 'zone_' . $zone; diff --git a/04_Laravel/app/Services/WalletService.php b/04_Laravel/app/Services/WalletService.php new file mode 100644 index 0000000..27242ac --- /dev/null +++ b/04_Laravel/app/Services/WalletService.php @@ -0,0 +1,53 @@ +increment('balance', $amount); + + return $this->createTransaction($wallet, $amount, 'deposit', $description, $transactionable); + }); + } + + /** + * برداشت وجه از کیف پول + */ + public function withdraw(Wallet $wallet, float $amount, string $description = null, $transactionable = null): WalletTransaction + { + if ($wallet->balance < $amount) { + throw new \Exception('موجودی کیف پول کافی نیست.'); + } + + return DB::transaction(function () use ($wallet, $amount, $description, $transactionable) { + $wallet->decrement('balance', $amount); + + // مبلغ برداشت به صورت منفی ثبت می‌شود + return $this->createTransaction($wallet, -$amount, 'withdraw', $description, $transactionable); + }); + } + + /** + * ثبت تراکنش در دیتابیس + */ + private function createTransaction(Wallet $wallet, float $amount, string $type, ?string $description, $transactionable): WalletTransaction + { + return WalletTransaction::create([ + 'wallet_id' => $wallet->id, + 'amount' => $amount, + 'type' => $type, + 'description' => $description, + 'transactionable_type' => $transactionable ? get_class($transactionable) : null, + 'transactionable_id' => $transactionable?->id, + ]); + } +}