import { InjectQueue } from "@nestjs/bullmq"; import { BadRequestException, HttpException, Injectable, InternalServerErrorException } from "@nestjs/common"; import { Queue } from "bullmq"; // eslint-disable-next-line import/no-named-as-default import Decimal from "decimal.js"; import { DataSource, Not, QueryRunner } from "typeorm"; import { PaginationDto } from "../../../common/DTO/pagination.dto"; import { CommonMessage, PaymentMessage, UserMessage, WalletMessage } from "../../../common/enums/message.enum"; import { Role } from "../../users/entities/role.entity"; import { User } from "../../users/entities/user.entity"; import { RoleEnum } from "../../users/enums/role.enum"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { Wallet } from "../../wallets/entities/wallet.entity"; import { WalletsService } from "../../wallets/providers/wallets.service"; import { PAYMENT } from "../constants"; import { CreateBankAccountDto } from "../DTO/create-bankaccount.dto"; import { GatewayDepositDto, TransferDepositDto } from "../DTO/deposit-wallet.dto"; import { PaymentTransactionQueryDto } from "../DTO/payment-transaction-query.dto"; import { SearchTransactionQueryDto } from "../DTO/search-transaction-query.dto"; import { UpdateBankAccountDto } from "../DTO/update-bankaccount.dto"; import { RejectDepositRequestDto } from "../DTO/update-deposit-request-status.dto"; import { VerifyQueryDto } from "../DTO/verify-payment.dto"; 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 { DepositRequestStatus } from "../enums/deposit-request-status.enum"; import { PaymentStatus } from "../enums/payment-status.enum"; import { PaymentGatewayFactory } from "../factories/payment.factory"; import { BankAccountsRepository } from "../repositories/bank-accounts.repository"; import { DepositRequestsRepository } from "../repositories/deposit-requests.repository"; import { PaymentGatewaysRepository } from "../repositories/payment-gateway.repository"; import { PaymentsRepository } from "../repositories/payments.repository"; import { GatewayType } from "../types/gateway.type"; @Injectable() export class PaymentsService { constructor( @InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME) private readonly paymentQueue: Queue, private readonly gatewayFactory: PaymentGatewayFactory, private readonly paymentGatewaysRepository: PaymentGatewaysRepository, private readonly paymentsRepository: PaymentsRepository, private readonly bankAccountsRepository: BankAccountsRepository, private readonly walletsService: WalletsService, private readonly depositRequestsRepository: DepositRequestsRepository, private dataSource: DataSource, ) {} //*********************************** */ async chargeWalletWithGateway(chargeDto: GatewayDepositDto, userId: string) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const { amount, gatewayId } = chargeDto; const user = await queryRunner.manager.findOneBy(User, { id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); const paymentGateway = await queryRunner.manager.findOneBy(PaymentGateway, { id: gatewayId }); if (!paymentGateway) throw new BadRequestException(PaymentMessage.PAYMENT_GATEWAY_NOT_FOUND); const gatewayData = await this.processPayment(paymentGateway.name, amount, WalletMessage.DEPOSIT_WALLET_IPG, user.email, user.phone); const payment = await this.createGatewayPaymentForUser(user, amount, gatewayData.reference, paymentGateway.id, queryRunner); await queryRunner.commitTransaction(); return { payment, ...gatewayData, }; } catch (error) { await queryRunner.rollbackTransaction(); if (error instanceof HttpException) throw error; throw new InternalServerErrorException(WalletMessage.ERROR_IN_CHARGE_WALLET); } finally { await queryRunner.release(); } } //*********************************** */ async chargeWalletWithTransfer(depositDto: TransferDepositDto, userId: string) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); const { amount, bankAccountId, method, transferReceiptUrl } = depositDto; try { const bankAccount = await queryRunner.manager.findOneBy(BankAccount, { id: bankAccountId }); if (!bankAccount) throw new BadRequestException(PaymentMessage.BANK_ACCOUNT_NOT_FOUND); const user = await queryRunner.manager.findOneBy(User, { id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); // if (method === TransferType.GATEWAY) throw new BadRequestException(PaymentMessage.TRANSFER_METHOD_NOT_ALLOWED); // const depositRequest = queryRunner.manager.create(DepositRequest, { bankAccount, user, receiptUrl: transferReceiptUrl, amount, type: method, }); await queryRunner.manager.save(DepositRequest, depositRequest); await queryRunner.commitTransaction(); return { message: PaymentMessage.DEPOSIT_CREATED, depositId: depositRequest.id, }; } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } //*********************************** */ //*********************************** */ async processPayment(provider: GatewayType, amount: number, description: string, email?: string, mobile?: string) { const paymentGateway = this.gatewayFactory.getPaymentGateway(provider); return paymentGateway.processPayment({ amount, description, email, mobile }); } //*********************************** */ //*********************************** */ async verifyPayment(gateway: GatewayType, queryDto: VerifyQueryDto) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const paymentGateway = this.gatewayFactory.getPaymentGateway(gateway); const payment = await queryRunner.manager.findOne(Payment, { where: { reference: queryDto.Authority }, relations: ["user"], // lock: { mode: "pessimistic_write" }, }); if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF); if (payment.status === PaymentStatus.COMPLETED) throw new BadRequestException(PaymentMessage.VALIDATED_BEFORE); if (queryDto.Status !== "OK") { await this.handleFailedPayment(payment.id, queryRunner); await queryRunner.commitTransaction(); return { message: "nok" }; } const verifyData = await paymentGateway.verifyPayment({ reference: queryDto.Authority, amount: payment.amount }); // if (verifyData.code === 100) { const transaction = await this.handleSuccessfulPayment( payment.id, payment.user.id, payment.amount, `${verifyData.ref_id}`, queryRunner, ); await queryRunner.commitTransaction(); return { message: "success", transaction, }; } if (verifyData.code === 101) { await queryRunner.commitTransaction(); return { message: "not valid payment", }; } await this.handleFailedPayment(payment.id, queryRunner); await queryRunner.commitTransaction(); return { message: "nok" }; } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } //*********************************** */ //*********************************** */ async createGatewayPaymentForUser(user: User, amount: number, reference: string, gatewayId: string, queryRunner: QueryRunner) { // const payment = queryRunner.manager.create(Payment, { amount, reference, user, paymentGateway: { id: gatewayId }, }); // await queryRunner.manager.save(Payment, payment); this.paymentQueue.add(PAYMENT.PAYMENT_START, { payment }, { delay: 1000 }); return payment; } //*********************************** */ //*********************************** */ async getAvailableGateways() { const paymentGateways = await this.paymentGatewaysRepository.find({ where: { isActive: true } }); return { paymentGateways }; } //*********************************** */ //*********************************** */ async getPaymentWithReference(reference: string) { const payment = await this.paymentsRepository.findOneWithReference(reference); if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF); return { payment }; } //*********************************** */ //*********************************** */ async handleFailedPayment(paymentId: string, queryRunner: QueryRunner) { await queryRunner.manager.update(Payment, { id: paymentId }, { status: PaymentStatus.CANCELLED }); } //*********************************** */ async handleSuccessfulPayment(paymentId: string, userId: string, amount: Decimal, transactionId: string, queryRunner: QueryRunner) { await queryRunner.manager.update(Payment, { id: paymentId }, { status: PaymentStatus.COMPLETED, transactionId }); const transaction = await this.walletsService.createPaymentTransaction(userId, amount, queryRunner); return transaction; } //*********************************** */ async addBankAccount(createDto: CreateBankAccountDto) { const existCardNumber = await this.bankAccountsRepository.findOneBy({ cardNumber: createDto.cardNumber }); if (existCardNumber) throw new BadRequestException(PaymentMessage.CARD_NUMBER_EXIST); // const existIBan = await this.bankAccountsRepository.findOneBy({ IBan: createDto.IBan }); if (existIBan) throw new BadRequestException(PaymentMessage.IBAN_EXIST); // const bankAccount = this.bankAccountsRepository.create({ ...createDto }); await this.bankAccountsRepository.save(bankAccount); return { message: CommonMessage.CREATED, bankAccount, }; } //*********************************** */ async updateBankAccount(updateDto: UpdateBankAccountDto, bankAccountId: string) { const bankAccount = await this.bankAccountsRepository.findOneBy({ id: bankAccountId }); if (!bankAccount) throw new BadRequestException(PaymentMessage.BANK_ACCOUNT_NOT_FOUND); if (updateDto.cardNumber) { const existCardNumber = await this.bankAccountsRepository.findOneBy({ cardNumber: updateDto.cardNumber, id: Not(bankAccountId) }); if (existCardNumber) throw new BadRequestException(PaymentMessage.CARD_NUMBER_EXIST); } // if (updateDto.IBan) { const existIBan = await this.bankAccountsRepository.findOneBy({ IBan: updateDto.IBan, id: Not(bankAccountId) }); if (existIBan) throw new BadRequestException(PaymentMessage.IBAN_EXIST); } await this.bankAccountsRepository.save({ ...bankAccount, ...updateDto }); } //*********************************** */ async getBankAccounts(role: Role) { const userRole = role as unknown as RoleEnum; let bankAccounts: BankAccount[]; if (userRole === RoleEnum.USER) { bankAccounts = await this.bankAccountsRepository.find({ where: { isActive: true } }); } else { bankAccounts = await this.bankAccountsRepository.find(); } return { bankAccounts, }; } //*********************************** */ async getBankAccountById(bankAccountId: string) { const bankAccount = await this.bankAccountsRepository.findOneBy({ id: bankAccountId }); if (!bankAccount) throw new BadRequestException(PaymentMessage.BANK_ACCOUNT_NOT_FOUND); return { bankAccount, }; } // async deleteBankAccount() //*********************************** */ async getTransactions(queryDto: SearchTransactionQueryDto) { return this.walletsService.getTransactionsForAdmin(queryDto); } //*********************************** */ async getDepositRequests(queryDto: PaymentTransactionQueryDto) { const { limit, skip } = PaginationUtils(queryDto); const { type } = queryDto; const queryBuilder = this.depositRequestsRepository .createQueryBuilder("depositRequest") .leftJoinAndSelect("depositRequest.user", "user") .leftJoinAndSelect("depositRequest.bankAccount", "bankAccount") .orderBy("depositRequest.createdAt", "DESC") .skip(skip) .take(limit); if (type) { queryBuilder.andWhere("depositRequest.type = :type", { type }); } const [depositRequests, count] = await queryBuilder.getManyAndCount(); return { depositRequests, count, paginate: true, }; } //*********************************** */ async getDepositGatewayPayment(queryDto: PaginationDto) { const { limit, skip } = PaginationUtils(queryDto); const queryBuilder = this.paymentsRepository .createQueryBuilder("payment") .leftJoinAndSelect("payment.user", "user") .leftJoinAndSelect("payment.paymentGateway", "paymentGateway") .orderBy("payment.createdAt", "DESC") .skip(skip) .take(limit); const [payments, count] = await queryBuilder.getManyAndCount(); return { payments, count, paginate: true, }; } //*********************************** */ async getDepositRequestById(depositId: string) { const depositRequest = await this.depositRequestsRepository.findOneBy({ id: depositId }); if (!depositRequest) throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND); return { depositRequest, }; } //*********************************** */ async approverDepositRequest(depositId: string) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const depositRequest = await queryRunner.manager.findOne(DepositRequest, { where: { id: depositId }, lock: { mode: "pessimistic_write" }, }); if (!depositRequest) throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND); if (depositRequest.status === DepositRequestStatus.APPROVED) throw new BadRequestException(PaymentMessage.DEPOSIT_ALREADY_APPROVED); const wallet = await queryRunner.manager.findOneBy(Wallet, { user: depositRequest.user }); if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND); await queryRunner.manager.update(Wallet, { id: wallet.id }, { balance: new Decimal(wallet.balance).add(depositRequest.amount) }); await queryRunner.manager.update(DepositRequest, { id: depositRequest.id }, { status: DepositRequestStatus.APPROVED }); await this.walletsService.createDepositTransaction(depositRequest.amount, wallet.id, queryRunner); await queryRunner.commitTransaction(); return { message: PaymentMessage.DEPOSIT_APPROVED, depositRequest, }; } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } //*********************************** */ async rejectDepositRequest(depositId: string, rejectDto: RejectDepositRequestDto) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const depositRequest = await queryRunner.manager.findOne(DepositRequest, { where: { id: depositId }, lock: { mode: "pessimistic_write" }, }); if (!depositRequest) throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND); if (depositRequest.status === DepositRequestStatus.REJECTED) throw new BadRequestException(PaymentMessage.DEPOSIT_ALREADY_REJECTED); await queryRunner.manager.update( DepositRequest, { id: depositRequest.id }, { status: DepositRequestStatus.REJECTED, comment: rejectDto.comment }, ); await queryRunner.commitTransaction(); return { message: PaymentMessage.DEPOSIT_REJECTED, depositRequest, }; } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } }