From 5af26445fa924cfe09bdd451a8c248d6f6587962 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Wed, 3 Dec 2025 12:56:10 +0330 Subject: [PATCH] verify payment --- .../controllers/payments.controller.ts | 13 ++ .../payments/dto/verify-payment.dto.ts | 17 +++ src/modules/payments/interface/payment.ts | 16 +++ .../payments/services/payments.service.ts | 127 +++++++++++++++++- 4 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 src/modules/payments/dto/verify-payment.dto.ts diff --git a/src/modules/payments/controllers/payments.controller.ts b/src/modules/payments/controllers/payments.controller.ts index 3ca8f94..5899fb8 100644 --- a/src/modules/payments/controllers/payments.controller.ts +++ b/src/modules/payments/controllers/payments.controller.ts @@ -5,6 +5,7 @@ import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; import { PaymentMethodService } from '../services/payment-method.service'; import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto'; import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto'; +import { VerifyPaymentDto } from '../dto/verify-payment.dto'; import { RestId } from 'src/common/decorators'; import { AuthGuard } from 'src/modules/auth/guards/auth.guard'; @@ -25,6 +26,18 @@ export class PaymentsController { return this.paymentMethodService.findByRestaurant(restId); } + @Post('public/payments/verify') + @ApiOperation({ summary: 'Verify a payment' }) + @ApiBody({ type: VerifyPaymentDto }) + @ApiNotFoundResponse({ description: 'Payment not found' }) + verifyPayment(@Body() verifyPaymentDto: VerifyPaymentDto) { + return this.paymentsService.verifyPayment( + verifyPaymentDto.authority, + verifyPaymentDto.amount, + verifyPaymentDto.orderId, + ); + } + // @UseGuards(AdminAuthGuard) // @ApiBearerAuth() // @Get('admin/payments') diff --git a/src/modules/payments/dto/verify-payment.dto.ts b/src/modules/payments/dto/verify-payment.dto.ts new file mode 100644 index 0000000..d350b4a --- /dev/null +++ b/src/modules/payments/dto/verify-payment.dto.ts @@ -0,0 +1,17 @@ +import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class VerifyPaymentDto { + @ApiProperty({ description: 'Payment authority token' }) + @IsString() + authority: string; + + @ApiPropertyOptional({ description: 'Payment amount (optional, will use payment amount if not provided)' }) + @IsOptional() + @IsNumber() + amount?: number; + + @ApiProperty({ description: 'Payment order id' }) + @IsString() + orderId: string; +} diff --git a/src/modules/payments/interface/payment.ts b/src/modules/payments/interface/payment.ts index 26d5ceb..40be253 100644 --- a/src/modules/payments/interface/payment.ts +++ b/src/modules/payments/interface/payment.ts @@ -30,3 +30,19 @@ export interface IPaymentResponse { fee_type: string; fee: number; } + +export interface IPaymentVerifyRequest { + merchantId: string; + amount: number; + authority: string; +} + +export interface IPaymentVerifyResponse { + code: number; + message: string; + refId: number; + cardPan: string; + cardHash: string; + fee_type: string; + fee: number; +} diff --git a/src/modules/payments/services/payments.service.ts b/src/modules/payments/services/payments.service.ts index eebace3..7e9f399 100644 --- a/src/modules/payments/services/payments.service.ts +++ b/src/modules/payments/services/payments.service.ts @@ -3,6 +3,8 @@ import axios from 'axios'; import { IPaymentRequest, IPaymentResponse, + IPaymentVerifyRequest, + IPaymentVerifyResponse, PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum, @@ -45,7 +47,7 @@ export class PaymentsService { // Create payment record and save authority const restaurantDomain = paymentMethod.restaurant.domain; - const gateway = paymentMethod.gateway as PaymentGatewayEnum | null; + const gateway = paymentMethod.gateway; const { authority } = await this.createPayment(restaurantDomain, { amount, orderId, @@ -98,10 +100,9 @@ export class PaymentsService { throw new BadRequestException('Payment gateway is required for online payments'); } - const callbackUrl = `${domain}/payments/callback`; - // Handle ZarinPal gateway if (gateway === PaymentGatewayEnum.ZarinPal) { + const callbackUrl = `${domain}/payments/${PaymentGatewayEnum.ZarinPal}/${orderId}/callback`; try { const gatewayResponse = await this.requestToZarinPalGateway({ amount, @@ -148,6 +149,126 @@ export class PaymentsService { } return null; } + + async verifyPayment(authority: string, amount: number | undefined, orderId: string): Promise { + // Find payment by authority and orderId + const payment = await this.em.findOne( + Payment, + { authority, order: { id: orderId } }, + { populate: ['order', 'order.paymentMethod'] }, + ); + if (!payment) { + throw new NotFoundException('Payment not found'); + } + + // If payment is already verified, return it + if (payment.status === PaymentStatusEnum.Paid) { + return payment; + } + + // Get payment method from order (already populated) + if (!payment.order.paymentMethod) { + throw new NotFoundException('Payment method not found for this order'); + } + + const paymentMethod = await this.em.findOne(PaymentMethod, { id: payment.order.paymentMethod.id }); + if (!paymentMethod) { + throw new NotFoundException('Payment method not found'); + } + + // For non-online payments, mark as paid directly + if (paymentMethod.method !== PaymentMethodEnum.Online) { + payment.status = PaymentStatusEnum.Paid; + payment.paidAt = new Date(); + await this.em.persistAndFlush(payment); + return payment; + } + + // Online payments require gateway verification + if (!paymentMethod.merchantId) { + throw new BadRequestException('Merchant ID is required for online payment verification'); + } + + if (!payment.gateway) { + throw new BadRequestException('Payment gateway is required for online payment verification'); + } + + // Use provided amount or payment amount + const verifyAmount = amount || payment.amount; + + // Handle ZarinPal gateway verification + if (payment.gateway === PaymentGatewayEnum.ZarinPal) { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const verifyResponse = await this.verifyZarinPalPayment({ + merchantId: paymentMethod.merchantId, + amount: verifyAmount, + authority, + }); + + // Type guard to ensure we have a valid response + if (!verifyResponse || typeof verifyResponse !== 'object') { + throw new BadRequestException('Invalid verification response from payment gateway'); + } + + // Type assertion after type guard + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const response: IPaymentVerifyResponse = verifyResponse as IPaymentVerifyResponse; + + // Check verification response code (100 means success for ZarinPal) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (response.code === 100) { + // Payment successful + payment.status = PaymentStatusEnum.Paid; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + payment.refId = String(response.refId); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + payment.cardPan = response.cardPan; + payment.verifyResponse = response as unknown as Record; + payment.paidAt = new Date(); + payment.order.paymentStatus = PaymentStatusEnum.Paid; + } else { + // Payment failed + payment.status = PaymentStatusEnum.Failed; + payment.verifyResponse = response as unknown as Record; + payment.failedAt = new Date(); + payment.order.paymentStatus = PaymentStatusEnum.Failed; + } + + await this.em.persistAndFlush(payment); + return payment; + } catch (error: unknown) { + // Mark payment as failed on error + payment.status = PaymentStatusEnum.Failed; + payment.failedAt = new Date(); + payment.order.paymentStatus = PaymentStatusEnum.Failed; + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + payment.verifyResponse = { + error: errorMessage, + }; + await this.em.persistAndFlush(payment); + + throw new BadRequestException(`Payment verification failed: ${errorMessage}`); + } + } + + // If gateway is not supported, throw an error + throw new BadRequestException(`Unsupported payment gateway: ${String(payment.gateway)}`); + } + + private async verifyZarinPalPayment(verifyRequest: IPaymentVerifyRequest): Promise { + const gatewayVerifyUrl = 'https://payment.zarinpal.com/pg/v4/payment/verify.json'; + try { + const response = await axios.post(gatewayVerifyUrl, verifyRequest); + if (!response.data) { + throw new BadRequestException('Empty response from payment gateway'); + } + return response.data; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new BadRequestException(`Failed to verify payment with gateway: ${errorMessage}`); + } + } // eslint-disable-next-line @typescript-eslint/no-unused-vars update(_id: number, _updatePaymentDto: unknown) { return `This action updates a #${_id} payment`;