import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment'; import { EntityManager } from '@mikro-orm/postgresql'; import { Order } from '../../order/entities/order.entity'; import { Logger } from '@nestjs/common'; import { GatewayManager } from '../gateways/gateway.manager'; import { OrderPaymentContext } from '../interface/payment'; import { PaymentMessage, OrderMessage } from 'src/common/enums/message.enum'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { onlinePaymentSucceedEvent, paymentSucceedEvent } from '../events/payment.events'; import { OrderService } from 'src/modules/order/providers/order.service'; import { PayOrderDto } from '../dto/pay-order.dto'; import { PaymentRepository } from '../repositories/payment.repository'; import { CreditRepository } from 'src/modules/user/repositories/credit.repository'; import { CreditService } from 'src/modules/user/providers/credit.service'; import { CreditTransactionType } from 'src/modules/user/interface/user'; import { Payment } from '../entities/payment.entity'; import { FindPaymentsDto } from '../dto/find-payments.dto'; import { Admin } from 'src/modules/admin/entities/admin.entity'; import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; @Injectable() export class PaymentService { private readonly logger = new Logger(PaymentService.name); constructor( private readonly em: EntityManager, private readonly orderService: OrderService, private readonly paymentRepository: PaymentRepository, private readonly gatewayManager: GatewayManager, private readonly eventEmitter: EventEmitter2, private readonly creditService: CreditService, private readonly creditRepository: CreditRepository, private readonly adminRepository: AdminRepository, ) { } async payOrder(orderId: string, dto: PayOrderDto, adminId?: string): Promise<{ paymentUrl: string | null }> { const ctx = await this.loadAndValidateOrder(orderId, dto, adminId) switch (dto.method) { case PaymentMethodEnum.Cash: await this.handleCashPayment(ctx); return { paymentUrl: null }; case PaymentMethodEnum.Credit: await this.handleCreditPayment(ctx); return { paymentUrl: null }; case PaymentMethodEnum.Online: return this.handleOnlinePayment(ctx); default: throw new BadRequestException(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD); } } private async loadAndValidateOrder(orderId: string, dto: PayOrderDto, adminId?: string): Promise { const { amount, method, attachments, description, gateway } = dto const order = await this.orderService.findOrderOrFail(orderId) if (!order) { throw new NotFoundException(OrderMessage.NOT_FOUND); } if (!order.balance) { throw new NotFoundException('Balance is not set'); } let admin: null | Admin = null if (adminId) { admin = await this.adminRepository.findOne({ id: adminId }) if (!admin) { throw new BadRequestException("Admin not found") } } if (amount <= 0) { throw new BadRequestException(PaymentMessage.AMOUNT_MUST_BE_GREATER_THAN_ZERO); } if (amount > order.balance) { throw new BadRequestException(`You cant pay more than ${order.balance}`); } return { admin, user: order.user, order, amount, attachments, description, gateway, method }; } private async handleCashPayment(ctx: OrderPaymentContext): Promise { const { order, amount, method, attachments, description, admin } = ctx const payment = this.paymentRepository.create({ creator: admin, order, amount, method, status: PaymentStatusEnum.Pending, attachments, description }) await this.em.persistAndFlush(payment) return } private async handleCreditPayment(ctx: OrderPaymentContext): Promise { const { order, amount, user, admin } = ctx await this.em.transactional(async em => { // 1. get User remained Credit const remainedCredit = await this.creditService.getCurrentCredit(user.id) if (remainedCredit < amount) { throw new BadRequestException("You Dont have enough Credit") } const newBalance = remainedCredit - amount; // 2. Create payment record const payment = this.paymentRepository.create({ creator: admin, amount: ctx.amount, method: PaymentMethodEnum.Credit, gateway: null, order, status: PaymentStatusEnum.Paid, paidAt: new Date(), }); // 3. Create Credit transaction record const newCreditTransaction = this.creditRepository.create({ user: order.user, amount, type: CreditTransactionType.WITHDRAW, balance: newBalance, orderId: order.id, }); em.persist([payment, newCreditTransaction]); await em.flush(); }); this.eventEmitter.emit( paymentSucceedEvent.name, new paymentSucceedEvent(ctx.order.id, String(ctx.order?.orderNumber) || '', ctx.method, ctx.amount), ); } private async handleOnlinePayment(ctx: OrderPaymentContext): Promise<{ paymentUrl: string }> { const { amount, order, gateway: requestedGateway, admin } = ctx const gateway = this.gatewayManager.get(requestedGateway!); const { token, paymentUrl } = await gateway.requestPayment({ amount: ctx.amount, orderId: ctx.order.id, }); const payment = this.paymentRepository.create({ creator: admin, order, amount, method: PaymentMethodEnum.Online, status: PaymentStatusEnum.Pending, gateway: requestedGateway, token, }) await this.em.persistAndFlush(payment); return { paymentUrl, }; } async verifyOnlinePayment(token: string): Promise { const payment = await this.em.transactional(async em => { const payment = await this.paymentRepository.findOne( { token }, { populate: ['order'] }, ); if (!payment) { throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND); } if (payment.status === PaymentStatusEnum.Paid) { return payment; } if (!payment.gateway) { throw new NotFoundException("Payment gateway not found"); } const gateway = this.gatewayManager.get(payment.gateway); const { raw, referenceId, success } = await gateway.verifyPayment({ amount: payment.amount, token: payment.token!, }); payment.verifyResponse = raw; if (!success) { payment.status = PaymentStatusEnum.Failed; payment.failedAt = new Date(); return payment; } payment.status = PaymentStatusEnum.Paid; payment.paidAt = new Date(); payment.transactionId = referenceId; // TODO : use decimal js // does need persist or not // payment.order.paidAmount! += payment.amount // payment.order.balance! -= payment.amount await em.flush(); return payment; }); this.eventEmitter.emit( onlinePaymentSucceedEvent.name, new onlinePaymentSucceedEvent(payment.id, payment.order.id, String(payment.order?.orderNumber) || '', payment.amount), ); return payment; } findAll(dto: FindPaymentsDto) { return this.paymentRepository.findAllPaginated(dto) } findAllByUser(dto: FindPaymentsDto, userId: string) { return this.paymentRepository.findAllPaginated({ ...dto, userId }) } async confirmCashPayment(paymentId: string): Promise { const payment = await this.em.transactional(async em => { const payment = await em.findOne(Payment, paymentId, { populate: ['order'] }); if (!payment) { throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND); } if (payment.method !== PaymentMethodEnum.Cash) { throw new BadRequestException(PaymentMessage.PAYMENT_METHOD_NOT_CASH); } if (payment.status === PaymentStatusEnum.Paid) { throw new BadRequestException(PaymentMessage.PAYMENT_ALREADY_PAID); } payment.status = PaymentStatusEnum.Paid; payment.paidAt = new Date(); payment.failedAt = null; // TODO : use decimal js // payment.order.paidAmount! += payment.amount // payment.order.balance! -= payment.amount em.persist(payment); em.persist(payment.order); await em.flush(); return payment; }); return payment; } async denyCashPayment(paymentId: string): Promise { const payment = await this.em.transactional(async em => { const payment = await em.findOne(Payment, paymentId, { populate: ['order'] }); if (!payment) { throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND); } if (payment.method !== PaymentMethodEnum.Cash) { throw new BadRequestException(PaymentMessage.PAYMENT_METHOD_NOT_CASH); } if (payment.status == PaymentStatusEnum.Paid) { // payment.order.paidAmount! -= payment.amount // payment.order.balance! += payment.amount em.persistAndFlush(payment.order) } payment.status = PaymentStatusEnum.Failed; payment.failedAt = new Date(); payment.paidAt = null em.persistAndFlush(payment); return payment; }); return payment; } }