verify payment

This commit is contained in:
2025-12-03 12:56:10 +03:30
parent 8a2390ea8e
commit 5af26445fa
4 changed files with 170 additions and 3 deletions
@@ -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')
@@ -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;
}
+16
View File
@@ -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;
}
@@ -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<Payment> {
// 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<string, any>;
payment.paidAt = new Date();
payment.order.paymentStatus = PaymentStatusEnum.Paid;
} else {
// Payment failed
payment.status = PaymentStatusEnum.Failed;
payment.verifyResponse = response as unknown as Record<string, any>;
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<IPaymentVerifyResponse> {
const gatewayVerifyUrl = 'https://payment.zarinpal.com/pg/v4/payment/verify.json';
try {
const response = await axios.post<IPaymentVerifyResponse>(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`;