Bosta-Webhook-Status-Receiver

# مستقبل حالة شحنات بوسطة (`Bosta-Webhook-Status-Receiver`) ![version](https://img.shields.io/badge/version-v1.18.0-blue) > بيتحمّل أوتوماتيك في كل جلسة 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`). **ممنوع** إرجاع قسم فلاتر فوق الجدول أو `