This commit is contained in:
mahyargdz
2025-09-14 15:15:03 +03:30
commit be82059172
534 changed files with 51310 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
import { Expose } from "class-transformer";
import { IsNotEmpty, IsNumber, IsOptional, IsString, Min } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { WithdrawalStatus } from "../models/abstraction/IWithdrawal";
export class WithdrawDTO {
@Expose()
@IsNotEmpty({ message: "مقدار برداشت نمی‌تواند خالی باشد" })
@IsNumber({}, { message: "مقدار برداشت باید یک عدد باشد" })
@Min(100000, { message: "حداقل مقدار برداشت باید بیشتر از 100000 باشد" })
@ApiProperty({ type: "number", description: "amount of the withdrawal", example: 100000 })
amount: number;
// @Expose()
// @IsNotEmpty({ message: "مقدار شماره شبا نمیتواند خالی باشد" })
// @IsString()
// @ApiProperty({ type: "string", description: "shaba for withdrawal request" })
// shaba: string;
// @Expose()
// @IsNotEmpty({ message: "مقدار شماره حساب نمیتواند خالی باشد" })
// @IsString()
// @ApiProperty({ type: "string", description: "bank account number for withdrawal request" })
// bankAccountNumber: string;
}
export class RejectWithdrawalDTO {
@Expose()
@IsOptional()
@IsString()
@ApiProperty({ type: "string", description: "reject reason for withdrawal request" })
rejectReason: string;
}
export class WithdrawalStatusDTO {
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "status for withdrawal request", example: "completed | failed" })
status: WithdrawalStatus;
@Expose()
@IsOptional()
@IsString()
@ApiProperty({ type: "string", description: "reject reason for withdrawal request" })
rejectReason: string;
}
+32
View File
@@ -0,0 +1,32 @@
export const TransactionReason = Object.freeze({
WITHDRAWAL: "برداشت پول",
ORDERـPAY: "پرداخت مبلغ سفارش",
BY_ADMIN: "شارژ توسط ادمین",
});
export const CSVHeaders = Object.freeze({
fullName: "نام و نام خانوادگی",
phoneNumber: "شماره تماس",
email: "ایمیل",
accountStatus: "وضعیت حساب",
isRegisterCompleted: "ثبت نام تکمیل شده",
businessType: "نوع فعالیت",
isWholesaler: "عمده فروش",
createdAt: "تاریخ ثبت",
updatedAt: "تاریخ بروزرسانی",
amount: "مبلغ",
status: "وضعیت برداشت",
processedAt: "تاریخ پردازش",
IBAN: "شماره شبا",
});
export const Translate = Object.freeze({
real: "حقیقی",
legal: "حقوقی",
Pending: "در انتظار",
Approved: "تایید شده",
Rejected: "رد شده",
Completed: "تکمیل شده",
Cancelled: "لغو شده",
Failed: "ناموفق",
});
@@ -0,0 +1,15 @@
import { Types } from "mongoose";
export enum TransactionType {
CREDIT = "CREDIT",
DEBIT = "DEBIT",
WITHDRAWAL = "WITHDRAWAL",
}
export interface ITransaction {
wallet: Types.ObjectId;
order: number;
amount: number;
transactionType: TransactionType;
reason: string;
}
@@ -0,0 +1,6 @@
import { Types } from "mongoose";
export interface IWallet {
seller: Types.ObjectId;
balance: number;
}
@@ -0,0 +1,17 @@
import { Types } from "mongoose";
export enum WithdrawalStatus {
PENDING = "Pending",
COMPLETED = "Completed",
FAILED = "Failed",
}
export interface IWithdrawal {
wallet: Types.ObjectId;
amount: number;
IBAN: string;
// bankAccountNumber: string;
status: WithdrawalStatus;
rejectReason: string;
processedAt: Date | null;
}
@@ -0,0 +1,18 @@
import { Schema, model } from "mongoose";
import { ITransaction, TransactionType } from "./abstraction/ITransaction";
const TransactionSchema = new Schema<ITransaction>(
{
wallet: { type: Schema.Types.ObjectId, ref: "Wallet", required: true },
order: { type: Number, ref: "Order", default: null },
amount: { type: Number, required: true },
transactionType: { type: String, enum: TransactionType, required: true },
reason: { type: String, required: false },
},
{ timestamps: true, toJSON: { versionKey: false, virtuals: true }, id: false },
);
const TransactionModel = model<ITransaction>("Transaction", TransactionSchema);
export { TransactionModel };
+15
View File
@@ -0,0 +1,15 @@
import { Schema, model } from "mongoose";
import { IWallet } from "./abstraction/IWallet";
const WalletSchema = new Schema<IWallet>(
{
seller: { type: Schema.Types.ObjectId, ref: "Seller", required: true, unique: true },
balance: { type: Number, default: 0 },
},
{ timestamps: true, toJSON: { versionKey: false, virtuals: true }, id: false },
);
const WalletModel = model<IWallet>("Wallet", WalletSchema);
export { WalletModel };
@@ -0,0 +1,20 @@
import { Schema, model } from "mongoose";
import { IWithdrawal, WithdrawalStatus } from "./abstraction/IWithdrawal";
const WithdrawalSchema = new Schema<IWithdrawal>(
{
wallet: { type: Schema.Types.ObjectId, ref: "Wallet", required: true },
amount: { type: Number, required: true },
IBAN: { type: String, required: true },
// bankAccountNumber: { type: String, default: null },
status: { type: String, enum: WithdrawalStatus, default: WithdrawalStatus.PENDING },
rejectReason: { type: String, default: null },
processedAt: { type: Date, default: null },
},
{ timestamps: true, toJSON: { versionKey: false, virtuals: true }, id: false },
);
const WithdrawalModel = model<IWithdrawal>("Withdrawal", WithdrawalSchema);
export { IWithdrawal, WithdrawalModel };
@@ -0,0 +1,15 @@
import { BaseRepository } from "../../../common/base/repository";
import { ITransaction } from "../models/abstraction/ITransaction";
import { TransactionModel } from "../models/transaction.model";
class TransactionRepo extends BaseRepository<ITransaction> {
constructor() {
super(TransactionModel);
}
}
function createTransactionRepo(): TransactionRepo {
return new TransactionRepo();
}
export { TransactionRepo, createTransactionRepo };
@@ -0,0 +1,15 @@
import { BaseRepository } from "../../../common/base/repository";
import { IWallet } from "../models/abstraction/IWallet";
import { WalletModel } from "../models/wallet.model";
class WalletRepo extends BaseRepository<IWallet> {
constructor() {
super(WalletModel);
}
}
function createWalletRepo(): WalletRepo {
return new WalletRepo();
}
export { WalletRepo, createWalletRepo };
@@ -0,0 +1,19 @@
import { BaseRepository } from "../../../common/base/repository";
import { WithdrawalStatus } from "../models/abstraction/IWithdrawal";
import { IWithdrawal, WithdrawalModel } from "../models/withdrawal.model";
class WithdrawalRepo extends BaseRepository<IWithdrawal> {
constructor() {
super(WithdrawalModel);
}
async getWithdrawalRequestsReport() {
return this.model.countDocuments({ status: WithdrawalStatus.PENDING });
}
}
function createWithdrawalRepo(): WithdrawalRepo {
return new WithdrawalRepo();
}
export { WithdrawalRepo, createWithdrawalRepo };
+63
View File
@@ -0,0 +1,63 @@
import { Request } from "express";
import { inject } from "inversify";
import { controller, httpGet, httpPost, request, requestBody } from "inversify-express-utils";
import { WalletService } from "./wallet.service";
import { HttpStatus } from "../../common";
import { WithdrawDTO } from "./DTO/withdraw.dto";
import { BaseController } from "../../common/base/controller";
import { ApiAuth, ApiModel, ApiOperation, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs";
import { Guard } from "../../core/middlewares/guard.middleware";
import { ValidationMiddleware } from "../../core/middlewares/validator.middleware";
import { IOCTYPES } from "../../IOC/ioc.types";
import { ISeller } from "../seller/models/Abstraction/ISeller";
@controller("/wallet")
@ApiTags("Wallet")
class WalletController extends BaseController {
@inject(IOCTYPES.WalletService) walletService: WalletService;
@ApiOperation("get all transaction of seller wallet ==> login as seller")
@ApiResponse("successful", HttpStatus.Ok)
@ApiAuth()
@httpGet("/transaction", Guard.authSeller())
public async getTransactions(@request() req: Request) {
const seller = req.user as ISeller;
const data = await this.walletService.getTransactions(seller._id.toString());
return this.response(data);
}
@ApiOperation("Get wallet balance ==> login as seller")
@ApiResponse("Successful", HttpStatus.Ok)
@ApiAuth()
@httpGet("/balance", Guard.authSeller())
public async getWalletBalance(@request() req: Request) {
const seller = req.user as ISeller;
const balance = await this.walletService.getWalletBalance(seller._id.toString());
return this.response(balance);
}
@ApiOperation("Get all seller withdrawal requests ==> login as seller")
@ApiResponse("Successful", HttpStatus.Ok)
@ApiAuth()
@httpGet("/withdraw", Guard.authSeller())
public async getWithdrawalRequests(@request() req: Request) {
const seller = req.user as ISeller;
const data = await this.walletService.getWithdrawalRequests(seller._id.toString());
return this.response(data);
}
@ApiOperation("withdraw request for seller wallet ==> login as seller")
@ApiResponse("Successful", HttpStatus.Created)
@ApiResponse("Insufficient balance or withdrawal failed", HttpStatus.BadRequest)
@ApiModel(WithdrawDTO)
@ApiAuth()
@httpPost("/withdraw", Guard.authSeller(), ValidationMiddleware.validateInput(WithdrawDTO))
public async withdrawRequest(@request() req: Request, @requestBody() withdrawDto: WithdrawDTO) {
const seller = req.user as ISeller;
const data = await this.walletService.withdrawRequest(seller, withdrawDto.amount);
return this.response(data, HttpStatus.Created);
}
}
export { WalletController };
+431
View File
@@ -0,0 +1,431 @@
import { stringify } from "csv";
import { Response } from "express";
import { inject, injectable } from "inversify";
import { ClientSession, startSession } from "mongoose";
import { CSVHeaders, TransactionReason } from "./constant";
import { TransactionType } from "./models/abstraction/ITransaction";
import { WithdrawalStatus } from "./models/abstraction/IWithdrawal";
import { TransactionRepo } from "./repository/transaction.repo";
import { WalletRepo } from "./repository/wallet.repo";
import { WithdrawalRepo } from "./repository/withdrawal.repo";
import { SellerMessage, WalletMessage } from "../../common/enums/message.enum";
import { BadRequestError, InternalError } from "../../core/app/app.errors";
import { Logger } from "../../core/logging/logger";
import { IOCTYPES } from "../../IOC/ioc.types";
import { TimeService } from "../../utils/time.service";
import { translate } from "../../utils/translate.utils";
import { NotificationService } from "../notification/notification.service";
import { WithdrawalStatusDTO } from "./DTO/withdraw.dto";
import { BusinessTypeEnum } from "../seller/enum/seller.enum";
import { ISeller } from "../seller/models/Abstraction/ISeller";
import { LegalSellerRepo } from "../seller/repository/legalSeller.repository";
import { RealSellerRepo } from "../seller/repository/realSeller.repository";
@injectable()
class WalletService {
private logger = new Logger(WalletService.name);
@inject(IOCTYPES.WalletRepo) private walletRepo: WalletRepo;
@inject(IOCTYPES.NotificationService) notificationService: NotificationService;
@inject(IOCTYPES.TransactionRepo) private transactionRepo: TransactionRepo;
@inject(IOCTYPES.WithdrawalRepo) private withdrawalRepo: WithdrawalRepo;
@inject(IOCTYPES.RealSellerRepo) private realSellerRepo: RealSellerRepo;
@inject(IOCTYPES.LegalSellerRepo) private legalSellerRepo: LegalSellerRepo;
//#######################################
//#######################################
async createSellerWallet(sellerId: string, session: ClientSession) {
const wallet = await this.walletRepo.model.create([{ seller: sellerId }], { session });
return { wallet: wallet[0] };
}
//#######################################
//#######################################
async creditSellerWallet(sellerId: string, amount: number) {
const session = await startSession();
session.startTransaction();
try {
const wallet = await this.walletRepo.model.findOne({ seller: sellerId });
if (!wallet) throw new BadRequestError(WalletMessage.WalletNotFound);
//create a transaction for credit the wallet balance
const transaction = await this.transactionRepo.model.create(
[
{
wallet: wallet._id,
amount,
transactionType: TransactionType.CREDIT,
reason: TransactionReason.BY_ADMIN,
},
],
{ session },
);
// update wallet balance
wallet.balance += amount;
await wallet.save({ session });
await this.notificationService.notifyWalletCredit(sellerId, amount, session);
await session.commitTransaction();
return { message: WalletMessage.CreditedSuccessfully, newBalance: wallet.balance, transaction };
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
}
//#######################################
//#######################################
async creditSellerWalletForOrder(sellerId: string, orderId: number, amount: number, session: ClientSession) {
const wallet = await this.walletRepo.model.findOneAndUpdate(
{ seller: sellerId },
{ $inc: { balance: amount } },
{ session, new: true },
);
if (!wallet) throw new BadRequestError(WalletMessage.WalletNotFound);
// create a transaction for credit the wallet balance
const transaction = await this.transactionRepo.model.create(
[
{
wallet: wallet._id,
order: orderId,
amount: amount,
transactionType: TransactionType.CREDIT,
reason: TransactionReason.ORDERـPAY,
},
],
{ session },
);
await this.notificationService.notifyWalletCredit(sellerId, amount, session);
return { wallet, transaction };
}
//#######################################
//#######################################
async debitSellerWallet(sellerId: string, orderId: number, debitAmount: number, reason: string) {
const session = await startSession();
session.startTransaction();
try {
const wallet = await this.walletRepo.model.findOne({ seller: sellerId });
if (!wallet || wallet.balance < debitAmount) throw new BadRequestError(WalletMessage.InsufficientBalance);
// create a new transaction for the debit
const transaction = await this.transactionRepo.model.create(
[
{
wallet: wallet._id,
orderId,
amount: debitAmount,
transactionType: TransactionType.DEBIT,
reason,
},
],
{ session },
);
// update wallet balance
wallet.balance -= debitAmount;
await wallet.save({ session });
await this.notificationService.notifyWalletDebit(sellerId, debitAmount, session);
await session.commitTransaction();
return { message: WalletMessage.DebitedSuccessfully, newBalance: wallet.balance, transaction };
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
}
//#######################################
//#######################################
async withdrawRequest(seller: ISeller, withdrawalAmount: number) {
const session = await startSession();
session.startTransaction();
try {
let IBAN: string = "";
if (seller.businessType === BusinessTypeEnum.REAL) {
//
const realSeller = await this.realSellerRepo.model.findOne({ seller: seller._id }).lean();
if (!realSeller) throw new BadRequestError(SellerMessage.SellerNotFound);
IBAN = realSeller.shebaNumber;
} else if (seller.businessType === BusinessTypeEnum.LEGAL) {
//
const legalSeller = await this.legalSellerRepo.model.findOne({ seller: seller._id }).lean();
if (!legalSeller) throw new BadRequestError(SellerMessage.SellerNotFound);
IBAN = legalSeller.IBan;
}
if (!IBAN) throw new BadRequestError(SellerMessage.InvalidBusinessType);
const wallet = await this.walletRepo.model.findOne({ seller: seller._id.toString() });
if (!wallet || wallet.balance < withdrawalAmount) throw new BadRequestError(WalletMessage.InsufficientBalanceForWithdrawal);
// create a new withdrawal request
const withdrawal = await this.withdrawalRepo.model.create(
[
{
wallet: wallet._id,
amount: withdrawalAmount,
IBAN,
},
],
{ session },
);
await this.notificationService.notifyWithdrawalRequest(seller._id.toString(), withdrawalAmount, session);
await session.commitTransaction();
return { message: WalletMessage.WithdrawalSubmitted, withdrawal: withdrawal[0] };
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
}
//#######################################
//#######################################
async processWithdrawal(withdrawalId: string, withdrawalStatusDto: WithdrawalStatusDTO) {
const session = await startSession();
session.startTransaction();
try {
const withdrawal = await this.withdrawalRepo.model.findOne({ _id: withdrawalId });
if (!withdrawal) throw new BadRequestError(WalletMessage.WithdrawalNotFound);
const wallet = await this.walletRepo.model.findOne({ _id: withdrawal.wallet });
if (!wallet) throw new BadRequestError(WalletMessage.WalletNotFound);
if (withdrawalStatusDto.status === WithdrawalStatus.COMPLETED) {
// check if the wallet has enough balance for the withdrawal
if (wallet.balance < withdrawal.amount) throw new BadRequestError(WalletMessage.InsufficientBalanceForWithdrawal);
// update balance and withdrawal status
wallet.balance -= withdrawal.amount;
withdrawal.status = WithdrawalStatus.COMPLETED;
withdrawal.processedAt = new Date();
// save the transaction as a debit
await this.transactionRepo.model.create(
[
{
wallet: wallet._id,
orderId: null,
amount: withdrawal.amount,
transactionType: TransactionType.WITHDRAWAL,
reason: TransactionReason.WITHDRAWAL,
},
],
{ session },
);
await this.notificationService.notifyWithdrawalCompleted(wallet.seller.toString(), withdrawal.amount, session);
//
} else if (withdrawalStatusDto.status === WithdrawalStatus.FAILED) {
// update withdrawal status when its failed
withdrawal.status = WithdrawalStatus.FAILED;
withdrawal.processedAt = new Date();
withdrawal.rejectReason = withdrawalStatusDto.rejectReason;
await this.notificationService.notifyWithdrawalFailed(wallet.seller.toString(), withdrawal.amount, session);
}
await withdrawal.save({ session });
await wallet.save({ session });
await session.commitTransaction();
return { message: `Withdrawal ${withdrawalStatusDto.status}`, newBalance: wallet.balance };
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
}
//#######################################
//#######################################
async getWalletBalance(sellerId: string) {
const wallet = await this.walletRepo.model.findOne({ seller: sellerId });
if (!wallet) throw new BadRequestError(WalletMessage.WalletNotFound);
return { balance: wallet.balance };
}
//#######################################
//#######################################
async getTransactions(sellerId: string) {
const wallet = await this.walletRepo.model.findOne({ seller: sellerId });
if (!wallet) throw new BadRequestError(WalletMessage.WalletNotFound);
const transaction = await this.transactionRepo.model.find({ wallet: wallet._id });
return { wallet, transaction };
}
//#######################################
//#######################################
async getWithdrawalRequests(sellerId: string) {
const wallet = await this.walletRepo.model.findOne({ seller: sellerId });
if (!wallet) throw new BadRequestError(WalletMessage.WalletNotFound);
const withdrawals = await this.withdrawalRepo.model.find({ wallet: wallet._id });
return { wallet, withdrawals, lastPaidAmount: withdrawals[0]?.amount ?? null, lastPaidDate: withdrawals[0]?.processedAt || null };
}
async getWithdrawalRequestsReport() {
return await this.withdrawalRepo.getWithdrawalRequestsReport();
}
//*********** */
async getWithdrawalRequestsForAdminPanel() {
const wallets = await this.walletRepo.model.find().populate("seller");
const walletData = await Promise.all(
wallets.map(async (wallet) => {
const withdrawals = await this.withdrawalRepo.model.find({ wallet: wallet._id }).sort({ createdAt: -1 });
return {
wallet,
withdrawals,
lastPaidAmount: withdrawals[0]?.amount ?? null,
lastPaidDate: withdrawals[0]?.processedAt || null,
};
}),
);
return {
walletData,
};
}
//*********** */
async getWithdrawalRequestsCsv(res: Response): Promise<void> {
const withdrawals = await this.withdrawalRepo.model
.find({ status: WithdrawalStatus.PENDING })
.populate({ path: "wallet", populate: { path: "seller" } })
.sort({ createdAt: -1 });
const columns = [
CSVHeaders.fullName,
CSVHeaders.phoneNumber,
CSVHeaders.email,
CSVHeaders.accountStatus,
CSVHeaders.businessType,
CSVHeaders.amount,
CSVHeaders.status,
// CSVHeaders.processedAt,
CSVHeaders.createdAt,
CSVHeaders.IBAN,
];
// Create a CSV stringifier
const stringifier = stringify({
header: true,
columns: columns,
});
// Write CSV data row by row
withdrawals.forEach((withdrawal: any) => {
stringifier.write({
[CSVHeaders.fullName]: withdrawal.wallet.seller.fullName,
[CSVHeaders.phoneNumber]: withdrawal.wallet.seller.phoneNumber,
[CSVHeaders.email]: withdrawal.wallet.seller.email,
[CSVHeaders.accountStatus]: translate(withdrawal.wallet.seller.accountStatus),
[CSVHeaders.businessType]: translate(withdrawal.wallet.seller.businessType),
[CSVHeaders.IBAN]: withdrawal.IBAN,
[CSVHeaders.amount]: Intl.NumberFormat("en").format(withdrawal.amount * 10),
[CSVHeaders.status]: translate(withdrawal.status),
// [CSVHeaders.processedAt]: TimeService.convertGregorianToPersian(withdrawal.processedAt),
[CSVHeaders.createdAt]: TimeService.convertGregorianToPersian(withdrawal.createdAt),
});
});
stringifier
.pipe(res)
.on("finish", () => {
this.logger.info("Withdrawal requests CSV generated successfully");
})
.on("error", (err) => {
this.logger.error("Error while generating withdrawal requests CSV", err);
throw new InternalError("something went wrong");
});
stringifier.end();
}
//*********** */
// async approveWithdrawalRequest(withdrawalRequestId: string) {
// const session = await startSession();
// session.startTransaction();
// try {
// const withdrawRequest = await this.withdrawalRepo.model.findOne({ _id: withdrawalRequestId });
// if (!withdrawRequest) throw new BadRequestError(WalletMessage.WithdrawalNotFound);
// const wallet = await this.walletRepo.findById(withdrawRequest.wallet.toString());
// if (!wallet) throw new BadRequestError(WalletMessage.WalletNotFound);
// await this.withdrawalRepo.model.findByIdAndUpdate(withdrawRequest._id, {
// status: WithdrawalStatus.COMPLETED,
// processedAt: new Date(),
// });
// //TODO:Check this line
// wallet.balance -= withdrawRequest.amount;
// const transaction = await this.transactionRepo.model.create(
// [
// {
// wallet: wallet._id,
// amount: withdrawRequest.amount,
// transactionType: TransactionType.DEBIT,
// reason: TransactionReason.WITHDRAWAL,
// },
// ],
// { session },
// );
// await wallet.save({ session });
// await session.commitTransaction();
// return {
// message: WalletMessage.WithdrawalSubmitted,
// transaction: transaction[0],
// };
// } catch (error) {
// await session.abortTransaction();
// throw error;
// } finally {
// await session.endSession();
// }
// }
async rejectWithdrawalRequest(withdrawalRequestId: string, rejectReason: string) {
const withdrawRequest = await this.withdrawalRepo.model.findOne({ _id: withdrawalRequestId });
if (!withdrawRequest) throw new BadRequestError(WalletMessage.WithdrawalNotFound);
await this.withdrawalRepo.model.findByIdAndUpdate(withdrawRequest._id, {
status: WithdrawalStatus.FAILED,
processedAt: new Date(),
rejectReason,
});
return {
message: WalletMessage.RejectWithdrawalRequest,
};
}
}
export { WalletService };