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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4ca932dd1f
commit
b1a060c5ed
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
/**
|
||||
* دفتر مال المستأجر (docs/24). **جدولان منفصلان لا جدول واحد بعمود نوع.**
|
||||
*
|
||||
* السبب ليس أناقة: استعلامُ تقريرٍ أو سحبٍ ينسى شرط النوع يخلط **مال المستأجر
|
||||
* بأمانات الركّاب**، فيسحب المالك من مالٍ ليس له. مع جدولين، الخطأ يصير
|
||||
* مستحيلاً لا مستبعَداً — لا يوجد صفٌّ خطأ ليُقرأ أصلاً.
|
||||
*
|
||||
* الدفتر **مضاف فقط**: كل حركة صفٌّ جديد، والرصيد مجموعٌ مشتق لا حقلٌ يُحدَّث.
|
||||
* (سيرو خزّن المال في `varchar(10)` وجمع نصوصاً — docs/21.)
|
||||
*/
|
||||
export abstract class TenantLedgerBase {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
tenant_id: string;
|
||||
|
||||
/** موجب = دخول، سالب = خروج. الإشارة في المبلغ نفسه لا في عمود اتجاه. */
|
||||
@Column({ type: 'numeric', precision: 14, scale: 3 })
|
||||
amount: number;
|
||||
|
||||
@Column({ default: 'JOD' })
|
||||
currency: string;
|
||||
|
||||
/** سبب الحركة — عليه تُبنى التقارير. راجع `LedgerReason`. */
|
||||
@Column()
|
||||
reason: string;
|
||||
|
||||
/**
|
||||
* مرجع العملية الخارجي (معرّف الدفع · مرجع كليك · رسالة المزوّد).
|
||||
* **مفتاح منع التكرار**: رسالة تأكيد تصل مرتين يجب ألّا تُسوّى مرتين.
|
||||
* فريدٌ لكل مستأجر؛ و`null` مسموح ومتعدّد (postgres لا يوحّد القيم الفارغة).
|
||||
*/
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
ref: string | null;
|
||||
|
||||
@Column({ type: 'jsonb', default: {} })
|
||||
meta: Record<string, any>;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
/** ما نملكه فعلاً: شحن السائقين · عمولة الرحلات · رسوم العمليات. */
|
||||
@Entity('tenant_revenue_ledger')
|
||||
@Index(['tenant_id', 'created_at'])
|
||||
@Index(['tenant_id', 'ref'], { unique: true })
|
||||
export class TenantRevenueEntry extends TenantLedgerBase {}
|
||||
|
||||
/** ما نحتفظ به لغيرنا: شحن الركّاب وأجرة لم تُسوَّ. التزامٌ لا دخل. */
|
||||
@Entity('tenant_pending_ledger')
|
||||
@Index(['tenant_id', 'created_at'])
|
||||
@Index(['tenant_id', 'ref'], { unique: true })
|
||||
export class TenantPendingEntry extends TenantLedgerBase {}
|
||||
|
||||
/** أسباب الحركة — نصوص ثابتة حتى لا تتفرّق التقارير على صياغات مختلفة. */
|
||||
export const LedgerReason = {
|
||||
DRIVER_CREDIT_TOPUP: 'driver_credit_topup', // إيراد: السائق يشحن رصيده التشغيلي
|
||||
TRIP_COMMISSION: 'trip_commission', // إيراد: عمولة الرحلة
|
||||
TRANSACTION_FEE: 'transaction_fee', // إيراد: رسم ثابت لكل عملية دفع
|
||||
RIDER_TOPUP: 'rider_topup', // أمانة: شحن الراكب
|
||||
TRIP_SETTLEMENT: 'trip_settlement', // أمانة: تسوية أجرة للسائق
|
||||
REFUND: 'refund', // أمانة: ردّ للراكب
|
||||
OWNER_WITHDRAWAL: 'owner_withdrawal', // إيراد: سحب المالك أرباحه
|
||||
} as const;
|
||||
|
||||
export type LedgerReasonValue = (typeof LedgerReason)[keyof typeof LedgerReason];
|
||||
Reference in New Issue
Block a user