ifnex/DEPLOYMENT.md
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

8.9 KiB
Raw Blame History

IFNEX Rate Importer — 4-Quadrant Model

This package implements the customer's original Excel format for rate imports, replacing the old 2-zone (Document × Parcel) model with a proper 4-quadrant model:

┌──────────┬──────────────┬──────────────────┐
│          │  DOCUMENT    │  NON-DOCUMENT    │
├──────────┼──────────────┼──────────────────┤
│ Export   │ Export×Doc   │ Export×Parcel    │
│ Import   │ Import×Doc   │ Import×Parcel    │
└──────────┴──────────────┴──────────────────┘

Each quadrant is a separate weight × zone matrix (10 zones, weights 0.5–30 kg). Each country can have different zones for Parcel vs Document shipping.


📦 Files in this package

File Purpose
IFNEX_Rate_Template.xlsx The new blank template — give this to the customer
migrations/2026_10_29_000001_extend_rate_entries_for_4_quadrants.php DB schema: rate_entries (4-quadrant) + country_zones
Models/RateEntry.php Eloquent model with lookup() helper
Models/CountryZone.php Eloquent model with lookupZone() helper
Services/RateImportService.php The Excel parser — handles the customer's 2-sheet format
Services/PriceCalculatorService.php Updated calculator using the 4-quadrant model
Filament/Resources/RateImportResource.php Filament admin resource
Filament/Resources/RateImportResource/Pages/ImportRates.php Upload form page
Filament/Resources/views/rate-import.blade.php Blade view for the upload page

🚀 Deployment steps

1. Copy files into your Laravel project

