feat(payments): implement BankTransferService payment gateway with admin confirmation

Add BANK_TRANSFER as a fully supported payment provider:
- BankTransferService implementing IPaymentGateway with HMAC-SHA256 verification
- ConfirmBankTransferCommand/Handler for admin manual payment confirmation
- POST /payments/:id/confirm-transfer admin endpoint (RBAC-protected)
- Atomic status updates with idempotency (PENDING/PROCESSING → COMPLETED)
- Registered in PaymentGatewayFactory alongside VNPAY, MOMO, ZALOPAY
- Comprehensive unit tests for service and handler

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-16 02:34:54 +07:00
parent 18bb6bfe17
commit 89aaa25bb6
11 changed files with 760 additions and 6 deletions

View File

@@ -20,6 +20,8 @@ import { Throttle } from '@nestjs/throttler';
import { PaymentProvider } from '@prisma/client';
import { 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 { ConfirmBankTransferResult } from '../../application/commands/confirm-bank-transfer/confirm-bank-transfer.handler';
import { CreatePaymentCommand } from '../../application/commands/create-payment/create-payment.command';
import { CreatePaymentResult } from '../../application/commands/create-payment/create-payment.handler';
import { HandleCallbackCommand } from '../../application/commands/handle-callback/handle-callback.command';
@@ -30,6 +32,7 @@ import { PaymentStatusDto } from '../../application/queries/get-payment-status/g
import { GetPaymentStatusQuery } from '../../application/queries/get-payment-status/get-payment-status.query';
import { TransactionListDto } from '../../application/queries/list-transactions/list-transactions.handler';
import { ListTransactionsQuery } from '../../application/queries/list-transactions/list-transactions.query';
import { ConfirmBankTransferDto } from '../dto/confirm-bank-transfer.dto';
import { CreatePaymentDto } from '../dto/create-payment.dto';
import { ListTransactionsDto } from '../dto/list-transactions.dto';
import { RefundPaymentDto } from '../dto/refund-payment.dto';
@@ -71,7 +74,7 @@ export class PaymentsController {
@ApiOperation({ summary: 'Handle payment provider callback (webhook)' })
@ApiResponse({ status: 201, description: 'Callback processed successfully' })
@ApiParam({ name: 'provider', enum: ['vnpay', 'momo', 'zalopay'] })
@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)
@@ -136,4 +139,24 @@ export class PaymentsController {
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<ConfirmBankTransferResult> {
return this.commandBus.execute(
new ConfirmBankTransferCommand(id, user.sub, dto.bankReference),
);
}
}