# Specification رفع مشکل Minimum Order و وزن‌های اعشاری در Price List Import

## هدف

در سیستم فعلی Import لیست قیمت، برای بعضی محصولات فله‌ای مقدار Minimum Order اشتباه Parse می‌شود. نمونه مشخص:

```text
محصول: لوکوم گل محمدی مغزدار
Minimum Order صحیح: 3.5 kg
مقدار فعلی سیستم: 4 kg
```

در دیتابیس فعلی، محصول `id=270` با نام `لوکوم گل محمدی مغزدار` دارای مقدار Legacy زیر است:

```text
minimum_weight = 3.50
dimensions = kg
```

پس مقدار صحیح 3.5 کیلوگرم است و نباید به 4 تبدیل شود.

---

# 1. دستور اصلی

این مشکل را فقط با تغییر مقدار دیتابیس حل نکن. **ریشه Bug را در کل Pipeline پیدا و اصلاح کن.**

مسیر کامل داده باید بررسی شود:

```text
PDF / OCR
    ↓
Raw extracted value
    ↓
Persian/Text Normalizer
    ↓
Number/Weight Parser
    ↓
PriceListItem
    ↓
Product Matcher
    ↓
Apply Service
    ↓
Product
    ↓
API Resource / UI
```

در هر مرحله مشخص کن مقدار واقعی چه بوده و آیا جایی تبدیل شده است یا نه.

---

# 2. Rule قطعی برای وزن

برای فیلدهای وزنی زیر:

```text
wholesale_min_order_kg
extracted_min_order_kg
wholesale_base_weight
extracted_base_weight
```

هرگز از این عملیات استفاده نکن:

```php
round($value);
ceil($value);
floor($value);
(int) $value;
intval($value);
sprintf('%d', $value);
```

این عملیات باعث از بین رفتن بخش اعشاری می‌شوند یا مقدار را به عدد دیگری تبدیل می‌کنند.

مثال:

```php
3.5
```

نباید هیچ‌وقت تبدیل شود به:

```text
4
```

یا:

```text
3
```

---

# 3. نوع داده

## Database

برای وزن:

```sql
DECIMAL(12,3)
```

استفاده شود.

از `FLOAT` برای ذخیره مقدار تجاری وزن در Database استفاده نکن.

## PHP

بعد از Parse مقدار به `float` تبدیل شود:

```php
$value = (float) $value;
```

## Laravel Cast

در Model:

```php
protected $casts = [
    'wholesale_min_order_kg' => 'float',
    'wholesale_base_weight' => 'float',
    'extracted_min_order_kg' => 'float',
    'extracted_base_weight' => 'float',
];
```

---

# 4. پشتیبانی از ممیزهای مختلف

در متن فارسی ممکن است مقدار 3.5 به شکل‌های مختلف دیده شود:

```text
3.5
۳.۵
3٫5
۳٫۵
3/5
۳/۵
```

در **context وزن** همه باید به یک مقدار Canonical تبدیل شوند:

```text
3.5
```

مثال:

```text
۳/۵ کیلو
```

باید تبدیل شود به:

```php
[
    'value' => 3.5,
    'unit' => 'kg',
]
```

### نکته مهم درباره `/`

کاراکتر `/` فقط وقتی می‌تواند به عنوان ممیز اعشاری تفسیر شود که Parser قبلاً مطمئن شده باشد متن مربوط به **Weight / Minimum Order** است.

مثلاً:

```text
۳/۵ کیلو
```

معتبر است.

اما در قیمت‌ها نباید:

```text
123/456
```

را کورکورانه به `123.456` تبدیل کرد.

---

# 5. نرمال‌سازی اعداد

این موارد باید پشتیبانی شوند:

```text
۴۰۰ → 400
٤٠٠ → 400
400 → 400
۳٫۵ → 3.5
۳.۵ → 3.5
3٫5 → 3.5
3.5 → 3.5
۳/۵ → 3.5
3/5 → 3.5
```

قبل از Parse، اعداد فارسی و عربی به ASCII تبدیل شوند.

