refactor(api): improve shipping rate import and pricing logic

Refactor the shipping rate import process to support bidirectional
data handling (import/export) and implement currency conversion using
system settings. Update the price calculator to ensure valid zone
data is retrieved.

- Update `ShippingRatesImport` to handle both import and export
  directions via `SimpleRateSheetImport`.
- Implement AED to IRR conversion during rate importation using
  `SystemSetting`.
- Add validation for zone numbers and rate values during import.
- Enhance `PriceCalculatorService` to filter for non-zero zone values.
- Add various debug and testing scripts for pricing and data
  structure verification.
This commit is contained in:
Kazem Alghasi 2026-08-04 07:24:23 +03:30
parent d6095265e7
commit 3aaa9a5481
14 changed files with 509 additions and 16 deletions

View File

@ -4,6 +4,7 @@ namespace App\Imports;
use App\Enums\ShipmentType;
use App\Models\ShippingRate;
use App\Models\SystemSetting;
use Maatwebsite\Excel\Concerns\OnEachRow;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Concerns\WithStartRow;
@ -14,11 +15,9 @@ class ShippingRatesImport implements WithMultipleSheets
public function sheets(): array
{
return [
'Import Rate' => new RateSheetImport('import'),
'Export Rate' => new RateSheetImport('export'),
'DocNor' => new SimpleRateSheetImport(ShipmentType::DocNormal),
'DocEco' => new SimpleRateSheetImport(ShipmentType::DocEconomy),
'Parcel' => new SimpleRateSheetImport(ShipmentType::Parcel),
'Import Rate' => new RateSheetImport('import'),
'DocEco' => new SimpleRateSheetImport(ShipmentType::DocEconomy, 'export'),
];
}
}
@ -71,13 +70,16 @@ class RateSheetImport implements OnEachRow, WithStartRow
return ShipmentType::Parcel;
}
return $rowIndex <= 7 ? ShipmentType::DocNormal : ShipmentType::Parcel;
return $rowIndex < 10 ? ShipmentType::DocNormal : ShipmentType::Parcel;
}
}
class SimpleRateSheetImport implements OnEachRow, WithStartRow
{
public function __construct(protected ShipmentType $type) {}
public function __construct(
protected ShipmentType $type,
protected string $direction = 'export'
) {}
public function startRow(): int
{
@ -94,22 +96,32 @@ class SimpleRateSheetImport implements OnEachRow, WithStartRow
$weight = (float) $cells[0];
$zoneNumber = (int) $cells[1];
$value = str_replace([',', ' '], '', (string) ($cells[2] ?? 0));
$value = is_numeric($value) ? (float) $value : 0;
$rawValue = str_replace([',', ' '], '', (string) ($cells[2] ?? 0));
$rawValue = is_numeric($rawValue) ? (float) $rawValue : 0;
if ($zoneNumber < 1 || $zoneNumber > 10 || $rawValue <= 0) {
return;
}
$aedToIrr = (float) SystemSetting::get('aed_to_irr', 455000);
$value = $aedToIrr > 0 ? $rawValue / $aedToIrr : $rawValue;
$zoneColumn = 'zone_' . $zoneNumber;
$data = [
'direction' => 'export',
'type' => $this->type->value,
'weight' => $weight,
];
$defaults = [];
for ($i = 1; $i <= 10; $i++) {
$data['zone_' . $i] = 0;
$defaults['zone_' . $i] = 0;
}
$record = ShippingRate::updateOrCreate($data, $data);
$record = ShippingRate::firstOrCreate(
[
'direction' => $this->direction,
'type' => $this->type->value,
'weight' => $weight,
],
$defaults
);
$record->{$zoneColumn} = $value;
$record->save();
}

View File

@ -71,6 +71,7 @@ class PriceCalculatorService
->where('direction', $direction)
->where('type', $type->value)
->where('weight', '<=', $weight)
->where($zoneColumn, '>', 0)
->orderByDesc('weight')
->value($zoneColumn);

View File

@ -0,0 +1,27 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
$sheet = $spreadsheet->getSheetByName('Assumptions');
echo "=== Assumptions sheet ===" . PHP_EOL;
echo "Total rows: " . $sheet->getHighestRow() . PHP_EOL;
echo "Total cols: " . $sheet->getHighestColumn() . PHP_EOL;
for ($row = 1; $row <= $sheet->getHighestRow(); $row++) {
$rowData = $sheet->rangeToArray("A{$row}:Z{$row}")[0];
echo "Row $row: " . implode(' | ', array_map(function($cell) {
return $cell === null ? '(null)' : $cell;
}, $rowData)) . PHP_EOL;
}
echo PHP_EOL . "=== Zone sheet ===" . PHP_EOL;
$sheet = $spreadsheet->getSheetByName('Zone');
for ($row = 1; $row <= $sheet->getHighestRow(); $row++) {
$rowData = $sheet->rangeToArray("A{$row}:Z{$row}")[0];
echo "Row $row: " . implode(' | ', array_map(function($cell) {
return $cell === null ? '(null)' : $cell;
}, $rowData)) . PHP_EOL;
}

View File

@ -0,0 +1,32 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$ss = IOFactory::load($file);
$sheet = $ss->getSheetByName('DocNor');
echo "=== Raw Excel values (DocNor) ===" . PHP_EOL;
for ($r = 9; $r <= 11; $r++) {
$row = $sheet->rangeToArray("A{$r}:C{$r}")[0];
echo "Row {$r}: weight={$row[0]}, zone={$row[1]}, raw={$row[2]}, /455000=" . ($row[2] / 455000) . PHP_EOL;
}
echo PHP_EOL . "=== Stored values (DB) ===" . PHP_EOL;
$records = \App\Models\ShippingRate::where('direction', 'export')
->where('type', 'DOC_NORMAL')
->where('weight', 1)
->first(['zone_1', 'zone_2', 'zone_3', 'zone_4', 'zone_5', 'zone_6', 'zone_7']);
echo json_encode($records, JSON_PRETTY_PRINT) . PHP_EOL;
echo PHP_EOL . "=== Raw Excel values (Export Rate) ===" . PHP_EOL;
$sheet2 = $ss->getSheetByName('Export Rate');
for ($r = 6; $r <= 9; $r++) {
$row = $sheet2->rangeToArray("A{$r}:J{$r}")[0];
echo "Row {$r}: " . implode(' | ', array_map(function($c) { return $c === null ? '(null)' : $c; }, $row)) . PHP_EOL;
}
echo PHP_EOL . "=== System Settings ===" . PHP_EOL;
echo "aed_to_irr: " . \App\Models\SystemSetting::get('aed_to_irr', 455000) . PHP_EOL;
echo "profit_margin: " . \App\Models\SystemSetting::get('profit_margin', 1.25) . PHP_EOL;
echo "vat_rate: " . \App\Models\SystemSetting::get('vat_rate', 0.09) . PHP_EOL;

View File

@ -0,0 +1,25 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
$sheet = $spreadsheet->getSheetByName('DocEco');
echo "=== DocEco: looking for weight 2, zone 3 ===" . PHP_EOL;
for ($row = 1; $row <= $sheet->getHighestRow(); $row++) {
$rowData = $sheet->rangeToArray("A{$row}:C{$row}")[0];
if ((float) $rowData[0] == 2 && (int) $rowData[1] == 3) {
echo "Row $row: Weight={$rowData[0]}, Zone={$rowData[1]}, Value={$rowData[2]}" . PHP_EOL;
echo "Divided by 455000: " . ($rowData[2] / 455000) . PHP_EOL;
echo "Divided by 37000: " . ($rowData[2] / 37000) . PHP_EOL;
echo "Divided by 30000: " . ($rowData[2] / 30000) . PHP_EOL;
break;
}
}
echo PHP_EOL . "=== DocEco: all rows ===" . PHP_EOL;
for ($row = 2; $row <= $sheet->getHighestRow(); $row++) {
$rowData = $sheet->rangeToArray("A{$row}:C{$row}")[0];
echo "Row $row: {$rowData[0]} | {$rowData[1]} | {$rowData[2]}" . PHP_EOL;
}

37
04_Laravel/debug_meta.php Normal file
View File

@ -0,0 +1,37 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
// Check all sheet names
echo "Sheet names:" . PHP_EOL;
foreach ($spreadsheet->getSheetNames() as $name) {
echo " - $name" . PHP_EOL;
}
// Check DocNor for any notes
$sheet = $spreadsheet->getSheetByName('DocNor');
echo PHP_EOL . "=== DocNor notes/comments ===" . PHP_EOL;
$comments = $sheet->getComments();
foreach ($comments as $cell => $comment) {
echo "Cell $cell: " . $comment->getText() . PHP_EOL;
}
// Check for any cells with formulas
echo PHP_EOL . "=== DocNor formulas ===" . PHP_EOL;
for ($row = 1; $row <= 5; $row++) {
for ($col = 1; $col <= 3; $col++) {
$cell = $sheet->getCellByColumnAndRow($col, $row);
if ($cell->getDataType() == \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_FORMULA) {
echo "Cell " . $cell->getCoordinate() . ": " . $cell->getValue() . " = " . $cell->getCalculatedValue() . PHP_EOL;
}
}
}
// Check if there's any metadata
echo PHP_EOL . "=== DocNor first cell values ===" . PHP_EOL;
echo "A1: " . $sheet->getCell('A1')->getValue() . PHP_EOL;
echo "B1: " . $sheet->getCell('B1')->getValue() . PHP_EOL;
echo "C1: " . $sheet->getCell('C1')->getValue() . PHP_EOL;

View File

@ -0,0 +1,29 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
$sheets = ['DocNor', 'DocEco', 'Parcel', 'Import Rate', 'Export Rate'];
foreach ($sheets as $sheetName) {
$sheet = $spreadsheet->getSheetByName($sheetName);
if (!$sheet) {
echo "Sheet '$sheetName' not found" . PHP_EOL;
continue;
}
echo "=== Sheet: $sheetName ===" . PHP_EOL;
echo "Total rows: " . $sheet->getHighestRow() . PHP_EOL;
echo "Total cols: " . $sheet->getHighestColumn() . PHP_EOL;
// Print first 10 rows
for ($row = 1; $row <= min(10, $sheet->getHighestRow()); $row++) {
$rowData = $sheet->rangeToArray("A{$row}:J{$row}")[0];
echo "Row $row: " . implode(' | ', array_map(function($cell) {
return $cell === null ? '(null)' : $cell;
}, $rowData)) . PHP_EOL;
}
echo PHP_EOL;
}

View File

@ -0,0 +1,19 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
// Search all sheets for exchange rate or currency info
foreach ($spreadsheet->getSheetNames() as $sheetName) {
$sheet = $spreadsheet->getSheetByName($sheetName);
echo "=== $sheetName ===" . PHP_EOL;
foreach ($sheet->getCellCollection() as $cell) {
$value = $sheet->getCell($cell)->getValue();
if (is_string($value) && (stripos($value, 'rate') !== false || stripos($value, 'exchange') !== false || stripos($value, 'currency') !== false || stripos($value, 'aed') !== false || stripos($value, 'irr') !== false || stripos($value, 'rial') !== false || stripos($value, 'dirham') !== false)) {
echo " Cell {$cell}: {$value}" . PHP_EOL;
}
}
}

View File

@ -0,0 +1,19 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
foreach (['Start', 'Form', 'DATES'] as $sheetName) {
$sheet = $spreadsheet->getSheetByName($sheetName);
if (!$sheet) continue;
echo "=== $sheetName ===" . PHP_EOL;
for ($row = 1; $row <= min(10, $sheet->getHighestRow()); $row++) {
$rowData = $sheet->rangeToArray("A{$row}:Z{$row}")[0];
echo "Row $row: " . implode(' | ', array_map(function($cell) {
return $cell === null ? '(null)' : $cell;
}, $rowData)) . PHP_EOL;
}
echo PHP_EOL;
}

View File

@ -0,0 +1,25 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$spreadsheet = IOFactory::load($file);
// Check Export Rate sheet more thoroughly
$sheet = $spreadsheet->getSheetByName('Export Rate');
echo "=== Export Rate full structure ===" . PHP_EOL;
for ($row = 1; $row <= 15; $row++) {
$rowData = $sheet->rangeToArray("A{$row}:L{$row}")[0];
echo "Row $row: " . implode(' | ', array_map(function($cell) {
return $cell === null ? '(null)' : $cell;
}, $rowData)) . PHP_EOL;
}
echo PHP_EOL . "=== Import Rate full structure ===" . PHP_EOL;
$sheet = $spreadsheet->getSheetByName('Import Rate');
for ($row = 1; $row <= 15; $row++) {
$rowData = $sheet->rangeToArray("A{$row}:K{$row}")[0];
echo "Row $row: " . implode(' | ', array_map(function($cell) {
return $cell === null ? '(null)' : $cell;
}, $rowData)) . PHP_EOL;
}

View File

@ -0,0 +1,20 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = 'storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx';
$ss = IOFactory::load($file);
echo "=== Export Rate structure ===" . PHP_EOL;
$sheet = $ss->getSheetByName('Export Rate');
for ($r = 4; $r <= 14; $r++) {
$row = $sheet->rangeToArray("A{$r}:L{$r}")[0];
echo "Row {$r}: " . implode(' | ', array_map(function($c) { return $c === null ? '(null)' : $c; }, $row)) . PHP_EOL;
}
echo PHP_EOL . "=== Import Rate structure ===" . PHP_EOL;
$sheet = $ss->getSheetByName('Import Rate');
for ($r = 4; $r <= 14; $r++) {
$row = $sheet->rangeToArray("A{$r}:K{$r}")[0];
echo "Row {$r}: " . implode(' | ', array_map(function($c) { return $c === null ? '(null)' : $c; }, $row)) . PHP_EOL;
}

View File

@ -0,0 +1,77 @@
<?php
require 'vendor/autoload.php';
$app = require_once 'bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
$service = new App\Services\PriceCalculatorService();
$testCases = [
[
'name' => 'PARCEL Export to AE 1kg',
'data' => [
'type' => 'PARCEL',
'direction' => 'Outbound',
'country_iso' => 'AE',
'weight' => 1,
'volumetric_weight' => 1,
'extra_service' => 0,
'packing_cost' => 100000,
'domestic_pickup' => 0,
'domestic_delivery' => 0,
'warehousing_cost' => 0,
'discount' => 0,
],
],
[
'name' => 'DOC_NORMAL Export to DE 0.5kg',
'data' => [
'type' => 'DOC_NORMAL',
'direction' => 'Outbound',
'country_iso' => 'DE',
'weight' => 0.5,
'volumetric_weight' => 0.5,
'extra_service' => 0,
'packing_cost' => 100000,
'domestic_pickup' => 0,
'domestic_delivery' => 0,
'warehousing_cost' => 0,
'discount' => 0,
],
],
[
'name' => 'PARCEL Import from US 5kg',
'data' => [
'type' => 'PARCEL',
'direction' => 'Inbound',
'country_iso' => 'US',
'weight' => 5,
'volumetric_weight' => 5,
'extra_service' => 50000,
'packing_cost' => 150000,
'domestic_pickup' => 20000,
'domestic_delivery' => 30000,
'warehousing_cost' => 50000,
'discount' => 10000,
],
],
];
foreach ($testCases as $case) {
echo "=== {$case['name']} ===" . PHP_EOL;
try {
$result = $service->calculate($case['data']);
echo "Base Price (AED): " . $result['base_price'] . PHP_EOL;
echo "Net Dirham: " . $result['net_dirham'] . PHP_EOL;
echo "Net Rial: " . number_format($result['net_rial'], 0) . PHP_EOL;
echo "Extra Service: " . number_format($result['extra_service'], 0) . PHP_EOL;
echo "Packing Cost: " . number_format($result['packing_cost'], 0) . PHP_EOL;
echo "VAT Amount: " . number_format($result['vat_amount'], 0) . PHP_EOL;
echo "Total Fee: " . number_format($result['total_fee'], 0) . PHP_EOL;
echo "Zone: " . $result['zone'] . PHP_EOL;
echo "Chargeable Weight: " . $result['chargeable_weight'] . PHP_EOL;
} catch (Exception $e) {
echo "ERROR: " . $e->getMessage() . PHP_EOL;
}
echo PHP_EOL;
}

View File

@ -0,0 +1,77 @@
<?php
require 'vendor/autoload.php';
$app = require_once 'bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
$service = new App\Services\PriceCalculatorService();
$testCases = [
[
'name' => 'DOC_NORMAL Export to DE 0.5kg',
'data' => [
'type' => 'DOC_NORMAL',
'direction' => 'Outbound',
'country_iso' => 'DE',
'weight' => 0.5,
'volumetric_weight' => 0.5,
'extra_service' => 0,
'packing_cost' => 100000,
'domestic_pickup' => 0,
'domestic_delivery' => 0,
'warehousing_cost' => 0,
'discount' => 0,
],
],
[
'name' => 'PARCEL Export to AE 1kg',
'data' => [
'type' => 'PARCEL',
'direction' => 'Outbound',
'country_iso' => 'AE',
'weight' => 1,
'volumetric_weight' => 1,
'extra_service' => 0,
'packing_cost' => 100000,
'domestic_pickup' => 0,
'domestic_delivery' => 0,
'warehousing_cost' => 0,
'discount' => 0,
],
],
[
'name' => 'PARCEL Import from US 5kg',
'data' => [
'type' => 'PARCEL',
'direction' => 'Inbound',
'country_iso' => 'US',
'weight' => 5,
'volumetric_weight' => 5,
'extra_service' => 50000,
'packing_cost' => 150000,
'domestic_pickup' => 20000,
'domestic_delivery' => 30000,
'warehousing_cost' => 50000,
'discount' => 10000,
],
],
];
foreach ($testCases as $case) {
echo "=== {$case['name']} ===" . PHP_EOL;
try {
$result = $service->calculate($case['data']);
echo "Base Price (AED): " . $result['base_price'] . PHP_EOL;
echo "Net Dirham: " . $result['net_dirham'] . PHP_EOL;
echo "Net Rial: " . number_format($result['net_rial'], 0) . PHP_EOL;
echo "Extra Service: " . number_format($result['extra_service'], 0) . PHP_EOL;
echo "Packing Cost: " . number_format($result['packing_cost'], 0) . PHP_EOL;
echo "VAT Amount: " . number_format($result['vat_amount'], 0) . PHP_EOL;
echo "Total Fee: " . number_format($result['total_fee'], 0) . PHP_EOL;
echo "Zone: " . $result['zone'] . PHP_EOL;
echo "Chargeable Weight: " . $result['chargeable_weight'] . PHP_EOL;
} catch (Exception $e) {
echo "ERROR: " . $e->getMessage() . PHP_EOL;
}
echo PHP_EOL;
}

View File

@ -0,0 +1,93 @@
<?php
require 'vendor/autoload.php';
$app = require_once 'bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
$service = new App\Services\PriceCalculatorService();
$testCases = [
[
'name' => 'DOC_NORMAL Export to DE 1kg',
'data' => [
'type' => 'DOC_NORMAL',
'direction' => 'Outbound',
'country_iso' => 'DE',
'weight' => 1,
'volumetric_weight' => 1,
'extra_service' => 0,
'packing_cost' => 100000,
'domestic_pickup' => 0,
'domestic_delivery' => 0,
'warehousing_cost' => 0,
'discount' => 0,
],
],
[
'name' => 'PARCEL Export to AE 1kg',
'data' => [
'type' => 'PARCEL',
'direction' => 'Outbound',
'country_iso' => 'AE',
'weight' => 1,
'volumetric_weight' => 1,
'extra_service' => 0,
'packing_cost' => 100000,
'domestic_pickup' => 0,
'domestic_delivery' => 0,
'warehousing_cost' => 0,
'discount' => 0,
],
],
[
'name' => 'PARCEL Import from US 5kg',
'data' => [
'type' => 'PARCEL',
'direction' => 'Inbound',
'country_iso' => 'US',
'weight' => 5,
'volumetric_weight' => 5,
'extra_service' => 50000,
'packing_cost' => 150000,
'domestic_pickup' => 20000,
'domestic_delivery' => 30000,
'warehousing_cost' => 50000,
'discount' => 10000,
],
],
[
'name' => 'DOC_ECONOMY Export to US 2kg',
'data' => [
'type' => 'DOC_ECONOMY',
'direction' => 'Outbound',
'country_iso' => 'US',
'weight' => 2,
'volumetric_weight' => 2,
'extra_service' => 0,
'packing_cost' => 80000,
'domestic_pickup' => 15000,
'domestic_delivery' => 25000,
'warehousing_cost' => 0,
'discount' => 0,
],
],
];
foreach ($testCases as $case) {
echo "=== {$case['name']} ===" . PHP_EOL;
try {
$result = $service->calculate($case['data']);
echo "Base Price (AED): " . $result['base_price'] . PHP_EOL;
echo "Net Dirham: " . $result['net_dirham'] . PHP_EOL;
echo "Net Rial: " . number_format($result['net_rial'], 0) . PHP_EOL;
echo "Extra Service: " . number_format($result['extra_service'], 0) . PHP_EOL;
echo "Packing Cost: " . number_format($result['packing_cost'], 0) . PHP_EOL;
echo "VAT Amount: " . number_format($result['vat_amount'], 0) . PHP_EOL;
echo "Total Fee: " . number_format($result['total_fee'], 0) . PHP_EOL;
echo "Zone: " . $result['zone'] . PHP_EOL;
echo "Chargeable Weight: " . $result['chargeable_weight'] . PHP_EOL;
} catch (Exception $e) {
echo "ERROR: " . $e->getMessage() . PHP_EOL;
}
echo PHP_EOL;
}