216 lines
5.9 KiB
TypeScript
216 lines
5.9 KiB
TypeScript
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<OrderPaymentContext> {
|
|
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<void> {
|
|
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<void> {
|
|
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<Payment> {
|
|
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'] },
|
|
);
|
|
}
|
|
|
|
}
|