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 = "روش انتفال اجباری است", TRANSFER_METHOD_REQUIRED = "روش انتفال اجباری است",
GATEWAY_ID_SHOULD_BE_UUID = "آیدی درگاه پرداخت باید UUID باشد", GATEWAY_ID_SHOULD_BE_UUID = "آیدی درگاه پرداخت باید UUID باشد",
DEPOSIT_WALLET_TRANSFER = "شارژ کیف پول از طریق انتقال بانکی", DEPOSIT_WALLET_TRANSFER = "شارژ کیف پول از طریق انتقال بانکی",
WITHDRAWAL_WALLET_TRANSFER = "برداشت از کیف پول بابت واریز پول به حساب مشتری",
INSUFFICIENT_BALANCE = "موجودی کیف پول کافی نیست", INSUFFICIENT_BALANCE = "موجودی کیف پول کافی نیست",
SUBSCRIPTION_WALLET_TRANSFER = "پرداخت اشتراک از طریق کیف پول", SUBSCRIPTION_WALLET_TRANSFER = "پرداخت اشتراک از طریق کیف پول",
SUPPORT_PLAN_WALLET_TRANSFER = "پرداخت پلن پشتیبانی از طریق کیف پول", SUPPORT_PLAN_WALLET_TRANSFER = "پرداخت پلن پشتیبانی از طریق کیف پول",
@@ -356,6 +357,7 @@ export const enum WalletMessage {
REFERRAL_REWARD_WALLET_TRANSFER = "پرداخت پاداش ارجاع از طریق کیف پول", REFERRAL_REWARD_WALLET_TRANSFER = "پرداخت پاداش ارجاع از طریق کیف پول",
INVOICE_ID_REQUIRED = "شناسه صورت حساب مورد نیاز است", INVOICE_ID_REQUIRED = "شناسه صورت حساب مورد نیاز است",
INVOICE_ID_SHOULD_BE_UUID = "شناسه صورت حساب باید یک UUID معتبر باشد", INVOICE_ID_SHOULD_BE_UUID = "شناسه صورت حساب باید یک UUID معتبر باشد",
BALANCE_IS_NOT_ENOUGH="مقدار برداشت بیشتر از موجودی است"
} }
export const enum PaymentMessage { 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 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 { BaseEntity } from "../../../common/entities/base.entity";
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer"; import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
import { WalletTransaction } from "../../wallets/entities/transaction.entity";
@Entity() @Entity()
export class RewardWithdrawRequest extends BaseEntity { export class RewardWithdrawRequest extends BaseEntity {
@ManyToOne(() => User, (user) => user.referralRewards) @ManyToOne(() => User, (user) => user.referralRewards)
user: User; user: User;
@Column({ type: "decimal", precision: 10, scale: 2, nullable: true, transformer: new DecimalTransformer() }) @Column({ type: "decimal", precision: 10, scale: 2, transformer: new DecimalTransformer() })
requestedAmount?: Decimal; requestedAmount: Decimal;
@Column({ type: "timestamp", nullable: true }) @Column({ type: "timestamp", nullable: true })
paidAt: Date; paidAt: Date;
@Column({ type: "varchar", length: 255, nullable: true }) @Column({ type: "varchar", length: 255, nullable: true })
transactionId?: string; 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 { ResellerService } from './reseller.service';
import { ApiOperation } from '@nestjs/swagger'; import { ApiOperation } from '@nestjs/swagger';
import { PermissionsDec } from '../../common/decorators/permission.decorator'; 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 { CreateWithdrawRequestDto } from './dto/create-withdraw-request.dto';
import { CreateUserBankAccountDto } from './dto/create-user-bank-account.dto'; import { CreateUserBankAccountDto } from './dto/create-user-bank-account.dto';
import { ResellerType } from '../auth/DTO/verify-reseller-otp.dto'; import { ResellerType } from '../auth/DTO/verify-reseller-otp.dto';
import { ConfirmwithdrawRequestDto } from './dto/confirm-withdraw-request.dto';
@Controller('reseller') @Controller('reseller')
@AuthGuards() @AuthGuards()
@@ -45,12 +46,20 @@ export class ResellerController {
//=========== Reward Requests ========= //=========== Reward Requests =========
@ApiOperation({ summary: "get reseller reward requests ==> admin route" }) @ApiOperation({ summary: "get withdraw requests ==> admin route" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.RESELLER) @PermissionsDec(PermissionEnum.RESELLER)
@Get('reward/request') @Get('withdraw-request/admin')
getRewardRequests() { getRewardRequests(@Query() query: SearchWithdrawRequestQueryDto) {
return this.resellerService.findAll() 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 *********** // ************** Reseller Panel Endpoints ***********
@@ -148,7 +157,7 @@ export class ResellerController {
@ResellerRoute([ResellerType.agent, ResellerType.reseller]) @ResellerRoute([ResellerType.agent, ResellerType.reseller])
@Get('withdraw-request') @Get('withdraw-request')
getPayments(@UserDec("id") userId: string, @Body() dto: SearchWithdrawRequestQueryDto) { getPayments(@UserDec("id") userId: string, @Body() dto: SearchWithdrawRequestQueryDto) {
return this.resellerService.getRewrdsRequests(userId, dto) return this.resellerService.getWithdrawRequests(userId, dto)
} }
// ============ Create Bank Account ============= // ============ Create Bank Account =============
+56 -4
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 { UsersService } from '../users/providers/users.service';
import { CreateResellerDto } from './dto/create-reseller.dto'; import { CreateResellerDto } from './dto/create-reseller.dto';
import { ResellerRepository } from './repositories/reseller.repository'; 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 { PaginationDto } from '../../common/DTO/pagination.dto';
import { WalletsService } from '../wallets/providers/wallets.service'; import { WalletsService } from '../wallets/providers/wallets.service';
import Decimal from 'decimal.js'; import Decimal from 'decimal.js';
import { IsNull } from 'typeorm'; import { DataSource, IsNull } from 'typeorm';
@Injectable() @Injectable()
export class ResellerService { export class ResellerService {
protected readonly logger = new Logger(ResellerService.name);
constructor( constructor(
private readonly usersService: UsersService, private readonly usersService: UsersService,
private readonly referralService: ReferralsService, private readonly referralService: ReferralsService,
@@ -27,6 +29,7 @@ export class ResellerService {
private readonly withdrawRequestRepository: WithdrawRequestRepository, private readonly withdrawRequestRepository: WithdrawRequestRepository,
private readonly userBankAccountRepository: UserBankAccountRepository, private readonly userBankAccountRepository: UserBankAccountRepository,
private readonly walletsService: WalletsService, private readonly walletsService: WalletsService,
private readonly dataSource: DataSource,
) { } ) { }
async create(dto: CreateResellerDto) { async create(dto: CreateResellerDto) {
@@ -111,6 +114,7 @@ export class ResellerService {
return this.referralService.findPaginatedReferedUsers(agentIds, query) return this.referralService.findPaginatedReferedUsers(agentIds, query)
} }
// *************** Invoice *************** // *************** Invoice ***************
async findInvoices(userId: string, type: ResellerType, query: SearchResellerInvoicesQueryDto) { async findInvoices(userId: string, type: ResellerType, query: SearchResellerInvoicesQueryDto) {
if (type == ResellerType.agent) { if (type == ResellerType.agent) {
@@ -167,14 +171,14 @@ export class ResellerService {
} }
//************* withdraw request ***********/ //************* withdraw request ***********/
async getRewrdsRequestsForAdmin(queryDto: SearchWithdrawRequestQueryDto) { async getwithdrawRequestsForAdmin(queryDto: SearchWithdrawRequestQueryDto) {
const [requests, count] = await this.withdrawRequestRepository.findPaginatedListForAdmin(queryDto); const [requests, count] = await this.withdrawRequestRepository.findPaginatedListForAdmin(queryDto);
return { requests, count, paginate: true }; 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); const [requests, count] = await this.withdrawRequestRepository.findPaginatedList(userId, queryDto);
@@ -230,6 +234,44 @@ export class ResellerService {
return this.withdrawRequestRepository.remove(request) 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 ***********/ //********* Bank Account ***********/
async getUserBankAccount(userId: string) { async getUserBankAccount(userId: string) {
@@ -242,6 +284,16 @@ export class ResellerService {
async createUserBankAccount(userId: string, dto: CreateUserBankAccountDto) { async createUserBankAccount(userId: string, dto: CreateUserBankAccountDto) {
const { user } = await this.usersService.findOneById(userId) 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({ const account = this.userBankAccountRepository.create({
user, user,
...dto ...dto
@@ -7,6 +7,7 @@ import { DecimalTransformer } from "../../../common/transformers/decimal.transfo
import { DepositRequest } from "../../payments/entities/deposit-request.entity"; import { DepositRequest } from "../../payments/entities/deposit-request.entity";
import { Payment } from "../../payments/entities/payment.entity"; import { Payment } from "../../payments/entities/payment.entity";
import { TransactionType } from "../enums/transaction-type.enum"; import { TransactionType } from "../enums/transaction-type.enum";
import { RewardWithdrawRequest } from "../../reseller/entities/rewards-withdraw-request.entity";
@Entity() @Entity()
export class WalletTransaction extends BaseEntity { export class WalletTransaction extends BaseEntity {
@@ -30,4 +31,7 @@ export class WalletTransaction extends BaseEntity {
@ManyToOne(() => DepositRequest, (depositRequest) => depositRequest.transactions, { onDelete: "CASCADE", nullable: true }) @ManyToOne(() => DepositRequest, (depositRequest) => depositRequest.transactions, { onDelete: "CASCADE", nullable: true })
depositRequest: DepositRequest | null; depositRequest: DepositRequest | null;
@ManyToOne(() => RewardWithdrawRequest, (depositRequest) => depositRequest.transactions, { onDelete: "CASCADE", nullable: true })
withdrawRequest: DepositRequest | null;
} }
@@ -14,6 +14,7 @@ import { Wallet } from "../entities/wallet.entity";
import { TransactionType } from "../enums/transaction-type.enum"; import { TransactionType } from "../enums/transaction-type.enum";
import { WalletTransactionsRepository } from "../repositories/wallet-transactions.repository"; import { WalletTransactionsRepository } from "../repositories/wallet-transactions.repository";
import { WalletsRepository } from "../repositories/wallets.repository"; import { WalletsRepository } from "../repositories/wallets.repository";
import { RewardWithdrawRequest } from "../../reseller/entities/rewards-withdraw-request.entity";
@Injectable() @Injectable()
export class WalletsService { export class WalletsService {
@@ -256,4 +257,28 @@ export class WalletsService {
if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND); if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND);
return wallet; 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;
}
} }