✅ ۱۰ مورد کامل شد (دیتابیس، مدل‌ها، API، Filament، Laravel 11، رفع باگ‌ها)

🔄 در حال انجام: تست دستی Filament
⏸️ بلاک: مهاجرت ۳۹۵۰ رکورد (منتظر فایل اکسل اصلاح‌شده)
⏸️ بلاک: پلاگین وردپرس (بعد از تست کامل API)
This commit is contained in:
Kazem Alghasi 2026-08-04 01:59:08 +03:30
parent 97fa3a9561
commit a59e192b32
56 changed files with 2713 additions and 1132 deletions

View File

@ -0,0 +1,229 @@
# ✅ IFNEX Phase 0 Checklist
> **فاز:** Phase 0 — بنیان داده + ترکینگ دستی + مهاجرت داده‌های تاریخی
> **تاریخ ایجاد:** 2026-08-04
> **وضعیت کلی:** در حال اجرا
> **مرجع اصلی:** `01_Documents/Phase0_Proposal.md`
---
## پیش‌نیازها
| # | شرط | وضعیت | توضیحات |
|---|------|-------|---------|
| P1 | Laravel 11 نصب و قابل اجرا | ✅ انجام شد | در پوشه `04_Laravel` |
| P2 | MySQL 8+ قابل دسترس | ✅ انجام شد | دیتابیس `ifnex_db` ایجاد شده |
| P3 | Composer dependencies نصب شده | ✅ انجام شد | filament 3.3.x, maatwebsite/excel, morilog/jalali 3 |
| P4 | فایل‌های اکسل اصلی در دسترس | ⚠️ موجود | فقط داده‌های ۲ ردیف اول شیت List واقعی هستند، بقیه فرمول Excel |
---
## بخش ۱: دیتابیس و مدل‌ها
| # | کار | وضعیت | توضیحات |
|---|-----|-------|---------|
| 1.1 | Migration `countries` با ۴ زون | ✅ انجام شد | iso_code + is_active اضافه شد |
| 1.2 | Migration `shipments` مطابق Phase 0 | ✅ انجام شد | ۱۱ فیلد مالی + ۴ زون + nullable حقایل |
| 1.3 | Migration `shipping_rates` با enum | ✅ انجام شد | direction + type (DOC_NORMAL/DOC_ECONOMY/PARCEL) |
| 1.4 | Migration `shipment_carrier_mappings` | ✅ انجام شد | جدول جدید برای نگاشت چند شرکت حمل |
| 1.5 | Migration `shipment_tracking_events` | ✅ انجام شد | جدول جدید برای تایم‌لاین رویدادها |
| 1.6 | Migration `system_settings` | ✅ انجام شد | جدول key-value |
| 1.7 | Migration `update_users` | ✅ انجام شد | phone, role, is_active |
| 1.8 | حل مشکل `unsignedDecimal` | ✅ انجام شد | استفاده از `decimal()->unsigned()` |
| 1.9 | حل مشکل FK constraints | ✅ انجام شد | `disableForeignKeyConstraints()` |
| 1.10 | اجرای همه migrations | ✅ انجام شد | بدون خطا |
### مدل‌ها و Enums
| # | کار | وضعیت | توضیحات |
|---|-----|-------|---------|
| 2.1 | مدل `Country.php` + روابط | ✅ انجام شد | fromShipments() + toShipments() |
| 2.2 | مدل `Shipment.php` + روالات کامل | ✅ انجام شد | items(), carrierMappings(), trackingEvents() |
| 2.3 | مدل `ShippingRate.php` | ✅ انجام شد | cast برای enums |
| 2.4 | مدل `User.php` + helper methods | ✅ انجام شد | phone, role, is_active |
| 2.5 | مدل `ShipmentCarrierMapping.php` | ✅ انجام شد | |
| 2.6 | مدل `ShipmentTrackingEvent.php` | ✅ انجام شد | |
| 2.7 | مدل `SystemSetting.php` | ✅ انجام شد | |
| 2.8 | Enum `ShipmentDirection` | ✅ انجام شد | import/export |
| 2.9 | Enum `ShipmentType` | ✅ انجام شد | DOC_NORMAL/DOC_ECONOMY/PARCEL |
| 2.10 | Enum `ShipmentStatus` | ✅ انجام شد | ۷ وضعیت |
| 2.11 | Enum `CarrierCode` | ✅ انجام شد | ۹ شرکت حمل |
| 2.12 | Enum `TrackingSource` | ✅ انجام شد | manual/api_carrier/api_aggregator |
| 2.13 | Enum `UserRole` | ✅ انجام شد | ۴ نقش |
---
## بخش ۲: Seeders و داده‌های اولیه
| # | کار | وضعیت | توضیحات |
|---|-----|-------|---------|
| 3.1 | Seeder ۲۳۲ کشور با ۴ زون | ✅ انجام شد | تصحیح کدهای ISO، حذف تکراری‌ها |
| 3.2 | Seeder System Settings | ✅ انجام شد | VAT 9%, packing 100000 IRR, profit_margin 1.25 |
| 3.3 | Seeder Super Admin User | ✅ انجام شد | |
| 3.4 | DatabaseSeeder ترکیبی | ✅ انجام شد | |
---
## بخش ۳: API ترکینگ
| # | کار | وضعیت | توضیحات |
|---|-----|-------|---------|
| 4.1 | ایجاد `TrackController` | ✅ انجام شد | JSON response کامل |
| 4.2 | ایجاد `ApiKeyMiddleware` | ✅ انجام شد | Bearer token validation |
| 4.3 | ثبت route `GET /api/v1/track/{awb_no}` | ✅ انجام شد | |
| 4.4 | تست با API Key صحیح | ✅ انجام شد | 200 OK |
| 4.5 | تست بدون API Key | ✅ انجام شد | 401 Unauthorized |
| 4.6 | تست با API Key نامعتبر | ✅ انجام شد | 403 Forbidden |
---
## بخش ۴: پنل مدیریت Filament
### Resources اصلی
| # | کار | وضعیت | توضیحات |
|---|-----|-------|---------|
| 5.1 | `CountryResource` | ✅ انجام شد | فرم ۴ زون + iso_code + is_active |
| 5.2 | `ShipmentResource` | ✅ انجام شد | ۷ Section کامل |
| 5.3 | `ShippingRateResource` | ✅ انجام شد | ۱۰ zone + enum |
| 5.4 | `ShipmentItemResource` | ✅ انجام شد | CRUD کامل |
| 5.5 | `CarrierMappingsRelationManager` | ✅ انجام شد | برای Shipment |
| 5.6 | `TrackingEventsRelationManager` | ✅ انجام شد | برای Shipment |
| 5.7 | `SettingsPage` | ✅ انجام شد | System Settings |
| 5.8 | صفحه‌های List/Create/Edit/View | ✅ انجام شد | برای همه Resources |
---
## بخش ۵: Services
| # | کار | وضعیت | توضیحات |
|---|-----|-------|---------|
| 6.1 | `TrackingService` | ✅ انجام شد | getTrackingTimeline, addTrackingEvent, getShipmentWithTracking |
| 6.2 | `PriceCalculatorService` | ✅ انجام شد | ۹ مرحله محاسبه |
---
## بخش ۶: Excel Imports
| # | کار | وضعیت | توضیحات |
|---|-----|-------|---------|
| 7.1 | `ShippingRatesImport` | ✅ انجام شد | ۴۰۴ رکورد import شد |
| 7.2 | `HistoricalShipmentsImport` | ⚠️ جزئی | فقط ۱ رکورد موفق (بقیه فرمول Excel هستند) |
| 7.3 | Artisan Command `ifnex:import:rates` | ✅ انجام شد | |
| 7.4 | Artisan Command `ifnex:import:shipments` | ✅ انجام شد | |
---
## بخش ۷: پیکربندی
| # | کار | وضعیت | توضیحات |
|---|-----|-------|---------|
| 8.1 | `config/ifnex.php` | ✅ انجام شد | تنظیمات اختصاصی |
| 8.2 | `config/cors.php` | ✅ انجام شد | |
| 8.3 | `.env.example` به‌روز | ✅ انجام شد | IFNEX_API_KEY, CORS_ALLOWED_ORIGINS, ... |
| 8.4 | سوییچ به Laravel 11 | ✅ انجام شد | Downgrade از v12 به v11 |
| 8.5 | نصب `morilog/jalali` v3 | ✅ انجام شد | |
| 8.6 | رفع خطای PSR-4 | ✅ انجام شد | SettingsPage.php |
| 8.7 | رفع خطای `MethodNotAllowedHttpException` | ✅ انجام شد | POST /admin/login |
---
## بخش ۸: Code Review
| # | کار | وضعیت | توضیحات |
|---|-----|-------|---------|
| 9.1 | بررسی `bootstrap/app.php` | ✅ انجام شد | مشکل: گروه `web` تعریف نشده |
| 9.2 | بررسی `AdminPanelProvider.php` | ✅ انجام شد |Middlewareهای طولانی به صورت دستی |
| 9.3 | بررسی `composer.json` | ✅ انجام شد | صحیح است |
| 9.4 | بررسی Routes | ✅ انجام شد | کافی برای حال حاضر |
| 9.5 | ارائه راه‌حل برای مشکلات | ✅ انجام شد | راه‌حل در STATUS.md ثبت شد |
---
## بخش ۹: بلاک‌های فعلی
| # | مشکل | اولویت | وضعیت | راه‌حل |
|---|------|--------|-------|--------|
| B1 | فایل اکسل List فقط ۲ ردیف داده واقعی دارد | 🔴 بالا | **بلاک** | دریافت فایل اصلی کامل از مشتری |
| B2 | گروه `web` در `bootstrap/app.php` تعریف نشده | 🔴 بالا | **بلاک** | اضافه کردن `$middleware->group('web', [...])` |
| B3 | فایل اکسل ترکینگ (Sheet1 + Refrence) موجود نیست | 🟡 متوسط | **بلاک** | دریافت از مشتری |
| B4 | پلاگین وردپرس IFNEX Bridge ساخته نشده | 🟡 متوسط | نشده | بعد از تست کامل API |
| B5 | تست دستی کامل Filament انجام نشده | 🟡 متوسط | نشده | بعد از رفع B2 |
---
## بخش ۱۰: کارهای باقی‌مانده فاز ۰
| # | کار | اولویت | وضعیت | یادداشت |
|---|-----|--------|-------|---------|
| R1 | رفع مشکل middleware `web` group | 🔴 بالا | **نیاز به اصلاح** | اولویت اول |
| R2 | ساده کردن `AdminPanelProvider.php` | 🔴 بالا | **نیاز به اصلاح** | بعد از R1 |
| R3 | تست دستی کامل Filament | 🔴 بالا | pending | ورود + CRUD + RelationManagers |
| R4 | دریافت فایل اکсел اصلاح‌شده List | 🔴 بالا | **بلاک** | بدون این فایل، R5 امکان‌پذیر نیست |
| R5 | مهاجرت ۳۹۵۰ رکورد تاریخی | 🔴 بالا | **بلاک** | منتظر R4 |
| R6 | مهاجرت داده‌های ترکینگ دستی | 🟡 متوسط | **بلاک** | منتظر فایل اکسل ترکینگ |
| R7 | ساخت پلاگین وردپرس IFNEX Bridge | 🟡 متوسط | pending | بعد از R3 |
| R8 | تست کامل API Track با داده‌های واقعی | 🟡 متوسط | pending | بعد از R5 |
| R9 | تست نهایی فاز ۰ و تحویل به مشتری | 🟢 پایین | pending | بعد از تأیید همه موارد بالا |
---
## بخش ۱۱: کارهای فاز ۱ (پس از تأیید فاز ۰)
| # | کار | اولویت | وضعیت |
|---|-----|--------|-------|
| F1.1 | تست کامل `PriceCalculatorService` با داده‌های واقعی | 🔴 بالا | pending |
| F1.2 | تکمیل جدول `shipping_rates` | 🟡 متوسط | ۴۰۴ رکورد import شد |
| F1.3 | فرم ثبت سفارش آنلاین با ۹ ردیف کالای گمرکی | 🟡 متوسط | pending |
| F1.4 | تولید PDF: AWB، INVOICE، Label | 🟡 متوسط | pending |
| F1.5 | ماژول ایمپورت اکسل تعرفه‌ها | 🟢 پایین | pending |
| F1.6 | صفحه استعلام قیمت واقعی | 🟢 پایین | pending |
---
## بخش ۱۲: کارهای فاز ۲ و ۳ (مرجع)
### فاز ۲: اتوماسیون مالی
- [ ] اتصال به درگاه پرداخت (زرین‌پال)
- [ ] ایجاد جدول کیف پول و تراکنش‌ها
- [ ] شارژ اعتبار آنلاین و دستی
- [ ] کرون‌جاب بروزرسانی نرخ ارز
- [ ] سیستم کوپن و تخفیف‌ها
- [ ] فرمول محاسبه قیمت رقبا
### فاز ۳: رهگیری زنده و عملیات
- [ ] راه‌اندازی VPS پل خارج (هلند/آلمان)
- [ ] اتصال به TrackingMore/17track API
- [ ] سیستم وب‌هوک
- [ ] مپ کردن کدهای رهگیری
- [ ] داشبورد مالی
- [ ] فرم نظرسنجی و رضایت مشتری
- [ ] پنل کاربری مشتریان در وردپرس
---
## Legend
| آیکون | معنی |
|--------|------|
| ✅ | انجام شده |
| 🔄 | در حال انجام |
| ⚠️ | جزئی/ناقص/بلاک |
| ❌ | لغو شده/خراب |
| ⏸️ | متوقف/منتظر |
| 🔴 بالا | اولویت بالا |
| 🟡 متوسط | اولویت متوسط |
| 🟢 پایین | اولویت پایین |
---
## نحوه استفاده از این فایل
۱. **در ابتدای هر جلسه:** این فایل را باز کنید و موارد `pending` را بررسی کنید.
۲. **بعد از انجام هر کار:** شماره کار را پیدا کنید و وضعیت را به‌روز کنید.
۳. **اگر بلاک جدیدی ایجاد شد:** در بخش «بلاک‌های فعلی» اضافه کنید.
۴. **اگر نیاز به Code Review دارید:** بخش ۹ را بررسی کنید.
---
© 2026 VernaSoft Group. Internal use only.

View File

