chore: parsian

This commit is contained in:
mahyargdz
2025-06-07 15:01:38 +03:30
parent f4ce67c00c
commit 4a7003a532
14 changed files with 582 additions and 19 deletions
+22
View File
@@ -0,0 +1,22 @@
import { ConfigService } from "@nestjs/config";
export function parsianConfig() {
return {
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
loginAccount: configService.getOrThrow<string>("PARSIAN_LOGIN_ACCOUNT"),
callBackUrl: configService.getOrThrow<string>("CALLBACK_URL"),
}),
};
}
export interface IParsianConfig {
loginAccount: string;
callBackUrl: string;
}
export const ParsianConfiguration = {
Address: "https://pec.shaparak.ir/NewIPG/?token=",
WsdlInitial: "https://pec.shaparak.ir/NewIPGServices/Sale/SaleService.asmx?wsdl",
WsdlConfirm: "https://pec.shaparak.ir/NewIPGServices/Confirm/ConfirmService.asmx?wsdl",
};
@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsUUID, IsUrl, Min } from "class-validator";
import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsUUID, IsUrl } from "class-validator";
import { WalletMessage } from "../../../common/enums/message.enum";
import { TransferType } from "../enums/payment-type.enum";
@@ -7,7 +7,7 @@ import { TransferType } from "../enums/payment-type.enum";
export class DepositDto {
@IsNotEmpty({ message: WalletMessage.AMOUNT_REQUIRED })
@IsInt({ message: WalletMessage.AMOUNT_MUST_BE_INTEGER })
@Min(100_000, { message: WalletMessage.AMOUNT_MINIMUM })
// @Min(100_000, { message: WalletMessage.AMOUNT_MINIMUM })
@ApiProperty({ description: "Amount to charge", example: 100_000 })
amount: number;
}
+6
View File
@@ -1,4 +1,10 @@
export const ZARINPAL_CONFIG = "ZARINPAL_CONFIG";
export const PARSIAN_CONFIG = "PARSIAN_CONFIG";
// SOAP Client Names
export const PARSIAN_SALE_CLIENT = "PARSIAN_SALE_CLIENT";
export const PARSIAN_CONFIRM_CLIENT = "PARSIAN_CONFIRM_CLIENT";
//===============================================
export const PAYMENT = Object.freeze({
@@ -1,3 +1,4 @@
export enum GatewayEnum {
ZARINPAL = "zarinpal",
PARSIAN = "parsian",
}
@@ -2,6 +2,7 @@ import { BadGatewayException, Injectable } from "@nestjs/common";
import { PaymentMessage } from "../../../common/enums/message.enum";
import { GatewayEnum } from "../enums/gateway.enum";
import { ParsianGateway } from "../gateways/parsian.gateway";
import { ZarinpalGateway } from "../gateways/zarinpal.gateway";
import { IPaymentGateway, IPaymentGatewayFactory } from "../interfaces/IPayment";
import { GatewayType } from "../types/gateway.type";
@@ -10,13 +11,17 @@ import { GatewayType } from "../types/gateway.type";
export class PaymentGatewayFactory implements IPaymentGatewayFactory {
private readonly gateways: Map<GatewayType, IPaymentGateway>;
constructor(private readonly zarinpalGateway: ZarinpalGateway) {
constructor(
private readonly zarinpalGateway: ZarinpalGateway,
private readonly parsianGateway: ParsianGateway,
) {
this.gateways = new Map<GatewayType, IPaymentGateway>();
this.initializeGateways();
}
private initializeGateways(): void {
this.gateways.set(GatewayEnum.ZARINPAL, this.zarinpalGateway);
this.gateways.set(GatewayEnum.PARSIAN, this.parsianGateway);
}
getPaymentGateway(provider: GatewayType): IPaymentGateway {
@@ -0,0 +1,165 @@
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
import { Client } from "nestjs-soap";
import { PaymentMessage } from "../../../common/enums/message.enum";
import { IParsianConfig, ParsianConfiguration } from "../../../configs/parsian.config";
import { PARSIAN_CONFIG, PARSIAN_CONFIRM_CLIENT, PARSIAN_SALE_CLIENT } from "../constants";
import { GatewayEnum } from "../enums/gateway.enum";
import {
IPaymentGateway,
IPaymentVerifyParams,
IProcessPaymentParams,
ParsianCallbackData,
ParsianConfirmPaymentResponse,
ParsianSalePaymentResponse,
} from "../interfaces/IPayment";
@Injectable()
export class ParsianGateway implements IPaymentGateway {
private readonly logger = new Logger(ParsianGateway.name);
constructor(
@Inject(PARSIAN_CONFIG) private readonly config: IParsianConfig,
@Inject(PARSIAN_SALE_CLIENT) private readonly saleClient: Client,
@Inject(PARSIAN_CONFIRM_CLIENT) private readonly confirmClient: Client,
) {
// Log the initialization status
this.logger.log(`ParsianGateway initialized - Sale client: ${!!this.saleClient}, Confirm client: ${!!this.confirmClient}`);
}
async processPayment(processParams: IProcessPaymentParams) {
try {
// Check if SOAP client is available
if (!this.saleClient) {
this.logger.error("SOAP sale client is not initialized");
throw new InternalServerErrorException({
message: "Payment service temporarily unavailable. Please try again later.",
code: "SOAP_CLIENT_NOT_INITIALIZED",
});
}
const requestData = {
requestData: {
LoginAccount: this.config.loginAccount,
OrderId: processParams.invoiceId || Date.now().toString(),
Amount: processParams.amount,
CallBackUrl: `${this.config.callBackUrl}/${GatewayEnum.PARSIAN}`,
Originator: processParams.mobile,
AdditionalData: processParams.description,
},
};
this.logger.log(`Processing payment request:`, {
LoginAccount: this.config.loginAccount,
OrderId: requestData.requestData.OrderId,
Amount: processParams.amount,
CallBackUrl: `${this.config.callBackUrl}/${GatewayEnum.PARSIAN}`,
});
const result = (await this.saleClient.SalePaymentRequestAsync(requestData))[0] as ParsianSalePaymentResponse;
if (!result || !result.SalePaymentRequestResult) {
this.logger.error("SalePaymentRequestResult not found in response");
throw new InternalServerErrorException({ message: PaymentMessage.ERROR_IN_PROCESS_PAYMENT, result });
}
const saleResult = result.SalePaymentRequestResult;
if (!saleResult.Token || Number(saleResult.Token) === 0) {
this.logger.error(`Parsian payment request failed: ${saleResult.Message}`);
throw new InternalServerErrorException({ message: PaymentMessage.ERROR_IN_PROCESS_PAYMENT, result });
}
this.logger.log(`Payment request successful - Token: ${saleResult.Token}`);
return {
redirectUrl: ParsianConfiguration.Address + saleResult.Token,
message: saleResult.Message || "Payment initialized successfully",
reference: saleResult.Token,
};
} catch (error) {
this.logger.error(`Payment processing error:`, error);
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PROCESS_PAYMENT);
}
}
async verifyPayment(verifyParams: IPaymentVerifyParams) {
try {
// Check if SOAP client is available
if (!this.confirmClient) {
this.logger.error("SOAP confirm client is not initialized");
throw new InternalServerErrorException({
message: "Payment verification service temporarily unavailable. Please try again later.",
code: "SOAP_CLIENT_NOT_INITIALIZED",
});
}
// For Parsian, the verification should happen in the callback
// This method might be called with callback data
const callbackData = verifyParams as unknown as ParsianCallbackData;
// Check if this is a successful callback
if (Number(callbackData.RRN) < 0 || Number(callbackData.status) !== 0) {
this.logger.warn(`Payment failed in callback - RRN: ${callbackData.RRN}, Status: ${callbackData.status}`);
return {
code: Number(callbackData.status) || -1,
message: "Payment failed",
ref_id: 0,
card_hash: "",
card_pan: "",
fee_type: "",
fee: 0,
};
}
// Confirm the payment with Parsian
const requestData = {
requestData: {
LoginAccount: this.config.loginAccount,
Token: callbackData.Token,
},
};
this.logger.log(`Confirming payment:`, {
LoginAccount: this.config.loginAccount,
Token: callbackData.Token,
});
const result = (await this.confirmClient.ConfirmPaymentAsync(requestData)) as ParsianConfirmPaymentResponse;
if (!result || !result.ConfirmPaymentResult) {
this.logger.error("ConfirmPaymentResult not found in response");
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT);
}
const confirmResult = result.ConfirmPaymentResult;
if (confirmResult.Status === 0) {
this.logger.log(`Payment confirmed successfully - RRN: ${confirmResult.RRN}`);
return {
code: 100, // Success code
message: confirmResult.Message || "Payment confirmed successfully",
ref_id: Number(confirmResult.RRN) || 0,
card_hash: confirmResult.CardNumberMasked || "",
card_pan: confirmResult.CardNumberMasked || "",
fee_type: "",
fee: 0,
};
} else {
this.logger.warn(`Payment confirmation failed - Status: ${confirmResult.Status}, Message: ${confirmResult.Message}`);
return {
code: confirmResult.Status,
message: confirmResult.Message || "Payment confirmation failed",
ref_id: 0,
card_hash: confirmResult.CardNumberMasked || "",
card_pan: confirmResult.CardNumberMasked || "",
fee_type: "",
fee: 0,
};
}
} catch (error) {
this.logger.error(`Payment verification error:`, error);
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT);
}
}
}
@@ -68,6 +68,7 @@ interface IZarinPalPGRequestData {
interface ZarinpalError {
code: number;
message: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
validations?: any[];
}
@@ -91,6 +92,50 @@ export interface ZarinPalPGVerifyData {
errors: ZarinpalError[];
}
//--------------------- Parsian --------------------------------
export interface ParsianSalePaymentRequest {
LoginAccount: string;
OrderId: string;
Amount: number;
CallBackUrl: string;
Originator?: string;
AdditionalData?: string;
}
export interface ParsianSalePaymentResponse {
SalePaymentRequestResult: {
Token: string;
Status: number;
Message: string;
};
}
export interface ParsianConfirmPaymentRequest {
LoginAccount: string;
Token: string;
}
export interface ParsianConfirmPaymentResponse {
ConfirmPaymentResult: {
Status: number;
Token: string;
RRN: string;
Message: string;
CardNumberMasked: string;
};
}
export interface ParsianCallbackData {
Token: string;
OrderId: string;
TerminalNo: string;
RRN: string;
status: string;
Amount: string;
STraceNo: string;
}
export interface PaymentVerificationResult {
status: PaymentStatus;
payment: Payment;
+26 -5
View File
@@ -1,23 +1,26 @@
import { BullModule } from "@nestjs/bullmq";
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { SoapModule } from "nestjs-soap";
import { PAYMENT, ZARINPAL_CONFIG } from "./constants";
import { PARSIAN_CONFIG, PARSIAN_CONFIRM_CLIENT, PARSIAN_SALE_CLIENT, PAYMENT, ZARINPAL_CONFIG } from "./constants";
import { BankAccount } from "./entities/bank-account.entity";
import { DepositRequest } from "./entities/deposit-request.entity";
import { PaymentGateway } from "./entities/payment-gateway.entity";
import { Payment } from "./entities/payment.entity";
import { PaymentGatewayFactory } from "./factories/payment.factory";
import { ParsianGateway } from "./gateways/parsian.gateway";
import { ZarinpalGateway } from "./gateways/zarinpal.gateway";
import { PaymentsController } from "./payments.controller";
import { PaymentsService } from "./providers/payments.service";
import { PaymentProcessor } from "./queue/payment.processor";
import { BankAccountsRepository } from "./repositories/bank-accounts.repository";
import { PaymentsRepository } from "./repositories/payments.repository";
import { zarinpalConfig } from "../../configs/zarinpal.config";
import { WalletsModule } from "../wallets/wallets.module";
import { DepositRequest } from "./entities/deposit-request.entity";
import { PaymentGateway } from "./entities/payment-gateway.entity";
import { DepositRequestsRepository } from "./repositories/deposit-requests.repository";
import { PaymentGatewaysRepository } from "./repositories/payment-gateway.repository";
import { PaymentsRepository } from "./repositories/payments.repository";
import { ParsianConfiguration, parsianConfig } from "../../configs/parsian.config";
import { zarinpalConfig } from "../../configs/zarinpal.config";
import { NotificationModule } from "../notifications/notifications.module";
import { ReferralsModule } from "../referrals/referrals.module";
@@ -31,6 +34,18 @@ import { ReferralsModule } from "../referrals/referrals.module";
removeOnFail: false,
},
}),
SoapModule.registerAsync({
clientName: PARSIAN_SALE_CLIENT,
useFactory: () => ({
uri: ParsianConfiguration.WsdlInitial,
}),
}),
SoapModule.registerAsync({
clientName: PARSIAN_CONFIRM_CLIENT,
useFactory: () => ({
uri: ParsianConfiguration.WsdlConfirm,
}),
}),
WalletsModule,
NotificationModule,
ReferralsModule,
@@ -40,6 +55,7 @@ import { ReferralsModule } from "../referrals/referrals.module";
PaymentsService,
PaymentGatewayFactory,
ZarinpalGateway,
ParsianGateway,
PaymentsRepository,
BankAccountsRepository,
PaymentGatewaysRepository,
@@ -50,6 +66,11 @@ import { ReferralsModule } from "../referrals/referrals.module";
useFactory: zarinpalConfig().useFactory,
inject: zarinpalConfig().inject,
},
{
provide: PARSIAN_CONFIG,
useFactory: parsianConfig().useFactory,
inject: parsianConfig().inject,
},
],
exports: [PaymentsService],
})
+1 -1
View File
@@ -1 +1 @@
export type GatewayType = "zarinpal";
export type GatewayType = "zarinpal" | "parsian";