payment refactor
This commit is contained in:
@@ -1,167 +1,257 @@
|
||||
import { HttpService } from "@nestjs/axios";
|
||||
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
import { AxiosError } from "axios";
|
||||
import { catchError, firstValueFrom, throwError } from "rxjs";
|
||||
|
||||
import { PaymentMessage } from "../../../common/enums/message.enum";
|
||||
import { IZarinpalConfig } from "../../../configs/zarinpal.config";
|
||||
import { ZARINPAL_CONFIG } from "../constants";
|
||||
import { GatewayEnum } from "../enums/gateway.enum";
|
||||
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
IPaymentGateway,
|
||||
IPaymentVerifyParams,
|
||||
IProcessPaymentData,
|
||||
IProcessPaymentParams,
|
||||
ZarinPalPGNewArgs,
|
||||
ZarinPalPGNewRequestData,
|
||||
ZarinPalPGVerifyData,
|
||||
} from "../interfaces/IPayment";
|
||||
IVerifyPayment,
|
||||
} from '../interface/gateway';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PaymentGatewayEnum } from '../interface/payment';
|
||||
|
||||
@Injectable()
|
||||
export class ZarinpalGateway implements IPaymentGateway {
|
||||
private readonly IPG_TYPE = "payment";
|
||||
private readonly logger = new Logger(ZarinpalGateway.name);
|
||||
private readonly gatewayApiUrl: string;
|
||||
private readonly requestHeader: Record<string, string> = { "Content-Type": "application/json", Accept: "application/json" };
|
||||
private readonly zarinpalRequestUrl: string;
|
||||
private readonly zarinpalPaymentBaseUrl: string;
|
||||
private readonly zarinpalVerifyUrl: string;
|
||||
|
||||
constructor(
|
||||
@Inject(ZARINPAL_CONFIG) private readonly config: IZarinpalConfig,
|
||||
private readonly httpService: HttpService,
|
||||
) {
|
||||
this.gatewayApiUrl = `https://${this.IPG_TYPE}.zarinpal.com`;
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
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 processPayment(processParams: IProcessPaymentParams) {
|
||||
try {
|
||||
const purchaseData: ZarinPalPGNewArgs = {
|
||||
merchant_id: this.config.merchantId,
|
||||
amount: processParams.amount,
|
||||
callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`,
|
||||
description: processParams.description,
|
||||
currency: "IRT",
|
||||
metadata: { email: processParams.email, mobile: processParams.mobile },
|
||||
};
|
||||
|
||||
this.logger.log(`Processing payment request:`, {
|
||||
merchant_id: this.config.merchantId,
|
||||
amount: processParams.amount,
|
||||
callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`,
|
||||
description: processParams.description,
|
||||
});
|
||||
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ZarinPalPGNewRequestData>(`${this.gatewayApiUrl}/pg/v4/payment/request.json`, purchaseData, {
|
||||
headers: this.requestHeader,
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error(`Payment request failed: ${err.message}`, err.stack);
|
||||
return throwError(() => new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT));
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Check for errors in response
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
this.logger.error(`Zarinpal payment request error:`, data.errors);
|
||||
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
|
||||
}
|
||||
|
||||
// Validate response data
|
||||
if (!data.data?.authority) {
|
||||
this.logger.error(`Invalid response from Zarinpal:`, data);
|
||||
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
|
||||
}
|
||||
|
||||
this.logger.log(`Payment request successful - Authority: ${data.data.authority}`);
|
||||
|
||||
return {
|
||||
redirectUrl: `${this.gatewayApiUrl}/pg/StartPay/${data.data.authority}`,
|
||||
message: data.data.message,
|
||||
reference: data.data.authority,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Payment processing error:`, error);
|
||||
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyPayment(verifyParams: IPaymentVerifyParams) {
|
||||
const verifyData = {
|
||||
merchant_id: this.config.merchantId,
|
||||
amount: Number(verifyParams.amount),
|
||||
authority: verifyParams.reference,
|
||||
async processPayment(params: IProcessPaymentParams): Promise<IProcessPaymentData> {
|
||||
// Transform camelCase to snake_case for Zarinpal API v4
|
||||
const zarinpalRequest = {
|
||||
amount: params.amount,
|
||||
merchant_id: params.merchantId,
|
||||
description: params.description,
|
||||
callback_url: params.callbackUrl,
|
||||
metadata: {
|
||||
order_id: params.metadata.orderId,
|
||||
},
|
||||
};
|
||||
|
||||
this.logger.log(`Verifying payment:`, {
|
||||
merchant_id: this.config.merchantId,
|
||||
amount: Number(verifyParams.amount),
|
||||
authority: verifyParams.reference,
|
||||
});
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ZarinPalPGVerifyData>(`${this.gatewayApiUrl}/pg/v4/payment/verify.json`, verifyData, {
|
||||
headers: this.requestHeader,
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error(`Verification request failed: ${err.message}`, err.stack);
|
||||
// Zarinpal API v4 returns response wrapped in { data: {...}, errors: [] }
|
||||
interface ZarinpalError {
|
||||
message?: string;
|
||||
code?: number;
|
||||
}
|
||||
interface ZarinpalResponse {
|
||||
data: IProcessPaymentData;
|
||||
errors: ZarinpalError[];
|
||||
}
|
||||
const response = await axios.post<ZarinpalResponse>(this.zarinpalRequestUrl, zarinpalRequest);
|
||||
|
||||
// If we have error response data from Zarinpal, extract it
|
||||
if (err.response?.data) {
|
||||
const errorData = err.response.data as ZarinPalPGVerifyData;
|
||||
this.logger.error(`Zarinpal error response:`, errorData);
|
||||
|
||||
// Return the Zarinpal error response if it has the expected structure
|
||||
if (errorData.errors) {
|
||||
return throwError(() => errorData);
|
||||
}
|
||||
}
|
||||
|
||||
return throwError(() => new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT));
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Log the verification result
|
||||
if (data.data) {
|
||||
this.logger.log(`Verification response - Code: ${data.data.code}, RefID: ${data.data.ref_id || "N/A"}`);
|
||||
} else if (data.errors && data.errors.length > 0) {
|
||||
this.logger.warn(`Verification error - Code: ${data.errors[0].code}, Message: ${data.errors[0].message}`);
|
||||
// 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 complete Zarinpal response - let the service handle the business logic
|
||||
return {
|
||||
code: data.data?.code || data.errors?.[0]?.code || -1,
|
||||
message: data.data?.message || data.errors?.[0]?.message || "Unknown error",
|
||||
ref_id: data.data?.ref_id || 0,
|
||||
card_hash: data.data?.card_hash || "",
|
||||
card_pan: data.data?.card_pan || "",
|
||||
fee_type: data.data?.fee_type || "",
|
||||
fee: data.data?.fee || 0,
|
||||
};
|
||||
// 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 it's a Zarinpal error response, extract the error code and return it
|
||||
if (error && typeof error === "object" && "errors" in error) {
|
||||
const zarinpalError = error as ZarinPalPGVerifyData;
|
||||
const firstError = zarinpalError.errors[0];
|
||||
this.logger.warn(`Zarinpal verification error - Code: ${firstError.code}, Message: ${firstError.message}`);
|
||||
|
||||
return {
|
||||
code: firstError.code,
|
||||
message: firstError.message,
|
||||
ref_id: 0,
|
||||
card_hash: "",
|
||||
card_pan: "",
|
||||
fee_type: "",
|
||||
fee: 0,
|
||||
};
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.error(`Payment verification error:`, error);
|
||||
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
zarinpalPaymentUrl(gateway: PaymentGatewayEnum | null, authority: string | null) {
|
||||
if (gateway === PaymentGatewayEnum.ZarinPal && authority) {
|
||||
return `${this.zarinpalPaymentBaseUrl}/${authority}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async verifyPayment(verifyRequest: IPaymentVerifyParams): Promise<IVerifyPayment> {
|
||||
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: IVerifyPayment;
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
// async processPayment(processParams: IProcessPaymentParams) {
|
||||
// try {
|
||||
// const purchaseData: ZarinPalPGNewArgs = {
|
||||
// merchant_id: this.config.merchantId,
|
||||
// amount: processParams.amount,
|
||||
// callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`,
|
||||
// description: processParams.description,
|
||||
// currency: "IRT",
|
||||
// metadata: { email: processParams.email, mobile: processParams.mobile },
|
||||
// };
|
||||
|
||||
// this.logger.log(`Processing payment request:`, {
|
||||
// merchant_id: this.config.merchantId,
|
||||
// amount: processParams.amount,
|
||||
// callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`,
|
||||
// description: processParams.description,
|
||||
// });
|
||||
|
||||
// const { data } = await firstValueFrom(
|
||||
// this.httpService
|
||||
// .post<ZarinPalPGNewRequestData>(`${this.gatewayApiUrl}/pg/v4/payment/request.json`, purchaseData, {
|
||||
// headers: this.requestHeader,
|
||||
// })
|
||||
// .pipe(
|
||||
// catchError((err: AxiosError) => {
|
||||
// this.logger.error(`Payment request failed: ${err.message}`, err.stack);
|
||||
// return throwError(() => new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT));
|
||||
// }),
|
||||
// ),
|
||||
// );
|
||||
|
||||
// // Check for errors in response
|
||||
// if (data.errors && data.errors.length > 0) {
|
||||
// this.logger.error(`Zarinpal payment request error:`, data.errors);
|
||||
// throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
|
||||
// }
|
||||
|
||||
// // Validate response data
|
||||
// if (!data.data?.authority) {
|
||||
// this.logger.error(`Invalid response from Zarinpal:`, data);
|
||||
// throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
|
||||
// }
|
||||
|
||||
// this.logger.log(`Payment request successful - Authority: ${data.data.authority}`);
|
||||
|
||||
// return {
|
||||
// redirectUrl: `${this.gatewayApiUrl}/pg/StartPay/${data.data.authority}`,
|
||||
// message: data.data.message,
|
||||
// reference: data.data.authority,
|
||||
// };
|
||||
// } catch (error) {
|
||||
// this.logger.error(`Payment processing error:`, error);
|
||||
// throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
|
||||
// }
|
||||
// }
|
||||
|
||||
// async verifyPayment(verifyParams: IPaymentVerifyParams) {
|
||||
// const verifyData = {
|
||||
// merchant_id: this.config.merchantId,
|
||||
// amount: Number(verifyParams.amount),
|
||||
// authority: verifyParams.reference,
|
||||
// };
|
||||
|
||||
// this.logger.log(`Verifying payment:`, {
|
||||
// merchant_id: this.config.merchantId,
|
||||
// amount: Number(verifyParams.amount),
|
||||
// authority: verifyParams.reference,
|
||||
// });
|
||||
|
||||
// try {
|
||||
// const { data } = await firstValueFrom(
|
||||
// this.httpService
|
||||
// .post<ZarinPalPGVerifyData>(`${this.gatewayApiUrl}/pg/v4/payment/verify.json`, verifyData, {
|
||||
// headers: this.requestHeader,
|
||||
// })
|
||||
// .pipe(
|
||||
// catchError((err: AxiosError) => {
|
||||
// this.logger.error(`Verification request failed: ${err.message}`, err.stack);
|
||||
|
||||
// // If we have error response data from Zarinpal, extract it
|
||||
// if (err.response?.data) {
|
||||
// const errorData = err.response.data as ZarinPalPGVerifyData;
|
||||
// this.logger.error(`Zarinpal error response:`, errorData);
|
||||
|
||||
// // Return the Zarinpal error response if it has the expected structure
|
||||
// if (errorData.errors) {
|
||||
// return throwError(() => errorData);
|
||||
// }
|
||||
// }
|
||||
|
||||
// return throwError(() => new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT));
|
||||
// }),
|
||||
// ),
|
||||
// );
|
||||
|
||||
// // Log the verification result
|
||||
// if (data.data) {
|
||||
// this.logger.log(`Verification response - Code: ${data.data.code}, RefID: ${data.data.ref_id || "N/A"}`);
|
||||
// } else if (data.errors && data.errors.length > 0) {
|
||||
// this.logger.warn(`Verification error - Code: ${data.errors[0].code}, Message: ${data.errors[0].message}`);
|
||||
// }
|
||||
|
||||
// // Return the complete Zarinpal response - let the service handle the business logic
|
||||
// return {
|
||||
// code: data.data?.code || data.errors?.[0]?.code || -1,
|
||||
// message: data.data?.message || data.errors?.[0]?.message || "Unknown error",
|
||||
// ref_id: data.data?.ref_id || 0,
|
||||
// card_hash: data.data?.card_hash || "",
|
||||
// card_pan: data.data?.card_pan || "",
|
||||
// fee_type: data.data?.fee_type || "",
|
||||
// fee: data.data?.fee || 0,
|
||||
// };
|
||||
// } catch (error) {
|
||||
// // If it's a Zarinpal error response, extract the error code and return it
|
||||
// if (error && typeof error === "object" && "errors" in error) {
|
||||
// const zarinpalError = error as ZarinPalPGVerifyData;
|
||||
// const firstError = zarinpalError.errors[0];
|
||||
// this.logger.warn(`Zarinpal verification error - Code: ${firstError.code}, Message: ${firstError.message}`);
|
||||
|
||||
// return {
|
||||
// code: firstError.code,
|
||||
// message: firstError.message,
|
||||
// ref_id: 0,
|
||||
// card_hash: "",
|
||||
// card_pan: "",
|
||||
// fee_type: "",
|
||||
// fee: 0,
|
||||
// };
|
||||
// }
|
||||
|
||||
// this.logger.error(`Payment verification error:`, error);
|
||||
// throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user