@ -1,9 +1,9 @@
# 🚨 STATUS.md — این فایل را اول بخوانید!
# 🚨 STATUS.md — این فایل را اول بخوانید
> **آخرین به‌روزرسانی:** August 2026
> **آخرین به‌روزرسانی:** 2026-08-04 01:10 (Asia/Tehran)
> **فاز در حال اجرا:** فاز ۰ (بنیان داده + ترکینگ دستی + مهاجرت داده‌های تاریخی)
> **توسعه‌دهنده:** VernaSoft Group — Kazem Alghasi
> **وضعیت کلی پروژه:** در حال اجرا — بازنگری مسیر از نقشه‌ی ۳ فازی به ۴ فازی انجام شده است
> **وضعیت کلی پروژه:** در حال اجرا — Code Review کامل شد، نیاز به رفع نقص middleware است
---
@ -32,7 +32,7 @@
| PHP | 8.2+ | الزام لاراول ۱۱ |
| MySQL | 8+ | برای پشتیبانی JSON columns |
| WordPress | آخرین نسخه پایدار | با Polylang برای چندزبانه |
| پنل ادمین | Laravel Filament 3.x | برای سرعت توسعه |
| پنل ادمین | Laravel Filament 3.3.x | برای سرعت توسعه |
| Frontend | وردپرس + قالب DHL-inspired | **کپی نکنید** — فقط الهام |
### معماری کلی
@ -42,58 +42,207 @@
│ (Frontend) │ پلاگین IFNEX Bridge │ (Backend) │
│ │ │ + Filament │
└─────────────────┘ └────────┬────────┘
│
┌────────┴────────┐
│ MySQL 8 │
└─────────────────┘
│
(فاز ۳) │
┌────────┴────────┐
│ VPS پل خارج │
│ (هلند/آلمان) │
└────────┬────────┘
│
┌────────┴────────┐
│ TrackingMore / │
│ 17track API │
└─────────────────┘
│
┌────────┴────────┐
│ MySQL 8 │
└─────────────────┘
│
(فاز ۳) │
┌────────┴────────┐
│ VPS پل خارج │
│ (هلند/آلمان) │
└────────┬────────┘
│
┌────────┴────────┐
│ TrackingMore / │
│ 17track API │
└─────────────────┘
```
---
## ✅ وضعیت فعلی کار
### کارهای انجام‌شده (تا آخرین به‌روزرسانی)
- [x] تحلیل کامل فایل‌های اکسل عملیاتی شرکت
- [x] شناسایی تناقضات PRD قدیمی با واقعیت اکسل (۴ زون به‌جای ۲، ۳ نوع سرویس به‌جای ۲، فیلدهای غایب VAT/Packing/Warehousing)
- [x] تدوین سند `Phase0_Proposal.md` (مرجع اصلی پروژه)
- [x] بازنگری نقشه‌ی راه از ۳ فازی به ۴ فازی
- [x] طراحی اسکیمای دیتابیس فاز ۰ (۶ جدول اصلی)
- [x] بازنویسی `README.md` با ساختار جدید
- [x] تدوین `EXCEL_ANALYSIS.md` (تحلیل کامل اکسل)
### کارهای انجام‌شده (تا تاریخ 2026-08-04)
#### ۱. اسکیمای دیتابیس فاز ۰ (کامل)
- [x] بازنویسی migration `countries` با ۴ زون مجزا + `iso_code` + `is_active`
- [x] بازنویسی migration `shipments` مطابق Phase 0 (فیلدهای مالی + ۴ زون + nullable حقایل)
- [x] بازنویسی migration `shipping_rates` با enum `direction` و `type` (DOC_NORMAL/DOC_ECONOMY/PARCEL)
- [x] ایجاد migration `shipment_carrier_mappings` (جدول جدید — نگاشت چند شرکت حمل)
- [x] ایجاد migration `shipment_tracking_events` (جدول جدید — تایم‌لاین رویدادها)
- [x] ایجاد migration `system_settings` (جدول جدید — key-value برای VAT، نرخ ارز و...)
- [x] ایجاد migration `update_users` (افزودن `phone`, `role`, `is_active`)
- [x] حل مشکل `unsignedDecimal` در Laravel 11 (استفاده از `decimal()->unsigned()`)
- [x] حل مشکل FK constraints در migrations (استفاده از `disableForeignKeyConstraints()`)
- [x] اجرای همه migrations در MySQL بدون خطا
#### ۲. مدل‌ها و Enums (کامل)
- [x] بازنویسی `Country.php` + روابط `fromShipments()` / `toShipments()`
- [x] بازنویسی `Shipment.php` + روابط `fromCountry()`, `toCountry()`, `items()`, `carrierMappings()`, `trackingEvents()`
- [x] بازنویسی `ShippingRate.php` + cast برای enums
- [x] بازنویسی `User.php` + فیلدهای `phone`, `role`, `is_active` + helper methods
- [x] ایجاد مدل‌های جدید:
- `ShipmentCarrierMapping.php`
- `ShipmentTrackingEvent.php`
- `SystemSetting.php`
- [x] ایجاد ۶ enum:
- `ShipmentDirection` (import/export)
- `ShipmentType` (DOC_NORMAL/DOC_ECONOMY/PARCEL)
- `ShipmentStatus` (processed/picked_up/in_transit/out_for_delivery/failed/delivered/returned)
- `CarrierCode` (DHL/FEDEX/UPS/ARAMEX/NAGHEL/EMX/APSITEX/IMPEX/OTHER)
- `TrackingSource` (manual/api_carrier/api_aggregator)
- `UserRole` (super_admin/tracking_operator/data_entry/customer)
#### ۳. Seeders (کامل)
- [x] بازنویسی `CountriesTableSeeder` با ۲۳۳ کشور + ۴ زون + iso_code تصحیح‌شده
- تصحیح کدهای ISO نادرست (Bonaire BQ، Curacao CW، حذف Somaliland XS)
- حذف کشورهای تکراری ("United Arab Emirates" و "Rest of Dubai" هر دو با zone 9 و 10)
- [x] ایجاد `SystemSettingSeeder` (VAT 9%، packing 100000 IRR، profit_margin 1.25، نرخ ارزهای اولیه)
- [x] بازنویسی `DatabaseSeeder` (countries + system_settings + super_admin user)
- [x] اجرای seeders — ۲۳۲ کشور + ۷ setting + ۱ کاربر ادمین
#### ۴. API ترکینگ (کامل)
- [x] ایجاد `TrackController` با JSON response کامل (shipment + carrier_mappings + timeline)
- [x] ایجاد `ApiKeyMiddleware` (Bearer token validation)
- [x] ثبت route `GET /api/v1/track/{awb_no}` با middleware API Key
- [x] تست‌های موفق:
- `GET /api/v1/track/TEST123` با API Key صحیح → **200 OK**
- بدون API Key → **401 Unauthorized**
- با API Key نامعتبر → **403 Forbidden**
#### ۵. Filament Resources (کامل)
- [x] `CountryResource` — فرم ۴ زون + iso_code + is_active + فیلترها
- [x] `ShipmentResource` — فرم کامل با ۷ Section + RelationManagers
- Section: Route & Status
- Section: Weight & Dimensions
- Section: Financial Info (۱۱ فیلد مالی)
- Section: Sender Info
- Section: Receiver Info
- Section: Customs Items (Repeater ۹ ردیف)
- [x] `ShippingRateResource` — فرم ۱۰ zone + enum type/direction
- [x] `ShipmentItemResource` — CRUD کامل
- [x] RelationManagers:
- `CarrierMappingsRelationManager` (برای Shipment)
- `TrackingEventsRelationManager` (برای Shipment)
- [x] Pages:
- `ListShipments`, `CreateShipment`, `EditShipment`, `ViewShipment`
- `ListCountries`, `CreateCountry`, `EditCountry`
- `ListShippingRates`, `CreateShippingRate`, `EditShippingRate`
- `ListShipmentItems`, `CreateShipmentItem`, `EditShipmentItem`
- [x] `SettingsPage` (Page سفارشی برای System Settings)
#### ۶. Services (کامل)
- [x] `TrackingService` — getTrackingTimeline, addTrackingEvent, getShipmentWithTracking
- [x] `PriceCalculatorService` — محاسبه قیمت کامل مطابق EXCEL_ANALYSIS.md
- مرحله ۱: وزن حجمی
- مرحله ۲: وزن قابل پرداخت
- مرحله ۳: استخراج زون از ۴ زون مجزا
- مرحله ۴: استخراج قیمت پایه از shipping_rates
- مرحله ۵: اعمال ضریب سود
- مرحله ۶: تبدیل درهم به ریال
- مرحله ۷: اضافه هزینه‌های جانبی
- مرحله ۸: اعمال VAT 9%
- مرحله ۹: ذخیره خروجی‌ها
#### ۷. Imports (Excel) (کامل)
- [x] `ShippingRatesImport` — import از ۵ شیت (Import Rate, Export Rate, DocNor, DocEco, Parcel)
- ۴۰۴ رکورد import شد: DocNormal: ۷۴، DocEconomy: ۷۰، Parcel: ۲۶۰
- [x] `HistoricalShipmentsImport` — import از شیت List فایل اصلی
- ⚠️ **مشکل:** فایل اکسل فقط ۲ ردیف داده واقعی دارد و بقیه فرمول‌های Excel هستند
- ۱ رکورد (980100010) موفقیت‌آمیز import شد
- [x] Artisan Commands:
- `ifnex:import:rates {path}`
- `ifnex:import:shipments {path}`
#### ۸. سوییچ به Laravel 11 (کامل با نقص باقی‌مانده)
- [x] تغییر `composer.json`: `laravel/framework` از `^12.0` به `^11.0`
- [x] تغییر `morilog/jalali` از `^4.3` به `^3.0` (v4 وجود ندارد)
- [x] اجرای `composer update` — Laravel از v12.64.0 به v11.55.0 downgrade شد
- [x] رفع خطای PSR-4 در `SettingsPage.php`
- [x] رفع مشکل middleware در `bootstrap/app.php` برای Laravel 11
- [x] رفع خطای `MethodNotAllowedHttpException` برای POST `/admin/login`
#### ٩. پیکربندی (کامل)
- [x] اضافه کردن `IFNEX_API_KEY` به `.env.example`
- [x] اضافه کردن `CORS_ALLOWED_ORIGINS` به `.env.example`
- [x] اضافه کردن `IFNEX_TRACKING_RATE_LIMIT` به `.env.example`
- [x] ایجاد `config/ifnex.php` برای تنظیمات اختصاصی
- [x] تغییر `APP_LOCALE` به `fa` در `.env.example`
- [x] اضافه کردن `morilog/jalali` به `composer.json`
- [x] ایجاد `config/cors.php`
---
### 🔴 Code Review Findings — نیاز به رفع
| # | فایل | مشکل | وضعیت | راه‌حل |
|---|------|------|-------|-------|
| 1 | `bootstrap/app.php` | گروه `web` تعریف نشده | ⚠️ باز | اضافه کردن `$middleware->group('web', [...])` |
| 2 | `AdminPanelProvider.php` | Middlewareهای طولانی به صورت لیست دستی | ⚠️ بهبودنیاز | استفاده از گروه `web` به جای لیست دستی |
| 3 | `bootstrap/app.php` | `statefulApi` تعریف نشده | 🟡 آینده | برای احراز هویت SPA در فاز بعد لازم است |
**توضیح مشکل #1:**
در Laravel 11، گروه `web` به‌صورت خودکار تعریف نمی‌شود. Filament برای لاگین نیاز به Session, Cookies, CSRF, ShareErrors دارد. فعلاً کار می‌کند چون در `AdminPanelProvider.php` همه Middlewareها به صورت لیست دستی نوشته شده‌اند، اما این روش **خوب نیست** و مطابق Laravel 11 conventions نیست. اگر جایی از `web` group استفاده شود، کار نمی‌کند.
**راه‌حل پیشنهادی برای `bootstrap/app.php`:**
```php
->withMiddleware(function (Middleware $middleware): void {
$middleware->api(prepend: \Illuminate\Http\Middleware\HandleCors::class);
$middleware->group('web', [
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
})
```
**راه‌حل پیشنهادی برای `AdminPanelProvider.php` بعد از تعریف گروه `web`:**
```php
->middleware([
'web',
\Filament\Http\Middleware\Authenticate::class,
])
```
---
### 🔴 مشکل حیاتی — مهاجرت داده‌های تاریخی
**مشکل:** فایل اکسل `4_5989927490271846355.xlsx` که در `storage/app/public` قرار دارد، شیت `List` را فقط برای **۲ ردیف اول داده واقعی** دارد و از ردیف ۳ به بعد **فرمول‌های Excel** است (مثل `=IF(B4<>"...",...)`).
**تأثیر:** فقط ۱ رکورد (AWB 980100010) موفقیت‌آمیز import شد. ۳۹۵۰ رکورد دیگر قابل import نیستند مگر اینکه:
1. فایل اکسل اصلی کامل (بدون فرمول) تحویل داده شود، **یا**
2. فرمول‌های Excel اعمال شوند تا مقادیر محاسبه‌شده استخراج شوند
**وضعیت:** **بلاک** — مهاجرت ۳۹۵۰ رکورد نیاز به فایل اکسل اصلاح‌شده دارد.
---
### کارهای در دست اقدام (فاز ۰)
- [ ] نصب و راه‌اندازی پروژه‌ی لاراول ۱۱ در پوشه `04_Laravel`
- [ ] نصب Filament و احراز هویت ادمین
- [ ] نوشتن Migration ها برای ۶ جدول اصلی:
- [ ] `countries` (با ۴ زون مجزا)
- [ ] `shipments` (فاز ۰ — فیلدهای حداقلی)
- [ ] `shipment_carrier_mappings`
- [ ] `shipment_tracking_events`
- [ ] `system_settings`
- [ ] `users` (با نقش‌های super_admin/tracking_operator/data_entry/customer)
- [ ] Seeder کشورها (۲۳۳ کشور با ۴ زون از شیت Zone اکسل)
- [ ] API ترکینگ: `GET /api/track/{awb_no}`
- [ ] پنل Filament با UX اپراتور ترکینگ (افزودن رویداد سریع)
- [ ] پلاگین وردپرس IFNEX Bridge با شورت‌کد `[ifnex_tracking_form]`
- [ ] اسکریپت مهاجرت ۳۹۵۰ رکورد تاریخی از شیت List اکسل
- [ ] راه‌اندازی وردپرس روی هاست مشتری
- [ ] طراحی لندینگ پیج DHL-inspired (بدون کپی)
- [ ] تست نهایی فاز ۰ و تحویل به مشتری
| اولویت | کار | وضعیت | توضیحات |
|--------|-----|-------|---------|
| 🔴 بالا | رفع مشکل middleware `web` group | نیاز به اصلاح | Code Review انجام شد، راه‌حل مشخص است |
| 🔴 بالا | تست کامل دستی Filament (ورود + CRUD) | نیاز به تست | بعد از رفع مشکل middleware انجام شود |
| 🔴 بالا | دریافت فایل اکسل اصلی List اصلاح‌شده | **بلاک** | بدون این فایل، مهاجرت ۳۹۵۰ رکورد امکان‌پذیر نیست |
| 🔴 بالا | مهاجرت ۳۹۵۰ رکورد تاریخی | **بلاک** | منتظر فایل اصلاح‌شده |
| 🟡 متوسط | مهاجرت داده‌های ترکینگ (Sheet1 + Refrence) | بلاک | فایل اکسل ترکینگ موجود نیست |
| 🟡 متوسط | ساخت پلاگین وردپرس IFNEX Bridge | نشده | بعد از تست کامل API |
| 🟡 متوسط | تست API Track با نمونه‌های بیشتر | انجام شد | ۳ تست پشت سر هم موفق |
| 🟢 پایین | نصب Dompdf/Snappy برای تولید PDF (فاز ۱) | نشده | بعد از تأیید فاز ۰ |
| 🟢 پایین | پیاده‌سازی فرم استعلام قیمت در وردپرس | نشده | بعد از تأیید فاز ۰ |
---
### کارهای فاز ۱ (پس از تأیید فاز ۰)
- [ ] موتور قیمت‌گذاری کامل (PriceCalculatorService)
- [ ] جدول `shipping_rates` با نرخ‌های Import/Export
- [ ] موتور قیمت‌گذاری کامل (PriceCalculatorService) — نوشته شد، نیاز به تست با داده‌های واقعی
- [ ] جدول `shipping_rates` — ۴۰۴ رکورد import شد، نیاز به تکمیل
- [ ] فرم ثبت سفارش آنلاین با ۹ ردیف کالای گمرکی
- [ ] تولید PDF: AWB، INVOICE، Label مطابق قالب اکسل
- [ ] ماژول ایمپورت اکسل تعرفه‌ها
@ -103,30 +252,28 @@
## 🚫 خط قرمزها (DO NOT) — هرگز این کارها را نکن
این قوانین بر اساس تجربه و تصمیمات تأییدشده‌ی مشتری تنظیم شده‌اند. نقض هر کدام = بازگشت به عقب و کار مضاعف.
### 🚫 اسکیمای دیتابیس
- **NEVER** جدول `countries` را به ۲ زون برگردانی — ۴ زون مجزا (export_parcel, export_doc, import_parcel, import_doc) الزامی است. هر کشور برای پارسل و داکیومنت زون‌های متفاوتی دارد (مثلاً افغانستان: پارسل=۷، داکیومنت=۵).
- **NEVER** فقط ۲ نوع سرویس (DOCUMENT/NON DOC) پیاده کن — ۳ نوع الزامی است: `DOC_NORMAL`، `DOC_ECONOMY`، `PARCEL` (مطابق شیت‌های DocNor، DocEco، Parcel در اکسل).
- **NEVER** فیلد `forwarder_track_id` را به‌عنوان فیلد واحد در `shipments` نگه دار — باید جدول جداگانه `shipment_carrier_mappings` ساخته شود، چون هر مرسوله ممکن است با چند شرکت حمل مرتبط باشد (مثلاً اول DHL سپس Aramex).
- **NEVER** فیلدهای مالی مهم (VAT، Domestic Pickup، Domestic Delivery، Warehousing Cost، Extra Service، Packing Cost) را حذف کن — حتی اگر در فاز ۰ استفاده نمی‌شوند، باید در Migration آماده باشند.
- **NEVER** فیلد `status` در `shipments` را به String تغییر دهی — Enum یکپارچه‌تر و امن‌تر است.
- **NEVER** جدول `countries` را به ۲ زون برگردانی — ۴ زون مجزا (export_parcel, export_doc, import_parcel, import_doc) الزامی است.
- **NEVER** فقط ۲ نوع سرویس پیاده کن — ۳ نوع الزامی است: `DOC_NORMAL`, `DOC_ECONOMY`, `PARCEL`.
- **NEVER** فیلد `forwarder_track_id` را به‌عنوان فیلد واحد در `shipments` نگه دار — باید `shipment_carrier_mappings` استفاده شود.
- **NEVER** فیلدهای مالی مهم (VAT، Domestic Pickup، Domestic Delivery، Warehousing Cost، Extra Service، Packing Cost) را حذف کن.
- **NEVER** فیلد `status` در `shipments` را به String تغییر دهی — Enum استفاده کن.
### 🚫 معماری
- **NEVER** ترکینگ را در وردپرس پیاده کن — همیشه در لاراول. وردپرس فقط نمایش می‌دهد. اگر این کار را بکنی، در فاز ۳ باید تمام داده‌ها را به لاراول مهاجرت دهی (دوباره‌کاری).
- **NEVER** ترکینگ را در وردپرس پیاده کن — همیشه در لاراول.
- **NEVER** در وردپرس پردازش داده‌ی سفارش انجام دهی — تمام فرم‌ها از طریق پلاگین IFNEX Bridge به لاراول ارسال می‌شوند.
- **NEVER** API لاراول را بدون API Key، Rate Limiting و CORS whitelist بگذاری — امنیت حیاتی است.
- **NEVER** API لاراول را بدون API Key، Rate Limiting و CORS whitelist بگذاری.
- **NEVER** از CORS `*` استفاده کنی — فقط دامنه‌ی تولیدی وردپرس باید whitelist شود.
- **NEVER** تاریخ‌ها را به شمسی در دیتابیس ذخیره کنی — همیشه به‌صورت `timestamp` میلادی. تبدیل به شمسی فقط در لایه‌ی نمایش (با `morilog/jalali`).
- **NEVER** تاریخ‌ها را به شمسی در دیتابیس ذخیره کنی — همیشه timestamp میلادی.
### 🚫 طراحی و کپی‌رایت
- **NEVER** از رنگ، لوگو یا عناصر هویت بصری DHL کپی کنی — نقض کپی‌رایت. الهام از چیدمان و UX مجاز است.
- **NEVER** خروجی PDF (AWB، Invoice، Label) را به فارسی بسازی — مطابق اکسل اصلی، PDF باید انگلیسی باشد. اما پنل ادمین و رابط کاربری فرانت‌اند فارسی است.
- **NEVER** از رنگ، لوگو یا عناصر هویت بصری DHL کپی کنی.
- **NEVER** خروجی PDF (AWB، Invoice، Label) را به فارسی بسازی — انگلیسی باشد.
### 🚫 فرآیند
- **NEVER** فایل `.env` را در Git کامیت کنی — در `.gitignore` است.
- **NEVER** فایل `.env` را در Git کامیت کنی.
- **NEVER** `APP_DEBUG=true` را در محیط تولید بگذاری.
- **NEVER** اسکوپ فاز ۰ را بدون Change Request رسمی تغییر دهی — اگر مشتری درخواست افزودن قابلیت کرد، قیمت‌گذاری جداگانه لازم است.
- **NEVER** اسکوپ فاز ۰ را بدون Change Request رسمی تغییر دهی.
- **NEVER** فاز ۱ را قبل از تأیید رسمی فاز ۰ توسط مشتری شروع کنی.
---
@ -134,54 +281,36 @@
## ❓ سوالات متداول (FAQ)
### س: کدام نسخه لاراول استفاده کنم؟
**ج:** لاراول ۱۱. اگر در PRD_v2.md نوشته «Laravel 10+» یا در Roadmap نوشته «Laravel 11»، نسخه نهایی **۱۱** است.
**ج:** لاراول **۱۱**. این نسخه در `STATUS.md` قفل شده و در `composer.json` هم تنظیم شده است.
### س: آیا PRD_v2.md هنوز معتبر است؟
**ج:** بخش‌های کلی آن (معماری Headless، توضیح کسب‌وکار، VPS پل) معتبرند. اما بخش‌های زیر قدیمی و اشتباه هستند:
- اسکیمای دیتابیس (۴.۱ تا ۴.۴) — به ۴ زون و ۳ نوع سرویس به‌روز نشده
- فازبندی — باید ۴ فازی باشد نه ۳ فازی
- ادعای «فاز ۱ تکمیل شده» — نادرست، فاز ۰ هنوز در حال اجراست
- فیلدهای مالی — VAT، Warehousing Cost، Domestic Pickup/Delivery غایب
### س: مهاجرت داده‌هایHistorical چقدر مهم است؟
**ج:** بسیار مهم. ۳۹۵۰ رکورد در شیت List وجود دارد. فایل اکسل موجود در `storage/app/public` فرمول‌های Excel دارد و فقط ۲ ردیف داده واقعی است. باید فایل اصلی کامل از مشتری دریافت شود.
برای اسکیمای دیتابیس، فقط به بخش ۶ `Phase0_Proposal.md` اعتماد کن.
### س: چه کارهایی در این جلسه انجام شد؟
**ج:**
1. بازنویسی کامل اسکیمای دیتابیس (۷ migration جدید)
2. بازنویسی تمام Model‌ها + ایجاد ۶ Enum
3. بازنویسی Seeders (countries + system_settings)
4. ایجاد API TrackController + ApiKeyMiddleware
5. بازنویسی کامل Filament Resources + RelationManagers + SettingsPage
6. ایجاد Services (TrackingService + PriceCalculatorService)
7. ایجاد Imports (ShippingRatesImport + HistoricalShipmentsImport)
8. ایجاد Artisan Commands (ifnex:import:rates, ifnex:import:shipments)
9. سوییچ به Laravel 11 + رفع همه خطاهای مربوطه
10. import ۴۰۴ shipping rate + ۱ shipment تاریخی
11. **Code Review کامل** — شناسایی مشکل middleware و ارائه راه‌حل
### س: چرا ترکینگ در لاراول است نه وردپرس؟
**ج:** چون در فاز ۳ قرار است API ترکینگ واقعی (TrackingMore/17track) متصل شود. اگر الان ترکینگ در وردپرس باشد، در فاز ۳ باید تمام داده‌ها به لاراول مهاجرت داده شوند. با ساخت آن در لاراول از ابتدا، در فاز ۳ فقط یک کلاس `TrackingSyncService` اضافه می‌شود و هیچ چیز دیگر تغییر نمی‌کند. این تصمیم در جلسه با مشتری تأیید شده است.
### س: چرا ۴ زون مجزا لازم است؟
**ج:** فایل اکسل عملیاتی نشان می‌دهد همان کشور برای پارسل و داکیومنت زون‌های متفاوتی دارد. مثلاً:
- افغانستان: پارسل=۷، داکیومنت=۵
- آلبانی: پارسل=۳، داکیومنت=۷
- استرالیا: پارسل=۷، داکیومنت=۶
اگر فقط ۲ زون (export/import) داشته باشیم، موتور قیمت‌گذاری برای DOCUMENTها اشتباه محاسبه می‌کند.
### س: چرا ۳ نوع سرویس داریم نه ۲؟
**ج:** فایل اکسل شیت‌های جداگانه دارد برای DocNor (Document Normal)، DocEco (Document Economy) و Parcel. هر کدام جدول قیمت جداگانه. پس `type` در `shipments` باید enum با سه مقدار باشد: `DOC_NORMAL`, `DOC_ECONOMY`, `PARCEL`.
### س: کدام فایل اکسل عملیاتی است؟
**ج:** دو فایل:
- **`4_5989927490271846355.xlsx`** — فایل اصلی عملیاتی شرکت با شیت‌های Form, List, COUNTRIES, AWB, INVOICE + label, label, Import Rate, Export Rate, Zone, DocNor, Parcel, DocEco, Assumptions, DATES
- **`Data entry 2026-06-28.xlsx`** — فایل ترکینگ دستی روزانه با شیت‌های Sheet1, Refrence, Paste, copy, Delivered, test
برای تحلیل کامل هر شیت، فایل `EXCEL_ANALYSIS.md` را بخوان.
### س: مهاجرت داده‌های تاریخی چقدر مهم است؟
**ج:** بسیار مهم. ۳۹۵۰ رکورد در شیت List وجود دارد از سال ۲۰۲۰ تا الان. این داده‌ها باید به جدول `shipments` مهاجرت داده شوند. بدون این کار، مشتریان قدیمی نمی‌توانند تاریخچه ببینند و اعتماد به سیستم جدید کاهش می‌یابد.
### س: آیا باید VPS پل را در فاز ۰ راه‌اندازی کنم؟
**ج:** خیر. VPS پل مخصوص فاز ۳ است. در فاز ۰ ترکینگ کاملاً دستی است (اپراتور در پنل Filament رویداد اضافه می‌کند). اما اسکیمای دیتابیس باید به‌گونه‌ای باشد که در فاز ۳ بتوان به‌سادگی API را اضافه کرد (به فیلد `source` در `shipment_tracking_events` و `last_synced_at` در `shipment_carrier_mappings` دقت کن).
### س: مشتری چه انتظاری از فاز ۰ دارد؟
**ج:** مشتری در جلسه صراحتاً گفت: «اول سایت بالا بیاید و ترکینگ دستی حل شود، بقیه بعد.» یعنی:
۱. وب‌سایت وردپرس کامل آنلاین شود
۲. مشتری نهایی بتواند با کد AWB، تایم‌لاین ترکینگ را ببیند
۳. اپراتور به‌جای اکسل، از پنل Filament استفاده کند
این سه هدف، حداقل قابل‌قبول برای تحویل فاز ۰ است.
### س: اگر باگی دیدم یا مشکل پیدا کردم چه کنم؟
**ج:** اول `EXCEL_ANALYSIS.md` و بخش «ریسک‌ها» در `Phase0_Proposal.md` را چک کن. اگر حل نشد، در گزارش کار (worklog) توضیح بده و به توسعه‌دهنده اصلی (Kazem) اطلاع بده.
### س: چه کارهایی باقی مانده؟
**ج:**
1. رفع مشکل middleware `web` group در `bootstrap/app.php`
2. ساده کردن `AdminPanelProvider.php` بعد از تعریف گروه `web`
3. تست دستی Filament (ورود + CRUD)
4. دریافت فایل اکسل اصلاح‌شده List
5. import ۳۹۵۰ رکورد تاریخی
6. ساخت پلاگین وردپرس IFNEX Bridge
7. مهاجرت داده‌های ترکینگ دستی
8. تست کامل API Track
9. تولید PDF (فاز ۱)
---
@ -193,37 +322,35 @@ cd 04_Laravel
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan db:seed --class=CountrySeeder
php artisan migrate --force
php artisan db:seed --force
php artisan serve
```
### ایجاد Model + Migration + Resource (Filament)
### Artisan Commands سفارشی
```bash
php artisan make:model Shipment -m
php artisan make:filament-resource Shipment
# Import shipping rates
php artisan ifnex:import:rates storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx
# Import historical shipments
php artisan ifnex:import:shipments storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx
```
### ایجاد API Controller
### تست API Track
```bash
php artisan make:controller Api/TrackController --api
# با API Key صحیح
curl -H "Authorization: Bearer ifnex-local-dev-key" http://localhost:8000/api/v1/track/TEST123
# بدون API Key
curl http://localhost:8000/api/v1/track/TEST123
# با API Key نامعتبر
curl -H "Authorization: Bearer wrong-key" http://localhost:8000/api/v1/track/TEST123
```
### اجرای تست
### پشتیبان‌گیری از دیتابیس
```bash
php artisan test
php artisan serve # سپس در مرورگر: http://localhost:8000/api/track/980103619
```
### مهاجرت داده‌های تاریخی (یک‌بار)
```bash
php artisan ifnex:migrate-historical-data
# این دستور باید ساخته شود — اسکریپت مخصوص خواندن شیت List اکسل
```
### پشتیبان‌گیری از دیتابیس (هر روز)
```bash
mysqldump -u root -p ifnex > backups/ifnex_$(date +%Y%m%d).sql
mysqldump -u root -p ifnex_db > backups/ifnex_$(date +%Y%m%d).sql
```
---
@ -231,21 +358,21 @@ mysqldump -u root -p ifnex > backups/ifnex_$(date +%Y%m%d).sql
## 📂 ساختار پوشه‌های پروژه (پس از تکمیل فاز ۰)
```
ifnex/
IFNEX-Logistics/
├── 01_Documents/
│ ├── STATUS.md ⭐ این فایل — اول بخوان
│ ├── Phase0_Proposal.md ⭐ مرجع اصلی پروژه
│ ├── EXCEL_ANALYSIS.md ⭐ تحلیل فایل‌های اکسل
│ ├── PRD_v2.md (قدیمی — مرجع تاریخی)
│ ├── Project_Roadmap.md (قدیمی — مرجع تاریخی)
│ └── AI_AGENT_GUIDE.md (راهنمای مخصوص AI Agents — اختیاری)
│ ├── STATUS.md ⭐ این فایل — اول بخوان
│ ├── Phase0_Proposal.md ⭐ مرجع اصلی پروژه
│ ├── EXCEL_ANALYSIS.md ⭐ تحلیل فایل‌های اکسل
│ ├── PRD_v2.md (قدیمی — مرجع تاریخی)
│ ├── Project_Roadmap.md (قدیمی — مرجع تاریخی)
│ └── IFNEX_Phase0_Checklist.md (چک‌لیست فاز ۰)
│
├── 02_Design/
│ └── Assets/ (لوگوها، آیکون‌ها، فایل‌های فیگما)
│ └── Assets/ (لوگوها، آیکون‌ها، فایل‌های فیگما)
│
├── 03_WordPress/
│ └── wp-content/plugins/
│ └── ifnex-bridge/ (پلاگین اختصاصی)
│ └── ifnex-bridge/ (پلاگین اختصاصی)
│ ├── ifnex-bridge.php
│ ├── includes/
│ │ ├── api-client.php
@ -263,32 +390,80 @@ ifnex/
│ │ │ ├── ShipmentCarrierMapping.php
│ │ │ ├── ShipmentTrackingEvent.php
│ │ │ ├── SystemSetting.php
│ │ │ ├── ShippingRate.php
│ │ │ ├── ShipmentItem.php
│ │ │ └── User.php
│ │ ├── Enums/
│ │ │ ├── ShipmentDirection.php
│ │ │ ├── ShipmentType.php
│ │ │ ├── ShipmentStatus.php
│ │ │ ├── CarrierCode.php
│ │ │ ├── TrackingSource.php
│ │ │ └── UserRole.php
│ │ ├── Services/
│ │ │ ├── TrackingService.php (فاز ۰)
│ │ │ ├── PriceCalculatorService.php (فاز ۱)
│ │ │ └── TrackingSyncService.php (فاز ۳)
│ │ ├── Http/Controllers/Api/
│ │ │ └── TrackController.php
│ │ │ ├── TrackingService.php
│ │ │ └── PriceCalculatorService.php
│ │ ├── Http/
│ │ │ ├── Controllers/
│ │ │ │ └── Api/
│ │ │ │ ├── TrackController.php
│ │ │ │ └── PricingController.php
│ │ │ └── Middleware/
│ │ │ └── ApiKeyMiddleware.php
│ │ ├── Imports/
│ │ │ ├── ShippingRatesImport.php (فاز ۱)
│ │ │ └── HistoricalShipmentsImport.php (فاز ۰)
│ │ └── Filament/
│ │ └── Resources/
│ │ ├── CountryResource.php
│ │ ├── ShipmentResource.php
│ │ └── Pages/
│ │ └── AddTrackingEvent.php (UX اختصاصی اپراتور)
│ │ │ ├── ShippingRatesImport.php
│ │ │ ├── HistoricalShipmentsImport.php
│ │ │ └── RateSheetImport.php
│ │ ├── Console/
│ │ │ └── Commands/
│ │ │ ├── ImportShippingRates.php
│ │ │ ├── ImportHistoricalData.php
│ │ │ └── DebugImportCommand.php
│ │ ├── Filament/
│ │ │ ├── Resources/
│ │ │ │ ├── CountryResource.php
│ │ │ │ ├── ShipmentResource.php
│ │ │ │ │ ├── Pages/
│ │ │ │ │ │ ├── CreateShipment.php
│ │ │ │ │ │ ├── EditShipment.php
│ │ │ │ │ │ ├── ViewShipment.php
│ │ │ │ │ │ └── ListShipments.php
│ │ │ │ │ └── RelationManagers/
│ │ │ │ │ ├── CarrierMappingsRelationManager.php
│ │ │ │ │ └── TrackingEventsRelationManager.php
│ │ │ │ ├── ShippingRateResource.php
│ │ │ │ └── ShipmentItemResource.php
│ │ │ └── Pages/
│ │ │ └── IfnexSettingsPage.php
│ │ └── Providers/
│ │ ├── AppServiceProvider.php
│ │ └── Filament/
│ │ └── AdminPanelProvider.php
│ ├── database/
│ │ ├── migrations/
│ │ │ ├── 2026_08_02_000001_create_countries_table.php
│ │ │ ├── 2026_08_02_000002_create_shipments_table.php
│ │ │ ├── 2026_08_02_000003_create_shipping_rates_table.php
│ │ │ ├── 2026_08_02_000004_create_shipment_carrier_mappings_table.php
│ │ │ ├── 2026_08_02_000005_create_shipment_tracking_events_table.php
│ │ │ ├── 2026_08_02_000006_create_system_settings_table.php
│ │ │ └── 2026_08_02_000007_update_users_table.php
│ │ └── seeders/
│ │ └── CountrySeeder.php
│ ├── routes/api.php
│ │ ├── CountriesTableSeeder.php
│ │ ├── SystemSettingSeeder.php
│ │ └── DatabaseSeeder.php
│ ├── routes/
│ │ ├── web.php
│ │ └── api.php
│ ├── config/
│ │ └── ifnex.php (تنظیمات اختصاصی)
│ │ ├── ifnex.php
│ │ └── cors.php
│ ├── bootstrap/
│ │ └── app.php
│ └── .env.example
│
├── README.md (نسخه به‌روز ۲)
├── README.md
├── composer.json
└── .gitignore
```
@ -303,9 +478,10 @@ ifnex/
۳. **`Phase0_Proposal.md`** را کامل بخوان — مرجع اصلی پروژه
۴. **`README.md`** ریشه را بخوان — برای نمای کلی
۵. کد موجود در `04_Laravel` را بررسی کن — ببین چه چیزی نوشته شده
۶. با کاربر (Kazem) هماهنگ کن — بپرس کدام کار را باید ادامه دهی
۶. **اولویت اول:** رفع مشکل middleware در `bootstrap/app.php` و `AdminPanelProvider.php` (راه‌حل در بالای این فایل آمده)
۷. با کاربر (Kazem) هماهنگ کن — بپرس کدام کار را باید ادامه دهی
**سپس کار را ادامه بده. موفق باشی! 🚀**
**مهم‌ترین کار بعدی:** رفع مشکل middleware + تست دستی Filament.
---

View File

@ -4,8 +4,8 @@ APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_LOCALE=fa
APP_FALLBACK_LOCALE=fa
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
@ -63,3 +63,7 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
IFNEX_API_KEY=
CORS_ALLOWED_ORIGINS=http://localhost,http://127.0.0.1
IFNEX_TRACKING_RATE_LIMIT=60

View File

@ -0,0 +1,21 @@
<?php
namespace App\Console\Commands;
use App\Imports\DebugImport;
use Illuminate\Console\Command;
use Maatwebsite\Excel\Facades\Excel;
class DebugImportCommand extends Command
{
protected $signature = 'ifnex:debug:import {path}';
protected $description = 'Debug import';
public function handle(): int
{
$path = $this->argument('path');
Excel::import(new DebugImport(), $path);
return 0;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Console\Commands;
use App\Imports\HistoricalShipmentsImport;
use Illuminate\Console\Command;
use Maatwebsite\Excel\Facades\Excel;
class ImportHistoricalData extends Command
{
protected $signature = 'ifnex:import:shipments {path : Path to the Excel file} {--sh|skip-header : Skip first row if it is a header}';
protected $description = 'Import historical shipments from Excel into the shipments table';
public function handle(): int
{
$path = $this->argument('path');
if (!file_exists($path)) {
$this->error("File not found: $path");
return 1;
}
$this->info('Starting historical shipments import...');
Excel::import(new HistoricalShipmentsImport(), $path);
$this->info('Historical shipments import completed.');
return 0;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Console\Commands;
use App\Imports\ShippingRatesImport;
use Illuminate\Console\Command;
use Maatwebsite\Excel\Facades\Excel;
class ImportShippingRates extends Command
{
protected $signature = 'ifnex:import:rates {path : Path to the Excel file}';
protected $description = 'Import shipping rates from Excel into the shipping_rates table';
public function handle(): int
{
$path = $this->argument('path');
if (!file_exists($path)) {
$this->error("File not found: $path");
return 1;
}
$this->info('Starting shipping rates import...');
Excel::import(new ShippingRatesImport(), $path);
$this->info('Shipping rates import completed.');
return 0;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Enums;
enum CarrierCode: string
{
case DHL = 'DHL';
case FEDEX = 'FEDEX';
case UPS = 'UPS';
case ARAMEX = 'ARAMEX';
case NAGHEL = 'NAGHEL';
case EMX = 'EMX';
case APSITEX = 'APSITEX';
case IMPEX = 'IMPEX';
case OTHER = 'OTHER';
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum ShipmentDirection: string
{
case Import = 'import';
case Export = 'export';
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Enums;
enum ShipmentStatus: string
{
case Processed = 'processed';
case PickedUp = 'picked_up';
case InTransit = 'in_transit';
case OutForDelivery = 'out_for_delivery';
case Failed = 'failed';
case Delivered = 'delivered';
case Returned = 'returned';
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum ShipmentType: string
{
case DocNormal = 'DOC_NORMAL';
case DocEconomy = 'DOC_ECONOMY';
case Parcel = 'PARCEL';
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum TrackingSource: string
{
case Manual = 'manual';
case ApiCarrier = 'api_carrier';
case ApiAggregator = 'api_aggregator';
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum UserRole: string
{
case SuperAdmin = 'super_admin';
case TrackingOperator = 'tracking_operator';
case DataEntry = 'data_entry';
case Customer = 'customer';
}

View File

@ -0,0 +1,109 @@
<?php
namespace App\Filament\Pages;
use App\Models\SystemSetting;
use Filament\Forms;
use Filament\Pages\Page;
class IfnexSettingsPage extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
protected static ?string $navigationLabel = 'Settings';
protected static ?string $navigationGroup = 'Settings';
protected static ?int $navigationSort = 1;
protected static string $view = 'filament::page';
public function mount(): void
{
$this->fillForms();
}
protected function getFormSchema(): array
{
return [
Forms\Components\TextInput::make('vat_rate')
->label('VAT Rate')
->required()
->numeric()
->step(0.01)
->default(0.09)
->helperText('Example: 0.09 for 9%')
->live(),
Forms\Components\TextInput::make('packing_cost_default')
->label('Default Packing Cost (IRR)')
->required()
->numeric()
->default(100000),
Forms\Components\TextInput::make('profit_margin')
->label('Profit Margin')
->required()
->numeric()
->step(0.01)
->default(1.25)
->helperText('Example: 1.25 for 25% margin'),
Forms\Components\TextInput::make('aed_to_irr')
->label('AED to IRR')
->required()
->numeric()
->default(455000),
Forms\Components\TextInput::make('usd_to_irr')
->label('USD to IRR')
->required()
->numeric()
->default(0),
Forms\Components\TextInput::make('cny_to_irr')
->label('CNY to IRR')
->required()
->numeric()
->default(0),
Forms\Components\TextInput::make('eur_to_irr')
->label('EUR to IRR')
->required()
->numeric()
->default(0),
];
}
protected function mutateFormDataBeforeSave(array $data): array
{
$userId = auth()->id();
foreach ($data as $key => $value) {
if (is_numeric($value)) {
SystemSetting::updateOrCreate(
['key' => $key],
['value' => $value, 'updated_by' => $userId]
);
}
}
return [];
}
protected function fillForms(): void
{
$settings = SystemSetting::all()->pluck('value', 'key')->toArray();
$defaults = [
'vat_rate' => 0.09,
'packing_cost_default' => 100000,
'profit_margin' => 1.25,
'aed_to_irr' => 455000,
'usd_to_irr' => 0,
'cny_to_irr' => 0,
'eur_to_irr' => 0,
];
$this->data = array_merge($defaults, $settings);
}
protected function getFormStatePath(): string
{
return 'data';
}
}

View File

@ -1,80 +0,0 @@
<?php
namespace App\Filament\Pages;
use App\Models\SystemSetting;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
class SettingsPage extends Page
{
use InteractsWithForms;
protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
protected static string $view = 'filament.pages.settings-page';
// نام منو در سایدبار
protected static ?string $navigationLabel = 'تنظیمات ارز و سود';
// اینکه کجا تو منو نمایش داده بشه (ترتیب)
protected static ?int $navigationSort = 100;
public ?SystemSetting $record = null;
// وقتی صفحه باز میشه، رکورد تنظیمات رو می‌گیره
public function mount(): void
{
$this->record = SystemSetting::first();
}
// ساختار فرم
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('dirham_rate')
->label('نرخ درهم')
->numeric()
->required()
->prefix('ریال'),
TextInput::make('dollar_rate')
->label('نرخ دلار')
->numeric()
->prefix('ریال'),
TextInput::make('yuan_rate')
->label('نرخ یوان')
->numeric()
->prefix('ریال'),
TextInput::make('euro_rate')
->label('نرخ یورو')
->numeric()
->prefix('ریال'),
TextInput::make('profit_percent')
->label('ضریب سود شرکت')
->numeric()
->required()
->suffix('ضرب')
->default(1.25),
])
->statePath('record');
}
// دکمه ذخیره
public function save(): void
{
$this->record->save();
Notification::make()
->title('ذخیره شد')
->body('تنظیمات نرخ ارز با موفقیت آپدیت شدند.')
->success()
->send();
}
}

View File

@ -14,9 +14,11 @@ class CountryResource extends Resource
{
protected static ?string $model = Country::class;
// تغییر آیکون به یک نماد مناسب‌تر برای کشورها
protected static ?string $navigationIcon = 'heroicon-o-globe-alt';
protected static ?string $navigationLabel = 'Countries';
protected static ?string $navigationGroup = 'Settings';
public static function form(Form $form): Form
{
return $form
@ -25,12 +27,42 @@ class CountryResource extends Resource
->required()
->maxLength(255)
->label('Country Name'),
Forms\Components\TextInput::make('export_zone')
Forms\Components\TextInput::make('iso_code')
->required()
->maxLength(50),
Forms\Components\TextInput::make('import_zone')
->required()
->maxLength(50),
->maxLength(2)
->unique(ignoreRecord: true)
->label('ISO Code'),
Forms\Components\Grid::make()
->schema([
Forms\Components\TextInput::make('export_zone_parcel')
->required()
->numeric()
->minValue(1)
->maxValue(10)
->label('Export Zone (Parcel)'),
Forms\Components\TextInput::make('export_zone_doc')
->required()
->numeric()
->minValue(1)
->maxValue(10)
->label('Export Zone (Doc)'),
Forms\Components\TextInput::make('import_zone_parcel')
->required()
->numeric()
->minValue(1)
->maxValue(10)
->label('Import Zone (Parcel)'),
Forms\Components\TextInput::make('import_zone_doc')
->required()
->numeric()
->minValue(1)
->maxValue(10)
->label('Import Zone (Doc)'),
])
->columns(4),
Forms\Components\Toggle::make('is_active')
->label('Active')
->default(true),
]);
}
@ -38,13 +70,16 @@ class CountryResource extends Resource
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')->searchable()->label('Country'),
Tables\Columns\TextColumn::make('export_zone')->searchable(),
Tables\Columns\TextColumn::make('import_zone')->searchable(),
Tables\Columns\TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('name')->searchable()->sortable()->label('Country'),
Tables\Columns\TextColumn::make('iso_code')->searchable()->sortable()->label('ISO'),
Tables\Columns\TextColumn::make('export_zone_parcel')->label('Export Parcel'),
Tables\Columns\TextColumn::make('export_zone_doc')->label('Export Doc'),
Tables\Columns\TextColumn::make('import_zone_parcel')->label('Import Parcel'),
Tables\Columns\TextColumn::make('import_zone_doc')->label('Import Doc'),
Tables\Columns\IconColumn::make('is_active')->boolean()->label('Active'),
])
->filters([
//
Tables\Filters\TernaryFilter::make('is_active')->label('Active Status'),
])
->actions([
Tables\Actions\EditAction::make(),

View File

@ -14,9 +14,11 @@ class ShipmentItemResource extends Resource
{
protected static ?string $model = ShipmentItem::class;
// تغییر آیکون به نماد مناسب‌تر برای اقلام کالا
protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-list';
protected static ?string $navigationLabel = 'Shipment Items';
protected static ?string $navigationGroup = 'Operations';
public static function form(Form $form): Form
{
return $form
@ -36,21 +38,25 @@ class ShipmentItemResource extends Resource
Forms\Components\TextInput::make('description')
->required()
->maxLength(255)
->columnSpan(2),
->columnSpan(2)
->label('Description'),
Forms\Components\TextInput::make('hs_code')
->required()
->label('HS Code'),
Forms\Components\TextInput::make('quantity')
->required()
->numeric(),
->numeric()
->label('Quantity'),
Forms\Components\TextInput::make('unit_price')
->required()
->numeric()
->prefix('USD'),
->prefix('USD')
->label('Unit Price'),
Forms\Components\TextInput::make('total_usd')
->required()
->numeric()
->prefix('USD'),
->prefix('USD')
->label('Total USD'),
])->columns(4);
}
@ -67,16 +73,23 @@ class ShipmentItemResource extends Resource
->label('Row'),
Tables\Columns\TextColumn::make('description')
->searchable()
->limit(30),
->limit(30)
->label('Description'),
Tables\Columns\TextColumn::make('hs_code')
->searchable()
->label('HS Code'),
Tables\Columns\TextColumn::make('quantity')
->numeric()
->sortable(),
->sortable()
->label('Qty'),
Tables\Columns\TextColumn::make('unit_price')
->money('USD')
->sortable()
->label('Unit Price'),
Tables\Columns\TextColumn::make('total_usd')
->money('USD')
->sortable(),
->sortable()
->label('Total'),
])
->filters([
//

View File

@ -9,134 +9,113 @@ use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use App\Imports\ShippingRatesImport;
use Maatwebsite\Excel\Facades\Excel;
use Filament\Actions\Action;
use Filament\Forms\Components\FileUpload;
class ShipmentResource extends Resource
{
protected static ?string $model = Shipment::class;
// تغییر آیکون به نماد کامیون/ارسال
protected static ?string $navigationIcon = 'heroicon-o-truck';
protected static ?string $navigationLabel = 'Shipments';
protected static ?string $navigationGroup = 'Operations';
public static function form(Form $form): Form
{
return $form
->schema([
// بخش ۱: اطلاعات مسیر و وضعیت
Forms\Components\Section::make('Route & Status Info')
Forms\Components\Section::make('Route & Status')
->schema([
Forms\Components\Select::make('user_id')
->relationship('user', 'name')
->searchable()
->preload()
->required(),
Forms\Components\TextInput::make('awb_no')
->required()
->unique(ignoreRecord: true)
->label('AWB No.'),
Forms\Components\Select::make('direction')
->options(['Outbound' => 'Outbound', 'Inbound' => 'Inbound'])
->options(['export' => 'Export', 'import' => 'Import'])
->required(),
Forms\Components\Select::make('type')
->options(['DOC' => 'DOC', 'NON DOC' => 'NON DOC'])
->options([
'DOC_NORMAL' => 'Doc Normal',
'DOC_ECONOMY' => 'Doc Economy',
'PARCEL' => 'Parcel',
])
->required(),
Forms\Components\TextInput::make('status')
Forms\Components\Select::make('status')
->options([
'processed' => 'Processed',
'picked_up' => 'Picked Up',
'in_transit' => 'In Transit',
'out_for_delivery' => 'Out For Delivery',
'failed' => 'Failed',
'delivered' => 'Delivered',
'returned' => 'Returned',
])
->default('processed')
->required(),
Forms\Components\TextInput::make('reason_for_export')
->required(),
Forms\Components\TextInput::make('forwarder')
->nullable(),
Forms\Components\TextInput::make('forwarder_track_id')
->nullable(),
])->columns(2),
// بخش ۲: اطلاعات وزن و ابعاد
Forms\Components\Section::make('Weight & Dimensions')
->schema([
Forms\Components\TextInput::make('weight')
->numeric()
->required()
->suffix('kg'),
Forms\Components\TextInput::make('volumetric_weight')
->numeric()
->required()
->suffix('kg'),
Forms\Components\TextInput::make('chargeable_weight')
->numeric()
->required()
->suffix('kg'),
Forms\Components\TextInput::make('dimensions')
->required()
->helperText('e.g. 10x20x30'),
])->columns(2),
// بخش ۳: اطلاعات مالی
Forms\Components\Section::make('Financial Info')
->schema([
Forms\Components\TextInput::make('shipping_price')
->numeric()
->required()
->prefix('درهم'),
Forms\Components\TextInput::make('extra_service')
->numeric()
->default(0)
->prefix('درهم'),
Forms\Components\TextInput::make('packing_cost')
->numeric()
->default(0)
->prefix('درهم'),
Forms\Components\TextInput::make('discount')
->numeric()
->default(0)
->prefix('درهم'),
Forms\Components\TextInput::make('total_fee')
->numeric()
->required()
->prefix('درهم'),
Forms\Components\TextInput::make('net_dirham')
->numeric()
->required()
->prefix('درهم'),
Forms\Components\TextInput::make('net_rial')
->numeric()
->required()
->suffix('ریال'),
Forms\Components\TextInput::make('reason_for_export')->nullable()->label('Reason for Export'),
Forms\Components\TextInput::make('forwarder')->nullable()->label('Forwarder'),
Forms\Components\Select::make('from_country_id')
->relationship('fromCountry', 'name')
->searchable()
->preload()
->label('From Country'),
Forms\Components\Select::make('to_country_id')
->relationship('toCountry', 'name')
->searchable()
->preload()
->label('To Country'),
])->columns(3),
// بخش ۴: اطلاعات فرستنده
Forms\Components\Section::make('Weight & Dimensions')
->schema([
Forms\Components\TextInput::make('weight')->numeric()->nullable()->suffix('kg')->label('Weight'),
Forms\Components\TextInput::make('volumetric_weight')->numeric()->nullable()->suffix('kg')->label('Volumetric Weight'),
Forms\Components\TextInput::make('chargeable_weight')->numeric()->nullable()->suffix('kg')->label('Chargeable Weight'),
Forms\Components\TextInput::make('dimensions')->nullable()->label('Dimensions (WxLxH)'),
])->columns(4),
Forms\Components\Section::make('Financial Info')
->schema([
Forms\Components\TextInput::make('shipping_price')->numeric()->nullable()->prefix('درهم')->label('Shipping Price'),
Forms\Components\TextInput::make('extra_service')->numeric()->default(0)->prefix('درهم')->label('Extra Service'),
Forms\Components\TextInput::make('packing_cost')->numeric()->default(0)->prefix('ریال')->label('Packing Cost'),
Forms\Components\TextInput::make('domestic_pickup')->numeric()->default(0)->prefix('ریال')->label('Domestic Pickup'),
Forms\Components\TextInput::make('domestic_delivery')->numeric()->default(0)->prefix('ریال')->label('Domestic Delivery'),
Forms\Components\TextInput::make('warehousing_cost')->numeric()->default(0)->prefix('ریال')->label('Warehousing Cost'),
Forms\Components\TextInput::make('vat_amount')->numeric()->default(0)->prefix('ریال')->label('VAT Amount'),
Forms\Components\TextInput::make('discount')->numeric()->default(0)->prefix('ریال')->label('Discount'),
Forms\Components\TextInput::make('total_fee')->numeric()->nullable()->prefix('ریال')->label('Total Fee'),
Forms\Components\TextInput::make('net_dirham')->numeric()->nullable()->prefix('درهم')->label('Net Dirham'),
Forms\Components\TextInput::make('net_rial')->numeric()->nullable()->prefix('ریال')->label('Net Rial'),
])->columns(4),
Forms\Components\Section::make('Sender Info')
->schema([
Forms\Components\TextInput::make('sender_name')->required(),
Forms\Components\TextInput::make('sender_company')->required(),
Forms\Components\TextInput::make('sender_phone')->required()->tel(),
Forms\Components\TextInput::make('sender_email')->required()->email(),
Forms\Components\TextInput::make('sender_address')->required(),
Forms\Components\TextInput::make('sender_city')->required(),
Forms\Components\TextInput::make('sender_zip')->required(),
Forms\Components\TextInput::make('sender_id_number')->required(),
])->columns(2),
Forms\Components\TextInput::make('sender_name')->nullable()->label('Name'),
Forms\Components\TextInput::make('sender_company')->nullable()->label('Company'),
Forms\Components\TextInput::make('sender_phone')->tel()->nullable()->label('Phone'),
Forms\Components\TextInput::make('sender_email')->email()->nullable()->label('Email'),
Forms\Components\TextInput::make('sender_address')->nullable()->label('Address'),
Forms\Components\TextInput::make('sender_city')->nullable()->label('City'),
Forms\Components\TextInput::make('sender_zip')->nullable()->label('ZIP'),
Forms\Components\TextInput::make('sender_id_number')->nullable()->label('ID Number'),
])->columns(4),
// بخش ۵: اطلاعات گیرنده
Forms\Components\Section::make('Receiver Info')
->schema([
Forms\Components\TextInput::make('receiver_name')->required(),
Forms\Components\TextInput::make('receiver_company')->required(),
Forms\Components\TextInput::make('receiver_phone')->required()->tel(),
Forms\Components\TextInput::make('receiver_email')->required()->email(),
Forms\Components\TextInput::make('receiver_address')->required(),
Forms\Components\TextInput::make('receiver_city')->required(),
Forms\Components\TextInput::make('receiver_zip')->required(),
Forms\Components\TextInput::make('receiver_id_number')->required(),
])->columns(2),
Forms\Components\TextInput::make('receiver_name')->nullable()->label('Name'),
Forms\Components\TextInput::make('receiver_company')->nullable()->label('Company'),
Forms\Components\TextInput::make('receiver_phone')->tel()->nullable()->label('Phone'),
Forms\Components\TextInput::make('receiver_email')->email()->nullable()->label('Email'),
Forms\Components\TextInput::make('receiver_address')->nullable()->label('Address'),
Forms\Components\TextInput::make('receiver_city')->nullable()->label('City'),
Forms\Components\TextInput::make('receiver_zip')->nullable()->label('ZIP'),
Forms\Components\TextInput::make('receiver_id_number')->nullable()->label('ID Number'),
])->columns(4),
// بخش ۶: اقلام گمرکی (Repeater)
Forms\Components\Section::make('Customs Items')
->schema([
Forms\Components\Repeater::make('items')
->relationship() // استفاده از رابطه items در مدل Shipment
->relationship()
->schema([
Forms\Components\TextInput::make('row_number')
->required()
@ -146,27 +125,32 @@ class ShipmentResource extends Resource
->label('Row'),
Forms\Components\TextInput::make('description')
->required()
->columnSpan(2),
->columnSpan(2)
->label('Description'),
Forms\Components\TextInput::make('hs_code')
->required()
->label('HS Code'),
Forms\Components\TextInput::make('quantity')
->required()
->numeric(),
->numeric()
->label('Qty'),
Forms\Components\TextInput::make('unit_price')
->required()
->numeric()
->prefix('USD'),
->prefix('USD')
->label('Unit Price'),
Forms\Components\TextInput::make('total_usd')
->required()
->numeric()
->prefix('USD'),
])->columns(4) // چیدمان ۴ ستونه برای فیلدهای داخل ریپیتر
->maxItems(9) // محدودیت ثبت حداکثر ۹ ردیف مطابق سند
->prefix('USD')
->label('Total'),
])
->columns(6)
->maxItems(9)
->defaultItems(1)
->reorderable()
->label(''),
])
->label('Items'),
]),
]);
}
@ -174,38 +158,41 @@ class ShipmentResource extends Resource
{
return $table
->columns([
Tables\Columns\TextColumn::make('awb_no')
->searchable()
->label('AWB No.')
->sortable(),
Tables\Columns\TextColumn::make('direction')
->badge()
->color(fn (string $state): string => match ($state) {
'Inbound' => 'success',
'Outbound' => 'info',
default => 'gray',
}),
Tables\Columns\TextColumn::make('status')
->badge()
->searchable(),
Tables\Columns\TextColumn::make('sender_name')
->searchable()
->toggleable(),
Tables\Columns\TextColumn::make('receiver_name')
->searchable()
->toggleable(),
Tables\Columns\TextColumn::make('total_fee')
->money('AED')
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('awb_no')->searchable()->label('AWB No.')->sortable(),
Tables\Columns\TextColumn::make('direction')->badge()->color(fn (string $state): string => match ($state) {
'import' => 'success',
'export' => 'info',
default => 'gray',
})->label('Direction'),
Tables\Columns\TextColumn::make('type')->badge()->label('Type'),
Tables\Columns\TextColumn::make('status')->badge()->searchable()->label('Status'),
Tables\Columns\TextColumn::make('sender_name')->searchable()->toggleable()->label('Sender'),
Tables\Columns\TextColumn::make('receiver_name')->searchable()->toggleable()->label('Receiver'),
Tables\Columns\TextColumn::make('total_fee')->money('IRR')->sortable()->label('Total Fee'),
Tables\Columns\TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
Tables\Filters\SelectFilter::make('status')
->options([
'processed' => 'Processed',
'picked_up' => 'Picked Up',
'in_transit' => 'In Transit',
'out_for_delivery' => 'Out For Delivery',
'failed' => 'Failed',
'delivered' => 'Delivered',
'returned' => 'Returned',
]),
Tables\Filters\SelectFilter::make('type')
->options([
'DOC_NORMAL' => 'Doc Normal',
'DOC_ECONOMY' => 'Doc Economy',
'PARCEL' => 'Parcel',
]),
Tables\Filters\SelectFilter::make('direction')
->options(['export' => 'Export', 'import' => 'Import']),
])
->actions([
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
])
->bulkActions([
@ -218,7 +205,8 @@ class ShipmentResource extends Resource
public static function getRelations(): array
{
return [
//
ShipmentResource\RelationManagers\CarrierMappingsRelationManager::class,
ShipmentResource\RelationManagers\TrackingEventsRelationManager::class,
];
}
@ -227,6 +215,7 @@ class ShipmentResource extends Resource
return [
'index' => Pages\ListShipments::route('/'),
'create' => Pages\CreateShipment::route('/create'),
'view' => Pages\ViewShipment::route('/{record}'),
'edit' => Pages\EditShipment::route('/{record}/edit'),
];
}

View File

@ -9,4 +9,9 @@ use Filament\Resources\Pages\CreateRecord;
class CreateShipment extends CreateRecord
{
protected static string $resource = ShipmentResource::class;
protected function getRedirectUrl(): string
{
return static::$resource::getUrl('index');
}
}

View File

@ -3,12 +3,8 @@
namespace App\Filament\Resources\ShipmentResource\Pages;
use App\Filament\Resources\ShipmentResource;
use App\Imports\OldShipmentsImport; // اضافه شدن کلاس ایمپورت صحیح
use Filament\Actions;
use Filament\Actions\Action; // اضافه شدن اکشن
use Filament\Forms\Components\FileUpload; // اضافه شدن فایل آپلود
use Filament\Resources\Pages\ListRecords;
use Maatwebsite\Excel\Facades\Excel;
class ListShipments extends ListRecords
{
@ -17,35 +13,6 @@ class ListShipments extends ListRecords
protected function getHeaderActions(): array
{
return [
// دکمه ایمپورت اکسل قدیمی
Action::make('import-old-shipments')
->label('آپلود اکسل سفارشات قدیمی')
->icon('heroicon-o-arrow-up-tray')
->color('warning') // رنگ زرد برای قدیمی بودن
->form([
FileUpload::make('file')
->label('فایل اکسل شیت List')
->required()
->directory('excel') // ذخیره موقت در این پوشه
->acceptedFileTypes([
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // اصلاح میم‌تایپ xlsx
'application/vnd.ms-excel' // xls
])
])
->action(function (array $data) {
// پاس دادن مسیر فایل آپلود شده به کلاس ایمپورت
Excel::import(new OldShipmentsImport, $data['file']);
\Illuminate\Support\Facades\Cache::forget('shipments_list');
\Filament\Notifications\Notification::make()
->title('عملیات موفق')
->body('سفارشات با موفقیت از فایل اکسل وارد دیتابیس شدند.')
->success()
->send();
}),
// دکمه‌های پیش‌فرض
Actions\CreateAction::make(),
];
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Filament\Resources\ShipmentResource\Pages;
use App\Filament\Resources\ShipmentResource;
use Filament\Actions;
use Filament\Resources\Pages\ViewRecord;
class ViewShipment extends ViewRecord
{
protected static string $resource = ShipmentResource::class;
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Filament\Resources\ShipmentResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class CarrierMappingsRelationManager extends RelationManager
{
protected static string $relationship = 'carrierMappings';
protected static ?string $title = 'Carrier Mappings';
protected static ?string $icon = 'heroicon-o-truck';
public function form(Form $form): Form
{
return $form->schema([
Forms\Components\Select::make('carrier_code')
->options([
'DHL' => 'DHL',
'FEDEX' => 'FedEx',
'UPS' => 'UPS',
'ARAMEX' => 'Aramex',
'NAGHEL' => 'Naqel',
'EMX' => 'EMX',
'APSITEX' => 'APSITEX',
'IMPEX' => 'IMPEX',
'OTHER' => 'Other',
])
->required(),
Forms\Components\TextInput::make('carrier_tracking_number')->required()->label('Tracking Number'),
Forms\Components\TextInput::make('booking_number')->nullable()->label('Booking Number'),
Forms\Components\Select::make('direction')
->options(['import' => 'Import', 'export' => 'Export'])
->nullable(),
Forms\Components\Select::make('delivery_status')
->options([
'processed' => 'Processed',
'picked_up' => 'Picked Up',
'in_transit' => 'In Transit',
'out_for_delivery' => 'Out For Delivery',
'failed' => 'Failed',
'delivered' => 'Delivered',
'returned' => 'Returned',
])
->nullable(),
Forms\Components\Textarea::make('notes')->nullable()->rows(2)->label('Notes'),
]);
}
public function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('carrier_code')->badge()->label('Carrier'),
Tables\Columns\TextColumn::make('carrier_tracking_number')->label('Tracking Number')->searchable(),
Tables\Columns\TextColumn::make('booking_number')->label('Booking Number'),
Tables\Columns\TextColumn::make('direction')->badge()->label('Direction'),
Tables\Columns\TextColumn::make('delivery_status')->badge()->label('Status'),
Tables\Columns\TextColumn::make('notes')->limit(50)->label('Notes'),
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Filament\Resources\ShipmentResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class TrackingEventsRelationManager extends RelationManager
{
protected static string $relationship = 'trackingEvents';
protected static ?string $title = 'Tracking Events';
protected static ?string $icon = 'heroicon-o-clock';
public function form(Form $form): Form
{
return $form->schema([
Forms\Components\DatePicker::make('event_date')
->required()
->default(now())
->label('Event Date'),
Forms\Components\TimePicker::make('event_time')
->required()
->default(now())
->label('Event Time'),
Forms\Components\TextInput::make('location')->nullable()->label('Location'),
Forms\Components\TextInput::make('event_description')->required()->label('Event Description'),
Forms\Components\Select::make('delivery_status')
->options([
'processed' => 'Processed',
'picked_up' => 'Picked Up',
'in_transit' => 'In Transit',
'out_for_delivery' => 'Out For Delivery',
'failed' => 'Failed',
'delivered' => 'Delivered',
'returned' => 'Returned',
])
->nullable()
->label('Delivery Status'),
]);
}
public function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('event_date')->date()->label('Date')->sortable(),
Tables\Columns\TextColumn::make('event_time')->time()->label('Time'),
Tables\Columns\TextColumn::make('location')->label('Location'),
Tables\Columns\TextColumn::make('event_description')->label('Description')->limit(60),
Tables\Columns\TextColumn::make('delivery_status')->badge()->label('Status'),
Tables\Columns\TextColumn::make('source')->badge()->label('Source'),
])
->defaultSort('event_date', 'desc')
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
}

View File

@ -16,20 +16,21 @@ class ShippingRateResource extends Resource
protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
protected static ?string $navigationLabel = 'Shipping Rates';
protected static ?string $navigationGroup = 'Settings';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Select::make('direction')
->options([
'import' => 'Import',
'export' => 'Export',
])
->options(['export' => 'Export', 'import' => 'Import'])
->required(),
Forms\Components\Select::make('type')
->options([
'DOCUMENT' => 'Document',
'NON DOC' => 'Non Document',
'DOC_NORMAL' => 'Doc Normal',
'DOC_ECONOMY' => 'Doc Economy',
'PARCEL' => 'Parcel',
])
->required(),
Forms\Components\TextInput::make('weight')
@ -37,42 +38,50 @@ class ShippingRateResource extends Resource
->numeric()
->suffix('kg')
->label('Weight (kg)'),
// بخش زون‌ها بر اساس فایل Import
Forms\Components\Section::make('Zone Prices')
->schema(array_map(fn ($i) =>
Forms\Components\TextInput::make("zone_{$i}")
->numeric()
->prefix('درهم')
->label("Zone {$i}")
->columnSpan(1),
range(1, 10)
))->columns(5),
])->columns(3);
Forms\Components\Grid::make()
->schema([
Forms\Components\TextInput::make('zone_1')->numeric()->label('Zone 1'),
Forms\Components\TextInput::make('zone_2')->numeric()->label('Zone 2'),
Forms\Components\TextInput::make('zone_3')->numeric()->label('Zone 3'),
Forms\Components\TextInput::make('zone_4')->numeric()->label('Zone 4'),
Forms\Components\TextInput::make('zone_5')->numeric()->label('Zone 5'),
Forms\Components\TextInput::make('zone_6')->numeric()->label('Zone 6'),
Forms\Components\TextInput::make('zone_7')->numeric()->label('Zone 7'),
Forms\Components\TextInput::make('zone_8')->numeric()->label('Zone 8'),
Forms\Components\TextInput::make('zone_9')->numeric()->label('Zone 9'),
Forms\Components\TextInput::make('zone_10')->numeric()->label('Zone 10'),
])
->columns(5),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('direction')
->badge()
->color(fn (string $state): string => $state === 'import' ? 'info' : 'success')
->searchable(),
Tables\Columns\TextColumn::make('type')
->badge()
->searchable(),
Tables\Columns\TextColumn::make('weight')
->numeric()
->sortable()
->suffix(' kg'),
Tables\Columns\TextColumn::make('zone_1')
->money('AED')
->label('Zone 1'),
Tables\Columns\TextColumn::make('direction')->badge()->label('Direction'),
Tables\Columns\TextColumn::make('type')->badge()->label('Type'),
Tables\Columns\TextColumn::make('weight')->numeric()->suffix(' kg')->label('Weight'),
Tables\Columns\TextColumn::make('zone_1')->label('Z1')->sortable(),
Tables\Columns\TextColumn::make('zone_2')->label('Z2')->sortable(),
Tables\Columns\TextColumn::make('zone_3')->label('Z3')->sortable(),
Tables\Columns\TextColumn::make('zone_4')->label('Z4')->sortable(),
Tables\Columns\TextColumn::make('zone_5')->label('Z5')->sortable(),
Tables\Columns\TextColumn::make('zone_6')->label('Z6')->sortable(),
Tables\Columns\TextColumn::make('zone_7')->label('Z7')->sortable(),
Tables\Columns\TextColumn::make('zone_8')->label('Z8')->sortable(),
Tables\Columns\TextColumn::make('zone_9')->label('Z9')->sortable(),
Tables\Columns\TextColumn::make('zone_10')->label('Z10')->sortable(),
])
->filters([
Tables\Filters\SelectFilter::make('direction'),
Tables\Filters\SelectFilter::make('type'),
Tables\Filters\SelectFilter::make('direction')
->options(['export' => 'Export', 'import' => 'Import']),
Tables\Filters\SelectFilter::make('type')
->options([
'DOC_NORMAL' => 'Doc Normal',
'DOC_ECONOMY' => 'Doc Economy',
'PARCEL' => 'Parcel',
]),
])
->actions([
Tables\Actions\EditAction::make(),

View File

@ -2,31 +2,40 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\PriceCalculatorService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class PricingController extends Controller
class PricingController
{
public function calculate(Request $request)
public function __construct(protected PriceCalculatorService $calculator) {}
public function calculate(Request $request): JsonResponse
{
$validated = $request->validate([
'direction' => 'required|in:import,export',
'type' => 'required|in:DOCUMENT,NON DOC',
'origin_country_id' => 'required|exists:countries,id',
'destination_country_id' => 'required|exists:countries,id',
'weight' => 'required|numeric|min:0.1',
'length' => 'required|numeric|min:1',
'width' => 'required|numeric|min:1',
'height' => 'required|numeric|min:1',
]);
try {
$validated = $request->validate([
'direction' => ['required', 'string', 'in:Outbound,Inbound'],
'type' => ['required', 'string', 'in:DOC_NORMAL,DOC_ECONOMY,PARCEL'],
'country_iso' => ['required', 'string', 'size:2'],
'weight' => ['required', 'numeric', 'min:0'],
'volumetric_weight' => ['required', 'numeric', 'min:0'],
'extra_service' => ['nullable', 'numeric', 'min:0'],
'packing_cost' => ['nullable', 'numeric', 'min:0'],
'domestic_pickup' => ['nullable', 'numeric', 'min:0'],
'domestic_delivery' => ['nullable', 'numeric', 'min:0'],
'warehousing_cost' => ['nullable', 'numeric', 'min:0'],
'discount' => ['nullable', 'numeric', 'min:0'],
]);
} catch (ValidationException $e) {
return response()->json([
'message' => 'Validation failed',
'errors' => $e->errors(),
], 422);
}
$service = new PriceCalculatorService();
$result = $service->calculate($validated);
$result = $this->calculator->calculate($validated);
return response()->json([
'success' => $result['status'] === 'success',
'data' => $result
]);
return response()->json($result);
}
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\Shipment;
use App\Services\TrackingService;
use Illuminate\Http\JsonResponse;
class TrackController
{
public function __construct(protected TrackingService $tracking) {}
public function show(string $awb_no): JsonResponse
{
$shipment = Shipment::where('awb_no', $awb_no)->firstOrFail();
$events = $shipment->trackingEvents()
->orderByDesc('event_date')
->orderByDesc('event_time')
->get();
$mappings = $shipment->carrierMappings;
return response()->json([
'awb_no' => $shipment->awb_no,
'direction' => $shipment->direction->value,
'type' => $shipment->type->value,
'status' => $shipment->status->value,
'sender' => [
'name' => $shipment->sender_name,
'company' => $shipment->sender_company,
'phone' => $shipment->sender_phone,
'city' => $shipment->sender_city,
],
'receiver' => [
'name' => $shipment->receiver_name,
'company' => $shipment->receiver_company,
'phone' => $shipment->receiver_phone,
'city' => $shipment->receiver_city,
],
'carrier_mappings' => $mappings->map(fn ($m) => [
'carrier_code' => $m->carrier_code->value,
'tracking_number' => $m->carrier_tracking_number,
'booking_number' => $m->booking_number,
'delivery_status' => $m->delivery_status?->value,
'notes' => $m->notes,
]),
'timeline' => $events->map(fn ($e) => [
'date' => $e->event_date->format('Y-m-d'),
'time' => $e->event_time->format('H:i'),
'location' => $e->location,
'description' => $e->event_description,
'delivery_status' => $e->delivery_status?->value,
'source' => $e->source->value,
]),
]);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ApiKeyMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$apiKey = $request->header('Authorization');
if (!$apiKey || !str_starts_with($apiKey, 'Bearer ')) {
return response()->json(['message' => 'Unauthorized'], 401);
}
$token = substr($apiKey, 7);
if ($token !== config('ifnex.api_key')) {
return response()->json(['message' => 'Forbidden'], 403);
}
return $next($request);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class SetCspHeaders
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
$response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.bunny.net https://fonts.bunny.net https://*.bunny.net; style-src 'self' 'unsafe-inline' https://cdn.bunny.net https://fonts.bunny.net https://*.bunny.net; font-src 'self' https://cdn.bunny.net https://fonts.bunny.net https://*.bunny.net; connect-src 'self'; img-src 'self' data: https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self';");
return $response;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Imports;
use Maatwebsite\Excel\Concerns\OnEachRow;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Concerns\WithStartRow;
use Maatwebsite\Excel\Row;
class DebugImport implements WithMultipleSheets
{
public function sheets(): array
{
return [
'List' => new DebugSheet(),
];
}
}
class DebugSheet implements OnEachRow, WithStartRow
{
public function startRow(): int
{
return 3;
}
public function onRow(Row $row)
{
$cells = $row->toArray();
echo 'Row: ' . $row->getIndex() . ' | AWB: ' . ($cells[0] ?? '') . PHP_EOL;
}
}

View File

@ -0,0 +1,164 @@
<?php
namespace App\Imports;
use App\Enums\ShipmentDirection;
use App\Enums\ShipmentStatus;
use App\Enums\ShipmentType;
use App\Models\Country;
use App\Models\Shipment;
use App\Models\ShipmentItem;
use Maatwebsite\Excel\Concerns\OnEachRow;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Concerns\WithStartRow;
use Maatwebsite\Excel\Row;
class HistoricalShipmentsImport implements WithMultipleSheets
{
public function sheets(): array
{
return [
'List' => new ShipmentsSheetImport(),
];
}
}
class ShipmentsSheetImport implements OnEachRow, WithStartRow
{
public function startRow(): int
{
return 3;
}
public function onRow(Row $row)
{
$cells = $row->toArray();
if (empty($cells[0]) || $cells[0] === 'HAWB No.' || $cells[0] === 'Date') {
return;
}
$awbNo = (string) $cells[0];
if (str_starts_with($awbNo, '=')) {
return;
}
$typeRaw = strtoupper(trim((string) ($cells[7] ?? '')));
$serviceRaw = strtoupper(trim((string) ($cells[6] ?? '')));
$type = match (true) {
str_contains($typeRaw, 'DOC') && str_contains($typeRaw, 'ECO') => ShipmentType::DocEconomy,
str_contains($typeRaw, 'DOC') => ShipmentType::DocNormal,
default => ShipmentType::Parcel,
};
$direction = str_contains($serviceRaw, 'INBOUND') ? ShipmentDirection::Import : ShipmentDirection::Export;
$fromCountry = $this->resolveCountry($cells[3] ?? '');
$toCountry = $this->resolveCountry($cells[4] ?? '');
$date = $cells[1] ?? null;
if ($date && !str_contains((string) $date, '-')) {
$date = null;
}
if (Shipment::where('awb_no', $awbNo)->exists()) {
return;
}
$shipment = Shipment::create([
'awb_no' => $awbNo,
'direction' => $direction,
'type' => $type,
'status' => ShipmentStatus::Processed,
'reason_for_export' => $cells[42] ?? null,
'forwarder' => $cells[2] ?? null,
'from_country_id' => $fromCountry?->id,
'to_country_id' => $toCountry?->id,
'weight' => $this->toFloat($cells[8] ?? null),
'volumetric_weight' => $this->toFloat($cells[9] ?? null),
'chargeable_weight' => $this->toFloat($cells[12] ?? null),
'dimensions' => $this->buildDimensions($cells[13] ?? null, $cells[14] ?? null, $cells[15] ?? null),
'sender_name' => $cells[18] ?? null,
'sender_company' => $cells[17] ?? null,
'sender_phone' => $cells[19] ?? null,
'sender_email' => $cells[20] ?? null,
'sender_address' => $cells[21] ?? null,
'sender_city' => $cells[22] ?? null,
'sender_zip' => $cells[23] ?? null,
'sender_id_number' => $cells[24] ?? null,
'receiver_name' => $cells[26] ?? null,
'receiver_company' => $cells[25] ?? null,
'receiver_phone' => $cells[27] ?? null,
'receiver_email' => $cells[28] ?? null,
'receiver_address' => $cells[29] ?? null,
'receiver_city' => $cells[30] ?? null,
'receiver_zip' => $cells[31] ?? null,
'receiver_id_number' => $cells[32] ?? null,
'shipping_price' => $this->toFloat($cells[33] ?? null),
'extra_service' => $this->toFloat($cells[34] ?? null) ?? 0,
'packing_cost' => $this->toFloat($cells[36] ?? null) ?? 0,
'domestic_pickup' => $this->toFloat($cells[35] ?? null) ?? 0,
'domestic_delivery' => $this->toFloat($cells[37] ?? null) ?? 0,
'warehousing_cost' => $this->toFloat($cells[38] ?? null) ?? 0,
'discount' => $this->toFloat($cells[39] ?? null) ?? 0,
'total_fee' => $this->toFloat($cells[40] ?? null),
'net_dirham' => null,
'net_rial' => null,
'created_at' => $date,
]);
for ($i = 0; $i < 9; $i++) {
$base = 43 + ($i * 6);
$description = $cells[$base] ?? null;
$hsCode = $cells[$base + 1] ?? null;
$quantity = $this->toFloat($cells[$base + 2] ?? null);
$unitPrice = $this->toFloat($cells[$base + 3] ?? null);
$totalUsd = $this->toFloat($cells[$base + 4] ?? null);
if (empty($description) && empty($hsCode)) {
continue;
}
ShipmentItem::create([
'shipment_id' => $shipment->id,
'row_number' => $i + 1,
'description' => $description,
'hs_code' => $hsCode,
'quantity' => $quantity ?? 0,
'unit_price' => $unitPrice ?? 0,
'total_usd' => $totalUsd ?? 0,
]);
}
}
private function toFloat($value): ?float
{
if ($value === null || $value === '') {
return null;
}
$value = str_replace([',', ' '], '', (string) $value);
return is_numeric($value) ? (float) $value : null;
}
private function buildDimensions($w, $l, $h): ?string
{
$parts = array_filter([$w, $l, $h]);
return $parts ? implode(' x ', $parts) . ' cm' : null;
}
private function resolveCountry(string $raw): ?Country
{
$raw = trim($raw);
if (preg_match('/\(([A-Z]{2})\)/', $raw, $matches)) {
return Country::where('iso_code', $matches[1])->first();
}
return Country::where('name', $raw)->first();
}
}

View File

@ -1,104 +0,0 @@
<?php
namespace App\Imports;
use App\Models\Shipment;
use Maatwebsite\Excel\Concerns\OnEachRow;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithStartRow;
class OldShipmentsImport implements OnEachRow, WithHeadingRow, WithStartRow
{
/**
* چون ممکنه در فایل اکسل چندین سطر اول هدر هستند، می‌اییم ۳ سطر اول رو رد کنیم.
* هدر اصلی معمولاً ردیف ۱۰ یا ۱۱ هست.
*/
protected int $startRow = 3;
/**
* متد الزامی برای WithStartRow
*/
public function startRow(): int
{
return $this->startRow;
}
/**
* تبدیل تاریخ شمسی/میلادی به میلادی برای ذخیره در دیتابیس
*/
private function convertDateToGregorian($dateStr)
{
if (preg_match('/\d{4}\/\d{2}\/\d{2,4}/', $dateStr)) {
$parts = explode('/', $dateStr);
if (strlen($parts[2]) === 4) {
list($y, $m, $d) = $parts;
return "$y-$m-$d";
}
}
return $dateStr;
}
public function onRow(\Maatwebsite\Excel\Row $row)
{
$rowIndex = $row->getIndex();
$cells = $row->toArray();
// ۱. ردیف‌های خالی یا هدرها را رد کنیم
if (empty($cells[1]) || in_array($cells[1], ['HAWB No.', 'Date', 'Forwarder', 'Shipper']) || in_array($cells[0], ['HEAD', 'List'])) {
return;
}
// ۲. استخراج داده‌ها (بر اساس شیت لیست شما)
$data = [
'awb_no' => $cells[0],
'direction' => 'Outbound', // طبق اکسل شما بیشتر صادرات است
'type' => $cells[8] === 'DOC' ? 'DOC' : 'NON DOC',
'status' => $cells[18] ?? 'Processed', // فیلد Last State
'reason_for_export' => $cells[17] ?? '',
'weight' => is_numeric($cells[11]) ? $cells[11] : 0,
'volumetric_weight' => is_numeric($cells[12]) ? $cells[12] : 0,
'chargeable_weight' => is_numeric($cells[13]) ? $cells[13] : 0,
'dimensions' => trim(($cells[14] ?? '') . ' * ' . ($cells[15] ?? '') . ' * ' . ($cells[16] ?? '')),
// اصلاح تداخل ایندکس: ایندکس 18 برای status رزرو شد، قیمت را از ایندکس بعدی شروع کردیم
'shipping_price' => is_numeric($cells[19] ?? null) ? $cells[19] : 0,
'extra_service' => is_numeric($cells[20] ?? null) ? $cells[20] : 0,
'packing_cost' => is_numeric($cells[21] ?? null) ? $cells[21] : 0,
'discount' => is_numeric($cells[22] ?? null) ? $cells[22] : 0,
'total_fee' => is_numeric($cells[23] ?? null) ? $cells[23] : 0,
'net_dirham' => is_numeric($cells[25] ?? null) ? $cells[25] : 0,
'net_rial' => is_numeric($cells[26] ?? null) ? $cells[26] : 0,
'sender_name' => $cells[2],
'sender_company' => '',
'sender_phone' => $cells[3],
'sender_email' => $cells[4],
'sender_address' => $cells[5],
'sender_city' => $cells[6],
'sender_zip' => $cells[7],
'sender_id_number' => '',
'receiver_name' => $cells[9],
'receiver_company' => '',
'receiver_phone' => $cells[10],
// اصلاح تداخل ایندکس: ایندکس 11 مربوط به weight است، ایمیل گیرنده را به ایندکس بعدی منتقل کردیم
'receiver_email' => $cells[12] ?? '',
// رفع خطای متغیر ناموجود: خواندن مستقیم از cells به جای data
'receiver_address' => ($cells[5] ?? '') . ' ' . ($cells[6] ?? ''),
'receiver_city' => $cells[6] ?? '',
'receiver_zip' => $cells[13],
'receiver_id_number' => $cells[14],
'user_id' => 1, // به صورت پیش‌فرض برای سفارشات قدیمی که کاربر خاصی ندارند
];
// ۳. تبدیل تاریخ (Date)
if (!empty($cells[1])) {
$data['created_at'] = $this->convertDateToGregorian($cells[1]);
}
// ۴. ذخیره یا آپدیت در دیتابیس
Shipment::updateOrCreate(
['awb_no' => $data['awb_no']],
$data
);
}
}

View File

@ -2,31 +2,34 @@
namespace App\Imports;
use App\Enums\ShipmentType;
use App\Models\ShippingRate;
use Maatwebsite\Excel\Concerns\OnEachRow;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithStartRow;
use Maatwebsite\Excel\Row;
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),
];
}
}
class RateSheetImport implements OnEachRow
class RateSheetImport implements OnEachRow, WithStartRow
{
private $direction;
public function __construct(protected string $direction) {}
public function __construct($direction)
public function startRow(): int
{
$this->direction = $direction;
return 5;
}
public function onRow(Row $row)
@ -34,45 +37,80 @@ class RateSheetImport implements OnEachRow
$rowIndex = $row->getIndex();
$cells = $row->toArray();
// ردیف اول و دوم معمولاً هدرهای نامرتب هستند، رد می‌کنیم
if ($rowIndex < 2) {
return;
}
// اگر سطر حاوی کلمه DOCUMENT یا NON - DOCUMENT بود، رد می‌کنیم (فقط برای تفکیک نوع استفاده می‌شوند)
if (isset($cells[3]) && (strtoupper($cells[3]) === 'DOCUMENT' || strtoupper($cells[3]) === 'NON - DOCUMENT')) {
return;
}
// اگر وزن خالی بود، ردیف را رد کن
if (empty($cells[0]) || !is_numeric($cells[0])) {
return;
}
// تعیین نوع بسته (DOCUMENT یا NON DOC) بر اساس شماره ردیف در فایل اکسل
// در فایل شما، ردیف‌های 2 تا 7 مدارک و ردیف‌های 9 به بعد غیر مدارک بودند
$type = ($rowIndex >= 2 && $rowIndex <= 7) ? 'DOCUMENT' : 'NON DOC';
$type = $this->resolveType($rowIndex, $cells);
// اگر در بخش غیر مدارک هستیم و به هدرها رسیدیم، رد می‌کنیم
if ($type === 'NON DOC' && isset($cells[0]) && strtoupper(trim($cells[0])) === 'WEIGHT (KG)') {
return;
}
// پاکسازی قیمت‌ها (حذف کاماها و تبدیل به عدد)
$zones = [];
for ($i = 1; $i <= 10; $i++) {
$val = str_replace(',', '', $cells[$i] ?? 0);
$zones["zone_{$i}"] = is_numeric($val) ? floatval($val) : 0;
$val = str_replace([',', ' '], '', (string) ($cells[$i] ?? 0));
$zones['zone_' . $i] = is_numeric($val) ? (float) $val : 0;
}
// درج یا آپدیت رکورد در دیتابیس
ShippingRate::updateOrCreate(
[
'direction' => $this->direction,
'type' => $type,
'weight' => floatval($cells[0]),
'type' => $type->value,
'weight' => (float) $cells[0],
],
$zones
);
}
}
private function resolveType(int $rowIndex, array $cells): ShipmentType
{
$header = strtoupper((string) ($cells[3] ?? ''));
if (str_contains($header, 'DOCUMENT') || str_contains($header, 'DOC')) {
return ShipmentType::DocNormal;
}
if (str_contains($header, 'NON') || str_contains($header, 'PARCEL')) {
return ShipmentType::Parcel;
}
return $rowIndex <= 7 ? ShipmentType::DocNormal : ShipmentType::Parcel;
}
}
class SimpleRateSheetImport implements OnEachRow, WithStartRow
{
public function __construct(protected ShipmentType $type) {}
public function startRow(): int
{
return 2;
}
public function onRow(Row $row)
{
$cells = $row->toArray();
if (empty($cells[0]) || !is_numeric($cells[0])) {
return;
}
$weight = (float) $cells[0];
$zoneNumber = (int) $cells[1];
$value = str_replace([',', ' '], '', (string) ($cells[2] ?? 0));
$value = is_numeric($value) ? (float) $value : 0;
$zoneColumn = 'zone_' . $zoneNumber;
$data = [
'direction' => 'export',
'type' => $this->type->value,
'weight' => $weight,
];
for ($i = 1; $i <= 10; $i++) {
$data['zone_' . $i] = 0;
}
$record = ShippingRate::updateOrCreate($data, $data);
$record->{$zoneColumn} = $value;
$record->save();
}
}

