fix(pricing): restore array-based calculate() and harden pricing API
Restore the array-based signature of PriceCalculatorService::calculate() to fix the TypeError thrown when PricingController and CustomerOrderController called it with an array (the previous commit had switched to 4 separate string params without updating the callers). - PriceCalculatorService: support multiple input formats for direction (export|import|Outbound|Inbound) and type (DOC_NORMAL|DOC_ECONOMY|PARCEL| DOCUMENT|NON DOC) for backward compatibility with admin and customer flows. - ExchangeRateService: add convertToIrr() for explicit AED→IRR conversion (was missing but referenced by the calculator). - PricingController: expand validation rules to accept new shipment types and a discount_code string (was 'discount' numeric). Add 3-layer exception handling (422 validation / 400 business / 500 server). - PriceTestPage: compute volumetric weight from dimensions (IATA: L×W×H/5000), resolve destination country by direction, and show errors as Filament notifications instead of crashing.
This commit is contained in:
parent
513e8d8e7e
commit
563f89495c
@ -81,8 +81,45 @@ class PriceTestPage extends Page
|
|||||||
public function calculate()
|
public function calculate()
|
||||||
{
|
{
|
||||||
$data = $this->form->getState();
|
$data = $this->form->getState();
|
||||||
|
|
||||||
$service = new PriceCalculatorService();
|
// تعیین کشور مقصد (برای صادرات = مقصد، برای واردات = مبدا)
|
||||||
$this->result = $service->calculate($data);
|
$destinationId = ($data['direction'] === 'export')
|
||||||
|
? ($data['destination_country_id'] ?? null)
|
||||||
|
: ($data['origin_country_id'] ?? null);
|
||||||
|
|
||||||
|
$country = Country::find($destinationId);
|
||||||
|
if (! $country) {
|
||||||
|
\Filament\Notifications\Notification::make()
|
||||||
|
->title('خطا')
|
||||||
|
->body('کشور مقصد یافت نشد.')
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// محاسبه وزن حجمی از ابعاد (فرمول IATA: L×W×H/5000)
|
||||||
|
$length = (float) ($data['length'] ?? 0);
|
||||||
|
$width = (float) ($data['width'] ?? 0);
|
||||||
|
$height = (float) ($data['height'] ?? 0);
|
||||||
|
$volumetricWeight = ($length * $width * $height) / 5000;
|
||||||
|
|
||||||
|
$service = app(PriceCalculatorService::class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->result = $service->calculate([
|
||||||
|
'direction' => $data['direction'], // 'export' | 'import'
|
||||||
|
'type' => $data['type'], // 'DOCUMENT' | 'NON DOC'
|
||||||
|
'country_iso' => $country->iso_code,
|
||||||
|
'weight' => (float) $data['weight'],
|
||||||
|
'volumetric_weight' => $volumetricWeight,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Filament\Notifications\Notification::make()
|
||||||
|
->title('خطا در محاسبه قیمت')
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
$this->result = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -15,8 +15,8 @@ class PricingController
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'direction' => ['required', 'string', 'in:Outbound,Inbound'],
|
'direction' => ['required', 'string', 'in:Outbound,Inbound,export,import'],
|
||||||
'type' => ['required', 'string', 'in:DOC_NORMAL,DOC_ECONOMY,PARCEL'],
|
'type' => ['required', 'string', 'in:DOC_NORMAL,DOC_ECONOMY,PARCEL,DOCUMENT,NON DOC'],
|
||||||
'country_iso' => ['required', 'string', 'size:2'],
|
'country_iso' => ['required', 'string', 'size:2'],
|
||||||
'weight' => ['required', 'numeric', 'min:0'],
|
'weight' => ['required', 'numeric', 'min:0'],
|
||||||
'volumetric_weight' => ['required', 'numeric', 'min:0'],
|
'volumetric_weight' => ['required', 'numeric', 'min:0'],
|
||||||
@ -25,17 +25,36 @@ class PricingController
|
|||||||
'domestic_pickup' => ['nullable', 'numeric', 'min:0'],
|
'domestic_pickup' => ['nullable', 'numeric', 'min:0'],
|
||||||
'domestic_delivery' => ['nullable', 'numeric', 'min:0'],
|
'domestic_delivery' => ['nullable', 'numeric', 'min:0'],
|
||||||
'warehousing_cost' => ['nullable', 'numeric', 'min:0'],
|
'warehousing_cost' => ['nullable', 'numeric', 'min:0'],
|
||||||
'discount' => ['nullable', 'numeric', 'min:0'],
|
'discount_code' => ['nullable', 'string', 'max:50'],
|
||||||
]);
|
]);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
'message' => 'Validation failed',
|
'message' => 'Validation failed',
|
||||||
'errors' => $e->errors(),
|
'errors' => $e->errors(),
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->calculator->calculate($validated);
|
try {
|
||||||
|
$result = $this->calculator->calculate($validated);
|
||||||
return response()->json($result);
|
return response()->json($result);
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
], 422);
|
||||||
|
} catch (\RuntimeException $e) {
|
||||||
|
// خطاهای «زون یافت نشد» یا «نرخ یافت نشد» — خطای کسبوکار، نه سرور
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
], 400);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
report($e);
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'خطای داخلی سرور هنگام محاسبه قیمت.',
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -27,6 +27,28 @@ class ExchangeRateService
|
|||||||
return (float) $currency->rate_to_rial;
|
return (float) $currency->rate_to_rial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* تبدیل مبلغ یک ارز به ریال ایران با نرخ روز.
|
||||||
|
* ریال ارز مبناست و نرخ تمام ارزها فقط نسبت به ریال ذخیره میشود.
|
||||||
|
*
|
||||||
|
* @param float $amount مبلغ به ارز دادهشده
|
||||||
|
* @param string $currencyCode کد ارز مبدأ (مثل AED, USD, EUR)
|
||||||
|
* @return float مبلغ معادل به ریال
|
||||||
|
*/
|
||||||
|
public function convertToIrr(float $amount, string $currencyCode = 'AED'): float
|
||||||
|
{
|
||||||
|
if ($amount == 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ریال → ریال: تبدیل لازم نیست
|
||||||
|
if (strtoupper($currencyCode) === 'IRR') {
|
||||||
|
return $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $amount * $this->getCurrentRate($currencyCode);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* تنظیم نرخ جدید برای یک ارز (نسبت به ریال).
|
* تنظیم نرخ جدید برای یک ارز (نسبت به ریال).
|
||||||
*
|
*
|
||||||
|
|||||||
@ -2,27 +2,36 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Models\RateEntry;
|
use App\Models\Country;
|
||||||
use App\Models\CountryZone;
|
use App\Models\CountryZone;
|
||||||
|
use App\Models\DiscountCode;
|
||||||
|
use App\Models\RateEntry;
|
||||||
|
use App\Models\SystemSetting;
|
||||||
use App\Enums\ShipmentDirection;
|
use App\Enums\ShipmentDirection;
|
||||||
use App\Enums\ShipmentType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PriceCalculatorService
|
* PriceCalculatorService
|
||||||
* ----------------------
|
* ----------------------
|
||||||
* 4-quadrant price calculator.
|
* موتور قیمتگذاری ۴-رُبعی:
|
||||||
*
|
*
|
||||||
* ┌──────────┬──────────────┬──────────────────┐
|
* ┌──────────┬──────────────┬──────────────────┐
|
||||||
* │ │ DOCUMENT │ NON-DOCUMENT │
|
* │ │ DOCUMENT │ NON-DOCUMENT │
|
||||||
* ├──────────┼──────────────┼──────────────────┤
|
* ├──────────┼──────────────┼──────────────────┤
|
||||||
* │ Export │ Export×Doc │ Export×Parcel │
|
* │ Export │ Export×Doc │ Export×Parcel │
|
||||||
* │ Import │ Import×Doc │ Import×Parcel │
|
* │ Import │ Import×Doc │ Import×Parcel │
|
||||||
* └──────────┴──────────────┴──────────────────┘
|
* └──────────┴──────────────┴──────────────────┘
|
||||||
*
|
*
|
||||||
* Currency flow:
|
* جریان محاسبه:
|
||||||
* 1. Look up AED rate for (direction, serviceType, weight, zone).
|
* 1. نرمالسازی ورودی (direction / type) و تعیین کشور مقصد با ISO.
|
||||||
* 2. Convert AED → IRR using ExchangeRateService at the current rate.
|
* 2. جستجوی زون کشور از جدول country_zones (زون Doc و Parcel ممکن است متفاوت باشد).
|
||||||
* 3. Return IRR amount (plus optional AED reference).
|
* 3. جستجوی نرخ AED از جدول rate_entries (نزدیکترین وزن بالاتر).
|
||||||
|
* 4. اعمال ضریب سود + هزینههای جانبی → ناخالص درهم.
|
||||||
|
* 5. تبدیل درهم → ریال با نرخ روز از ExchangeRateService.
|
||||||
|
* 6. محاسبه مالیات بر ارزش افزوده (۹٪) و کسر تخفیف (در صورت وجود کد).
|
||||||
|
*
|
||||||
|
* امضای متد calculate(): آرایهای از ورودیها را میگیرد (برای سازگاری با
|
||||||
|
* PricingController، CustomerOrderController و PriceTestPage) و آرایهای کامل
|
||||||
|
* از فیلدهای نمایشی برمیگرداند که وردپرس منتظرش است.
|
||||||
*/
|
*/
|
||||||
class PriceCalculatorService
|
class PriceCalculatorService
|
||||||
{
|
{
|
||||||
@ -31,69 +40,271 @@ class PriceCalculatorService
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the price for a shipment.
|
* محاسبه قیمت نهایی محموله.
|
||||||
*
|
*
|
||||||
* @param string $direction 'import' | 'export'
|
* @param array $input {
|
||||||
* @param string $serviceType 'document' | 'parcel'
|
* @var string $direction 'export' | 'import' (یا 'Outbound' | 'Inbound' برای backward-compat)
|
||||||
* @param float $weightKg actual shipment weight
|
* @var string $type 'DOC_NORMAL' | 'DOC_ECONOMY' | 'PARCEL'
|
||||||
* @param string $countryName destination country name (matched via country_zones)
|
* (یا 'DOCUMENT' | 'NON DOC' | 'DOC' از فرم تست ادمین)
|
||||||
* @return array {
|
* @var string $country_iso کد ISO دو حرفی کشور مقصد (مثلاً 'US', 'AE')
|
||||||
* @var float $aed AED amount
|
* @var float $weight وزن واقعی (کیلوگرم)
|
||||||
* @var float $irr IRR amount (rounded to nearest 1000)
|
* @var float $volumetric_weight وزن حجمی (کیلوگرم)
|
||||||
* @var string $currency Final currency code ('IRR')
|
* @var float|null $extra_service هزینه سرویس اضافی (درهم)
|
||||||
* @var RateEntry|null $rate the rate row that was used
|
* @var float|null $packing_cost هزینه بستهبندی (درهم)
|
||||||
|
* @var float|null $domestic_pickup هزینه جمعآوری داخلی (درهم)
|
||||||
|
* @var float|null $domestic_delivery هزینه تحویل داخلی (درهم)
|
||||||
|
* @var float|null $warehousing_cost هزینه انبارداری (درهم)
|
||||||
|
* @var string|null $discount_code کد تخفیف اختیاری
|
||||||
* }
|
* }
|
||||||
* @throws \RuntimeException when no rate is found
|
*
|
||||||
|
* @return array {
|
||||||
|
* @var float $base_price قیمت پایه (درهم)
|
||||||
|
* @var float $net_dirham ناخالص شامل سود + هزینهها (درهم)
|
||||||
|
* @var int $net_rial ناخالص به ریال
|
||||||
|
* @var float $packing_cost هزینه بستهبندی (درهم)
|
||||||
|
* @var int $vat_amount مبلغ مالیات (ریال)
|
||||||
|
* @var bool $discount_applied آیا تخفیف اعمال شد؟
|
||||||
|
* @var int $discount_amount مبلغ تخفیف (ریال)
|
||||||
|
* @var int $total_fee مبلغ نهایی قابل پرداخت (ریال)
|
||||||
|
* @var float $weight وزن واقعی
|
||||||
|
* @var float $volumetric_weight وزن حجمی
|
||||||
|
* @var float $chargeable_weight وزن قابل محاسبه (max واقعی، حجمی)
|
||||||
|
* @var int $zone زون محاسبهشده
|
||||||
|
* @var string $service_type 'document' | 'parcel'
|
||||||
|
* @var string $direction 'export' | 'import'
|
||||||
|
* @var string $country نام کشور
|
||||||
|
* @var string $country_iso ISO کشور
|
||||||
|
* @var int $rate_entry_id آیدی رکورد نرخ استفادهشده
|
||||||
|
* @var float $profit_margin ضریب سود اعمالشده
|
||||||
|
* @var string $currency همیشه 'IRR'
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException ورودی نامعتبر
|
||||||
|
* @throws \RuntimeException زون یا نرخ یافت نشد
|
||||||
*/
|
*/
|
||||||
public function calculate(
|
public function calculate(array $input): array
|
||||||
string $direction,
|
{
|
||||||
string $serviceType,
|
// ───────────────────────────────────────────────────────────────
|
||||||
float $weightKg,
|
// ۱. نرمالسازی direction
|
||||||
string $countryName,
|
// ───────────────────────────────────────────────────────────────
|
||||||
): array {
|
$directionRaw = strtolower((string) ($input['direction'] ?? ''));
|
||||||
// 1. Look up zone for this country × service type
|
$direction = match ($directionRaw) {
|
||||||
|
'outbound' => '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);
|
$zone = CountryZone::lookupZone($countryName, $serviceType);
|
||||||
if ($zone === null) {
|
if ($zone === null) {
|
||||||
throw new \RuntimeException(
|
throw new \RuntimeException(
|
||||||
"No zone mapping for country '{$countryName}' service type '{$serviceType}'"
|
"No zone mapping for country '{$countryName}' service type '{$serviceType}'. "
|
||||||
|
. "Please import the Zone sheet via admin → Rate Management."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Look up the AED rate for this weight/zone
|
// ───────────────────────────────────────────────────────────────
|
||||||
$rateEntry = RateEntry::lookup($direction, $serviceType, $weightKg, $zone, 'AED');
|
// ۶. جستجوی نرخ (۴-رُبعی)
|
||||||
|
// ───────────────────────────────────────────────────────────────
|
||||||
|
$rateEntry = RateEntry::lookup($direction, $serviceType, $chargeableWeight, $zone, 'AED');
|
||||||
if (! $rateEntry) {
|
if (! $rateEntry) {
|
||||||
throw new \RuntimeException(
|
throw new \RuntimeException(
|
||||||
"No rate found for {$direction}×{$serviceType} weight={$weightKg}kg zone={$zone}"
|
"No rate found for {$direction}×{$serviceType} weight={$chargeableWeight}kg zone={$zone}. "
|
||||||
|
. "Please import rates via admin → Rate Management."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Convert AED → IRR using current exchange rate
|
// ───────────────────────────────────────────────────────────────
|
||||||
$aedAmount = (float) $rateEntry->rate;
|
// ۷. محاسبه هزینهها در درهم
|
||||||
$irrAmount = $this->exchangeRate->convertToIrr($aedAmount, 'AED');
|
// ───────────────────────────────────────────────────────────────
|
||||||
$irrAmount = (int) round($irrAmount / 1000) * 1000; // round to nearest 1000 IRR
|
$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 [
|
return [
|
||||||
'aed' => $aedAmount,
|
// فیلدهای نمایشی که وردپرس از آنها استفاده میکند
|
||||||
'irr' => $irrAmount,
|
'base_price' => round($basePriceAed, 2),
|
||||||
'currency' => 'IRR',
|
'net_dirham' => round($netDirham, 2),
|
||||||
'rate' => $rateEntry,
|
'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',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience: calculate from a Shipment model.
|
* راحتی: محاسبه قیمت از روی مدل Shipment.
|
||||||
|
*
|
||||||
|
* @param \App\Models\Shipment $shipment
|
||||||
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function calculateForShipment($shipment): array
|
public function calculateForShipment($shipment): array
|
||||||
{
|
{
|
||||||
$direction = $shipment->direction === ShipmentDirection::Export->value
|
$direction = $shipment->direction?->value ?? (string) $shipment->direction;
|
||||||
|| $shipment->direction === 'export' ? 'export' : 'import';
|
$type = $shipment->type?->value ?? (string) $shipment->type;
|
||||||
$serviceType = $shipment->type === ShipmentType::Document->value
|
$weight = (float) ($shipment->chargeable_weight ?? $shipment->weight ?? 0);
|
||||||
|| $shipment->type === 'document' ? 'document' : 'parcel';
|
|
||||||
$weight = (float) ($shipment->total_weight ?? $shipment->weight ?? 0);
|
|
||||||
$country = $shipment->toCountry?->name
|
|
||||||
?? $shipment->receiver_country
|
|
||||||
?? $shipment->destination_country;
|
|
||||||
|
|
||||||
return $this->calculate($direction, $serviceType, $weight, $country);
|
// کشور مقصد: برای صادرات = 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),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user