---

# 6. تست اجباری WeightParser

برای `WeightParser` تست Unit/Feature اضافه کن.

مثال:

```php
it('parses fractional kilogram written with slash', function () {
    $result = $parser->parse('حداقل خرید ۳/۵ کیلو');

    expect($result['value'])->toBe(3.5);
    expect($result['unit'])->toBe('kg');
});
```

همچنین:

```php
it('does not round fractional weight', function () {
    $value = 3.5;

    expect((float) $value)->toBe(3.5);
});
```

---

# 7. Minimum Order از Base Weight جدا است

این دو مفهوم نباید هیچ‌وقت با هم قاطی شوند.

## فله

مثال:

```text
minimum_order_kg = 3.5
base_weight = 1
base_weight_unit = kg
```

این یعنی:

```text
حداقل خرید = 3.5 کیلو
مبنای قیمت/وزن پایه = 1 کیلو
```

نباید از `base_weight` برای ساخت Minimum Order استفاده شود.

## بسته‌ای

مثال:

```text
minimum_order_count = 10
base_weight = 400
base_weight_unit = g
```

---

# 8. Minimum Order باید مستقیم از Source خودش Parse شود

اگر PDF حاوی:

```text
۳/۵ کیلو
```

است:

```text
raw PDF value
   ↓
۳/۵
   ↓
3.5
   ↓
wholesale_min_order_kg = 3.5
```

نباید چنین Pipelineای وجود داشته باشد:

```text
۳/۵
↓
3.5
↓
round()
↓
4
```

همچنین این کار هم ممنوع است:

```text
minimum order = base weight * something
```

مگر اینکه Business Rule صراحتاً چنین چیزی تعریف کرده باشد؛ در سیستم فعلی چنین Ruleای وجود ندارد.

---

# 9. Source Tracking برای پیدا کردن Bug

برای هر مقدار Parsed، اطلاعات Source را در `raw_data` نگه دار.

مثال:

```json
{
  "minimum_order": {
    "raw": "۳/۵ کیلو",
    "normalized": "3.5 کیلو",
    "value": 3.5,
    "unit": "kg",
    "source": "description"
  }
}
```

اگر از سلول جدول استخراج می‌شود:

```json
{
  "minimum_order": {
    "raw": "۳/۵",
    "value": 3.5,
    "unit": "kg",
    "source": "table_cell",
    "row_index": 12,
    "column": "description"
  }
}
```

هدف: بتوانیم دقیقاً بفهمیم مقدار `3.5` در کدام مرحله به `4` تبدیل شده است.

---

# 10. Debug الزامی برای محصول مورد نظر

محصول:

```text
id = 270
name = لوکوم گل محمدی مغزدار
```

مقدار صحیح Legacy:

```text
minimum_weight = 3.50
dimensions = kg
```

هنگام Import/Apply برای این محصول مقدارهای زیر را Trace کن:

```text
raw_value
normalized_value
parsed_value
parsed_unit
minimum_order_kg
base_weight
base_weight_unit
source_row
source_column
match_method
matched_product_id
```

در صورت امکان Log موقت یا Structured Debug ایجاد کن تا مقدار هر مرحله قابل مشاهده باشد.

---

# 11. Match نباید باعث تغییر وزن اشتباه شود

Product Matching و Weight Parsing دو مسئولیت جدا هستند.

Match فقط مشخص کند:

```text
PDF Row -> Product ID
```

Weight Parser مشخص کند:

```text
PDF Row -> minimum/base weight
```

بعد از Match، مقدار وزن استخراج‌شده نباید مجدداً از نام محصول دیگری خوانده شود.

---

# 12. Legacy Fields

در حال حاضر `products` دارای این فیلدهای Legacy است:

```text
weight
minimum_weight
dimensions
```

این Bug را با اتکا به `minimum_weight` و `dimensions` حل نکن؛ ابتدا مشخص کن منطق جدید از کدام فیلد استفاده می‌کند.