View File

@ -1,13 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Country extends Model
{
protected $fillable = [
'name',
'export_zone',
'import_zone',
'iso_code',
'export_zone_parcel',
'export_zone_doc',
'import_zone_parcel',
'import_zone_doc',
'is_active',
];
protected function casts(): array
{
return [
'is_active' => 'boolean',
];
}
public function fromShipments(): HasMany
{
return $this->hasMany(Shipment::class, 'from_country_id');
}
public function toShipments(): HasMany
{
return $this->hasMany(Shipment::class, 'to_country_id');
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
@ -8,24 +9,53 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Shipment extends Model
{
protected $fillable = [
'user_id', 'awb_no', 'forwarder', 'forwarder_track_id',
'direction', 'type', 'status', 'reason_for_export',
'user_id', 'awb_no', 'forwarder', 'direction', 'type', 'status', 'reason_for_export',
'weight', 'volumetric_weight', 'chargeable_weight', 'dimensions',
'shipping_price', 'extra_service', 'packing_cost', 'discount',
'total_fee', 'net_dirham', 'net_rial',
'shipping_price', 'extra_service', 'packing_cost', 'domestic_pickup', 'domestic_delivery',
'warehousing_cost', 'vat_amount', 'discount', 'total_fee', 'net_dirham', 'net_rial',
'from_country_id', 'to_country_id',
'sender_name', 'sender_company', 'sender_phone', 'sender_email',
'sender_address', 'sender_city', 'sender_zip', 'sender_id_number',
'receiver_name', 'receiver_company', 'receiver_phone', 'receiver_email',
'receiver_address', 'receiver_city', 'receiver_zip', 'receiver_id_number',
];
protected function casts(): array
{
return [
'direction' => \App\Enums\ShipmentDirection::class,
'type' => \App\Enums\ShipmentType::class,
'status' => \App\Enums\ShipmentStatus::class,
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function fromCountry(): BelongsTo
{
return $this->belongsTo(Country::class, 'from_country_id');
}
public function toCountry(): BelongsTo
{
return $this->belongsTo(Country::class, 'to_country_id');
}
public function items(): HasMany
{
return $this->hasMany(ShipmentItem::class);
}
public function carrierMappings(): HasMany
{
return $this->hasMany(ShipmentCarrierMapping::class);
}
public function trackingEvents(): HasMany
{
return $this->hasMany(ShipmentTrackingEvent::class);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ShipmentCarrierMapping extends Model
{
protected $fillable = [
'shipment_id',
'carrier_code',
'carrier_tracking_number',
'booking_number',
'direction',
'delivery_status',
'last_synced_at',
'manually_updated_at',
'notes',
];
protected function casts(): array
{
return [
'carrier_code' => \App\Enums\CarrierCode::class,
'direction' => \App\Enums\ShipmentDirection::class,
'delivery_status' => \App\Enums\ShipmentStatus::class,
'last_synced_at' => 'datetime',
'manually_updated_at' => 'datetime',
];
}
public function shipment(): BelongsTo
{
return $this->belongsTo(Shipment::class);
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ShipmentTrackingEvent extends Model
{
protected $fillable = [
'shipment_id',
'event_date',
'event_time',
'location',
'event_description',
'delivery_status',
'source',
'carrier_mapping_id',
'raw_payload',
];
protected function casts(): array
{
return [
'event_date' => 'date',
'event_time' => 'datetime:H:i',
'delivery_status' => \App\Enums\ShipmentStatus::class,
'source' => \App\Enums\TrackingSource::class,
'raw_payload' => 'array',
];
}
public function shipment(): BelongsTo
{
return $this->belongsTo(Shipment::class);
}
public function carrierMapping(): BelongsTo
{
return $this->belongsTo(ShipmentCarrierMapping::class, 'carrier_mapping_id');
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
@ -12,4 +13,12 @@ class ShippingRate extends Model
'zone_1', 'zone_2', 'zone_3', 'zone_4', 'zone_5',
'zone_6', 'zone_7', 'zone_8', 'zone_9', 'zone_10',
];
protected function casts(): array
{
return [
'direction' => \App\Enums\ShipmentDirection::class,
'type' => \App\Enums\ShipmentType::class,
];
}
}

View File

@ -3,16 +3,44 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SystemSetting extends Model
{
protected $table = 'system_settings';
protected $fillable = [
'dirham_rate',
'dollar_rate',
'yuan_rate',
'euro_rate',
'profit_percent',
'key',
'value',
'description',
'updated_by',
];
}
protected function casts(): array
{
return [
'value' => 'decimal:4',
];
}
public function updatedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
public static function get(string $key, $default = null)
{
$setting = static::where('key', $key)->first();
return $setting ? $setting->value : $default;
}
public static function set(string $key, $value, ?string $description = null, ?int $updatedBy = null): void
{
static::updateOrCreate(
['key' => $key],
[
'value' => $value,
'description' => $description,
'updated_by' => $updatedBy,
]
);
}
}

View File

@ -2,8 +2,6 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -13,37 +11,47 @@ class User extends Authenticatable
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
'phone',
'role',
'is_active',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_active' => 'boolean',
'role' => \App\Enums\UserRole::class,
];
}
public function isSuperAdmin(): bool
{
return $this->role === \App\Enums\UserRole::SuperAdmin;
}
public function isTrackingOperator(): bool
{
return $this->role === \App\Enums\UserRole::TrackingOperator;
}
public function isDataEntry(): bool
{
return $this->role === \App\Enums\UserRole::DataEntry;
}
public function isCustomer(): bool
{
return $this->role === \App\Enums\UserRole::Customer;
}
}

View File

@ -3,20 +3,11 @@
namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\AuthenticateSession;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider
{
@ -33,23 +24,17 @@ class AdminPanelProvider extends PanelProvider
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
Pages\Dashboard::class,
\Filament\Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
\Filament\Widgets\AccountWidget::class,
\Filament\Widgets\FilamentInfoWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
'web',
\Filament\Http\Middleware\DisableBladeIconComponents::class,
\Filament\Http\Middleware\DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,

View File

@ -2,76 +2,82 @@
namespace App\Services;
use App\Enums\ShipmentType;
use App\Models\Country;
use App\Models\ShippingRate;
use App\Models\Shipment;
use App\Models\SystemSetting;
use InvalidArgumentException;
class PriceCalculatorService
{
/**
* محاسبه قیمت ارسال
*
* @param array $data (شامل: direction, type, origin_country_id, destination_country_id, weight, length, width, height)
* @return array
*/
public function calculate(array $data): array
{
// 1. محاسبه وزن حجمی (طبق استاندارد جهانی تقسیم بر 5000)
$dimensions = ($data['length'] * $data['width'] * $data['height']) / 5000;
// 2. تعیین وزن قابل پرداخت (ماکزیموم وزن واقعی و حجمی)
$chargeableWeight = max($data['weight'], $dimensions);
$type = ShipmentType::from($data['type']);
$direction = $data['direction'] === 'Outbound' ? 'export' : 'import';
$chargeableWeight = max($data['weight'], $data['volumetric_weight']);
// 3. استخراج زون مقصد/مبدأ بر اساس جهت ارسال
if ($data['direction'] === 'export') {
// صادرات: زون صادرات کشور مقصد مد نظر است
$country = Country::find($data['destination_country_id']);
$zone = $country->export_zone;
} else {
// واردات: زون واردات کشور مبدأ مد نظر است
$country = Country::find($data['origin_country_id']);
$zone = $country->import_zone;
}
$country = Country::where('iso_code', $data['country_iso'])->firstOrFail();
// 4. پیدا کردن ردیف تعرفه مناسب
// منطق: باید نزدیک‌ترین وزنی که بزرگتر مساوی وزن ماست رو پیدا کنیم (مثلاً اگر 1.2 کیلو است، ردیف 1.5 را می‌گیرد)
$rateRecord = ShippingRate::where('direction', $data['direction'])
->where('type', $data['type'])
->where('weight', '>=', $chargeableWeight)
->orderBy('weight', 'asc')
->first();
$zone = match (true) {
$type === ShipmentType::Parcel && $direction === 'export' => $country->export_zone_parcel,
$type === ShipmentType::Parcel && $direction === 'import' => $country->import_zone_parcel,
$type !== ShipmentType::Parcel && $direction === 'export' => $country->export_zone_doc,
$type !== ShipmentType::Parcel && $direction === 'import' => $country->import_zone_doc,
};
// اگر وزن بالای 30 کیلو باشد یا رکوردی پیدا نشود (Spot Rate)
if (!$rateRecord) {
return [
'status' => 'error',
'message' => 'برای این وزن نرخ ثبت نشده است (نیاز به استعلام نرخ ویژه / Spot Rate).',
'chargeable_weight' => $chargeableWeight,
'volumetric_weight' => $dimensions,
];
}
$rate = $this->lookupRate($direction, $type, $chargeableWeight, $zone);
// 5. استخراج قیمت پایه بر اساس زون (مثلاً ستون zone_3)
$zoneColumn = "zone_{$zone}";
$basePriceDirham = $rateRecord->$zoneColumn;
$profitMargin = (float) SystemSetting::get('profit_margin', 1.25);
$aedToIrr = (float) SystemSetting::get('aed_to_irr', 455000);
$vatRate = (float) SystemSetting::get('vat_rate', 0.09);
$packingCostDefault = (float) SystemSetting::get('packing_cost_default', 100000);
// 6. دریافت نرخ ارز و ضریب سود از تنظیمات
$settings = SystemSetting::first();
$dirhamRate = $settings->dirham_rate; // نرخ درهم به ریال
$profitPercent = $settings->profit_percent; // مثلاً 1.25
$netDirham = $rate * $profitMargin;
$netRial = $netDirham * $aedToIrr;
// 7. اجرای فرمول نهایی (دقیقاً مطابق اکسل)
$netDirham = $basePriceDirham * $profitPercent;
$netRial = $netDirham * $dirhamRate;
$extraService = (float) ($data['extra_service'] ?? 0);
$packingCost = (float) ($data['packing_cost'] ?? $packingCostDefault);
$domesticPickup = (float) ($data['domestic_pickup'] ?? 0);
$domesticDelivery = (float) ($data['domestic_delivery'] ?? 0);
$warehousingCost = (float) ($data['warehousing_cost'] ?? 0);
$discount = (float) ($data['discount'] ?? 0);
$subtotal = $netRial + $extraService + $packingCost + $domesticPickup + $domesticDelivery + $warehousingCost - $discount;
$totalFee = $subtotal * (1 + $vatRate);
$vatAmount = $totalFee - $subtotal;
return [
'status' => 'success',
'volumetric_weight' => round($dimensions, 3),
'chargeable_weight' => round($chargeableWeight, 2),
'base_price' => $rate,
'net_dirham' => $netDirham,
'net_rial' => $netRial,
'extra_service' => $extraService,
'packing_cost' => $packingCost,
'domestic_pickup' => $domesticPickup,
'domestic_delivery' => $domesticDelivery,
'warehousing_cost' => $warehousingCost,
'discount' => $discount,
'vat_amount' => $vatAmount,
'total_fee' => $totalFee,
'zone' => $zone,
'base_price_dirham' => $basePriceDirham,
'net_dirham' => round($netDirham, 2),
'net_rial' => round($netRial),
'chargeable_weight' => $chargeableWeight,
];
}
}
private function lookupRate(string $direction, ShipmentType $type, float $weight, int $zone): float
{
$zoneColumn = 'zone_' . $zone;
$rate = \App\Models\ShippingRate::query()
->where('direction', $direction)
->where('type', $type->value)
->where('weight', '<=', $weight)
->orderByDesc('weight')
->value($zoneColumn);
if ($rate === null) {
throw new InvalidArgumentException('No rate found for the given parameters.');
}
return (float) $rate;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Services;
use App\Models\Shipment;
use App\Models\ShipmentTrackingEvent;
class TrackingService
{
public function getTrackingTimeline(string $awb_no)
{
$shipment = Shipment::where('awb_no', $awb_no)->firstOrFail();
return $shipment->trackingEvents()
->orderByDesc('event_date')
->orderByDesc('event_time')
->get();
}
public function addTrackingEvent(int $shipment_id, array $data): ShipmentTrackingEvent
{
return ShipmentTrackingEvent::create(array_merge($data, [
'shipment_id' => $shipment_id,
'source' => 'manual',
]));
}
public function getShipmentWithTracking(string $awb_no)
{
$shipment = Shipment::where('awb_no', $awb_no)
->with(['carrierMappings', 'trackingEvents'])
->firstOrFail();
return $shipment;
}
}

View File

@ -11,11 +11,17 @@ return Application::configure(basePath: dirname(__DIR__))
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//$middleware->statefulApi();
// فعال کردن CORS برای API ها
->withMiddleware(function (Middleware $middleware): void {
$middleware->api(prepend: \Illuminate\Http\Middleware\HandleCors::class);
$middleware->group('web', [
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//

View File

@ -8,9 +8,10 @@
"require": {
"php": "^8.2",
"filament/filament": "3.3.*",
"laravel/framework": "^12.0",
"laravel/framework": "^11.0",
"laravel/tinker": "^2.10.1",
"maatwebsite/excel": "^3.1"
"maatwebsite/excel": "^3.1",
"morilog/jalali": "^3.0"
},
"require-dev": {
"fakerphp/faker": "^1.23",

317
04_Laravel/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ee15d92e7f7847eea355ca3b5bb3af47",
"content-hash": "36a7ef85e0bbcd022965ce2b7234718f",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@ -71,6 +71,73 @@
},
"time": "2026-06-20T14:30:25+00:00"
},
{
"name": "beberlei/assert",
"version": "v3.3.4",
"source": {
"type": "git",
"url": "https://github.com/beberlei/assert.git",
"reference": "f193f4613c7d7fbcee2c05e4daff4061d49c040e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/beberlei/assert/zipball/f193f4613c7d7fbcee2c05e4daff4061d49c040e",
"reference": "f193f4613c7d7fbcee2c05e4daff4061d49c040e",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*",
"phpunit/phpunit": ">=6.0.0",
"yoast/phpunit-polyfills": "^0.1.0"
},
"suggest": {
"ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles"
},
"type": "library",
"autoload": {
"files": [
"lib/Assert/functions.php"
],
"psr-4": {
"Assert\\": "lib/Assert"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de",
"role": "Lead Developer"
},
{
"name": "Richard Quadling",
"email": "rquadling@gmail.com",
"role": "Collaborator"
}
],
"description": "Thin assertion library for input validation in business models.",
"keywords": [
"assert",
"assertion",
"validation"
],
"support": {
"issues": "https://github.com/beberlei/assert/issues",
"source": "https://github.com/beberlei/assert/tree/v3.3.4"
},
"time": "2026-06-10T19:47:05+00:00"
},
{
"name": "blade-ui-kit/blade-heroicons",
"version": "2.7.0",
@ -2243,20 +2310,20 @@
},
{
"name": "laravel/framework",
"version": "v12.64.0",
"version": "v11.55.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "727a8ea2949c23ca8b5316b86a00984b6017b7a0"
"reference": "dc7ec34ae95bacf4a63b96ec81482b4f3e702289"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/727a8ea2949c23ca8b5316b86a00984b6017b7a0",
"reference": "727a8ea2949c23ca8b5316b86a00984b6017b7a0",
"url": "https://api.github.com/repos/laravel/framework/zipball/dc7ec34ae95bacf4a63b96ec81482b4f3e702289",
"reference": "dc7ec34ae95bacf4a63b96ec81482b4f3e702289",
"shasum": ""
},
"require": {
"brick/math": "^0.11|^0.12|^0.13|^0.14",
"brick/math": "^0.9.3|^0.10.2|^0.11|^0.12|^0.13|^0.14",
"composer-runtime-api": "^2.2",
"doctrine/inflector": "^2.0.5",
"dragonmantank/cron-expression": "^3.4",
@ -2271,34 +2338,32 @@
"fruitcake/php-cors": "^1.3",
"guzzlehttp/guzzle": "^7.8.2",
"guzzlehttp/uri-template": "^1.0",
"laravel/prompts": "^0.3.0",
"laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
"laravel/serializable-closure": "^1.3|^2.0",
"league/commonmark": "^2.8.1",
"league/commonmark": "^2.7",
"league/flysystem": "^3.25.1",
"league/flysystem-local": "^3.25.1",
"league/uri": "^7.5.1",
"monolog/monolog": "^3.0",
"nesbot/carbon": "^3.8.4",
"nesbot/carbon": "^2.72.6|^3.8.4",
"nunomaduro/termwind": "^2.0",
"php": "^8.2",
"psr/container": "^1.1.1|^2.0.1",
"psr/log": "^1.0|^2.0|^3.0",
"psr/simple-cache": "^1.0|^2.0|^3.0",
"ramsey/uuid": "^4.7",
"symfony/console": "^7.2.0",
"symfony/error-handler": "^7.2.0",
"symfony/finder": "^7.2.0",
"symfony/console": "^7.0.3",
"symfony/error-handler": "^7.0.3",
"symfony/finder": "^7.0.3",
"symfony/http-foundation": "^7.2.0",
"symfony/http-kernel": "^7.2.0",
"symfony/mailer": "^7.2.0",
"symfony/mime": "^7.2.0",
"symfony/polyfill-php83": "^1.33",
"symfony/polyfill-php84": "^1.34",
"symfony/polyfill-php85": "^1.34",
"symfony/process": "^7.2.0",
"symfony/routing": "^7.2.0",
"symfony/uid": "^7.2.0",
"symfony/var-dumper": "^7.2.0",
"symfony/http-kernel": "^7.0.3",
"symfony/mailer": "^7.0.3",
"symfony/mime": "^7.0.3",
"symfony/polyfill-php83": "^1.31",
"symfony/process": "^7.0.3",
"symfony/routing": "^7.0.3",
"symfony/uid": "^7.0.3",
"symfony/var-dumper": "^7.0.3",
"tijsverkoyen/css-to-inline-styles": "^2.2.5",
"vlucas/phpdotenv": "^5.6.1",
"voku/portable-ascii": "^2.0.2"
@ -2330,7 +2395,6 @@
"illuminate/filesystem": "self.version",
"illuminate/hashing": "self.version",
"illuminate/http": "self.version",
"illuminate/json-schema": "self.version",
"illuminate/log": "self.version",
"illuminate/macroable": "self.version",
"illuminate/mail": "self.version",
@ -2340,7 +2404,6 @@
"illuminate/process": "self.version",
"illuminate/queue": "self.version",
"illuminate/redis": "self.version",
"illuminate/reflection": "self.version",
"illuminate/routing": "self.version",
"illuminate/session": "self.version",
"illuminate/support": "self.version",
@ -2364,18 +2427,17 @@
"league/flysystem-read-only": "^3.25.1",
"league/flysystem-sftp-v3": "^3.25.1",
"mockery/mockery": "^1.6.10",
"opis/json-schema": "^2.4.1",
"orchestra/testbench-core": "^10.9.0",
"pda/pheanstalk": "^5.0.6|^7.0.0",
"orchestra/testbench-core": "^9.18.0",
"pda/pheanstalk": "^5.0.6",
"php-http/discovery": "^1.15",
"phpstan/phpstan": "^2.1.41",
"phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1",
"predis/predis": "^2.3|^3.0",
"resend/resend-php": "^0.10.0|^1.0",
"symfony/cache": "^7.2.0",
"symfony/http-client": "^7.2.0",
"symfony/psr-http-message-bridge": "^7.2.0",
"symfony/translation": "^7.2.0"
"phpstan/phpstan": "2.1.41",
"phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1",
"predis/predis": "^2.3",
"resend/resend-php": "^0.10.0",
"symfony/cache": "^7.0.3",
"symfony/http-client": "^7.0.3",
"symfony/psr-http-message-bridge": "^7.0.3",
"symfony/translation": "^7.0.3"
},
"suggest": {
"ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",
@ -2390,7 +2452,7 @@
"ext-pdo": "Required to use all database features.",
"ext-posix": "Required to use all features of the queue worker.",
"ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).",
"fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).",
"fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).",
"filp/whoops": "Required for friendly error pages in development (^2.14.3).",
"laravel/tinker": "Required to use the tinker console command (^2.0).",
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).",
@ -2401,22 +2463,22 @@
"mockery/mockery": "Required to use mocking (^1.6).",
"pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).",
"php-http/discovery": "Required to use PSR-7 bridging features (^1.15).",
"phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).",
"predis/predis": "Required to use the predis connector (^2.3|^3.0).",
"phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.3.6|^12.0.1).",
"predis/predis": "Required to use the predis connector (^2.3).",
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).",
"resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).",
"symfony/cache": "Required to PSR-6 cache bridge (^7.2).",
"symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).",
"symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).",
"symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).",
"symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).",
"symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)."
"resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).",
"symfony/cache": "Required to PSR-6 cache bridge (^7.0).",
"symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).",
"symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).",
"symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).",
"symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).",
"symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "12.x-dev"
"dev-master": "11.x-dev"
}
},
"autoload": {
@ -2427,7 +2489,6 @@
"src/Illuminate/Filesystem/functions.php",
"src/Illuminate/Foundation/helpers.php",
"src/Illuminate/Log/functions.php",
"src/Illuminate/Reflection/helpers.php",
"src/Illuminate/Support/functions.php",
"src/Illuminate/Support/helpers.php"
],
@ -2436,8 +2497,7 @@
"Illuminate\\Support\\": [
"src/Illuminate/Macroable/",
"src/Illuminate/Collections/",
"src/Illuminate/Conditionable/",
"src/Illuminate/Reflection/"
"src/Illuminate/Conditionable/"
]
}
},
@ -2461,7 +2521,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2026-07-14T14:25:37+00:00"
"time": "2026-07-14T14:25:27+00:00"
},
{
"name": "laravel/prompts",
@ -2651,16 +2711,16 @@
},
{
"name": "league/commonmark",
"version": "2.8.3",
"version": "2.9.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "1902f60f984235023acbe03db6ad614a37b3c3e7"
"reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/1902f60f984235023acbe03db6ad614a37b3c3e7",
"reference": "1902f60f984235023acbe03db6ad614a37b3c3e7",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/5703d83ba3da3b2e356a5fedc848ed6d8ffb6529",
"reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529",
"shasum": ""
},
"require": {
@ -2697,7 +2757,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.9-dev"
"dev-main": "2.10-dev"
}
},
"autoload": {
@ -2754,7 +2814,7 @@
"type": "tidelift"
}
],
"time": "2026-07-12T15:29:16+00:00"
"time": "2026-08-03T13:42:31+00:00"
},
{
"name": "league/config",
@ -3811,6 +3871,71 @@
],
"time": "2026-01-02T08:56:05+00:00"
},
{
"name": "morilog/jalali",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/morilog/jalali.git",
"reference": "40b906c405b72edd20e73797b40be1e650e872c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/morilog/jalali/zipball/40b906c405b72edd20e73797b40be1e650e872c1",
"reference": "40b906c405b72edd20e73797b40be1e650e872c1",
"shasum": ""
},
"require": {
"beberlei/assert": "^3.0",
"nesbot/carbon": "^1.21 || ^2.0 || ^3.0",
"php": "^7.1 | ^8.0"
},
"require-dev": {
"phpunit/phpunit": ">4.0"
},
"type": "library",
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Morilog\\Jalali\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Milad Rey",
"email": "miladr@gmail.com"
},
{
"name": "Morteza Parvini",
"email": "m.parvini@outlook.com"
}
],
"description": "This Package helps developers to easily work with Jalali (Shamsi or Iranian) dates in PHP applications, based on Jalali (Shamsi) DateTime class.",
"keywords": [
"Jalali",
"date",
"datetime",
"laravel",
"morilog"
],
"support": {
"issues": "https://github.com/morilog/jalali/issues",
"source": "https://github.com/morilog/jalali/tree/v3.5.0"
},
"funding": [
{
"url": "https://issuehunt.io/r/morilog",
"type": "issuehunt"
}
],
"time": "2025-12-25T07:12:28+00:00"
},
{
"name": "nesbot/carbon",
"version": "3.13.1",
@ -7154,86 +7279,6 @@
],
"time": "2026-07-01T12:47:55+00:00"
},
{
"name": "symfony/polyfill-php84",
"version": "v1.38.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php84.git",
"reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa",
"reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php84\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-26T12:51:13+00:00"
},
{
"name": "symfony/polyfill-php85",
"version": "v1.41.0",

View File

@ -0,0 +1,25 @@
<?php
return [
'api_key' => env('IFNEX_API_KEY', 'change-me'),
'cors_allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', 'http://localhost,http://127.0.0.1')),
'tracking_rate_limit' => env('IFNEX_TRACKING_RATE_LIMIT', 60),
'carrier_codes' => [
'DHL', 'FEDEX', 'UPS', 'ARAMEX', 'NAGHEL', 'EMX', 'APSITEX', 'IMPEX', 'OTHER',
],
'tracking_statuses' => [
'processed',
'picked_up',
'in_transit',
'out_for_delivery',
'failed',
'delivered',
'returned',
],
];

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::disableForeignKeyConstraints();
Schema::dropIfExists('countries');
Schema::enableForeignKeyConstraints();
Schema::create('countries', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('iso_code', 2)->unique();
$table->unsignedTinyInteger('export_zone_parcel');
$table->unsignedTinyInteger('export_zone_doc');
$table->unsignedTinyInteger('import_zone_parcel');
$table->unsignedTinyInteger('import_zone_doc');
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('countries');
}
};

