Files
dmenu-api/src/modules/payments/services/payment-gateway.service.ts
T
2025-12-06 08:45:59 +03:30

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}`);
}
}
}