refactor : payment
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ export const enum PaymentMessage {
|
|||||||
CartPaymentDesc = "خرید کالا:",
|
CartPaymentDesc = "خرید کالا:",
|
||||||
PaymentNotBelongToUser = "پرداخت برای این کاربر نمیباشد",
|
PaymentNotBelongToUser = "پرداخت برای این کاربر نمیباشد",
|
||||||
PaymentRequestFailed = "درخواست پرداخت موفقیت آمیز نبود",
|
PaymentRequestFailed = "درخواست پرداخت موفقیت آمیز نبود",
|
||||||
|
RefNumIsAlreadyUsed = "این شماره رسید قبلا برای تایید استفاده شده .",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum CouponMessage {
|
export const enum CouponMessage {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export enum GatewayProvider {
|
export enum GatewayProvider {
|
||||||
Zarinpal = "Zarinpal",
|
Zarinpal = "Zarinpal",
|
||||||
Asanpardakht = "asanPardakht",
|
|
||||||
SEP = "SEP",
|
SEP = "SEP",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user