الأساس المالي حسب 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>
51 lines
2.5 KiB
TypeScript
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)));
|
|
}
|