# PROJECT_MEMORY — moon4 (Issues Portal agent)

> كاش محلي للسرعة/الـoffline. **المرجع الرسمي هو المنصّة** (`/api/issues/{ref}/context` + الـKB) — لو فيه تعارض، المنصّة هي اللي تتبع.
> ممنوع كتابة أي أسرار/توكنات هنا.

## هويّة الشغل

| | |
|---|---|
| المشروع على المنصّة | **moon4 — `project=291`** (مؤكّد من `GET /api/projects`) |
| الـBASE_URL | `https://portal.elbaset.com` |
| التوكن | في متغيّر البيئة `PORTAL_TOKEN` — **لا يُطبع ولا يُكتب هنا** |
| السورس كود المحلي | `/home/moonui4/public_html` — Moon ERP: FE `moon-erp/` (Angular 21) · BE `../moon-erp-be/` (Laravel 12, 17 modules) · storefront `moon-erp/projects/storefront/` |
| قواعد الريبو | `CLAUDE.md` في الجذر + per-module `CLAUDE.md` — تُقرأ قبل لمس أي موديول |

⚠️ كل تعامل مع المنصّة = **HTTP API فقط**. ممنوع mysql/artisan/ملفات المنصّة. المحلي يُلمس للتحليل والتنفيذ بس.

## البروتوكول المعتمد (من `/api/agent/manual`)

**دورة الحياة:** `new → analysis → awaiting_client → in_implementation → verification → client_review → closed`
- `/analysis` من `new`/`analysis` بس (غيره 409) — وأي سؤال قرار **لازم** يبقى في `questions[]` المهيكلة، مش نص جوه الـmarkdown.
- `/verification` من `in_implementation` بس — وهو **الطريق الوحيد** لإقفال شغل. ممنوع نشر تقرير إنجاز كـanalysis.
- `/comment` محايد للحالة — لطلب صورة/مثال بس، **مش** لإدارة دورة الحياة (تذكرة `new` بتتكلم فيها بـcomment = بتستنّى للأبد).
- **أول نداء في أي تذكرة: `GET /api/issues/{ref}/context`** (تفاصيل + الأم + الإخوة + task_kb + project_kb + كل البوابات في نداء واحد).

**البوابات اللي بتتقري قبل أي شغل:** `agent_hint` (أمر مُلزِم خفي عن العميل) · `feature_gate` (عقد نطاق إجباري) · `planning_gate` (repro + سبب جذري + مصفوفة بيئات قبل أي verification) · `ping_pong` (≥3 جولات = إعادة تأطير) · `scope_creep` (warning = اقترح التقسيم) · `scope_freeze` (العقد المثبّت هو المرجع) · `workflow_alert` (تصحيح ذاتي).

**تقييم الحجم (5 معايير):** 0–1 = تويك يُنفّذ في مكانه · **2+ = فيتشر** → `POST /spinoff` + تحليل كامل بعقد نطاق (In-Scope مرقّم · Out-of-Scope · معايير القبول) قبل أي كود. حد أمان: 3 جولات تسليم بعد الموافقة = وقف وتقسيم.

**المرفقات إلزامية:** الصور تُفتح من `url`، والمستندات من `text_url` (نص مستخرَج من السيرفر). ممنوع التحليل من العنوان والوصف مع وجود مرفق.

**قبل كل verification:** `POST /kb` بأي قرار تصميمي/مطب/اصطلاح اتعلمته.

## المؤشّر الحدثي (cursor)

| | |
|---|---|
| آخر `latest_id` معالَج | **8639+** (تحليل 9348) |
| آخر مسح | 2026-07-27 |

المراقبة حدثية عبر `GET /api/agent/changes?project=291&since=<CURSOR>` — **ممنوع اللوب الدوري**.


> **مؤشّر `since` الحالي:** `10230` (آخر `latest_id` من `GET /api/agent/changes?project=291` — 2026-08-19).

## سجل التذاكر (الأحدث فوق)

