Implement a new pricing architecture based on a 4-quadrant model (Export/Import x Document/Non-Document) to replace the previous 2-zone model. This includes the ability to handle different zones for parcels and documents per country. - Add `RateEntry` and `CountryZone` models for managing complex rate matrices. - Implement `RateImportService` to parse Excel templates using PhpSpreadsheet. - Add `RateImportResource` in Filament for administrative rate uploads. - Refactor `PriceCalculatorService` to utilize the new 4-quadrant lookup logic. - Add database migrations to extend `rate_entries` and create `country_zones`. - Update `DEPLOYMENT.md` with new architectural details and deployment steps.
100 lines
3.8 KiB
PHP
100 lines
3.8 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\RateEntry;
|
||
use App\Models\CountryZone;
|
||
use App\Enums\ShipmentDirection;
|
||
use App\Enums\ShipmentType;
|
||
|
||
/**
|
||
* PriceCalculatorService
|
||
* ----------------------
|
||
* 4-quadrant price calculator.
|
||
*
|
||
* ┌──────────┬──────────────┬──────────────────┐
|
||
* │ │ DOCUMENT │ NON-DOCUMENT │
|
||
* ├──────────┼──────────────┼──────────────────┤
|
||
* │ Export │ Export×Doc │ Export×Parcel │
|
||
* │ Import │ Import×Doc │ Import×Parcel │
|
||
* └──────────┴──────────────┴──────────────────┘
|
||
*
|
||
* Currency flow:
|
||
* 1. Look up AED rate for (direction, serviceType, weight, zone).
|
||
* 2. Convert AED → IRR using ExchangeRateService at the current rate.
|
||
* 3. Return IRR amount (plus optional AED reference).
|
||
*/
|
||
class PriceCalculatorService
|
||
{
|
||
public function __construct(
|
||
protected ExchangeRateService $exchangeRate,
|
||
) {}
|
||
|
||
/**
|
||
* Calculate the price for a shipment.
|
||
*
|
||
* @param string $direction 'import' | 'export'
|
||
* @param string $serviceType 'document' | 'parcel'
|
||
* @param float $weightKg actual shipment weight
|
||
* @param string $countryName destination country name (matched via country_zones)
|
||
* @return array {
|
||
* @var float $aed AED amount
|
||
* @var float $irr IRR amount (rounded to nearest 1000)
|
||
* @var string $currency Final currency code ('IRR')
|
||
* @var RateEntry|null $rate the rate row that was used
|
||
* }
|
||
* @throws \RuntimeException when no rate is found
|
||
*/
|
||
public function calculate(
|
||
string $direction,
|
||
string $serviceType,
|
||
float $weightKg,
|
||
string $countryName,
|
||
): array {
|
||
// 1. Look up zone for this country × service type
|
||
$zone = CountryZone::lookupZone($countryName, $serviceType);
|
||
if ($zone === null) {
|
||
throw new \RuntimeException(
|
||
"No zone mapping for country '{$countryName}' service type '{$serviceType}'"
|
||
);
|
||
}
|
||
|
||
// 2. Look up the AED rate for this weight/zone
|
||
$rateEntry = RateEntry::lookup($direction, $serviceType, $weightKg, $zone, 'AED');
|
||
if (! $rateEntry) {
|
||
throw new \RuntimeException(
|
||
"No rate found for {$direction}×{$serviceType} weight={$weightKg}kg zone={$zone}"
|
||
);
|
||
}
|
||
|
||
// 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
|
||
|
||
return [
|
||
'aed' => $aedAmount,
|
||
'irr' => $irrAmount,
|
||
'currency' => 'IRR',
|
||
'rate' => $rateEntry,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Convenience: calculate from a Shipment model.
|
||
*/
|
||
public function calculateForShipment($shipment): array
|
||
{
|
||
$direction = $shipment->direction === ShipmentDirection::Export->value
|
||
|| $shipment->direction === 'export' ? 'export' : 'import';
|
||
$serviceType = $shipment->type === ShipmentType::Document->value
|
||
|| $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);
|
||
}
|
||
}
|