البند ٢ — استخبارات التسعير (docs/35 جديد):
- توصيف تقني دقيق من كود سيرو: المصدر ليس "كشط أسعار معلنة" بل
بوت أندرويد على أجهزة حقيقية يشغّل 7 تطبيقات منافسة مُسمّاة
بمعرّف الحزمة (standalone_worker.php:665) بدفعات حتى 100 مهمة
- فصل ثلاثة أخطار مستقلة: الوصول الآلي · قانون المنافسة · البيانات
- الخطر الأكبر (لم يكن موثّقاً): بيع المحرّك لمشغّلَين في نفس المدينة
= ترتيب hub-and-spoke تقع مسؤوليته على المحور أي Tripz
- ضابط مقترح: مشغّل واحد لكل مدينة، في العقد وفي الكود
- تنظيف صياغة الموقع: صفر أسماء منافسين، وحُذفت "اعرف كيف يسعّر
منافسك قبل أن يعرف هو" لأنها تصلح دليل نيّة في نزاع
البند ٣ — طبقة القياس (docs/32 §5):
- شريط موافقة بأربع لغات، الرفض بنفس بروز القبول
- البكسل لا يُحمَّل قبل الموافقة (مطلوب لزوّار الاتحاد الأوروبي:
الموقع بالفرنسية والإسبانية)
- 8 أحداث تغطي القمع كاملاً حتى Lead بمعامل القناة
- PIXEL_ID فارغ عمداً: لا شيء ينكسر ولا شيء يُقاس حتى يُملأ
تحقُّق: صفر أخطاء console · صفر عناصر i18n فارغة في اللغات الأربع ·
fbq غير معرّف قبل الموافقة · الأحداث تُطلق بعدها
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
تنفيذ توصيات المراجعة النقدية التي لم تُنفَّذ في الجولة السابقة:
- حذف «الأمان والامتثال» $79 كوحدة → النواة (سبب قانوني: بيع الأمان
يُنشئ وثيقة إدانة يوم يقع حادث عند مستأجر لم يشترِ الوحدة)
- حذف «حزمة المدفوعات» $99 → النواة (الحلقة الأساسية لا ترقية)
- دمج «ذكاء الأسطول» $99 في باقة أسطول+ (تناقض تسموي)
- «المواصلات» تخرج من الشبكة → قسم #transit مستقل بجمهوره وCTA خاص
- شريط .core-band يعرض ما لا يُباع كحجة بيع
- إصلاح الانقلاب السعري: كانت سيادة $879 أرخص من أسطول+ محمّلة $903
الآن: علامة $585 < أسطول+ $599 < سيادة $879
- الشارات ← «متاح الآن» بتأكيد المالك أنها تعمل في إنتاج سيرو
- تصحيح سردية docs/33 §0 المستحيلة (كانت تبني التوسّع على باقة
تنتهي بعد 90 يوماً)
الترجمات مكتملة في اللغات الأربع — تحقُّق برمجي: صفر عناصر فارغة.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✨ تحسينات رئيسية:
1️⃣ إصلاحات توثيقية (docs/21, docs/33):
- S18 ليست مشكلة — النظام يفرض سحب الرصيد الكامل فقط
- إضافة تنبيه حالة عن عدم جاهزية الوحدات
2️⃣ إصلاحات الموقع (marketing/index.html):
- حذف قسم #ai-modules المكرر (التكرار المحرج)
- إضافة رابط «الإضافات» في القائمة العلوية (4 لغات)
- تغيير جميع الشارات إلى «قريباً الربع القادم» (حماية قانونية)
- 7 وحدات مدفوعة واضحة مع أسعار + وصف
3️⃣ النموذج التجاري:
- النواة رخيصة = سلاح اكتساب
- الوحدات = هامش الربح الحقيقي (land-and-expand)
- بدون تكرار محرج للزائر
⚠️ الحد الأدنى قبل التسويق:
- S4 و S5 تُصلح قبل أول استنساخ على سيرفر مستأجر
- جرد صدق بند بند على إنتاج سيرو
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
الأهم أولاً: `PaymentsService.webhook()` كان يقبل أي جسم `{ payment_id }`
بلا أي تحقّق توقيع — من يعرف معرّف دفعة معلَّقة كان يستطيع تحويلها «ناجحة»
ويشحن رصيداً من عدم (محفظة راكب أو رصيد سائق تشغيلي). الآن كل تغيير حالة
محروس بـ`adapter.verifyWebhook(headers, payload, tenant)`، ولا شيء يُقرأ من
الحمولة قبل ذلك كقرار ثقة — قراءة المرجع لتحديد المستأجر ليست قراراً.
البنية (docs/07 · docs/24 — P1):
- `PaymentAdapter`: charge() يعيد instant (كاش) · redirect (بوابة API حقيقية)
· invoice (بلا API، تسوية عبر P2).
- PayMob (مصر): تسلسل auth→order→payment_key→iframe حقيقي عبر fetch،
وتحقّق HMAC-SHA512 على تسلسل حقول ثابت (بروتوكول PayMob الرسمي بالضبط)
بمقارنة ثابتة الزمن. مفاتيح كل مستأجر مستقلة — حساب تاجر خاص به.
- كليق/شام كاش/MTN/سيرياتيل/زين كاش: محوّل مشترك واحد لأن سلوكها متطابق
فعلياً في سيرو (`create_*_invoice.php` تُنشئ فاتورة فقط، لا نداء بوابة
حيّاً) — مرجع + حساب استلام معروض، والتسوية عبر رسالة SMS لا webhook.
MTN/سيرياتيل الحقيقيَّين (توكن+OTP) موثَّقان كبند مفتوح: لا نبني تكاملاً
لا نملك اعتماداً حيّاً للتحقّق منه.
- `PATCH /payments/settings` لأدمن المستأجر: مفاتيح PayMob · حسابات
الاستلام · سرّ webhook الرسائل — الاستجابة لا تُعيد الأسرار.
تنظيف: إزالة الإشارات المتبقّية لحاوية `martin` من docs/07 (أُزيلت فعلياً
سابقاً)، وتحديث هيكل الكود الموثَّق ليطابق ما هو مبنيّ فعلاً.
25 اختباراً جديداً (226 إجمالاً) — منها توقيع PayMob محسوب فعلياً ومُتحقَّق،
وتلاعبٌ بالحمولة بعد التوقيع يُرفض، وسبع حالات تثبت إغلاق ثغرة الـwebhook.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
P2 (كليك/شام كاش بلا API):
- الرسالة تُحفظ خاماً **قبل** أي تحليل: التحليل قد يفشل فنحتاج الأصل لإعادة
المعالجة، وعند النزاع يكون النصّ الأصلي هو الحجّة لا تفسيرُنا له.
- النقطة تصنع المال، فرسالة مزوّرة = رصيد من عدم. الحماية: سرّ لكل مستأجر
بمقارنة ثابتة الزمن (المقارنة النصّية تسرّب السرّ حرفاً حرفاً زمنياً)،
وبصمة محتوى فريدة تمنع احتساب إعادة الإرسال مرتين.
- Gemini بحرارة صفر ومطالَب بإرجاع null عند عدم اليقين: نموذج يخمّن مبلغاً
يسوّي فاتورة بمال لم يصل. بلا مبلغ صريح → مراجعة بشرية لا تسوية.
- المطابقة بالمرجع أولاً، ثم بالمبلغ خلال 24 ساعة وبشرط فاتورة وحيدة —
فاتورتان بنفس المبلغ التباسٌ يُراجَع، لا تسويةٌ عشوائية لإحداهما.
- التسوية تمرّ بـmarkSuccess نفسه فلا يتفرّع مسار مالي ثانٍ.
إزالة حاوية martin: انطلق منصّة قائمة بذاتها لها خوادمها؛ دورنا طلب وردّ لا
استضافة خرائط (قرار المالك).
docs/25: جرد الحاويات · لماذا الدفع ليس خدمة منفصلة · سيرفر السوبر-أدمن
المنفصل ونطاقه الفرعي · WebSocket مقابل الاستطلاع بالأرقام · أحجام السيرفرات
على أساس الذروة لا المعدّل · نقل مستأجر · ترتيب التوسّع.
13 اختباراً جديداً (201 إجمالاً، كلها خضراء).
Co-Authored-By: Claude Fable 5 <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>
إصلاح انحدار أدخلتُه: /admin/overview كان يستعلم عن `t.created_at` وهو غير
موجود في كيان الرحلة (الاسم `requested_at`)، فترجع النقطة 500 ويُفرَّغ جدول
المستأجرين في لوحة السوبر-أدمن. فحص الأنواع لم يمسكه لأن استعلامات
QueryBuilder نصوص، والمحكّ الذي كان سيمسكه لم يُشغَّل.
قرارات المالك 2026-07-18:
- الخرائط: `intaleq_maps` حصراً (SDK انطلق على MapLibre). تصحيح قرار سابق
خاطئ: flutter_map + latlong2 طبقة منافسة تعطي نوعَي LatLng متضاربين.
السبب ليس عمل جوجل في سوريا — بل قِدَم بياناتها في المنطقة.
- docs/24 جديد: فلسفة المال — فصل الإيراد (شحن السائق · رسوم العمليات) عن
الأمانة (شحن الراكب)، بمحفظتين لا بحقل حالة، ودفتر مضاف فقط.
- P موسّعة (بوابات · تسوية بالرسائل لكليك/شام كاش · رسوم بالدولة · تقارير)
و O5 (ترتيب المهام المجدولة)، وتأجيل Android Auto/CarPlay.
- الحزم: نسخ سيرو الأحدث + live_activities لشاشة القفل في iOS.
تنظيف: إخراج 463 ملف بناء NDK من التتبّع، وإخراج شهادات التوقيع ومفتاح
App Store من المستودع (تُولَّد من جديد عند الحاجة) مع تحديث .gitignore.
الملفات باقية على القرص — أُزيلت من الفهرس فقط.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
الباك إند:
- تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً،
وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً.
- OTP_DEV_MODE كان يفشل مفتوحاً (`!== 'false'`): غياب المتغيّر أو خطأ مطبعي
يترك الإنتاج برمز ثابت يفتح كل حساب، ويحرس السحب المالي كذلك. صار
`=== 'true'` في الإعداد وفي قارئَيه، مع تحذير عند الإقلاع.
- N2: التحقّق من اللوغو عند الرفع (PNG/JPEG · 512+ · مربّع · سقف 5MB) عبر
قراءة الترويسة بلا اعتمادية — بدل اكتشاف أيقونة ممطوطة بعد النشر.
N3: السكربت يولّد build_config.dart بأعلام const (طبقات docs/22 §1.5)،
ويضبط bundle IDs واسم التطبيق والأيقونات/splash، و--build يشغّل Shorebird.
فلاتر:
- docs/23: قانون مُلزِم للتطبيقين (البنية · Cubit · طبقة الشبكة · الأعلام).
- السائق: أُزيل Dart القديم (GetX) مع الإبقاء على الأصيل والإضافات وشهادات
التوقيع وShorebird وFirebase الخاص به، وأُعيد هيكلته مطابقاً للراكب.
- طبقة الشبكة ترسل x-device-id وتجدّد التوكن عند 401 مرة واحدة فقط.
- الحزم موحّدة بين التطبيقين، والـAPI https حصراً في الاثنين.
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>
socket.io-client كان يُثبَّت في /tmp لكن السكربت في /s، وESM يبحث عن الحزم
من مجلد السكربت لا cwd. الأمر الصحيح ينسخ السكربت إلى /tmp قبل التشغيل.
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>
B4 نقاط التوقف: stops jsonb؛ المسار والتسعير يمرّان بالنقاط بالترتيب
(أصل → توقف₁ → … → وجهة) بجمع أرجل المسار. بلا توقفات = رِجل واحدة كالسابق.
B5 الحجز المسبق: scheduled_at + حالة scheduled جديدة (لا تدخل Redis ولا
تُوزَّع الآن). ScheduledTripsSweeper يعمل داخل الـAPI لا الـworker — لأن
الإطلاق يبثّ عروضاً عبر RealtimeGateway الموجود في الـAPI فقط. آمن مع عدة
نسخ: releaseDueScheduled يستعمل UPDATE شرطياً (scheduled→searching) فلا
تُطلَق رحلة مرتين. فهرس جزئي على scheduled المستحقة.
B8 مطابقة الوجهة: is_destination_match + destination_match_discount +
destination_match_discount_pct في التعرفة، يُطبَّق في settleFare. الخصم
على الراكب والسائق يقبض المخفَّض كاملاً (يربح رحلة في طريقه أصلاً). مؤجَّل:
محرّك المطابقة الذي يضبط العلم (وضع وجهة السائق).
هجرة: TripStopsSchedulingMatch. حالة trip جديدة: scheduled (تنتقل إلى
searching أو cancelled).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C1: جدول vehicles (مكافئ CarRegistration عند سيرو) — plate/vin مشفَّران،
make/model/year، color+color_hex (لتلوين السيارة في فلاتر)، fuel/owner/
category، is_default، status. سائق قد يملك أكثر من مركبة؛ VehiclesService
يضمن افتراضية واحدة دائماً (أول مركبة تلقائياً، حذف الافتراضية يرقّي غيرها).
C2: حقول الملف على drivers — gender، national_number (مشفَّر + فهرس أعمى
فريد لكل مستأجر، نفس نمط الهاتف)، name_arabic (مشفَّر)، birthdate، address،
الرخصة (type/categories/issue/expiry)، rejected_reason. عبر PATCH
/drivers/profile.
C3: ai_data + user_input (jsonb) على drivers و vehicles — مخرجات Gemini
مقابل مدخلات السائق، تُراكَم للمقارنة حقلاً بحقل.
C4: vehicle_photo min:2 في كتالوج الوثائق.
C5: نوع وثيقة face_liveness (فيديو) — الرفع يقبله بلا قيد mime.
قرار: المركبة كيان مستقل لا حقول مسطّحة (سيرو يفصلها بـisDefault). الحقول
القديمة على drivers تبقى للتوافق؛ المصدر الجديد vehicles.
هجرة: VehiclesAndDriverProfile (جدول vehicles + أعمدة السائق + فهرس فريد
جزئي على الرقم الوطني).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
سجلّ قابل للمعالجة لكل ما رُصد في كود سيرو الحيّ أثناء بناء Tripz: IDOR
السحب · double-spend · تسرّب رسم 3500 · 5 كتابات بلا معاملة · مقارنة كلمة
مرور نصّية تتجاوز الـhash · كلمة مرور مشتركة ثابتة · OTP 3 خانات · مسح
جدول وفكّ تشفير للتحقق · AES-CBC بـIV ثابت · إلخ. كل بند بخطورته وإصلاحه
المقترح لسيرو وكيف عالجناه في Tripz.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
مراجعة سيرو (auth/otp/): مصر تستعمل Kazumi SMS مع failover لواتساب،
وسوريا/الأردن Nabeh. عندنا كان مزوّد واحد فقط.
- integrations/otp/: واجهة OtpProvider + OtpDispatcher يوجّه حسب
tenant.countryPack بسلسلة failover (eg: [kazumi, nabeh] · jo/sy: [nabeh])
- KazumiSmsProvider (مصر SMS) + NabehOtpProvider (غلاف على NabehService)
- كل مزوّد يرسل رمزاً نولّده نحن (Redis) — استبعدنا نمط Intaleq (يولّد الرمز
بنفسه) حفاظاً على مصدر واحد للرمز
- موحَّد: AuthService.sendOtp و PayoutsService.issueOtp يمرّان بنفس المُوزِّع
الآن — أُزيل كل استدعاء Nabeh مباشر. أي مسار يحتاج رسالة يستدعي الخدمة
الموحّدة (طلب المالك)
- فشل السلسلة كاملة = ServiceUnavailable صريح، لا صمت
قرار المالك (2026-07-17): بلا كلمة مرور إطلاقاً. المصادقة = هاتف + OTP مرة
→ جلسة مربوطة بالجهاز (D2). التسجيل بالهاتف لا Google/Apple (محظوران في بعض
الدول). سيرو نفسه لا كلمة مرور حقيقية له (password = hash(email) وهمي،
ومقارنة نصّية غير آمنة في مسار التجربة). يطابق أوبر/كريم/inDrive.
مراجعة حدّ الطلبات في سيرو (RateLimiter.php): حدود مسمّاة لكل نوع + fallback
بملف عند تعطّل Redis (fail-closed). عندنا مسار OTP fail-closed أصلاً؛
تخزين Throttler على Redis مؤجَّل للتوسّع الأفقي الفعلي (موثّق في D4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ThrottlerModule.forRoot(...) كان مسجَّلاً في app.module.ts منذ البداية —
بلا أي حارس يطبّقه (لا APP_GUARD ولا @UseGuards). التسجيل وحده لا يفعل شيئاً
في NestJS: كل نقطة في الـAPI، بما فيها verify-otp و payouts/*، كانت بلا أي
حدّ طلبات إطلاقاً منذ اليوم الأول.
- APP_GUARD → ThrottlerGuard يُفعّل الحدّ العام (120/دقيقة) على كل نقطة
- عدّاد محاولات لكل (مستأجر، رقم) في AuthService.verifyOtp — الحماية
الحقيقية ضد تخمين الرمز لا الحدّ العام: رمز 4 خانات = 10000 احتمال،
ومهاجم يدوّر IP يتجاوز أي حدّ بالـIP وحده. 5 محاولات ثم إبطال الرمز،
نفس نمط payouts.service (I4)
- حدود أضيق للأهداف عالية القيمة: send-otp (كل إرسال يكلّف رسالة واتساب
مدفوعة فعلياً)، verify-otp، payouts/request، payouts/confirm
- HealthController مُستثنى (@SkipThrottle) — مراقبة تشغيل بلا بيانات حساسة
قرار وعي بالمخاطرة: بنيت أولاً حارساً مخصَّصاً يتتبّع بالمستخدم المصادَق
لا بالـIP وحده (مهم لموبايل — NAT عند مشغّلي الجوّال يجمع آلاف المستخدمين
خلف IP واحد)، لكن تطلّب حقناً يدوياً دقيقاً (InjectThrottlerOptions/
InjectThrottlerStorage) — خطأ فيه يمنع إقلاع التطبيق كاملاً، ولا بيئة هنا
لاختبار NestFactory.create() قبل الدفع (jest ينشئ الخدمات يدوياً فلا يكشف
أخطاء DI لحارس عالمي). حذفته ورجّحت الأمان بالتتبّع الافتراضي — موثّق في
docs/17 D4 لتُنفَّذ حين يمكن اختبارها فعلياً على السيرفر.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
E — اتصال ثم إلغاء (من driver_ride_scam عند سيرو):
- E1: driver_called_passenger + last_call_at/by/count على الرحلة نفسها لا
Redis فقط — حقيقة دائمة تفيد التحقيق بعد انتهاء الرحلة وزوالها من الكاش.
يُسجَّل عند call:offer (بدء المكالمة الفعلي) في RealtimeGateway عبر
Repository<Trip> مباشرة — لا استيراد TripsModule كاملاً (يستورد
RealtimeModule أصلاً، فدورة استيراد)
- E2: FraudService.recordCallThenCancel — عدّاد يومي منفصل عن عدّاد الإلغاء
العام بالساعة. 3/يوم = إنذار لا يمنع الإلغاء، 6/يوم = حظر مؤقت (طلب
المالك حرفياً). يرتبط فقط إن كانت المكالمة خلال 30 دقيقة من الإلغاء
تشفير الهاتف at-rest (ملاحظة المالك) — بفهرس أعمى منفصل، لا تشفير ساذج:
- AES-256-GCM عشوائي IV يمنع WHERE phone = :x من المطابقة أصلاً (كل تشفير
مختلف شكلاً حتى لنفس الرقم) — لو طُبِّق التشفير مباشرة بلا هذا لكسر تسجيل
الدخول بالكامل بصمت
- users.phone_bidx = HMAC-SHA256(الرقم المطبَّع) — حتمي، يصلح للبحث
وللقيد الفريد. القيد انتقل من (tenant_id, phone) إلى (tenant_id,
phone_bidx) — القديم كان سيقبل تكرار نفس الرقم الحقيقي فعلياً
- findByPhone/create يحسبان البصمة؛ لا نقطة أخرى تقرأ phone بمساواة خام
- هجرة EncryptPhone تُشفّر الصفوف القائمة فوراً وتبني البصمة بأثر رجعي —
استثناء متعمَّد من تسامح decrypt() المعتاد (الهاتف عمود بحث يومي، خطره
كنص صافٍ أعلى من name)
- يعتمد على تطبيع D1: بصمة حتمية لنفس *النص*، فلا بد أن يصل الرقم مطبَّعاً
قبلها وإلا عاد نفس الرقم بصيغتين ليصير رقمين مرة أخرى
هجرات: TripCallSignal · EncryptPhone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
D1 — تطبيع أرقام الهاتف (JO/EG/SY):
- common/phone/phone.service.ts: مفتاح قانوني واحد لكل رقم حقيقي — يقبل
صفراً محلياً/دولياً/+/00/بلا صفر ويرجع صيغة دولية موحّدة بلا +
- يعالج لبس مصر تحديداً الذي ذكره المالك: "01012345678" يُطبَّع بمفتاح 20
الصحيح لا "2" — الخوارزمية عامة فلا تحتاج معرفة اللبس، فقط تحذف الصفر
البادئ وتضيف مفتاح الدولة الحقيقي
- send-otp و verify-otp و platform/users/role تمرّ كلها بالتطبيع الآن؛
كانت تخزّن/تبحث بالرقم الخام كما كُتب — رقم واحد بصيغتين = حسابان
- هجرة NormalizePhones تُصحّح حسابات الاختبار الموجودة على السيرفر، بحذر:
تتخطّى أي تصادم بدل كسر القيد الفريد (tenant_id, phone)
- أُزيل config.callingCodes المكرّر — مصدر واحد للحقيقة
D2 — ربط الجلسة بالجهاز (نمط سيرو): **مبنيّ ومطفأ**
(AUTH_REQUIRE_DEVICE_BINDING=false) حتى يرسل فلاتر x-device-id. منفَّذ داخل
JwtStrategy.validate نفسها (passReqToCallback) لا كحارس يُضاف يدوياً لكل
متحكّم — فلا نقطة محميّة يمكن نسيانها. التوكن يحمل hash(deviceId) لا القيمة
الخام. verifyOtp/refresh يمرّران x-device-id من المتحكّم عند إصدار التوكن.
D3 — HMAC: منفَّذ فعلاً ضمن I6 (SigningService/SignatureGuard)، لا تكرار.
خطأ ضبطته قبل الدفع: كتبت مفتاح `auth:` ثانياً في configuration.ts —
كائنات JS تسمح بمفاتيح مكرّرة والأخير يطغى، فكان سيمحو otpDevMode/otpTtl/
otpLength بالكامل. دُمج في الكتلة الأصلية بدل مفتاح جديد.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
النطاق: tripz-api.intaleqapp.com (قرار المالك) — مملوك وجاهز، ومحايد عن
اسم العلامة الذي لم يُحسم فلا يحتاج تغييراً لاحقاً.
ثغرتان كانتا ستُبطلان TLS من أصله:
- API كان منشوراً على 0.0.0.0:4010 → صار 127.0.0.1. بلا هذا يبقى
http://IP:4010 مفتوحاً فيتجاوز TLS كلياً وتصير الشهادة زينة
- **Postgres كان مكشوفاً على 0.0.0.0:55432** — القاعدة على الإنترنت مباشرة
→ صار 127.0.0.1. المنفذ غير القياسي إخفاء لا أمان. الوصول من الماك عبر
نفق SSH (docs/20 §4)
trust proxy = 1: خلف Nginx كان req.ip يصير 127.0.0.1 للجميع فيمتلئ سجل
التدقيق المالي (I7) بعنوان البروكسي بدل السائق — أثرٌ بلا قيمة عند النزاع.
قفزة واحدة فقط: الثقة المفتوحة تسمح للعميل بتزوير X-Forwarded-For.
docs/20: DNS → موقع reverse proxy → شهادة → **إعداد ترقية WebSocket**
(بدونه يسقط التتبّع الحيّ) + مهلة 3600s للنبضات طويلة العمر + فحص تحقّق
أهمّه أن http://IP:4010 يجب أن **يفشل**.
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>
PlatformGuard (x-platform-secret، مقارنة ثابتة الزمن) يحرس admin/tenants*
و admin/features و platform/*. بدونه كان أدمن أي مستأجر يرقّي اشتراكه بنفسه
— أي أن المجموعة K كلها كانت زينة. سرّ غير مضبوط = منع الجميع لا فتح الباب
(الإعداد الناقص خطأ تشغيلي شائع، وفتح الباب عنده أسوأ من إغلاقه).
admin/users/:id/role تبقى لأدمن المستأجر: نطاقها من التوكن، وليست نقطة منصة.
سياسة الدين لكل مستأجر (قرار المالك): كل مشغّل يحدّد كم ديناً يسمح به،
و0 خيار صريح = لا دين إطلاقاً.
- tenants.settings jsonb — سياسات تشغيل يحكمها المستأجر، منفصلة عن features
(استحقاقات يحكمها السوبر-أدمن): «كيف تشتغل» مقابل «ماذا اشتريت»
- credit-policy.ts: debt_allowance + signup_bonus لكل عملة. الافتراضات
صُحّحت لقيم المالك: 2 JOD · 200 SYP · 200 EGP (كانت 5/500/500)
- تُخزَّن موجبةً («كم ديناً أسمح؟») لا كأرضية سالبة — أوضح للمشغّل و0 بلا
لبس في الإشارة
- PATCH /credit/policy لأدمن المستأجر؛ updateSettings يدمج عميقاً فلا يمحو
سياسة أخرى، ويُبطل الكاش وإلا سرت السياسة القديمة حتى ساعة
- إعداد مكسور يسقط للافتراض لا لدين بلا حدّ. والصفر لا يُبتلع (فخّ ||)
هجرة: TenantSettings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
القاعدة (docs/19): علم الميزة في التطبيق قرار عرض لا حدّ أمني. من يفكّك
التطبيق ويفعّل الأعلام يرى الشاشة ثم يصطدم بـ403 من FeatureGuard.
- K1: FeatureGuard + @RequiresFeature — tenant_id من التوكن الموقَّع لا من
ترويسة. الوحدة عالمية عمداً: إجبار كل وحدة على استيرادها = نقطة منسيّة
يوماً ما، والمنسيّة ميزة مجانية للجميع
- K2: كتالوج الميزات + افتراضات الباقات. الافتراض هو المنع (قائمة بيضاء):
ميزة جديدة تبقى محجوبة حتى تُمنح صراحةً
- K3: GET /admin/features · GET /admin/tenants/:id/entitlements ·
PATCH /admin/tenants/:id/subscription — يدمج features لا يستبدلها
(استبدالها كان سيمحو المشتريات السابقة) ويُبطل الكاش فوراً
- K4: drivers_max عند apply — عند الإنشاء فقط، فسائق قائم لا يُطرد بتغيير باقة
- K5: /tenant/config يرجع الاستحقاقات المحسوبة لا features الخام (الخام
تجاوزات فقط فكان سيُظهر ميزات الباقة مطفأة)
- K6: اختبار أن الترويسة لا تزوّر المستأجر + أن الافتراض منع لا سماح
- K7: السيادة = كل الميزات — المستأجر يملك السيرفر فالحجب هناك وهم لا حماية
محروس: dispatch · chat · payments/charge. المدفوعات لكل نقطة لا للصنف —
الـwebhook بلا JWT فحارس الصنف كان سيمنعه.
seed: المستأجر التجريبي siro صار sovereign (قرار المالك: أول مشترك بالباقة
الكاملة) — وأيضاً يمنع حجب dispatch وحدّ 500 سائق من إسقاط سكربتات التحقق.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
فلسفة العمولة الصحيحة (docs/18): السائق يقبض أجرة الراكب **كاملة**،
والعمولة تُخصم من رصيد تشغيلي مدفوع سلفاً. ما بُني في f3fdc1b كان نموذج
أوبر (اقتطاع من الأرباح) — عكس المطلوب.
- J1: driver_credits + credit_txns. خصم ذرّي كنمط I1 لكن **بلا شرط رصيد**
ولا قيد >= 0: الدين مسموح عمداً (قرار المالك: الرحلة لا تُقطع أبداً)
- J2: خصم العمولة عند completed لا paid — الرحلة تمّت فالعمولة استُحقّت.
قاعدة واحدة للكاش والمحفظة (§5.5): لا تفريع حسب وسيلة الدفع
- J3: price_for_driver = price_for_passenger. TariffEngine.split →
commission، وبلا سقف بالأجرة لأن الخصم على رصيد منفصل يجوز أن يسلب
- J4: الحجب عند تجاوز credit_floor فقط، عند setOnline و accept — لا أثناء
رحلة. الحجب يمنع الاتصال أصلاً وإلا عُرضت رحلات يُرفض قبولها
- J6: مكافأة التسجيل عند الاعتماد (3 JOD / 300 SYP جديدة / 300 EGP).
حارسان: فحص تطبيقي للحالة الشائعة + فهرس فريد جزئي للسباق
- J7: credit_floor لكل عملة
- J8: GET /credit + /credit/transactions
تصحيح B3: مشوار الوصول تعويضُ إلغاء بعد انقضاء الانتظار المجاني — لا بند
في كل أجرة. أُزيل مفتاح pickup_leg.charge.
هجرة: DriverCredit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
18 — الرصيد التشغيلي: السائق يشحن سلفاً، الراكب يدفع له الأجرة كاملة،
والعمولة تُخصم من الرصيد لا من الأجرة. مقارنة صريحة: هذا نموذج inDrive/
Yango/سيرو لا نموذج أوبر — وهو المعيار الفعلي في أسواق الكاش المستهدفة.
ثمنه ثلاثة التزامات: الحجب عند نفاد الرصيد (بدونه ينهار)، باقات لكل عملة،
ومعالجة حاجز دخول السائق الجديد.
19 — الاستحقاقات: علم الميزة في التطبيق قرار عرض لا حدّ أمني. الهندسة
العكسية تكشف شاشة تنادي نقطة ترجع 403. الحدّ الحقيقي FeatureGuard على
السيرفر + tenant_id من JWT موقَّع. الحقيقة التي تُقال صراحةً: ما يعمل كلياً
على الجهاز لا يُحمى، ووضع السيادة لا يُحمى تقنياً أصلاً (المستأجر يملك
السيرفر) — فليشمل سعره كل شيء بدل وهم حماية.
تصحيحان على المجموعة B (كلاهما بُني على افتراض خاطئ مني):
- B3: مشوار الوصول تعويضُ عدم حضور عند الإلغاء بعد 5 دقائق انتظار،
لا بند في كل أجرة كما بنيته
- B6: price_for_driver = price_for_passenger — العمولة لا تُقتطع من الأجرة
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
B6 فصل السعر (الأهم — أساس كل عمل مالي لاحق):
- price_for_passenger / price_for_driver / commission_amount / commission_rate
- العمولة تُعرَّف في التعرفة: commission { percent, flat, min }، ولا تتجاوز
الأجرة أبداً (دخل السائق لا يصير سالباً)
- **إصلاح**: تسوية المحفظة كانت تُضيف للسائق كامل أجرة الراكب — أي أن
العمولة كانت تضيع. صار الراكب يُخصم passenger والسائق يُضاف له driver
- العرض يحمل earnings (ما يقبضه السائق) لا أجرة الراكب
B2 الانتظار: يبدأ عند driver_arrived ويُحتسب عند in_progress. أول 5 دقائق
مجانية (free_waiting_min قابل للضبط لكل تعرفة)، والزائد بسعر النافذة.
B3 مشوار الوصول: يُقاس من موقع السائق الحيّ لحظة القبول حتى نقطة الالتقاط.
لا يُحتسب على الراكب ما لم يُفعّله المالك (pickup_leg.charge) — تفعيله يرفع
كل أجرة فهو قرار لا افتراض صامت. فشل القياس لا يُفشل القبول.
B7 الطوابع: driver_going_at · arrived_at · started_at (+ completed_at).
كشف «الإنهاء السريع» صار يقيس من بدء الرحلة لا إسنادها — المشوار والانتظار
ليسا جزءاً من زمنها.
B1: كان منفَّذاً أصلاً — driver_arrived و in_progress حالتان منفصلتان منذ P1.
معلَّق على I2: العمولة محسوبة ومسجّلة لكنها لا تُقيَّد في محفظة المنصة
(لم تُبنَ بعد).
هجرة: TripPricingAndTimestamps (تملأ الرحلات القائمة: دفع = قبض).
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>
كان كل نبضة موقع (كل 1-3 ثوانٍ لكل سائق متصل) تكلّف استعلاماً + كتابة صف
السائق في Postgres — أثقل حمل كان على القاعدة، وأثقل حتى من سيرو.
- H1/H2: نبضة الموقع = Redis فقط، بعتبات سيرو (10م / 1.0 سرعة / 5° اتجاه).
سائق واقف = EXPIRE واحد: لا كتابة، لا نقطة مسار، ولا بثّ للراكب
- H3: لقطة آخر موقع على drivers (heading/speed/loc_status/loc_updated_at)
يكتبها الـworker دورياً. لم نُنشئ جدولاً منفصلاً — drivers أصلاً صف واحد
لكل سائق
- H4: driver_tracks — تتراكم في قائمة Redis ويُدرجها الـworker بعبارة واحدة
- H5: فهرسا available/busy — البحث يمسح المتاحين فقط. السائق يصير busy عند
القبول ويعود available عند الإنهاء/الإلغاء
- H8: المطابقة والاحتيال و«الرحلات المتاحة» تقرأ الموقع الحيّ من Redis.
كشف تزوير الوصول خاصةً يجب ألّا يحكم بلقطة دورية
الـworker صار حقيقياً (كان setInterval فارغاً): سياق Nest مستقل، تفريغ كل
5s، قفل يمنع تراكب الدورات، وتفريغة أخيرة عند SIGTERM.
مؤجَّل من H: H6 (سلوك السائق) و H7 (ساعات العمل) — تحليلات تُبنى من
driver_tracks لاحقاً.
هجرة: DriverLocations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
الأساس: common/cache — كاش-جانبي فوق Redis. مبدأ صارم: فشل Redis لا يُسقط
الطلب (يُسجَّل ويُرجَع للقاعدة) — الكاش تحسين أداء لا مصدر حقيقة.
- G1: لغة المستخدم في Redis (كانت استعلاماً قبل كل إشعار)؛ تُكتب عند
PATCH /users/me فيصير الإصابة دائمة
- G2/G3: توكنات FCM في Redis — يُبطَل عند register وعند اكتشاف توكن ميت.
البث الجماعي صار بلا استعلامات قاعدة
- G4: كاش tenant resolve/config + إبطال عند الإنشاء
- G5: كاش التعرفة و ride-types + إبطال صريح عند أي إنشاء
- G6: التقييم يتراكم في Redis، والقاعدة تُكتب مرة واحدة يومياً لكل طرف
(حجز الكتابة ذرّي بـLua). التأخير مقصود ليبعد الاحتكاك بين الطرفين.
أُضيف ratings.target_user_id و users.rating — تقييم السائق للراكب كان
يُخزَّن بلا هدف فلا يُجمَّع أبداً
- كاش الخرائط (route/reverse/geocode) بإحداثيات مقرَّبة 4 خانات (~11م):
نداء انطلق كان يهيمن على p50 (2.9s). لا يُخزَّن الرجوع لخط مستقيم ولا
استجابة فاشلة
تصحيح: ادّعيت سابقاً أن tenants.resolve يعمل على كل request — خطأ.
tenant_id يأتي من الـJWT؛ resolve يُستدعى عند الدخول و3 كنترولرات فقط.
الحمل الأثقل فعلياً هو رفع موقع السائق (المجموعة H).
هجرة: RatingsBothParties.
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>
Groups: A realtime/FCM/Redis/race · B trip model (started/waiting/price split/stops) · C driver+car data (color_hex, ai_data) · D security (phone normalize EG, device fingerprint, HMAC) · E fraud (driver_ride_scam) · F chat
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- RedisIoAdapter wires @socket.io/redis-adapter with dedicated pub/sub clients
- channel key 'tripz:sio' isolates on shared Redis
- wired in main.ts via useWebSocketAdapter; multiple app replicas can now share WS connections
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>