191 lines
6.8 KiB
TypeScript
191 lines
6.8 KiB
TypeScript
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}/payments/${orderId}/callback`;
|
|
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}`);
|
|
}
|
|
}
|
|
}
|