منطق جدید باید بر اساس فیلدهای مشخص زیر باشد:

```text
wholesale_sale_mode
wholesale_min_order_kg
wholesale_min_order_count
wholesale_base_weight
wholesale_base_weight_unit
```

---

# 13. Acceptance Test اصلی

این تست باید حتماً Pass شود:

```text
Product:
لوکوم گل محمدی مغزدار

Input / PDF:
3.5 kg

Expected:
Database = 3.500
PHP = 3.5
API = 3.5
UI = 3.5 kg
```

---

# 14. Regression Tests

حداقل این موارد را تست کن:

```text
3 kg    → 3.0
3.5 kg  → 3.5
4 kg    → 4.0
4.5 kg  → 4.5
7.25 kg → 7.25
```

و برای فرمت‌های فارسی:

```text
۳ کیلو      → 3.0 kg
۳/۵ کیلو    → 3.5 kg
۴/۵ کیلو    → 4.5 kg
۷/۲۵ کیلو   → 7.25 kg
```

هیچ‌کدام نباید Round شوند.

---

# 15. Regression برای واحدها

این‌ها باید همگی Canonical=`kg` شوند:

```text
کیلو
کیلوگرم
کیلو گرم
کيلو
کيلوگرم
kg
KG
Kg
```

و این‌ها Canonical=`g`:

```text
گرم
گرمی
g
gr
gram
```

---

# 16. خروجی مورد انتظار از AI

لطفاً فقط یک تغییر سطحی انجام نده.

این موارد را تحویل بده:

1. Root cause دقیق Bug.
2. فایل‌ها/کلاس‌هایی که باعث تبدیل `3.5` به `4` شده‌اند.
3. کد اصلاح‌شده.
4. Migration در صورت نیاز.
5. تغییرات Model/ casts در صورت نیاز.
6. تست‌های جدید WeightParser و Import.
7. تست Regression برای `3.5`.
8. توضیح اینکه مقدار `3.5` از PDF دقیقاً از کدام Source استخراج می‌شود.
9. اطمینان از اینکه UI/API نیز مقدار صحیح `3.5` را نمایش می‌دهند.
10. اصلاح Specification/MD اصلی پروژه در صورتی که Rule وزن اعشاری در آن ناقص است.

---

# 17. Definition of Done

این Task فقط زمانی Done است که همه موارد زیر برقرار باشند:

- `3.5` به `4` تبدیل نشود.
- `3.5` به `3` تبدیل نشود.
- وزن‌های اعشاری در PHP به‌صورت float قابل استفاده باشند.
- وزن در DB با DECIMAL ذخیره شود.
- `/` در context وزن به عنوان decimal separator پشتیبانی شود.
- `3/5 kg` دقیقاً به `3.5 kg` تبدیل شود.
- Minimum Order و Base Weight کاملاً جدا باشند.
- Match باعث تغییر مقدار وزن نشود.
- API مقدار صحیح را برگرداند.
- UI مقدار صحیح را نمایش دهد.
- محصول `id=270` با Minimum Order برابر `3.5 kg` بدون Round پردازش شود.
- تست Regression وجود داشته باشد.
- Root cause مشخص و مستند شده باشد.

---

# 18. نتیجه‌ی تحلیل و ریشه‌یابی (Root Cause Analysis)

## 18.1 زنجیره‌ی داده برای محصول `لوکوم گل محمدی مغزدار گردویی`

خروجی واقعی اجزای Pipeline برای ردیف PDF زیر:

```text
لوکوم گل محمدی مغزدار گردویی | چهار جعبه 3/5 کیلوگرم
```

