feat: implement billing module for tenant subscription management and super-admin payment processing

This commit is contained in:
Hamza-Ayed
2026-07-18 17:26:16 +03:00
parent d4ac38ca87
commit d239f51d5d
17 changed files with 593 additions and 67 deletions
@@ -1,4 +1,4 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, Post, 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';
@@ -39,4 +39,35 @@ export class TenantWalletController {
currency || 'JOD',
);
}
/** كشف حساب تفصيلي للدفتر. book يمكن أن يكون 'revenue' أو 'pending' */
@Get('statement')
statement(
@CurrentUser() user: AuthUser,
@Query('book') book: 'revenue' | 'pending',
@Query('currency') currency?: string,
@Query('limit') limit?: string,
@Query('offset') offset?: string,
) {
return this.wallet.statement(
book || 'revenue',
user.tenantId,
currency || 'JOD',
limit ? parseInt(limit, 10) : 100,
offset ? parseInt(offset, 10) : 0,
);
}
/** سحب أرباح المستأجر (من دفتر الإيرادات حصراً) */
@Post('withdraw')
withdraw(
@CurrentUser() user: AuthUser,
@Body() body: { amount: number; currency?: string },
) {
return this.wallet.withdraw(
user.tenantId,
Number(body.amount),
body.currency || 'JOD',
);
}
}
@@ -142,6 +142,43 @@ export class TenantWalletService {
}));
}
/**
* كشف الحساب التفصيلي للدفتر.
*/
async statement(book: Book, tenantId: string, currency = 'JOD', limit = 100, offset = 0) {
const repo = this.repoFor(book);
const [rows, total] = await repo.findAndCount({
where: { tenant_id: tenantId, currency } as any,
order: { created_at: 'DESC' },
take: limit,
skip: offset,
});
return { data: rows, total, limit, offset };
}
/**
* سحب أرباح المستأجر (من دفتر الإيرادات حصراً).
*/
async withdraw(tenantId: string, amount: number, currency = 'JOD') {
if (amount <= 0) throw new BadRequestException('Amount must be positive');
const currentBalance = await this.balance('revenue', tenantId, currency);
if (currentBalance < amount) {
throw new BadRequestException('Insufficient revenue balance');
}
const ref = `PAYOUT-${Date.now()}`;
const entry = await this.post('revenue', {
tenantId,
amount: -amount, // سالب = سحب
currency,
reason: 'payout', // Should exist or fall under general
ref,
});
return { success: true, ref, withdrawn: amount, remaining: currentBalance - amount };
}
/** اختصارات مقروءة عند نقطة الاستدعاء — تمنع تمرير الدفتر الخطأ سهواً. */
creditRevenue(e: PostEntry, em?: EntityManager) {
return this.post('revenue', e, em);