payment refactor

This commit is contained in:
2025-12-16 23:35:39 +03:30
parent f2ecada5a1
commit ea2b7d7968
10 changed files with 263 additions and 463 deletions
+54 -197
View File
@@ -3,63 +3,58 @@ import axios from 'axios';
import {
IPaymentGateway,
IPaymentVerifyParams,
IProcessPaymentData,
IProcessPaymentParams,
IVerifyPayment,
IRequestPaymentData,
IRequestPaymentParams,
IPaymentVerifyData,
IZarinpalPaymentResponse,
IZarinpalRequestPayment,
IZarinpalVerifyRequest,
IZarinpalVerifyResponse,
} from '../interface/gateway';
import { ConfigService } from '@nestjs/config';
import { PaymentGatewayEnum } from '../interface/payment';
@Injectable()
export class ZarinpalGateway implements IPaymentGateway {
private readonly logger = new Logger(ZarinpalGateway.name);
private readonly zarinpalRequestUrl: string;
private readonly zarinpalPaymentBaseUrl: string;
private readonly zarinpalVerifyUrl: string;
private readonly zarinpalPaymentBaseUrl: string;
private readonly axiosConfig = {
timeout: 10_000,
headers: {
'Content-Type': 'application/json',
},
};
constructor(private readonly configService: ConfigService) {
const zarinpalBaseUrl = this.configService.get<string>('ZARINPAL_BASE_URL') || 'https://sandbox.zarinpal.com';
this.zarinpalPaymentBaseUrl = zarinpalBaseUrl;
this.zarinpalRequestUrl = `${zarinpalBaseUrl}/pg/v4/payment/request.json`;
this.zarinpalPaymentBaseUrl = `${zarinpalBaseUrl}/pg/StartPay`;
this.zarinpalVerifyUrl = `${zarinpalBaseUrl}/pg/v4/payment/verify.json`;
}
async processPayment(params: IProcessPaymentParams): Promise<IProcessPaymentData> {
async requestPayment({ amount, merchantId, orderId, domain }: IRequestPaymentParams): Promise<IRequestPaymentData> {
// 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,
const callbackUrl = `${domain}/verify/${orderId}`;
const zarinpalRequest: IZarinpalRequestPayment = {
amount,
merchant_id: merchantId,
description: `Payment for order #${orderId}`,
callback_url: callbackUrl,
metadata: {
order_id: params.metadata.orderId,
order_id: orderId,
},
};
try {
// 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);
const res = await axios.post<IZarinpalPaymentResponse>(this.zarinpalRequestUrl, zarinpalRequest, this.axiosConfig);
const { code, message, authority, errors } = res.data;
// 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}`);
if ((code !== 100 && code !== 0) || !authority) {
throw new BadRequestException(message ?? 'Zarinpal payment request error');
}
// Return the nested data object
if (!response.data.data) {
throw new BadRequestException('Payment gateway returned invalid response structure');
}
return response.data.data;
return { transactionId: authority };
} catch (error) {
// Log the actual API error response for debugging
if (axios.isAxiosError(error) && error.response) {
@@ -73,185 +68,47 @@ export class ZarinpalGateway implements IPaymentGateway {
}
}
zarinpalPaymentUrl(gateway: PaymentGatewayEnum | null, authority: string | null) {
if (gateway === PaymentGatewayEnum.ZarinPal && authority) {
return `${this.zarinpalPaymentBaseUrl}/${authority}`;
}
return null;
}
async verifyPayment(verifyRequest: IPaymentVerifyParams): Promise<IVerifyPayment> {
async verifyPayment({ merchantId, amount, transactionId }: IPaymentVerifyParams): Promise<IPaymentVerifyData> {
try {
// Transform camelCase to snake_case for Zarinpal API v4
const zarinpalVerifyRequest = {
merchant_id: verifyRequest.merchantId,
amount: verifyRequest.amount,
authority: verifyRequest.authority,
const zarinpalVerifyRequest: IZarinpalVerifyRequest = {
merchant_id: merchantId,
amount,
authority: transactionId,
};
// Zarinpal API v4 returns response wrapped in { data: {...}, errors: [] }
interface ZarinpalError {
message?: string;
code?: number;
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('Invalid response from Zarinpal API');
}
interface ZarinpalVerifyResponse {
data: IVerifyPayment;
errors: ZarinpalError[];
}
const response = await axios.post<ZarinpalVerifyResponse>(this.zarinpalVerifyUrl, zarinpalVerifyRequest);
const { code, message, ref_id, card_pan } = res.data.data;
// 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}`);
if (code !== 100 && code !== 0) {
throw new BadRequestException(message ?? 'Zarinpal error');
}
// Return the nested data object
if (!response.data.data) {
throw new BadRequestException('Payment gateway returned invalid response structure');
}
return response.data.data;
return {
success: code === 100,
referenceId: ref_id ? ref_id.toString() : undefined,
cardPan: card_pan,
raw: res.data,
};
} catch (error) {
if (error instanceof BadRequestException) {
throw 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(`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,
// });
getPaymentUrl(authority: string): string {
return `${this.zarinpalPaymentBaseUrl}/pg/StartPay/${authority}`;
}
// 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);
// }
// }
}