import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment'; import { Payment } from '../entities/payment.entity'; import { EntityManager } from '@mikro-orm/core'; import { Order } from '../../orders/entities/order.entity'; import { Logger } from '@nestjs/common'; import { GatewayManager } from './gateway.manager'; import { UserWallet } from 'src/modules/users/entities/user-wallet.entity'; import { OrderPaymentContext } from '../interface/payment'; @Injectable() export class PaymentsService { private readonly logger = new Logger(PaymentsService.name); constructor( private readonly em: EntityManager, private readonly gatewayManager: GatewayManager, ) { } async payOrder( orderId: string, restId: string, ): Promise<{ paymentUrl: string | null }> { const ctx = await this.loadAndValidateOrder(orderId, restId); switch (ctx.method) { case PaymentMethodEnum.Cash: await this.handleCashPayment(ctx); return { paymentUrl: null }; case PaymentMethodEnum.Wallet: await this.handleWalletPayment(ctx); return { paymentUrl: null }; case PaymentMethodEnum.Online: return this.handleOnlinePayment(ctx); default: throw new BadRequestException('Unsupported payment method'); } } private async loadAndValidateOrder( orderId: string, restId: string, ): Promise { const order = await this.em.findOne( Order, { id: orderId, restaurant: { id: restId } }, { populate: ['user', 'paymentMethod', 'paymentMethod.restaurant'] }, ); if (!order) { throw new NotFoundException('Order not found'); } if (order.total <= 0) { throw new BadRequestException('Amount must be greater than zero'); } const pm = order.paymentMethod; if (!pm) { throw new NotFoundException('Payment method not found'); } if (pm.method === PaymentMethodEnum.Online && !pm.merchantId) { throw new BadRequestException('Merchant ID is required for online payment'); } return { order, amount: order.total, method: pm.method, gateway: pm.gateway ?? null, merchantId: pm.merchantId ?? null, restaurantDomain: pm.restaurant.domain ?? null, }; } private async handleCashPayment(ctx: OrderPaymentContext): Promise { const payment = this.em.create(Payment, { amount: ctx.amount, order: ctx.order, method: PaymentMethodEnum.Cash, gateway: null, transactionId: null, status: PaymentStatusEnum.Pending, }); await this.em.persistAndFlush(payment); } private async handleWalletPayment(ctx: OrderPaymentContext): Promise { await this.em.transactional(async em => { const wallet = await em.findOne(UserWallet, { user: { id: ctx.order.user.id }, restaurant: { id: ctx.order.restaurant.id }, }); if (!wallet) { throw new NotFoundException('User wallet not found'); } if (wallet.wallet < ctx.amount) { throw new BadRequestException('Insufficient wallet balance'); } wallet.wallet -= ctx.amount; const payment = em.create(Payment, { amount: ctx.amount, order: ctx.order, method: PaymentMethodEnum.Wallet, gateway: null, status: PaymentStatusEnum.Paid, paidAt: new Date(), }); ctx.order.paymentStatus = PaymentStatusEnum.Paid; em.persist([wallet, payment, ctx.order]); await em.flush(); }); } private async handleOnlinePayment( ctx: OrderPaymentContext, ): Promise<{ paymentUrl: string }> { const gateway = this.gatewayManager.get(ctx.gateway!); const { transactionId } = await gateway.requestPayment({ amount: ctx.amount, orderId: ctx.order.id, merchantId: ctx.merchantId!, domain: ctx.restaurantDomain!, }); const payment = this.em.create(Payment, { amount: ctx.amount, order: ctx.order, method: PaymentMethodEnum.Online, gateway: ctx.gateway, transactionId, status: PaymentStatusEnum.Pending, }); await this.em.persistAndFlush(payment); return { paymentUrl: gateway.getPaymentUrl(transactionId), }; } async verifyPayment( transactionId: string, orderId: string, ): Promise { return this.em.transactional(async em => { const payment = await em.findOne( Payment, { transactionId, order: { id: orderId } }, { populate: ['order', 'order.paymentMethod'] }, ); if (!payment) { throw new NotFoundException('Payment not found'); } if (payment.status === PaymentStatusEnum.Paid) { return payment; } const pm = payment.order.paymentMethod; if (!pm?.merchantId || !payment.gateway) { throw new BadRequestException('Invalid payment configuration'); } const gateway = this.gatewayManager.get(payment.gateway); const result = await gateway.verifyPayment({ merchantId: pm.merchantId, amount: payment.amount, transactionId: payment.transactionId!, }); payment.verifyResponse = result.raw; if (!result.success) { payment.status = PaymentStatusEnum.Failed; payment.failedAt = new Date(); payment.order.paymentStatus = PaymentStatusEnum.Failed; } else { payment.status = PaymentStatusEnum.Paid; payment.referenceId = result.referenceId; payment.cardPan = result.cardPan; payment.paidAt = new Date(); payment.order.paymentStatus = PaymentStatusEnum.Paid; } await em.flush(); return payment; }); } findAllByRestaurantId(restId: string, userId: string) { return this.em.find( Payment, { order: { restaurant: { id: restId }, user: { id: userId } } }, { populate: ['order', 'order.paymentMethod'] }, ); } }