| التاريخ | Ref | الطلب | القرار | الملفات | الحالة |
|---|---|---|---|---|---|
| 2026-08-18 | **ISS-2026-9515** | خصم النقاط مابيظهرش في `checkout/calculate` (bug — بلاغ فريق التطبيق) | **✅ نُفّذ ونُشرت verification.** **إعادة إنتاج حيّة:** نفس النداء بـ`points_to_redeem:80` وبدونه رجّع ردًّا **متطابقًا حرفيًا**، بينما `/loyalty/preview` قيّم نفس الـ80 بـ40. **السبب:** `CheckoutCalculateRequest` فيه `address_id` و`coupon_code` بس ⇒ Laravel بيرمي الحقل بصمت، و`calculate()` مالهاش باراميتر نقاط أصلًا. **🔴 الأخطر:** `placeOrder` **بيطبّقه** ⇒ الشاشة 133 والطلب 93 — **رابع تكرار لصنف «مسارين لحقيقة واحدة»** (النقاط · الشحن · «عليه عرض» · حالة الدفع). **الحل:** `calculate()` بتقيّم النقاط بـ`previewRedemption()` (نفس ميثود `/loyalty/preview` وقواعد `redeem()`) + الرد فيه `points_redeemed`/`points_discount` منفصلين عن خصم الكوبون. **⚠️ `placeOrder` بيفضل ينادي `calculate()` بلا نقاط عن قصد** — `redeem()` بيخصم من الإجمالي قبل النقاط، وتمريرها في الاتنين = **خصم مرتين**. 4 اختبارات، أهمها **«calculate و place-order يدّوا نفس الإجمالي»** (غيابه هو اللي سمح بالعطل — النقاط كانت متغطّاة في place-order بس). تحقق حيّ: 109 → 69 بخصم 40. **KB id 392.** مافيش تغيير مطلوب في التطبيق — نداؤه صح، بس بقى فيه حقلين جداد يعرضهم. BE `da9c2659a` · CHANGELOG `[STORE-086]` | `CheckoutCalculateRequest.php` · `Services/CheckoutService.php` · `Storefront/CheckoutController.php` · `StorefrontCheckoutApiTest.php` | `client_review` |
| 2026-08-18 | **ISS-2026-9384** | «مفتاح FCM token» (**feature**) — تنفيذ | العميل وافق بلا إجابات فردية (17/08) ⇒ كل توصياتي: الاستقبال الآن والإرسال منفصل · أكتر من جهاز · Firebase عنده. **الحل:** جدول `store_customer_devices` (**التوكن مفتاح فريد عالميًا — بينتقل ولا يتكرّر**، وده يمنع وصول إشعارات عميل لموبايل اتباع) · `Services/CustomerDeviceRegistrar` المالك الوحيد للربط · `fcm_token`+`platform` **اختياريين** على register/login/PUT profile (التطبيق بياخد التوكن في لحظة مش متحكّم فيها) · **`POST /auth/logout` جديد** (يبطّل الجلسة + يفكّ الجهاز، ومحدود بالعميل) · `devices_count` في قائمة عملاء الأدمن. 9 اختبارات + تحقق حيّ (دخول ربط الجهاز، خروج شاله). **⚠️ مطب:** عمود التوكن **191** حرف — أوسع فهرس فريد MySQL بلا prefix، والـprefix مابيشتغلش على sqlite بتاع الاختبارات. **KB id 386**. **decompose مطلوب قبل الـverification** (اتكرر نفس درس 9459) → 4 مهام **9511–9514** كلها `client_review`. **الإرسال منفصل → ISS-2026-9510** (موقوف على مشروع Firebase + service account من العميل). BE `a109a86a4` · CHANGELOG `[STORE-085]` | `store_customer_devices` · `CustomerDeviceRegistrar.php` · `Storefront/AuthController.php` · `ProfileController.php` · `AdminClientController.php` · `StoreCustomerResource.php` | الأم `in_implementation` · الفروع `client_review` |
| 2026-08-09 | **ISS-2026-9470** | الربط والحماية والترجمة (مهمة 5/5 — **فيتشر الروشتة اكتمل**) | **✅ نُفّذت ونُشرت verification.** بند «روشتاتي» في **كل قوالب الهيدر الستة** (شكل الهيدر إعداد قابل للتغيير — إضافته في واحد بس كانت هتخفي الميزة عند تغيير القالب). **الحماية:** `authGuard` بـ`returnUrl` موجود مسبقًا — تحققت منه ما بنيتوش؛ + **اختبارا ملكية جدد**: فتح/تعديل/حذف روشتة غيرك مرفوض، والقائمة مابتسرّبش (صفحة التفاصيل عنوانها قابل للتخمين فالقاعدة لازم تكون سيرفرية). RTL بخصائص اتجاهية (`ms-/ps-/start-`) وأسهم `rtl:rotate-180` · الموبايل: اللوحتان بتتراصّوا والشريط بيلتفّ. تحقق: **6 مسارات حيّة** (3 صفحات × لغتين) كلها 200 · **21/21** في ملف الروشتات · `I18N_VERSION=20260809e`. **مؤجّل بقرار العميل:** زرار «إعادة الطلب» (مفيش ربط روشتة↔طلب). BE `5cec3c960` · FE `fbcfaeda` | `slots/header/variants/*.html` ×6 · `StorePrescriptionApiTest.php` · i18n | `client_review` — والأم 9415 `in_implementation` لحد ما العميل يقفل الخمسة |
| 2026-08-09 | **ISS-2026-9469** | صفحة تفاصيل الروشتة (مهمة 4/5 من 9415) | **✅ نُفّذت ونُشرت verification + منشورة.** عرض كامل + تكبير/تدوير/إعادة/تنزيل للصور، و«فتح/تنزيل» للـPDF (تقليد عارض PDF أسوأ من عارض المتصفح). **ملاحظة العميل ورد الصيدلي في لوحتين منفصلتين** — دمجهم كان هيرجّع لخبطة الخانة الواحدة بعد ما البيانات اتصلحت في 9466. التعديل والحذف **مشروطين بحالة pending** (تعديل السؤال بعد الرد يسيب ردًّا على طلب مش موجود) + تأكيد قبل الحذف. **مافيش زرار «إعادة الطلب»** ولا حتى معطّل (الزرار الرمادي الدائم بيخلّي الناس تسأل «إيه اللي باظ»). **كروت القائمة بقت روابط** دلوقتي. تحقق: التلات مسارات 200 · **ترتيب المسارات متحقَّق: `/new` قبل `/:id`** · id غلط يرجّع للقائمة · 533 مفتاح متطابقين · `I18N_VERSION=20260809d`. FE `8ae33dd4` | `features/account/prescription-detail.page.{ts,html}` · `prescriptions.page.html` · `app.routes.ts` · `styles.scss` | `client_review` |
| 2026-08-09 | **ISS-2026-9468** | صفحة «روشتاتي» بالتبويبات (مهمة 3/5 من 9415) | **✅ نُفّذت ونُشرت verification + منشورة.** صورة مصغّرة/تاريخ/حالة/ملاحظة + ترقيم + 4 تبويبات. **المصيدة المتجنَّبة:** العدّادات بتحسب **كل** الروشتات مش الصفحة الظاهرة — 3 نداءات `?status=` متوازية والإجمالي = مجموعهم (الحالات حصرية). فشل العدّادات **مابيكسرش القائمة** (زينة مش محتوى). الحالات متفرّقة بالألوان · PDF بياخد شارة ملف. **⚠️ الكروت مش روابط عمدًا** لحد ما 9469 تخلص (نفس منطق 9467) — والقائمة كاملة الفايدة من غيرها؛ وصفحة الرفع بقت تربط رجوع للقائمة. تحقق: الصفحتان 200 · 510 مفتاح ترجمة متطابقين · `I18N_VERSION=20260809c`. FE `af023ecf` | `features/account/prescriptions.page.{ts,html}` · `app.routes.ts` · `prescription-upload.page.html` | `client_review` |
| 2026-08-09 | **ISS-2026-9467** | صفحة «رفع روشتة» في المتجر (مهمة 2/5 من 9415) | **✅ نُفّذت ونُشرت verification + منشورة على `/store`.** سحب وإفلات (منطقة الإفلات = المساحة كلها) · معاينة · ملاحظة 2000 حرف · **نداء واحد** بـ`FormData` (الـupload العام staff-only). **3 قرارات:** (١) الفحص (5MB + الصيغ) **قبل** الإرسال — السيرفر فاضل المرجع، ده تسهيل مش حماية · (٢) الـPDF بياخد **بطاقة ملف** مش معاينة (الأيقونة المكسورة بتتقري «ملفي باظ») · (٣) **بعد النجاح الصفحة بتأكّد مكانها ومابتنقلش** — صفحتَي القائمة والتفاصيل لسه مش موجودين (مهام 9468/9469)، والنقل كان هيحوّل رفعًا ناجحًا لـ404. روابط المعاينة بتتلغى (`revokeObjectURL`) عند التغيير والنجاح. تحقق: `‎/store/ar/account/prescriptions/new` → 200 والنصوص بتتقري بعد رفع `I18N_VERSION=20260809b`. FE `7a1488f9` | `features/account/prescription-upload.page.{ts,html}` · `core/models/prescription.model.ts` · `core/http/api-client.service.ts` · `app.routes.ts` · `account.page.ts` | `client_review` |
| 2026-08-09 | **ISS-2026-9415** | «اضافه روشته (ويب)» (**feature**) — تنفيذ | العميل أخذ بالتوصية ×4 (قبول PDF · عمود مستقل لرد الصيدلي · تأجيل «إعادة الطلب» · إشعار عند المراجعة). **اتعلّمت من 9459 فعملت `/decompose` الأول** → 5 مهام: **9466** باك-إند · **9467** صفحة الرفع · **9468** «روشتاتي» · **9469** التفاصيل · **9470** الربط والترجمة. **✅ 9466 خلصت ونُشرت verification:** عمود `review_note` (كان رد الصيدلي **بيمسح ملاحظة العميل** بلا نسخة) · قبول `application/pdf` **بتوسيع `assertImageContents()` عبر باراميتر `alsoAllow` للمستدعي ده وحده** — القائمة الأساسية ما اتلمستش فالسكربت المتسمّي `.pdf` لسه بيترفض · `?status=` على قائمة العميل (بدونه التبويبات بتعدّ الصفحة الظاهرة بس) · إشعار `prescriptionReviewed` (والرجوع لـpending مابيبعتش). 7 اختبارات والملف 19/19. **KB id 326** (خانة بدورين + توسيع حارس البايتات + مطب `User::factory()` بلا أدوار = super-admin في اختبارات الصلاحيات). BE `c30ef1cea` · CHANGELOG `[STORE-084]`. **الباقي: 9467–9470 (الصفحات نفسها)** | `store_prescriptions` (+`review_note`) · `Storefront/PrescriptionController.php` · `Admin/AdminPrescriptionController.php` · `HandlesImageUpload.php` · `StorefrontNotificationService.php` | الأم `in_implementation` · 9466 `client_review` |
| 2026-08-09 | **ISS-2026-9459** | شاشة إعدادات البريد SMTP (**feature**) | **✅ نُفّذ بالكامل.** العميل أخذ بالتوصية ×3 (SMTP فقط · سجل البريد تذكرة منفصلة · نطاق شركة). **⚠️ درس منصّة:** `POST /verification` رجّع **409** لأن الفيتشر عقده 5 بنود In-Scope ⇒ **لازم `/decompose` قبل أول verification تكاملية**. اتقسّم لـ5 مهام (**9461–9465**) وكلها اتنشرت verification ⇒ `client_review`؛ الأم بتفضل `in_implementation` لحد ما العميل يقفلهم. **الحل:** جدول `mail_configs` (صف/شركة، `password` مشفّر ومابيرجعش) · **`Core\Mail\CompanyMailer` هو الطريق الوحيد لأي إرسال** (اصطلاح مُلزِم — **KB id 318**) مع رجوع لـ`.env` لو مفيش إعداد · `GET/PUT /api/core/mail/config` + `POST /api/core/mail/test` بصلاحية `core.mail.manage` · شاشة `settings/mail` + بند في القائمة الجانبية · المُرسِلان (`LabResultPublishService` + `StoreOtpNotifier`) اتحوّلوا ويبعتوا من عنوان الشركة · `StoreOtpNotifier` بيسأل `isConfigured()` قبل ما يدّعي التسليم. **تحقق حيّ:** غير متظبّط → متظبّط، كلمة السر اتخزّنت **200 حرف مشفّر**، والإرسال لخادم وهمي رجّع رسالة الخادم الحقيقية. **الأعطال:** الـ4 المعروفة في WebStore فقط؛ و**10 أعطال في Core سابقة للتغيير** — أثبتّها بتشغيل نفس الملفات والتعديلات في stash (10/76 متطابقة). حدّثت اختبارًا قديمًا كان بيتوقّع رسالة «تم الإرسال» الكاذبة. ⚠️ اختبار أي مستهلك بريد بيحتاج تبديل `CompanyMailer` في الحاوية (`Mail::fake()` مابيسجّلش `raw`). BE `3c20226b8`+`0f1ed61a3` · FE `052d02b7` · CHANGELOG `[STORE-083]` · نشر `/app` (`main-TW5LKSWK.js`) | `Core/Models/MailConfig.php` · `Core/Mail/CompanyMailer.php` · `Core/Http/Controllers/MailSettingController.php` · هجرة `mail_configs` · `features/settings/mail/*` · `core/services/mail.service.ts` | الأم `in_implementation` · الفروع `client_review` |
| 2026-08-09 | **ISS-2026-9460** | مستحيل تسجيل إن طلب المتجر «مدفوع» (bug) | **✅ نُفّذ ونُشرت verification.** العميل أخذ بالتوصية في الأسئلة التلاتة. **الحل:** `Services/OrderPaymentService` بقى **الكاتب الوحيد** للحقل (اصطلاح مُلزِم — **KB id 313**): تسليم COD ⇒ مدفوع تلقائيًا · زرار «تسجيل التحصيل» لأي حالة تانية · إرجاع **كامل فقط** ⇒ مسترد (الجزئي مابيلمسش السجل). **الأونلاين/البطاقة مابيتفترضوش مدفوعين** — مفيش بوابة تثبت الوصول. `PUT /store/admin/orders/{order}/payment-status` بصلاحية `webstore.orders.update` + تحقّق شركة. أمر `webstore:backfill-order-payments` (معاينة افتراضية · يرفض بلا `--from` · idempotent · بيعرض الطلبات غير-COD كـ«قرار بشري» بدل ما يعدّيها بصمت). الحالات بتتطابق **دلاليًا** (`isPaid`/`isRefunded`) فالمتجر اللي بيعيد التسمية بياخد تحذير مش نص إصلاح. **تحقق حيّ:** طلب 300 بحالة Pending + COD → التسليم خلّاه Paid، والأمر الرجعي قال «already paid». 7 اختبارات؛ المجموعة كاملة 897 ناجح ونفس الـ4 أعطال السابقة. FE: زرار «تسجيل التحصيل» في السطر والتفاصيل + الحالة المدفوعة بتتقري من قائمة الشركة مش ثابتة + `I18N_VERSION=20260809b` + نشر `/app` (`main-3L5E5ZD4.js`). **مسيّب عمدًا:** مفيش عمود مين/إمتى للتحصيل — عرضت على العميل تذكرة لو محتاجها. BE `41cadb549` · FE `22279367` · CHANGELOG `[STORE-082]` | `Services/OrderPaymentService.php` (جديد) · `OrderLifecycleService.php` · `Actions/ReturnStoreOrderItems.php` · `Admin/AdminOrderController.php` · `Console/BackfillOrderPayments.php` (جديد) · `webstore-orders.component.{ts,html}` + api | `client_review` |
| 2026-08-09 | **ISS-2026-9387** | «العروض (ويب)» (bug) — عرض المنتجات المشمولة بالعروض فقط | **تكرار مؤكَّد لـISS-2026-9417** — «العروض» في المتجر **مش صفحة مستقلة**: كل روابط العروض (9 أماكن في القائمة والفوتر) بتودّي على `/products?on_sale=1`، وهي نفس شاشة 9417. استوفيت `planning_gate` بإعادة إنتاج على تثبيت العميل الحيّ (12 مقابل 17128) ونشرت verification بيشرح إن الشغل خلص في `[STORE-080]`. **ما نفّذتش أي كود جديد** وقلت ده صراحةً للعميل. اتأكدت إن `257de0b33` بقى على `origin/main`، وإن **تثبيت العميل لسه بالكود القديم** (ملف الفلتر عنده مافيهوش الشرط الجديد) | `Storefront/CatalogProductController.php` (نفس ملف 9417) | `client_review` |
| 2026-08-09 | **ISS-2026-9384** | «مفتاح FCM token في register/login/profile» (**feature**) | `feature_gate` مفعّلة → **تحليل بعقد نطاق تلاتي** (In-Scope 5 · Out-of-Scope 5 · معايير قبول 7) + **3 أسئلة**. **الوضع:** بحث في الباك-إند كله = **صفر بنية إشعارات موبايل** (لا عمود ولا جدول ولا خدمة ولا شاشة إرسال). الموجود بس إشعارات داخل الموقع (`StorefrontNotificationService`). **إعادة تأطير:** الطلب المكتوب «مفتاح في 3 نداءات» لكن هدفه المعلَن «الأدمن يبعت إشعار للعميل» — الاستقبال ≈ ربع الشغل، فقسّمتها: استقبال+تخزين الآن، والإرسال (Firebase + شاشة) تذكرة منفصلة. **3 نقاط تصميم:** (١) العميل له أكتر من جهاز ⇒ **جدول أجهزة** مش عمود (وإلا الإشعارات تقف على باقي أجهزته بصمت) · (٢) 🔴 **خصوصية:** توكن FCM ممكن يُعاد استخدامه/الجهاز يتباع ⇒ التوكن **مفتاح فريد عالميًا** ينفكّ عن العميل القديم فورًا، وإلا إشعارات عميل توصل لغريب · (٣) **مفيش endpoint تسجيل خروج للمتجر أصلًا** ⇒ ضمّيت فكّ ارتباط الجهاز في النطاق. **مفيش كود** | `store_customers` (24 عمود، مفيش fcm) · `RegisterRequest`/`LoginRequest`/`ProfileController` · جدول أجهزة جديد | `awaiting_client` |
| 2026-08-09 | **ISS-2026-9385 / 9383** | مراجعة QA لـ«تعريب ردود verify-code» و«رفع صورة الروشتة من الموبايل» | **✅ الاتنين `qa-review: ok`.** 9385: كل `'message'` في `Storefront/AuthController` بينادي `__()`، 9 مفاتيح `auth_*` متطابقة في اللغتين، الاختبار الحارس (`StoreAuthApiTest:527`) بيقرا ملف الكنترولر ويرفض أي نص إنجليزي ثابت، و**اتعرّض لتعديل حقيقي بعده** (`[STORE-079]`) وفضل أخضر ⇒ الحماية اشتغلت فعلًا؛ + نداء حيّ `ar`/`en` رجّع الرسالتين صح. 9383: `prescriptionImageRules()` بقواعد `mimes` صريحة (SVG مرفوض) و5MB، ورفع الموظفين لسه مقفول (`public.php:78`)، والمسار النصّي القديم شغّال، والملف 12/12. **⚠️ لقيت خطأ في نص تسليم 9383:** خطوة التجربة قالت «من المتجر على الويب: روشتاتي ← إضافة روشتة» و**الشاشة دي مش موجودة أصلًا** (اتأكد في 9415) — نشرت تصحيحًا للعميل. الإصلاح نفسه سليم ⇒ `ok` مش `reopen` | `Storefront/AuthController.php` · `lang/{ar,en}/webstore.php` · `Storefront/PrescriptionController.php` · `routes/public.php` | `closed` (QA ✅) |
| 2026-08-09 | **ISS-2026-9410 / 9411** | مراجعة QA لمهمتَي «حقل الخصم في فورم المنتج» و«عمود الخصم في القائمة» | **✅ اتختمت الاتنين `qa-review: ok`** بعد مطابقة بند ببند مع الكود: الضوابط في `.ts:250-251` · التحميل عند التعديل من `.ts:530-531` ← `StoreProductResource:27` ← `ProductDiscountService::current()` · المسح عند القيمة ≤ 0 · الاستدعاء في الإنشاء (`:115`) والتعديل (`:207`) · مفاتيح `DISCOUNT*` الخمسة في اللغتين · `discountLabel()` بيرجّع شرطة لو مفيش خصم وبيستخدم عملة الشركة مش ثابتة. **نقطة زيادة اتأكدت منها:** حارس `$discountSent` بيمنع إن تعديل جزئي مايبعتش الحقل يمسح الخصم بالغلط. **مافيش ملاحظات** | `webstore-products.component.{ts,html}` · `AdminProductController.php` · `StoreProductResource.php` | `closed` (QA ✅) |
| 2026-08-09 | **ISS-2026-9460** | مستحيل تسجيل إن طلب المتجر «مدفوع» (bug، منبثقة من 9348) | استوفيت `planning_gate` ثم **تحليل بـ3 أسئلة**. **السبب:** `payment_status_id` بيتكتب في `CheckoutService` وقت الإنشاء بس (دايمًا pending) — **مفيش endpoint ولا Form Request ولا تكامل محاسبي** بيغيّره؛ `PUT /orders/{order}/status` بيغيّر حالة الطلب مش الدفع. **أثر ملموس اتأكد في الكود:** زرار «منح نقاط الطلب» في `webstore-orders.component.ts:426` مشروط بـ`payment_status?.is_paid === true` ⇒ **مخفي على كل طلب في النظام** — وده يفسّر ليه المنح اليدوي محصلش أبدًا. و3 من 4 حالات الدفع المزروعة (Paid/Refunded/Failed) مستحيل يتوصلّهم. **الأسئلة:** لحظة التسجيل (تلقائي عند التسليم لـCOD + زرار يدوي — توصيتي الاتنين) · تصحيح رجعي بمعاينة · مين يحطّ «مرتجع»/«فشل الدفع». **مفيش كود** | `Services/CheckoutService.php` · `Admin/AdminOrderController.php` · شاشة `webstore/orders` | `awaiting_client` |
| 2026-08-09 | **ISS-2026-9348** | «النقاط» (bug) — البرنامج مفعّل والأرصدة صفر | **✅ نُفّذ ونُشرت verification.** العميل وافق بلا إجابات فردية ⇒ كل توصياتي (التسليم = اللحظة · سكربت رجعي محدود · أنواع الأسعار مفتوحة). **السبب:** `earnForOrder()` ليه مستدعي واحد = زرار أدمن يدوي. **🔴 مانع تانٍ اتكشف أثناء التنفيذ وكان هيخلّي الإصلاح بلا فايدة:** `payment_status_id` بيتكتب في **مكان واحد في الكود كله** (`CheckoutService` وقت الإنشاء، دايمًا pending) و**مفيش أي endpoint بيغيّره** ⇒ `isPaid()` **مستحيل** ترجع true لأي طلب متجر. **الحل:** المنح جوّه معاملة `updateStatus` لما `isDelivered()` · `earn()` اتقسم: `earnForOrder()` بيرمي (زرار الأدمن) و`earnOnDelivery()` بيتخطّى بصمت (رمي الخطأ كان هيرجّع تغيير الحالة) · **طلب COD متسلّم = مدفوع فعلًا** (الكاش بيتقبض عند الباب)؛ الأونلاين/البطاقة لسه محتاج paid صريح · أمر `webstore:backfill-loyalty-points` (معاينة افتراضية · بيرفض بلا `--from` · idempotent · بيحذّر من الطلبات المتسلّمة بلا `delivered_at`) · `isDelivered()` بقى يعرف «تم التسليم». **تحقق حيّ:** طلب 250 بحالة دفع Pending + COD → التسليم منح 250 ونضّفت البيانات. 7 اختبارات (المنح التلقائي fail-then-pass بـstash)؛ المجموعة كاملة: نفس الـ4 أعطال السابقة، صفر جديد. **فجوة مفتوحة → ISS-2026-9460** (مستحيل تسجيل إن الطلب مدفوع — السجل المحاسبي). **KB id 308**. BE `c9b21cb30` · CHANGELOG `[STORE-081]` | `Services/LoyaltyPointsService.php` · `Services/OrderLifecycleService.php` · `Console/BackfillLoyaltyPoints.php` (جديد) · `Models/StoreOrderStatus.php` · `WebStoreServiceProvider.php` | `client_review` |
| 2026-08-09 | **ISS-2026-9459** | شاشة إعدادات البريد SMTP في اللوحة (**feature**، منبثقة من 9414) | `feature_gate` مفعّلة → **تحليل بعقد نطاق تلاتي** (In-Scope 5 · Out-of-Scope 5 · معايير قبول 7) + **3 أسئلة** (SMTP فقط؟ · سجل بريد الآن أم تذكرة منفصلة؟ · نطاق شركة أم فرع؟). **الوضع:** إعدادات البريد بتتقري من `.env` وهي `MAIL_MAILER=log` ⇒ **مفيش بريد بيخرج من أي تثبيت**. المُرسِلان الوحيدان: بريد نتيجة التحليل (`LabResultPublishService`) وكود التحقق في المتجر (`StoreOtpNotifier`) — الاتنين معطّلين فعليًا. **النموذج جاهز:** شاشة SMS (`SmsConfig` + `SmsSettingController` + `core.sms.manage` + اختبار الاتصال) هي نفس النمط ⇒ تكرار نمط مثبت مش اختراع. **القاعدة الملتزَم بيها:** طبقة واحدة مشتركة + الرجوع لـ`.env` لو مفيش إعداد (مافيش تثبيت هيقع). نبّهت العميل إن SPF/DKIM والمنافذ المقفولة على cPanel خطوة عنده وبتحدّد الوصول للوارد. **مفيش كود** | `Core/Models/SmsConfig.php` (نموذج) · `LabResultPublishService.php` · `StoreOtpNotifier.php` · شاشة إعدادات جديدة | `awaiting_client` |
| 2026-08-09 | **ISS-2026-9458** | مزوّد رسائل SMS تاني يوصّل لمصر (**feature**، منبثقة من 9414) | `feature_gate` مفعّلة → **تحليل بعقد نطاق تلاتي** (In-Scope 5 · Out-of-Scope 5 · معايير قبول 5) + **سؤالين**. **الخلاصة:** المنظومة مبنيّة للتوسّع — مزوّد = كلاس + سطر في `ProviderRegistry`، و**شاشة الإعدادات بتتبني من وصف الحقول تلقائيًا** فمفيش شغل واجهة ولا هجرة. الناقص = المزوّد المصري نفسه (الوحيد المبرمَج = 4Jawaly سعودي). **موقوف على العميل:** اسم المزوّد + توثيق الـAPI + حساب تجربة. ونبّهته إن **تسجيل اسم المرسِل لدى الجهاز القومي** خطوة عنده وبتاخد أيام وبدونها المزوّد بيرفض كل الرسايل مهما كان الكود سليم. **مفيش كود** | `Core/Sms/ProviderRegistry.php` · `Core/Contracts/SmsProvider.php` · مزوّد جديد | `awaiting_client` |
| 2026-08-09 | **ISS-2026-9415** | «اضافه روشته (ويب)» (**feature**) — رفع روشتة من الموقع | `feature_gate` مفعّلة → **تحليل بعقد نطاق تلاتي** (In-Scope 6 · Out-of-Scope 5 · معايير قبول 7) + **4 أسئلة**. **الاكتشاف:** الباك-إند **جاهز بالكامل** (`/store/prescriptions` CRUD + شاشة مراجعة الصيدلي، اتصلحت في 9383 للموبايل) — الناقص هو **المتجر**: صفر صفحات روشتات. وتصميم الصفحات التلاتة موجود في الثيم (`prescriptions/prescription-upload/prescription-detail.html`). **3 فجوات تصميم↔باك-إند:** (١) الثيم بيوعد بـPDF والباك-إند بيرفضه · (٢) **رد الصيدلي بيمسح ملاحظة العميل** — خانة `note` واحدة للطرفين (**KB id 307**) · (٣) زرار «إعادة الطلب» مالوش أي أساس (مفيش ربط روشتة↔طلب). + قائمة العميل مافيهاش `?status=` فالتبويبات هتدّي عدّادات غلط — ضمّنتها In-Scope. **مفيش كود** — مستنّي الموافقة، وبعدها `/decompose` لـ5 مهام | `Storefront/PrescriptionController.php` · `Admin/AdminPrescriptionController.php` · storefront (صفحات جديدة) | `awaiting_client` |
| 2026-08-09 | **ISS-2026-9417** | «فلتر المنتجات اللي عليها عروض» (bug) — الفلتر بيعرض منتجات من غير خصم | **✅ نُفّذ فورًا ونُشرت verification** (بسيط، بلا قرار). استوفيت `planning_gate` بإعادة إنتاج على **تثبيت العميل الحيّ** (`GET tarshoby.../api/store/products?on_sale=1` → 12 مقابل 17128 بدون الفلتر) وطابقتها مع `GET /api/store/offers`. **السبب:** تعريفين لـ«عليه عرض» — الفلتر بيسأل «مربوط بعرض ساري؟» والشاشة بتسأل «بيقلّل السعر فعلًا؟». عرضين عند العميل («رفيق الدواء» و«عرض اليوم الواحد») بـ`discount_value=0` و`custom_price=null` ⇒ **6 من 12** كانوا بيظهروا بسعرهم الكامل. نفس صنف أعطال النقاط/الشحن. **الحل:** شرط `on_sale` بقى يعكس `priceFor()`/`resolveDiscount()`: `custom_price` أقل من `sale_price` (والصفر هدية صحيحة) **أو** `discount_value > 0`. 3 اختبارات (اتنين fail-then-pass مُثبَتين بـstash) + الملف كامل 17/17؛ تحقق حيّ على dev (عرض صفر ⇒ استُبعد، والقاعدة القديمة كانت هترجّعه) والعرض التجريبي اتشال. **مسيّب عمدًا:** `PublicStoreOfferController` لسه بيرجّع عروض بخصم صفر/بلا منتجات — قرار محتوى، مذكور للعميل. **KB id 306**. BE `257de0b33` · CHANGELOG `[STORE-080]` | `Storefront/CatalogProductController.php` · `StorefrontProductApiTest.php` | `client_review` |
| 2026-08-09 | **ISS-2026-9414** | «استعادة كلمة المرور (ويب)» (bug) — الرمز المكوّن من 6 أرقام مش بيوصل | **✅ نُفّذ ونُشرت verification.** **السبب:** `generateOtp()` بيولّد الكود ويكتبه في `laravel.log` **وبس** — مفيش قناة تسليم. **+ عائق تانٍ:** `PhoneNormalizer` سعودي بالثابت والإعداد `sms.default_country_code` الموثّق **مالوش أي مستهلك** ⇒ الأرقام المصرية بتترفض. العميل وافق على توصيتي في س1 (الاتنين، SMS أولًا) وس4 (رسالة صريحة بدل الكذب)؛ وس2 «هنضيف مزوّد تاني» وس3 «إعدادات SMTP في اللوحة» **اتفصلوا spinoff → ISS-2026-9458 / ISS-2026-9459**. **الحل:** `StoreOtpNotifier` جوّه `generateOtp` (فالتسجيل وإعادة الإرسال ورثوه) بشركة `StoreCompany::id()` · النص من مفاتيح الترجمة **مش** `SmsTemplateRenderer` · `SmsSender` بيقرا مفتاح الدولة بنطاق الشركة · وقف كتابة الكود في اللوج على الإنتاج · الرد فيه `delivered` والواجهة بطّلت تنقل العميل لشاشة الكود لما الإرسال يفشل. **تحقق حيّ:** بمزوّد `log` → `201099030301` status=`sent` والنص فيه الكود؛ ومن غير مزوّد → `delivered:false` بالرسالة الصريحة. 4 اختبارات جديدة + `PublishViaSmsTest` 15/15؛ الأعطال الباقية سابقة (أثبتّها بـstash). ⚠️ **ترشوبي محتاج بعد الريليس:** مفتاح الدولة = **20**. **KB id 304** (قائمة `SmsTemplateRenderer` البيضاء بتمسح `{{code}}`). BE `e86d5ee6e` · FE `1d2a2e51` · CHANGELOG `[STORE-079]` | `WebStore/Services/StoreOtpNotifier.php` (جديد) · `StoreAuthService.php` · `Storefront/AuthController.php` · `Core/Sms/SmsSender.php` · `SettingDefinitionSeeder.php` · `lang/{ar,en}/webstore.php` · storefront `auth/forgot-password.page.ts` + `verify-code.page.ts` + i18n | `client_review` |
| 2026-08-09 | **ISS-2026-9348** | «النقاط» (bug) — البرنامج مفعّل وفيه طلبات مكتملة لكن الرصيد صفر | **تحليل بـ3 أسئلة (فلوس ⇒ قرار العميل).** **السبب:** `earnForOrder()` ليه **مستدعي واحد بس** = endpoint إداري `POST /admin/orders/{order}/award-points`، و**مفيش listener** على حالة الطلب ⇒ المنح **يدوي بالكامل** مش تلقائي عند الاكتمال. **+ مانع تانٍ:** «أنواع الأسعار المؤهلة» = `retail, wholesale` وعملاء المتجر `price_type_id=NULL` ⇒ استبعاد الكل — **مُصلَح في `[STORE-070]` ومستني الريليس**. السكربت الرجعي غير موجود = إضافة جديدة. **مفيش كود** | `LoyaltyPointsService` · `AdminOrderController::awardPoints` · `EventServiceProvider` | `awaiting_client` |
| 2026-08-04 | **ISS-2026-9385** | «صفحه verify-code» (bug, عاجل) — الرد إنجليزي رغم `Accept-Language: ar` | **✅ نُفّذ فورًا.** **السبب:** `SetLocaleMiddleware` مسجّل **عالميًا** وشغال صح، لكن `Storefront/AuthController` **مابيستخدمش `__()` ولا مرة** — **8 ردود** نصوص إنجليزية ثابتة. أضفت 8 مفاتيح للغتين (16 لكل ملف، مجموعات مفاتيح متطابقة) واستبدلت الـ8. **اختبار حارس** بيرفض أي رد إنجليزي ثابت يتضاف بعدين. BE `cb9ded118` · CHANGELOG `[STORE-077]` | `Storefront/AuthController.php` · `lang/{ar,en}/webstore.php` | `client_review` |
| 2026-08-04 | **ISS-2026-9386** | «حقل الخصم في فورم المنتج» (**feature**) | العميل وافق على التحليل ⇒ توصياتي تسري: الحقل يعمل **خصمًا في نظام العروض** (مصدر واحد للسعر) · الخصم يفضل شغّال لحد ما يتوقف · عند التعارض **الأقل سعرًا** يكسب. **✅ نُفّذ بالكامل.** اتجزّأت لـ5 مهام (9409–9413) وكلها في `client_review`. **التصميم:** الخصم بيتخزّن كـ**عرض حقيقي** بعمود `owner_product_id` جديد — مش عمود على المنتج، عشان يفضل مصدر واحد للسعر. قاعدة «الأقل سعرًا يكسب» **كانت موجودة أصلًا** في `OfferState`. العروض اليدوية مابتتلمسش. BE `92c0a910b` · FE `17e64001` · CHANGELOG `[STORE-078]` · 6 اختبارات | شاشة `webstore/products` · `store_offer_product` | `in_implementation` |
| 2026-08-03 | **ISS-2026-9386** | «اضافه منتج من الادمن» (**feature**) — حقل خصم في فورم المنتج | `feature_gate` مفعّلة → **تحليل بعقد نطاق** + 3 أسئلة. **الاكتشاف:** جدول المنتجات **مفيهوش أي عمود خصم** — الخصومات كلها في العروض (بتواريخ + `custom_price`). حقل خصم مستقل = **مصدر تاني للسعر** (نفس صنف أعطال النقاط/الشحن). المقترح: الحقل يعمل خصمًا في نظام العروض من ورا الكواليس. **مفيش كود** | شاشة `webstore/products` · `store_offer_product` | `awaiting_client` |
| 2026-08-02 | **ISS-2026-9383** | «اضافه روشته في الموبيل» (bug) — رفع صورة الروشتة بيرجّع 403 | **✅ اتصلح ونُشرت verification (الحل أ).** **السبب:** `/store/upload` مقفول على الموظفين (`webstore.uploads.manage`) بينما `/store/prescriptions` بيطلب `image` كمسار نصي إلزامي ⇒ **الميزة مستحيلة على العميل بالتركيب**. العميل وافق على التحليل → نفّذت (أ): الروشتة بقت تستقبل الصورة كملف في نفس الطلب (`mimes` صريحة + 5MB)، و`/store/upload` **فضل مقفول** (اتأكدت حيًّا: 403). BE `52e9d2577` · CHANGELOG `[STORE-076]` · KB مسجّل. ⚠️ التذكرة كانت فيها **توكن دخول حقيقي** — نبّهت العميل يسجّل خروج | `routes/public.php` · `StaffPermissionMiddleware` · `PrescriptionController` | `awaiting_client` |
| 2026-08-03 | **ISS-2026-9368** | «عند تحويل الموقع علي الموبايل» (bug) — زراري الرئيسية والسلة في الشريط السفلي مش شغالين | **✅ نُفّذ فورًا.** **السبب:** إشعار الكوكيز (`fixed`, z-index **10000**) بيغطي الشريط السفلي (z-50) — كان بيرفع نفسه تحت **700px** بس بينما الشريط `lg:hidden` (ظاهر لـ**1024px**)، فالنطاق 700–1024 مغطّى. **الأزرار الخمسة** كلها كانت محجوبة مش اتنين. أثبتّه بـ`elementFromPoint` على متجر العميل بمقاس شاشته (943px). الحل: نقطة الكسر اتوحّدت مع الشريط. FE `4ca75c82` · CHANGELOG `[STORE-075]` `91fade512` | storefront `shared/consent-preferences.component.ts` | `client_review` |
| 2026-07-29 | **ISS-2026-9334** | «كود الخصم» (bug) — رسالة الكوبون بتعرض `{{amount}}` كنص خام | **✅ نُفّذ فورًا** (تصنيف: بسيط). **السبب:** الـendpoint بيرد 422 لحالتين (تحت الحد الأدنى / منتهي)، والواجهة كانت بتترجم كل 422 لرسالة الحد الأدنى فالـplaceholder مالقاش قيمة. الحل: مصدر واحد لاستخراج `minimum_order_value` يحدّد الرسالة والقيمة معًا + مفتاح `CART.COUPON_EXPIRED`. FE `fab8d906` · CHANGELOG `[STORE-072]` `b1de6645c` | storefront `shared/coupon-input.component.ts` + i18n | `client_review` |
| 2026-07-29 | **ISS-2026-9331** | «العروض» (**feature**) — إظهار المنتجات اللي عليها عروض | `feature_gate` مفعّلة → **تحليل بعقد نطاق تلاتي** (In-Scope 5 بنود · Out-of-Scope 5 · معايير قبول 5) + 3 أسئلة. **فجوة حقيقية:** روابط «العروض» في المتجر بتروح لـ`?on_sale=1` و**الباك-إند مافيهوش الفلتر ده أصلًا** فبتعرض كل المنتجات. **مفيش كود** — مستنّي الموافقة | `CatalogProductController.php` · `store_offer_product` · صفحة منتجات المتجر | `awaiting_client` |
| 2026-07-29 | **ISS-2026-9292** | «العروض API» (bug) — بيرجّع عرض واحد رغم إضافة أكتر من عرض | **الفرضية اتنفت بالدليل:** الصورة اتاخدت 26/7 و3 عروض من الـ4 تواريخهم كانت عدّت — الـAPI رجّع الساري الوحيد وهو **شغال صح**. المشكلة الحقيقية: **شاشة إدارة العروض بتكتب ACTIVE على عرض منتهي** (بتعرض مفتاح التفعيل مش الحالة الزمنية). ما نفّذتش الطلب حرفيًا لأنه هيعرض عروض منتهية للعملاء. تحليل بـ3 أسئلة | `PublicStoreOfferController.php` (سليم) · شاشة إدارة العروض | `awaiting_client` |
| 2026-07-28 | **ISS-2026-9332** | «قيمه الشحن» (bug) — سطر الشحن في إتمام الطلب بيقول «اختر عنوان» رغم إن العنوان مختار، والمجموع = المجموع الفرعي بلا شحن (صورة من متجر ترشوبي) | استوفيت `planning_gate` (إعادة إنتاج فعلية + سبب جذري + مصفوفة) ثم **تحليل بـ3 أسئلة مهيكلة**. **السبب:** مصدرين للشحن — إعداد `store.shipping_value` **مستهلكه الوحيد خلاصة Meta/Google مش الطلبات**، والشيك-أوت بيحسب من `cities.shipping_cost`؛ + الواجهة بتخلط «صفر» بـ«مفيش عنوان». **✅ اتصلح ونُشرت verification.** العميل أخذ بالتوصية في الـ3 أسئلة و`scope_freeze` اتسجّل. **الحل:** `ShippingCalculator` بقى يرجع لـ`store.shipping_value` (بـ`StoreCompany::id()` مش شركة العميل) لما تكلفة المدينة = 0 — والمدينة المسعّرة تفضل تغلب؛ + الواجهة بقى فيها `shippingKnown` فبطّلت تخلط «صفر» بـ«مفيش عنوان» وبتقول «شحن مجاني». BE `f37b1edfc` · FE `635b8b6a` · CHANGELOG `[STORE-071]` · KB `id 187` | `Services/ShippingCalculator.php` · `Services/StorefrontMerchantFeedService.php` · storefront `shared/order-summary.component.ts` · شاشة إدارة المدن | `client_review` |
| 2026-07-27 | **ISS-2026-9293** | «نظام النقاط» (bug) — الإعدادات متملّية في الأدمن بس `/store/loyalty/summary` بيرجّع `program_enabled:false` (بلاغ من تطبيق Flutter على `tarshoby.elbaset.com`) | **✅ اتصلح ونُشرت verification.** تحقيق كامل بـ`problem-investigation` (تقرير: `his-analysis/webstore-loyalty-not-enabled-investigation.html`). **السبب:** المالك أكّد **شركة واحدة/فروع متعددة** ⇒ صف `loyalty.enabled` اتحفظ **بنطاق فرع**، و`LoyaltySettings::for()` بيقرأ **نطاق الشركة فقط** ⇒ غير مرئي ⇒ رجع للافتراضي `'0'`. ومعاه 3 آليات صمت تانية. **+ مانع تانٍ:** `customerEligible()` كان بيستبعد كل العملاء (كلهم `price_type_id=NULL`) ⇒ صفر نقاط للأبد. BE `7d1b46ef2` · FE `987de647` · CHANGELOG `[STORE-070]` | BE: `Core/SettingController.php` (حارس النطاق) · `Admin/AdminLoyaltyController.php` (applied/ignored + معاملة) · `UpdateLoyaltySettingsRequest.php` (aliases التفعيل) · `Support/LoyaltySettings.php` (الأهلية) · هجرة `2026_07_27_120000_lift_branch_scoped_loyalty_settings_to_company` · FE: `webstore-loyalty-settings.component.ts` + i18n | ✅ `closed` — والعميل قفلها؛ QA اتختمت `ok` (تحققت إن كل عناصر الإصلاح على `main` وشغالة حيّ) |

