chore: change the payment logic
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class ChangeEnumValueOfPaymentStatus1749124463575 implements MigrationInterface {
|
||||||
|
name = "ChangeEnumValueOfPaymentStatus1749124463575";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TYPE "public"."payment_status_enum" RENAME TO "payment_status_enum_old"`);
|
||||||
|
await queryRunner.query(`CREATE TYPE "public"."payment_status_enum" AS ENUM('PENDING', 'COMPLETED', 'CANCELLED')`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "payment" ALTER COLUMN "status" DROP DEFAULT`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "payment" ALTER COLUMN "status" TYPE "public"."payment_status_enum" USING "status"::"text"::"public"."payment_status_enum"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE "payment" ALTER COLUMN "status" SET DEFAULT 'PENDING'`);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."payment_status_enum_old"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TYPE "public"."payment_status_enum_old" AS ENUM('CANCELLED', 'COMPLETED', 'FAILED', 'PENDING')`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "payment" ALTER COLUMN "status" DROP DEFAULT`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "payment" ALTER COLUMN "status" TYPE "public"."payment_status_enum_old" USING "status"::"text"::"public"."payment_status_enum_old"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE "payment" ALTER COLUMN "status" SET DEFAULT 'PENDING'`);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."payment_status_enum"`);
|
||||||
|
await queryRunner.query(`ALTER TYPE "public"."payment_status_enum_old" RENAME TO "payment_status_enum"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,16 +5,12 @@ export function zarinpalConfig() {
|
|||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
merchantId: configService.getOrThrow<string>("ZARINPAL_MERCHANT_ID"),
|
merchantId: configService.getOrThrow<string>("ZARINPAL_MERCHANT_ID"),
|
||||||
gatewayApiUrl: configService.getOrThrow<string>("ZARINPAL_API_URL"),
|
|
||||||
callBackUrl: configService.getOrThrow<string>("CALLBACK_URL"),
|
callBackUrl: configService.getOrThrow<string>("CALLBACK_URL"),
|
||||||
ipgType: configService.getOrThrow<string>("IPG_TYPE"),
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IZarinpalConfig {
|
export interface IZarinpalConfig {
|
||||||
merchantId: string;
|
merchantId: string;
|
||||||
gatewayApiUrl: string;
|
|
||||||
callBackUrl: string;
|
callBackUrl: string;
|
||||||
ipgType: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export enum PaymentStatus {
|
export enum PaymentStatus {
|
||||||
PENDING = "PENDING",
|
PENDING = "PENDING",
|
||||||
COMPLETED = "COMPLETED",
|
COMPLETED = "COMPLETED",
|
||||||
FAILED = "FAILED",
|
|
||||||
CANCELLED = "CANCELLED",
|
CANCELLED = "CANCELLED",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
export enum ZarinpalErrorCode {
|
||||||
|
// Success codes
|
||||||
|
SUCCESS = 100,
|
||||||
|
ALREADY_VERIFIED = 101,
|
||||||
|
|
||||||
|
// Error codes
|
||||||
|
MERCHANT_NOT_FOUND = -9,
|
||||||
|
MERCHANT_INACTIVE = -10,
|
||||||
|
MERCHANT_INVALID = -11,
|
||||||
|
AMOUNT_INVALID = -12,
|
||||||
|
DESCRIPTION_INVALID = -15,
|
||||||
|
CALLBACK_URL_INVALID = -16,
|
||||||
|
CURRENCY_NOT_SUPPORTED = -17,
|
||||||
|
TRANSACTION_FAILED = -22,
|
||||||
|
AMOUNT_TOO_LOW = -33,
|
||||||
|
AMOUNT_TOO_HIGH = -34,
|
||||||
|
AUTHORITY_INVALID = -50,
|
||||||
|
SESSION_INVALID = -51,
|
||||||
|
PAYMENT_NOT_FOUND = -54,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ZarinpalErrorMessages: Record<ZarinpalErrorCode, string> = {
|
||||||
|
[ZarinpalErrorCode.SUCCESS]: "Payment successful",
|
||||||
|
[ZarinpalErrorCode.ALREADY_VERIFIED]: "Payment already verified",
|
||||||
|
[ZarinpalErrorCode.MERCHANT_NOT_FOUND]: "Merchant not found",
|
||||||
|
[ZarinpalErrorCode.MERCHANT_INACTIVE]: "Merchant inactive",
|
||||||
|
[ZarinpalErrorCode.MERCHANT_INVALID]: "Merchant invalid",
|
||||||
|
[ZarinpalErrorCode.AMOUNT_INVALID]: "Amount invalid",
|
||||||
|
[ZarinpalErrorCode.DESCRIPTION_INVALID]: "Description invalid",
|
||||||
|
[ZarinpalErrorCode.CALLBACK_URL_INVALID]: "Callback URL invalid",
|
||||||
|
[ZarinpalErrorCode.CURRENCY_NOT_SUPPORTED]: "Currency not supported",
|
||||||
|
[ZarinpalErrorCode.TRANSACTION_FAILED]: "Transaction failed",
|
||||||
|
[ZarinpalErrorCode.AMOUNT_TOO_LOW]: "Amount too low",
|
||||||
|
[ZarinpalErrorCode.AMOUNT_TOO_HIGH]: "Amount too high",
|
||||||
|
[ZarinpalErrorCode.AUTHORITY_INVALID]: "Authority invalid",
|
||||||
|
[ZarinpalErrorCode.SESSION_INVALID]: "Session is not valid, session is not active paid try",
|
||||||
|
[ZarinpalErrorCode.PAYMENT_NOT_FOUND]: "Payment not found",
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { HttpService } from "@nestjs/axios";
|
import { HttpService } from "@nestjs/axios";
|
||||||
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { catchError, firstValueFrom } from "rxjs";
|
import { catchError, firstValueFrom, throwError } from "rxjs";
|
||||||
|
|
||||||
import { PaymentMessage } from "../../../common/enums/message.enum";
|
import { PaymentMessage } from "../../../common/enums/message.enum";
|
||||||
import { IZarinpalConfig } from "../../../configs/zarinpal.config";
|
import { IZarinpalConfig } from "../../../configs/zarinpal.config";
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ZarinpalGateway implements IPaymentGateway {
|
export class ZarinpalGateway implements IPaymentGateway {
|
||||||
|
private readonly IPG_TYPE = "payment";
|
||||||
private readonly logger = new Logger(ZarinpalGateway.name);
|
private readonly logger = new Logger(ZarinpalGateway.name);
|
||||||
private readonly gatewayApiUrl: string;
|
private readonly gatewayApiUrl: string;
|
||||||
private readonly requestHeader: Record<string, string> = { "Content-Type": "application/json", Accept: "application/json" };
|
private readonly requestHeader: Record<string, string> = { "Content-Type": "application/json", Accept: "application/json" };
|
||||||
@@ -26,73 +27,140 @@ export class ZarinpalGateway implements IPaymentGateway {
|
|||||||
@Inject(ZARINPAL_CONFIG) private readonly config: IZarinpalConfig,
|
@Inject(ZARINPAL_CONFIG) private readonly config: IZarinpalConfig,
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
) {
|
) {
|
||||||
this.gatewayApiUrl = `https://${this.config.ipgType}.${this.config.gatewayApiUrl}`;
|
this.gatewayApiUrl = `https://${this.IPG_TYPE}.zarinpal.com`;
|
||||||
}
|
}
|
||||||
//*************************************** */
|
|
||||||
async processPayment(processParams: IProcessPaymentParams) {
|
async processPayment(processParams: IProcessPaymentParams) {
|
||||||
try {
|
try {
|
||||||
const purchaseData: ZarinPalPGNewArgs = {
|
const purchaseData: ZarinPalPGNewArgs = {
|
||||||
merchant_id: this.config.merchantId,
|
merchant_id: this.config.merchantId,
|
||||||
amount: processParams.amount,
|
amount: processParams.amount,
|
||||||
callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}${processParams.invoiceId ? "/" + processParams.invoiceId : ""}`,
|
callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`,
|
||||||
description: processParams.description,
|
description: processParams.description,
|
||||||
currency: "IRT",
|
currency: "IRT",
|
||||||
metadata: { email: processParams.email, mobile: processParams.mobile },
|
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(
|
const { data } = await firstValueFrom(
|
||||||
this.httpService
|
this.httpService
|
||||||
.post<ZarinPalPGNewRequestData>(`${this.gatewayApiUrl}/v4/payment/request.json`, purchaseData, {
|
.post<ZarinPalPGNewRequestData>(`${this.gatewayApiUrl}/pg/v4/payment/request.json`, purchaseData, {
|
||||||
headers: this.requestHeader,
|
headers: this.requestHeader,
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError((err: AxiosError) => {
|
catchError((err: AxiosError) => {
|
||||||
this.logger.error(err);
|
this.logger.error(`Payment request failed: ${err.message}`, err.stack);
|
||||||
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
|
return throwError(() => new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT));
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
redirectUrl: `${this.gatewayApiUrl}/StartPay/${data.data.authority}`,
|
|
||||||
message: data.data.message,
|
|
||||||
reference: data.data.authority,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(error);
|
|
||||||
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//********************************** */
|
|
||||||
async verifyPayment(verifyParams: IPaymentVerifyParams) {
|
|
||||||
try {
|
|
||||||
const verifyData = {
|
|
||||||
authority: verifyParams.reference,
|
|
||||||
amount: verifyParams.amount,
|
|
||||||
merchant_id: this.config.merchantId,
|
|
||||||
};
|
|
||||||
const { data } = await firstValueFrom(
|
|
||||||
this.httpService
|
|
||||||
.post<ZarinPalPGVerifyData>(`${this.gatewayApiUrl}/v4/payment/verify.json`, verifyData, { headers: this.requestHeader })
|
|
||||||
.pipe(
|
|
||||||
catchError((err: AxiosError) => {
|
|
||||||
this.logger.error(err);
|
|
||||||
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_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 {
|
return {
|
||||||
code: data.data.code,
|
redirectUrl: `${this.gatewayApiUrl}/pg/StartPay/${data.data.authority}`,
|
||||||
message: data.data.message,
|
message: data.data.message,
|
||||||
card_hash: data.data.card_hash,
|
reference: data.data.authority,
|
||||||
card_pan: data.data.card_pan,
|
|
||||||
ref_id: data.data.ref_id,
|
|
||||||
fee_type: data.data.fee_type,
|
|
||||||
fee: data.data.fee,
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error({ error });
|
this.logger.error(`Payment processing error:`, error);
|
||||||
this.logger.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 any;
|
||||||
|
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 any;
|
||||||
|
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);
|
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ export interface IProcessPaymentData {
|
|||||||
export interface IVerifyPayment {
|
export interface IVerifyPayment {
|
||||||
code: number;
|
code: number;
|
||||||
ref_id: number;
|
ref_id: number;
|
||||||
|
message?: string;
|
||||||
|
card_hash?: string;
|
||||||
|
card_pan?: string;
|
||||||
|
fee_type?: string;
|
||||||
|
fee?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPaymentVerifyParams {
|
export interface IPaymentVerifyParams {
|
||||||
@@ -59,9 +64,16 @@ interface IZarinPalPGRequestData {
|
|||||||
fee: number;
|
fee: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zarinpal error structure
|
||||||
|
interface ZarinpalError {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
validations?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ZarinPalPGNewRequestData {
|
export interface ZarinPalPGNewRequestData {
|
||||||
data: IZarinPalPGRequestData;
|
data: IZarinPalPGRequestData;
|
||||||
error: string[];
|
errors: ZarinpalError[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IZarinpalPaymentVerifyData {
|
interface IZarinpalPaymentVerifyData {
|
||||||
@@ -76,7 +88,7 @@ interface IZarinpalPaymentVerifyData {
|
|||||||
|
|
||||||
export interface ZarinPalPGVerifyData {
|
export interface ZarinPalPGVerifyData {
|
||||||
data: IZarinpalPaymentVerifyData;
|
data: IZarinpalPaymentVerifyData;
|
||||||
error: string[];
|
errors: ZarinpalError[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaymentVerificationResult {
|
export interface PaymentVerificationResult {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { Payment } from "../entities/payment.entity";
|
|||||||
import { DepositRequestStatus } from "../enums/deposit-request-status.enum";
|
import { DepositRequestStatus } from "../enums/deposit-request-status.enum";
|
||||||
import { PaymentStatus } from "../enums/payment-status.enum";
|
import { PaymentStatus } from "../enums/payment-status.enum";
|
||||||
import { TransferType } from "../enums/payment-type.enum";
|
import { TransferType } from "../enums/payment-type.enum";
|
||||||
|
import { ZarinpalErrorCode } from "../enums/zarinpal-error-codes.enum";
|
||||||
import { PaymentGatewayFactory } from "../factories/payment.factory";
|
import { PaymentGatewayFactory } from "../factories/payment.factory";
|
||||||
import { IProcessPaymentParams, IVerifyPayment } from "../interfaces/IPayment";
|
import { IProcessPaymentParams, IVerifyPayment } from "../interfaces/IPayment";
|
||||||
import { BankAccountsRepository } from "../repositories/bank-accounts.repository";
|
import { BankAccountsRepository } from "../repositories/bank-accounts.repository";
|
||||||
@@ -138,7 +139,9 @@ export class PaymentsService {
|
|||||||
|
|
||||||
if (payment.status === PaymentStatus.COMPLETED) throw new BadRequestException(PaymentMessage.VALIDATED_BEFORE);
|
if (payment.status === PaymentStatus.COMPLETED) throw new BadRequestException(PaymentMessage.VALIDATED_BEFORE);
|
||||||
|
|
||||||
if (queryDto.Status !== "OK") await this.handleFailedPaymentRedirect(payment, queryRunner, rep, frontUrl);
|
if (queryDto.Status !== "OK") {
|
||||||
|
return await this.handleFailedPaymentRedirect(payment, queryRunner, rep, frontUrl);
|
||||||
|
}
|
||||||
|
|
||||||
const verifyData = await paymentGateway.verifyPayment({
|
const verifyData = await paymentGateway.verifyPayment({
|
||||||
reference: queryDto.Authority,
|
reference: queryDto.Authority,
|
||||||
@@ -173,22 +176,37 @@ export class PaymentsService {
|
|||||||
//===============================================
|
//===============================================
|
||||||
|
|
||||||
async processVerificationResult(verifyData: IVerifyPayment, payment: Payment, qryRnr: QueryRunner) {
|
async processVerificationResult(verifyData: IVerifyPayment, payment: Payment, qryRnr: QueryRunner) {
|
||||||
if (verifyData.code === 100) {
|
switch (verifyData.code) {
|
||||||
return this.processSuccessfulVerification(payment, verifyData.ref_id, qryRnr);
|
case ZarinpalErrorCode.SUCCESS:
|
||||||
} else if (verifyData.code === 101) {
|
this.logger.log(`Payment ${payment.id} verification successful (code 100)`);
|
||||||
const freshPayment = await qryRnr.manager.findOne(Payment, { where: { id: payment.id } });
|
|
||||||
//
|
|
||||||
if (freshPayment && freshPayment.status === PaymentStatus.COMPLETED) {
|
|
||||||
this.logger.log(`Payment ${payment.id} already marked as COMPLETED in DB (code 101)`);
|
|
||||||
return { status: payment.status, payment };
|
|
||||||
} else {
|
|
||||||
this.logger.log(`Payment ${payment.id} marked as COMPLETED after gateway verification (code 101)`);
|
|
||||||
return await this.processSuccessfulVerification(payment, verifyData.ref_id, qryRnr);
|
return await this.processSuccessfulVerification(payment, verifyData.ref_id, qryRnr);
|
||||||
|
|
||||||
|
case ZarinpalErrorCode.ALREADY_VERIFIED: {
|
||||||
|
const freshPayment = await qryRnr.manager.findOne(Payment, { where: { id: payment.id } });
|
||||||
|
if (freshPayment && freshPayment.status === PaymentStatus.COMPLETED) {
|
||||||
|
this.logger.log(`Payment ${payment.id} already marked as COMPLETED in DB (code 101)`);
|
||||||
|
return { status: payment.status, payment };
|
||||||
|
} else {
|
||||||
|
this.logger.log(`Payment ${payment.id} marked as COMPLETED after gateway verification (code 101)`);
|
||||||
|
return await this.processSuccessfulVerification(payment, verifyData.ref_id, qryRnr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//
|
|
||||||
} else {
|
case ZarinpalErrorCode.SESSION_INVALID:
|
||||||
await this.handleFailedPayment(payment.id, qryRnr);
|
this.logger.warn(`Payment ${payment.id} has invalid/expired session (code -51), marking as failed`);
|
||||||
return { status: payment.status, payment };
|
await this.handleFailedPayment(payment.id, qryRnr);
|
||||||
|
return { status: PaymentStatus.CANCELLED, payment };
|
||||||
|
|
||||||
|
case ZarinpalErrorCode.PAYMENT_NOT_FOUND:
|
||||||
|
this.logger.warn(`Payment ${payment.id} not found in gateway (code -54), marking as failed`);
|
||||||
|
await this.handleFailedPayment(payment.id, qryRnr);
|
||||||
|
return { status: PaymentStatus.CANCELLED, payment };
|
||||||
|
|
||||||
|
default:
|
||||||
|
// For all other error codes, mark as failed
|
||||||
|
this.logger.warn(`Payment ${payment.id} verification failed with code ${verifyData.code}, marking as failed`);
|
||||||
|
await this.handleFailedPayment(payment.id, qryRnr);
|
||||||
|
return { status: PaymentStatus.CANCELLED, payment };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//===============================================
|
//===============================================
|
||||||
@@ -475,7 +493,7 @@ export class PaymentsService {
|
|||||||
await this.handleFailedPayment(payment.id, queryRunner);
|
await this.handleFailedPayment(payment.id, queryRunner);
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
this.addPaymentParamsToUrl(frontUrl, payment, PaymentStatus.FAILED);
|
this.addPaymentParamsToUrl(frontUrl, payment, PaymentStatus.CANCELLED);
|
||||||
|
|
||||||
return rep.status(HttpStatus.FOUND).redirect(frontUrl.toString());
|
return rep.status(HttpStatus.FOUND).redirect(frontUrl.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ export class PaymentProcessor extends WorkerProcessor {
|
|||||||
private async handleFirstReminder(job: Job<{ paymentId: string }>, queryRunner: QueryRunner) {
|
private async handleFirstReminder(job: Job<{ paymentId: string }>, queryRunner: QueryRunner) {
|
||||||
const payment = await this.fetchPayment(job.data.paymentId, queryRunner);
|
const payment = await this.fetchPayment(job.data.paymentId, queryRunner);
|
||||||
|
|
||||||
|
// Check if payment is cancelled - if so, handle deletion
|
||||||
|
if (payment.status === PaymentStatus.CANCELLED) {
|
||||||
|
this.logger.log(`Payment ${payment.id} is cancelled, triggering deletion of related entities`);
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if payment is still pending
|
||||||
const isPending = await this.isPaymentPending(payment, queryRunner);
|
const isPending = await this.isPaymentPending(payment, queryRunner);
|
||||||
|
|
||||||
if (!isPending) {
|
if (!isPending) {
|
||||||
@@ -94,6 +101,13 @@ export class PaymentProcessor extends WorkerProcessor {
|
|||||||
private async handleSecondReminder(job: Job<{ paymentId: string }>, queryRunner: QueryRunner) {
|
private async handleSecondReminder(job: Job<{ paymentId: string }>, queryRunner: QueryRunner) {
|
||||||
const payment = await this.fetchPayment(job.data.paymentId, queryRunner);
|
const payment = await this.fetchPayment(job.data.paymentId, queryRunner);
|
||||||
|
|
||||||
|
// Check if payment is cancelled - if so, handle deletion
|
||||||
|
if (payment.status === PaymentStatus.CANCELLED) {
|
||||||
|
this.logger.log(`Payment ${payment.id} is cancelled, triggering deletion of related entities`);
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if payment is still pending
|
||||||
const isPending = await this.isPaymentPending(payment, queryRunner);
|
const isPending = await this.isPaymentPending(payment, queryRunner);
|
||||||
|
|
||||||
if (!isPending) {
|
if (!isPending) {
|
||||||
@@ -111,6 +125,13 @@ export class PaymentProcessor extends WorkerProcessor {
|
|||||||
private async handlePaymentCancellation(job: Job<{ paymentId: string }>, queryRunner: QueryRunner) {
|
private async handlePaymentCancellation(job: Job<{ paymentId: string }>, queryRunner: QueryRunner) {
|
||||||
const payment = await this.fetchPayment(job.data.paymentId, queryRunner);
|
const payment = await this.fetchPayment(job.data.paymentId, queryRunner);
|
||||||
|
|
||||||
|
// Check if payment is already cancelled - if so, handle deletion
|
||||||
|
if (payment.status === PaymentStatus.CANCELLED) {
|
||||||
|
this.logger.log(`Payment ${payment.id} is already cancelled, triggering deletion of related entities`);
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if payment is still pending
|
||||||
const isPending = await this.isPaymentPending(payment, queryRunner);
|
const isPending = await this.isPaymentPending(payment, queryRunner);
|
||||||
|
|
||||||
if (!isPending) {
|
if (!isPending) {
|
||||||
@@ -118,6 +139,7 @@ export class PaymentProcessor extends WorkerProcessor {
|
|||||||
return payment;
|
return payment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancel the payment and schedule deletion
|
||||||
await this.cancelPayment(payment, queryRunner);
|
await this.cancelPayment(payment, queryRunner);
|
||||||
|
|
||||||
return payment;
|
return payment;
|
||||||
@@ -185,9 +207,9 @@ export class PaymentProcessor extends WorkerProcessor {
|
|||||||
//===============================================
|
//===============================================
|
||||||
|
|
||||||
private async isPaymentPending(payment: Payment, queryRunner: QueryRunner): Promise<boolean> {
|
private async isPaymentPending(payment: Payment, queryRunner: QueryRunner): Promise<boolean> {
|
||||||
// 1. Trust DB first
|
// 1. Trust DB first - only check for PENDING status
|
||||||
if (payment.status !== PaymentStatus.PENDING) {
|
if (payment.status !== PaymentStatus.PENDING) {
|
||||||
this.logger.log(`Payment ${payment.id} is not in pending status`);
|
this.logger.log(`Payment ${payment.id} is not in pending status (current: ${payment.status})`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,18 +221,25 @@ export class PaymentProcessor extends WorkerProcessor {
|
|||||||
|
|
||||||
const result = await this.paymentsService.processVerificationResult(verifyData, payment, queryRunner);
|
const result = await this.paymentsService.processVerificationResult(verifyData, payment, queryRunner);
|
||||||
|
|
||||||
// If still pending or failed, return true (will trigger reminder/cancellation as needed)
|
// If still pending, return true
|
||||||
if (result.status === PaymentStatus.PENDING || result.status === PaymentStatus.FAILED || result.status === PaymentStatus.CANCELLED) {
|
if (result.status === PaymentStatus.PENDING) {
|
||||||
this.logger.log(`Payment ${payment.id} is still pending or failed or cancelled (gateway code: ${verifyData.code})`);
|
this.logger.log(`Payment ${payment.id} is still pending (gateway code: ${verifyData.code})`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If completed, return false
|
||||||
if (result.status === PaymentStatus.COMPLETED) {
|
if (result.status === PaymentStatus.COMPLETED) {
|
||||||
this.logger.log(`Payment ${payment.id} is completed (gateway code: ${verifyData.code})`);
|
this.logger.log(`Payment ${payment.id} is completed (gateway code: ${verifyData.code})`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For any other code, treat as not pending
|
// If cancelled, return false (will be handled by cancellation logic)
|
||||||
|
if (result.status === PaymentStatus.CANCELLED) {
|
||||||
|
this.logger.log(`Payment ${payment.id} is cancelled (gateway code: ${verifyData.code})`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For any other status, treat as not pending
|
||||||
this.logger.log(`Payment ${payment.id} gateway returned code ${verifyData.code}, treating as not pending`);
|
this.logger.log(`Payment ${payment.id} gateway returned code ${verifyData.code}, treating as not pending`);
|
||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user