import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { PaymentGatewayService } from './payment-gateway.service'; import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment'; import { Payment } from '../entities/payment.entity'; import { EntityManager, RequiredEntityData } from '@mikro-orm/core'; import { Order } from '../../orders/entities/order.entity'; import { PaymentMethod } from '../entities/payment-method.entity'; import { CreatePaymentDto } from '../dto/create-payment.dto'; import { User } from '../../users/entities/user.entity'; @Injectable() export class PaymentsService { constructor( private readonly em: EntityManager, private readonly paymentGatewayService: PaymentGatewayService, ) {} // async initializePayment( // paymentMethodId: string, // amount: number, // orderId: string, // ): Promise<{ paymentUrl: string | null }> { // // Validate amount // if (amount <= 0) { // throw new BadRequestException('Amount must be greater than zero'); // } // // Validate order exists // const order = await this.em.findOne(Order, { id: orderId }); // if (!order) { // throw new NotFoundException('Order not found'); // } // // Load restaurant payment method with payment method relationship // const paymentMethod = await this.em.findOne(PaymentMethod, { id: paymentMethodId }, { populate: ['restaurant'] }); // if (!paymentMethod) { // throw new NotFoundException('Payment method not found'); // } // if (paymentMethod.method === PaymentMethodEnum.Online && !paymentMethod.merchantId) { // throw new BadRequestException('Merchant ID is required for online payments'); // } // // Create payment record and save authority // const restaurantDomain = paymentMethod.restaurant.domain; // const gateway = paymentMethod.gateway; // const { authority } = await this.createPayment(restaurantDomain, { // amount, // orderId, // paymentMethod: paymentMethod.method, // merchantId: paymentMethod.merchantId ?? null, // gateway, // }); // const paymentUrl = this.paymentGatewayService.zarinpalPaymentUrl(gateway, authority); // return { paymentUrl }; // } async startPayment(orderId: string): Promise<{ paymentUrl: string | null }> { const { amount, method, restaurantDomain, gateway, merchantId, user } = await this.validateOrder(orderId); // Handle Wallet payment immediately if (method === PaymentMethodEnum.Wallet) { // Check wallet balance if (user.wallet < amount) { throw new BadRequestException('Insufficient wallet balance'); } // Deduct from wallet and create payment record const payment = await this.processWalletPayment(orderId, amount, user); return { paymentUrl: null }; // No redirect needed for wallet } const { authority } = await this.createPayment(restaurantDomain, { amount, orderId, method, merchantId, gateway, }); const paymentUrl = this.paymentGatewayService.zarinpalPaymentUrl(gateway, authority); return { paymentUrl }; } private async validateOrder(orderId: string) { const order = await this.em.findOne(Order, { id: orderId }, { populate: ['user', 'paymentMethod'] }); if (!order) { throw new NotFoundException('Order not found'); } const paymentMethod = order.paymentMethod; const amount = order.total; // Validate amount if (amount <= 0) { throw new BadRequestException('Amount must be greater than zero'); } // Load restaurant payment method with payment method relationship if (!paymentMethod) { throw new NotFoundException('Payment method not found'); } if (paymentMethod.method === PaymentMethodEnum.Online && !paymentMethod.merchantId) { throw new BadRequestException('Merchant ID is required for online payments'); } 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 }; } async createPayment(domain: string, dto: CreatePaymentDto) { const { amount, orderId, merchantId, gateway, method } = dto; const { authority } = await this.paymentGatewayService.requestToGateway( method, amount, orderId, merchantId, domain, gateway, ); const payment = this.em.create(Payment, { amount, authority: authority, order: this.em.getReference(Order, orderId), gateway, status: PaymentStatusEnum.Pending, } as RequiredEntityData); await this.em.persistAndFlush(payment); return { authority, payment }; } private async processWalletPayment(orderId: string, amount: number, user: User): Promise { return this.em.transactional(async em => { // Reload user to get latest wallet balance const freshUser = await em.findOne(User, { id: user.id }); if (!freshUser) { throw new NotFoundException('User not found'); } // Double-check balance if (freshUser.wallet < amount) { throw new BadRequestException('Insufficient wallet balance'); } // Deduct from wallet freshUser.wallet -= amount; em.persist(freshUser); // Create payment record const payment = em.create(Payment, { amount, authority: null, order: em.getReference(Order, orderId), gateway: null, status: PaymentStatusEnum.Paid, paidAt: new Date(), } as RequiredEntityData); em.persist(payment); // Update order payment status const order = await em.findOne(Order, { id: orderId }); if (order) { order.paymentStatus = PaymentStatusEnum.Paid; em.persist(order); } await em.flush(); return payment; }); } async verifyPayment(authority: string, orderId: string): Promise { // Find payment by authority and orderId const payment = await this.em.findOne( Payment, { authority, 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 this.em.findOne(PaymentMethod, { id: payment.order.paymentMethod.id }); if (!paymentMethod) { throw new NotFoundException('Payment method not found'); } // For Wallet payments, they're already paid during startPayment if (paymentMethod.method === PaymentMethodEnum.Wallet) { if (payment.status === PaymentStatusEnum.Paid) { return payment; } throw new BadRequestException('Wallet payment was not processed correctly'); } // For non-online payments, mark as paid directly // if (paymentMethod.method !== PaymentMethodEnum.Online) { // payment.status = PaymentStatusEnum.Paid; // payment.paidAt = new Date(); // await this.em.persistAndFlush(payment); // return payment; // } // 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; // Handle ZarinPal gateway verification if (payment.gateway === PaymentGatewayEnum.ZarinPal) { try { const verifyResponse = await this.paymentGatewayService.verifyZarinPalPayment({ merchantId: paymentMethod.merchantId, amount: verifyAmount, authority, }); // Type guard to ensure we have a valid response if (!verifyResponse || typeof verifyResponse !== 'object') { throw new BadRequestException('Invalid verification response from payment gateway'); } const response = verifyResponse; if (response.code === 100) { // Payment successful payment.status = PaymentStatusEnum.Paid; payment.refId = String(response.refId); payment.cardPan = response.cardPan; payment.verifyResponse = response as unknown as Record; payment.paidAt = new Date(); payment.order.paymentStatus = PaymentStatusEnum.Paid; } else { // Payment failed payment.status = PaymentStatusEnum.Failed; payment.verifyResponse = response as unknown as Record; payment.failedAt = new Date(); payment.order.paymentStatus = PaymentStatusEnum.Failed; } await this.em.persistAndFlush(payment); return payment; } catch (error: unknown) { // Mark payment as failed on error payment.status = PaymentStatusEnum.Failed; payment.failedAt = new Date(); payment.order.paymentStatus = PaymentStatusEnum.Failed; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; payment.verifyResponse = { error: errorMessage, }; await this.em.persistAndFlush(payment); throw new BadRequestException(`Payment verification failed: ${errorMessage}`); } } // If gateway is not supported, throw an error throw new BadRequestException(`Unsupported payment gateway: ${String(payment.gateway)}`); } // update(_id: number, _updatePaymentDto: unknown) { // return `This action updates a #${_id} payment`; // } // findAll() { // return this.em.find(Payment, {}); // } // remove(id: number) { // return `This action removes a #${id} payment`; // } }