refactor : payment

This commit is contained in:
2026-01-03 17:35:18 +03:30
parent 60f7b46779
commit a65ad8b5ff
15 changed files with 159 additions and 224 deletions
+1 -3
View File
@@ -67,9 +67,8 @@ import { FaqService } from "../modules/faq/faq.service";
import { FineService } from "../modules/fine/fine.service"; import { FineService } from "../modules/fine/fine.service";
import { FineRepo, createFineRepo } from "../modules/fine/repository/fine.repository"; import { FineRepo, createFineRepo } from "../modules/fine/repository/fine.repository";
import { FineRuleRepo, createFineRuleRepo } from "../modules/fine/repository/fineRule.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 { SepGateway } from "../modules/IPG/gateways/sep";
import { ZarinPalGateway } from "../modules/IPG/gateways/zarinpal";
import { PaymentGateway } from "../modules/IPG/PaymentGateway"; import { PaymentGateway } from "../modules/IPG/PaymentGateway";
import { JobService } from "../modules/job/job.service"; import { JobService } from "../modules/job/job.service";
import { JobRepo, createJobRepo } from "../modules/job/repository/job.repo"; 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.PassportAuth).to(PassportAuth).inSingletonScope();
bind<PaymentGateway>(IOCTYPES.PaymentGateway).to(PaymentGateway).inSingletonScope(); bind<PaymentGateway>(IOCTYPES.PaymentGateway).to(PaymentGateway).inSingletonScope();
bind<ZarinPalGateway>(IOCTYPES.ZarinPalGateway).to(ZarinPalGateway).inSingletonScope(); bind<ZarinPalGateway>(IOCTYPES.ZarinPalGateway).to(ZarinPalGateway).inSingletonScope();
bind<AsanPardakhtGateway>(IOCTYPES.AsanPardakhtGateway).to(AsanPardakhtGateway).inSingletonScope();
bind<SepGateway>(IOCTYPES.SEPGateway).to(SepGateway).inSingletonScope(); bind<SepGateway>(IOCTYPES.SEPGateway).to(SepGateway).inSingletonScope();
// #region services // #region services
bind<CategoryService>(IOCTYPES.CategoryService).to(CategoryService).inSingletonScope(); bind<CategoryService>(IOCTYPES.CategoryService).to(CategoryService).inSingletonScope();
-1
View File
@@ -139,7 +139,6 @@ export const IOCTYPES = {
// #endregion // #endregion
Logger: Symbol.for("Logger"), Logger: Symbol.for("Logger"),
ZarinPalGateway: Symbol.for("ZarinPalGateway"), ZarinPalGateway: Symbol.for("ZarinPalGateway"),
AsanPardakhtGateway: Symbol.for("AsanPardakhtGateway"),
SEPGateway: Symbol.for("SEPGateway"), SEPGateway: Symbol.for("SEPGateway"),
PaymentGateway: Symbol.for("PaymentGateway"), PaymentGateway: Symbol.for("PaymentGateway"),
}; };
+1
View File
@@ -197,6 +197,7 @@ export const enum PaymentMessage {
CartPaymentDesc = "خرید کالا:", CartPaymentDesc = "خرید کالا:",
PaymentNotBelongToUser = "پرداخت برای این کاربر نمی‌باشد", PaymentNotBelongToUser = "پرداخت برای این کاربر نمی‌باشد",
PaymentRequestFailed = "درخواست پرداخت موفقیت آمیز نبود", PaymentRequestFailed = "درخواست پرداخت موفقیت آمیز نبود",
RefNumIsAlreadyUsed = "این شماره رسید قبلا برای تایید استفاده شده .",
} }
export const enum CouponMessage { export const enum CouponMessage {
-1
View File
@@ -1,6 +1,5 @@
export enum GatewayProvider { export enum GatewayProvider {
Zarinpal = "Zarinpal", Zarinpal = "Zarinpal",
Asanpardakht = "asanPardakht",
SEP = "SEP", SEP = "SEP",
} }
+28 -38
View File
@@ -1,59 +1,49 @@
import { inject, injectable } from "inversify"; import { inject, injectable } from "inversify";
import { AsanPardakhtGateway } from "./gateways/asanpardakht";
import { ZarinPalGateway } from "./gateways/zarinpal";
import { SepGateway } from "./gateways/sep"; 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 { GatewayProvider } from "../../common/enums/payment.enum";
import { BadRequestError } from "../../core/app/app.errors"; import { BadRequestError } from "../../core/app/app.errors";
import { IOCTYPES } from "../../IOC/ioc.types"; import { IOCTYPES } from "../../IOC/ioc.types";
@injectable() @injectable()
class PaymentGateway implements IPaymentGateway { class PaymentGateway implements IPaymentGateway {
@inject(IOCTYPES.ZarinPalGateway) zarinPalGateway: ZarinPalGateway; @inject(IOCTYPES.ZarinPalGateway) private zarinPalGateway: ZarinPalGateway;
@inject(IOCTYPES.AsanPardakhtGateway) asanpardakhtGateway: AsanPardakhtGateway; @inject(IOCTYPES.SEPGateway) private sepGateway: SepGateway;
@inject(IOCTYPES.SEPGateway) sepGateway: SepGateway;
private readonly gateways: Map<GatewayProvider, IGateway> = 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<any> { public async requestPayment(gatewayType: GatewayProvider, data: NewPaymentData): Promise<any> {
switch (gatewayType) { const gateway = this.getGateway(gatewayType);
case GatewayProvider.Zarinpal: return await gateway.processPayment(data.amount, data.description, data.email, data.mobile, data.callbackPath);
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`);
}
} }
public async verifyPayment(gatewayType: GatewayProvider, data: VerifyPaymentData): Promise<any> { public async verifyPayment(gatewayType: GatewayProvider, data: VerifyPaymentData): Promise<any> {
switch (gatewayType) { const gateway = this.getGateway(gatewayType);
case GatewayProvider.Zarinpal: const verifyData = gatewayType === GatewayProvider.SEP
return await this.zarinPalGateway.verifyPayment(data.authority, data.amount); ? { refNum: data.refNum, amount: data.amount }
case GatewayProvider.Asanpardakht: : { authority: data.authority, amount: data.amount };
//TODO:fix this
return this.asanpardakhtGateway.time(); return await gateway.verifyPayment(verifyData);
case GatewayProvider.SEP:
return await this.sepGateway.verifyPayment(data.authority, data.amount);
default:
throw new BadRequestError(`Payment provider ${gatewayType} is not supported`);
}
} }
public async retryPayment(gatewayType: GatewayProvider, authority: string) { public async retryPayment(gatewayType: GatewayProvider, authority: string) {
switch (gatewayType) { const gateway = this.getGateway(gatewayType);
case GatewayProvider.Zarinpal: return await gateway.retryPayment(authority);
return await this.zarinPalGateway.retryPayment(authority); }
case GatewayProvider.Asanpardakht:
//TODO:fix this private getGateway(gatewayType: GatewayProvider): IGateway {
return this.asanpardakhtGateway.token(); const gateway = this.gateways.get(gatewayType);
case GatewayProvider.SEP: if (!gateway) {
return await this.sepGateway.retryPayment(authority); throw new BadRequestError(`Payment provider ${gatewayType} is not supported`);
default:
throw new BadRequestError(`Payment provider ${gatewayType} is not supported`);
} }
return gateway;
} }
} }
-141
View File
@@ -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 };
+30 -12
View File
@@ -3,38 +3,54 @@ import { injectable } from "inversify";
import { InternalError } from "../../../core/app/app.errors"; import { InternalError } from "../../../core/app/app.errors";
import { Logger } from "../../../core/logging/logger"; import { Logger } from "../../../core/logging/logger";
import { IGateway } from "../interface/IPG";
interface SepRequestPaymentResponse { interface SepRequestPaymentResponse {
status: 1 | -1; status: 1 | -1;
errorCode?: string; errorCode?: string;
errorDesc?: string; errorDesc?: string;
token?: 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() @injectable()
class SepGateway { class SepGateway implements IGateway {
private logger = new Logger(SepGateway.name); private logger = new Logger(SepGateway.name);
private gatewayApiUrl: string = process.env.SEP_HOST || "https://sep.shaparak.ir"; private gatewayApiUrl: string = process.env.SEP_HOST || "https://sep.shaparak.ir";
private callBackURL = `https://api.fajrtabloshop.com/payment/verify/SEP/`; 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" }; private requestHeader = { "Content-Type": "application/json", Accept: "application/json" };
async processPayment(amount: number, description: string, email: string, mobile: string, callBackPath: string) { async processPayment(amount: number, _description: string, _email: string, _mobile: string, callBackPath: string) {
if (!this.gatewayApiUrl || !this.TERMINAL_ID) { if (!this.gatewayApiUrl || !this.terminalNumber) {
throw new InternalError(["SEP gateway is not configured"]); throw new InternalError(["SEP gateway is not configured"]);
} }
await this.checkOutgoingIp(); await this.checkOutgoingIp();
const requesturl = `${this.gatewayApiUrl}/onlinepg/onlinepg`; const requesturl = `${this.gatewayApiUrl}/onlinepg/onlinepg`;
console.log("SEP Gateway processPayment called", email, description);
try { try {
const purchaseData = { const purchaseData = {
Action: "Token", Action: "Token",
TerminalId: this.TERMINAL_ID, TerminalId: this.terminalNumber,
Amount: amount, // ریال Amount: amount, // ریال
RedirectUrl: `${this.callBackURL}${callBackPath}`, RedirectUrl: `${this.callBackURL}${callBackPath}`,
ResNum: Date.now().toString(), ResNum: Date.now().toString(),
CellNumber: mobile, CellNumber: _mobile,
}; };
const response = await axios.post<SepRequestPaymentResponse>(requesturl, purchaseData, { headers: this.requestHeader }); const response = await axios.post<SepRequestPaymentResponse>(requesturl, purchaseData, { headers: this.requestHeader });
@@ -58,22 +74,24 @@ class SepGateway {
async checkOutgoingIp() { async checkOutgoingIp() {
try { try {
const response = await axios.get("https://api.ipify.org?format=json"); 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; return response.data.ip;
} catch (error) { } catch (error) {
console.error("Could not determine outgoing IP", error); console.error("Could not determine outgoing IP", error);
} }
} }
async verifyPayment(authority: string, amount: number) { async verifyPayment(data: { authority?: string; refNum?: string; amount: number }) {
if (!this.gatewayApiUrl || !this.TERMINAL_ID) { if (!this.gatewayApiUrl || !this.terminalNumber) {
throw new InternalError(["SEP gateway is not configured"]); throw new InternalError(["SEP gateway is not configured"]);
} }
// https://sep.shaparak.ir/verifyTxnRandomSessionkey/ipg/VerifyTransaction // https://sep.shaparak.ir/verifyTxnRandomSessionkey/ipg/VerifyTransaction
try { try {
const verifyData = { authority, amount, terminal_id: this.TERMINAL_ID }; const verifyData = { RefNum: data.refNum, TerminalNumber: this.terminalNumber };
const response = await axios.post(`${this.gatewayApiUrl}/payment/verify`, verifyData, { headers: this.requestHeader }); const response = await axios.post<SepVerifyPaymentResponse>(`${this.gatewayApiUrl}/verifyTxnRandomSessionkey/ipg/VerifyTransaction`,
verifyData,
{ headers: this.requestHeader }
);
return response.data; return response.data;
} catch (error) { } catch (error) {
this.logger.error("error in SEP payment verification", error); this.logger.error("error in SEP payment verification", error);
+4 -3
View File
@@ -4,9 +4,10 @@ import { injectable } from "inversify";
import { ZarinPalPGNewArgs, ZarinPalPGNewRequestData, ZarinPalPGVerifyData } from "../../../common/types/paymentGateway.type"; import { ZarinPalPGNewArgs, ZarinPalPGNewRequestData, ZarinPalPGVerifyData } from "../../../common/types/paymentGateway.type";
import { InternalError } from "../../../core/app/app.errors"; import { InternalError } from "../../../core/app/app.errors";
import { Logger } from "../../../core/logging/logger"; import { Logger } from "../../../core/logging/logger";
import { IGateway } from "../interface/IPG";
@injectable() @injectable()
class ZarinPalGateway { class ZarinPalGateway implements IGateway {
private logger = new Logger(ZarinPalGateway.name); private logger = new Logger(ZarinPalGateway.name);
private gatewayApiUrl: string = `https://${process.env.IPG_TYPE}.zarinpal.com/pg`; //or api subdomain 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 { try {
//TODO:check if amount should be in toman or riyal //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 response = await axios.post(`${this.gatewayApiUrl}/v4/payment/verify.json`, verifyData, { headers: this.requestHeader });
const { data } = response.data as ZarinPalPGVerifyData; const { data } = response.data as ZarinPalPGVerifyData;
+8 -1
View File
@@ -23,6 +23,12 @@ export interface IPaymentGateway {
retryPayment(gatewayType: GatewayProvider, authority: string): Promise<any>; retryPayment(gatewayType: GatewayProvider, authority: string): Promise<any>;
} }
export interface IGateway {
processPayment(amount: number, description: string, email: string, mobile: string, callBackPath: string): Promise<any>;
verifyPayment(data: { authority?: string; refNum?: string; amount: number }): Promise<any>;
retryPayment(authority: string): Promise<any>;
}
export interface NewPaymentData { export interface NewPaymentData {
amount: number; amount: number;
description: string; description: string;
@@ -32,6 +38,7 @@ export interface NewPaymentData {
} }
export interface VerifyPaymentData { export interface VerifyPaymentData {
authority: string;
amount: number; amount: number;
authority?: string;
refNum?:string
} }
@@ -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;
}
@@ -7,7 +7,7 @@ export interface ICartPayment {
_id: Types.ObjectId; _id: Types.ObjectId;
user_id: Types.ObjectId; user_id: Types.ObjectId;
authority: string; authority: string;
transaction_id: number; transaction_id: string | null;
payment_method: Types.ObjectId; payment_method: Types.ObjectId;
priceDetails: IPriceDetails; priceDetails: IPriceDetails;
totalPrice: number; totalPrice: number;
@@ -34,7 +34,7 @@ export interface IProductRequestPayment {
_id: Types.ObjectId; _id: Types.ObjectId;
seller_id: Types.ObjectId; seller_id: Types.ObjectId;
authority: string; authority: string;
transaction_id: number; transaction_id: string | null;
payment_method: Types.ObjectId; payment_method: Types.ObjectId;
priceDetails: IPRPriceDetails; priceDetails: IPRPriceDetails;
totalPrice: number; totalPrice: number;
+2 -2
View File
@@ -19,7 +19,7 @@ const CartPaymentSchema = new Schema<ICartPayment>(
{ {
user_id: { type: Schema.Types.ObjectId, ref: "User", required: true }, user_id: { type: Schema.Types.ObjectId, ref: "User", required: true },
authority: { type: String, 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 }, payment_method: { type: Schema.Types.ObjectId, ref: "PaymentMethod", required: true },
priceDetails: { type: PaymentPriceDetails, required: true }, priceDetails: { type: PaymentPriceDetails, required: true },
totalPrice: { type: Number, required: true }, totalPrice: { type: Number, required: true },
@@ -46,7 +46,7 @@ const PRPaymentSchema = new Schema<IProductRequestPayment>(
{ {
seller_id: { type: Schema.Types.ObjectId, ref: "Seller", required: true }, seller_id: { type: Schema.Types.ObjectId, ref: "Seller", required: true },
authority: { type: String, 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 }, payment_method: { type: Schema.Types.ObjectId, ref: "PaymentMethod", required: true },
priceDetails: { type: PRPriceDetailSchema }, priceDetails: { type: PRPriceDetailSchema },
totalPrice: { type: Number, required: true }, totalPrice: { type: Number, required: true },
+21
View File
@@ -6,6 +6,7 @@ import { PaymentService } from "./providers/payment.service";
import { HttpStatus } from "../../common"; import { HttpStatus } from "../../common";
import { CartCheckoutDTO } from "./DTO/cartCheckout.dto"; import { CartCheckoutDTO } from "./DTO/cartCheckout.dto";
import { PRCheckoutDTO } from "./DTO/PRCheckout.dto"; import { PRCheckoutDTO } from "./DTO/PRCheckout.dto";
import { VerifyOnlinePaymentDTO } from "./DTO/verifyOnlinePayment.dto";
import { PRPaymentService } from "./providers/PR-payment.service"; import { PRPaymentService } from "./providers/PR-payment.service";
import { BaseController } from "../../common/base/controller"; import { BaseController } from "../../common/base/controller";
import { ApiAuth, ApiModel, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs"; import { ApiAuth, ApiModel, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs";
@@ -104,6 +105,26 @@ class PaymentController extends BaseController {
// return this.response(data); // 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") @ApiOperation("verify a payment for current session seller for product request checkout")
@ApiResponse("successful", HttpStatus.Ok) @ApiResponse("successful", HttpStatus.Ok)
@ApiParam("gateway", "the name of api gateway", true) @ApiParam("gateway", "the name of api gateway", true)
@@ -110,13 +110,23 @@ export class PRPaymentService {
//################################################################## //##################################################################
//################################################################## //##################################################################
async verifyPRPayment(gateway: GatewayProvider, verifyCallbackData: { status: string; authority: string }) { async verifyPRPayment(gateway: GatewayProvider, verifyCallbackData: { status: string; authority?: string; refNum?: string }) {
const { status, authority } = verifyCallbackData; const { status, authority, refNum } = verifyCallbackData;
const session = await startSession(); const session = await startSession();
session.startTransaction(); session.startTransaction();
try { 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 productRequest = await this.findProductRequestByPaymentId(paymentData._id.toString(), session);
const PRId = productRequest._id.toString(); const PRId = productRequest._id.toString();
@@ -125,9 +135,12 @@ export class PRPaymentService {
return this.buildPaymentResponse(status, PaymentMessage.PaymentNotSuccessful, paymentData._id.toString(), PRId); 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) { 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); 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( await this.PRPaymentRepo.model.findByIdAndUpdate(
paymentId, paymentId,
{ paymentStatus: PaymentStatus.Completed, transaction_id: refId }, { paymentStatus: PaymentStatus.Completed, transaction_id: transactionId },
{ session }, { session },
); );
await session.commitTransaction(); await session.commitTransaction();
@@ -140,12 +140,11 @@ class PaymentService {
// extracted to handle payment creation // extracted to handle payment creation
const { payment, paymentData } = await this.handlePaymentCreation(paymentMethod, price, user, description, session); const { payment, paymentData } = await this.handlePaymentCreation(paymentMethod, price, user, description, session);
// create order and order items // create order and order items
const { order, orderItems } = await this.orderService.createOrder(user, payment, cartShipmentItems, session); const { order, orderItems } = await this.orderService.createOrder(user, payment, cartShipmentItems, session);
// clear cart and adjust stock // clear cart and adjust stock
await this.finalizeOrder(user, cartShipmentItems, session); await this.finalizeOrder(user, cartShipmentItems, session);
await session.commitTransaction(); await session.commitTransaction();
await session.endSession(); await session.endSession();
return { paymentData, order, orderItems }; return { paymentData, order, orderItems };
@@ -161,12 +160,22 @@ class PaymentService {
//################################################################## //##################################################################
//################################################################## //##################################################################
//verify callback of cart payment //verify callback of cart payment
async verifyCartPayment(gateway: GatewayProvider, verifyCallbackData: { authority: string; status: string }) { async verifyCartPayment(gateway: GatewayProvider, verifyCallbackData: { status: string; authority?: string; refNum?: string }) {
const { status, authority } = verifyCallbackData; const { status, authority, refNum } = verifyCallbackData;
const session = await startSession(); const session = await startSession();
session.startTransaction(); session.startTransaction();
try { 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); if (!paymentData) throw new BadRequestError(PaymentMessage.PaymentNotFound);
@@ -185,10 +194,16 @@ class PaymentService {
} }
// Verify payment through gateway // 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) { if (data.code === 100 || data.code === 101) {
await this.handleSellerNotify(order._id, user, session); 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); 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) { private async handleSuccessfulPayment(paymentId: string, orderId: number, transactionId: string, session: ClientSession) {
await this.updatePaymentStatusAndReference(paymentId, refId, PaymentStatus.Completed, session); await this.updatePaymentStatusAndReference(paymentId, transactionId, PaymentStatus.Completed, session);
await this.orderService.updateOrderStatus(orderId, OrdersStatus.process_by_seller, 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) { private async updatePaymentStatusAndReference(paymentId: string, transactionId: string | null, status: PaymentStatus, session: ClientSession) {
return await this.cartPaymentRepo.model.findByIdAndUpdate(paymentId, { paymentStatus: status, transaction_id: refId }, { session }); return await this.cartPaymentRepo.model.findByIdAndUpdate(paymentId, { paymentStatus: status, transaction_id: transactionId }, { session });
} }
} }