View File

@ -0,0 +1,73 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::disableForeignKeyConstraints();
Schema::dropIfExists('shipments');
Schema::enableForeignKeyConstraints();
Schema::create('shipments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('awb_no')->unique();
$table->string('forwarder')->nullable();
$table->enum('direction', ['import', 'export']);
$table->enum('type', ['DOC_NORMAL', 'DOC_ECONOMY', 'PARCEL']);
$table->enum('status', ['processed', 'picked_up', 'in_transit', 'out_for_delivery', 'failed', 'delivered', 'returned'])->default('processed');
$table->string('reason_for_export')->nullable();
$table->decimal('weight', 8, 2)->nullable()->unsigned();
$table->decimal('volumetric_weight', 8, 2)->nullable()->unsigned();
$table->decimal('chargeable_weight', 8, 2)->nullable()->unsigned();
$table->string('dimensions')->nullable();
$table->decimal('shipping_price', 12, 2)->nullable()->unsigned();
$table->decimal('extra_service', 12, 2)->default(0)->unsigned();
$table->decimal('packing_cost', 12, 2)->default(0)->unsigned();
$table->decimal('domestic_pickup', 12, 2)->default(0)->unsigned();
$table->decimal('domestic_delivery', 12, 2)->default(0)->unsigned();
$table->decimal('warehousing_cost', 12, 2)->default(0)->unsigned();
$table->decimal('vat_amount', 12, 2)->default(0)->unsigned();
$table->decimal('discount', 12, 2)->default(0)->unsigned();
$table->decimal('total_fee', 12, 2)->nullable()->unsigned();
$table->decimal('net_dirham', 12, 2)->nullable()->unsigned();
$table->decimal('net_rial', 12, 2)->nullable()->unsigned();
$table->foreignId('from_country_id')->nullable()->constrained('countries')->nullOnDelete();
$table->foreignId('to_country_id')->nullable()->constrained('countries')->nullOnDelete();
$table->string('sender_name')->nullable();
$table->string('sender_company')->nullable();
$table->string('sender_phone')->nullable();
$table->string('sender_email')->nullable();
$table->string('sender_address')->nullable();
$table->string('sender_city')->nullable();
$table->string('sender_zip')->nullable();
$table->string('sender_id_number')->nullable();
$table->string('receiver_name')->nullable();
$table->string('receiver_company')->nullable();
$table->string('receiver_phone')->nullable();
$table->string('receiver_email')->nullable();
$table->string('receiver_address')->nullable();
$table->string('receiver_city')->nullable();
$table->string('receiver_zip')->nullable();
$table->string('receiver_id_number')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('shipments');
}
};

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::disableForeignKeyConstraints();
Schema::dropIfExists('shipping_rates');
Schema::enableForeignKeyConstraints();
Schema::create('shipping_rates', function (Blueprint $table) {
$table->id();
$table->enum('direction', ['import', 'export']);
$table->enum('type', ['DOC_NORMAL', 'DOC_ECONOMY', 'PARCEL']);
$table->decimal('weight', 8, 2)->unsigned();
$table->decimal('zone_1', 10, 2)->unsigned();
$table->decimal('zone_2', 10, 2)->unsigned();
$table->decimal('zone_3', 10, 2)->unsigned();
$table->decimal('zone_4', 10, 2)->unsigned();
$table->decimal('zone_5', 10, 2)->unsigned();
$table->decimal('zone_6', 10, 2)->unsigned();
$table->decimal('zone_7', 10, 2)->unsigned();
$table->decimal('zone_8', 10, 2)->unsigned();
$table->decimal('zone_9', 10, 2)->unsigned();
$table->decimal('zone_10', 10, 2)->unsigned();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('shipping_rates');
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::disableForeignKeyConstraints();
Schema::dropIfExists('shipment_carrier_mappings');
Schema::enableForeignKeyConstraints();
Schema::create('shipment_carrier_mappings', function (Blueprint $table) {
$table->id();
$table->foreignId('shipment_id')->constrained()->cascadeOnDelete();
$table->enum('carrier_code', ['DHL', 'FEDEX', 'UPS', 'ARAMEX', 'NAGHEL', 'EMX', 'APSITEX', 'IMPEX', 'OTHER']);
$table->string('carrier_tracking_number', 50);
$table->string('booking_number', 50)->nullable();
$table->enum('direction', ['import', 'export'])->nullable();
$table->enum('delivery_status', ['processed', 'picked_up', 'in_transit', 'out_for_delivery', 'failed', 'delivered', 'returned'])->nullable();
$table->timestamp('last_synced_at')->nullable();
$table->timestamp('manually_updated_at')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('shipment_carrier_mappings');
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::disableForeignKeyConstraints();
Schema::dropIfExists('shipment_tracking_events');
Schema::enableForeignKeyConstraints();
Schema::create('shipment_tracking_events', function (Blueprint $table) {
$table->id();
$table->foreignId('shipment_id')->constrained()->cascadeOnDelete();
$table->date('event_date');
$table->time('event_time');
$table->string('location', 200)->nullable();
$table->string('event_description', 500);
$table->enum('delivery_status', ['processed', 'picked_up', 'in_transit', 'out_for_delivery', 'failed', 'delivered', 'returned'])->nullable();
$table->enum('source', ['manual', 'api_carrier', 'api_aggregator'])->default('manual');
$table->foreignId('carrier_mapping_id')->nullable()->constrained('shipment_carrier_mappings')->nullOnDelete();
$table->json('raw_payload')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('shipment_tracking_events');
}
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::disableForeignKeyConstraints();
Schema::dropIfExists('system_settings');
Schema::enableForeignKeyConstraints();
Schema::create('system_settings', function (Blueprint $table) {
$table->id();
$table->string('key', 100)->unique();
$table->decimal('value', 15, 4)->unsigned();
$table->string('description', 255)->nullable();
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('system_settings');
}
};

View File

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone', 20)->nullable()->after('email');
$table->enum('role', ['super_admin', 'tracking_operator', 'data_entry', 'customer'])->nullable()->after('phone');
$table->boolean('is_active')->default(true)->after('role');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['phone', 'role', 'is_active']);
});
}
};