## ملاحظات على المشروع

- **2026-07-27 — بداية التشغيل:** كل طوابير moon4 كانت فاضية، وأول تذكرة نزلت (ISS-2026-9293).
- **الكود المرجعي = moon4 المحلي** (`/home/moonui4`) — نفس كود تثبيتات العملاء؛ المالك بيعمل الريليس بنفسه بعد التسليم. تثبيت العميل `tarshoby.elbaset.com` موجود على نفس السيرفر (`/home/tarshoby`) و**قراءة فقط** بإذن المالك — الـ`.env` بتاعهم محجوب (وده مش لازم أصلاً).
- **🔴 مطب `loyalty.*`:** الإعدادات بتتكتب من الأدمن على `auth()->user()->company_id` بينما `LoyaltyController::summary()` بيقراها على `$customer->company_id` (بتيجي من `branches.company_id` وقت التسجيل). باقي كنترولرات إعدادات المتجر بتستخدم `ResolvesStoreCompany`/`StoreCompany::id()` — فاللويالتي شاذ عن الاصطلاح. الافتراضي المزروع لـ`point_value` = `0`، فرجوع `point_value: 1` مع `program_enabled: false` = الأرقام محفوظة والتفعيل لأ.
- **إعادة الإنتاج الجاهزة:** `PUT /store/admin/loyalty/settings {"enabled":true}` ثم `GET /store/loyalty/summary` بتوكن عميل → `program_enabled: true`. (اختُبرت على moonui4 و**رجّعت الحالة الأصلية** `loyalty.enabled=0` بعدها.)
- سياق تقني مهم في `knowledge-base/` محلياً (خصوصاً `plans/webstore-storefront/LEDGER.md`) — يُقرأ قبل أي تذكرة تخص المتجر الإلكتروني.
