# WP4 — رسالة رفض الصرف الزائد + تثبيت قرار العميل باختبارات

**التذكرة:** ISS-2026-9260 · **الريبو:** BE فقط · **[FIN]** (بيمسّ بوابة الصرف الزائد — كميات وحجز وWIP) · **migration:** لا

---

## الخلفية — قرار العميل (portal step 6242، مثبّت في LEDGER)

العميل حكم على الصرف الزائد لما الصرف بيتم من **إذن صرف يدوي** مربوط بأمر إنتاج:

| الحالة | القرار |
|---|---|
| **1. الأمر مالوش خطة خامات إطلاقًا** (وده حال «الأمر المؤقت» بالظبط: منتج + كمية + وحدة بلا materials) | **البوابة لا تُطبَّق — الصرف حر** |
| **2. الأمر عنده خطة خامات** | الخيار (ب): **يُرفض** الزائد **برسالة واضحة** توجّه المستخدم لطلب اعتماد صرف زائد من **شاشة أمر الإنتاج** |

## الوضع الحالي في الكود — **السلوك صح بالفعل، الناقص هو الرسالة + تثبيت السلوك**

- **الحالة 1 شغّالة بلا أي تعديل.** `checkOverIssueAllowance` (`IssueMaterials.php` :726-729) و`consumeOverIssueAllowances` (:812-815) **الاتنين** بيعملوا `continue` للسطر اللي `material === null`. الأمر المؤقت مالوش صفوف خامات ⇒ كل سطوره `material = null` ⇒ متخطّاة ⇒ **صرف حر**. ✅
- **الحالة 2 بترفض بالفعل.** مسار الـlistener بيستدعي `applyEffects(..., $enforceOverIssue)` (`ApplyMaterialIssueOnApproval.php` آخر سطر)، و`applyEffects` :321-322 بينادي `consumeOverIssueAllowances` اللي بيرمي `ValidationException` لما السماحية المعتمدة ماتغطّيش الزيادة (:851-855). الرمي جوّه transaction الاعتماد ⇒ بيرجّع حركة المخزون. ✅
- **الناقص:** الرسالة الحالية `production::production.over_issue_allowance_exhausted` بتقول بس «السماحية المعتمدة غير كافية: المطلوب X والمتبقّي Y» — **مابتقولش لأي صنف**، و**مابتوجّهش** المستخدم لمكان طلب الاعتماد. أمين المخزن اللي بيعتمد إذن الصرف بيقف قدام رسالة مايعرفش منها لا الصنف ولا الخطوة التالية (وده الـHIGH-2 اللي بعتّه للعميل: زرار طلب الصرف الزائد في **شاشة الإنتاج** مش شاشة المخزن).

---

## المطلوب (Goal)

1. رسالة رفض تحدّد **الصنف** و**رقم أمر الإنتاج** وتوجّه صراحةً لطلب اعتماد صرف زائد من شاشة أمر الإنتاج — بالعربي والإنجليزي.
2. اختبارات تثبّت **الحالتين** بالظبط زي ما العميل قرّرهم، عشان مايتكسروش لاحقًا بالغلط.

## الملفات

- `Modules/Production/app/Actions/IssueMaterials.php` → `consumeOverIssueAllowances()` (الـthrow عند :851-855) — أضف اسم الصنف ورقم الأمر للـplaceholders.
  - اسم الصنف: `$material->product?->getLocalizedName()` — **انتبه**: `$materials[$mid]` جاي من `$line['material']`؛ في مسار الـlistener الـmaterial متحمّل بـ`$order->materials()->where(...)->first()` **بدون** `product`، فـلازم تتأكد إن العلاقة متحمّلة قبل الاستخدام (زي ما `checkOverIssueAllowance` :770-772 بيعمل بالظبط بـ`relationLoaded('product') ? ... : optional($material->product()->first())->getLocalizedName()`). استعمل **نفس** الأسلوب — ماتعملش N+1 جديد لكل سطر ولا تكسر لما العلاقة مش محمّلة.
  - رقم الأمر: `$order->order_number`.
- `Modules/Production/lang/ar/production.php` + `Modules/Production/lang/en/production.php` → المفتاح `over_issue_allowance_exhausted` (:123 في الاتنين).
  - ⚠️ المفتاح ده **مستخدم في المسارين** (المباشر من شاشة الإنتاج + اليدوي من إذن الصرف). التوجيه لـ«شاشة أمر الإنتاج» صحيح في الحالتين، فمفيش داعي لمفتاح تاني — **مفتاح واحد يخدم الاتنين**.
  - النص العربي المقترح (عدّله لو لقيت أفضل، بس لازم يحتوي الصنف + رقم الأمر + التوجيه):
    `'الصنف «:product» يتجاوز الكمية المخططة في أمر الإنتاج :order بمقدار :excess، والسماحية المعتمدة المتبقّية :remaining. اطلب اعتماد صرف زائد من شاشة أمر الإنتاج ثم أعد الاعتماد.'`
  - الإنجليزي المقابل بنفس الـplaceholders.
