feat: implement rewards module with referral/coupon systems, update tariff seeding, and add trip distance service for improved billing accuracy.

This commit is contained in:
Hamza-Ayed
2026-07-19 00:20:37 +03:00
parent 22d18f32fe
commit 92f1eeb48c
39 changed files with 3612 additions and 208 deletions
@@ -44,6 +44,45 @@ export class DriverCreditService {
return this.apply(tenantId, driverId, amount, 'promo_bonus', { ref });
}
/**
* مكافأة إحالة للسائق (المجموعة L) — **مرة واحدة لكل إحالة**.
*
* `ref` هو معرّف الإحالة، ويحرسه فهرس فريد جزئي على القاعدة
* (`IDX_tripz_credit_txns_referral_once`) بنفس منطق J6: الماسح والمسار
* الحدثي قد يصرفان معاً، والقيد وحده هو ما يمنع الصرف مرتين.
*
* يعيد `false` إذا كانت مصروفة سلفاً — فيعرف المنادي ألّا يعلّمها مدفوعة
* بناءً على نجاحٍ لم يحدث.
*/
async grantReferralBonus(
tenantId: string,
driverId: string,
amount: number,
referralId: string,
currency = 'JOD',
): Promise<boolean> {
if (!(amount > 0)) return false;
// حارسان مثل J6: فحص تطبيقي للحالة الشائعة (الماسح يعيد المحاولة)،
// وفهرس فريد جزئي على القاعدة للسباق الحقيقي. الفحص وحده يسقط تحت
// التزامن، والفهرس وحده يجعل الحالة الشائعة استثناءً — فكلاهما.
const already = await this.txns.findOne({
where: { tenant_id: tenantId, type: 'referral_bonus', ref: referralId },
});
if (already) return false;
try {
await this.apply(tenantId, driverId, amount, 'referral_bonus', { ref: referralId }, currency);
return true;
} catch (e: any) {
if (e?.code === '23505' || e?.driverError?.code === '23505') {
this.logger.debug(`referral bonus already granted for ${referralId}`);
return false;
}
throw e;
}
}
/**
* خصم عمولة رحلة (docs/18 §5.2).
* **لا يفشل أبداً** — ولو صار الرصيد سالباً. الرحلة تمّت فالعمولة استُحقّت؛
@@ -15,6 +15,7 @@ export type CreditTxnType =
| 'topup' // شحن مدفوع — إيراد
| 'signup_bonus' // مكافأة تسجيل — تكلفة تجنيد
| 'promo_bonus' // حافز باقة («اشحن 50 خذ 55») — تكلفة تسويق
| 'referral_bonus'// مكافأة إحالة — تكلفة تجنيد، منفصلة عن حافز الباقة
| 'commission' // خصم عمولة رحلة
| 'adjustment'; // تسوية يدوية من الأدمن