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, QueryRunner } from "typeorm"; import { PaymentMessage, WalletMessage } from "../../../common/enums/message.enum"; import { User } from "../../users/entities/user.entity"; import { Wallet } from "../../wallets/entities/wallet.entity"; import { WalletsService } from "../../wallets/providers/wallets.service"; import { PAYMENT } from "../constants"; import { GatewayDepositDto } from "../DTO/deposit-wallet.dto"; import { VerifyQueryDto } from "../DTO/verify-payment.dto"; import { PaymentMethod } from "../entities/payment-method.entity"; import { Payment } from "../entities/payment.entity"; import { PaymentStatus } from "../enums/payment-status.enum"; import { PaymentType } from "../enums/payment-type.enum"; import { PaymentGatewayFactory } from "../factories/payment.factory"; 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 paymentsRepository: PaymentsRepository, private readonly walletsService: WalletsService, private dataSource: DataSource, ) {} //*********************************** */ async chargeWalletWithGateway(chargeDto: GatewayDepositDto, userId: string) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const { amount, gateway } = chargeDto; const wallet = await queryRunner.manager.findOne(Wallet, { where: { user: { id: userId } }, relations: ["user"] }); if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND); const gatewayData = await this.processPayment( gateway, amount, WalletMessage.DEPOSIT_WALLET_IPG, wallet.user.email, wallet.user.phone, ); const payment = await this.createPaymentForUser(wallet.user, amount, gatewayData.reference, gateway, 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 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) { return { message: "completed 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 createPaymentForUser(user: User, amount: number, reference: string, gateway: GatewayType, queryRunner: QueryRunner) { const paymentMethod = queryRunner.manager.create(PaymentMethod, { gatewayName: gateway, description: WalletMessage.DEPOSIT_WALLET_IPG, }); await queryRunner.manager.save(PaymentMethod, paymentMethod); const payment = queryRunner.manager.create(Payment, { type: PaymentType.GATEWAY, amount, reference, user, paymentMethod, }); await queryRunner.manager.save(Payment, payment); this.paymentQueue.add(PAYMENT.PAYMENT_START, { payment }, { delay: 1000 }); return payment; } //*********************************** */ //*********************************** */ async getAvailableGateways() { return this.gatewayFactory.getAvailablePaymentGateways(); } //*********************************** */ //*********************************** */ 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; } }