- **⛔ ماتغيّرش المنطق نفسه** — لا حدود الـtolerance (`+ 0.0005`)، ولا الـ`continue` على `material === null` (ده **بالظبط** الحالة 1 اللي العميل طلبها)، ولا ترتيب الخصم oldest-first، ولا الـ`lockForUpdate`.

## معايير القبول

- الرسالة تحتوي: اسم الصنف المحلّي · رقم أمر الإنتاج · الزيادة · المتبقّي من السماحية · جملة توجيه لشاشة أمر الإنتاج.
- الرسالة تتبني بلا استعلام إضافي لما `product` محمّلة، وماترميش خطأ لما مش محمّلة.
- سلوك الحالة 1 (بلا خطة) **ماتغيّرش ولا حرف**.
- المفتاح شغّال في اللغتين، والـplaceholders كلها متبدّلة (مافيش `:product` ظاهر خام).

## الاختبارات

في `Modules/Production/tests/Feature/ManualIssueAgainstOrderTest.php` (الملف بتاع WP1 — **امتداد له، مش ملف جديد**، لأن نفس السيناريو: إذن صرف يدوي مربوط بأمر):

1. **حالة 1 — أمر بلا خطة خامات:** أمر Released بلا صفوف `ProductionOrderMaterial` + إذن صرف يدوي مربوط بيه فيه صنف بأي كمية ⇒ الاعتماد **ينجح**، `MfgMaterialIssue` بيتعمل، المخزون بينقص، **ومفيش أي `ProductionOverIssueApproval` بيتقري ولا يترمي 422** — حتى والإعداد `production.over_issue_requires_approval` **مفعّل (true)**. ← ده أهم اختبار في الـWP: هو اللي بيمنع كسر «الأمر المؤقت».
2. **حالة 2 — أمر عنده خطة والكمية زايدة بلا سماحية:** planned=10, consumed=0, إذن صرف بـ15 ⇒ الاعتماد **يترفض** بـ`ValidationException`، والرسالة **بتحتوي اسم الصنف ورقم الأمر**، و**المخزون مارجعش ناقص** (الترانزاكشن اترجعت) و**مفيش `MfgMaterialIssue`** اتعمل.
3. **حالة 2 — الزيادة مغطّاة بسماحية معتمدة:** نفس السيناريو + `ProductionOverIssueApproval` بحالة approved و`requested_quantity=5, used_quantity=0` ⇒ الاعتماد **ينجح**، والسماحية `used_quantity` بقت 5.
4. **حالة 2 — الصنف المصروف مش ضمن خطة الأمر** (الأمر عنده خامات تانية بس): ⇒ **يعدّي حر** (لأن `material === null` للسطر ده) — **ثبّت السلوك ده باختبار صريح**، لأني بلّغت العميل عكسه غلط ولازم يفضل مرصود.
5. الإعداد `production.over_issue_requires_approval` = **false** + زيادة عن الخطة ⇒ يعدّي بلا رفض.

**شغّل بـCLI php صراحةً** (الـphp الافتراضي هنا php-cgi وبيقتل الرَنَر بصمت — exit 255 بلا أي مخرجات):
```bash
cd /home/moonui2/moon-erp-be && /opt/cpanel/ea-php82/root/usr/bin/php -d memory_limit=1G vendor/bin/pest Modules/Production/tests
```
**الأساس (baseline) — ماتتعدّاهوش:** Production **594 passed / 10 failed** (العشرة الموجودين أصلًا: 6 `ProductionVarianceTest`، 3 `CostAiAnomalyVarianceTest`، 1 `ConsignmentFoundationTest`).

## خارج النطاق

- ⛔ نقل زرار «طلب صرف زائد» لشاشة إذن الصرف — ده HIGH-2 وقرار منتج لسه مش مطلوب في العقد المجمّد. الرسالة بتوجّه لشاشة الإنتاج وبس.
- ⛔ `ApplyMaterialIssueOnApproval.php` · `Store/UpdateInventoryIssueRequest.php` · أي ملف FE · أي شيء بتاع WP2 (الإعداد + الأمر المؤقت + المنتقي).
- ⛔ إصلاح «البوابة بتقيس بوحدة السطر مقابل خطة بالوحدة الأساسية» — **متبلّغ للعميل كمتبقٍّ** ومش في نطاق الـWP ده.

## بعد الانتهاء

`./vendor/bin/pint` على الملفات الملموسة + `chown moonui2:moonui2` عليها. **بلا commit وبلا git** — سيب الشجرة dirty.
