Files
tripz-llc/backend/src/modules/tenant-wallet/transaction-fee.ts
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

51 lines
2.5 KiB
TypeScript

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)));
}