162 lines
5.5 KiB
TypeScript
Executable File
162 lines
5.5 KiB
TypeScript
Executable File
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 zarinpalBaseUrl: string;
|
|
private readonly zarinpalMerchantId: string;
|
|
private readonly apiUrl: string
|
|
private readonly axiosConfig = {
|
|
timeout: 10_000,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
};
|
|
|
|
constructor(private readonly configService: ConfigService) {
|
|
this.zarinpalBaseUrl = this.configService.get<string>('ZARINPAL_BASE_URL') || 'https://sandbox.zarinpal.com';
|
|
this.zarinpalRequestUrl = `${this.zarinpalBaseUrl}/pg/v4/payment/request.json`;
|
|
this.zarinpalVerifyUrl = `${this.zarinpalBaseUrl}/pg/v4/payment/verify.json`;
|
|
this.zarinpalMerchantId = this.configService.getOrThrow<string>('ZARINPAL_BASE_MERCHANT')
|
|
this.apiUrl = this.configService.get<string>('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<IRequestPaymentData> {
|
|
// Transform camelCase to snake_case for Zarinpal API v4
|
|
const callbackUrl = `${this.apiUrl}/public/payments/online/zarin/verify`;
|
|
const zarinpalRequest: IZarinpalRequestPayment = {
|
|
amount,
|
|
merchant_id: this.zarinpalMerchantId,
|
|
description: `Payment for order #${invoiceId}`,
|
|
callback_url: callbackUrl,
|
|
currency: 'IRR',
|
|
metadata: {
|
|
order_id: invoiceId,
|
|
},
|
|
};
|
|
|
|
try {
|
|
const res = await axios.post<IZarinpalPaymentResponse>(
|
|
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.zarinpalBaseUrl}/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<IPaymentVerifyData> {
|
|
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<IZarinpalVerifyResponse>(
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
}
|