الأساس المالي حسب 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>
43 lines
1.7 KiB
TypeScript
43 lines
1.7 KiB
TypeScript
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
|
import { TenantWalletService } from './tenant-wallet.service';
|
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
|
import { RolesGuard } from '../auth/guards/roles.guard';
|
|
import { Roles } from '../auth/decorators/roles.decorator';
|
|
import { CurrentUser, AuthUser } from '../auth/decorators/current-user.decorator';
|
|
|
|
/**
|
|
* محفظة المستأجر — يقرأها **أدمن المستأجر** لا السوبر-أدمن (docs/24).
|
|
* النطاق من التوكن دائماً: `user.tenantId`، فلا يمكن لأدمن مستأجر قراءة
|
|
* أرقام مستأجر آخر بتمرير معرّف في الطلب.
|
|
*/
|
|
@ApiTags('tenant-wallet')
|
|
@ApiBearerAuth()
|
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
@Roles('admin')
|
|
@Controller('admin/wallet')
|
|
export class TenantWalletController {
|
|
constructor(private readonly wallet: TenantWalletService) {}
|
|
|
|
/** الأرباح القابلة للسحب مقابل الأمانات المحتفَظ بها. */
|
|
@Get('summary')
|
|
summary(@CurrentUser() user: AuthUser, @Query('currency') currency?: string) {
|
|
return this.wallet.summary(user.tenantId, currency || 'JOD');
|
|
}
|
|
|
|
/** من أين جاء الإيراد: شحن سائقين · عمولات · رسوم. */
|
|
@Get('revenue-by-reason')
|
|
byReason(
|
|
@CurrentUser() user: AuthUser,
|
|
@Query('days') days?: string,
|
|
@Query('currency') currency?: string,
|
|
) {
|
|
const n = Number(days);
|
|
return this.wallet.revenueByReason(
|
|
user.tenantId,
|
|
Number.isFinite(n) && n > 0 ? Math.min(n, 365) : 30,
|
|
currency || 'JOD',
|
|
);
|
|
}
|
|
}
|