ifnex/04_Laravel/app/Services/PriceCalculatorService.php
Kazem Alghasi 513e8d8e7e feat(api): implement 4-quadrant rate import and pricing model
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.
2026-10-29 07:18:52 +03:30

100 lines
3.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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);
}
}