قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.
الخريطة:
backend · payment_server · loction_server · ride_server ·
passenger_server · docker · dashboard · stress_test → الجذر
siro_rider → apps/rider siro_driver → apps/driver
siro_admin → dashboards/admin siro_service → dashboards/service
android_bot → apps/android_bot socialBot → apps/socialBot
نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.
لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).
⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
يجب ضمّ الحزم داخله أسوة بـ apps/rider.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
قرار المالك 2026-07-27: الباك إند المعتمد صار باك إند سيرو PHP، ويُنقل
إلى هذا المستودع كنسخة جديدة باسم «انطلق» على api.intaleqapp.com.
backend/ → backend-archive/ (305 ملفاً، إعادة تسمية بلا تعديل محتوى)
+ README-ARCHIVE.md يوضّح سبب الأرشفة وسبب عدم الحذف: scripts/e2e-test.mjs
هو أدق توثيق سلوكي لدورة الرحلة كاملة، ويبقى مرجعاً عند بناء الوحدات
الناقصة في باك إند PHP (المحفظتان · الاستحقاقات · محرّك التسعير · الوحدات).
مسارات backend/ في docs/*.md لم تُعدَّل عمداً — سجلّ تاريخي.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
الدورة السابقة أعلنت الفشل والنظام سليم — العطل كان في السكربت:
1. **`fixed` تُرفض بـ400 وهذا هو المطلوب** (أُخرجت من الزرع عمداً)، لكن
السكربت كان يعدّها فئة متوقَّعة فأسقط التغطية إلى 72/81. صارت في
`REJECTED_CLASSES`: الرفض يُتحقَّق منه صراحةً، وغيابه هو الفشل.
2. **429 على `send-otp`** (10 لكل 5 دقائق) عند تشغيلتين متتاليتين — يُنتظر
ويُعاد بدل الإبلاغ عن فشل ليس في النظام.
3. **إبطال كاش التعرفة صار خطوة في الدورة.** `getActive` تكاشي 900 ثانية،
و**Redis حاوية منفصلة لا تتأثر بإعادة تشغيل الـapi** — وتطبيق
`tariffs.sql` مباشرةً على القاعدة يتجاوز التطبيق فلا يُبطل شيئاً. النتيجة
أن النظام خدم أسعاراً قديمة عبر دورتَي نشر كاملتين وهو يعلن النجاح.
المسح الآن مبنيّ في السكربت لا متروك للتذكّر.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ضاعت دورتان كاملتان على السبب نفسه: تسلسل `up -d && restart && <اختبار>`
كان يشغّل الاختبار بينما الـapi ساقطة، فيظهر خطأ الإقلاع كـ`EAI_AGAIN` داخل
الاختبار — أي في المكان الخطأ تماماً، فيُشخَّص كعطل شبكة أو عطل سكربت.
`verify-and-test.sh` يجعل الدورة واحدة ومحروسة:
- يفحص `.env` قبل الإقلاع ويصلّح التعارض القاتل (OTP_DEV_MODE=true مع
NODE_ENV=production — حارس `main.ts` ينهي العملية لا يحذّر، فتدخل الحاوية
حلقة إعادة تشغيل).
- يميّز «حلقة إعادة تشغيل» عن «بطء إقلاع»: الأولى خطأ إقلاع يُطبع سببه من
اللوغ فوراً بدل انتظار عقيم.
- **لا اختبار قبل ردّ /health فعلياً** — هذه هي البوابة التي كانت غائبة.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
نصيحتي السابقة بتفعيل `OTP_DEV_MODE=true` على السيرفر كانت خاطئة: حارس في
`main.ts` يرفض الإقلاع عند `OTP_DEV_MODE=true` مع `NODE_ENV=production`،
فسقطت حاوية الـapi في حلقة إعادة تشغيل وظهر الفشل كـ`EAI_AGAIN` في السكربت.
الحارس صحيح ولا يُضعَف لأجل اختبار: رمز ثابت على سيرفر متاح للإنترنت يعني
أن معرفة رقم هاتف تكفي لدخول أي حساب.
السكربت صار يقرأ الرمز الحقيقي من Redis (`send-otp` يخزّنه قبل محاولة
الإرسال، فيبقى محفوظاً حتى حين يفشل الإرسال لرقم وهمي). عميل RESP أدنى على
`node:net` بلا تبعيات — الحاوية عابرة بلا `node_modules`. المفتاح يستعمل
UUID المستأجر لا الـslug، فتُجلب خريطة الـslug→UUID من `/admin/tenants`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
عطلان في سكربت التسعير، كشفهما أول تشغيل حقيقي:
1. **إعلان نجاح على العدم** — الأخطر. فشل تسجيل الدخول فجُمع صفر صفّ، فمرّ
كل تحقّق فراغاً وطبع «الاختبار ناجح». اختبار يعلن النجاح وهو لم ينفّذ
شيئاً أسوأ من اختبار يفشل: الفشل يُرى. أُضيف حارس عدم + تحقّق تغطية
(المنفَّذ مقابل المتوقَّع)، وكلاهما يُخرج برمز فشل.
2. **تسجيل الدخول تجاوز `send-otp`** — تجاوز التطوير في `verifyOtp` مشروط
بـ`OTP_DEV_MODE=true`، وبدونه لا رمز في Redis فيُرفض الثابت 1234 بـ401.
صار يمرّ بـ`send-otp` ويأخذ `dev_code` من استجابتها، ويقول صراحةً حين
يكون الوضع التطويري مطفأً بدل أن يفشل بلا تفسير.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
يطلب رحلة حقيقية لكل فئة خدمة (9) × 3 مسافات × 3 بلدان عبر الـAPI الحي،
فالمسافة والزمن يأتيان من محرّك المسارات (انطلق) لا من افتراض.
يتحقّق آلياً من أمرين كانا عطلين حقيقيين قبل المجموعة M:
- ولا طلب يرجع بلا سعر (التعرفة المفقودة كانت تنتج رحلة مجانية بعمولة صفر).
- الفان أغلى من الاقتصادي في كل بلد (الأرقام الخام السابقة عكست الترتيب).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
الأساس المالي حسب docs/24: فصل ما نملكه عمّا نحتفظ به لغيرنا.
- جدولان منفصلان (`tenant_revenue_ledger` · `tenant_pending_ledger`) لا جدول
واحد بعمود نوع: استعلامٌ ينسى الشرط كان يجعل المالك يسحب من مال الركّاب.
مضافان فقط، والرصيد مشتقّ لا حقل يُحدَّث.
- فهرس فريد (tenant_id, ref) = حارس التسوية المزدوجة. التصادم يُلتقط من
القاعدة لا بالفحص المسبق وحده — نداءان متزامنان يمرّان معاً قبل أي كتابة.
- رسم العملية من مصدر واحد (35 ل.س · 5 ج.م · 0.20 د.أ) قابل للتجاوز من إعداد
المستأجر، مع قصّه عند المبلغ حتى لا يخرج المستخدم بصافٍ سالب.
- توجيه الدفع: شحن السائق ← إيراد + رصيده التشغيلي · شحن الراكب ← أمانة ·
الرسم ← إيراد. وفُتح مسار شحن السائق الذي لم يكن له مدخل إطلاقاً.
- ثغرة سُدّت: `purpose` يصل من الجسم، فراكب كان يستطيع إرسال `credit_topup`
فيُسجَّل مالُه إيراداً ويُشحن حساب سائق لا يملكه. الدور الآن من التوكن.
- `/admin/overview`: الإيراد من الدفتر، وعمولة الرحلات حقل منفصل عنه.
- `/admin/wallet/summary` و`/revenue-by-reason` لأدمن المستأجر.
25 اختباراً جديداً (188 إجمالاً، كلها خضراء). أحدها أمسك عطلاً فعلياً: صافٍ
صفري كان ينادي الدفتر بصفر فيرمي خطأً بعد قيد الرسم — عملية نصف مطبَّقة.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- اللوحات الثلاث تُخدم من نفس أصل الـAPI (/panel/{superadmin,admin,service})
عبر useStaticAssets + bind mount، فترث TLS القائم بلا دومين ولا CORS.
- تعليق المستأجر كان زخرفة: tenant.status يُكتب ويُعرض بلا أي فرض. الآن
يُفرض في JwtStrategy (نقطة واحدة) + AuthService.resolveTenant، مع إبطال
الكاش ليسري فوراً، وتمرير عند تعذّر القراءة حتى لا تسقط المنصة كلها.
- GET /admin/overview: رحلات · GMV · إيراد المنصة لكل مستأجر باستعلام
واحد مجمَّع (لا N+1).
- توثيق قرار طبقات التطبيق (const flags + tree-shaking · أصيل موحّد
لـShorebird · طبقتا الإعداد) في docs/22 §1.5.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
نقاط سرد إدارية (خلف RolesGuard admin/dispatcher):
- GET /drivers/admin/list — سائقو المستأجر (بترشيح الاعتماد)
- GET /trips/admin/list — رحلات المستأجر (بترشيح الحالة/الراكب)
- GET /payouts/admin/list — سحوبات المستأجر (بترشيح الحالة)
- GET /admin/users/search?phone= — بحث خدمة العملاء (يُطبَّع ويُبحث بالفهرس
الأعمى، لا يُعرض الرقم الخام)
اللوحتان (SPA مكتفية ذاتياً، vanilla JS، RTL):
- admin-web: دخول أدمن (هاتف+OTP) → سائقون (اعتماد) · رحلات · مراجعة وثائق
(قبول/رفض) · سحوبات (تحويل/فشل). تحقّقت أنها تُصيَّر نظيفاً في المعاينة.
- service-web: بحث مستخدم برقمه → بياناته ورحلاته + تفاصيل رحلة بالمعرّف.
المصادقة هاتف+OTP → JWT بدور admin/dispatcher (نفس الموبايل). الحماية على
السيرفر؛ الواجهة عرض فقط. الوصول بـ?tenant=<slug>.
E2E موسّع: يثبت أن السائق/الراكب (غير أدمن) يُرفضان من النقاط الإدارية (403).
الشكاوى تحتاج وحدة complaints (لاحقاً). لا هجرة.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
المولّد السابق أنتج 11 خانة فرفضه تطبيع الهاتف (الأردن يريد 0 + 9). صار
uniqJoPhone ينتج 07 + 8 خانات = 10، و uniqStr للرقم الوطني/اللوحة (لا تمرّ
بالتطبيع).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
نقطة التقييم عند الفتح (طلب المالك): GET /trips/rating/pending يرجع أحدث
رحلة منتهية لم يقيّمها المستخدم (سائقاً أو راكباً) + العدد. التطبيق يناديها
عند كل فتح ليفرض شاشة التقييم قبل رحلة جديدة. TripsService.
finishedForParticipant يجلب المنتهية، و RatingsService.pendingRating يطرح
المقيَّمة باستعلام واحد.
scripts/e2e-test.mjs: اختبار شامل متكامل لكل وظائف الباك إند —
- تسجيل الدخول/الحساب + تطبيع الهاتف (نفس الرقم بصيغتين = حساب واحد)
- إعداد السائق: تقديم + ملف (رقم وطني مشفّر) + مركبة + اعتماد + إعادة دخول
للحصول على دور driver في التوكن
- الرصيد التشغيلي (مكافأة التسجيل) + عدم الحجب
- اتصال السائق ورفع الموقع (Redis) + المطابقة + /trips/available
- طلب رحلة (تسعير + عرض) + كل الحالات (assigned→...→completed→paid)
- WebSocket: اتصال JWT + رصد trip:update (اختياري، يتخطّى إن غاب
socket.io-client)
- خصم العمولة من الرصيد التشغيلي عند الإنهاء
- تسوية المحفظة عند paid (خصم الأجرة من الراكب)
- التقييم: رحلة معلّقة → تقييم الطرفين → تُمسح المعلّقة + منع التكرار
- السحب بـOTP خطوتين (طلب بلا خصم → تأكيد بالرمز → حجز)
أوامر التشغيل في docs/15 (مع/بلا WebSocket). يتطلّب OTP_DEV_MODE=true.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
I4 — OTP على السحب عبر نبيه. تدفّق خطوتين، وقاعدته: **لا يتحرك مال قبل
إثبات الهوية**:
- POST /payouts/request → يرسل رمزاً، حالة pending_otp، **بلا خصم**
- POST /payouts/:id/confirm → يتحقق ثم يحجز ذرّياً (خصم I1)
- حدّ 5 محاولات (رمز 4 خانات يُخمَّن في دقائق بلا حدّ)، الرمز يُستهلك مرة
- complete يرفض طلباً بلا otp_verified_at (حارس ضد تخطّي التأكيد)
- fail لا يردّ مالاً لطلب pending_otp — لم يُخصم منه شيء، وردّه يخلق مالاً
I5 — biometric_method/at + device_id + request_ip على السحب. أثرٌ للتحقيق
لا مصادقة: العميل يستطيع ادّعاءها، والمصادقة الحقيقية JWT + رمز واتساب.
I6 — HMAC **مبنيّ ومطفأ** (PAYMENTS_REQUIRE_SIGNATURE=false) حتى يوقّع
فلاتر؛ تفعيله الآن يقطع كل سحب. مفتاح **لكل جلسة** يُصدره الدخول لا سرّ
ثابت في التطبيق (الثابت يُستخرج بالهندسة العكسية فيصير التوقيع مسرحية).
يوقّع timestamp.METHOD.path.body بنافذة 5 دقائق؛ rawBody مفعّل في main.
I7 — tripz_audit_log append-only: من·ماذا·متى·أي IP وجهاز. لا يرمي أبداً —
فشل التدقيق يجب ألّا يُسقط عمليةً مالية نجحت.
I2 أُلغيت الحاجة إليها: إيراد المنصة = الشحن، و credit_txns دفتره فعلاً؛
محفظة ثانية = دفتر مزدوج يحتاج مطابقة. الباقي تقرير لا محفظة.
I3/I8 مؤجَّلتان بوعي (توثيق في docs/17).
wallet-race-test.mjs حُدِّث: السباق انتقل من request إلى confirm، وأُضيف
محكّ أن الطلب وحده لا يمسّ الرصيد.
هجرة: PayoutSecurityAndAudit (تعتبر السحوبات القائمة مُتحقَّقة وإلا رفض
complete صرفها للأبد).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
loadtest يرسل نبضة واحدة لكل سائق فلا يختبر العتبات إطلاقاً. هذا السكربت
يرسل 10 نبضات من مكان ثابت ثم 8 نبضات بتحرّك 30م، ويتحقق من علم significant
الذي ترجعه POST /drivers/location — أي العتبات على Redis حقيقي عبر الـAPI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
المجموعة A (docs/17):
- A1: FCM على كل انتقال حالة (priority high) + حذف التوكنات الميتة
- A2: common/i18n (ar/en) + عمود users.language — الإشعارات بلغة المستخدم
- A3: TripStateService — حالة الرحلة الجارية في Redis hash (TTL 6س)؛
الانتقال صار UPDATE شرطي + قراءة واحدة بدل ~5 استعلامات
- A4: قبول ذرّي — CAS بـLua في Redis + UPDATE ... WHERE status='searching'
كحَكَم نهائي؛ أول سائق يفوز والباقي يُرفضون بلا لمس القاعدة
- A5: مجموعة العروض في Redis + بث trip:offer_taken و FCM لبقية السائقين
- A6: GET /trips/available — السائق يسحب الطلبات القريبة
- A7: FCM data-only بحمولة كاملة للـoverlay
I1 — إصلاح سباق المحفظة (ثغرة مالية):
- credit/debit كانا read-modify-write على balance بلا قفل → خصمان متزامنان
يكتبان فوق بعضهما. صارا UPDATE ذرّي واحد بشرط balance >= :amount،
والقيد+الرصيد في معاملة واحدة
- wallet_txns.balance_after للتدقيق + CHECK (balance >= 0) كشبكة أمان
- إنشاء المحفظة عبر ON CONFLICT DO NOTHING (سباق ثانٍ كان كامناً)
الاختبارات تعمل على السيرفر (docs/15):
- npm test صار جزءاً من مرحلة builder — فشل اختبار = فشل بناء = لا نشر
- pg-mem + ioredis-mock: بلا شبكة وبلا قاعدة حقيقية
- scripts/wallet-race-test.mjs للتزامن الحقيقي على السيرفر
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>