ifnex/01_Documents/EXCEL_ANALYSIS.md
2026-08-02 05:13:12 +03:30

881 lines
43 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 📊 EXCEL_ANALYSIS.md — تحلیل کامل فایل‌های اکسل عملیاتی
> **هدف:** مرجع کامل برای هر توسعه‌دهنده‌ای که با داده‌های تاریخی IFNEX کار می‌کند
> **فایل‌های تحلیل‌شده:** دو فایل اکسل آپلودشده توسط مشتری در جلسه اولیه
> **تاریخ تحلیل:** August 2026
> **وضعیت:** کامل — برای مهاجرت داده و طراحی اسکیمای دیتابیس استفاده شود
---
## 📁 فهرست فایل‌های تحلیل‌شده
### فایل ۱: `4_5989927490271846355.xlsx` (فایل اصلی عملیاتی)
این فایل قلب کسب‌وکار IFNEX است. شامل ۱۵ شیت است که تمام منطق کسب‌وکار، داده‌های تاریخی و قالب‌های خروجی را در خود جای داده.
**شیت‌ها:**
1. `Start` — خالی (صفحه شروع)
2. `Form` — فرم ثبت یک مرسوله (۳۴ ردیف، ۱۱۵ ستون)
3. `List` — لیست کامل مرسولات تاریخی (**۳۹۵۰ ردیف**، ۱۰۲ ستون)
4. `COUNTRIES` — جدول ۲۳۳ کشور با زون‌ها
5. `AWB` — قالب بارنامه (Air Waybill)
6. `INVOICE + label` — قالب فاکتور
7. `label` — قالب لیبل
8. `Import Rate` — جدول نرخ‌های واردات (۳۸۵ ردیف)
9. `Export Rate` — جدول نرخ‌های صادرات (۳۸۵ ردیف)
10. `Zone` — جدول زون‌بندی کشورها (۲ بخش: PARCEL و DOCUMENT)
11. `DocNor` — جدول قیمت Document Normal
12. `Parcel` — جدول قیمت Parcel
13. `DocEco` — جدول قیمت Document Economy
14. `Assumptions` — تنظیمات (VAT، Packing، روزهای هفته، شهرها)
15. `DATES` — تقویم میلادی به جلالی (۷۳۲ ردیف)
### فایل ۲: `Data entry 2026-06-28.xlsx` (فایل ترکینگ دستی)
این فایل به‌صورت روزانه توسط اپراتورها پر می‌شود و شامل داده‌های ترکینگ مرسولات است. در فاز ₀، این فایل باید با پنل Filament جایگزین شود.
**شیت‌ها:**
1. `Sheet1` — لیست رویدادهای ترکینگ (۳۱۱ ردیف)
2. `Refrence` — نگاشت کد AWB به کد ترکینگ خارجی (۱۵ ردیف)
3. `Paste` — داده خام کپی‌شده از وب‌سایت‌های DHL/FedEx (۴۰۴ ردیف)
4. `copy` — نسخه‌ی تمیزشده Sheet1 (۴۱۵ ردیف)
5. `Delivered` — لیست مرسولات تحویل‌شده (۹۴ ردیف)
6. `test` — آزمایش‌های اپراتور (۳۹۶ ردیف)
---
## 🗂️ تحلیل شیت به شیت — فایل اصلی
### شیت ۱: `Form` — فرم ثبت یک مرسوله
**ساختار:** ۳۴ ردیف × ۱۱۵ ستون (فرم افقی، نه جدولی)
**محتوای کلیدی:**
- اطلاعات مسیر (Route Information): HAWB No.، Date، Forwarder، From، To، Zone، Service، Type
- اطلاعات بسته (Shipment): Content، Weight، Volumetric Weight، Chargable Weight، Dimensions (W×L×H)
- اطلاعات فرستنده (Shipper): Company Name، Contact، Telephone، Email، Address، City، Zip، ID Number
- اطلاعات گیرنده (Receiver): همان فیلدها
- اطلاعات مالی (Payment): Shipping Price، Extra Service، Domestic Pickup، Packing Cost، Domestic Delivery، Warehousing Cost، Discount، Total Fee، Cash on Delivery
- اقلام گمرکی (۹ ردیف): No.، Description، H.S. Code، Quantity، Unit Price، Total in USD
- تنظیمات فرمول: Percent (ضریب سود)، نرخ ارز
**نکته مهم:** این شیت نشان می‌دهد **۹ ردیف کالای گمرکی** در هر مرسوله قابل ثبت است. این باید در فرم ثبت سفارش آنلاین (فاز ۱) لحاظ شود.
**نمونه داده واقعی:**
```
Date: 2026-07-31
Forwarder: (خالی)
From: Iran (IR)
To: USA (US)
Zone: 3
Service: Outbound
Type: NON DOC
Weight: 0 kg, Volumetric: 0 kg
Content: ITEM BEING SENT AS A GIFT N...
Status: Processed
```
**نگاشت به دیتابیس:** این شیت اساس طراحی جدول `shipments` و `shipment_items` در فاز ۱ است.
---
### شیت ۲: `List` — لیست کامل مرسولات تاریخی ⭐ حیاتی برای مهاجرت
**ساختار:** ۳۹۵۰ ردیف × ۱۰۲ ستون
**توضیح:** این شیت، جدول اصلی داده‌های تاریخی IFNEX است. هر ردیف یک مرسوله از سال ۲۰۲۰ تا الان. **این داده‌ها باید در فاز ₀ به جدول `shipments` مهاجرت داده شوند.**
**ستون‌های کلیدی (ردیف ۱ و ۲ سرتیتر هستند):**
| # | ستون | نوع | مثال | نگاشت به DB |
|---|------|-----|------|-------------|
| ۱ | HAWB No. | String | 980100010 | `shipments.awb_no` |
| ۲ | Date | DateTime | 2020-05-02 | `shipments.created_at` |
| ۳ | Forwarder | String | DHL / FedEx / 0 | `shipment_carrier_mappings.carrier_code` |
| ۴ | From | String | Iran (IR) | `shipments.from_country_id` |
| ۵ | To | String | Germany | `shipments.to_country_id` |
| ۶ | Zone | Integer | 3 | (محاسبه می‌شود — ذخیره نمی‌شود) |
| ۷ | Service | Enum | Outbound / Inbound | `shipments.direction` |
| ۸ | Type | Enum | DocNor / NON DOC | `shipments.type` (تبدیل به DOC_NORMAL/DOC_ECONOMY/PARCEL) |
| ۹ | Weight | Decimal | 0.5 | `shipments.weight` |
| ۱۰ | Volumetric W. | Decimal | 0 | `shipments.volumetric_weight` |
| ۱۱ | Value | Decimal | 0 | (فاز ۱ — فیلد ارزش محموله) |
| ۱۲ | Content | String | EDUCATINAL DOCUEMTS | `shipments.content_description` |
| ۱۳ | Chargable Weight | Decimal | 0.5 | `shipments.chargeable_weight` |
| ۱۴-۱۶ | WIDHTH/LENGTH/HEIGHT | Integer | 25, 15, 3 | `shipments.dimensions` (به‌صورت JSON یا فیلد جداگانه) |
| ۱۷ | Third party | Boolean | 0/1 | (فاز ۱) |
| ۱۸-۲۴ | Sender fields | String | (متغیر) | `shipments.sender_*` |
| ۲۵-۳۲ | Receiver fields | String | (متغیر) | `shipments.receiver_*` |
| ۳۳-۴۰ | Price fields | Decimal | (متغیر) | `shipments.shipping_price`، `extra_service`، `packing_cost`، `discount`، `total_fee` |
| ۴۱ | REASON FOR EXPORT | String | ITEM BEING SENT AS A SAMPLE... | `shipments.reason_for_export` |
| ۴۲-۸۶ | ۹ ردیف کالای گمرکی | String | (متغیر) | `shipment_items` (۹ ردیف) |
| ۸۷ | TOTAL INVOICE AMOUNT IN USD | Decimal | 114 | `shipments.invoice_total_usd` |
| ۸۸ | Last State | String | Processed | `shipments.status` (تبدیل شود) |
| ۸۹ | Last Load | DateTime | 2025-05-26 | (metadata) |
| ۹۰ | Net Dirham | Decimal | 160.69 | `shipments.net_dirham` |
| ۹۱ | Net Rial | Decimal | 73113385.32 | `shipments.net_rial` |
**نمونه رکورد کامل (ردیف ۴ — 980100011):**
```
AWB: 980100011
Date: 2026-02-26
Forwarder: 0
From: Iran (IR)
To: China (CN)
Zone: 3
Service: Outbound
Type: NON DOC
Weight: 0.1 kg, Volumetric: 0.225 kg
Value: 60 USD
Content: Electronics PCB Board
Chargeable Weight: 0.5
Dimensions: 25×15×3 cm
Sender: Akbar Salmanizadeh, +989132027178, Isfahan, IR, ID: 1283855623
Receiver: Chinapcbone Technology LTD, Ms Bindy Zhang, +8615814401212, SHENZHEN, CN, ZIP: 518103
Items:
1. IC LT1668, HS: 8542390001, Qty: 104, Unit: $1.1, Total: $114
Status: Processed
Net Dirham: 160.69
Net Rial: 73,113,385.32
```
**نکات مهاجرت:**
- ردیف ۱ و ۲ سرتیتر هستند — ردیف ۳ به بعد داده واقعی
- ردیف‌های خالی زیاد است — اسکریپت باید آنها را فیلتر کند
- نام کشورها به فرمت `Iran (IR)` است — باید به `iso_code` تبدیل شود
- فیلد `Type` در اکسل شامل مقادیر متنوع است: `DocNor`، `NON DOC`، `Outbound`، `Inbound` — نیاز به استانداردسازی به enum سه‌حالته
- فیلد `Service` در اکسل با فیلد `direction` در DB یکی است (Outbound=export, Inbound=import)
- تاریخ‌ها میلادی هستند (نه جلالی) — خوب، نیازی به تبدیل نیست
---
### شیت ۳: `COUNTRIES` — جدول کشورها
**ساختار:** ۲۳۵ ردیف × ۵ ستون
**ستون‌ها:**
1. `COUNTRIES` — نام کشور با کد ISO در پرانتز، مثلاً `Afghanistan (AF)`
2. `EXPORT ZONES` — زون صادرات (عدد ۱ تا ۱۰)
3. `IMPORT ZONES` — زون واردات (عدد ۱ تا ۱۰)
4. `Service` — فقط چند ردیف اول پر است (مثلاً `Inbound`، `Outbound`، `Visa Pick Up`) — به‌نظر می‌رسد دستی وارد شده، نادیده بگیر
5. `Content` — فقط چند ردیف اول پر است (`DOC`، `NON DOC`) — نادیده بگیر
**نکته مهم:** این شیت، **منبع نهایی زون‌بندی نیست**. شیت `Zone` (شیت ۱۰) منبع دقیق‌تری است چون زون‌های مجزا برای PARCEL و DOCUMENT دارد. اما این شیت (`COUNTRIES`) برای تأیید تعداد کشورها (۲۳۳ کشور واقعی، با چند مورد تکراری) استفاده می‌شود.
**نمونه داده:**
```
Afghanistan (AF) | 7 | 8
Albania (AL) | 6 | 8
Algeria (DZ) | 6 | 8
...
Iran (IR) | (خالی — ایران مبدا/مقصد داخلی است)
...
Yemen (YE) | 7 | 8
```
**نگاشت به دیتابیس:** این شیت فقط برای استخراج `name` و `iso_code` استفاده می‌شود. زون‌ها از شیت `Zone` (که دقیق‌تر است) گرفته می‌شوند.
---
### شیت ۴: `AWB` — قالب بارنامه (Air Waybill)
**ساختار:** ۱۸۵۶ ردیف × ۱۵ ستون (قالب افقی، چند بارنامه در یک شیت)
**محتوا:** قالب PDF بارنامه IFNEX. هر بارنامه شامل:
- شماره AWB (مثلاً 980100011)
- تاریخ
- لوگوی IFNEX (با متن "We Deliver Value")
- اطلاعات فرستنده و گیرنده (در دو بلوک)
- اطلاعات بسته (وزن، ابعاد)
- اطلاعات پرداخت (قیمت به IRR)
- بارکد (در اکسل تصویر، در لاراول باید تولید شود)
**نکته:** این قالب باید در فاز ۱ به‌صورت PDF در لاراول بازسازی شود. خروجی PDF باید **انگلیسی** باشد (مطابق اکسل اصلی).
**نگاشت:** قالب PDF با Dompdf یا Snappy در لاراول — فاز ۱.
---
### شیت ۵: `INVOICE + label` — قالب فاکتور
**ساختار:** ۱۸۳۵ ردیف × ۱۲ ستون
**محتوا:** قالب فاکتور تجاری (Commercial Invoice) شامل:
- INVOICE NO (همان AWB)
- DATE
- SHIPPER (نام شرکت، آدرس، تلفن، ایمیل)
- CONSIGNEE (همان فیلدها برای گیرنده)
- لیست اقلام (Description، HS Code، Quantity، Unit Price، Total)
- TOTAL INVOICE AMOUNT IN USD
**نکته:** در فاز ۱، این قالب به‌صورت PDF تولید می‌شود. فیلدهای invoice باید با فیلدهای `shipment_items` در دیتابیس منطبق باشند.
---
### شیت ۶: `label` — قالب لیبل
**ساختار:** ۱۱ ردیف × ۱۳ ستون
**محتوا:** قالب لیبل چاپی برای چاپگرهای حرارتی. شامل:
- شماره AWB (بارکد)
- وزن ناخالص (Gross Weight)
- ابعاد (W×L×H)
- وزن حجمی (Volumetric)
- تاریخ
- کشور مبدا و مقصد
**نکته:** در فاز ۱، این قالب به‌صورت PDF کوچک (مثلاً ۱۰۰×۱۰۰ میلی‌متر) تولید می‌شود. مخصوص چاپگرهای حرارتی دفتر اصفهان.
---
### شیت ۷: `Import Rate` — نرخ‌های واردات
**ساختار:** ۳۸۵ ردیف × ۱۱ ستون
**ساختار جدول:**
- ردیف ۳: عنوان "ROW TO IRAN - IMPORT RATE SCHEDULE"
- ردیف ۴: زیرعنوان "DOCUMENT"
- ردیف ۵: سرتیتر ستون‌ها — `Weight (kg) | Zone 1 | Zone 2 | Zone 3 | ... | Zone 10`
- ردیف ۶ به بعد: قیمت پایه به **درهم (AED)** برای هر ترکیب وزن × زون
- ردیف ۱۰: زیرعنوان "NON - DOCUMENT"
- ردیف ۱۱: سرتیتر (تکراری)
- ردیف ۱۲ به بعد: قیمت برای NON-DOC
**نمونه داده (DOCUMENT):**
```
Weight | Zone 1 | Zone 2 | Zone 3 | Zone 4 | Zone 5 | Zone 6 | Zone 7 | Zone 8 | Zone 9 | Zone 10
0.5 | 149.32 | 169.67 | 214.14 | 231.90 | 242.64 | 255.16 | 273.96 | 383.02 | 85.71 | 85.71
1.0 | 197.20 | 195.61 | 253.99 | 245.99 | 295.54 | 308.56 | 329.35 | 390.16 | 92.86 | 92.86
1.5 | 243.41 | 237.62 | 301.99 | 272.61 | 348.30 | 348.30 | 409.41 | 429.65 | 100 | 100
2.0 | 280.61 | 282.51 | 317.42 | 290.62 | 384.39 | 384.39 | 494.54 | 474.51 | 114.29 | 114.29
```
**نکته مهم:** این جدول فقط ۲ بخش دارد (DOCUMENT و NON-DOCUMENT). اما شیت‌های `DocNor`، `DocEco`، `Parcel` نشان می‌دهند که در واقع **۳ نوع سرویس** وجود دارد. تضاد وجود دارد:
- شیت `Import Rate` فقط ۲ نوع دارد (DOCUMENT و NON-DOC)
- شیت‌های جداگانه `DocNor`، `DocEco`، `Parcel` قیمت‌های متفاوتی نشان می‌دهند
**تفسیر:** احتمالاً شیت `Import Rate` جدول قدیمی است و شیت‌های DocNor/DocEco/Parcel نسخه‌ی جدیدتر و دقیق‌تر هستند. در فاز ۱، باید این موضوع را با مشتری تأیید کرد. فعلاً در فاز ۰، فقط جدول `shipping_rates` را با فیلد `type` از نوع enum سه‌حالته (`DOC_NORMAL`, `DOC_ECONOMY`, `PARCEL`) طراحی می‌کنیم.
**نگاشت به دیتابیس:** جدول `shipping_rates` در فاز ۱ (طبق `Phase0_Proposal.md` بخش ۶.۲).
---
### شیت ۸: `Export Rate` — نرخ‌های صادرات
**ساختار:** مشابه شیت Import Rate — ۳۸۵ ردیف × ۱۱ ستون
**عنوان:** "IRAN TO ROW - EXPORT RATE SCHEDULE"
**نکته:** ساختار و تضادها دقیقاً مشابه شیت Import Rate است.
---
### شیت ۹: `Zone` — جدول زون‌بندی کشورها ⭐ منبع نهایی زون‌ها
**ساختار:** ۲۳۰ ردیف × ۷ ستون (دو جدول کنار هم)
**چیدمان:**
- ستون ۱-۳: جدول PARCEL (شماره، کشور، زون)
- ستون ۵-۷: جدول DOCUMENT (شماره، کشور، زون)
**نمونه داده:**
```
PARCEL DOCUMENT
No. | Country | Zone No. | Country | Zone
1 | Afghanistan | 7 1 | Afghanistan | 5
2 | Albania | 3 2 | Albania | 7
3 | Algeria | 4 3 | Algeria | 7
4 | Americam Samoa | 7 4 | Andorra | 7
5 | Andorra | 3 5 | Angola | 6
6 | Angola | 7 6 | Anguilla | 6
7 | Anguilla | 7 7 | Antigua | 6
8 | Antigua | 7 8 | Argentina | 7
9 | Argentina | 7 9 | Armenia | 7
10 | Armenia | 3 10 | Aruba | 7
11 | Aruba | 7 11 | Australia | 6
12 | Australia | 7 12 | Austria | 3
```
**⚠️ کشف کلیدی:** همان کشور برای PARCEL و DOCUMENT زون‌های متفاوتی دارد!
| کشور | PARCEL Zone | DOCUMENT Zone | تفاوت |
|------|-------------|---------------|-------|
| Afghanistan | 7 | 5 | ۲ |
| Albania | 3 | 7 | ۴ |
| Algeria | 4 | 7 | ۳ |
| Armenia | 3 | 7 | ۴ |
| Australia | 7 | 6 | ۱ |
| Austria | 3 | 3 | ۰ |
**نتیجه:** جدول `countries` باید **۴ زون مجزا** داشته باشد:
- `export_zone_parcel` — زون صادرات برای پارسل
- `export_zone_doc` — زون صادرات برای داکیومنت
- `import_zone_parcel` — زون واردات برای پارسل
- `import_zone_doc` — زون واردات برای داکیومنت
این موضوع در `Phase0_Proposal.md` بخش ۶.۱ منعکس شده. هرگز به ۲ زون برگردان.
**نکته:** شیت `Zone` فقط زون‌های export را دارد (از ایران به سایر کشورها). زون‌های import باید از شیت `COUNTRIES` استخراج شوند یا از مشتری درخواست شود.
---
### شیت ۱۰: `DocNor` — جدول قیمت Document Normal
**ساختار:** ۷۱ ردیف × ۳ ستون
**فرمت:** Long format (نه Wide)
```
Weight | Attribute | Value
0.5 | 1 | 2,810,429.42
0.5 | 2 | 3,600,277.54
0.5 | 3 | 5,104,556.32
0.5 | 4 | 5,641,878.04
0.5 | 5 | 6,108,148.95
0.5 | 6 | 7,213,877.11
0.5 | 7 | 8,289,630.71
1.0 | 1 | 4,049,673.89
...
```
**تفسیر:**
- `Weight` — وزن (۰.۵، ۱، ۱.۵، ۲، ۲.۵ کیلوگرم)
- `Attribute` — شماره زون (۱ تا ۱۰)
- `Value` — قیمت **به ریال ایران (IRR)**
**نکته:** برخلاف شیت‌های Import/Export Rate که قیمت به **درهم** بود، اینجا قیمت به **ریال** است. این یعنی فرمول تبدیل (درهم × ضریب سود × نرخ روز درهم = ریال) در این شیت اعمال شده.
**نگاشت:** این داده‌ها در فاز ۱ به جدول `shipping_rates` با `type = DOC_NORMAL` مهاجرت داده می‌شوند. اما به‌جای ذخیره ریال، باید قیمت پایه درهم را ذخیره کنیم (مطابق شیت Import/Export Rate).
---
### شیت ۱۱: `Parcel` — جدول قیمت Parcel
**ساختار:** ۱۴۱ ردیف × ۳ ستون (مشابه DocNor)
**نکته:** وزن‌ها تا ۵ کیلوگرم (یا بیشتر) می‌رسد — برای پارسل محدوده وزن بیشتر است.
**نگاشت:** جدول `shipping_rates` با `type = PARCEL`.
---
### شیت ۱۲: `DocEco` — جدول قیمت Document Economy
**ساختار:** ۷۱ ردیف × ۳ ستون (مشابه DocNor)
**نگاشت:** جدول `shipping_rates` با `type = DOC_ECONOMY`.
---
### شیت ۱۳: `Assumptions` — تنظیمات
**ساختار:** ۲۵۱ ردیف × ۱۷ ستون (ترکیبی از تنظیمات و جداول کمکی)
**محتوای کلیدی:**
- ردیف ۳-۶: تنظیمات سرویس‌ها و VAT
- `DocNor` با VAT: ۰.۰۹ (۹٪) و Packing: ۱۰۰,۰۰۰ ریال
- `DocEco` — خالی
- `Parcel` — خالی
- ردیف ۳-۹ (ستون ۸-۹): روزهای هفته (میلادی و شمسی)
- ردیف ۳-۹ (ستون ۱۴-۱۵): شهرهای ایران با کد (اصفهان=۰۱، شیراز=۰۲، مشهد=۰۳، ...)
- ردیف ۱۰ به بعد (ستون ۱۷): لیست کشورها (به ترتیب حروف الفبا)
**⚠️ کشف کلیدی — VAT:**
VAT در فایل اکسل ۹٪ است (`0.09`). این فیلد در PRD قدیمی ذکر نشده بود. در `Phase0_Proposal.md` به فیلدهای مالی جدول `shipments` اضافه شده.
**⚠️ کشف کلیدی — Packing:**
هزینه بسته‌بندی پیش‌فرض: ۱۰۰,۰۰۰ ریال. این فیلد هم در PRD غایب بود.
**نگاشت به دیتابیس:** این مقادیر در جدول `system_settings` ذخیره می‌شوند:
```sql
('vat_rate', 0.09, 'VAT rate — 9%'),
('packing_cost_default', 100000, 'Default packing cost in IRR'),
('profit_margin', 1.25, 'Profit margin — set per company'),
('aed_to_irr', [نرخ روز], 'AED to IRR exchange rate'),
```
---
### شیت ۱۴: `DATES` — تقویم میلادی به جلالی
**ساختار:** ۷۳۲ ردیف × ۱۲ ستون
**ستون‌ها:** Miladi، Jalali_1، Jalali_2، Jalali_3، myear، jyear، mmonthN، jmonthN، mmonthT، jmonthT، mnime، jnime
**نکته:** این جدول روش قدیمی IFNEX برای تبدیل تاریخ بوده. در لاراول نیازی به این نیست — از پکیج `morilog/jalali` استفاده می‌شود. این شیت در مهاجرت نادیده گرفته می‌شود.
---
## 🗂️ تحلیل شیت به شیت — فایل ترکینگ دستی
### شیت ۱: `Sheet1` — لیست رویدادهای ترکینگ
**ساختار:** ۳۱۱ ردیف × ۸ ستون
**ستون‌ها:**
1. `AWB` — شماره بارنامه IFNEX (مثلاً 980103619)
2. `Last State` — آخرین وضعیت (مثلاً "Failed attempt") — فقط در موارد خاص پر شده
3. `Date` — تاریخ رویداد
4. `Time` — زمان رویداد
5. `State` — توضیح رویداد (مثلاً "Picked up by Naqel")
6. `Country` — لوکیشن (مثلاً DUBAI، MUSCAT - OMAN)
7. `Location` — معمولاً "." (نقطه)
8. `Order No` — شماره ترتیب رویداد
**نمونه داده (بارنامه 980103619):**
```
AWB | Last State | Date | Time | State | Country | Location | Order No
980103619 | | 2026-06-20 | 06:35 | Waybill created. Shipment h... | SYSTEM | . | 9
980103619 | | 2026-06-24 | 02:56 | Picked up by Naqel | DUBAI | . | 10
980103619 | | 2026-06-24 | 03:01 | Arrived at Naqel Facility | DUBAI | . | 11
980103619 | | 2026-06-24 | 03:06 | Re-Weight | DUBAI | . | 12
980103619 | | 2026-06-24 | 07:05 | Out For Delivery with Courier | DUBAI | . | 13
980103619 | | 2026-06-24 | 07:57 | Prepared for delivery | DUBAI | . | 14
980103619 | | 2026-06-24 | 17:16 | Delivery attempted City/A... | AJMAN | . | 15
980103619 | | 2026-06-25 | 07:27 | Out For Delivery with Courier | DUBAI | . | 16
980103619 | | 2026-06-25 | 10:34 | Prepared for delivery | DUBAI | . | 17
980103619 | Failed attempt| 2026-06-25 | 19:43 | Delivery attempted Consig... | DUBAI | . | 18
```
**نگاشت به دیتابیس:** این شیت مستقیماً به جدول `shipment_tracking_events` نگاشت می‌شود:
- `AWB``shipment_id` (با lookup در `shipments`)
- `Date` + `Time``event_date` + `event_time`
- `State``event_description`
- `Country``location`
- `Last State``delivery_status` (اگر پر شده باشد)
- `Order No` → نادیده (ترتیب با timestamp مشخص می‌شود)
**نکته مهاجرت:** این داده‌ها می‌توانند در فاز ₀ به `shipment_tracking_events` با `source = 'manual'` مهاجرت داده شوند.
---
### شیت ۲: `Refrence` — نگاشت کد AWB به کد ترکینگ خارجی ⭐ حیاتی
**ساختار:** ۱۵ ردیف × ۸ ستون
**ستون‌ها:**
1. `Tracking Number` — کد ترکینگ شرکت خارجی
2. `Reference Number` — شماره AWB داخلی IFNEX
3. `Delivery` — وضعیت تحویل (مثلاً "Delivered")
4. `Booking Number` — شماره رزرو
5. `IM/EX` — شرکت حمل (DHL، Naghel، UPS، ...)
6. `To Country` — کشور مقصد
7. `Item Description` — توضیحات (گاهی شامل هزینه‌های اضافی مثل "100.00AED disposal charge")
8. `Delivery Agent` — عامل تحویل
**نمونه داده:**
```
Tracking Number | Reference Number | Delivery | Booking Number | IM/EX | To Country | Item Description
2329941040 | 980103609 | | | DHL | |
408638268 | 980103612 | | | Naghel | |
8252492625 | 980103613 | | | DHL | |
7095511345 | 980103614 | | | DHL | |
1Z483Y5W0491404336 | 980103079 | 991035590| | | China | UPS, 100.00AED disposal charge
```
**⚠️ کشف کلیدی — چندین شرکت حمل:**
این شیت نشان می‌دهد که شرکت‌های حمل زیر در سیستم IFNEX فعال هستند:
| کد | نام شرکت | نوع | پشتیبانی API |
|----|----------|-----|--------------|
| DHL | DHL Express | بین‌المللی | ✅ |
| FEDEX | FedEx | بین‌المللی | ✅ |
| UPS | UPS | بین‌المللی | ✅ |
| ARAMEX | Aramex | منطقه‌ای (خاورمیانه) | ✅ |
| NAGHEL / Naqel | Naqel | منطقه‌ای (امارات) | ⚠️ محدود |
| EMX | EMX | منطقه‌ای | ❌ |
| APSITEX | APSITEX | داخلی ایران | ❌ |
| IMPEX | IMPEX | منطقه‌ای | ❌ |
**نتیجه:** فقط حدود ۵۰-۶۰٪ مرسولات در فاز ۳ از طریق API قابل ترکینگ خودکار هستند. بقیه همچنان دستی می‌مانند. این موضوع در `Phase0_Proposal.md` بخش ۷.۴ لحاظ شده.
**⚠️ کشف کلیدی — هزینه‌های جانبی متغیر:**
ردیف ۱۵ نشان می‌دهد که گاهی هزینه‌های اضافی در زمان تحویل از سمت شرکت حمل اعمال می‌شود (مثلاً "100.00AED disposal charge"). این فیلد باید در `shipment_carrier_mappings.notes` ذخیره شود و در محاسبه سود نهایی لحاظ گردد.
**نگاشت به دیتابیس:** این شیت مستقیماً به جدول `shipment_carrier_mappings` نگاشت می‌شود:
- `Tracking Number``carrier_tracking_number`
- `Reference Number``shipment_id` (با lookup)
- `Booking Number``booking_number`
- `IM/EX``carrier_code` (تبدیل به enum)
- `To Country` → (نادیده یا ذخیره در metadata)
- `Item Description``notes`
- `Delivery``delivery_status`
---
### شیت ۳: `Paste` — داده خام کپی‌شده
**ساختار:** ۴۰۴ ردیف × ۱۱ ستون
**توضیح:** این شیت داده‌های خام کپی‌شده از وب‌سایت‌های DHL/FedEx/UPS را در خود دارد. اپراتور این داده‌ها را پردازش می‌کند و به `Sheet1` و `copy` منتقل می‌کند.
**ستون‌ها:** Tracking number (دو ستون)، شماره ترتیب N، اطلاعات Origin/Destination، Date، Local Time، Location، Event label، Delivery status
**نکته:** این شیت برای مهاجرت نادیده گرفته می‌شود — داده‌های تمیز در `Sheet1` و `copy` هستند.
---
### شیت ۴: `copy` — نسخه تمیزشده
**ساختار:** ۴۱۵ ردیف × ۹ ستون
**توضیح:** نسخه‌ی پاکسازی‌شده‌ی Sheet1 با فیلدهای مرتب‌تر. ستون‌ها:
1. Tracking number
2. AWB
3. Delivery status
4. Date
5. Local Time
6. Event label
7. Location
8. (خالی)
9. Order No
**نکته:** این شیت و `Sheet1` تکراری هستند. برای مهاجرت از `Sheet1` استفاده کنید چون کامل‌تر است.
---
### شیت ۵: `Delivered` — لیست مرسولات تحویل‌شده
**ساختار:** ۹۴ ردیف × ۸ ستون
**توضیح:** لیست مرسولاتی که با موفقیت تحویل داده شده‌اند. شامل:
- Tracking Number
- Reference Number (AWB)
- Sl No (معمولاً "Delivered" یا شماره)
- Booking Number
- Company Code (مثلاً 1012، IMPEX)
- To Country
- Item Description
- Delivery Agent
**نکته:** این شیت برای آمارگیری استفاده می‌شود. در دیتابیس نیازی به آن نیست — می‌توان با query روی `shipment_tracking_events` لیست مشابه تولید کرد.
---
### شیت ۶: `test` — آزمایش‌های اپراتور
**ساختار:** ۳۹۶ ردیف × ۸ ستون (با چند ساختار متفاوت در یک شیت)
**توضیح:** شیت آزمایشی اپراتور. داده‌ها معتبر نیستند و برای مهاجرت نادیده گرفته می‌شوند.
---
## 🔢 فرمول قیمت‌گذاری (Pricing Formula)
بر اساس شیت `Form` و `Assumptions`، فرمول کامل محاسبه قیمت نهایی:
### مراحل محاسبه
#### مرحله ۱: محاسبه وزن حجمی
```
Volumetric Weight (kg) = (Width × Length × Height) / 5000
```
- ابعاد به سانتی‌متر
- تقسیم بر ۵۰۰۰ استاندارد جهانی هوایی
#### مرحله ۲: تعیین وزن قابل پرداخت
```
Chargeable Weight = MAX(Actual Weight, Volumetric Weight)
```
#### مرحله ۳: استخراج زون
```
Zone = lookup(country, direction, type)
```
- اگر direction=export و type=PARCEL → از `countries.export_zone_parcel`
- اگر direction=export و type=DOC_* → از `countries.export_zone_doc`
- اگر direction=import و type=PARCEL → از `countries.import_zone_parcel`
- اگر direction=import و type=DOC_* → از `countries.import_zone_doc`
#### مرحله ۴: استخراج قیمت پایه (به درهم)
```
Base Price (AED) = shipping_rates[direction, type, chargeable_weight, zone]
```
#### مرحله ۵: اعمال ضریب سود
```
After Profit (AED) = Base Price × Profit Margin (مثلاً 1.25)
```
#### مرحله ۶: تبدیل به ریال
```
After Conversion (IRR) = After Profit (AED) × AED_to_IRR_Rate
```
#### مرحله ۷: اضافه هزینه‌های جانبی
```
Subtotal (IRR) = After Conversion
+ Extra Service (IRR)
+ Domestic Pickup (IRR)
+ Packing Cost (IRR)
+ Domestic Delivery (IRR)
+ Warehousing Cost (IRR)
- Discount (IRR)
```
#### مرحله ۸: اعمال VAT
```
Total Fee (IRR) = Subtotal × (1 + VAT Rate)
```
- VAT Rate = 0.09 (۹٪) — از شیت Assumptions
#### مرحله ۹: ذخیره خروجی‌ها
```
shipments.shipping_price = Base Price (AED)
shipments.net_dirham = After Profit (AED)
shipments.net_rial = Total Fee (IRR)
shipments.total_fee = Total Fee (IRR)
```
### نکته‌ی مهم: محموله‌های بالای ۳۰ کیلوگرم
طبق PRD قدیمی (بخش ۳.۱): «محموله‌های بالای ۳۰ کیلوگرم مشمول نرخ‌های ویژه (Spot Rate) هستند و محاسبه آنلاین ندارند و باید توسط ادمین تأیید شوند.»
این قانون باید در فرم استعلام قیمت (فاز ۱) لحاظ شود: اگر وزن > ۳۰ کیلوگرم، به‌جای محاسبه، پیام «کارشناس ما تماس می‌گیرد» نمایش داده شود.
### نمونه محاسبه واقعی (از شیت List، AWB 980100011)
```
Direction: Export (Outbound)
Type: NON DOC → PARCEL
From: Iran (IR)
To: China (CN)
Weight: 0.1 kg
Volumetric Weight: (25 × 15 × 3) / 5000 = 0.225 kg
Chargeable Weight: MAX(0.1, 0.225) = 0.225 kg → 0.5 (طبق اکسل)
Zone: 3 (از شیت List)
Items:
1. IC LT1668, HS: 8542390001, Qty: 104, Unit: $1.1, Total: $114
Invoice Total: $114
Net Dirham: 160.69
Net Rial: 73,113,385.32
محاسبه:
Base Price (AED) = ? (از جدول Parcel)
After Profit (AED) = Base × 1.25
Net Dirham = 160.69 → پس Base Price = 160.69 / 1.25 = 128.55 AED
AED_to_IRR = 73,113,385.32 / 160.69 ≈ 455,057 IRR per AED
```
این تحلیل نشان می‌دهد که **نرخ درهم به ریال در زمان این محموله حدود ۴۵۵,۰۰۰ ریال بوده**. این نرخ باید در `system_settings` ذخیره شود و قابل به‌روزرسانی باشد.
---
## 📋 قوانین رهگیری (Tracking Logic)
### نگاشت کد IFNEX به کد خارجی
مشتری IFNEX به مشتری یک کد اختصاصی می‌دهد (مثلاً `IFN-525` یا `980103619`). اما بسته ممکن است با کد دیگری (مثلاً `65986555` در DHL) ارسال شود.
**در دیتابیس:**
- `shipments.awb_no` = کد IFNEX (مثلاً 980103619)
- `shipment_carrier_mappings.carrier_tracking_number` = کد خارجی (مثلاً 408638240)
مشتری همیشه با کد IFNEX جستجو می‌کند. سیستم در پس‌زمینه:
- در فاز ₀: رویدادها را به‌صورت دستی در `shipment_tracking_events` با `source = 'manual'` ذخیره می‌کند
- در فاز ۳: با استفاده از کد خارجی، از API TrackingMore/17track رویدادها را دریافت و با `source = 'api_aggregator'` ذخیره می‌کند
### وضعیت‌های ممکن (Status Enum)
بر اساس شیت‌های ترکینگ، این وضعیت‌ها در سیستم IFNEX وجود دارند:
| وضعیت | توضیح | نگاشت به enum |
|-------|-------|---------------|
| Waybill created | بارنامه صادر شد | `processed` |
| Shipment picked up | بسته دریافت شد | `picked_up` |
| Processed at [Location] | در حال پردازش | `in_transit` |
| Shipment has departed | حرکت کرد | `in_transit` |
| Arrived at Sort Facility | رسید به مرکز دسته‌بندی | `in_transit` |
| Re-Weight | وزن‌مجدد | `in_transit` |
| Out For Delivery with Courier | در مسیر تحویل | `out_for_delivery` |
| Prepared for delivery | آماده تحویل | `out_for_delivery` |
| Delivery attempted | تلاش برای تحویل (ناموفق) | `failed` |
| Delivered | تحویل داده شد | `delivered` |
| Failed attempt | تلاش ناموفق | `failed` |
| Returned | برگشت خورده | `returned` |
این enum در `shipments.status` و `shipment_tracking_events.delivery_status` استفاده می‌شود.
---
## 🛠️ نکات مهاجرت داده (Migration Notes)
### اسکریپت مهاجرت — فاز ₀
این کارها باید توسط اسکریپت `HistoricalShipmentsImport` (در `04_Laravel/app/Imports/`) انجام شود:
#### ۱. مهاجرت کشورها (از شیت COUNTRIES و Zone)
```php
// خواندن شیت COUNTRIES برای استخراج نام و ISO
// خواندن شیت Zone برای استخراج زون‌های PARCEL و DOCUMENT
// درج در جدول countries با ۴ زون
```
#### ۲. مهاجرت مرسولات تاریخی (از شیت List)
```php
// خواندن ردیف ۳ به بعد (ردیف ۱ و ۲ سرتیتر)
// برای هر ردیف:
// - استخراج iso_code از نام کشور (مثلاً "Iran (IR)" → "IR")
// - تبدیل تاریخ میلادی به timestamp
// - تبدیل نوع سرویس (DocNor → DOC_NORMAL، NON DOC → PARCEL، ...)
// - درج در جدول shipments
```
#### ۳. مهاجرت نگاشت کدهای ترکینگ (از شیت Refrence)
```php
// برای هر ردیف:
// - lookup shipment_id با awb_no
// - تبدیل IM/EX به carrier_code enum
// - درج در جدول shipment_carrier_mappings
```
#### ۴. مهاجرت رویدادهای ترکینگ (از شیت Sheet1 — اختیاری)
```php
// برای هر ردیف:
// - lookup shipment_id با awb_no
// - parse Date و Time
// - درج در جدول shipment_tracking_events با source = 'manual'
```
### چالش‌های مهاجرت
۱. **نام کشورها با فرمت‌های متفاوت:**
- `Iran (IR)`، `IRAN`، `iran` — باید استاندارد شوند
- راه‌حل: استخراج iso_code از پرانتز، یا fuzzy matching با جدول countries
۲. **فیلد Type متناقض:**
- مقادیر اکسل: `DocNor`، `DocEco`، `NON DOC`، `Outbound`، `Inbound`، خالی
- مقادیر DB: `DOC_NORMAL`، `DOC_ECONOMY`، `PARCEL`
- راه‌حل: mapping table در اسکریپت مهاجرت
۳. **فیلد Service متناقض:**
- مقادیر اکسل: `Outbound`، `Inbound`، خالی
- مقادیر DB: `import`، `export`
- راه‌حل: `Outbound → export`، `Inbound → import`
۴. **ردیف‌های خالی زیاد:**
- در شیت List، حدود نیمی از ردیف‌ها خالی هستند
- راه‌حل: `if (empty($row['awb_no'])) continue;`
۵. **تاریخ‌ها با فرمت متنوع:**
- بعضی ردیف‌ها `2020-05-02`، بعضی `2020-05-02 00:00:00`، بعضی خالی
- راه‌حل: Carbon::parse() با مدیریت خطا
۶. **نام شرکت‌های حمل با املای متفاوت:**
- `DHL`، `dhl`، `DHL Express`، `DHL8336801664`
- راه‌حل: استانداردسازی به enum
### تست مهاجرت
پس از مهاجرت، این تست‌ها باید انجام شوند:
```bash
# ۱. شمارش رکوردها
php artisan tinker
>>> Shipment::count(); # باید حدود 3950 باشد
# ۲. نمونه‌گیری تصادفی 50 رکورد
>>> Shipment::inRandomOrder()->take(50)->get();
# دستی با اکسل مقایسه شود
# ۳. بررسی یکتایی AWB
>>> Shipment::where('awb_no', '980100011')->count(); # باید 1 باشد
# ۴. بررسی روابط
>>> Shipment::find(1)->carrierMappings;
>>> Shipment::find(1)->trackingEvents;
```
---
## 📊 خلاصه‌ی نگاشت شیت‌ها به جداول دیتابیس
| شیت اکسل | جدول DB | فاز | یادداشت |
|----------|---------|-----|---------|
| `COUNTRIES` | `countries` | ۰ | برای name و iso_code |
| `Zone` | `countries` | ۰ | برای ۴ زون (منبع نهایی) |
| `List` | `shipments` | ۰ | ۳۹۵۰ رکورد تاریخی |
| `List` (۹ ردیف کالای گمرکی) | `shipment_items` | ۱ | در فاز ۱ ساخته می‌شود |
| `Form` | (قالب رابط کاربری) | ۱ | فرم ثبت سفارش |
| `Import Rate` | `shipping_rates` | ۱ | برای type=DOC_NORMAL/PARCEL |
| `Export Rate` | `shipping_rates` | ۱ | برای type=DOC_NORMAL/PARCEL |
| `DocNor` | `shipping_rates` | ۱ | برای type=DOC_NORMAL |
| `DocEco` | `shipping_rates` | ۱ | برای type=DOC_ECONOMY |
| `Parcel` | `shipping_rates` | ۱ | برای type=PARCEL |
| `Assumptions` | `system_settings` | ۰ | VAT، Packing، Profit Margin |
| `AWB` | (قالب PDF) | ۱ | تولید PDF با Dompdf |
| `INVOICE + label` | (قالب PDF) | ۱ | تولید PDF |
| `label` | (قالب PDF) | ۱ | تولید PDF برای چاپگر حرارتی |
| `DATES` | — | — | نادیده (از morilog/jalali استفاده می‌شود) |
| `Start` | — | — | نادیده (خالی) |
**از فایل ترکینگ دستی:**
| شیت اکسل | جدول DB | فاز | یادداشت |
|----------|---------|-----|---------|
| `Sheet1` | `shipment_tracking_events` | ۰ | مهاجرت رویدادهای تاریخی |
| `Refrence` | `shipment_carrier_mappings` | ۰ | مهاجرت نگاشت‌ها |
| `Paste` | — | — | نادیده (داده خام) |
| `copy` | — | — | نادیده (تکراری با Sheet1) |
| `Delivered` | — | — | نادیده (با query قابل تولید) |
| `test` | — | — | نادیده (آزمایشی) |
---
## ⚠️ ریسک‌ها و هشدارهای داده‌ای
### ۱. کیفیت داده‌های تاریخی
- حدود ۲۰٪ رکوردها فیلدهای کلیدی خالی دارند (مخصوصاً اطلاعات گیرنده)
- تاریخ‌ها گاهی به‌صورت متن ذخیره شده‌اند (نه DateTime)
- نام کشورها گاهی با غلط املایی هستند (مثلاً `Americam Samoa` به‌جای `American Samoa`)
**راهکار:** اسکریپت مهاجرت باید دارای validation و reporting باشد — تعداد ردیف‌های نامعتبر را گزارش کند.
### ۲. تضاد قیمت‌ها
- شیت Import/Export Rate فقط ۲ نوع دارد (DOCUMENT/NON-DOC)
- شیت‌های DocNor/DocEco/Parcel قیمت‌های متفاوتی دارند (۳ نوع)
**راهکار:** در فاز ۱، با مشتری تأیید کنید کدام منبع معتبر است. فعلاً در فاز ₀، جدول `shipping_rates` را با enum ۳-حالته طراحی کنید تا آماده هر سناریو باشد.
### ۳. زون‌های Import
- شیت `Zone` فقط زون‌های export دارد
- زون‌های import باید از شیت `COUNTRIES` استخراج شوند (اما فقط ۱ زون import دارد، نه ۲)
**راهکار:** در فاز ₁، با مشتری درباره زون‌های import پارسل/داکیومنت صحبت کنید. فعلاً در فاز ₀، فیلدهای `import_zone_parcel` و `import_zone_doc` را با مقدار یکسان از `COUNTRIES` پر کنید.
### ۴. فیلدهای مالی گاهی خالی
- در شیت List، بسیاری از فیلدهای مالی (`shipping_price`، `extra_service`، ...) صفر یا خالی هستند
- ممکن است داده‌های مالی واقعی در سیستم حسابداری جداگانه‌ای باشد
**راهکار:** در مهاجرت، فیلدهای خالی را به `null` تبدیل کنید، نه به `0`. این کمک می‌کند تمایز بین «صفر واقعی» و «داده گم‌شده» حفظ شود.
---
## 🎯 جمع‌بندی برای نمونه‌ی جدید
اگر نمونه‌ی جدیدی هستی که این فایل را می‌خوانی، نکات کلیدی زیر را به خاطر بسپار:
۱. **فایل‌های اکسل دو نوعند:** فایل اصلی (`4_5989927490271846355.xlsx`) و فایل ترکینگ دستی (`Data entry 2026-06-28.xlsx`).
۲. **شیت `Zone` منبع نهایی زون‌هاست** — ۴ زون مجزا (PARCEL × DOCUMENT × Export × Import).
۳. **۳ نوع سرویس وجود دارد:** DocNor، DocEco، Parcel — نه ۲ نوع.
۴. **VAT = ۹٪** و **Packing Cost پیش‌فرض = ۱۰۰,۰۰۰ ریال** — از شیت Assumptions.
۵. **۹ ردیف کالای گمرکی** در هر مرسوله — برای فرم ثبت سفارش (فاز ₁).
۶. **۳۹۵۰ رکورد تاریفی** در شیت List — باید در فاز ₀ مهاجرت داده شوند.
۷. **۸ شرکت حمل فعال:** DHL، FedEx، UPS، Aramex، Nagel، EMX، APSITEX، IMPEX — فقط ۴ تای اول API دارند.
۸. **هزینه‌های جانبی متغیر** گاهی در فیلد description شیت Refrence ذخیره شده‌اند (مثلاً "100AED disposal charge").
۹. **محموله‌های بالای ۳۰ کیلوگرم** مشمول نرخ ویژه (Spot Rate) هستند — محاسبه آنلاین ندارند.
۱۰. **PDFها باید انگلیسی باشند** — مطابق اکسل اصلی. اما پنل ادمین و رابط کاربری فارسی است.
برای جزئیات بیشتر درباره اسکیمای دیتابیس و فرمول قیمت‌گذاری، فایل `Phase0_Proposal.md` بخش ۶ و ۷ را بخوانید.
---
© 2026 VernaSoft Group. Internal use only.