Files
tripz-llc/docs/24-tenant-wallet-revenue.md
T
Hamza-AyedandClaude Fable 5 da035e46a4 feat: P2 — التسوية بالرسائل + إزالة مارتن + مستند البنية (docs/25)
P2 (كليك/شام كاش بلا API):
- الرسالة تُحفظ خاماً **قبل** أي تحليل: التحليل قد يفشل فنحتاج الأصل لإعادة
  المعالجة، وعند النزاع يكون النصّ الأصلي هو الحجّة لا تفسيرُنا له.
- النقطة تصنع المال، فرسالة مزوّرة = رصيد من عدم. الحماية: سرّ لكل مستأجر
  بمقارنة ثابتة الزمن (المقارنة النصّية تسرّب السرّ حرفاً حرفاً زمنياً)،
  وبصمة محتوى فريدة تمنع احتساب إعادة الإرسال مرتين.
- Gemini بحرارة صفر ومطالَب بإرجاع null عند عدم اليقين: نموذج يخمّن مبلغاً
  يسوّي فاتورة بمال لم يصل. بلا مبلغ صريح → مراجعة بشرية لا تسوية.
- المطابقة بالمرجع أولاً، ثم بالمبلغ خلال 24 ساعة وبشرط فاتورة وحيدة —
  فاتورتان بنفس المبلغ التباسٌ يُراجَع، لا تسويةٌ عشوائية لإحداهما.
- التسوية تمرّ بـmarkSuccess نفسه فلا يتفرّع مسار مالي ثانٍ.

إزالة حاوية martin: انطلق منصّة قائمة بذاتها لها خوادمها؛ دورنا طلب وردّ لا
استضافة خرائط (قرار المالك).

docs/25: جرد الحاويات · لماذا الدفع ليس خدمة منفصلة · سيرفر السوبر-أدمن
المنفصل ونطاقه الفرعي · WebSocket مقابل الاستطلاع بالأرقام · أحجام السيرفرات
على أساس الذروة لا المعدّل · نقل مستأجر · ترتيب التوسّع.

13 اختباراً جديداً (201 إجمالاً، كلها خضراء).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 15:19:17 +03:00

9.9 KiB
Raw Blame History

24 — محفظة المستأجر وإيرادات المنصة (فلسفة المال)

قرارات المالك 2026-07-18. هذا المستند يحكم كل ما يتعلّق بحركة المال، ويسبق أي كود في المجموعة P. ذو صلة: 18 (الرصيد والعمولة) · 22 §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 الكلي.

منجَز أيضاً (P2 — التسوية بالرسائل):

  • tripz_pay_raw_sms: الرسالة تُحفظ خاماً وقبل أي تحليل، وفهرس فريد على بصمة المحتوى يمنع معالجة إعادة الإرسال مرتين.
  • POST /payments/sms/webhook/:tenantSlug — بلا JWT (الرافع جهاز)، محميّ بسرّ لكل مستأجر (settings.payments.sms_webhook_secret) بمقارنة ثابتة الزمن؛ ومستأجر بلا سرّ = النقطة مغلقة لا مفتوحة.
  • GeminiService.extractTransferSms بحرارة صفر وتعليمات صريحة بإرجاع null عند عدم اليقين — النموذج الذي يخمّن مبلغاً يسوّي فاتورة بمال لم يصل.
  • المطابقة: المرجع أولاً، ثم المبلغ ضمن 24 ساعة وبشرط فاتورة وحيدة؛ فاتورتان بنفس المبلغ = التباس يُراجَع بشرياً.
  • التسوية تمرّ بـsettleFromSms → markSuccess نفسه، فلا مسار مالي ثانٍ يتفرّع ويتناقض.
  • GET /payments/sms/review + POST /payments/sms/review/:id/match لطابور المراجعة والربط اليدوي.
  • 13 اختباراً تغطّي التزوير والتكرار والتخمين والالتباس.

الباقي: P1 (بوابات فعلية) · P4 (تقارير أوسع) · وسحب المالك أرباحه من tenant_wallet.

7. أثره على ما هو مبنيّ الآن

GET /admin/overview يحسب revenue = مجموع عمولة الرحلات فقط. بعد هذا المستند يصير الإيراد الحقيقي = عمولة الرحلات + شحن السائقين + عمولات عمليات الدفع، والأمانات تُستثنى صراحةً. النقطة تحتاج تحديثاً ضمن P.