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,50 @@
|
||||
import { Tenant } from '../../database/entities/tenant.entity';
|
||||
|
||||
/**
|
||||
* رسم ثابت على كل عملية دفع → إيراد المستأجر (docs/24 §3.3).
|
||||
*
|
||||
* **مصدر واحد للرقم.** في سيرو كُتب رسم السحب (3500) في موضعين: الفحص يشترط
|
||||
* `amount + 3500` والتسوية تخصم `amount − 3500` — فتناقضا وتسرّب المال في كل
|
||||
* عملية (docs/21). لذلك لا يُكتب أي رسم في منطق الأعمال إطلاقاً؛ يُقرأ من هنا.
|
||||
*/
|
||||
|
||||
/** الافتراضات حسب دولة المستأجر. تُتجاوَز من `tenant.settings.payments`. */
|
||||
export const DEFAULT_TRANSACTION_FEE: Record<string, { fee: number; currency: string }> = {
|
||||
sy: { fee: 35, currency: 'SYP' }, // بالليرة الجديدة (بعد حذف صفرين)
|
||||
eg: { fee: 5, currency: 'EGP' },
|
||||
jo: { fee: 0.2, currency: 'JOD' }, // 20 قرشاً
|
||||
};
|
||||
|
||||
export const FALLBACK_FEE = { fee: 0, currency: 'JOD' };
|
||||
|
||||
/**
|
||||
* رسم العملية الفعلي للمستأجر.
|
||||
*
|
||||
* الأولوية: إعداد المستأجر ← افتراض دولته ← صفر. الافتراض الأخير **صفر لا
|
||||
* رقمٌ مخمَّن**: أن نعجز عن معرفة الرسم فنقتطع مبلغاً عشوائياً من مستخدم
|
||||
* حقيقي أسوأ من ألّا نقتطع شيئاً ونكتشف الإعداد الناقص من التقارير.
|
||||
*/
|
||||
export function transactionFeeFor(tenant: Pick<Tenant, 'countryPack' | 'settings'>): {
|
||||
fee: number;
|
||||
currency: string;
|
||||
} {
|
||||
const configured = tenant?.settings?.payments;
|
||||
const byCountry = DEFAULT_TRANSACTION_FEE[tenant?.countryPack ?? ''] ?? FALLBACK_FEE;
|
||||
|
||||
const fee = Number(configured?.transaction_fee);
|
||||
return {
|
||||
// `Number.isFinite` لا `??`: قيمة `0` صالحة تماماً (مستأجر بلا رسوم)،
|
||||
// و`||` كان سيبتلعها ويعيدها إلى افتراض الدولة.
|
||||
fee: Number.isFinite(fee) && fee >= 0 ? fee : byCountry.fee,
|
||||
currency: configured?.currency ?? byCountry.currency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* الرسم لا يتجاوز المبلغ نفسه. بلا هذا، شحنٌ صغير في سوريا (20 ل.س ورسمه 35)
|
||||
* يُنتج صافياً سالباً — أي أن المستخدم يدفع ويخرج برصيد أقل مما بدأ به.
|
||||
*/
|
||||
export function cappedFee(fee: number, amount: number): number {
|
||||
if (!(fee > 0)) return 0;
|
||||
return Math.min(fee, Math.max(0, Number(amount)));
|
||||
}
|
||||
Reference in New Issue
Block a user