'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 $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 → نام $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." ); } // ۶. جستجوی نرخ — مستقیماً به ریال (IRR) $rateEntry = RateEntry::lookup($direction, $serviceType, $chargeableWeight, $zone, 'IRR'); if (! $rateEntry) { throw new \RuntimeException( "No rate found for {$direction}×{$serviceType} weight={$chargeableWeight}kg zone={$zone}. " . "Please import rates via admin → Rate Management." ); } // ۷. قیمت پایه — مستقیماً به ریال $basePriceIrr = (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), ]; $extrasTotalIrr = array_sum($extras); // ۹. اعمال ضریب سود + هزینه‌های جانبی $profitMargin = (float) SystemSetting::get( 'profit_margin', config('ifnex.financial_defaults.profit_margin', 1.25) ); if ($profitMargin <= 0) { $profitMargin = 1.0; } $netRial = ($basePriceIrr * $profitMargin) + $extrasTotalIrr; // ۱۰. مالیات بر ارزش افزوده $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 { $discountAmount = min((float) $discount->value, $subtotal); } } } } // ۱۲. مبلغ نهایی (گردش به ۱۰۰۰ ریال) $totalFee = $netRial + $vatAmount - $discountAmount; $totalFee = (int) round($totalFee / 1000) * 1000; return [ 'base_price' => round($basePriceIrr, 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', ]; } public function calculateForShipment($shipment): array { $direction = $shipment->direction?->value ?? (string) $shipment->direction; $type = $shipment->type?->value ?? (string) $shipment->type; $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), ]); } }