Files
goodgo-platform/apps/api/src/modules/payments/presentation/controllers/payments.controller.ts
Ho Ngoc Hai d4e100a00c feat(api): add price history, Stringee SMS, Zalo OA, WebSocket notifications, and feature-listing command
- Add PriceHistory model + migration, price-changed domain event, and event handler
- Add GetPriceHistory query handler and controller endpoint
- Implement StringeeSmsService and ZaloOaService with unit tests
- Add Zalo ZNS templates for Vietnamese notification messages
- Add WebSocket notification gateway for real-time push
- Add FeatureListingCommand for promoted listings
- Apply remaining consistent-type-imports lint fixes across API modules

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-16 05:15:04 +07:00

163 lines
6.6 KiB
TypeScript

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<CreatePaymentResult> {
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<string, string>,
@Query() queryData: Record<string, string>,
): Promise<HandleCallbackResult> {
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<PaymentStatusDto> {
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<TransactionListDto> {
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<RefundPaymentResult> {
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<ConfirmBankTransferResult> {
return this.commandBus.execute(
new ConfirmBankTransferCommand(id, user.sub, dto.bankReference),
);
}
}