withdraw request

This commit is contained in:
2026-04-16 11:23:10 +03:30
parent 36a57db33b
commit 6871c8fd34
8 changed files with 141 additions and 15 deletions
@@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class WithDraaRequest1776325917845 implements MigrationInterface {
name = 'WithDraaRequest1776325917845'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "wallet_transaction" ADD "withdrawRequestId" uuid`);
await queryRunner.query(`ALTER TABLE "reward_withdraw_request" ALTER COLUMN "requestedAmount" SET NOT NULL`);
await queryRunner.query(`ALTER TABLE "wallet_transaction" ADD CONSTRAINT "FK_91eedc96b8ae9c6246208c6e96c" FOREIGN KEY ("withdrawRequestId") REFERENCES "reward_withdraw_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "wallet_transaction" DROP CONSTRAINT "FK_91eedc96b8ae9c6246208c6e96c"`);
await queryRunner.query(`ALTER TABLE "reward_withdraw_request" ALTER COLUMN "requestedAmount" DROP NOT NULL`);
await queryRunner.query(`ALTER TABLE "wallet_transaction" DROP COLUMN "withdrawRequestId"`);
}
}
+2
View File
@@ -348,6 +348,7 @@ export const enum WalletMessage {
TRANSFER_METHOD_REQUIRED = "روش انتفال اجباری است",
GATEWAY_ID_SHOULD_BE_UUID = "آیدی درگاه پرداخت باید UUID باشد",
DEPOSIT_WALLET_TRANSFER = "شارژ کیف پول از طریق انتقال بانکی",
WITHDRAWAL_WALLET_TRANSFER = "برداشت از کیف پول بابت واریز پول به حساب مشتری",
INSUFFICIENT_BALANCE = "موجودی کیف پول کافی نیست",
SUBSCRIPTION_WALLET_TRANSFER = "پرداخت اشتراک از طریق کیف پول",
SUPPORT_PLAN_WALLET_TRANSFER = "پرداخت پلن پشتیبانی از طریق کیف پول",
@@ -356,6 +357,7 @@ export const enum WalletMessage {
REFERRAL_REWARD_WALLET_TRANSFER = "پرداخت پاداش ارجاع از طریق کیف پول",
INVOICE_ID_REQUIRED = "شناسه صورت حساب مورد نیاز است",
INVOICE_ID_SHOULD_BE_UUID = "شناسه صورت حساب باید یک UUID معتبر باشد",
BALANCE_IS_NOT_ENOUGH="مقدار برداشت بیشتر از موجودی است"
}
export const enum PaymentMessage {
+12
View File
@@ -0,0 +1,12 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString, Length } from "class-validator";
export class ConfirmwithdrawRequestDto {
@IsNotEmpty()
@IsString()
@Length(2, 50,)
@ApiProperty({ description: "name", example: "mahyar" })
transactionId: string;
}
@@ -1,20 +1,24 @@
import Decimal from "decimal.js";
import { Column, Entity, ManyToOne } from "typeorm";
import { Column, Entity, ManyToOne, OneToMany } from "typeorm";
import { BaseEntity } from "../../../common/entities/base.entity";
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
import { User } from "../../users/entities/user.entity";
import { WalletTransaction } from "../../wallets/entities/transaction.entity";
@Entity()
export class RewardWithdrawRequest extends BaseEntity {
@ManyToOne(() => User, (user) => user.referralRewards)
user: User;
@Column({ type: "decimal", precision: 10, scale: 2, nullable: true, transformer: new DecimalTransformer() })
requestedAmount?: Decimal;
@Column({ type: "decimal", precision: 10, scale: 2, transformer: new DecimalTransformer() })
requestedAmount: Decimal;
@Column({ type: "timestamp", nullable: true })
paidAt: Date;
@Column({ type: "varchar", length: 255, nullable: true })
transactionId?: string;
@OneToMany(() => WalletTransaction, (walletTransaction) => walletTransaction.withdrawRequest)
transactions: WalletTransaction[]
}
+15 -6
View File
@@ -1,4 +1,4 @@
import { Controller, Get, Post, Body, Delete, Param, Query } from '@nestjs/common';
import { Controller, Get, Post, Body, Delete, Param, Query, Patch } from '@nestjs/common';
import { ResellerService } from './reseller.service';
import { ApiOperation } from '@nestjs/swagger';
import { PermissionsDec } from '../../common/decorators/permission.decorator';
@@ -17,6 +17,7 @@ import { WalletsService } from '../wallets/providers/wallets.service';
import { CreateWithdrawRequestDto } from './dto/create-withdraw-request.dto';
import { CreateUserBankAccountDto } from './dto/create-user-bank-account.dto';
import { ResellerType } from '../auth/DTO/verify-reseller-otp.dto';
import { ConfirmwithdrawRequestDto } from './dto/confirm-withdraw-request.dto';
@Controller('reseller')
@AuthGuards()
@@ -45,12 +46,20 @@ export class ResellerController {
//=========== Reward Requests =========
@ApiOperation({ summary: "get reseller reward requests ==> admin route" })
@ApiOperation({ summary: "get withdraw requests ==> admin route" })
@AdminRoute()
@PermissionsDec(PermissionEnum.RESELLER)
@Get('reward/request')
getRewardRequests() {
return this.resellerService.findAll()
@Get('withdraw-request/admin')
getRewardRequests(@Query() query: SearchWithdrawRequestQueryDto) {
return this.resellerService.getwithdrawRequestsForAdmin(query)
}
@ApiOperation({ summary: "get withdraw requests ==> admin route" })
@AdminRoute()
@PermissionsDec(PermissionEnum.RESELLER)
@Patch('withdraw-request/:id')
confirmWithdrawRequests(@Param('id') requestId: string, @Body() dto: ConfirmwithdrawRequestDto) {
return this.resellerService.confirmWithdrawRequest(requestId, dto.transactionId)
}
// ************** Reseller Panel Endpoints ***********
@@ -148,7 +157,7 @@ export class ResellerController {
@ResellerRoute([ResellerType.agent, ResellerType.reseller])
@Get('withdraw-request')
getPayments(@UserDec("id") userId: string, @Body() dto: SearchWithdrawRequestQueryDto) {
return this.resellerService.getRewrdsRequests(userId, dto)
return this.resellerService.getWithdrawRequests(userId, dto)
}
// ============ Create Bank Account =============
+57 -5
View File
@@ -1,4 +1,4 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { UsersService } from '../users/providers/users.service';
import { CreateResellerDto } from './dto/create-reseller.dto';
import { ResellerRepository } from './repositories/reseller.repository';
@@ -15,10 +15,12 @@ import { ResellerType } from '../auth/DTO/verify-reseller-otp.dto';
import { PaginationDto } from '../../common/DTO/pagination.dto';
import { WalletsService } from '../wallets/providers/wallets.service';
import Decimal from 'decimal.js';
import { IsNull } from 'typeorm';
import { DataSource, IsNull } from 'typeorm';
@Injectable()
export class ResellerService {
protected readonly logger = new Logger(ResellerService.name);
constructor(
private readonly usersService: UsersService,
private readonly referralService: ReferralsService,
@@ -27,6 +29,7 @@ export class ResellerService {
private readonly withdrawRequestRepository: WithdrawRequestRepository,
private readonly userBankAccountRepository: UserBankAccountRepository,
private readonly walletsService: WalletsService,
private readonly dataSource: DataSource,
) { }
async create(dto: CreateResellerDto) {
@@ -111,6 +114,7 @@ export class ResellerService {
return this.referralService.findPaginatedReferedUsers(agentIds, query)
}
// *************** Invoice ***************
async findInvoices(userId: string, type: ResellerType, query: SearchResellerInvoicesQueryDto) {
if (type == ResellerType.agent) {
@@ -167,14 +171,14 @@ export class ResellerService {
}
//************* withdraw request ***********/
async getRewrdsRequestsForAdmin(queryDto: SearchWithdrawRequestQueryDto) {
async getwithdrawRequestsForAdmin(queryDto: SearchWithdrawRequestQueryDto) {
const [requests, count] = await this.withdrawRequestRepository.findPaginatedListForAdmin(queryDto);
return { requests, count, paginate: true };
}
async getRewrdsRequests(userId: string, queryDto: SearchWithdrawRequestQueryDto) {
async getWithdrawRequests(userId: string, queryDto: SearchWithdrawRequestQueryDto) {
const [requests, count] = await this.withdrawRequestRepository.findPaginatedList(userId, queryDto);
@@ -197,7 +201,7 @@ export class ResellerService {
if (pendingWithdrawRequest) {
throw new BadRequestException("امکان ایجاد درخواست برداشت جدید نیست")
}
const { balance } = await this.walletsService.getBalance(userId)
if (new Decimal(balance).lessThan(requestedAmount)) {
@@ -230,6 +234,44 @@ export class ResellerService {
return this.withdrawRequestRepository.remove(request)
}
async confirmWithdrawRequest(requestId: string,transactionId:string) {
const request = await this.withdrawRequestRepository.findOne({ where: { id: requestId }, relations: ['user'] })
if (!request) {
throw new BadRequestException("درخواست برداشت یافت نشد .")
}
if (request.paidAt) {
throw new BadRequestException("قبلا تایید شده است")
}
this.logger.log(`confirm withdraw request for request id ${requestId} `);
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const amount = new Decimal(request.requestedAmount)
await this.walletsService.createWithdrawTransaction(request.user.id, amount, request, queryRunner)
request.paidAt = new Date()
request.transactionId = transactionId
await queryRunner.manager.save(request)
await queryRunner.commitTransaction();
return true;
} catch (error) {
this.logger.error(`Failed to send announcement:`, error);
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
//********* Bank Account ***********/
async getUserBankAccount(userId: string) {
@@ -242,6 +284,16 @@ export class ResellerService {
async createUserBankAccount(userId: string, dto: CreateUserBankAccountDto) {
const { user } = await this.usersService.findOneById(userId)
const exitingBankAccount = await this.userBankAccountRepository.findOne({
where: {
IBan: dto.IBan
}
})
if (exitingBankAccount) {
throw new BadRequestException("شماره شبا موجود می باشد .")
}
const account = this.userBankAccountRepository.create({
user,
...dto
@@ -7,6 +7,7 @@ import { DecimalTransformer } from "../../../common/transformers/decimal.transfo
import { DepositRequest } from "../../payments/entities/deposit-request.entity";
import { Payment } from "../../payments/entities/payment.entity";
import { TransactionType } from "../enums/transaction-type.enum";
import { RewardWithdrawRequest } from "../../reseller/entities/rewards-withdraw-request.entity";
@Entity()
export class WalletTransaction extends BaseEntity {
@@ -30,4 +31,7 @@ export class WalletTransaction extends BaseEntity {
@ManyToOne(() => DepositRequest, (depositRequest) => depositRequest.transactions, { onDelete: "CASCADE", nullable: true })
depositRequest: DepositRequest | null;
@ManyToOne(() => RewardWithdrawRequest, (depositRequest) => depositRequest.transactions, { onDelete: "CASCADE", nullable: true })
withdrawRequest: DepositRequest | null;
}
@@ -14,13 +14,14 @@ import { Wallet } from "../entities/wallet.entity";
import { TransactionType } from "../enums/transaction-type.enum";
import { WalletTransactionsRepository } from "../repositories/wallet-transactions.repository";
import { WalletsRepository } from "../repositories/wallets.repository";
import { RewardWithdrawRequest } from "../../reseller/entities/rewards-withdraw-request.entity";
@Injectable()
export class WalletsService {
constructor(
private readonly walletsRepository: WalletsRepository,
private readonly walletsTransactionRepo: WalletTransactionsRepository,
) {}
) { }
//*********************************** */
async getBalance(userId: string) {
@@ -256,4 +257,28 @@ export class WalletsService {
if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND);
return wallet;
}
async createWithdrawTransaction(userId: string, amount: Decimal, withdrawRequest: RewardWithdrawRequest, queryRunner: QueryRunner) {
const wallet = await queryRunner.manager.findOne(Wallet, { where: { user: { id: userId } }, lock: { mode: "pessimistic_write" } });
if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND);
if (wallet.balance.lessThan(amount)) {
throw new BadRequestException(WalletMessage.BALANCE_IS_NOT_ENOUGH)
}
const transaction = queryRunner.manager.create(WalletTransaction, {
amount,
wallet: { id: wallet.id },
type: TransactionType.WITHDRAWAL,
description: WalletMessage.WITHDRAWAL_WALLET_TRANSFER,
withdrawRequest,
});
wallet.balance = new Decimal(wallet.balance).minus(transaction.amount);
await queryRunner.manager.save(Wallet, wallet);
await queryRunner.manager.save(WalletTransaction, transaction);
return transaction;
}
}