# From your project root (e.g. /home/ifnex/web/system.ifnex.ir)
cp download/ifnex-rate-importer/migrations/*.php    database/migrations/
mkdir -p app/Models
cp download/ifnex-rate-importer/Models/*.php       app/Models/
mkdir -p app/Services
cp download/ifnex-rate-importer/Services/*.php     app/Services/
mkdir -p app/Filament/Resources
cp -r download/ifnex-rate-importer/Filament/Resources/RateImportResource  app/Filament/Resources/
cp download/ifnex-rate-importer/Filament/Resources/RateImportResource.php app/Filament/Resources/
mkdir -p resources/views/filament/pages
cp download/ifnex-rate-importer/Filament/Resources/views/rate-import.blade.php resources/views/filament/pages/rate-import.blade.php

2. Install PhpSpreadsheet (if not already)

composer require phpoffice/phpspreadsheet

3. Run the migration

php artisan migrate

The migration is safe — it will:

  • Create rate_entries table if it doesn't exist
  • Or add the missing columns (direction, service_type, currency, effective_at) if it does
  • Create country_zones table for the country→zone mapping

4. Clear caches

php artisan optimize:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
php artisan filament:assets

5. Visit the admin panel

Navigate to: https://system.ifnex.ir/admin/rate-import

You should see the upload form. Upload the IFNEX_Rate_Template.xlsx (with your filled-in rates) and click Import Rates.


🔧 Filament v3 compatibility notes

This page targets Filament 3.x. Key points:

  1. No CanPoll trait — that trait was removed in Filament 3. We use InteractsWithForms instead.
  2. Form state path — form values are stored in $data array, set via ->statePath('data') on the form.
  3. mount() method — required to initialize the form on page load via $this->form->fill().
  4. Form rendering — the view calls {{ $this->form }} directly inside the <form wire:submit="import"> tag.
  5. FileUpload disk — uses the local disk (storage/app/). The uploaded file path returned is relative to that disk, so we prepend storage_path('app/').

If you see "Trait not found" errors, double-check you're on Filament 3.x:

composer show filament/filament | grep versions

📊 Template structure (matches customer's Excel exactly)

Sheet 1: Import Rate — ROW → IRAN rates

Row 1:  IFNEX IMPORT RATE TABLE — ROW → IRAN
Row 2:  (subtitle)
Row 4:  [merged E4:K4]  "DOCUMENT"
Row 5:  Weight (kg) | Zone 1 | Zone 2 | ... | Zone 10
Row 6:  0.5          | <rate> | <rate> | ... | <rate>
Row 7:  1            | <rate> | ...
Row 8:  1.5          | ...
Row 9:  2            | ...

Row 11: [merged]      "NON - DOCUMENT"
Row 12: Weight (kg) | Zone 1 | ... | Zone 10
Row 13: 0.5          | <rate>
...   (60 rows: 0.5 → 30.0 in 0.5 steps)
Row 73: 30.0         | <rate>
Row 75: ★ Anything above 30 kg requires Spot/Special rates.

Sheet 2: Export Rate — IRAN → ROW rates

Same structure as Import Rate.

Sheet 3: Zone — country → zone mapping

Cols A-C: No. | Destination Country | PARCEL Zone
Cols E-G: No. | Destination Country | DOCUMENT Zone

CRUCIAL: same country can have DIFFERENT zones!
   Afghanistan  →  Parcel Zone 7,  Document Zone 5
   Belgium      →  Parcel Zone 1,  Document Zone 3
   China        →  Parcel Zone 6,  Document Zone 4

Sheet 4: README — instructions (not parsed)


🔍 How the importer detects sections

The RateImportService walks every row and looks for:

  1. Section markers — text cells in column E containing DOCUMENT or NON - DOCUMENT
  2. Column headers — Weight (kg) in column A + at least one Zone N header
  3. Data rows — numeric weight in column A + numeric rates in zone columns

Section markers are normalized: DOCUMENT, Doc, DOC NORMAL all map to 'document'. Similarly NON - DOCUMENT, NON-DOCUMENT, NONDOC, PARCEL all map to 'parcel'.

Empty cells in the rate matrix are skipped (not stored) — the calculator will use the next-higher weight tier.


💱 Currency flow

  1. Rates are stored in AED (matches customer's Excel format).
  2. PriceCalculatorService::calculate() looks up the AED rate.
  3. ExchangeRateService::convertToIrr($aedAmount, 'AED') converts to IRR at the current rate.
  4. Result is rounded to the nearest 1,000 IRR.

If you ever store IRR rates directly (e.g. by uploading with currency = IRR), the calculator will return them without conversion.


✅ What changed from the old model

Aspect Old (2-zone) New (4-quadrant)
Quadrants 2 (Doc / Parcel) 4 (Import×Doc, Import×Parcel, Export×Doc, Export×Parcel)
Zone mapping Single zone per country Separate Parcel Zone + Document Zone per country
Currency Mixed AED in DB, IRR at runtime
History No effective_at effective_at timestamp + lookup picks latest
Weight rounding Buggy (<= matched wrong) Correct (>= rounds UP to next tier)
Excel format Custom narrow format Customer's exact operational Excel format

🧪 Verifying the import worked

After uploading, you should see a notification like:

✓ Imported: 480  |  Updated: 0  |  Skipped: 12  |  Zone rows: 32

Math check:

  • Import Rate sheet: 4 doc weights × 10 zones = 40 cells
  • Import Rate sheet: 60 parcel weights × 10 zones = 600 cells
  • Export Rate sheet: same — 40 + 600 = 640 cells
  • Total expected per direction: 640 → 1,280 across both sheets
  • Minus skipped empty cells

You can verify in tinker:

php artisan tinker
>>> App\Models\RateEntry::count();
=> 1280  // expected count

>>> App\Models\RateEntry::where('direction', 'export')->where('service_type', 'document')->count();
=> 40

>>> App\Models\CountryZone::count();
=> 32  // countries in the Zone sheet

🐛 Common issues

Symptom Cause Fix
No rate found error when calculating Country has no zone in country_zones Upload the Zone sheet, OR add the country manually in admin
Non-numeric rate at weight=0.5 zone=3 warning Cell contains text (e.g. "TBD") Replace with empty cell or a number
Imported count is 0 Sheet names don't match exactly Rename sheets to Import Rate and Export Rate
Section marker not detected Marker text in wrong column Move DOCUMENT / NON - DOCUMENT text to column E
Calculator returns wrong weight tier Old PriceCalculatorService cached php artisan optimize:clear + restart PHP-FPM

🔄 Re-importing rates

You can re-upload the same file anytime. Existing rows with the same (direction, service_type, weight, zone, currency, effective_at) will be UPDATED rather than duplicated. Old rates from previous dates are preserved for history.


📝 Roadmap (not yet implemented)

  • Filament admin page to view/edit rate_entries directly (matrix view)
  • Country management page for country_zones
  • Bulk export current rates back to Excel
  • Pre-import preview ("This will import X rows, update Y. Continue?")
  • Scheduled rate imports from S3/URL