From a65ad8b5ffdbf04804b5b7767c66c7c85f7f73f2 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sat, 3 Jan 2026 17:35:18 +0330 Subject: [PATCH] refactor : payment --- src/IOC/ioc.config.ts | 4 +- src/IOC/ioc.types.ts | 1 - src/common/enums/message.enum.ts | 1 + src/common/enums/payment.enum.ts | 1 - src/modules/IPG/PaymentGateway.ts | 66 ++++---- src/modules/IPG/gateways/asanpardakht.ts | 141 ------------------ src/modules/IPG/gateways/sep.ts | 42 ++++-- src/modules/IPG/gateways/zarinpal.ts | 7 +- src/modules/IPG/interface/IPG.ts | 9 +- .../payment/DTO/verifyOnlinePayment.dto.ts | 14 ++ .../payment/models/Abstraction/IPayments.ts | 4 +- src/modules/payment/models/payments.model.ts | 4 +- src/modules/payment/payment.controller.ts | 21 +++ .../payment/providers/PR-payment.service.ts | 27 +++- .../payment/providers/payment.service.ts | 41 +++-- 15 files changed, 159 insertions(+), 224 deletions(-) delete mode 100644 src/modules/IPG/gateways/asanpardakht.ts create mode 100644 src/modules/payment/DTO/verifyOnlinePayment.dto.ts diff --git a/src/IOC/ioc.config.ts b/src/IOC/ioc.config.ts index 9a54bb4..c8ac385 100644 --- a/src/IOC/ioc.config.ts +++ b/src/IOC/ioc.config.ts @@ -67,9 +67,8 @@ import { FaqService } from "../modules/faq/faq.service"; import { FineService } from "../modules/fine/fine.service"; import { FineRepo, createFineRepo } from "../modules/fine/repository/fine.repository"; import { FineRuleRepo, createFineRuleRepo } from "../modules/fine/repository/fineRule.repository"; -import { AsanPardakhtGateway } from "../modules/IPG/gateways/asanpardakht"; -import { ZarinPalGateway } from "../modules/IPG/gateways/zarinpal"; import { SepGateway } from "../modules/IPG/gateways/sep"; +import { ZarinPalGateway } from "../modules/IPG/gateways/zarinpal"; import { PaymentGateway } from "../modules/IPG/PaymentGateway"; import { JobService } from "../modules/job/job.service"; import { JobRepo, createJobRepo } from "../modules/job/repository/job.repo"; @@ -159,7 +158,6 @@ const containerModules = new AsyncContainerModule(async (bind) => { // bind(IOCTYPES.PassportAuth).to(PassportAuth).inSingletonScope(); bind(IOCTYPES.PaymentGateway).to(PaymentGateway).inSingletonScope(); bind(IOCTYPES.ZarinPalGateway).to(ZarinPalGateway).inSingletonScope(); - bind(IOCTYPES.AsanPardakhtGateway).to(AsanPardakhtGateway).inSingletonScope(); bind(IOCTYPES.SEPGateway).to(SepGateway).inSingletonScope(); // #region services bind(IOCTYPES.CategoryService).to(CategoryService).inSingletonScope(); diff --git a/src/IOC/ioc.types.ts b/src/IOC/ioc.types.ts index 21efde4..7a13470 100644 --- a/src/IOC/ioc.types.ts +++ b/src/IOC/ioc.types.ts @@ -139,7 +139,6 @@ export const IOCTYPES = { // #endregion Logger: Symbol.for("Logger"), ZarinPalGateway: Symbol.for("ZarinPalGateway"), - AsanPardakhtGateway: Symbol.for("AsanPardakhtGateway"), SEPGateway: Symbol.for("SEPGateway"), PaymentGateway: Symbol.for("PaymentGateway"), }; diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index bb4ecc6..ed5b558 100644 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -197,6 +197,7 @@ export const enum PaymentMessage { CartPaymentDesc = "خرید کالا:", PaymentNotBelongToUser = "پرداخت برای این کاربر نمی‌باشد", PaymentRequestFailed = "درخواست پرداخت موفقیت آمیز نبود", + RefNumIsAlreadyUsed = "این شماره رسید قبلا برای تایید استفاده شده .", } export const enum CouponMessage { diff --git a/src/common/enums/payment.enum.ts b/src/common/enums/payment.enum.ts index 7245994..5b85970 100644 --- a/src/common/enums/payment.enum.ts +++ b/src/common/enums/payment.enum.ts @@ -1,6 +1,5 @@ export enum GatewayProvider { Zarinpal = "Zarinpal", - Asanpardakht = "asanPardakht", SEP = "SEP", } diff --git a/src/modules/IPG/PaymentGateway.ts b/src/modules/IPG/PaymentGateway.ts index d5c0572..40a0c78 100644 --- a/src/modules/IPG/PaymentGateway.ts +++ b/src/modules/IPG/PaymentGateway.ts @@ -1,59 +1,49 @@ import { inject, injectable } from "inversify"; -import { AsanPardakhtGateway } from "./gateways/asanpardakht"; -import { ZarinPalGateway } from "./gateways/zarinpal"; import { SepGateway } from "./gateways/sep"; -import { IPaymentGateway, NewPaymentData, VerifyPaymentData } from "./interface/IPG"; +import { ZarinPalGateway } from "./gateways/zarinpal"; +import { IPaymentGateway, NewPaymentData, VerifyPaymentData, IGateway } from "./interface/IPG"; import { GatewayProvider } from "../../common/enums/payment.enum"; import { BadRequestError } from "../../core/app/app.errors"; import { IOCTYPES } from "../../IOC/ioc.types"; @injectable() class PaymentGateway implements IPaymentGateway { - @inject(IOCTYPES.ZarinPalGateway) zarinPalGateway: ZarinPalGateway; - @inject(IOCTYPES.AsanPardakhtGateway) asanpardakhtGateway: AsanPardakhtGateway; - @inject(IOCTYPES.SEPGateway) sepGateway: SepGateway; + @inject(IOCTYPES.ZarinPalGateway) private zarinPalGateway: ZarinPalGateway; + @inject(IOCTYPES.SEPGateway) private sepGateway: SepGateway; + + private readonly gateways: Map = new Map(); + + constructor() { + this.gateways.set(GatewayProvider.Zarinpal, this.zarinPalGateway); + this.gateways.set(GatewayProvider.SEP, this.sepGateway); + } public async requestPayment(gatewayType: GatewayProvider, data: NewPaymentData): Promise { - switch (gatewayType) { - case GatewayProvider.Zarinpal: - return await this.zarinPalGateway.processPayment(data.amount, data.description, data.email, data.mobile, data.callbackPath); - case GatewayProvider.Asanpardakht: - //TODO:fix this - return this.asanpardakhtGateway.token(); - case GatewayProvider.SEP: - return await this.sepGateway.processPayment(data.amount, data.description, data.email, data.mobile, data.callbackPath); - default: - throw new BadRequestError(`Payment provider ${gatewayType} is not supported`); - } + const gateway = this.getGateway(gatewayType); + return await gateway.processPayment(data.amount, data.description, data.email, data.mobile, data.callbackPath); } public async verifyPayment(gatewayType: GatewayProvider, data: VerifyPaymentData): Promise { - switch (gatewayType) { - case GatewayProvider.Zarinpal: - return await this.zarinPalGateway.verifyPayment(data.authority, data.amount); - case GatewayProvider.Asanpardakht: - //TODO:fix this - return this.asanpardakhtGateway.time(); - case GatewayProvider.SEP: - return await this.sepGateway.verifyPayment(data.authority, data.amount); - default: - throw new BadRequestError(`Payment provider ${gatewayType} is not supported`); - } + const gateway = this.getGateway(gatewayType); + const verifyData = gatewayType === GatewayProvider.SEP + ? { refNum: data.refNum, amount: data.amount } + : { authority: data.authority, amount: data.amount }; + + return await gateway.verifyPayment(verifyData); } public async retryPayment(gatewayType: GatewayProvider, authority: string) { - switch (gatewayType) { - case GatewayProvider.Zarinpal: - return await this.zarinPalGateway.retryPayment(authority); - case GatewayProvider.Asanpardakht: - //TODO:fix this - return this.asanpardakhtGateway.token(); - case GatewayProvider.SEP: - return await this.sepGateway.retryPayment(authority); - default: - throw new BadRequestError(`Payment provider ${gatewayType} is not supported`); + const gateway = this.getGateway(gatewayType); + return await gateway.retryPayment(authority); + } + + private getGateway(gatewayType: GatewayProvider): IGateway { + const gateway = this.gateways.get(gatewayType); + if (!gateway) { + throw new BadRequestError(`Payment provider ${gatewayType} is not supported`); } + return gateway; } } diff --git a/src/modules/IPG/gateways/asanpardakht.ts b/src/modules/IPG/gateways/asanpardakht.ts deleted file mode 100644 index 157e873..0000000 --- a/src/modules/IPG/gateways/asanpardakht.ts +++ /dev/null @@ -1,141 +0,0 @@ -import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; -import { injectable } from "inversify"; - -@injectable() -class AsanPardakhtGateway { - private invoiceId: string | null = null; - private amount: number | null = null; - private callBackURL = `${process.env.SITE_URL}/verify/ap`; - - private readonly GatewayAuth = { - user: process.env.ASANPARDAKHT_USER, - password: process.env.ASANPARDAKHT_PASS, - merchant_id: process.env.ASANPARDAKHT_MERCHANT_ID, - merchantConfigID: process.env.ASANPARDAKHT_MERCHANT_CONFIG_ID, - }; - - // API endpoint URLs - private readonly TokenURL = "v1/Token"; - private readonly TimeURL = "v1/Time"; - private readonly TranResultURL = "v1/TranResult"; - private readonly CardHashURL = "v1/CardHash"; - private readonly SettlementURL = "v1/Settlement"; - private readonly VerifyURL = "v1/Verify"; - private readonly CancelURL = "v1/Cancel"; - private readonly ReverseURL = "v1/Reverse"; - private readonly GatewayApiURL = "https://ipgrest.asanpardakht.ir"; - - setInvoiceId(id: string): this { - this.invoiceId = id; - return this; - } - - setAmount(amount: number): this { - this.amount = amount; - return this; - } - - async time(): Promise<{ code: number; content: any }> { - return await this.callAPI("GET", this.TimeURL); - } - - async TranResult(): Promise<{ code: number; content: any }> { - const res = await this.callAPI( - "GET", - `${this.TranResultURL}?${new URLSearchParams({ - merchantConfigurationId: this.GatewayAuth.merchantConfigID!, - localInvoiceId: this.invoiceId!, - }).toString()}`, - ); - return { code: res.code, content: JSON.parse(res.content) }; - } - - async CardHash(): Promise<{ code: number; content: any }> { - return await this.callAPI( - "GET", - `${this.CardHashURL}?${new URLSearchParams({ - merchantConfigurationId: this.GatewayAuth.merchantConfigID!, - localInvoiceId: this.invoiceId!, - }).toString()}`, - ); - } - - async token(): Promise<{ code: number; content: any }> { - if (this.callBackURL) { - this.callBackURL += this.callBackURL.includes("?") ? "&" : "?"; - } - const currentDate = new Date().toISOString().split("T").join(" ").split(".")[0].replace(/[-:]/g, ""); - return await this.callAPI("POST", this.TokenURL, { - serviceTypeId: 1, - merchantConfigurationId: this.GatewayAuth.merchantConfigID!, - localInvoiceId: this.invoiceId!, - amountInRials: this.amount!, - localDate: currentDate, - callbackURL: `${this.callBackURL}${new URLSearchParams({ invoice: this.invoiceId! }).toString()}`, - paymentId: 0, - additionalData: "", - }); - } - - async settlement(transId: string): Promise<{ code: number; content: any }> { - return await this.callAPI("POST", this.SettlementURL, { - merchantConfigurationId: this.GatewayAuth.merchantConfigID!, - payGateTranId: transId, - }); - } - - async verify(transId: string): Promise<{ code: number; content: any }> { - return await this.callAPI("POST", this.VerifyURL, { - merchantConfigurationId: this.GatewayAuth.merchantConfigID!, - payGateTranId: transId, - }); - } - - async reverse(transId: string): Promise<{ code: number; content: any }> { - return await this.callAPI("POST", this.ReverseURL, { - merchantConfigurationId: this.GatewayAuth.merchantConfigID!, - payGateTranId: transId, - }); - } - - async cancel(transId: string): Promise<{ code: number; content: any }> { - return await this.callAPI("POST", this.CancelURL, { - merchantConfigurationId: this.GatewayAuth.merchantConfigID!, - payGateTranId: transId, - }); - } - - /** - * Internal method to call the API. - * @param method - The HTTP method to use. - * @param endpoint - The API endpoint to call. - * @param data - Optional data to send with the request. - * @returns A promise resolving to the API response. - */ - private async callAPI(method: "GET" | "POST", endpoint: string, data: any = null): Promise<{ code: number; content: any }> { - const url = `${this.GatewayApiURL}/${endpoint}`; - const options: AxiosRequestConfig = { - method, - url, - headers: { - Accept: "application/json", - Usr: this.GatewayAuth.user || "", - Pwd: this.GatewayAuth.password || "", - "Content-Type": "application/json", - }, - data: data ? JSON.stringify(data) : null, - }; - - try { - const response: AxiosResponse = await axios(options); - return { content: response.data, code: response.status }; - } catch (error: any) { - if (error.response) { - return { content: error.response.data, code: error.response.status }; - } else { - return { content: error.message, code: 500 }; - } - } - } -} -export { AsanPardakhtGateway }; diff --git a/src/modules/IPG/gateways/sep.ts b/src/modules/IPG/gateways/sep.ts index 7dc6536..14282e1 100644 --- a/src/modules/IPG/gateways/sep.ts +++ b/src/modules/IPG/gateways/sep.ts @@ -3,38 +3,54 @@ import { injectable } from "inversify"; import { InternalError } from "../../../core/app/app.errors"; import { Logger } from "../../../core/logging/logger"; +import { IGateway } from "../interface/IPG"; interface SepRequestPaymentResponse { status: 1 | -1; errorCode?: string; errorDesc?: string; token?: string; } +interface SepVerifyPaymentResponse { + TransactionDetail: { + "RRN": string, + "RefNum": string, + "MaskedPan": string, + "HashedPan": string, + "TerminalNumber": number, + "OrginalAmount": number, + "AffectiveAmount": number, + "StraceDate": string, + "StraceNo": string, + }, + "ResultCode": number, + "ResultDescription": string, + "Success": boolean +} @injectable() -class SepGateway { +class SepGateway implements IGateway { private logger = new Logger(SepGateway.name); private gatewayApiUrl: string = process.env.SEP_HOST || "https://sep.shaparak.ir"; private callBackURL = `https://api.fajrtabloshop.com/payment/verify/SEP/`; - private TERMINAL_ID: string = process.env.SEP_TERMINAL_ID || "15364098"; + private terminalNumber: string = process.env.SEP_TERMINAL_NUMBER || "15364098"; private requestHeader = { "Content-Type": "application/json", Accept: "application/json" }; - async processPayment(amount: number, description: string, email: string, mobile: string, callBackPath: string) { - if (!this.gatewayApiUrl || !this.TERMINAL_ID) { + async processPayment(amount: number, _description: string, _email: string, _mobile: string, callBackPath: string) { + if (!this.gatewayApiUrl || !this.terminalNumber) { throw new InternalError(["SEP gateway is not configured"]); } await this.checkOutgoingIp(); const requesturl = `${this.gatewayApiUrl}/onlinepg/onlinepg`; - console.log("SEP Gateway processPayment called", email, description); try { const purchaseData = { Action: "Token", - TerminalId: this.TERMINAL_ID, + TerminalId: this.terminalNumber, Amount: amount, // ریال RedirectUrl: `${this.callBackURL}${callBackPath}`, ResNum: Date.now().toString(), - CellNumber: mobile, + CellNumber: _mobile, }; const response = await axios.post(requesturl, purchaseData, { headers: this.requestHeader }); @@ -58,22 +74,24 @@ class SepGateway { async checkOutgoingIp() { try { const response = await axios.get("https://api.ipify.org?format=json"); - console.log("Your Server's Public Outgoing IP is:", response.data.ip); return response.data.ip; } catch (error) { console.error("Could not determine outgoing IP", error); } } - async verifyPayment(authority: string, amount: number) { - if (!this.gatewayApiUrl || !this.TERMINAL_ID) { + async verifyPayment(data: { authority?: string; refNum?: string; amount: number }) { + if (!this.gatewayApiUrl || !this.terminalNumber) { throw new InternalError(["SEP gateway is not configured"]); } // https://sep.shaparak.ir/verifyTxnRandomSessionkey/ipg/VerifyTransaction try { - const verifyData = { authority, amount, terminal_id: this.TERMINAL_ID }; - const response = await axios.post(`${this.gatewayApiUrl}/payment/verify`, verifyData, { headers: this.requestHeader }); + const verifyData = { RefNum: data.refNum, TerminalNumber: this.terminalNumber }; + const response = await axios.post(`${this.gatewayApiUrl}/verifyTxnRandomSessionkey/ipg/VerifyTransaction`, + verifyData, + { headers: this.requestHeader } + ); return response.data; } catch (error) { this.logger.error("error in SEP payment verification", error); diff --git a/src/modules/IPG/gateways/zarinpal.ts b/src/modules/IPG/gateways/zarinpal.ts index f164343..6203efe 100644 --- a/src/modules/IPG/gateways/zarinpal.ts +++ b/src/modules/IPG/gateways/zarinpal.ts @@ -4,9 +4,10 @@ import { injectable } from "inversify"; import { ZarinPalPGNewArgs, ZarinPalPGNewRequestData, ZarinPalPGVerifyData } from "../../../common/types/paymentGateway.type"; import { InternalError } from "../../../core/app/app.errors"; import { Logger } from "../../../core/logging/logger"; +import { IGateway } from "../interface/IPG"; @injectable() -class ZarinPalGateway { +class ZarinPalGateway implements IGateway { private logger = new Logger(ZarinPalGateway.name); private gatewayApiUrl: string = `https://${process.env.IPG_TYPE}.zarinpal.com/pg`; //or api subdomain @@ -39,10 +40,10 @@ class ZarinPalGateway { } } - async verifyPayment(authority: string, amount: number) { + async verifyPayment(verifyParams: { authority?: string; refNum?: string; amount: number }) { try { //TODO:check if amount should be in toman or riyal - const verifyData = { authority, amount, merchant_id: this.MERCHANT_ID }; + const verifyData = { authority: verifyParams.authority, amount: verifyParams.amount, merchant_id: this.MERCHANT_ID }; const response = await axios.post(`${this.gatewayApiUrl}/v4/payment/verify.json`, verifyData, { headers: this.requestHeader }); const { data } = response.data as ZarinPalPGVerifyData; diff --git a/src/modules/IPG/interface/IPG.ts b/src/modules/IPG/interface/IPG.ts index df5d23f..48b8670 100644 --- a/src/modules/IPG/interface/IPG.ts +++ b/src/modules/IPG/interface/IPG.ts @@ -23,6 +23,12 @@ export interface IPaymentGateway { retryPayment(gatewayType: GatewayProvider, authority: string): Promise; } +export interface IGateway { + processPayment(amount: number, description: string, email: string, mobile: string, callBackPath: string): Promise; + verifyPayment(data: { authority?: string; refNum?: string; amount: number }): Promise; + retryPayment(authority: string): Promise; +} + export interface NewPaymentData { amount: number; description: string; @@ -32,6 +38,7 @@ export interface NewPaymentData { } export interface VerifyPaymentData { - authority: string; amount: number; + authority?: string; + refNum?:string } diff --git a/src/modules/payment/DTO/verifyOnlinePayment.dto.ts b/src/modules/payment/DTO/verifyOnlinePayment.dto.ts new file mode 100644 index 0000000..86d7f71 --- /dev/null +++ b/src/modules/payment/DTO/verifyOnlinePayment.dto.ts @@ -0,0 +1,14 @@ +export class VerifyOnlinePaymentDTO { + MID: string; + State: string; + Status: string; + RRN: string; + RefNum: string; + ResNum: string; + TerminalId: string; + TraceNo: string; + Amount: string; + Wage: string; + SecurePan: string; + HashedCardNumber: string; +} diff --git a/src/modules/payment/models/Abstraction/IPayments.ts b/src/modules/payment/models/Abstraction/IPayments.ts index b36372f..e86680c 100644 --- a/src/modules/payment/models/Abstraction/IPayments.ts +++ b/src/modules/payment/models/Abstraction/IPayments.ts @@ -7,7 +7,7 @@ export interface ICartPayment { _id: Types.ObjectId; user_id: Types.ObjectId; authority: string; - transaction_id: number; + transaction_id: string | null; payment_method: Types.ObjectId; priceDetails: IPriceDetails; totalPrice: number; @@ -34,7 +34,7 @@ export interface IProductRequestPayment { _id: Types.ObjectId; seller_id: Types.ObjectId; authority: string; - transaction_id: number; + transaction_id: string | null; payment_method: Types.ObjectId; priceDetails: IPRPriceDetails; totalPrice: number; diff --git a/src/modules/payment/models/payments.model.ts b/src/modules/payment/models/payments.model.ts index a195311..8a51f1b 100644 --- a/src/modules/payment/models/payments.model.ts +++ b/src/modules/payment/models/payments.model.ts @@ -19,7 +19,7 @@ const CartPaymentSchema = new Schema( { user_id: { type: Schema.Types.ObjectId, ref: "User", required: true }, authority: { type: String, required: true }, - transaction_id: { type: Number, default: null }, + transaction_id: { type: String, default: null }, payment_method: { type: Schema.Types.ObjectId, ref: "PaymentMethod", required: true }, priceDetails: { type: PaymentPriceDetails, required: true }, totalPrice: { type: Number, required: true }, @@ -46,7 +46,7 @@ const PRPaymentSchema = new Schema( { seller_id: { type: Schema.Types.ObjectId, ref: "Seller", required: true }, authority: { type: String, required: true }, - transaction_id: { type: Number, default: null }, + transaction_id: { type: String, default: null }, payment_method: { type: Schema.Types.ObjectId, ref: "PaymentMethod", required: true }, priceDetails: { type: PRPriceDetailSchema }, totalPrice: { type: Number, required: true }, diff --git a/src/modules/payment/payment.controller.ts b/src/modules/payment/payment.controller.ts index 640b56f..674e162 100644 --- a/src/modules/payment/payment.controller.ts +++ b/src/modules/payment/payment.controller.ts @@ -6,6 +6,7 @@ import { PaymentService } from "./providers/payment.service"; import { HttpStatus } from "../../common"; import { CartCheckoutDTO } from "./DTO/cartCheckout.dto"; import { PRCheckoutDTO } from "./DTO/PRCheckout.dto"; +import { VerifyOnlinePaymentDTO } from "./DTO/verifyOnlinePayment.dto"; import { PRPaymentService } from "./providers/PR-payment.service"; import { BaseController } from "../../common/base/controller"; import { ApiAuth, ApiModel, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs"; @@ -104,6 +105,26 @@ class PaymentController extends BaseController { // return this.response(data); } + @ApiModel(VerifyOnlinePaymentDTO) + @ApiOperation("verify a payment for current session user for cartCheckout (method post for SEP)") + @ApiResponse("successful", HttpStatus.Ok) + @ApiParam("gateway", "the name of api gateway", true) + @httpPost("/verify/:gateway/cart") + public async VerifyCartCheckoutPost( + @requestParam("gateway") gateway: string, + @response() res: Response, + @requestBody() dto: VerifyOnlinePaymentDTO, + ) { + const verifyCallbackData = { + status: dto.Status, + refNum: dto.RefNum, + }; + const data = await this.paymentService.verifyCartPayment(gateway as GatewayProvider, verifyCallbackData); + const html = verifyPaymentTemplate(data); + return res.send(html); + // return this.response(data); + } + @ApiOperation("verify a payment for current session seller for product request checkout") @ApiResponse("successful", HttpStatus.Ok) @ApiParam("gateway", "the name of api gateway", true) diff --git a/src/modules/payment/providers/PR-payment.service.ts b/src/modules/payment/providers/PR-payment.service.ts index aadd185..7f19966 100644 --- a/src/modules/payment/providers/PR-payment.service.ts +++ b/src/modules/payment/providers/PR-payment.service.ts @@ -110,13 +110,23 @@ export class PRPaymentService { //################################################################## //################################################################## - async verifyPRPayment(gateway: GatewayProvider, verifyCallbackData: { status: string; authority: string }) { - const { status, authority } = verifyCallbackData; + async verifyPRPayment(gateway: GatewayProvider, verifyCallbackData: { status: string; authority?: string; refNum?: string }) { + const { status, authority, refNum } = verifyCallbackData; const session = await startSession(); session.startTransaction(); try { - const paymentData = await this.findPaymentByAuthority(authority, session); + // For SEP gateway, refNum is used as authority in database + const authorityToFind = gateway === GatewayProvider.SEP ? refNum : authority; + if (!authorityToFind) throw new BadRequestError(PaymentMessage.PaymentNotFound); + + // For SEP gateway, check if refNum has already been used as transaction_id + if (gateway === GatewayProvider.SEP && refNum) { + const existingPayment = await this.PRPaymentRepo.model.findOne({ transaction_id: refNum }).session(session); + if (existingPayment) throw new BadRequestError(PaymentMessage.RefNumIsAlreadyUsed); + } + + const paymentData = await this.findPaymentByAuthority(authorityToFind, session); const productRequest = await this.findProductRequestByPaymentId(paymentData._id.toString(), session); const PRId = productRequest._id.toString(); @@ -125,9 +135,12 @@ export class PRPaymentService { return this.buildPaymentResponse(status, PaymentMessage.PaymentNotSuccessful, paymentData._id.toString(), PRId); } - const verificationResult = await this.verifyPaymentWithGateway(gateway, authority, paymentData.totalPrice); + const verificationResult = await this.verifyPaymentWithGateway(gateway, authorityToFind, paymentData.totalPrice); if (verificationResult.success) { - await this.handleSuccessPayment(paymentData._id.toString(), verificationResult.refId!, session); + // For SEP gateway, save refNum as transaction_id; for Zarinpal, save ref_id + const transactionId = gateway === GatewayProvider.SEP ? refNum : verificationResult.refId; + if (!transactionId) throw new BadRequestError(PaymentMessage.PaymentNotSuccessful); + await this.handleSuccessPayment(paymentData._id.toString(), transactionId, session); return this.buildPaymentResponse(status, PaymentMessage.PaymentSuccessful, paymentData._id.toString(), PRId); } @@ -161,10 +174,10 @@ export class PRPaymentService { } /******************************/ - private async handleSuccessPayment(paymentId: string, refId: string, session: ClientSession) { + private async handleSuccessPayment(paymentId: string, transactionId: string, session: ClientSession) { await this.PRPaymentRepo.model.findByIdAndUpdate( paymentId, - { paymentStatus: PaymentStatus.Completed, transaction_id: refId }, + { paymentStatus: PaymentStatus.Completed, transaction_id: transactionId }, { session }, ); await session.commitTransaction(); diff --git a/src/modules/payment/providers/payment.service.ts b/src/modules/payment/providers/payment.service.ts index 66a9b99..17b0443 100644 --- a/src/modules/payment/providers/payment.service.ts +++ b/src/modules/payment/providers/payment.service.ts @@ -140,12 +140,11 @@ class PaymentService { // extracted to handle payment creation const { payment, paymentData } = await this.handlePaymentCreation(paymentMethod, price, user, description, session); - // create order and order items const { order, orderItems } = await this.orderService.createOrder(user, payment, cartShipmentItems, session); - // clear cart and adjust stock - await this.finalizeOrder(user, cartShipmentItems, session); - + // clear cart and adjust stock + await this.finalizeOrder(user, cartShipmentItems, session); + await session.commitTransaction(); await session.endSession(); return { paymentData, order, orderItems }; @@ -161,12 +160,22 @@ class PaymentService { //################################################################## //################################################################## //verify callback of cart payment - async verifyCartPayment(gateway: GatewayProvider, verifyCallbackData: { authority: string; status: string }) { - const { status, authority } = verifyCallbackData; + async verifyCartPayment(gateway: GatewayProvider, verifyCallbackData: { status: string; authority?: string; refNum?: string }) { + const { status, authority, refNum } = verifyCallbackData; const session = await startSession(); session.startTransaction(); try { - const paymentData = await this.cartPaymentRepo.model.findOne({ authority }).session(session); + // For SEP gateway, refNum is used as authority in database + const authorityToFind = gateway === GatewayProvider.SEP ? refNum : authority; + if (!authorityToFind) throw new BadRequestError(PaymentMessage.PaymentNotFound); + + // For SEP gateway, check if refNum has already been used as transaction_id + if (gateway === GatewayProvider.SEP && refNum) { + const existingPayment = await this.cartPaymentRepo.model.findOne({ transaction_id: refNum }).session(session); + if (existingPayment) throw new BadRequestError(PaymentMessage.RefNumIsAlreadyUsed); + } + + const paymentData = await this.cartPaymentRepo.model.findOne({ authority: authorityToFind }).session(session); if (!paymentData) throw new BadRequestError(PaymentMessage.PaymentNotFound); @@ -185,10 +194,16 @@ class PaymentService { } // Verify payment through gateway - const data = await this.paymentGateway.verifyPayment(gateway, { authority, amount: paymentData.totalPrice }); + const verifyData = + gateway === GatewayProvider.SEP + ? { refNum: authority, amount: paymentData.totalPrice } + : { authority, amount: paymentData.totalPrice }; + const data = await this.paymentGateway.verifyPayment(gateway, verifyData); if (data.code === 100 || data.code === 101) { await this.handleSellerNotify(order._id, user, session); - await this.handleSuccessfulPayment(paymentData._id.toString(), order._id, data.ref_id, session); + // For SEP gateway, save refNum as transaction_id; for Zarinpal, save ref_id + const transactionId = gateway === GatewayProvider.SEP ? refNum : data.ref_id; + await this.handleSuccessfulPayment(paymentData._id.toString(), order._id, transactionId, session); return this.buildPaymentResponse(status, PaymentMessage.PaymentSuccessful, paymentData._id.toString(), order._id); } @@ -279,8 +294,8 @@ class PaymentService { } //################################################################## //################################################################## - private async handleSuccessfulPayment(paymentId: string, orderId: number, refId: number, session: ClientSession) { - await this.updatePaymentStatusAndReference(paymentId, refId, PaymentStatus.Completed, session); + private async handleSuccessfulPayment(paymentId: string, orderId: number, transactionId: string, session: ClientSession) { + await this.updatePaymentStatusAndReference(paymentId, transactionId, PaymentStatus.Completed, session); await this.orderService.updateOrderStatus(orderId, OrdersStatus.process_by_seller, session); @@ -385,8 +400,8 @@ class PaymentService { } //################################################################## //################################################################## - private async updatePaymentStatusAndReference(paymentId: string, refId: number | null, status: PaymentStatus, session: ClientSession) { - return await this.cartPaymentRepo.model.findByIdAndUpdate(paymentId, { paymentStatus: status, transaction_id: refId }, { session }); + private async updatePaymentStatusAndReference(paymentId: string, transactionId: string | null, status: PaymentStatus, session: ClientSession) { + return await this.cartPaymentRepo.model.findByIdAndUpdate(paymentId, { paymentStatus: status, transaction_id: transactionId }, { session }); } }