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.
246 lines
8.9 KiB
Markdown
246 lines
8.9 KiB
Markdown
# 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
|
||
|
||
```bash
|
||
# 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)
|
||
|
||
```bash
|
||
composer require phpoffice/phpspreadsheet
|
||
```
|
||
|
||
### 3. Run the migration
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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:
|
||
```bash
|
||
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`:
|
||
|
||
```bash
|
||
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
|