Files
tripz-llc/docs/24-tenant-wallet-revenue.md
T
Hamza-AyedandClaude Fable 5 b1a060c5ed feat: P0/P3/P5 — دفترا المستأجر ورسم العملية وتوجيه المال
الأساس المالي حسب 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>
2026-07-18 15:04:45 +03:00

92 lines
8.6 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.
# 24 — محفظة المستأجر وإيرادات المنصة (فلسفة المال)
> قرارات المالك 2026-07-18. هذا المستند يحكم **كل** ما يتعلّق بحركة المال، ويسبق أي كود في المجموعة P.
> ذو صلة: [18](18-driver-credit-commission.md) (الرصيد والعمولة) · [22](22-full-product-roadmap.md) §P · [[siro-reference]].
---
## 1. القاعدة الحاكمة: مالٌ لنا، ومالٌ نحتفظ به لغيرنا
الخطأ الذي يقع فيه أغلب التطبيقات — **وسيرو منها** — هو خلط النوعين في وعاء واحد، فيبدو الرصيد ضخماً بينما أغلبه دَيْن على المنصة لا ملكاً لها.
| النوع | مثاله | مِلك مَن؟ | في الميزانية |
|---|---|---|---|
| **إيراد** | شحن السائق لرصيده التشغيلي · عمولة كل عملية دفع | **المستأجر** (مالك التطبيق) | دخل مُحقَّق |
| **أمانة (pending)** | شحن الراكب لمحفظته · أجرة رحلة لم تُسوَّ بعد | **الراكب/السائق** | **التزام**، لا دخل |
> **المحصّلة العملية**: نقود الأمانة تمرّ عبر حسابنا لكنها ليست لنا — سنسلّمها للسائق أو نردّها للراكب. عدّها إيراداً يعني تقريراً ماليّاً كاذباً، وقراراً بالإنفاق من مالٍ ليس لنا.
## 2. محفظتا المستأجر
لكل مستأجر (مثال: `siro`) محفظتان منفصلتان — لا حقل واحد بحالتين:
| المحفظة | يدخلها | يخرج منها |
|---|---|---|
| **`tenant_wallet`** (محفظة المستأجر — «سيرو والت») | شحن الرصيد التشغيلي للسائقين · عمولة عمليات الدفع · عمولة الرحلات | سحب المالك لأرباحه |
| **`tenant_pending`** (أمانات) | شحن الراكب · أجرة محصَّلة لم تُسلَّم | تسوية للسائق · ردّ للراكب |
**لماذا محفظتان لا حقل `status`**: التقارير والسحب يقرآن `tenant_wallet` وحده. لو كان الفصل بحقلٍ في صفوف واحدة، فاستعلامٌ واحد ينسى شرط الحالة يجعل المالك يسحب من أموال الركّاب — وهذا خطأ يقع مرة واحدة ويكلّف ثقة السوق.
**دفتر مضاف فقط (append-only)** لكل محفظة: كل حركة صفٌّ جديد بمرجعها وسببها، لا تحديثٌ لرصيد. الرصيد مجموعٌ مشتق. (سيرو خزّن المال في `varchar(10)` وجمع نصوصاً — لا يُنقل هذا إطلاقاً.)
## 3. مسارات المال الثلاثة
### 3.1 شحن السائق رصيده التشغيلي ← **إيراد مباشر**
هذا هو نموذج العمل الأساسي (docs/18). السائق يدفع مقدَّماً ليعمل؛ المبلغ الحقيقي يصل → **`tenant_wallet`** فور نجاح الدفع. لا مرحلة أمانة: المال صار للمستأجر مقابل خدمة تشغيلية.
### 3.2 شحن الراكب محفظته ← **أمانة**
المال يدخل حسابنا لكنه **يبقى ملك الراكب** → **`tenant_pending`** + رصيد الراكب. حين يركب، يُخصم من رصيده وتُسوَّى الأجرة للسائق (ناقص العمولة). الجزء الوحيد الذي يتحوّل إلى إيراد هو **العمولة**.
### 3.3 عمولة عملية الدفع ← **إيراد**
رسم ثابت على **كل** عملية تحويل/دفع، محدَّد بالدولة (`countryPack`):
| الدولة | الرسم لكل عملية |
|---|---|
| سوريا | **35 ل.س** (بالعملة الجديدة) |
| مصر | **5 ج.م** |
| الأردن | **20 قرشاً** (0.20 د.أ) |
يُقتطع عند العملية ويُقيَّد في `tenant_wallet`. يُخزَّن **في إعداد المستأجر لا في الكود** — سيرو كتب الرقم مرتين في مكانين فتناقضا وتسرّب المال (docs/21).
## 4. جدول لكل وسيلة دفع + تقارير
نمط سيرو الصحيح: جدول مستقل لكل وسيلة (`cliq_invoices` · `invoices_shamcash` · `mtn_invoices` · `ecash_transactions` …) لأن لكل مزوّد حقولَه ومرجعَه وسلوكَ تأكيده. فوقها **محوّل موحّد** يعطي الباقي واجهة واحدة.
التقارير المطلوبة للوحة المستأجر: الدخل حسب الوسيلة · حسب اليوم · شحن السائقين مقابل الأمانات · العمولات المحصَّلة · المعلّق غير المسوّى.
## 5. الأسواق بلا API (كليك · شام كاش) — تسوية بالرسائل
كليك وشام كاش **لا توفّران API**. الحل المُثبَت في سيرو ويُنقل:
1. تطبيق أندرويد صغير على جهاز مخصّص يلتقط رسائل التأكيد الواردة من المزوّد.
2. يرفعها **خاماً** إلى webhook عندنا (`raw_sms_log`) — تُحفظ كما وصلت، لا تُحلَّل على الجهاز.
3. يُستخرج المبلغ والمرجع والمرسل (**Gemini** كما في `process_with_gemini.php`) ثم تُطابَق مع فاتورة معلّقة وتُسوَّى المحفظة.
**شروط لا تُتنازل عنها**: الرسالة الخام تُحفظ دائماً (أثر للنزاع) · المطابقة **مرة واحدة** بمفتاح تفرّد على مرجع العملية (وإلا سوّت رسالةٌ مكرّرة الفاتورة مرتين) · وما لا يُطابَق آلياً يذهب لطابور مراجعة بشرية لا يُهمَل بصمت.
## 6. ما لا يُنقل من سيرو (مُدقَّق — docs/21)
- **IDOR في `request_payout.php`**: هوية السائق من الطلب لا من التوكن → سحبُ رصيد الغير. (Tripz سليم: من التوكن.)
- **بلا حجز رصيد عند طلب السحب** → طلبات متزامنة تمرّ كلها = صرفٌ مزدوج.
- **رسم 3500 متناقض**: الفحص يشترط `amount + 3500` والتسوية تخصم `amount − 3500` → تسريب مال.
- **`finalizePayout` خمس كتابات بلا معاملة** → فشلٌ في المنتصف يترك سحباً نصف مسوّى.
- **المال في `varchar(10)`**.
- **جدولان متداخلان** لنفس المفهوم (`payout_requests` و`driver_withdrawal_requests`).
## 6.5 حالة التنفيذ (2026-07-18)
**منجَز (P0 · P3 · التوجيه · P5):**
- `tenant_revenue_ledger` و`tenant_pending_ledger` — جدولان مضافان فقط بهجرة `1721930000000`، وفهرس فريد على `(tenant_id, ref)` يمنع التسوية المزدوجة.
- `TenantWalletService`: `creditRevenue` · `creditPending` · `releasePending` · `balance` · `summary` · `revenueByReason`. التكرار يُلتقط من القاعدة لا بالفحص المسبق وحده (نداءان متزامنان يمرّان معاً قبل أن يكتب أيّهما).
- `transactionFeeFor` — مصدر واحد للرقم، و`cappedFee` يمنع صافياً سالباً حين يفوق الرسمُ المبلغَ.
- توجيه الدفع في `markSuccess`: `credit_topup` → إيراد + رصيد السائق · `topup` → أمانة + محفظة الراكب · الرسم → إيراد دائماً.
- **`credit_topup` للسائقين فقط** — الدور من التوكن لا من الجسم. بدونه يرسل راكبٌ `credit_topup` فيُسجَّل مالُه إيراداً محقَّقاً ويُشحن حساب سائق لا يملكه.
- `GET /admin/wallet/summary` و`/revenue-by-reason` لأدمن المستأجر (نطاقه من التوكن).
- `/admin/overview` يفصل `trip_commission` عن `revenue` الكلي.
**الباقي**: P1 (بوابات فعلية) · P2 (تسوية بالرسائل) · P4 (تقارير أوسع) · وسحب المالك أرباحه من `tenant_wallet`.
## 7. أثره على ما هو مبنيّ الآن
`GET /admin/overview` يحسب `revenue` = مجموع عمولة الرحلات فقط. بعد هذا المستند يصير الإيراد الحقيقي = **عمولة الرحلات + شحن السائقين + عمولات عمليات الدفع**، والأمانات تُستثنى صراحةً. النقطة تحتاج تحديثاً ضمن P.