Files
dmenu-api/src/modules/payments/services/payments.service.ts
T
2025-12-07 16:57:28 +03:30

304 lines
10 KiB
TypeScript

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<Payment>);
await this.em.persistAndFlush(payment);
return { authority, payment };
}
private async processWalletPayment(orderId: string, amount: number, user: User): Promise<Payment> {
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<Payment>);
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<Payment> {
// 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<string, any>;
payment.paidAt = new Date();
payment.order.paymentStatus = PaymentStatusEnum.Paid;
} else {
// Payment failed
payment.status = PaymentStatusEnum.Failed;
payment.verifyResponse = response as unknown as Record<string, any>;
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`;
// }
}