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, + ]); + } +}