Files
tripz-llc/docs/18-driver-credit-commission.md
T
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

131 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 18 — الرصيد التشغيلي وعمولة السائق (نموذج الدفع المسبق)
> **طبقتان مختلفتان لا تُخلطان:**
> - **[05-pricing-billing](05-pricing-billing.md)** = كيف تقبض **Tripz** من المستأجر (اشتراك + نسبة GMV).
> - **هذا الملف** = كيف يقبض **المستأجر** (سيرو مثلاً) عمولته من **سائقيه**.
>
> مصدره: قرار المالك (2026-07-17) — «فلسفة التطبيق اللي بدنا إياه».
---
## 1. الفكرة في سطر واحد
السائق **يشتري رصيداً تشغيلياً مقدَّماً**. الراكب يدفع للسائق **الأجرة كاملة** (كاش أو محفظة). المنصة **لا تلمس أجرة الرحلة إطلاقاً** — بل تخصم عمولتها من الرصيد التشغيلي المدفوع سلفاً.
## 2. المثال المرجعي (كلام المالك حرفياً)
```
السائق يشحن رصيده التشغيلي → 4.000 دينار
الراكب يصل ويدفع للسائق (كاش/محفظة) → 4.000 دينار ← يأخذها السائق كاملة
عمولة التطبيق 10% من 4.000 → 0.400 دينار
تُخصم من الرصيد التشغيلي → 4.000 - 0.400 = 3.600 دينار
```
**الرصيد التشغيلي بعد الرحلة = 3.600 دينار.** أجرة الراكب لم تُمسّ.
> ملاحظة على الترقيم: الـ«4 دنانير» في المثال صدفة — رصيد الشحن وأجرة الرحلة رقمان مستقلان تماماً. لو شحن السائق 50 ديناراً وأنهى نفس الرحلة لصار رصيده 49.600.
## 3. هل هذا نموذج متعارف عليه عالمياً؟ — **نعم، لكنه ليس نموذج أوبر**
هناك نموذجان سائدان، وكلاهما شرعي:
| | **اقتطاع من الأرباح (take-rate)** | **الرصيد المدفوع مسبقاً (prepaid credit)** ← نموذجنا |
|---|---|---|
| **من يقبض المال؟** | المنصة تقبض الأجرة ثم تحوّل للسائق الصافي | السائق يقبض الأجرة كاملة مباشرة |
| **من يستعمله** | Uber · Bolt · Careem · Grab · Didi | **inDrive** · Yango/Yandex (شركاء) · Maxim · أغلب التطبيقات المحلية في الشرق الأوسط وأفريقيا وجنوب آسيا · **سيرو** |
| **يناسب** | أسواق الدفع الإلكتروني الغالب | **أسواق الكاش الغالب** |
| **مشكلة الكاش** | السائق يصير **مديناً** للمنصة → تحصيل ومطاردة وديون معدومة | **لا توجد** — العمولة مقبوضة سلفاً |
| **التدفق النقدي** | متأخر | **مقدَّم** |
| **حاجز الدخول للسائق** | صفر | يدفع قبل أن يكسب ← يحتاج معالجة |
**الحكم:** النموذج الذي تصفه هو **المعيار الفعلي في الأسواق التي تستهدفها** (سوريا · الأردن · مصر) حيث الكاش هو الغالب. أوبر نفسها في الأسواق كثيفة الكاش تضطر لتتبّع **رصيد سائق سالب** ثم مطاردة تحصيله — وهو بالضبط الوجع الذي يلغيه نموذجك من أصله. اختيارك سليم، وليس نسخاً أعمى عن سيرو.
**لكن ثمنه ثلاثة التزامات** (تفصيلها في §5): الحجب عند نفاد الرصيد، وباقات شحن لكل عملة، ومعالجة حاجز دخول السائق الجديد.
## 4. ما الذي يتغيّر في الكود (تصحيح لما نُفِّذ في B6)
الشريحة الأولى من المجموعة B (commit `f3fdc1b`) نُفِّذت على **نموذج الاقتطاع**:
```ts
price_for_driver = price_for_passenger - commission // ❌ ليس فلسفتنا
```
الصحيح حسب هذا المستند:
```ts
price_for_driver = price_for_passenger // السائق يأخذ الأجرة كاملة
commission_amount = split(price_for_passenger) // تُحسب وتُسجَّل على الرحلة
→ ثم تُخصم من driver_credit (رصيد منفصل تماماً)، لا من الأجرة
```
**`price_for_driver` و`price_for_passenger` يبقيان مفيدين** — لكن سبب اختلافهما ليس العمولة، بل: خصم على الراكب (كوبون/عرض) بينما السائق يقبض كاملاً، أو مكافأة تفاوض (`ai_negotiated_bonus` عند سيرو).
## 5. الآليات المطلوبة
### 5.1 الرصيد التشغيلي (`driver_credit`)
- محفظة **ثانية منفصلة** عن محفظة أرباح السائق. لا تُخلط: هذه للعمولة سلفاً، وتلك لأمواله.
- دفتر قيود append-only + خصم ذرّي — **نفس نمط I1 حرفياً** (`UPDATE … WHERE balance >= :amount`).
- الخصم يحدث عند `completed` (لا عند `paid`): الرحلة تمّت فالعمولة استُحقّت، بغضّ النظر عن وسيلة الدفع.
### 5.2 الرصيد السالب مسموح — والرحلة لا تُقطع أبداً (قرار المالك 2026-07-17)
> «نعطي رصيداً سالباً مثل أوبر… ولا تتوقف العملية عند الرصيد. لو بدأ رحلة وعمولتها أكبر من رصيده، يكمل عادي وتُضاف إلى الدين — حتى نُسهّل التجنيد.»
فالتصميم **ليس** حجباً عند الصفر:
```
الرحلة الجارية → لا تُقطع أبداً مهما كان الرصيد. العمولة تُخصم ولو صار سالباً.
الدين ضمن الأرضية → يبقى متاحاً ويعمل طبيعياً (إشعار تذكير فقط).
الدين تجاوز الأرضية → يخرج من فهرس المتاحين: «اشحن رصيدك التشغيلي».
```
- **الخصم لا يفشل أبداً** — بخلاف محفظة الأرباح (I1) حيث الخصم مشروط بالرصيد. هنا الشرط الوحيد هو الأرضية، ويُفحص **بعد** الخصم لا قبله.
- **مقدار الدين المسموح سياسةُ المستأجر، لا ثابتٌ في الكود** (قرار المالك): كل مشغّل يحدّد كم ديناً يسمح به لسائقيه، **و0 خيار صريح = لا دين إطلاقاً**. يُخزَّن في `tenants.settings.credit.debt_allowance.{CURRENCY}` ويُضبط عبر `PATCH /credit/policy` (أدمن المستأجر). الافتراضات: **2 JOD · 200 SYP · 200 EGP**.
- يُخزَّن **موجباً** («كم ديناً أسمح؟») لا كأرضية سالبة — أوضح لواجهة المشغّل، و0 بلا لبس في الإشارة. الفحص عند `setOnline` و`accept` — **لا** أثناء رحلة جارية.
- مكافأة التسجيل كذلك قابلة للضبط (`settings.credit.signup_bonus`)، و0 = بلا مكافأة.
- إعداد مكسور (نص/سالب) يسقط للافتراض — لا لدين بلا حدّ ولا لحجب الجميع.
- **لماذا لا نحجب عند الصفر:** حجبٌ عند الصفر يعني سائقاً يُترك في منتصف يومه، وراكباً بلا سائق، وتجنيداً يموت. الدين المحدود أرخص من ذلك بكثير. لكنه يظلّ **محدوداً** — أرضية بلا سقف تعني عمولة غير قابلة للتحصيل إلى الأبد.
### 5.3 باقات الشحن لكل عملة
> ⚠️ **الليرة السورية الجديدة حذفت صفرين** — كل الأرقام السورية تُكتب بالعملة الجديدة.
لكل عملة باقاتها (أرقام مبدئية تُضبط من لوحة الأدمن):
```
JOD: [3, 5, 10, 25]
SYP: [500, 1000, 2500] ← بالعملة الجديدة
EGP: [100, 250, 500]
```
- الشحن عبر **[07-integrations](07-integrations.md)** وطرق الدفع لكل دولة (كليك · شام كاش · إي كاش · بيموب · فوري …).
- باقة قد تحمل **حافزاً**: «اشحن 50 واحصل على 55» — أداة تسويق مباشرة بيد المستأجر.
- الحافز يُسجَّل قيداً منفصلاً في الدفتر (`bonus`) لا يُخلط بالمدفوع فعلاً — وإلا فسدت المحاسبة.
### 5.4 مكافأة التسجيل (محسومة — قرار المالك)
السائق الجديد يبدأ **برصيد هدية** يُقيَّد في دفتره فور اعتماده، فيعمل من أول دقيقة بلا دفع:
| العملة | المكافأة |
|--------|----------|
| SYP (الجديدة) | **300** |
| EGP | **300** |
| JOD | **3–4** (يُحسم رقم واحد عند التنفيذ) |
- تُقيَّد كنوع `signup_bonus` **منفصلاً عن المدفوع فعلاً** — وإلا اختلطت الهدايا بالإيرادات في المحاسبة.
- تُمنح **مرة واحدة لكل سائق** — الحارس: قيد واحد بهذا النوع لكل `driver_id` (فريد على مستوى القاعدة، لا فحص تطبيقي).
### 5.5 وسيلة الدفع لا تغيّر شيئاً
| الدفع | من يقبض الأجرة | من أين العمولة |
|---|---|---|
| كاش | السائق مباشرة | الرصيد التشغيلي |
| محفظة الراكب | السائق (تُحوَّل لمحفظة أرباحه) | الرصيد التشغيلي |
**قاعدة واحدة لا استثناء لها** — وهذا أبسط بكثير من تفريع المنطق حسب وسيلة الدفع، وهو ما قاله المالك حرفياً: «إن كانت كاش أو محفظة».
> **مفارقة تستحق الانتباه:** في الدفع بالمحفظة، المنصة **تلمس المال فعلاً**، فتستطيع تقنياً اقتطاع العمولة منه. لكننا **لا نفعل** — عمداً. لأن قاعدتين مختلفتين حسب وسيلة الدفع تعني منطقاً مزدوجاً، ومحاسبةً مزدوجة، وسائقاً لا يفهم لماذا اختلف دخله. الاتساق أثمن من التحسين هنا.
## 6. لماذا تُحسب العمولة على الرحلة رغم أنها تُخصم من مكان آخر؟
لأن `commission_amount` على الرحلة هو **سبب** القيد في دفتر الرصيد التشغيلي. بدونه لا يستطيع السائق (ولا خدمة العملاء) الإجابة عن: «لماذا نقص رصيدي 400 فلس؟». كل خصم يشير إلى `trip_id`.
## 7. البنود المطلوبة (تُضاف للـ[17-backend-backlog](17-backend-backlog.md) كمجموعة J)
| # | البند |
|---|-------|
| J1 | جدول `driver_credit` + دفتر قيوده — خصم ذرّي داخل معاملة (نمط I1) |
| J2 | خصم العمولة من الرصيد التشغيلي عند `completed` (لا من الأجرة) |
| J3 | **تصحيح B6**: `price_for_driver = price_for_passenger` (السائق يقبض كاملاً) |
| J4 | الرصيد السالب مسموح: الخصم لا يفشل أبداً؛ الحجب فقط عند تجاوز `credit_floor` — عند `setOnline`/`accept`، **لا** أثناء رحلة جارية |
| J5 | باقات الشحن لكل عملة (SYP بالعملة الجديدة) + الحافز كقيد منفصل |
| J6 | مكافأة التسجيل: 300 SYP · 300 EGP · 3–4 JOD — مرة واحدة لكل سائق (قيد فريد على القاعدة) |
| J7 | `credit_floor` لكل مستأجر/عملة |
| J8 | شاشة/نقاط: رصيدي · تاريخ الخصومات · الشحن |
> **لا مستخدمين في Tripz بعد** (2026-07-17) — فلا حاجة لهجرة بيانات ولا توافق خلفي. مستخدمو سيرو يُهاجَرون لاحقاً في عملية منفصلة وبسيطة.
---
← ذو صلة: [05-pricing-billing](05-pricing-billing.md) · [17-backend-backlog](17-backend-backlog.md) · [19-entitlements-licensing](19-entitlements-licensing.md)