837f0fa63f
Close deployment IDOR and gate stub payment endpoints, add production secret validation, health probes, Redis-backed build progress, GitHub Actions CI, expanded tests, billing/k8s refactors, and ops runbooks. Co-authored-by: Cursor <cursoragent@cursor.com>
150 lines
4.9 KiB
TypeScript
150 lines
4.9 KiB
TypeScript
import {
|
|
Controller,
|
|
Get,
|
|
Post,
|
|
Patch,
|
|
Body,
|
|
Param,
|
|
Query,
|
|
UseGuards,
|
|
Request,
|
|
} from '@nestjs/common';
|
|
import { AuthGuard } from '@nestjs/passport';
|
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
|
import { BillingService } from './billing.service';
|
|
import { BillingOpsService } from './billing-ops.service';
|
|
import { assertStubGatewayAllowed } from './payment-gateway.util';
|
|
import {
|
|
ChargeWalletDto,
|
|
InitiateInvoicePaymentDto,
|
|
VerifyInvoiceGatewayDto,
|
|
UpdateInvoiceStatusDto,
|
|
} from './dto/billing.dto';
|
|
import { RolesGuard } from '../common/guards/roles.guard';
|
|
import { Roles } from '../common/decorators/roles.decorator';
|
|
import { UserRole, InvoiceStatus, PaymentMethod } from '../common/enums';
|
|
|
|
@ApiTags('Billing')
|
|
@ApiBearerAuth()
|
|
@Controller('billing')
|
|
@UseGuards(AuthGuard('jwt'), RolesGuard)
|
|
export class BillingInvoicesController {
|
|
constructor(
|
|
private readonly billingService: BillingService,
|
|
private readonly billingOpsService: BillingOpsService,
|
|
) {}
|
|
|
|
// ─── Invoices ─────────────────────────────────────────────────────
|
|
|
|
@Get('invoices')
|
|
@ApiOperation({ summary: 'List my invoices' })
|
|
async listMyInvoices(
|
|
@Request() req: any,
|
|
@Query('status') status?: InvoiceStatus,
|
|
@Query('applicationId') applicationId?: string,
|
|
@Query('limit') limit?: string,
|
|
) {
|
|
return this.billingService.listInvoices(req.user, {
|
|
status,
|
|
applicationId,
|
|
limit: limit ? parseInt(limit, 10) : undefined,
|
|
});
|
|
}
|
|
|
|
@Get('invoices/:id')
|
|
@ApiOperation({ summary: 'Get one invoice with line items and transactions' })
|
|
async getInvoice(@Request() req: any, @Param('id') id: string) {
|
|
return this.billingService.getInvoiceForUser(id, req.user);
|
|
}
|
|
|
|
@Post('invoices/:id/pay/mixed')
|
|
@ApiOperation({ summary: 'Pay invoice with wallet first, then gateway for the remaining amount' })
|
|
async initiateInvoiceMixed(
|
|
@Request() req: any,
|
|
@Param('id') id: string,
|
|
@Body() dto: InitiateInvoicePaymentDto,
|
|
) {
|
|
const result = await this.billingService.initiateInvoiceMixedPayment(id, req.user, dto.callbackUrl);
|
|
const effect = await this.billingOpsService.completePaidInvoiceEffect(result.invoice);
|
|
return { ...result, effect };
|
|
}
|
|
|
|
@Post('invoices/:id/gateway/verify')
|
|
@ApiOperation({ summary: 'Verify invoice gateway payment' })
|
|
async verifyInvoiceGateway(
|
|
@Request() req: any,
|
|
@Param('id') id: string,
|
|
@Body() dto: VerifyInvoiceGatewayDto,
|
|
) {
|
|
assertStubGatewayAllowed();
|
|
const result = await this.billingService.verifyInvoiceGatewayPayment(
|
|
id,
|
|
req.user,
|
|
dto.trackingCode,
|
|
dto.amount,
|
|
);
|
|
const effect = await this.billingOpsService.completePaidInvoiceEffect(result.invoice);
|
|
return { ...result, effect };
|
|
}
|
|
|
|
// ─── Invoice Admin ────────────────────────────────────────────────
|
|
|
|
@Get('admin/invoices')
|
|
@Roles(UserRole.ADMIN)
|
|
@ApiOperation({ summary: 'List all invoices (Admin)' })
|
|
async listAdminInvoices(
|
|
@Request() req: any,
|
|
@Query('status') status?: InvoiceStatus,
|
|
@Query('userId') userId?: string,
|
|
@Query('applicationId') applicationId?: string,
|
|
@Query('paymentMethod') paymentMethod?: PaymentMethod,
|
|
@Query('search') search?: string,
|
|
@Query('limit') limit?: string,
|
|
) {
|
|
return this.billingService.listInvoices(req.user, {
|
|
status,
|
|
userId,
|
|
applicationId,
|
|
paymentMethod,
|
|
search,
|
|
limit: limit ? parseInt(limit, 10) : undefined,
|
|
});
|
|
}
|
|
|
|
@Get('admin/invoices/:id')
|
|
@Roles(UserRole.ADMIN)
|
|
@ApiOperation({ summary: 'Get invoice details (Admin)' })
|
|
async getAdminInvoice(@Request() req: any, @Param('id') id: string) {
|
|
return this.billingService.getInvoiceForUser(id, req.user);
|
|
}
|
|
|
|
@Patch('admin/invoices/:id/status')
|
|
@Roles(UserRole.ADMIN)
|
|
@ApiOperation({ summary: 'Update invoice status with reason (Admin)' })
|
|
async updateAdminInvoiceStatus(
|
|
@Param('id') id: string,
|
|
@Body() dto: UpdateInvoiceStatusDto,
|
|
) {
|
|
return this.billingService.updateInvoiceStatus(id, dto.status, dto.reason);
|
|
}
|
|
|
|
// ─── Wallet Admin ─────────────────────────────────────────────────
|
|
|
|
@Get('admin/wallets')
|
|
@Roles(UserRole.ADMIN)
|
|
@ApiOperation({ summary: 'List all wallets (Admin)' })
|
|
async getAllWallets() {
|
|
return this.billingService.getAllWallets();
|
|
}
|
|
|
|
@Post('admin/wallets/:userId/charge')
|
|
@Roles(UserRole.ADMIN)
|
|
@ApiOperation({ summary: 'Charge a user\'s wallet (Admin)' })
|
|
async adminChargeWallet(
|
|
@Param('userId') userId: string,
|
|
@Body() dto: ChargeWalletDto,
|
|
) {
|
|
return this.billingService.adminChargeWallet(userId, dto.amount, dto.description);
|
|
}
|
|
}
|