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>
This commit is contained in:
Hamza-Ayed
2026-07-17 14:03:23 +03:00
co-authored by Claude Opus 4.8
parent 2edd8f6916
commit d9b0faab2d
17 changed files with 736 additions and 142 deletions
@@ -0,0 +1,53 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
/**
* أنواع حركات الرصيد التشغيلي (docs/18).
* `topup` و`signup_bonus` و`promo_bonus` مفصولة عمداً: المدفوع فعلاً إيراد،
* والهدايا تكلفة تسويق — خلطها يُفسد المحاسبة.
*/
export type CreditTxnType =
| 'topup' // شحن مدفوع — إيراد
| 'signup_bonus' // مكافأة تسجيل — تكلفة تجنيد
| 'promo_bonus' // حافز باقة («اشحن 50 خذ 55») — تكلفة تسويق
| 'commission' // خصم عمولة رحلة
| 'adjustment'; // تسوية يدوية من الأدمن
/** حركة على الرصيد التشغيلي. الجدول: tripz_credit_txns. دفتر append-only. */
@Entity('credit_txns')
@Index(['tenant_id', 'driver_id', 'created_at'])
export class CreditTxn {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
tenant_id: string;
@Column({ type: 'uuid' })
driver_id: string;
/** موجب = إضافة · سالب = خصم. القيمة الموقَّعة تجعل مجموع الدفتر = الرصيد. */
@Column({ type: 'numeric', precision: 12, scale: 3 })
amount: number;
@Column({ type: 'varchar' })
type: CreditTxnType;
@Column({ type: 'numeric', precision: 12, scale: 3, nullable: true })
balance_after: number | null;
/** الرحلة سبب الخصم — بدونه لا يعرف السائق «لماذا نقص رصيدي؟» (docs/18 §6). */
@Column({ type: 'uuid', nullable: true })
trip_id: string | null;
@Column({ type: 'varchar', nullable: true })
ref: string | null;
@CreateDateColumn()
created_at: Date;
}