payment refactor
This commit is contained in:
@@ -1,190 +0,0 @@
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
IPaymentRequest,
|
||||
IPaymentResponse,
|
||||
IPaymentVerifyRequest,
|
||||
IPaymentVerifyResponse,
|
||||
PaymentGatewayEnum,
|
||||
PaymentMethodEnum,
|
||||
} from '../interface/payment';
|
||||
import axios from 'axios';
|
||||
|
||||
@Injectable()
|
||||
export class PaymentGatewayService {
|
||||
private readonly logger = new Logger(PaymentGatewayService.name);
|
||||
private readonly zarinpalRequestUrl: string;
|
||||
private readonly zarinpalPaymentBaseUrl: string;
|
||||
private readonly zarinpalVerifyUrl: string;
|
||||
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
// Get Zarinpal base URL from environment or default to sandbox for development
|
||||
const zarinpalBaseUrl = this.configService.get<string>('ZARINPAL_BASE_URL') || 'https://sandbox.zarinpal.com';
|
||||
|
||||
this.zarinpalRequestUrl = `${zarinpalBaseUrl}/pg/v4/payment/request.json`;
|
||||
this.zarinpalPaymentBaseUrl = `${zarinpalBaseUrl}/pg/StartPay`;
|
||||
this.zarinpalVerifyUrl = `${zarinpalBaseUrl}/pg/v4/payment/verify.json`;
|
||||
}
|
||||
async requestToGateway(
|
||||
paymentMethod: PaymentMethodEnum,
|
||||
amount: number,
|
||||
orderId: string,
|
||||
merchantId: string | null | undefined,
|
||||
domain: string,
|
||||
gateway: PaymentGatewayEnum | null | undefined,
|
||||
): Promise<{ authority: string | null }> {
|
||||
// For non-online payment methods, no gateway request is needed
|
||||
if (paymentMethod !== PaymentMethodEnum.Online) {
|
||||
return { authority: null };
|
||||
}
|
||||
|
||||
// Online payments require merchantId and gateway
|
||||
if (!merchantId) {
|
||||
throw new BadRequestException('Merchant ID is required for online payments');
|
||||
}
|
||||
|
||||
if (!gateway) {
|
||||
throw new BadRequestException('Payment gateway is required for online payments');
|
||||
}
|
||||
|
||||
// Handle ZarinPal gateway
|
||||
if (gateway === PaymentGatewayEnum.ZarinPal) {
|
||||
const callbackUrl = `${domain}/verify/${orderId}`;
|
||||
try {
|
||||
const gatewayResponse = await this.requestToZarinPalGateway({
|
||||
amount,
|
||||
merchantId,
|
||||
description: `Payment for order #${orderId}`,
|
||||
callbackUrl,
|
||||
metadata: {
|
||||
orderId,
|
||||
},
|
||||
});
|
||||
this.logger.log('gatewayResponse', gatewayResponse);
|
||||
// Check gateway response code (typically 100 means success for Iranian gateways)
|
||||
if (gatewayResponse.code !== 100 && gatewayResponse.code !== 0) {
|
||||
throw new BadRequestException(`Payment gateway error: ${gatewayResponse.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
if (!gatewayResponse.authority) {
|
||||
throw new BadRequestException('Payment gateway did not return an authority token');
|
||||
}
|
||||
|
||||
return { authority: gatewayResponse.authority };
|
||||
} catch (error) {
|
||||
this.logger.error('Error in request to gateway', error);
|
||||
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
throw new BadRequestException(`Failed to connect to payment gateway: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If gateway is not supported, throw an error
|
||||
throw new BadRequestException(`Unsupported payment gateway: ${String(gateway)}`);
|
||||
}
|
||||
|
||||
private async requestToZarinPalGateway(requestPayment: IPaymentRequest): Promise<IPaymentResponse> {
|
||||
// Transform camelCase to snake_case for Zarinpal API v4
|
||||
const zarinpalRequest = {
|
||||
amount: requestPayment.amount,
|
||||
merchant_id: requestPayment.merchantId,
|
||||
description: requestPayment.description,
|
||||
callback_url: requestPayment.callbackUrl,
|
||||
metadata: {
|
||||
order_id: requestPayment.metadata.orderId,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
// Zarinpal API v4 returns response wrapped in { data: {...}, errors: [] }
|
||||
interface ZarinpalError {
|
||||
message?: string;
|
||||
code?: number;
|
||||
}
|
||||
interface ZarinpalResponse {
|
||||
data: IPaymentResponse;
|
||||
errors: ZarinpalError[];
|
||||
}
|
||||
const response = await axios.post<ZarinpalResponse>(this.zarinpalRequestUrl, zarinpalRequest);
|
||||
|
||||
// Check if there are errors in the response
|
||||
if (response.data.errors && response.data.errors.length > 0) {
|
||||
const errorMessage = response.data.errors.map(err => err.message || JSON.stringify(err)).join(', ');
|
||||
throw new BadRequestException(`Payment gateway error: ${errorMessage}`);
|
||||
}
|
||||
|
||||
// Return the nested data object
|
||||
if (!response.data.data) {
|
||||
throw new BadRequestException('Payment gateway returned invalid response structure');
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
// Log the actual API error response for debugging
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
this.logger.error('Zarinpal API error response', {
|
||||
status: error.response.status,
|
||||
data: JSON.stringify(error.response.data),
|
||||
request: zarinpalRequest,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
zarinpalPaymentUrl(gateway: PaymentGatewayEnum | null, authority: string | null) {
|
||||
if (gateway === PaymentGatewayEnum.ZarinPal && authority) {
|
||||
return `${this.zarinpalPaymentBaseUrl}/${authority}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async verifyZarinPalPayment(verifyRequest: IPaymentVerifyRequest): Promise<IPaymentVerifyResponse> {
|
||||
try {
|
||||
// Transform camelCase to snake_case for Zarinpal API v4
|
||||
const zarinpalVerifyRequest = {
|
||||
merchant_id: verifyRequest.merchantId,
|
||||
amount: verifyRequest.amount,
|
||||
authority: verifyRequest.authority,
|
||||
};
|
||||
|
||||
// Zarinpal API v4 returns response wrapped in { data: {...}, errors: [] }
|
||||
interface ZarinpalError {
|
||||
message?: string;
|
||||
code?: number;
|
||||
}
|
||||
interface ZarinpalVerifyResponse {
|
||||
data: IPaymentVerifyResponse;
|
||||
errors: ZarinpalError[];
|
||||
}
|
||||
const response = await axios.post<ZarinpalVerifyResponse>(this.zarinpalVerifyUrl, zarinpalVerifyRequest);
|
||||
|
||||
// Check if there are errors in the response
|
||||
if (response.data.errors && response.data.errors.length > 0) {
|
||||
const errorMessage = response.data.errors.map(err => err.message || JSON.stringify(err)).join(', ');
|
||||
throw new BadRequestException(`Payment gateway error: ${errorMessage}`);
|
||||
}
|
||||
|
||||
// Return the nested data object
|
||||
if (!response.data.data) {
|
||||
throw new BadRequestException('Payment gateway returned invalid response structure');
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
throw new BadRequestException(`Failed to verify payment with gateway: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PaymentGatewayService } from './payment-gateway.service';
|
||||
import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment';
|
||||
import { Payment } from '../entities/payment.entity';
|
||||
import { EntityManager, RequiredEntityData } from '@mikro-orm/core';
|
||||
@@ -7,56 +6,18 @@ import { Order } from '../../orders/entities/order.entity';
|
||||
import { PaymentMethod } from '../entities/payment-method.entity';
|
||||
import { CreatePaymentDto } from '../dto/create-payment.dto';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
import { ZarinpalGateway } from '../gateways/zarinpal.gateway';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class PaymentsService {
|
||||
private readonly logger = new Logger(PaymentsService.name);
|
||||
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly paymentGatewayService: PaymentGatewayService,
|
||||
private readonly zarinpalGateway: ZarinpalGateway,
|
||||
) {}
|
||||
|
||||
// async initializePayment(
|
||||
// paymentMethodId: string,
|
||||
// amount: number,
|
||||
// orderId: string,
|
||||
// ): Promise<{ paymentUrl: string | null }> {
|
||||
// // Validate amount
|
||||
// if (amount <= 0) {
|
||||
// throw new BadRequestException('Amount must be greater than zero');
|
||||
// }
|
||||
|
||||
// // Validate order exists
|
||||
// const order = await this.em.findOne(Order, { id: orderId });
|
||||
// if (!order) {
|
||||
// throw new NotFoundException('Order not found');
|
||||
// }
|
||||
|
||||
// // Load restaurant payment method with payment method relationship
|
||||
// const paymentMethod = await this.em.findOne(PaymentMethod, { id: paymentMethodId }, { populate: ['restaurant'] });
|
||||
// if (!paymentMethod) {
|
||||
// throw new NotFoundException('Payment method not found');
|
||||
// }
|
||||
|
||||
// if (paymentMethod.method === PaymentMethodEnum.Online && !paymentMethod.merchantId) {
|
||||
// throw new BadRequestException('Merchant ID is required for online payments');
|
||||
// }
|
||||
|
||||
// // Create payment record and save authority
|
||||
// const restaurantDomain = paymentMethod.restaurant.domain;
|
||||
// const gateway = paymentMethod.gateway;
|
||||
// const { authority } = await this.createPayment(restaurantDomain, {
|
||||
// amount,
|
||||
// orderId,
|
||||
// paymentMethod: paymentMethod.method,
|
||||
// merchantId: paymentMethod.merchantId ?? null,
|
||||
// gateway,
|
||||
// });
|
||||
|
||||
// const paymentUrl = this.paymentGatewayService.zarinpalPaymentUrl(gateway, authority);
|
||||
|
||||
// return { paymentUrl };
|
||||
// }
|
||||
|
||||
async startPayment(orderId: string, restId: string): Promise<{ paymentUrl: string | null }> {
|
||||
const { amount, method, restaurantDomain, gateway, merchantId, user } = await this.validateOrder(orderId, restId);
|
||||
|
||||
@@ -81,7 +42,7 @@ export class PaymentsService {
|
||||
gateway,
|
||||
});
|
||||
|
||||
const paymentUrl = this.paymentGatewayService.zarinpalPaymentUrl(gateway, authority);
|
||||
const paymentUrl = this.zarinpalGateway.zarinpalPaymentUrl(gateway, authority);
|
||||
|
||||
return { paymentUrl };
|
||||
}
|
||||
@@ -123,14 +84,7 @@ export class PaymentsService {
|
||||
async createPayment(domain: string, dto: CreatePaymentDto) {
|
||||
const { amount, orderId, merchantId, gateway, method } = dto;
|
||||
|
||||
const { authority } = await this.paymentGatewayService.requestToGateway(
|
||||
method,
|
||||
amount,
|
||||
orderId,
|
||||
merchantId,
|
||||
domain,
|
||||
gateway,
|
||||
);
|
||||
const { authority } = await this.requestToGateway(method, amount, orderId, merchantId, domain, gateway);
|
||||
|
||||
const payment = this.em.create(Payment, {
|
||||
amount,
|
||||
@@ -242,7 +196,7 @@ export class PaymentsService {
|
||||
// Handle ZarinPal gateway verification
|
||||
if (payment.gateway === PaymentGatewayEnum.ZarinPal) {
|
||||
try {
|
||||
const verifyResponse = await this.paymentGatewayService.verifyZarinPalPayment({
|
||||
const verifyResponse = await this.zarinpalGateway.verifyPayment({
|
||||
merchantId: paymentMethod.merchantId,
|
||||
amount: verifyAmount,
|
||||
authority,
|
||||
@@ -293,10 +247,6 @@ export class PaymentsService {
|
||||
throw new BadRequestException(`Unsupported payment gateway: ${String(payment.gateway)}`);
|
||||
}
|
||||
|
||||
// update(_id: number, _updatePaymentDto: unknown) {
|
||||
// return `This action updates a #${_id} payment`;
|
||||
// }
|
||||
|
||||
findAllByRestaurantId(restId: string, userId: string) {
|
||||
return this.em.find(
|
||||
Payment,
|
||||
@@ -305,7 +255,65 @@ export class PaymentsService {
|
||||
);
|
||||
}
|
||||
|
||||
// remove(id: number) {
|
||||
// return `This action removes a #${id} payment`;
|
||||
// }
|
||||
async requestToGateway(
|
||||
paymentMethod: PaymentMethodEnum,
|
||||
amount: number,
|
||||
orderId: string,
|
||||
merchantId: string | null | undefined,
|
||||
domain: string,
|
||||
gateway: PaymentGatewayEnum | null | undefined,
|
||||
): Promise<{ authority: string | null }> {
|
||||
// For non-online payment methods, no gateway request is needed
|
||||
if (paymentMethod !== PaymentMethodEnum.Online) {
|
||||
return { authority: null };
|
||||
}
|
||||
|
||||
// Online payments require merchantId and gateway
|
||||
if (!merchantId) {
|
||||
throw new BadRequestException('Merchant ID is required for online payments');
|
||||
}
|
||||
|
||||
if (!gateway) {
|
||||
throw new BadRequestException('Payment gateway is required for online payments');
|
||||
}
|
||||
|
||||
// Handle ZarinPal gateway
|
||||
if (gateway === PaymentGatewayEnum.ZarinPal) {
|
||||
const callbackUrl = `${domain}/verify/${orderId}`;
|
||||
try {
|
||||
const gatewayResponse = await this.zarinpalGateway.processPayment({
|
||||
amount,
|
||||
merchantId,
|
||||
description: `Payment for order #${orderId}`,
|
||||
callbackUrl,
|
||||
metadata: {
|
||||
orderId,
|
||||
},
|
||||
});
|
||||
this.logger.log('gatewayResponse', gatewayResponse);
|
||||
// Check gateway response code (typically 100 means success for Iranian gateways)
|
||||
if (gatewayResponse.code !== 100 && gatewayResponse.code !== 0) {
|
||||
throw new BadRequestException(`Payment gateway error: ${gatewayResponse.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
if (!gatewayResponse.authority) {
|
||||
throw new BadRequestException('Payment gateway did not return an authority token');
|
||||
}
|
||||
|
||||
return { authority: gatewayResponse.authority };
|
||||
} catch (error) {
|
||||
this.logger.error('Error in request to gateway', error);
|
||||
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
throw new BadRequestException(`Failed to connect to payment gateway: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If gateway is not supported, throw an error
|
||||
throw new BadRequestException(`Unsupported payment gateway: ${String(gateway)}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user