import { Injectable, Logger, BadRequestException } from '@nestjs/common'; import axios from 'axios'; import { IPaymentGateway, IPaymentVerifyParams, IRequestPaymentData, IRequestPaymentParams, IPaymentVerifyData, IZarinpalPaymentResponse, IZarinpalRequestPayment, IZarinpalVerifyRequest, IZarinpalVerifyResponse, } from '../interface/gateway'; import { ConfigService } from '@nestjs/config'; import { PaymentMessage } from 'src/common/enums/message.enum'; @Injectable() export class ZarinpalGateway implements IPaymentGateway { private readonly logger = new Logger(ZarinpalGateway.name); private readonly zarinpalRequestUrl: string; private readonly zarinpalVerifyUrl: string; private readonly zarinpalPaymentBaseUrl: string; private readonly zarinpalMerchantId: string; private readonly apiUrl: string = 'https://negareh-api.dev.danakcorp.com/' private readonly axiosConfig = { timeout: 10_000, headers: { 'Content-Type': 'application/json', }, }; constructor(private readonly configService: ConfigService) { const zarinpalBaseUrl = this.configService.get('ZARINPAL_BASE_URL') || 'https://sandbox.zarinpal.com'; this.zarinpalRequestUrl = `${zarinpalBaseUrl}/pg/v4/payment/request.json`; this.zarinpalVerifyUrl = `${zarinpalBaseUrl}/pg/v4/payment/verify.json`; this.zarinpalMerchantId = this.configService.getOrThrow('ZARINPAL_BASE_MERCHANT') this.apiUrl = this.configService.getOrThrow('API_URL') || 'https://negareh-api.dev.danakcorp.com/' } private getAxiosErrorData(error: unknown): { status?: number; data?: unknown } | null { if (!axios.isAxiosError(error) || !error.response) return null; return { status: error.response.status, data: error.response.data }; } async requestPayment({ amount, invoiceId }: IRequestPaymentParams): Promise { // Transform camelCase to snake_case for Zarinpal API v4 const callbackUrl = `${this.apiUrl}/verify/${invoiceId}`; const zarinpalRequest: IZarinpalRequestPayment = { amount, merchant_id: this.zarinpalMerchantId, description: `Payment for order #${invoiceId}`, callback_url: callbackUrl, currency: 'IRT', metadata: { order_id: invoiceId, }, }; try { const res = await axios.post( this.zarinpalRequestUrl, zarinpalRequest, this.axiosConfig, ); const { data, errors } = res.data ?? {}; const code = data?.code; const message = data?.message; const authority = data?.authority; if (!data) { this.logger.error('Invalid response from Zarinpal API (missing data)', JSON.stringify(res.data)); throw new BadRequestException(PaymentMessage.ZARINPAL_INVALID_RESPONSE); } if ((Array.isArray(errors) && errors.length > 0) || code !== 100 || !authority) { this.logger.error( 'Zarinpal payment request failed', JSON.stringify({ code, message, errors, callbackUrl, amount, invoiceId, }), ); throw new BadRequestException(message ?? PaymentMessage.ZARINPAL_PAYMENT_REQUEST_ERROR); } const paymentUrl = `${this.zarinpalPaymentBaseUrl}/pg/StartPay/${authority}` return { token: authority, paymentUrl }; } catch (error) { if (error instanceof BadRequestException) throw error; const axiosErr = this.getAxiosErrorData(error); if (axiosErr) { this.logger.error( 'Zarinpal payment request axios error', JSON.stringify({ status: axiosErr.status, data: axiosErr.data, callbackUrl, amount, invoiceId, }), ); throw new BadRequestException(PaymentMessage.ZARINPAL_PAYMENT_REQUEST_ERROR); } const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.logger.error('Zarinpal payment request error', errorMessage); throw new BadRequestException(PaymentMessage.GATEWAY_ERROR); } } async verifyPayment({ amount, token }: IPaymentVerifyParams): Promise { try { // Transform camelCase to snake_case for Zarinpal API v4 const zarinpalVerifyRequest: IZarinpalVerifyRequest = { merchant_id: this.zarinpalMerchantId, amount, authority: token, }; const res = await axios.post( this.zarinpalVerifyUrl, zarinpalVerifyRequest, this.axiosConfig, ); // Check if response data exists if (!res.data || !res.data.data) { throw new BadRequestException(PaymentMessage.ZARINPAL_INVALID_RESPONSE); } const { code, message, ref_id, card_pan } = res.data.data; // Check if there are errors in the response if (code !== 100 && code !== 0) { throw new BadRequestException(message ?? PaymentMessage.ZARINPAL_VERIFY_ERROR); } return { success: code === 100, referenceId: ref_id ? ref_id.toString() : '', cardPan: card_pan, raw: res.data, }; } catch (error) { if (axios.isAxiosError(error) && error.response) { this.logger.error('Zarinpal verify failed', error.response.data); throw new BadRequestException(error.response.data); } const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new BadRequestException(PaymentMessage.GATEWAY_ERROR); } } }