# 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 `
` 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 | | | ... | Row 7: 1 | | ... Row 8: 1.5 | ... Row 9: 2 | ... Row 11: [merged] "NON - DOCUMENT" Row 12: Weight (kg) | Zone 1 | ... | Zone 10 Row 13: 0.5 | ... (60 rows: 0.5 → 30.0 in 0.5 steps) Row 73: 30.0 | 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