import { Controller, Get, Post, Body, Param, Query, UseGuards, Request, BadRequestException, Inject, forwardRef, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { BillingService } from './billing.service'; import { assertStubGatewayAllowed } from './payment-gateway.util'; import { AppLifecycleService } from '../lifecycle/app-lifecycle.service'; import { ApplicationsService } from '../applications/applications.service'; import { ChargeWalletDto, PayApplicationDto } from './dto/billing.dto'; import { RolesGuard } from '../common/guards/roles.guard'; import { BillingCycle, InvoiceReason } from '../common/enums'; @ApiTags('Billing') @ApiBearerAuth() @Controller('billing') @UseGuards(AuthGuard('jwt'), RolesGuard) export class BillingWalletController { constructor( private readonly billingService: BillingService, @Inject(forwardRef(() => AppLifecycleService)) private readonly lifecycleService: AppLifecycleService, @Inject(forwardRef(() => ApplicationsService)) private readonly applicationsService: ApplicationsService, ) {} // ─── Wallet (User) ─────────────────────────────────────────────── @Get('wallet') @ApiOperation({ summary: 'Get my wallet balance' }) async getBalance(@Request() req: any) { return this.billingService.getBalance(req.user.id); } @Post('wallet/charge') @ApiOperation({ summary: 'Charge my wallet (self top-up)' }) async chargeMyWallet(@Request() req: any, @Body() dto: ChargeWalletDto) { return this.billingService.chargeWallet(req.user.id, dto.amount, dto.description || 'Self top-up'); } @Get('wallet/transactions') @ApiOperation({ summary: 'Get my wallet transactions' }) async getTransactions(@Request() req: any, @Query('limit') limit?: string) { return this.billingService.getTransactions(req.user.id, limit ? parseInt(limit, 10) : 50); } @Get('resource-credits') @ApiOperation({ summary: 'List active prepaid resource credits (from deleted apps)' }) async getResourceCredits(@Request() req: any) { const credits = await this.billingService.getActiveCredits(req.user.id); return credits.map((c) => this.billingService.formatCreditForApi(c)); } @Post('wallet/pay/:applicationId') @ApiOperation({ summary: 'Pay for an application plan from wallet — activates/renews the app' }) async payForApplication( @Request() req: any, @Param('applicationId') applicationId: string, @Body() body: PayApplicationDto, ) { const cycle = body.cycle as BillingCycle; if (!Object.values(BillingCycle).includes(cycle)) { throw new BadRequestException(`Invalid billing cycle: ${body.cycle}`); } const app = await this.applicationsService.findOne(applicationId, req.user.id); const payment = await this.billingService.resolveAppPayment( req.user.id, app, cycle, ); const coupon = await this.billingService.resolveCoupon( req.user.id, body.couponCode, await this.billingService.getAppChargeBreakdown(app), cycle, payment.amountDue, ); let invoice = null; if (payment.amountDue > 0) { invoice = await this.billingService.createInvoice({ userId: req.user.id, applicationId: app.id, reason: InvoiceReason.DEPLOY, lines: [ { label: `Application payment: ${app.name}`, description: `Billing cycle: ${cycle}`, amount: payment.amountDue, metadata: { cycle, waivedAmount: payment.waivedAmount, creditApplied: payment.creditId || null, }, }, ], metadata: { action: 'activate', cycle, }, discount: coupon ?? undefined, }); } let tx = null; if (invoice) { const paid = await this.billingService.payInvoiceWithWallet(invoice.id, req.user); tx = paid.transaction; invoice = paid.invoice; } const activated = await this.lifecycleService.activateApp( applicationId, cycle, ); return { transaction: tx, invoice, creditApplied: payment.creditId || null, waivedAmount: payment.waivedAmount, discountAmount: coupon?.amount ?? 0, discountCode: coupon?.code ?? null, paidAmount: invoice ? Number(invoice.total) : 0, application: { id: activated.id, name: activated.name, lifecycleStatus: activated.lifecycleStatus, planExpiresAt: activated.planExpiresAt, }, message: payment.waivedAmount > 0 ? payment.amountDue > 0 ? `Application "${activated.name}" activated — prepaid credit applied; you paid ${payment.amountDue} Toman for additional services.` : `Application "${activated.name}" activated using your prepaid resource credit (no charge).` : payment.amountDue > 0 ? `Application "${activated.name}" activated until ${activated.planExpiresAt?.toISOString()}` : `Application "${activated.name}" activated`, }; } // ─── Payment Gateway ───────────────────────────────────────────── @Post('gateway/initiate') @ApiOperation({ summary: 'Initiate a payment gateway transaction' }) async initiateGateway( @Request() req: any, @Body() body: { amount: number; description?: string; callbackUrl: string }, ) { assertStubGatewayAllowed(); const trackingCode = `PAY-${Date.now()}-${Math.random().toString(36).substring(2, 8).toUpperCase()}`; return { success: true, trackingCode, gatewayUrl: `${body.callbackUrl}?trackingCode=${trackingCode}&amount=${body.amount}&status=success`, message: 'Redirect user to gatewayUrl to complete payment', }; } @Post('gateway/verify') @ApiOperation({ summary: 'Verify a payment gateway transaction and charge wallet' }) async verifyGateway( @Request() req: any, @Body() body: { trackingCode: string; amount: number }, ) { assertStubGatewayAllowed(); await this.billingService.chargeWallet( req.user.id, body.amount, `Payment gateway: ${body.trackingCode}`, ); return { success: true, message: 'Payment verified and wallet charged', trackingCode: body.trackingCode, }; } }