'export', 'inbound' => 'import', 'export', 'import' => $directionRaw, default => null, }; if ($direction === null) { throw new \InvalidArgumentException( "Invalid direction: " . ($input['direction'] ?? '(empty)') . " — expected 'export'|'import'|'Outbound'|'Inbound'" ); } // ─────────────────────────────────────────────────────────────── // ۲. نرمال‌سازی type → service_type // جدول rate_entries فقط 'document' | 'parcel' را ذخیره می‌کند. // DOC_ECONOMY فعلاً به همان نرخ DOC_NORMAL محاسبه می‌شود // (تفکیک کامل نیازمند گسترش service_type در دیتابیس است). // ─────────────────────────────────────────────────────────────── $typeRaw = strtoupper(trim((string) ($input['type'] ?? ''))); $serviceType = match ($typeRaw) { 'DOC_NORMAL', 'DOC_ECONOMY', 'DOCUMENT', 'DOC', 'DOCNORMAL', 'DOCECONOMY' => 'document', 'PARCEL', 'NON DOC', 'NON-DOC', 'NONDOC', 'NON_DOCUMENT' => 'parcel', default => null, }; if ($serviceType === null) { throw new \InvalidArgumentException( "Invalid type: " . ($input['type'] ?? '(empty)') . " — expected DOC_NORMAL|DOC_ECONOMY|PARCEL" ); } // ─────────────────────────────────────────────────────────────── // ۳. حل کشور با ISO → نام (CountryZone با نام کشور جستجو می‌شود) // ─────────────────────────────────────────────────────────────── $countryIso = strtoupper(trim((string) ($input['country_iso'] ?? ''))); if (strlen($countryIso) !== 2) { throw new \InvalidArgumentException( "Invalid country_iso: must be exactly 2 letters, got: " . ($input['country_iso'] ?? '(empty)') ); } $country = Country::where('iso_code', $countryIso)->first(); if (! $country) { throw new \RuntimeException("Country not found for ISO: {$countryIso}"); } $countryName = $country->name; // ─────────────────────────────────────────────────────────────── // ۴. اوزان // ─────────────────────────────────────────────────────────────── $weight = (float) ($input['weight'] ?? 0); $volumetricWeight = (float) ($input['volumetric_weight'] ?? 0); $chargeableWeight = max($weight, $volumetricWeight); // ─────────────────────────────────────────────────────────────── // ۵. جستجوی زون (برای کشور × نوع سرویس) // ─────────────────────────────────────────────────────────────── $zone = CountryZone::lookupZone($countryName, $serviceType); if ($zone === null) { throw new \RuntimeException( "No zone mapping for country '{$countryName}' service type '{$serviceType}'. " . "Please import the Zone sheet via admin → Rate Management." ); } // ─────────────────────────────────────────────────────────────── // ۶. جستجوی نرخ (۴-رُبعی) // ─────────────────────────────────────────────────────────────── $rateEntry = RateEntry::lookup($direction, $serviceType, $chargeableWeight, $zone, 'AED'); if (! $rateEntry) { throw new \RuntimeException( "No rate found for {$direction}×{$serviceType} weight={$chargeableWeight}kg zone={$zone}. " . "Please import rates via admin → Rate Management." ); } // ─────────────────────────────────────────────────────────────── // ۷. محاسبه هزینه‌ها در درهم // ─────────────────────────────────────────────────────────────── $basePriceAed = (float) $rateEntry->rate; $extras = [ 'extra_service' => (float) ($input['extra_service'] ?? 0), 'packing_cost' => (float) ($input['packing_cost'] ?? 0), 'domestic_pickup' => (float) ($input['domestic_pickup'] ?? 0), 'domestic_delivery' => (float) ($input['domestic_delivery'] ?? 0), 'warehousing_cost' => (float) ($input['warehousing_cost'] ?? 0), ]; $extrasTotalAed = array_sum($extras); // ─────────────────────────────────────────────────────────────── // ۸. اعمال ضریب سود (روی قیمت پایه، نه روی هزینه‌های جانبی) // ─────────────────────────────────────────────────────────────── $profitMargin = (float) SystemSetting::get( 'profit_margin', config('ifnex.financial_defaults.profit_margin', 1.25) ); if ($profitMargin <= 0) { $profitMargin = 1.0; // محافظت در برابر صفر یا منفی } $netDirham = ($basePriceAed * $profitMargin) + $extrasTotalAed; // ─────────────────────────────────────────────────────────────── // ۹. تبدیل به ریال // ─────────────────────────────────────────────────────────────── $netRial = $this->exchangeRate->convertToIrr($netDirham, 'AED'); // ─────────────────────────────────────────────────────────────── // ۱۰. مالیات بر ارزش افزوده // ─────────────────────────────────────────────────────────────── $vatRate = (float) SystemSetting::get( 'vat_rate', config('ifnex.financial_defaults.vat_rate', 0.09) ); $vatAmount = $netRial * $vatRate; // ─────────────────────────────────────────────────────────────── // ۱۱. تخفیف (در صورت وجود کد) // ─────────────────────────────────────────────────────────────── $discountApplied = false; $discountAmount = 0.0; $discountCode = trim((string) ($input['discount_code'] ?? '')); if ($discountCode !== '') { $discount = DiscountCode::where('code', $discountCode) ->where('is_active', true) ->where(function ($q) { $q->whereNull('expires_at') ->orWhere('expires_at', '>', now()); }) ->first(); if ($discount) { $subtotal = $netRial + $vatAmount; // بررسی حداقل مبلغ سفارش $minOrder = (float) ($discount->min_order_amount ?? 0); if ($minOrder > 0 && $subtotal < $minOrder) { // زیر سقف — تخفیف اعمال نمی‌شود } else { $discountApplied = true; if ($discount->type === 'percentage') { $discountAmount = $subtotal * ((float) $discount->value / 100); } else { // fixed $discountAmount = min((float) $discount->value, $subtotal); } } } } // ─────────────────────────────────────────────────────────────── // ۱۲. مبلغ نهایی (گردش به نزدیک‌ترین ۱۰۰۰ ریال) // ─────────────────────────────────────────────────────────────── $totalFee = $netRial + $vatAmount - $discountAmount; $totalFee = (int) round($totalFee / 1000) * 1000; return [ // فیلدهای نمایشی که وردپرس از آن‌ها استفاده می‌کند 'base_price' => round($basePriceAed, 2), 'net_dirham' => round($netDirham, 2), 'net_rial' => (int) round($netRial), 'packing_cost' => round($extras['packing_cost'], 2), 'vat_amount' => (int) round($vatAmount), 'discount_applied' => $discountApplied, 'discount_amount' => (int) round($discountAmount), 'total_fee' => $totalFee, // تفکیک هزینه‌ها 'extra_service' => round($extras['extra_service'], 2), 'domestic_pickup' => round($extras['domestic_pickup'], 2), 'domestic_delivery' => round($extras['domestic_delivery'], 2), 'warehousing_cost' => round($extras['warehousing_cost'], 2), // اوزان 'weight' => round($weight, 3), 'volumetric_weight' => round($volumetricWeight, 3), 'chargeable_weight' => round($chargeableWeight, 3), // متادیتای جستجو 'zone' => $zone, 'service_type' => $serviceType, 'direction' => $direction, 'country' => $countryName, 'country_iso' => $countryIso, 'rate_entry_id' => $rateEntry->id, 'profit_margin' => $profitMargin, 'currency' => 'IRR', ]; } /** * راحتی: محاسبه قیمت از روی مدل Shipment. * * @param \App\Models\Shipment $shipment * @return array */ public function calculateForShipment($shipment): array { $direction = $shipment->direction?->value ?? (string) $shipment->direction; $type = $shipment->type?->value ?? (string) $shipment->type; $weight = (float) ($shipment->chargeable_weight ?? $shipment->weight ?? 0); // کشور مقصد: برای صادرات = toCountry، برای واردات = fromCountry $country = ($direction === 'export') ? $shipment->toCountry : $shipment->fromCountry; return $this->calculate([ 'direction' => $direction, 'type' => $type, 'country_iso' => $country?->iso_code, 'weight' => (float) ($shipment->weight ?? 0), 'volumetric_weight' => (float) ($shipment->volumetric_weight ?? 0), 'extra_service' => (float) ($shipment->extra_service ?? 0), 'packing_cost' => (float) ($shipment->packing_cost ?? 0), 'domestic_pickup' => (float) ($shipment->domestic_pickup ?? 0), 'domestic_delivery' => (float) ($shipment->domestic_delivery ?? 0), 'warehousing_cost' => (float) ($shipment->warehousing_cost ?? 0), ]); } }