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>
131 lines
12 KiB
Markdown
131 lines
12 KiB
Markdown
# 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)
|