الأساس المالي حسب 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>
53 lines
2.3 KiB
TypeScript
53 lines
2.3 KiB
TypeScript
import { transactionFeeFor, cappedFee, DEFAULT_TRANSACTION_FEE } from './transaction-fee';
|
|
|
|
const tenant = (countryPack: string, settings: any = {}) =>
|
|
({ countryPack, settings }) as any;
|
|
|
|
describe('transactionFeeFor — رسم العملية (docs/24 §3.3)', () => {
|
|
it('يطبّق افتراض الدولة حين لا إعداد', () => {
|
|
expect(transactionFeeFor(tenant('sy'))).toEqual({ fee: 35, currency: 'SYP' });
|
|
expect(transactionFeeFor(tenant('eg'))).toEqual({ fee: 5, currency: 'EGP' });
|
|
expect(transactionFeeFor(tenant('jo'))).toEqual({ fee: 0.2, currency: 'JOD' });
|
|
});
|
|
|
|
it('إعداد المستأجر يتقدّم على افتراض دولته', () => {
|
|
const t = tenant('sy', { payments: { transaction_fee: 50 } });
|
|
expect(transactionFeeFor(t).fee).toBe(50);
|
|
});
|
|
|
|
it('رسم صفر إعدادٌ صالح — لا يعود إلى افتراض الدولة', () => {
|
|
// الفخّ: `configured || byCountry` كان سيبتلع الصفر ويقتطع 35 من مستأجر
|
|
// اتّفقنا معه على ألّا رسوم عليه.
|
|
const t = tenant('sy', { payments: { transaction_fee: 0 } });
|
|
expect(transactionFeeFor(t).fee).toBe(0);
|
|
});
|
|
|
|
it('يتجاهل الإعداد غير الصالح ويعود للافتراض', () => {
|
|
expect(transactionFeeFor(tenant('jo', { payments: { transaction_fee: 'كثير' } })).fee).toBe(0.2);
|
|
expect(transactionFeeFor(tenant('jo', { payments: { transaction_fee: -5 } })).fee).toBe(0.2);
|
|
});
|
|
|
|
it('دولة غير معروفة = بلا رسم، لا رقم مخمَّن', () => {
|
|
expect(transactionFeeFor(tenant('xx')).fee).toBe(0);
|
|
});
|
|
|
|
it('الرقم مصدره واحد — لا يُكتب في منطق الأعمال (درس سيرو)', () => {
|
|
expect(DEFAULT_TRANSACTION_FEE.sy.fee).toBe(35);
|
|
});
|
|
});
|
|
|
|
describe('cappedFee — الرسم لا يبتلع المبلغ', () => {
|
|
it('يقصّ الرسم عند مبلغ أصغر منه', () => {
|
|
// شحن 20 ل.س ورسمه 35: بلا القصّ يخرج المستخدم بصافٍ سالب.
|
|
expect(cappedFee(35, 20)).toBe(20);
|
|
});
|
|
|
|
it('يمرّ الرسم كاملاً حين المبلغ أكبر', () => {
|
|
expect(cappedFee(35, 1000)).toBe(35);
|
|
});
|
|
|
|
it('بلا رسم يعيد صفراً', () => {
|
|
expect(cappedFee(0, 100)).toBe(0);
|
|
});
|
|
});
|