Files
dmenu-api/src/modules/payments/services/payments.service.ts
T
2025-12-03 23:23:32 +03:30

195 lines
6.5 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';
@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 createPayment(domain: string, dto: CreatePaymentDto) {
const { amount, orderId, merchantId, gateway, paymentMethod } = dto;
const { authority } = await this.paymentGatewayService.requestToGateway(
paymentMethod,
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 };
}
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 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`;
// }
}