Commit Graph
20 Commits
Author SHA1 Message Date
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 551aa897e1 fix: trip_id عمود uuid — استخدام معرّف صالح في اختبارات الرصيد التشغيلي
كسر بناء السيرفر. مرّرت 'trip-1' (نصّ عادي) كـ trip_id في
driver-credit.service.spec.ts؛ العمود uuid فرفضه pg-mem تماماً كما سيرفضه
Postgres الحقيقي في الإنتاج. الخطأ في الاختبار لا في الكود — chargeCommission
في الإنتاج يستقبل دائماً trip.id وهو UUID فعلي.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 16:51:44 +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 da37dffa44 fix: أخطاء «الصفر الصادق» + عزل حالة ioredis-mock في الاختبارات
أول تشغيل فعلي للاختبارات على السيرفر كشف خطأين حقيقيين في الكود، لا في
الاختبارات:

- RatingAggregateService.get: البذر يكتب count='0' لهدف بلا تقييمات، و'0'
  نصٌّ صادق في JS — ففحص !h.count لا يمسكه وكانت ترجع {avg:0,count:0} بدل
  null. صارت مقارنة رقمية.
- DriverLocationService.readHash: نفس الفخ — خط عرض '0' إحداثي صالح لكنه
  نصٌّ صادق؛ صار الفحص == null.

الاختبارات:
- ioredis-mock يشارك مخزناً واحداً بين النسخ رغم new RedisMock() — أُضيف
  flushall في كل beforeEach (تسرّبت مفاتيح بين الاختبارات).
- ioredis-mock لا ينفّذ geoadd داخل multi() ولا redis.call، فلا يمكن اختبار
  فهرس GEO به: صار MatchingService ضعفاً مزيّفاً في اختبارات المواقع،
  والتحقق ينصبّ على منطق العتبات. سلوك GEO الحقيقي يُتحقَّق على السيرفر.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:56:50 +03:00
Hamza-AyedandClaude Opus 4.8 17c111a8f9 fix: multi().exec() قد يرجع null فلا يُفكَّك مباشرة (TS2488)
كسر بناء السيرفر. ioredis يعرّف exec() كـ [error, result][] | null،
فتفكيكه إلى [sumRes, countRes] خطأ نوع. صار يُقرأ بالفهرسة مع ??.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:52:30 +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-Ayed 8b3b57fe0f feat: integrate Gemini service and update dependency lockfile 2026-07-17 02:25:14 +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
Hamza-Ayed 4397b07d2f fix(auth): resolve tenant slug to UUID before user queries 2026-07-16 16:14:30 +03:00
Hamza-Ayed 4f11580d4a fix: ts compilation errors in auth 2026-07-16 16:05:28 +03:00
Hamza-Ayed 83f2d0854a Update codebase 2026-07-16 16:01:28 +03:00