feat: المجموعة J — الرصيد التشغيلي + تصحيح نموذج العمولة (B3/B6)
فلسفة العمولة الصحيحة (docs/18): السائق يقبض أجرة الراكب **كاملة**،
والعمولة تُخصم من رصيد تشغيلي مدفوع سلفاً. ما بُني في f3fdc1b كان نموذج
أوبر (اقتطاع من الأرباح) — عكس المطلوب.
- J1: driver_credits + credit_txns. خصم ذرّي كنمط I1 لكن **بلا شرط رصيد**
ولا قيد >= 0: الدين مسموح عمداً (قرار المالك: الرحلة لا تُقطع أبداً)
- J2: خصم العمولة عند completed لا paid — الرحلة تمّت فالعمولة استُحقّت.
قاعدة واحدة للكاش والمحفظة (§5.5): لا تفريع حسب وسيلة الدفع
- J3: price_for_driver = price_for_passenger. TariffEngine.split →
commission، وبلا سقف بالأجرة لأن الخصم على رصيد منفصل يجوز أن يسلب
- J4: الحجب عند تجاوز credit_floor فقط، عند setOnline و accept — لا أثناء
رحلة. الحجب يمنع الاتصال أصلاً وإلا عُرضت رحلات يُرفض قبولها
- J6: مكافأة التسجيل عند الاعتماد (3 JOD / 300 SYP جديدة / 300 EGP).
حارسان: فحص تطبيقي للحالة الشائعة + فهرس فريد جزئي للسباق
- J7: credit_floor لكل عملة
- J8: GET /credit + /credit/transactions
تصحيح B3: مشوار الوصول تعويضُ إلغاء بعد انقضاء الانتظار المجاني — لا بند
في كل أجرة. أُزيل مفتاح pickup_leg.charge.
هجرة: DriverCredit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2edd8f6916
commit
d9b0faab2d
@@ -1,17 +1,21 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Driver } from './entities/driver.entity';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { DriverLocationService } from '../locations/driver-location.service';
|
||||
import { DriverCreditService } from '../credit/driver-credit.service';
|
||||
|
||||
@Injectable()
|
||||
export class DriversService {
|
||||
private readonly logger = new Logger('Drivers');
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Driver)
|
||||
private readonly repo: Repository<Driver>,
|
||||
private readonly users: UsersService,
|
||||
private readonly locations: DriverLocationService,
|
||||
private readonly credit: DriverCreditService,
|
||||
) {}
|
||||
|
||||
findByUser(tenantId: string, userId: string): Promise<Driver | null> {
|
||||
@@ -59,7 +63,13 @@ export class DriversService {
|
||||
const driver = await this.findById(tenantId, driverId);
|
||||
if (!driver) throw new NotFoundException('Driver not found');
|
||||
driver.verification_status = 'approved';
|
||||
return this.repo.save(driver);
|
||||
const saved = await this.repo.save(driver);
|
||||
// مكافأة التسجيل عند الاعتماد لا عند التقديم — حتى لا يحصدها من رُفض
|
||||
// (docs/18 — J6). آمنة للتكرار: القاعدة تضمن مرة واحدة لكل سائق.
|
||||
await this.credit.grantSignupBonus(tenantId, driver.id).catch((e: any) =>
|
||||
this.logger.warn(`signup bonus failed for ${driver.id}: ${e?.message}`),
|
||||
);
|
||||
return saved;
|
||||
}
|
||||
|
||||
/** يبدّل حالة الاتصال؛ عند الاتصال يدخل فهرس المتاحين، وعند الفصل يُزال. */
|
||||
@@ -70,6 +80,11 @@ export class DriversService {
|
||||
): Promise<Driver> {
|
||||
const driver = await this.findByUser(tenantId, userId);
|
||||
if (!driver) throw new NotFoundException('Driver profile not found');
|
||||
// الدين تجاوز الأرضية → لا يتصل أصلاً؛ وإلا عُرضت عليه رحلات يُرفض قبولها
|
||||
// فيبدو النظام معطوباً بدل أن يفهم أن عليه الشحن (docs/18 — J4).
|
||||
if (online && (await this.credit.isBlocked(tenantId, driver.id))) {
|
||||
throw new ForbiddenException('credit_exhausted');
|
||||
}
|
||||
driver.is_online = online;
|
||||
await this.repo.save(driver);
|
||||
await this.locations.setAvailability(
|
||||
|
||||
Reference in New Issue
Block a user