diff --git a/src/modules/payments/entities/payment.entity.ts b/src/modules/payments/entities/payment.entity.ts index 974c8df..546f41b 100644 --- a/src/modules/payments/entities/payment.entity.ts +++ b/src/modules/payments/entities/payment.entity.ts @@ -1,7 +1,7 @@ import { Entity, ManyToOne, Property, Enum } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Order } from '../../orders/entities/order.entity'; -import { PaymentGatewayEnum, PaymentStatusEnum } from '../interface/payment'; +import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment'; @Entity({ tableName: 'payments' }) export class Payment extends BaseEntity { @@ -14,6 +14,9 @@ export class Payment extends BaseEntity { @Property({ unique: true, nullable: true }) referenceId?: string | null; + @Enum(() => PaymentMethodEnum) + method!: PaymentMethodEnum; + @Enum(() => PaymentGatewayEnum) gateway?: PaymentGatewayEnum | null = null; diff --git a/src/modules/payments/interface/payment.ts b/src/modules/payments/interface/payment.ts index 1ee1872..7f07883 100644 --- a/src/modules/payments/interface/payment.ts +++ b/src/modules/payments/interface/payment.ts @@ -1,3 +1,5 @@ +import type{ Order } from "src/modules/orders/entities/order.entity"; + export enum PaymentMethodEnum { Online = 'Online', Cash = 'Cash', @@ -18,4 +20,13 @@ export interface ICreatePayment { method: PaymentMethodEnum; transactionId: string; gateway?: PaymentGatewayEnum | null; -} \ No newline at end of file +} + +export interface OrderPaymentContext { + order: Order; + amount: number; + method: PaymentMethodEnum; + gateway: PaymentGatewayEnum | null; + merchantId: string | null; + restaurantDomain: string | null; +} diff --git a/src/modules/payments/services/payments.service.ts b/src/modules/payments/services/payments.service.ts index 0eabe8e..1a05628 100644 --- a/src/modules/payments/services/payments.service.ts +++ b/src/modules/payments/services/payments.service.ts @@ -1,12 +1,12 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment'; +import { PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment'; import { Payment } from '../entities/payment.entity'; -import { EntityManager, RequiredEntityData } from '@mikro-orm/core'; +import { EntityManager } from '@mikro-orm/core'; import { Order } from '../../orders/entities/order.entity'; -import { PaymentMethod } from '../entities/payment-method.entity'; - import { Logger } from '@nestjs/common'; +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 { @@ -17,130 +17,186 @@ export class PaymentsService { private readonly gatewayManager: GatewayManager, ) { } - async payOrder(orderId: string, restId: string): Promise<{ paymentUrl: string | null }> { - const { amount, method, restaurantDomain, gateway, merchantId, user } = - await this.validateOrder(orderId, restId); + async payOrder( + orderId: string, + restId: string, + ): Promise<{ paymentUrl: string | null }> { + const ctx = await this.loadAndValidateOrder(orderId, restId); - // Handle Wallet payment immediately - if (method === PaymentMethodEnum.Cash) { - const payment = this.em.create(Payment, { - amount, - transactionId: null, - order: this.em.getReference(Order, orderId), - gateway: null, - method, - status: PaymentStatusEnum.Pending, - } as RequiredEntityData); + switch (ctx.method) { + case PaymentMethodEnum.Cash: + await this.handleCashPayment(ctx); + return { paymentUrl: null }; - await this.em.persistAndFlush(payment); + case PaymentMethodEnum.Wallet: + await this.handleWalletPayment(ctx); + return { paymentUrl: null }; + case PaymentMethodEnum.Online: + return this.handleOnlinePayment(ctx); + + default: + throw new BadRequestException('Unsupported payment method'); } - if (method === PaymentMethodEnum.Wallet) { - const userWallet = await this.em.findOne(UserWallet, { user: { id: user.id }, restaurant: { id: restId } }); - // Check wallet balance - if (!userWallet) { - throw new NotFoundException('User wallet not found'); - } - if (userWallet.wallet < amount) { - throw new BadRequestException('Insufficient wallet balance'); - } - - // Deduct from wallet and create payment record - await this.processWalletPayment(orderId, amount, user.id, restId); - - return { paymentUrl: null }; // No redirect needed for wallet - } - const gatewayInstance = this.gatewayManager.get(gateway as PaymentGatewayEnum); - const { transactionId } = await gatewayInstance.requestPayment({ - amount, - orderId, - merchantId: merchantId as string, - domain: restaurantDomain, - }); - - const payment = this.em.create(Payment, { - amount, - transactionId, - order: this.em.getReference(Order, orderId), - gateway, - method, - status: PaymentStatusEnum.Pending, - } as RequiredEntityData); - - await this.em.persistAndFlush(payment); - - return { paymentUrl: gatewayInstance.getPaymentUrl(transactionId) }; } - private async validateOrder(orderId: string, restId: string) { + private async loadAndValidateOrder( + orderId: string, + restId: string, + ): Promise { const order = await this.em.findOne( Order, { id: orderId, restaurant: { id: restId } }, - { populate: ['user', 'paymentMethod'] }, + { populate: ['user', 'paymentMethod', 'paymentMethod.restaurant'] }, ); + if (!order) { throw new NotFoundException('Order not found'); } - const paymentMethod = order.paymentMethod; - const amount = order.total; - // Validate amount - if (amount <= 0) { + if (order.total <= 0) { throw new BadRequestException('Amount must be greater than zero'); } - // Load restaurant payment method with payment method relationship - if (!paymentMethod) { + const pm = order.paymentMethod; + if (!pm) { throw new NotFoundException('Payment method not found'); } - if (paymentMethod.method === PaymentMethodEnum.Online && !paymentMethod.merchantId) { - throw new BadRequestException('Merchant ID is required for online payments'); + if (pm.method === PaymentMethodEnum.Online && !pm.merchantId) { + throw new BadRequestException('Merchant ID is required for online payment'); } - const merchantId = paymentMethod.merchantId; - const user = order.user; - // Create payment record and save authority - const restaurantDomain = paymentMethod.restaurant.domain; - const gateway = paymentMethod.gateway; - const method = paymentMethod.method; - return { amount, method, restaurantDomain, gateway, merchantId, user }; + + 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, + }); - private async processWalletPayment(orderId: string, amount: number, userId: string, restId: string): Promise { - return this.em.transactional(async em => { - // Reload user to get latest wallet balance - const userWallet = await em.findOne(UserWallet, { user: { id: userId }, restaurant: { id: restId } }); - if (!userWallet) { + 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'); } - // Double-check balance - if (userWallet.wallet < amount) { + if (wallet.wallet < ctx.amount) { throw new BadRequestException('Insufficient wallet balance'); } - // Deduct from wallet - userWallet.wallet -= amount; - em.persist(userWallet); + wallet.wallet -= ctx.amount; - // Create payment record const payment = em.create(Payment, { - amount, - authority: null, - order: em.getReference(Order, orderId), + amount: ctx.amount, + order: ctx.order, + method: PaymentMethodEnum.Wallet, gateway: null, status: PaymentStatusEnum.Paid, paidAt: new Date(), - } as RequiredEntityData); + }); - em.persist(payment); + ctx.order.paymentStatus = PaymentStatusEnum.Paid; - // Update order payment status - const order = await em.findOne(Order, { id: orderId }); - if (order) { - order.paymentStatus = PaymentStatusEnum.Paid; - em.persist(order); + 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(); @@ -148,80 +204,6 @@ export class PaymentsService { }); } - async verifyPayment(transactionId: string, orderId: string): Promise { - return this.em.transactional(async em => { - - // Find payment by authority and orderId - const payment = await em.findOne( - Payment, - { transactionId, order: { id: orderId } }, - { populate: ['order', 'order.paymentMethod'] }, - ); - if (!payment) { - throw new NotFoundException('Payment not found'); - } - - // If payment is already verified, return it - if (payment.status === PaymentStatusEnum.Paid) { - return payment; - } - - // Get payment method from order (already populated) - if (!payment.order.paymentMethod) { - throw new NotFoundException('Payment method not found for this order'); - } - - const paymentMethod = await em.findOne(PaymentMethod, { id: payment.order.paymentMethod.id }); - if (!paymentMethod) { - throw new NotFoundException('Payment method not found'); - } - - // Online payments require gateway verification - if (!paymentMethod.merchantId) { - throw new BadRequestException('Merchant ID is required for online payment verification'); - } - - if (!payment.gateway) { - throw new BadRequestException('Payment gateway is required for online payment verification'); - } - - // Use payment amount - const verifyAmount = payment.amount; - - const gatewayInstance = this.gatewayManager.get(payment.gateway as PaymentGatewayEnum); - - - const verifyResponse = await gatewayInstance.verifyPayment({ - merchantId: paymentMethod.merchantId, - amount: verifyAmount, - transactionId: payment.transactionId as string, - }); - - const { success, referenceId, cardPan, raw } = verifyResponse; - - if (!success) { - // Payment failed - payment.status = PaymentStatusEnum.Failed; - payment.verifyResponse = raw; - payment.failedAt = new Date(); - payment.order.paymentStatus = PaymentStatusEnum.Failed; - } - // Payment successful - payment.status = PaymentStatusEnum.Paid; - payment.referenceId = referenceId - - payment.cardPan = cardPan; - payment.verifyResponse = raw; - payment.paidAt = new Date(); - payment.order.paymentStatus = PaymentStatusEnum.Paid; - - - await em.persistAndFlush(payment); - return payment; - }) - } - - findAllByRestaurantId(restId: string, userId: string) { return this.em.find( Payment,