import { Body, Controller, Get, Ip, Param, Post, Query, UseGuards, } from '@nestjs/common'; import { type CommandBus, type QueryBus } from '@nestjs/cqrs'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam, } from '@nestjs/swagger'; import { Throttle } from '@nestjs/throttler'; import { type PaymentProvider } from '@prisma/client'; import { type JwtPayload, CurrentUser, Roles, JwtAuthGuard, RolesGuard } from '@modules/auth'; import { EndpointRateLimit, EndpointRateLimitGuard } from '@modules/shared'; import { ConfirmBankTransferCommand } from '../../application/commands/confirm-bank-transfer/confirm-bank-transfer.command'; import { type ConfirmBankTransferResult } from '../../application/commands/confirm-bank-transfer/confirm-bank-transfer.handler'; import { CreatePaymentCommand } from '../../application/commands/create-payment/create-payment.command'; import { type CreatePaymentResult } from '../../application/commands/create-payment/create-payment.handler'; import { HandleCallbackCommand } from '../../application/commands/handle-callback/handle-callback.command'; import { type HandleCallbackResult } from '../../application/commands/handle-callback/handle-callback.handler'; import { RefundPaymentCommand } from '../../application/commands/refund-payment/refund-payment.command'; import { type RefundPaymentResult } from '../../application/commands/refund-payment/refund-payment.handler'; import { type PaymentStatusDto } from '../../application/queries/get-payment-status/get-payment-status.handler'; import { GetPaymentStatusQuery } from '../../application/queries/get-payment-status/get-payment-status.query'; import { type TransactionListDto } from '../../application/queries/list-transactions/list-transactions.handler'; import { ListTransactionsQuery } from '../../application/queries/list-transactions/list-transactions.query'; import { type ConfirmBankTransferDto } from '../dto/confirm-bank-transfer.dto'; import { type CreatePaymentDto } from '../dto/create-payment.dto'; import { type ListTransactionsDto } from '../dto/list-transactions.dto'; import { type RefundPaymentDto } from '../dto/refund-payment.dto'; @ApiTags('payments') @Controller('payments') export class PaymentsController { constructor( private readonly commandBus: CommandBus, private readonly queryBus: QueryBus, ) {} @ApiBearerAuth('JWT') @ApiOperation({ summary: 'Create a new payment' }) @ApiResponse({ status: 201, description: 'Payment created successfully' }) @ApiResponse({ status: 400, description: 'Bad request' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @UseGuards(JwtAuthGuard) @Post() async createPayment( @Body() dto: CreatePaymentDto, @CurrentUser() user: JwtPayload, @Ip() ip: string, ): Promise { return this.commandBus.execute( new CreatePaymentCommand( user.sub, dto.provider, dto.type, BigInt(dto.amountVND), dto.description, dto.returnUrl, ip || '127.0.0.1', dto.transactionId, dto.idempotencyKey, ), ); } @ApiOperation({ summary: 'Handle payment provider callback (webhook)' }) @ApiResponse({ status: 201, description: 'Callback processed successfully' }) @ApiParam({ name: 'provider', enum: ['vnpay', 'momo', 'zalopay', 'bank_transfer'] }) @Throttle({ 'payment-callback': { ttl: 60_000, limit: 20 } }) @EndpointRateLimit({ limit: 100, windowSeconds: 60, keyStrategy: 'ip', adminBypass: false }) @UseGuards(EndpointRateLimitGuard) @Post('callback/:provider') async handleCallback( @Param('provider') provider: string, @Body() callbackData: Record, @Query() queryData: Record, ): Promise { const providerUpper = provider.toUpperCase() as PaymentProvider; // Merge query params and body (VNPay sends via query, MoMo/ZaloPay via body) const mergedData = { ...queryData, ...callbackData }; return this.commandBus.execute( new HandleCallbackCommand(providerUpper, mergedData), ); } @ApiBearerAuth('JWT') @ApiOperation({ summary: 'Get payment status by ID' }) @ApiResponse({ status: 200, description: 'Payment status retrieved' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 404, description: 'Payment not found' }) @UseGuards(JwtAuthGuard) @Get(':id') async getPaymentStatus( @Param('id') id: string, @CurrentUser() user: JwtPayload, ): Promise { return this.queryBus.execute(new GetPaymentStatusQuery(id, user.sub)); } @ApiBearerAuth('JWT') @ApiOperation({ summary: 'List transactions for the authenticated user' }) @ApiResponse({ status: 200, description: 'Transactions retrieved' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @UseGuards(JwtAuthGuard) @Get() async listTransactions( @CurrentUser() user: JwtPayload, @Query() dto: ListTransactionsDto, ): Promise { return this.queryBus.execute( new ListTransactionsQuery(user.sub, dto.status, dto.limit, dto.offset), ); } @ApiBearerAuth('JWT') @ApiOperation({ summary: 'Refund a payment (admin only)' }) @ApiResponse({ status: 201, description: 'Refund initiated successfully' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden — admin role required' }) @ApiResponse({ status: 404, description: 'Payment not found' }) @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN') @Post(':id/refund') async refundPayment( @Param('id') id: string, @Body() dto: RefundPaymentDto, @CurrentUser() user: JwtPayload, ): Promise { return this.commandBus.execute( new RefundPaymentCommand(id, dto.reason, user.sub), ); } @ApiBearerAuth('JWT') @ApiOperation({ summary: 'Confirm a bank transfer payment (admin only)' }) @ApiResponse({ status: 201, description: 'Bank transfer confirmed successfully' }) @ApiResponse({ status: 400, description: 'Payment is not a bank transfer or invalid status' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden — admin role required' }) @ApiResponse({ status: 404, description: 'Payment not found' }) @UseGuards(JwtAuthGuard, RolesGuard) @Roles('ADMIN') @Post(':id/confirm-transfer') async confirmBankTransfer( @Param('id') id: string, @Body() dto: ConfirmBankTransferDto, @CurrentUser() user: JwtPayload, ): Promise { return this.commandBus.execute( new ConfirmBankTransferCommand(id, user.sub, dto.bankReference), ); } }