refactor payment
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
export interface OrderPaymentContext {
|
||||
order: Order;
|
||||
amount: number;
|
||||
method: PaymentMethodEnum;
|
||||
gateway: PaymentGatewayEnum | null;
|
||||
merchantId: string | null;
|
||||
restaurantDomain: string | null;
|
||||
}
|
||||
|
||||
@@ -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<Payment>);
|
||||
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<Payment>);
|
||||
|
||||
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<OrderPaymentContext> {
|
||||
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<void> {
|
||||
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<Payment> {
|
||||
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<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');
|
||||
}
|
||||
|
||||
// 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<Payment>);
|
||||
});
|
||||
|
||||
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<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();
|
||||
@@ -148,80 +204,6 @@ export class PaymentsService {
|
||||
});
|
||||
}
|
||||
|
||||
async verifyPayment(transactionId: string, orderId: string): Promise<Payment> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user