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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4ca932dd1f
commit
b1a060c5ed
@@ -2,12 +2,14 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Tenant } from '../../database/entities/tenant.entity';
|
||||
import { Trip } from '../trips/entities/trip.entity';
|
||||
import { TenantRevenueEntry } from '../tenant-wallet/entities/tenant-ledger.entity';
|
||||
import { TenantsService } from './tenants.service';
|
||||
import { TenantsController } from './tenants.controller';
|
||||
|
||||
@Module({
|
||||
// Trip هنا للقراءة فقط — تقرير GMV للسوبر-أدمن (docs/22 — N1).
|
||||
imports: [TypeOrmModule.forFeature([Tenant, Trip])],
|
||||
// Trip ودفتر الإيراد هنا **للقراءة فقط** — تقرير GMV والإيراد للسوبر-أدمن
|
||||
// (docs/22 — N1، docs/24 — P5). الكتابة في الدفتر تخصّ TenantWalletService وحده.
|
||||
imports: [TypeOrmModule.forFeature([Tenant, Trip, TenantRevenueEntry])],
|
||||
controllers: [TenantsController],
|
||||
providers: [TenantsService],
|
||||
exports: [TenantsService],
|
||||
|
||||
@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Tenant, TenantPlan } from '../../database/entities/tenant.entity';
|
||||
import { Trip } from '../trips/entities/trip.entity';
|
||||
import { TenantRevenueEntry } from '../tenant-wallet/entities/tenant-ledger.entity';
|
||||
import { CacheService, CacheKeys, TTL } from '../../common/cache/cache.service';
|
||||
import { EntitlementsService } from '../../common/entitlements/entitlements.service';
|
||||
import { defaultPaymentMethods, validPaymentMethods } from '../../common/entitlements/payment-methods';
|
||||
@@ -25,6 +26,8 @@ export class TenantsService {
|
||||
private readonly repo: Repository<Tenant>,
|
||||
@InjectRepository(Trip)
|
||||
private readonly trips: Repository<Trip>,
|
||||
@InjectRepository(TenantRevenueEntry)
|
||||
private readonly revenueLedger: Repository<TenantRevenueEntry>,
|
||||
private readonly cache: CacheService,
|
||||
private readonly entitlements: EntitlementsService,
|
||||
) {}
|
||||
@@ -191,8 +194,10 @@ export class TenantsService {
|
||||
* المستأجرين يصير الثاني N+1 يُبطئ اللوحة الأهم عندنا.
|
||||
*
|
||||
* `gmv` = مجموع الأجرة النهائية للرحلات المكتملة/المدفوعة (حجم الأعمال).
|
||||
* `revenue` = مجموع العمولة — **إيراد المنصة الفعلي**، وهو الرقم الذي يهم
|
||||
* عند التسعير لا الـGMV.
|
||||
* `trip_commission` = عمولة الرحلات وحدها.
|
||||
* `revenue` = **الإيراد الحقيقي** من دفتر الإيراد (docs/24 — P5): عمولة
|
||||
* الرحلات + شحن السائقين + رسوم العمليات. الأمانات مستثناة بحكم كونها في
|
||||
* دفتر آخر — لا شرط ينساه أحد.
|
||||
*/
|
||||
async platformOverview(days = 30) {
|
||||
const since = new Date(Date.now() - days * 86400_000);
|
||||
@@ -212,7 +217,7 @@ export class TenantsService {
|
||||
)
|
||||
.addSelect(
|
||||
"COALESCE(SUM(t.commission_amount) FILTER (WHERE t.status IN ('completed','paid')), 0)",
|
||||
'revenue',
|
||||
'trip_commission',
|
||||
)
|
||||
// `requested_at` لا `created_at`: كيان الرحلة يسمّي طابع الإنشاء بوقت
|
||||
// **طلب** الرحلة (docs/17 — B7). الاسم الخاطئ كان يُسقط النقطة بـ500
|
||||
@@ -221,6 +226,17 @@ export class TenantsService {
|
||||
.groupBy('t.tenant_id')
|
||||
.getRawMany();
|
||||
|
||||
// الإيراد الحقيقي من دفتر الإيراد لا من الرحلات (docs/24 — P5): يشمل شحن
|
||||
// السائقين ورسوم العمليات، وهي غالباً أكبر من عمولة الرحلات.
|
||||
const revenueRows = await this.revenueLedger
|
||||
.createQueryBuilder('l')
|
||||
.select('l.tenant_id', 'tenant_id')
|
||||
.addSelect('COALESCE(SUM(l.amount), 0)', 'revenue')
|
||||
.where('l.created_at >= :since', { since })
|
||||
.groupBy('l.tenant_id')
|
||||
.getRawMany();
|
||||
const revenueBy = new Map(revenueRows.map((r: any) => [r.tenant_id, Number(r.revenue ?? 0)]));
|
||||
|
||||
const byTenant = new Map(rows.map((r: any) => [r.tenant_id, r]));
|
||||
const perTenant = tenants.map((t) => {
|
||||
const r: any = byTenant.get(t.id) ?? {};
|
||||
@@ -237,11 +253,12 @@ export class TenantsService {
|
||||
trips: Number(r.trips ?? 0),
|
||||
completed_trips: Number(r.completed_trips ?? 0),
|
||||
gmv: Number(r.gmv ?? 0),
|
||||
revenue: Number(r.revenue ?? 0),
|
||||
trip_commission: Number(r.trip_commission ?? 0),
|
||||
revenue: revenueBy.get(t.id) ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
const sum = (k: 'trips' | 'completed_trips' | 'gmv' | 'revenue') =>
|
||||
const sum = (k: 'trips' | 'completed_trips' | 'gmv' | 'trip_commission' | 'revenue') =>
|
||||
perTenant.reduce((a, t) => a + t[k], 0);
|
||||
|
||||
return {
|
||||
@@ -252,6 +269,7 @@ export class TenantsService {
|
||||
trips: sum('trips'),
|
||||
completed_trips: sum('completed_trips'),
|
||||
gmv: sum('gmv'),
|
||||
trip_commission: sum('trip_commission'),
|
||||
revenue: sum('revenue'),
|
||||
},
|
||||
tenants: perTenant.sort((a, b) => b.gmv - a.gmv),
|
||||
|
||||
Reference in New Issue
Block a user