# مستقبل حالة شحنات بوسطة (`Bosta-Webhook-Status-Receiver`)

> بيتحمّل أوتوماتيك في كل جلسة Claude — في Claude Code وCowork.
**بتعمل إيه:** بتستقبل أحداث الويبهوك من بوسطة (تغيير حالة شحنة)، تسجّلها خام
في D1، وتكتب آخر حالة في ٤ ميتافيلدات على الأوردر المطابق في شوبيفاي.
**مين بيستخدمها:** فريق العمليات — شاشة مراقبة للأحداث والاستثناءات.
**الإصدار:** Worker `v1.10.0` · الواجهة اتنقلت لمركز عمليات الشحن والتحصيل (آخر نسخة مستقلة `v1.18.0`)
> 🔴 **الريبو ده Worker وبس من 12-10-2026 (قرار أحمد).** الواجهة بقت صفحة جوّه
> `Delivery-COD-Operations-Center` → `Bosta-Webhook-Status-Receiver.html` (قسم «أدوات أخرى» · هب `v1.35.0`)،
> وهي **النسخة الوحيدة**. `index.html` اتشال من هنا **بلا تحويل من الرابط القديم** (بقى 404 من GitHub Pages).
> · أي تعديل على **الشاشة** (الجدول · الفلاتر · السجل · التابات) بيتعمل في ريبو الهب، وأي تعديل على الـ Worker هنا.
> · نص «فخاخ الأداة» و«استرجاع النسخ» تحت **بيوصف الواجهة المستقلة وقت حصوله** — الأسماء (`LS_SECRET` · `currentEmployee` · `IDLE-JS` · `list_events`...) جزء منها اتشال أو بقى في الـ shell؛
> آخر commit فيه الواجهة المستقلة `17fe81e` (`git show 17fe81e:index.html`).
> · 🔴 **السر:** الصفحة في الهب بتبعت سر المجموعة `delivery_cod_ops` — `WORKER_SECRET` على الـ Worker لازم يتدوّر لقيمتها + **Promote** (قبلها `401`).
> · ⚠️ `AUTH_APPS`/`login`/`logout` في `index.js` لسه موجودين (الكتلة القياسية) بس **الصفحة مابقتش بتناديهم** — الدخول من الرئيسية على Worker الهب.
> · ⛔ **`MIN_WORKER_VERSION` بتاعة الصفحة بقت `DCO_WORKERS.bostaWebhook.min` في ريبو الهب** (`1.7.0`) — ارفعها هناك لما الصفحة تعتمد على حاجة جديدة هنا.
---
## 🟢 الأداة شغّالة على الإنتاج — من 21-09-2026
الأداة بتستقبل أحداث بوسطة فعليًا وبتخزّنها. اللي اتقفل:
| البند | الحالة |
|---|---|
| الـ Worker مربوط بالريبو + Build watch paths | ✅ 20-09-2026 |
| GitHub Pages | ⛔ مابقاش مطلوب — الواجهة اتنقلت للهب (12-10-2026) |
| الأسرار على Cloudflare | ✅ 20-09-2026 — كلها خضرا في `diag` |
| تسجيل رابط الويبهوك في داشبورد بوسطة | ✅ 20-09-2026 (`x-bosta-webhook-key`) |
| جدول `bosta_webhook_events` في D1 | ✅ 21-09-2026 — **يدويًا من D1 Console** |
| الكتابة الفعلية على شوبيفاي | ✅ 22-09-2026 — أول كتابة ناجحة، بعد إصلاح `findOrder` |
> 🔴 **درس 20/21-09-2026 — كل أحداث اليومين دول ضاعت.** بوسطة كانت بتبعت
> والتحقق كان بيعدّي، بس `db.exec(SCHEMA_SQL)` كان بيفشل لأن الـ SQL كانت
> منسّقة على ٢٦ سطر (عقد `exec` = عبارة واحدة لكل سطر)، والـ Worker كان
> بيرد **200** فبوسطة ما أعادتش. والشاشة كانت بتعرض «مفيش أي حدث اتسجّل من
> بوسطة» لأن `list_events` كان بيبلع `no such table` ويرجّع `[]`.
> الاتنين اتصلحوا في v1.1.0/v1.2.0، والأحداث الضايعة **مش قابلة للاسترجاع**.
> 🔴 **درس 21→22-09-2026 — 100% من أحداث الكتابة فشلت من أول يوم، وده كان
> مختفي ورا رسالة عامة.** من أول ما جدول `bosta_webhook_events` اشتغل
> (21-09) لحد إصلاح 22-09، **الـ 448 حدث كلهم بلا استثناء** اتسجّلوا
> `write_status = 'shopify_write_failed'` — صفر `written`، صفر
> `match_failed`، صفر `dry_run_matched` (اتأكد بـ
> `SELECT write_status, COUNT(*) FROM bosta_webhook_events GROUP BY
> write_status` من D1 المباشر). السبب: `FIND_ORDER_QUERY` كان بيطلب
> `metafields(identifiers: [...]) { key value }` على `Order` — الحقل ده
> نوعه `MetafieldConnection` في شوبيفاي، مش بيقبل `identifiers` ومش بيرجّع
> `{ key value }` مباشرة. شوبيفاي كانت بترفض الاستعلام بخطأ GraphQL
> (`argumentNotAccepted` + `undefinedField` × 2) على **كل** حدث، قبل حتى ما
> يوصل لمرحلة تحديد S1/S2 — ولذلك عمودي «الجنب» و«طريقة المطابقة» في
> الشاشة كانوا فاضيين على كل صف، مش بس على بعضهم. الاختبار عن طريق `diag`
> (`Shopify OAuth: نجح`) **ماكانش كافي** لأنه بيستخدم استعلام مختلف تمامًا
> (`currentAppInstallation`) مش بيلمس `Order.metafields` خالص — الدرس:
> `diag` بيثبت إن التوكن شغّال، مش إن كل استعلام في الكود صحيح.
> بقى الاستعلام بيستخدم `metafield(namespace, key)` المفرد بـ alias لكل
> حقل (نفس نمط `shopify-graphql-helper` §3.3) بدل الصيغة الجماعية.
> **البانر الأحمر بتاع «فشل تخزين في D1» اللي فاضل شغّال دلوقتي مش مشكلة
> جديدة** — هو ذيل حادثة `SCHEMA_SQL` القديمة (آخر فشل تخزين مسجَّل
> 21-09-2026 08:49) لسه جوه نافذة الـ٢٤ ساعة المتحركة في `diag`، وهيوصل
> صفر لوحده من غير أي تدخّل.
### لسه مطلوب (مش بيمنع التشغيل):
1. **تسجيل `ecommoda-constants` §7 — قبل أول `writeLog` حقيقي.**
القيم دي **مأخوذة حرفيًا من تكليف البناء (brief v1.0.0, 19-09-2026,
Ahmed)** ولسه مش موجودة في نسخة `ecommoda-constants` المتاحة لهذه الجلسة
(آخر تحديث فيها 13-09-2026 — قبل تاريخ البريف):
```
tool : bosta_webhook_status
type : status_event · duplicate_skipped · match_failed ·
shopify_write_failed · unauthorized · login · logout
```
هذه الجلسة **مالهاش وصول** لتعديل ملف `ecommoda-constants` نفسه (مش ريبو
Git متاح في نطاق هذه الجلسة). 🔴 **الفلاج `WRITE_METAFIELDS="true"` اتعمله
push بالفعل (21-09-2026) والأداة بتكتب على شوبيفاي — فالتسجيل ده بقى
**متأخّر**، لازم يتعمل في أقرب فرصة.**
2. **الأداة تنضم لمجموعة سر `delivery_cod_ops` من أول يوم (قرار أحمد
19-09-2026)** — هذا القرار **جديد ومش مسجّل بعد** في
`ecommoda-constants` → `references/secret-groups.md` (المجموعة المسجّلة
حاليًا هناك `warehouse_ops` بس).
✅ **مؤكَّد من أحمد 20-09-2026: `delivery_cod_ops` مجموعة جديدة كليًا —
مفيش أي أداة تانية محتاجة تنضم لها دلوقتي.** يعني التسجيل في
`secret-groups.md` بسيط: قسم مجموعة جديد بعضو واحد بس
(`Bosta-Webhook-Status-Receiver`)، بمفتاح مشترك اسمه
`delivery_cod_ops_worker_secret` — مفيش حاجة تتراجع مع أداة تانية. لسه
لازم يتسجّل قبل ما القيمة الفعلية على Cloudflare تتحول من سر فريد
(الوضع الحالي) لقيمة المجموعة + `LS_SECRET` في `index.html` يتحدّث لنفس
الاسم (Standards Changelog #39 في `ecommoda-html-builder`).
---
## الروابط
```
الواجهة : https://ecommoda-dev.github.io/Delivery-COD-Operations-Center/Bosta-Webhook-Status-Receiver.html ← جوّه الهب (الرابط القديم .../Bosta-Webhook-Status-Receiver/ بقى 404 بقرار)
الـ Worker : https://bosta-webhook-status-receiver-worker.ecommoda-dev.workers.dev
اسم الـ Worker في الداشبورد: bosta-webhook-status-receiver-worker ← لازم يطابق name في wrangler.toml حرف بحرف
```
## الـ Endpoints
| المسار / `?action=` | البوابة | بيعمل إيه |
|---|---|---|
| `POST /webhook` | هيدر مخصص (اسمه = `BOSTA_WEBHOOK_HEADER_NAME`) | استقبال حدث بوسطة — **قبل** بوابة `WORKER_SECRET` في ترتيب الـ handlers |
| `check_employee` · `register_pin` · `verify_employee` · `log_logout` · `get_employees` | `WORKER_SECRET` | Universal D1 Auth القياسي |
| `list_events` | `WORKER_SECRET` | للشاشة — فلتر بـ `order` / `tracking` / `dateFrom` / `dateTo`، بيرجّع `shopify_order_id` (من v1.2.0) لهايبرلينك رقم الأوردر. 🔴 **من v1.10.0 بلا `raw_payload`** (`EVENT_LIST_COLUMNS` — ممنوع `SELECT *`) |
| `get_event_payload` | `WORKER_SECRET` | (v1.10.0) `?id=` → `raw_payload` لحدث واحد على الطلب (زرار «Raw Payload» في لوحة السجل) — `list_events` مابقاش بيرجّعه |
| `queue_summary` | `WORKER_SECRET` | (v1.9.0) عدّادات صف الرئيسية في مركز الشحن: `main` (أرقام التتبع تحت التوصيل + توزيع المحاولات `0/1/2+/null`) و`inReturn` — SQL مجمّع، قراءة بحتة. ⚠️ تعريفه مكرّر في `computeTabRows` بالواجهة (ريبو الهب) |
| `diag` | `WORKER_SECRET` | آخر حدث امتى · عدد آخر ٢٤ ساعة · `duplicate_skipped`/`match_failed` · وجود الأسرار (`!!` بس) · Shopify OAuth |
| `get_config` | `WORKER_SECRET` | `WORKER_VERSION` (الكتابة على شوبيفاي دائمة، مفيش فلاج يتفحص) |
| `get_logs` / `get_logs_count` / `get_logs_export` | `WORKER_SECRET` | سجل `logs` المشترك (تلقائي من §SHARED) |
## D1
```
tool : bosta_webhook_status
type : status_event · duplicate_skipped · match_failed · shopify_write_failed · unauthorized · login · logout
```
> 🔴 القيم دي **لسه مش مسجّلة** في `ecommoda-constants` §7 — راجع قسم STOP فوق.
**جدول إضافي غير `logs`/`employees` المشتركين:** `bosta_webhook_events` —
الحدث الخام كامل (idempotency claim على `UNIQUE(bosta_id, state,
bosta_timestamp)` + نتيجة المطابقة/الكتابة لكل حدث). الـ Worker بينشئه
تلقائيًا أول مرة (`CREATE TABLE IF NOT EXISTS`)، أو شغّل السطر ده يدويًا في
D1 Console الأول (سطر واحد، بدون تعليقات — قاعدة `ecommoda-constants` §8):
```sql
CREATE TABLE IF NOT EXISTS bosta_webhook_events (id INTEGER PRIMARY KEY AUTOINCREMENT, bosta_id TEXT NOT NULL, state INTEGER NOT NULL, bosta_timestamp INTEGER NOT NULL, tracking_number TEXT NOT NULL, business_reference TEXT NOT NULL, order_number TEXT, bosta_type TEXT, description TEXT, event_type TEXT, delivery_promise_date TEXT, number_of_attempts INTEGER, cod REAL, is_confirmed_delivery INTEGER, exception_reason TEXT, exception_code TEXT, matched_slot TEXT, match_method TEXT, write_status TEXT NOT NULL DEFAULT 'processing', metafields_written INTEGER NOT NULL DEFAULT 0, shopify_order_id TEXT, raw_payload TEXT NOT NULL, received_at TEXT NOT NULL, UNIQUE(bosta_id, state, bosta_timestamp)); CREATE INDEX IF NOT EXISTS idx_bwe_order ON bosta_webhook_events(order_number); CREATE INDEX IF NOT EXISTS idx_bwe_tracking ON bosta_webhook_events(tracking_number); CREATE INDEX IF NOT EXISTS idx_bwe_received_at ON bosta_webhook_events(received_at);
```
> 🔴 **`shopify_order_id` (v1.2.0)** — عمود جديد على جدول موجود بالفعل في
> الإنتاج. الـ `CREATE TABLE IF NOT EXISTS` فوق بيغطي التركيب من الصفر بس؛
> على القاعدة الحالية العمود بيتضاف **ذاتيًا** أول مرة `updateEventRow`
> تواجه `no such column` (نفس نمط self-healing بتاع `SCHEMA_SQL`) —
> **مفيش خطوة يدوية مطلوبة في D1 Console**. القيمة هي `legacyResourceId`
> بتاع الأوردر على شوبيفاي، بتتسجّل في أي حدث اتلاقاله أوردر (حتى لو فشلت
> بعد كده مطابقة S1/S2 أو الكتابة) — الأحداث اللي `الأوردر مش موجود على
> شوبيفاي` (`match_failed` من غير `orderNode`) بتفضل من غير قيمة، ورقم
> الأوردر في الشاشة بيبقى نص عادي بلا هايبرلينك ليها.
> 🔴 **`write_status = 'delivered_duplicate_skipped'` (v1.5.0)** — قيمة جديدة
> على عمود موجود بالفعل، مفيش تعديل schema. بتتكتب لما حدث Delivered (كود 45)
> يوصل لأوردر/slot اتسجّل عليه Delivered بنجاح فعليًا (`write_status='written'`)
> قبل كده — الحدث بيتسجّل في `bosta_webhook_events` عادي (وفي `logs` بنوع
> `status_event`) لكن **من غير أي نداء `metafieldsSet` تاني**. راجع "فخاخ الأداة
> دي" تحت لتفاصيل القرار.
## الأسرار — لسه محتاجة تتسجّل على Cloudflare (Dashboard → Settings → Variables → Secret)
| السر | القيمة |
|---|---|
| `WORKER_SECRET` | ✅ متسجّل حاليًا (20-09-2026) كقيمة فريدة لهذه الأداة. 🔴 لسه محتاج يتحوّل لقيمة مجموعة `delivery_cod_ops` المشتركة بعد ما تتسجّل في `secret-groups.md` (بند ٤ في STOP فوق) — مجموعة جديدة بعضو واحد، مفيش أداة تانية تتأثر |
| `BOSTA_WEBHOOK_HEADER_NAME` | اسم الهيدر **بالظبط** زي ما هو مسجّل في داشبورد بوسطة (Authorization key name) — **مش** `Authorization` |
| `BOSTA_WEBHOOK_HEADER_VALUE` | قيمة نفس المفتاح (Authorization Key) |
| `CLIENT_ID` / `CLIENT_SECRET` | نفس الـ Custom App المشترك في الستاك (`ecommoda-constants` §1) |
⚠️ اكتب أسماء الـ Variables **يدويًا بالكيبورد** — اللصق بيسيب مسافة مخفية
تخلي القيمة `undefined` بصمت. وبعد أي سر جديد → **Promote**.
⚠️ **لا يوجد أي هيدر `Authorization` في طلب بوسطة نفسه.** اسم الهيدر هو اسم
المفتاح المسجّل في داشبورد بوسطة حرفيًا (مثال حي من البريف:
`test-456: TEST-123`، مش `Authorization: TEST-123`).
## المضبوط فعليًا في الداشبورد
> اللي **متظبط بالفعل** — مش اللي المفروض يكون. محدَّث من `diag` و
> لقطات الداشبورد 21-09-2026.
```
Bindings : DB → ecommoda-dev-logs ✅
Secrets : WORKER_SECRET · BOSTA_WEBHOOK_HEADER_NAME (19 حرف) · ✅
BOSTA_WEBHOOK_HEADER_VALUE · BOSTA_API_KEY ·
CLIENT_ID · CLIENT_SECRET
Vars : SHOP_DOMAIN=6c7e1a-53.myshopify.com · ✅
SILENCE_THRESHOLD_HOURS="3" ← من [vars] في wrangler.toml
(WRITE_METAFIELDS اتشال نهائيًا من [vars] في 25-09-2026 — الكتابة بقت سلوك دائم بلا فلاج)
Cron : */30 * * * * (مراقبة السكوت — §7.1) ✅
Build watch paths : index.js + wrangler.toml ✅
Production branch : main · Builds for non-production branches: ON
```
> ⚠️ **الداشبورد مش مصدر حقيقة للـ Vars.** أي تعديل من الداشبورد بيتمسح عند
> أول build من git. `WRITE_METAFIELDS` اتغيّرت من الداشبورد لـ `true` في
> 20-09-2026 وكانت هتترجّع `false` عند أول ميرج — اتثبتت في `wrangler.toml`
> في 21-09-2026 (`ecommoda-constants` §6).
## CORS
`ALLOWED_ORIGINS` صارمة (`https://ecommoda-dev.github.io`) — لأن الأداة بتكتب
ميتافيلدات على أوردرات حقيقية في شوبيفاي. مسار `/webhook` نفسه غير محكوم
بـ CORS (نداء سيرفر-لسيرفر من بوسطة، مفيش Origin متصفح).
## فخاخ الأداة دي
- 🔴 **`db.exec()` في D1 = عبارة واحدة لكل سطر.** بيقسّم النص عند كل `\n`
ويعتبر كل سطر عبارة كاملة. SQL منسّق على أكتر من سطر بيرجّع
`incomplete input: SQLITE_ERROR` — **والجدول ما بيتعملش**. ده كلّفنا كل
أحداث 20/21-09-2026. `SCHEMA_SQL` في `index.js` دلوقتي مصفوفة
`.join('\n')` — عبارة لكل عنصر. **ممنوع إعادة تنسيقها على أسطر.**
(نفس قاعدة `ecommoda-constants` §8 اللي على أوامر الـ Console.)
- 🔴 **`200` على فشل التخزين = ضياع نهائي.** `/webhook` بيرد 200 على أي فشل
بعد التحقق (عشان بوسطة ماتحذفش الاشتراك) — يعني بوسطة **مش هتعيد** الحدث.
أي فشل في الكتابة لازم يبان في `diag` فورًا، مش في السجل بس.
- 🔴 **صفر صفوف مش دليل** (`ecommoda-constants` §7.0). `list_events` كان
بيبلع `no such table` ويرجّع `[]`، فالشاشة عرضت «مفيش أحداث» بدل «D1
مكسورة». دلوقتي بيرجّع **503** برسالة صريحة، وفيه بانر صحة منفصل.
- **مفيش هيدر `Authorization` في `/webhook`** — التحقق بهيدر اسمه ديناميكي من
`env.BOSTA_WEBHOOK_HEADER_NAME`. أي كود بيدوّر على `Authorization` هيرفض
١٠٠٪ من الأحداث بصمت.
- **بوسطة بتكرر نفس الحدث** حتى بعد رد 200 — الادّعاء الذرّي على
`UNIQUE(bosta_id, state, bosta_timestamp)` هو الحارس الوحيد.
- **الشحنات الأقدم من تسجيل رابط الويبهوك عمرها ما هتبعت حاجة** — مقصود
ومقبول (قرار أحمد)، **ممنوع بناء أي backfill من `/deliveries/search`**.
- 🔴 **الفلاج `WRITE_METAFIELDS` اتشال نهائيًا من الكود في 25-09-2026** — كان
`"true"` من 21-09-2026 (قرار أحمد) وماكانش فيه نية رجوع لـ`false` أصلًا،
فاتحوّلت الكتابة لسلوك دائم بلا شرط: الأداة بتكتب على شوبيفاي فعليًا من
أول حدث جاي **دايمًا** — مفيش `env.WRITE_METAFIELDS` في `index.js` تاني،
ومفيش `[vars] WRITE_METAFIELDS` في `wrangler.toml`، ومسار `dry_run_matched`
بقى كود ميت (الليبل لسه في `index.html` بس لعرض أي صف قديم لو موجود).
⚠️ **مرحلة `dry_run_matched` اتخطّت ومحصلتش أصلًا، وبعدها المطابقة فشلت
100% لحد 22-09-2026** بسبب باغ `FIND_ORDER_QUERY` (راجع درس 21→22-09-2026
فوق) — يعني المطابقة **لسه ما اتراجعتش على أحداث حقيقية ناجحة ولا مرة**.
أول أسبوع من الكتابة الفعلية (من 22-09-2026) لازم يتراقب من الشاشة (عمودي
«الجنب» و«طريقة المطابقة») وبانر فشل المطابقة.
- 🔴 **`Order.metafields` في GraphQL نوعها `MetafieldConnection` — مش بتقبل
`identifiers` ومش بترجّع `{ key value }` مباشرة.** ده اللي خلّى كل حدث
يفشل بـ `shopify_write_failed` من 21 لـ22-09-2026 (448/448). الصيغة الصح
للحقول المسمّاة المتعددة: `metafield(namespace, key)` المفرد + alias لكل
حقل — مش `metafields(identifiers: [...])` كأنها على `Query` الجذر
(`shopify-graphql-helper` §3.3). و**`diag` بيثبت إن التوكن شغّال بس مش
إن كل استعلام في الكود صحيح** — استعلامه (`currentAppInstallation`) مختلف
تمامًا عن `FIND_ORDER_QUERY`.
🔴 **والكتابة **مش** على الأربعة ميتافيلدات مع بعض —
حدث واحد بيكتب **حقلين بس**، حسب الجنب اللي اتحدد له (S1 أو S2):
```
حدث اتحدد له S1 → custom.bosta_webhook_status_update_s1 + custom.bosta_webhook_last_update_s1
حدث اتحدد له S2 → custom.bosta_webhook_status_update_s2 + custom.bosta_webhook_last_update_s2
```
الأربعة الميتافيلدات (`_s1`/`_s2` × status/last_update) موجودين بالفعل على
الأوردر من أدوات تانية — الأداة دي **مش** بتنشئهم، بتكتب فيهم بس. الحقلين
التانيين (بتوع الجنب اللي ما اتحددش) بيفضلوا زي ما هما — مفيش أي أوردر
بيتكتب عليه S1 وS2 مع بعض من نفس الحدث.
- 🔴 **أكشن RTO ← `In-Return` (v1.8.0 — طلب أحمد 13-10-2026).** حدث `type = RTO` بيكتب `In-Return` على الجنب اللي اتحدد له: S1 ← `custom.manual_status` · S2 ← `custom.status_2_r_e` (في نفس `metafieldsSet`، §4.4 في `matchAndWriteMetafields`). استثناء واعٍ تاني من قاعدة «ممنوع كتابة status_1/status_2». حارسين عشان الكتابة ماتدهسش شغل الموظف: ⛔ ماتتكتبش على حدث نهائي (45/46/48 — 46 معناها Returned)، ⛔ ولا لو القيمة الحالية على الأوردر `In-Return`/`Returned`/`Delivered`/`Cancelled` (بتتقرا من `FIND_ORDER_QUERY`). 🔴 **مرة واحدة فقط (v1.8.1):** الكتابة عند أول ظهور لـ RTO على الأوردر/الـ slot (مفيش حدث RTO تاني `written` عليهم في D1) — أي تحديث RTO بعده مايكتبش تاني حتى لو القيمة اتغيّرت. لو الكتابة الأولى فشلت المحاولة الجاية لسه أول مرة. أوردرات RTO القديمة مش بتتحدّث بأثر رجعي. ما اتجرّبش على شوبيفاي حقيقية.
- **ممنوع كتابة `custom.status_1`/`custom.status_2`** — دول بيشغّلوا إجراءات
مخزون في أداة تانية (`Order-Status-Updater`)، وكتابة أوتوماتيك هنا = مخزون
ما رجعش.
🔴 **استثناء واعٍ ومحدود (قرار أحمد 22-09-2026):** لما `type=SEND` +
`matched_slot=S1` + `state=45` + `description="Delivered"` حرفيًا (مش
`"45 · Delivered"`) بيتكتب كمان على `custom.manual_status` (قيمة `Delivered`
فقط) و`custom.package_whereabouts_s1` (قيمة `Client`) — إضافة على الحقلين
المعتاديين، مش بدل منهم. القيمة `Client` **مش** من enum
`ecommoda-order-lifecycle` (`Warehouse`/`Office`/`Courier`) وقناة بوسطة
(`Other_Regions`) خارج نطاق `package_whereabouts` أصلًا حسب Rule 17 من نفس
المهارة — الاتنين استثناء مقصود بطلب أحمد، مش سهو. الكود في
`matchAndWriteMetafields` (`§4.3`) — نفس شرط `WRITE_METAFIELDS`/الترتيب
الزمني اللي بيحكم باقي الحقول، مفيش مسار كتابة منفصل.
- **رقم التتبع أولًا، بعدين `type` كـ fallback** (§4.1 في البريف الأصلي) —
فشل الاتنين = `match_failed` + صفر كتابة، مش تخمين.
- **قيم `type` في payload الويبهوك شكل تالت** (`SEND`/`EXCHANGE`/...) مختلف
تمامًا عن `type.code` في `/deliveries/*` — الجدولين منفصلين
(`TYPE_TO_SLOT_FALLBACK` في الكود).
- ⚠️ **تسمية أعمدة الشاشة اتغيّرت في v1.3.0 — الإشارات التاريخية فوق (درس
21→22-09-2026، جدول استرجاع النسخ) بتستخدم الاسم القديم لأنها بتوصف وقت
حصولها.** الاسم الحالي: عمود `matched_slot` (S1/S2) بقى معنون **"الحالة"**
(كان "الجنب")، وعمود كود بوسطة الرقمي الخام (`state`) بقى معنون **"كود
الحالة"** (كان "الحالة") — عشان الاسمين ما يتلخبطوش مع بعض. البيانات في D1
ما اتغيّرتش، التسمية في الواجهة بس.
- 🔴 **أي حقل تاريخ راجع من بوسطة (`delivery_promise_date` تحديدًا) مش
مضمون يكون بصيغة `Date` قادر يفهمها.** `new Date(v)` على قيمة غير مفهومة
بترجّع Invalid Date بصمت، لكن `Intl.DateTimeFormat.formatToParts()` عليها
**بترمي** `RangeError: Invalid time value` — وحدث واحد بس بتاريخ غريب
وسط 200 كان كافي يوقف رسم الجدول كله (v1.3.0 → اتصلح v1.3.1، راجع
changelog تحت). `toValidDate()` في §CONFIG هي الحارس — أي كود جديد بيعرض
أو بيرتّب على حقل تاريخ **مش من `received_at`** (اللي الـ Worker نفسه
بيكتبه بـ `.toISOString()` فمضمون) لازم يعدّي عليها الأول، مش يستخدم
`new Date(v)` مباشرة.
- 🔴 **حدث Delivered تاني على نفس الأوردر/الـ slot مايكتبش على شوبيفاي تاني
(قرار أحمد 26-09-2026).** بوسطة بتبعت أكتر من حدث بنفس الحالة أحيانًا
(تأكيد/إعادة إرسال)، وإعادة نفس الكتابة على `bosta_webhook_status_update_s1`/
`_s2` وتاريخ التحديث كل مرة مالهاش داعي. الحارس في `matchAndWriteMetafields`
(قبل §4.2) بيدوّر على صف تاني بنفس `business_reference`+`matched_slot`+
`state=45`+`write_status='written'` قبل ما يكمل — لو لقى، الحدث الجديد
بيتسجّل بـ `write_status='delivered_duplicate_skipped'` من غير أي نداء
`metafieldsSet`. **الحارس مبني على `matched_slot` مش على `state` بس** — لو
الأوردر اتسجّل Delivered على S1 وبعدين جالك حدث Delivered تاني على S2 (نادر
لكن ممكن نظريًا)، الحدث ده هيتكتب عادي لأنه slot مختلف.
🔴 **نفس الحارس بقى على Returned to business (كود 46) كمان (قرار أحمد
27-09-2026)** — `write_status='returned_duplicate_skipped'`، نفس الشروط
بالظبط (`business_reference`+`matched_slot`+`state=46`+`written`). الحالتين
متعرّفين في `DUPLICATE_GUARDED_STATES` في `index.js` — أي حالة تالتة تتضاف
هناك بس.
- 🔴 **التحديث الدوري للشاشة لازم يفضل stale-while-revalidate (v1.11.0).**
قبل كده `loadEvents()` كانت بتمسح الـ٥ جداول بـ«جاري التحميل» كل ٦٠ث وتسحب
كل الأحداث على صفحات، فالجدول كان بيختفي ويرجع. القواعد الحالية: «جاري
التحميل» في أول تحميل بس · تحميل تراكمي بـ`afterId` + `ids` (صفوف حديثة أو
`processing` لأن `write_status` بيتغيّر بعد الإدراج — `afterId` لوحده هيفوّتها)
· مزامنة كاملة كل ٣٠ دقيقة وعند زرار «تحديث» · `isLoadingEvents` يمنع
التداخل · مفيش رسم من جديد لو مفيش تغيير · فشل دوري ما يمسحش الجدول ·
الدورة (٢ دقيقة) بتوقف والتاب متخبّي. **ممنوع** إرجاع أي `innerHTML =
'جاري التحميل'` في مسار التحديث الدوري. `list_events` بترجّع `maxId` و`updated`
(Worker ≥ 1.7.0)؛ الواجهة بترجع للسحب الكامل تلقائيًا لو الرد من غير `maxId`.
- 🔴 **تاب "🚚 الأوردرات تحت التوصيل" = صف واحد لكل `tracking_number` بآخر حالة فقط
(30-09-2026).** "الأحدث" = أكبر `bosta_timestamp` ثم `received_at` ثم `id`
(`computeLatestStateByTracking()` → `latestEventByTracking`). عدد "النتائج" بقى
عدد أرقام التتبع مش الأحداث، والفلاتر (وصف/كود/محاولات/نتيجة) بتشتغل على آخر
حالة بس. رقم التتبع اللي آخر حالته 47 مايظهرش هنا (بيروح تاب Exception)، واللي
وصل 45/46 بيروح الأرشيف. السجل الكامل لأي رقم بيتفتح من زرار "🕘 السجل" في
Drawer جانبي (`openHistoryDrawer`) — كله client-side على `rawEvents`، مفيش
تغيير Worker. **التجميع على مستوى رقم التتبع مش الأوردر** — أوردر بـ S1 وS2
بيبان صفين (قرار أحمد).
- 🔴 **تاب الأرشيف اتحذف (08-10-2026) والحالات النهائية = 45 · 46 · 48.**
`FINAL_STATES` في `index.html` — رقم تتبع وصله Delivered (45) أو Returned to business (46)
أو Terminated (48) ولو مرة بيتشال من "🚚 الأوردرات تحت التوصيل" (`computeFinalTrackingNumbers()`)،
ومفيش تاب تجميعي ليه. تابات "↩️ Returned to business" (46) و"✅ Delivered" (45) و"🛑 Terminated"
(48) **صف واحد لكل `tracking_number`** (آخر حدث بنفس الحالة — `latestPerTracking()`)، والتكرار
بيبان في "🕘 عرض السجل" بس. نتيجة التابات متخزّنة في `tabRows` وبتتحسب مرة في `renderAllTabs()`.
- 🔴 **الفلاتر في رأس العمود (v1.16.0 — `data-table-standard.md` v8.1.0).** مفيش قسم فلاتر فوق
الجدول؛ زرار القمع `[data-flt]` جنب اسم كل عمود بيفتح قايمة عائمة `.pop` على `body`
(`openPop`/`buildPop`/`popAction`)، وحالة الفلاتر لكل تاب في `tabState[tabKey]` (مفيش state
متشارك). فلتر «الوصف» و«كود الحالة» اندمجوا في فلتر واحد على «الحالة عند بوسطة»
(`stateDesc` = `45 · Delivered`). **ممنوع** إرجاع قسم فلاتر فوق الجدول أو `