Commit Graph
21 Commits
Author SHA1 Message Date
Hamza-AyedandClaude Opus 4.8 8f6c46f0ac feat: D5 — OTP متعدد المزوّدين حسب الدولة (من سيرو) + قرار بلا كلمة مرور
مراجعة سيرو (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>
2026-07-17 19:24:01 +03:00
Hamza-AyedandClaude Opus 4.8 c258a8c4b8 fix: D4 — حدّ الطلبات كان مُعطَّلاً كلياً رغم أنه يبدو مفعَّلاً
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>
2026-07-17 17:47:03 +03:00
Hamza-AyedandClaude Opus 4.8 253fbb4b2a feat: المجموعة E (كشف الاحتيال) + تشفير الهاتف at-rest
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>
2026-07-17 17:25:18 +03:00
Hamza-AyedandClaude Opus 4.8 1eb79fee99 feat: المجموعة D — تطبيع الهاتف + ربط الجلسة بالجهاز
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>
2026-07-17 17:03:56 +03:00
Hamza-AyedandClaude Opus 4.8 5cf288b30b docs: تثبيت نتيجة TLS على السيرفر (منجَز ومُثبَت)
https://tripz-api.intaleqapp.com → 200 عبر nginx+شهادة.
http://194.163.173.157:4010 → Connection refused (لا 401 بل رفض اتصال).
110/110 اختباراً بعد إصلاح trip_id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 16:55:35 +03:00
Hamza-AyedandClaude Opus 4.8 d2a34f753c feat: TLS — إغلاق المنافذ المكشوفة + الثقة بالبروكسي + دليل الإصدار
النطاق: 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>
2026-07-17 16:36:14 +03:00
Hamza-AyedandClaude Opus 4.8 1605754722 feat: باقي المجموعة I — أمان السحب (OTP + بصمة + HMAC + تدقيق)
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>
2026-07-17 16:22:00 +03:00
Hamza-AyedandClaude Opus 4.8 91e1692457 feat: حارس السوبر-أدمن + سياسة الدين لكل مستأجر
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>
2026-07-17 15:26:41 +03:00
Hamza-AyedandClaude Opus 4.8 aae01fc4ee feat: المجموعة K — استحقاقات الاشتراك مفروضة على السيرفر
القاعدة (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>
2026-07-17 14:24:50 +03:00
Hamza-AyedandClaude Opus 4.8 d9b0faab2d feat: المجموعة J — الرصيد التشغيلي + تصحيح نموذج العمولة (B3/B6)
فلسفة العمولة الصحيحة (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>
2026-07-17 14:03:23 +03:00
Hamza-AyedandClaude Opus 4.8 2edd8f6916 docs: نموذج الرصيد التشغيلي (18) والاستحقاقات (19) + تصحيحان على B
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>
2026-07-17 13:32:32 +03:00
Hamza-AyedandClaude Opus 4.8 f3fdc1bc20 feat: المجموعة B (شريحة 1) — فصل السعر + الانتظار + مشوار الوصول + الطوابع
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>
2026-07-17 03:11:36 +03:00
Hamza-AyedandClaude Opus 4.8 7e24c63dd8 test: سكربت تحقّق مسار المواقع على السيرفر (H)
loadtest يرسل نبضة واحدة لكل سائق فلا يختبر العتبات إطلاقاً. هذا السكربت
يرسل 10 نبضات من مكان ثابت ثم 8 نبضات بتحرّك 30م، ويتحقق من علم significant
الذي ترجعه POST /drivers/location — أي العتبات على Redis حقيقي عبر الـAPI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 03:03:34 +03:00
Hamza-AyedandClaude Opus 4.8 0ed3a3160a feat: المجموعة H — المواقع في Redis، والقاعدة من الـworker فقط
كان كل نبضة موقع (كل 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>
2026-07-17 02:49:45 +03:00
Hamza-AyedandClaude Opus 4.8 a83dd7e5e5 feat: المجموعة G — Redis خط أول والقاعدة خط احتياط
الأساس: 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>
2026-07-17 02:39:34 +03:00
Hamza-AyedandClaude Opus 4.8 857652404b docs: تثبيت نتيجة اختبار تزامن المحفظة على السيرفر (I1 مُثبَت)
100 خصم متزامن → 50 نجاح / 50 رفض، 250→0، لا مال ضائع أو مخلوق.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:31:04 +03:00
Hamza-AyedandClaude Opus 4.8 9d6b752ea8 feat: المجموعة A (زمن حقيقي + FCM + Redis) + إصلاح سباق المحفظة
المجموعة 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>
2026-07-17 02:23:00 +03:00
HamzaandClaude Opus 4.8 b7e91e5de6 docs: backend backlog from owner review + Siro schema comparison (17)
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>
2026-07-17 01:42:04 +03:00
HamzaandClaude Opus 4.8 f3383c3cf8 feat(security): field encryption AES-256-GCM + random IV (at-rest)
- common/crypto/crypto.util.ts: encrypt/decrypt (GCM, random 96-bit IV), format v1:base64(iv|tag|ct)
- EncryptedTransformer applied to users.name + drivers.vehicle_plate (auto, no service change)
- blindIndex (HMAC) helper for future searchable-field encryption (phone)
- tolerant decrypt for legacy plaintext → no migration needed (varchar holds base64)
- ENCRYPTION_KEY env + boot warning if unset; docs/16-encryption.md
- fixes Siro's documented CBC+fixed-IV flaw

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 19:24:27 +03:00
HamzaandClaude Opus 4.8 e0ccd2efb7 feat(realtime): Socket.IO Redis adapter — enable horizontal scaling
- 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>
2026-07-16 18:06:24 +03:00
HamzaandClaude Opus 4.8 95fea546f5 first commit: منصة Tripz — خطط كاملة + سكافولد باك إند NestJS/Docker
- docs/00-15: دراسة، بنية، محرك تعرفة، تسعير، نموذج استئجار، تكاملات، بيانات، realtime، خطة، devops، لاندنج، مخاطر، اصطلاحات سيرفر، تدفق نشر
- backend/: NestJS 11 على Docker (health + tenants + عزل tenant_id + بادئة tripz_ + Redis DB 3)
- apps/rider, apps/driver, dashboards/admin-web, dashboards/superadmin-web (هياكل)
- sync-to-server.sh + .gitignore

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 15:22:06 +03:00