| مرحله | مقدار |
| --- | --- |
| raw PDF | `چهار جعبه 3/5 کیلوگرم` |
| Normalizer | `چهار جعبه 3/5 کیلوگرم` (اسلش حفظ می‌شود تا در context وزن تفسیر شود) |
| PdfPriceListParser (Legacy) | `base_quantity = 3.5`, `base_unit = kg`, `minimum_purchase_quantity = 4`, `minimum_purchase_unit = count` |
| WeightParser / MinimumOrderParser | `3/5 => 3.5` (ممیز اعشاری، نه کسر) |
| PriceListItem (`extracted_minimum_weight` = legacy؛ `extracted_base_weight`/`extracted_min_order_kg` = مسیر جدید) | `3.5` |
| Product (`minimum_weight` / `wholesale_base_weight`) | `3.500` (DECIMAL) |
| PHP (cast) | `3.5` |
| UI | `3.5 کیلوگرم` |

## 18.2 دو ریشه‌ی مستقل برای دیده‌شدن عدد `4`

### باگ ۱ (اصلی) — رُند شدن در نمایش UI

دیتابیس درست `3.5` را نگه می‌داشت، اما ویوهای فروشگاه مقدار را با
`number_format($value)` (بدون آرگومان اعشار) نمایش می‌دادند و PHP به‌صورت
پیش‌فرض صفر رقم اعشار می‌کند، پس `3.5` به `4` رُند می‌شد:

```text
number_format(3.5) => "4"
```

فایل‌های آسیب‌دیده:

```text
resources/views/site/pages/shop/show.blade.php   (وزن هر جعبه)
resources/views/site/pages/my-cart.blade.php     (وزن هر جعبه)
resources/views/site/pages/checkout.blade.php    (وزن در توضیح قلم سفارش)
```

اصلاح: هلپر `formatWeight()` که وزن را با حفظ اعشار (تا ۳ رقم) برمی‌گرداند:

```text
formatWeight(3.5) => "3.5"
formatWeight(7.25) => "7.25"
formatWeight(400) => "400"
```

### باگ ۲ (ثانویه) — ممیز اسلش در `WeightParser` / `MinimumOrderParser`

کلاس‌های جدید `App\Services\PriceList\WeightParser` و
`App\Services\PriceList\MinimumOrderParser` فرمت `۳/۵` را Decimal نمی‌خواندند:

```text
WeightParser       "۳/۵ کیلو" => value = 3 (اشتباه)
MinimumOrderParser "۳/۵ کیلو" => kg = 5    (اشتباه)
```

دلیل: الگوی دوم (`(\d+)/(\d+)`) به‌صورت `$1 + $2/10` حساب می‌کرد و در نهایت
هم فقط عدد دوم دیده می‌شد. اصلاح: در context وزن، اسلش بین دو رقم به ممیز
اعشاری تبدیل می‌شود (`3/5 => 3.5`, `7/25 => 7.25`) و واحدهای فارسی/عربی و
حرف‌های `ي/ك` نیز قبل از تشخیص واحد نرمال می‌شوند.

## 18.3 فایل‌های تغییر یافته

```text
app/Helpers/helper.php                              (formatWeight)
app/Services/PriceList/WeightParser.php             (اسلش + نرمال‌سازی واحد)
app/Services/PriceList/MinimumOrderParser.php       (اسلش + نرمال‌سازی حروف)
resources/views/site/pages/shop/show.blade.php      (نمایش وزن)
resources/views/site/pages/my-cart.blade.php        (نمایش وزن)
resources/views/site/pages/checkout.blade.php       (نمایش وزن)
tests/Unit/WeightParserTest.php                     (تست Regression)
tests/Feature/WholesaleWeightDisplayTest.php        (تست Import + UI)
```

## 18.4 تست‌های Regression

```text
3 kg    => 3.0
3.5 kg  => 3.5
4 kg    => 4.0
4.5 kg  => 4.5
7.25 kg => 7.25

۳ کیلو    => 3.0 kg
۳/۵ کیلو  => 3.5 kg
۴/۵ کیلو  => 4.5 kg
۷/۲۵ کیلو => 7.25 kg
```

هیچ‌کدام رُند نمی‌شوند و تست `WholesaleWeightDisplayTest` تضمین می‌کند صفحه‌ی
محصول مقدار `3.5 کیلوگرم` را نشان دهد و `4 کیلوگرم` را نشان ندهد.
