From 6871c8fd34028587fb75268b4ea6396e3eacc4bc Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 16 Apr 2026 11:23:10 +0330 Subject: [PATCH] withdraw request --- .../1776325917845-WithDraaRequest.ts | 18 ++++++ src/common/enums/message.enum.ts | 2 + .../dto/confirm-withdraw-request.dto.ts | 12 ++++ .../rewards-withdraw-request.entity.ts | 10 ++- src/modules/reseller/reseller.controller.ts | 21 +++++-- src/modules/reseller/reseller.service.ts | 62 +++++++++++++++++-- .../wallets/entities/transaction.entity.ts | 4 ++ .../wallets/providers/wallets.service.ts | 27 +++++++- 8 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 database/migrations/1776325917845-WithDraaRequest.ts create mode 100755 src/modules/reseller/dto/confirm-withdraw-request.dto.ts diff --git a/database/migrations/1776325917845-WithDraaRequest.ts b/database/migrations/1776325917845-WithDraaRequest.ts new file mode 100644 index 0000000..f068203 --- /dev/null +++ b/database/migrations/1776325917845-WithDraaRequest.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class WithDraaRequest1776325917845 implements MigrationInterface { + name = 'WithDraaRequest1776325917845' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`); + } + +} diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index c021481..c835eb2 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -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 { diff --git a/src/modules/reseller/dto/confirm-withdraw-request.dto.ts b/src/modules/reseller/dto/confirm-withdraw-request.dto.ts new file mode 100755 index 0000000..2bec94d --- /dev/null +++ b/src/modules/reseller/dto/confirm-withdraw-request.dto.ts @@ -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; + +} diff --git a/src/modules/reseller/entities/rewards-withdraw-request.entity.ts b/src/modules/reseller/entities/rewards-withdraw-request.entity.ts index 64d806f..f1cb634 100755 --- a/src/modules/reseller/entities/rewards-withdraw-request.entity.ts +++ b/src/modules/reseller/entities/rewards-withdraw-request.entity.ts @@ -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[] } diff --git a/src/modules/reseller/reseller.controller.ts b/src/modules/reseller/reseller.controller.ts index 1dd9def..492c529 100644 --- a/src/modules/reseller/reseller.controller.ts +++ b/src/modules/reseller/reseller.controller.ts @@ -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 ============= diff --git a/src/modules/reseller/reseller.service.ts b/src/modules/reseller/reseller.service.ts index b77a11b..0dca433 100644 --- a/src/modules/reseller/reseller.service.ts +++ b/src/modules/reseller/reseller.service.ts @@ -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 diff --git a/src/modules/wallets/entities/transaction.entity.ts b/src/modules/wallets/entities/transaction.entity.ts index 26fc051..cfbf680 100755 --- a/src/modules/wallets/entities/transaction.entity.ts +++ b/src/modules/wallets/entities/transaction.entity.ts @@ -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; } diff --git a/src/modules/wallets/providers/wallets.service.ts b/src/modules/wallets/providers/wallets.service.ts index 12dfde7..5a577b8 100755 --- a/src/modules/wallets/providers/wallets.service.ts +++ b/src/modules/wallets/providers/wallets.service.ts @@ -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; + } }