View File

@ -2,253 +2,261 @@
namespace Database\Seeders;
use App\Models\Country;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class CountriesTableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$countries = [
['name' => 'Afghanistan (AF)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Albania (AL)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Algeria (DZ)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'American Samoa (AS)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Andorra (AD)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Angola (AO)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Anguilla (AI)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Antigua (AG)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Argentina (AR)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Armenia (AM)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Aruba (AW)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Australia (AU)', 'export_zone' => 4, 'import_zone' => 5],
['name' => 'Austria (AT)', 'export_zone' => 3, 'import_zone' => 3],
['name' => 'Azerbaijan (AZ)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Bahamas (BS)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Bahrain (BH)', 'export_zone' => 1, 'import_zone' => 1],
['name' => 'Bangladesh (BD)', 'export_zone' => 2, 'import_zone' => 2],
['name' => 'Barbados (BB)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Belarus (BY)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Belgium (BE)', 'export_zone' => 3, 'import_zone' => 3],
['name' => 'Belize (BZ)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Benin (BJ)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Bermuda (BM)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Bhutan (BT)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Bolivia (BO)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Bonaire (XB)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Bosnia & Herzegovina(BA)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Botswana (BW)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Brazil (BR)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Brunei (BN)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Bulgaria (BG)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Burkina Faso (BF)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Burundi (BI)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Cambodia (KH)', 'export_zone' => 4, 'import_zone' => 8],
['name' => 'Cameroon (CM)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Canada (CA)', 'export_zone' => 3, 'import_zone' => 4],
['name' => 'Canary Islands, The (IC)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Cape Verde (CV)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Cayman Islands (KY)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Central African Rep(CF)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Chad (TD)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Chile (CL)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'China (CN)', 'export_zone' => 3, 'import_zone' => 4],
['name' => 'Colombia (CO)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Comoros (KM)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Congo (CG)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Congo, DPR (CD)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Cook Islands (CK)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Costa Rica (CR)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Cote D Ivoire (CI)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Croatia (HR)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Cuba (CU)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Curacao (XC)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Cyprus (CY)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Czech Rep., The (CZ)', 'export_zone' => 5, 'import_zone' => 3],
['name' => 'Denmark (DK)', 'export_zone' => 3, 'import_zone' => 3],
['name' => 'Djibouti (DJ)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Dominica (DM)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Dominican Rep. (DO)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Ecuador (EC)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Egypt (EG)', 'export_zone' => 2, 'import_zone' => 2],
['name' => 'El Salvador (SV)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Eritrea (ER)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Estonia (EE)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Ethiopia (ET)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Falkland Islands (FK)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Faroe Islands (FO)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Fiji (FJ)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Finland (FI)', 'export_zone' => 3, 'import_zone' => 3],
['name' => 'France (FR)', 'export_zone' => 3, 'import_zone' => 2],
['name' => 'French Guyana (GF)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Gabon (GA)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Gambia (GM)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Georgia (GE)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Germany (DE)', 'export_zone' => 3, 'import_zone' => 2],
['name' => 'Ghana (GH)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Gibraltar (GI)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Greece (GR)', 'export_zone' => 3, 'import_zone' => 2],
['name' => 'Greenland (GL)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Grenada (GD)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Guadeloupe (GP)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Guam (GU)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Guatemala (GT)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Guernsey (GG)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Guinea Rep. (GN)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Guinea-Bissau (GW)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Guinea-Equatorial (GQ)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Guyana (British) (GY)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Haiti (HT)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Honduras (HN)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Hong Kong SAR China (HK)', 'export_zone' => 5, 'import_zone' => 5],
['name' => 'Hungary (HU)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Iceland (IS)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'India (IN)', 'export_zone' => 2, 'import_zone' => 2],
['name' => 'Indonesia (ID)', 'export_zone' => 5, 'import_zone' => 4],
['name' => 'Iran (IR)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Iraq (IQ)', 'export_zone' => 8, 'import_zone' => 8],
['name' => 'Ireland, Rep. Of (IE)', 'export_zone' => 3, 'import_zone' => 3],
['name' => 'Italy (IT)', 'export_zone' => 3, 'import_zone' => 3],
['name' => 'Jamaica (JM)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Japan (JP)', 'export_zone' => 5, 'import_zone' => 4],
['name' => 'Jersey (JE)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Jordan (JO)', 'export_zone' => 3, 'import_zone' => 2],
['name' => 'Kazakhstan (KZ)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Kenya (KE)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Kiribati (KI)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Korea, Rep. Of (KR)', 'export_zone' => 5, 'import_zone' => 4],
['name' => 'Korea, D.P.R Of (KP)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Kosovo (KV)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Kuwait (KW)', 'export_zone' => 1, 'import_zone' => 1],
['name' => 'Kyrgyzstan (KG)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Laos (LA)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Latvia (LV)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Lebanon (LB)', 'export_zone' => 3, 'import_zone' => 3],
['name' => 'Lesotho (LS)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Liberia (LR)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Libya (LY)', 'export_zone' => 8, 'import_zone' => 8],
['name' => 'Liechtenstein (LI)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Lithuania (LT)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Luxembourg (LU)', 'export_zone' => 3, 'import_zone' => 5],
['name' => 'Macau SAR China (MO)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Madagascar (MG)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Malawi (MW)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Malaysia (MY)', 'export_zone' => 5, 'import_zone' => 4],
['name' => 'Maldives (MV)', 'export_zone' => 5, 'import_zone' => 5],
['name' => 'Mali (ML)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Malta (MT)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Mariana Islands (MP)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Marshall Islands (MH)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Martinique (MQ)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Mauritania (MR)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Mauritius (MU)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Mayotte (YT)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Mexico (MX)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Micronesia (FM)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Moldova, Rep. Of (MD)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Monaco (MC)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Mongolia (MN)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Montenegro, Rep Of (ME)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Montserrat (MS)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Morocco (MA)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Mozambique (MZ)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Myanmar (MM)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Namibia (NA)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Nauru, Rep. Of (NR)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Nepal (NP)', 'export_zone' => 5, 'import_zone' => 5],
['name' => 'Netherlands, The (NL)', 'export_zone' => 3, 'import_zone' => 3],
['name' => 'Nevis (XN)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'New Caledonia (NC)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'New Zealand (NZ)', 'export_zone' => 5, 'import_zone' => 6],
['name' => 'Nicaragua (NI)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Niger (NE)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Nigeria (NG)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Niue (NU)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'North Macedonia (MK)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Norway (NO)', 'export_zone' => 6, 'import_zone' => 6],
['name' => 'Oman (OM)', 'export_zone' => 1, 'import_zone' => 1],
['name' => 'Pakistan (PK)', 'export_zone' => 2, 'import_zone' => 2],
['name' => 'Palau (PW)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Panama (PA)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Papua New Guinea (PG)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Paraguay (PY)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Peru (PE)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Philippines, The (PH)', 'export_zone' => 3, 'import_zone' => 5],
['name' => 'Poland (PL)', 'export_zone' => 6, 'import_zone' => 4],
['name' => 'Portugal (PT)', 'export_zone' => 3, 'import_zone' => 3],
['name' => 'Puerto Rico (PR)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Qatar (QA)', 'export_zone' => 1, 'import_zone' => 1],
['name' => 'Reunion, Island Of (RE)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Romania (RO)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Russian Federation (RU)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Rwanda (RW)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Saint Helena (SH)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Samoa (WS)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'San Marino (SM)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Sao Tome And Principe (ST)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Saudi Arabia (SA)', 'export_zone' => 2, 'import_zone' => 1],
['name' => 'Senegal (SN)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Serbia, Rep. Of (RS)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Seychelles (SC)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Sierra Leone (SL)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Singapore (SG)', 'export_zone' => 3, 'import_zone' => 5],
['name' => 'Slovakia (SK)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Slovenia (SI)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Solomon Islands (SB)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Somalia (SO)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Somaliland, Rep Of (XS)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'South Africa (ZA)', 'export_zone' => 6, 'import_zone' => 6],
['name' => 'South Sudan (SS)', 'export_zone' => 8, 'import_zone' => 8],
['name' => 'Spain (ES)', 'export_zone' => 3, 'import_zone' => 3],
['name' => 'Sri Lanka (LK)', 'export_zone' => 2, 'import_zone' => 2],
['name' => 'St. Barthelemy (XY)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'St. Eustatius (XE)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'St. Kitts (KN)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'St. Lucia (LC)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'St. Maarten (XM)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'St. Vincent (VC)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Sudan (SD)', 'export_zone' => 8, 'import_zone' => 8],
['name' => 'Suriname (SR)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Swaziland (SZ)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Sweden (SE)', 'export_zone' => 3, 'import_zone' => 3],
['name' => 'Switzerland (CH)', 'export_zone' => 3, 'import_zone' => 3],
['name' => 'Syria (SY)', 'export_zone' => 8, 'import_zone' => 8],
['name' => 'Tahiti (PF)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Taiwan (TW)', 'export_zone' => 5, 'import_zone' => 4],
['name' => 'Tajikistan (TJ)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Tanzania (TZ)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Thailand (TH)', 'export_zone' => 5, 'import_zone' => 4],
['name' => 'Timor-Leste (TL)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Togo (TG)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Tonga (TO)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Trinidad And Tobago (TT)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Tunisia (TN)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Turkey (TR)', 'export_zone' => 5, 'import_zone' => 3],
['name' => 'Turkmenistan (TM)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Turks & Caicos (TC)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Tuvalu (TV)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'USA (US)', 'export_zone' => 3, 'import_zone' => 4],
['name' => 'Uganda (UG)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Ukraine (UA)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'United Kingdom (GB)', 'export_zone' => 3, 'import_zone' => 2],
['name' => 'Uruguay (UY)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Uzbekistan (UZ)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Vanuatu (VU)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Vatican City (VA)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Venezuela (VE)', 'export_zone' => 6, 'import_zone' => 8],
['name' => 'Vietnam (VN)', 'export_zone' => 5, 'import_zone' => 4],
['name' => 'Virgin Islands-British (VG)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Virgin Islands-US (VI)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Yemen, Rep. Of (YE)', 'export_zone' => 8, 'import_zone' => 8],
['name' => 'Zambia (ZM)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'Zimbabwe (ZW)', 'export_zone' => 7, 'import_zone' => 8],
['name' => 'United Arab Emirates (AE)', 'export_zone' => 9, 'import_zone' => 9],
['name' => 'Rest of Dubai (AE)', 'export_zone' => 10, 'import_zone' => 10],
$rows = [
['Afghanistan','AF',7,7,8,8],
['Albania','AL',6,6,8,8],
['Algeria','DZ',6,6,8,8],
['American Samoa','AS',7,7,8,8],
['Andorra','AD',7,7,8,8],
['Angola','AO',7,7,8,8],
['Anguilla','AI',7,7,8,8],
['Antigua','AG',7,7,8,8],
['Argentina','AR',6,6,8,8],
['Armenia','AM',6,6,8,8],
['Aruba','AW',7,7,8,8],
['Australia','AU',4,4,5,5],
['Austria','AT',3,3,3,3],
['Azerbaijan','AZ',6,6,8,8],
['Bahamas','BS',7,7,8,8],
['Bahrain','BH',1,1,1,1],
['Bangladesh','BD',2,2,2,2],
['Barbados','BB',7,7,8,8],
['Belarus','BY',6,6,8,8],
['Belgium','BE',3,3,3,3],
['Belize','BZ',7,7,8,8],
['Benin','BJ',7,7,8,8],
['Bermuda','BM',7,7,8,8],
['Bhutan','BT',7,7,8,8],
['Bolivia','BO',6,6,8,8],
['Bonaire','BQ',7,7,8,8],
['Bosnia and Herzegovina','BA',6,6,8,8],
['Botswana','BW',7,7,8,8],
['Brazil','BR',6,6,8,8],
['Brunei','BN',7,7,8,8],
['Bulgaria','BG',6,6,8,8],
['Burkina Faso','BF',7,7,8,8],
['Burundi','BI',7,7,8,8],
['Cambodia','KH',4,4,8,8],
['Cameroon','CM',7,7,8,8],
['Canada','CA',3,3,4,4],
['Canary Islands, The','IC',6,6,8,8],
['Cape Verde','CV',7,7,8,8],
['Cayman Islands','KY',6,6,8,8],
['Central African Rep','CF',7,7,8,8],
['Chad','TD',7,7,8,8],
['Chile','CL',6,6,8,8],
['China','CN',3,3,4,4],
['Colombia','CO',6,6,8,8],
['Comoros','KM',7,7,8,8],
['Congo','CG',7,7,8,8],
['Congo, DPR','CD',6,6,8,8],
['Cook Islands','CK',7,7,8,8],
['Costa Rica','CR',7,7,8,8],
["Cote D Ivoire",'CI',6,6,8,8],
['Croatia','HR',6,6,8,8],
['Cuba','CU',6,6,8,8],
['Curacao','CW',7,7,8,8],
['Cyprus','CY',6,6,8,8],
['Czech Rep., The','CZ',5,5,3,3],
['Denmark','DK',3,3,3,3],
['Djibouti','DJ',7,7,8,8],
['Dominica','DM',7,7,8,8],
['Dominican Rep.','DO',6,6,8,8],
['Ecuador','EC',6,6,8,8],
['Egypt','EG',2,2,2,2],
['El Salvador','SV',7,7,8,8],
['Eritrea','ER',7,7,8,8],
['Estonia','EE',6,6,8,8],
['Ethiopia','ET',6,6,8,8],
['Falkland Islands','FK',7,7,8,8],
['Faroe Islands','FO',7,7,8,8],
['Fiji','FJ',7,7,8,8],
['Finland','FI',3,3,3,3],
['France','FR',3,3,2,2],
['French Guyana','GF',7,7,8,8],
['Gabon','GA',7,7,8,8],
['Gambia','GM',7,7,8,8],
['Georgia','GE',6,6,8,8],
['Germany','DE',3,3,2,2],
['Ghana','GH',6,6,8,8],
['Gibraltar','GI',7,7,8,8],
['Greece','GR',3,3,2,2],
['Greenland','GL',7,7,8,8],
['Grenada','GD',7,7,8,8],
['Guadeloupe','GP',7,7,8,8],
['Guam','GU',7,7,8,8],
['Guatemala','GT',7,7,8,8],
['Guernsey','GG',7,7,8,8],
['Guinea Rep.','GN',6,6,8,8],
['Guinea-Bissau','GW',7,7,8,8],
['Guinea-Equatorial','GQ',7,7,8,8],
['Guyana (British)','GY',7,7,8,8],
['Haiti','HT',7,7,8,8],
['Honduras','HN',7,7,8,8],
['Hong Kong SAR China','HK',5,5,5,5],
['Hungary','HU',6,6,8,8],
['Iceland','IS',6,6,8,8],
['India','IN',2,2,2,2],
['Indonesia','ID',5,5,4,4],
['Iran','IR',0,0,0,0],
['Iraq','IQ',8,8,8,8],
['Ireland, Rep. Of','IE',3,3,3,3],
['Italy','IT',3,3,3,3],
['Jamaica','JM',7,7,8,8],
['Japan','JP',5,5,4,4],
['Jersey','JE',7,7,8,8],
['Jordan','JO',3,3,2,2],
['Kazakhstan','KZ',6,6,8,8],
['Kenya','KE',6,6,8,8],
['Kiribati','KI',7,7,8,8],
['Korea, Rep. Of','KR',5,5,4,4],
['Korea, D.P.R Of','KP',7,7,8,8],
['Kosovo','KV',7,7,8,8],
['Kuwait','KW',1,1,1,1],
['Kyrgyzstan','KG',6,6,8,8],
['Laos','LA',7,7,8,8],
['Latvia','LV',6,6,8,8],
['Lebanon','LB',3,3,3,3],
['Lesotho','LS',7,7,8,8],
['Liberia','LR',7,7,8,8],
['Libya','LY',8,8,8,8],
['Liechtenstein','LI',6,6,8,8],
['Lithuania','LT',6,6,8,8],
['Luxembourg','LU',3,3,5,5],
['Macau SAR China','MO',7,7,8,8],
['Madagascar','MG',7,7,8,8],
['Malawi','MW',7,7,8,8],
['Malaysia','MY',5,5,4,4],
['Maldives','MV',5,5,5,5],
['Mali','ML',6,6,8,8],
['Malta','MT',6,6,8,8],
['Mariana Islands','MP',7,7,8,8],
['Marshall Islands','MH',7,7,8,8],
['Martinique','MQ',7,7,8,8],
['Mauritania','MR',7,7,8,8],
['Mauritius','MU',7,7,8,8],
['Mayotte','YT',7,7,8,8],
['Mexico','MX',6,6,8,8],
['Micronesia','FM',7,7,8,8],
['Moldova, Rep. Of','MD',7,7,8,8],
['Monaco','MC',7,7,8,8],
['Mongolia','MN',6,6,8,8],
['Montenegro, Rep Of','ME',7,7,8,8],
['Montserrat','MS',7,7,8,8],
['Morocco','MA',6,6,8,8],
['Mozambique','MZ',7,7,8,8],
['Myanmar','MM',7,7,8,8],
['Namibia','NA',7,7,8,8],
['Nauru, Rep. Of','NR',7,7,8,8],
['Nepal','NP',5,5,5,5],
['Netherlands, The','NL',3,3,3,3],
['Nevis','XN',7,7,8,8],
['New Caledonia','NC',7,7,8,8],
['New Zealand','NZ',5,5,6,6],
['Nicaragua','NI',7,7,8,8],
['Niger','NE',7,7,8,8],
['Nigeria','NG',6,6,8,8],
['Niue','NU',7,7,8,8],
['North Macedonia','MK',7,7,8,8],
['Norway','NO',6,6,6,6],
['Oman','OM',1,1,1,1],
['Pakistan','PK',2,2,2,2],
['Palau','PW',7,7,8,8],
['Panama','PA',6,6,8,8],
['Papua New Guinea','PG',7,7,8,8],
['Paraguay','PY',6,6,8,8],
['Peru','PE',6,6,8,8],
['Philippines, The','PH',3,3,5,5],
['Poland','PL',6,6,4,4],
['Portugal','PT',3,3,3,3],
['Puerto Rico','PR',7,7,8,8],
['Qatar','QA',1,1,1,1],
['Reunion, Island Of','RE',7,7,8,8],
['Romania','RO',6,6,8,8],
['Russian Federation','RU',7,7,8,8],
['Rwanda','RW',7,7,8,8],
['Saint Helena','SH',7,7,8,8],
['Samoa','WS',7,7,8,8],
['San Marino','SM',7,7,8,8],
['Sao Tome And Principe','ST',7,7,8,8],
['Saudi Arabia','SA',2,2,1,1],
['Senegal','SN',7,7,8,8],
['Serbia, Rep. Of','RS',6,6,8,8],
['Seychelles','SC',7,7,8,8],
['Sierra Leone','SL',6,6,8,8],
['Singapore','SG',3,3,5,5],
['Slovakia','SK',6,6,8,8],
['Slovenia','SI',6,6,8,8],
['Solomon Islands','SB',7,7,8,8],
['Somalia','SO',7,7,8,8],
['South Africa','ZA',6,6,6,6],
['South Sudan','SS',8,8,8,8],
['Spain','ES',3,3,3,3],
['Sri Lanka','LK',2,2,2,2],
['St. Barthelemy','XY',7,7,8,8],
['St. Eustatius','XE',7,7,8,8],
['St. Kitts','KN',7,7,8,8],
['St. Lucia','LC',7,7,8,8],
['St. Maarten','XM',7,7,8,8],
['St. Vincent','VC',7,7,8,8],
['Sudan','SD',8,8,8,8],
['Suriname','SR',7,7,8,8],
['Swaziland','SZ',7,7,8,8],
['Sweden','SE',3,3,3,3],
['Switzerland','CH',3,3,3,3],
['Syria','SY',8,8,8,8],
['Tahiti','PF',7,7,8,8],
['Taiwan','TW',5,5,4,4],
['Tajikistan','TJ',6,6,8,8],
['Tanzania','TZ',6,6,8,8],
['Thailand','TH',5,5,4,4],
['Timor-Leste','TL',7,7,8,8],
['Togo','TG',7,7,8,8],
['Tonga','TO',7,7,8,8],
['Trinidad And Tobago','TT',7,7,8,8],
['Tunisia','TN',6,6,8,8],
['Turkey','TR',5,5,3,3],
['Turkmenistan','TM',6,6,8,8],
['Turks & Caicos','TC',7,7,8,8],
['Tuvalu','TV',7,7,8,8],
['USA','US',3,3,4,4],
['Uganda','UG',7,7,8,8],
['Ukraine','UA',6,6,8,8],
['United Kingdom','GB',3,3,2,2],
['Uruguay','UY',6,6,8,8],
['Uzbekistan','UZ',6,6,8,8],
['Vanuatu','VU',7,7,8,8],
['Vatican City','VA',7,7,8,8],
['Venezuela','VE',6,6,8,8],
['Vietnam','VN',5,5,4,4],
['Virgin Islands-British','VG',7,7,8,8],
['Virgin Islands-US','VI',7,7,8,8],
['Yemen, Rep. Of','YE',8,8,8,8],
['Zambia','ZM',7,7,8,8],
['Zimbabwe','ZW',7,7,8,8],
['United Arab Emirates','AE',9,9,9,9],
['Rest of Dubai','AE',10,10,10,10],
];
DB::table('countries')->insert($countries);
foreach ($rows as $row) {
Country::updateOrCreate(
['iso_code' => $row[1]],
[
'name' => $row[0],
'export_zone_parcel' => $row[2],
'export_zone_doc' => $row[3],
'import_zone_parcel' => $row[4],
'import_zone_doc' => $row[5],
'is_active' => true,
]
);
}
}
}
}

View File

@ -3,23 +3,25 @@
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
$this->call([
CountriesTableSeeder::class,
SystemSettingSeeder::class,
]);
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
'name' => 'Kazem Alghasi',
'email' => 'kazem@vernasoft.group',
'password' => Hash::make('password'),
'phone' => '09123456789',
'role' => 'super_admin',
'is_active' => true,
]);
}
}

View File

@ -2,6 +2,8 @@
namespace Database\Seeders;
use App\Models\SystemSetting;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
@ -9,12 +11,18 @@ class SystemSettingSeeder extends Seeder
{
public function run(): void
{
DB::table('system_settings')->insert([
'dirham_rate' => 455000,
'dollar_rate' => 0,
'yuan_rate' => 0,
'euro_rate' => 0,
'profit_percent' => 1.25,
]);
$settings = [
['key' => 'vat_rate', 'value' => 0.0900, 'description' => 'VAT rate — 9%'],
['key' => 'packing_cost_default', 'value' => 100000.0000, 'description' => 'Default packing cost in IRR'],
['key' => 'profit_margin', 'value' => 1.2500, 'description' => 'Profit margin — set per company'],
['key' => 'aed_to_irr', 'value' => 455000.0000, 'description' => 'AED to IRR exchange rate'],
['key' => 'usd_to_irr', 'value' => 0.0000, 'description' => 'USD to IRR exchange rate'],
['key' => 'cny_to_irr', 'value' => 0.0000, 'description' => 'CNY to IRR exchange rate'],
['key' => 'eur_to_irr', 'value' => 0.0000, 'description' => 'EUR to IRR exchange rate'],
];
foreach ($settings as $setting) {
SystemSetting::updateOrCreate(['key' => $setting['key']], $setting);
}
}
}
}

View File

@ -1,19 +1,10 @@
<?php
use App\Http\Controllers\Api\TrackController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\PricingController;
use App\Http\Middleware\ApiKeyMiddleware;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
// مسیر محاسبه قیمت (فعلاً روی GET تنظیم شده تا راحت تو مرورگر تست بشه)
Route::get('/calculate-price', [PricingController::class, 'calculate']);
Route::middleware([ApiKeyMiddleware::class])->prefix('v1')->group(function () {
Route::get('/track/{awb_no}', [TrackController::class, 'show']);
});