online payment
This commit is contained in:
@@ -1,16 +1,22 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment';
|
||||
import { Payment } from '../entities/payment.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Order } from '../../order/entities/order.entity';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { GatewayManager } from '../gateways/gateway.manager';
|
||||
import { OrderPaymentContext } from '../interface/payment';
|
||||
import { OrderStatusEnum } from 'src/modules/order/interface/order.interface';
|
||||
import { ChartPeriodEnum, PaymentChartDto } from '../dto/payment-chart.dto';
|
||||
import { PaymentMessage, OrderMessage } from 'src/common/enums/message.enum';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { onlinePaymentSucceedEvent, paymentSucceedEvent } from '../events/payment.events';
|
||||
import { OrderService } from 'src/modules/order/providers/order.service';
|
||||
import { PayOrderDto } from '../dto/pay-order.dto';
|
||||
import { PaymentRepository } from '../repositories/payment.repository';
|
||||
import { CreditRepository } from 'src/modules/user/repositories/credit.repository';
|
||||
import { CreditService } from 'src/modules/user/providers/credit.service';
|
||||
import { CreditTransactionType } from 'src/modules/user/interface/credit';
|
||||
import { Payment } from '../entities/payment.entity';
|
||||
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class PaymentsService {
|
||||
@@ -18,218 +24,204 @@ export class PaymentsService {
|
||||
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly orderService: OrderService,
|
||||
private readonly paymentRepository: PaymentRepository,
|
||||
private readonly gatewayManager: GatewayManager,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly creditService: CreditService,
|
||||
private readonly creditRepository: CreditRepository,
|
||||
) { }
|
||||
|
||||
// async payOrder(orderId: string): Promise<{ paymentUrl: string | null }> {
|
||||
// const ctx = await this.loadAndValidateOrder(orderId);
|
||||
async payOrder(orderId: string, dto: PayOrderDto): Promise<{ paymentUrl: string | null }> {
|
||||
|
||||
// // Idempotency: avoid creating/charging again for already-paid orders
|
||||
// if (ctx.order.status === OrderStatus.PAID) {
|
||||
// return { paymentUrl: null };
|
||||
// }
|
||||
const { method } = dto
|
||||
|
||||
// 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(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD);
|
||||
// }
|
||||
// }
|
||||
|
||||
// private async loadAndValidateOrder(orderId: string): Promise<OrderPaymentContext> {
|
||||
// const order = await this.em.findOne(
|
||||
// Order,
|
||||
// { id: orderId },
|
||||
// { populate: ['user', 'restaurant', 'paymentMethod', 'paymentMethod.restaurant'] },
|
||||
// );
|
||||
|
||||
// if (!order) {
|
||||
// throw new NotFoundException(OrderMessage.NOT_FOUND);
|
||||
// }
|
||||
|
||||
// if (order.total <= 0) {
|
||||
// throw new BadRequestException(PaymentMessage.AMOUNT_MUST_BE_GREATER_THAN_ZERO);
|
||||
// }
|
||||
|
||||
// const pm = order.paymentMethod;
|
||||
// if (!pm) {
|
||||
// throw new NotFoundException(PaymentMessage.PAYMENT_METHOD_NOT_FOUND);
|
||||
// }
|
||||
|
||||
// if (pm.method === PaymentMethodEnum.Online) {
|
||||
// if (!pm.gateway) throw new BadRequestException(PaymentMessage.GATEWAY_REQUIRED);
|
||||
// if (!pm.merchantId) throw new BadRequestException(PaymentMessage.MERCHANT_ID_REQUIRED);
|
||||
// if (!pm.restaurant?.domain) throw new BadRequestException(PaymentMessage.RESTAURANT_DOMAIN_REQUIRED);
|
||||
// }
|
||||
|
||||
// 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> {
|
||||
// await this.getOrCreateLatestPendingPayment(ctx.order.id, {
|
||||
// amount: ctx.amount,
|
||||
// method: PaymentMethodEnum.Cash,
|
||||
// gateway: null,
|
||||
// });
|
||||
// }
|
||||
const ctx = await this.loadAndValidateOrder(orderId, dto)
|
||||
|
||||
|
||||
// private async handleWalletPayment(ctx: OrderPaymentContext): Promise<void> {
|
||||
// await this.em.transactional(async em => {
|
||||
// const order = await em.findOne(Order, { id: ctx.order.id }, { populate: ['user', 'restaurant'] });
|
||||
// if (!order) throw new NotFoundException(OrderMessage.NOT_FOUND);
|
||||
switch (method) {
|
||||
case PaymentMethodEnum.Cash:
|
||||
await this.handleCashPayment(ctx);
|
||||
return { paymentUrl: null };
|
||||
|
||||
// if (order.status === OrderStatus.PAID) {
|
||||
// return;
|
||||
// }
|
||||
case PaymentMethodEnum.Credit:
|
||||
await this.handleCreditPayment(ctx);
|
||||
return { paymentUrl: null };
|
||||
|
||||
// const walletTransaction = await em.findOne(WalletTransaction, {
|
||||
// user: { id: order.user.id },
|
||||
// restaurant: { id: order.restaurant.id },
|
||||
// }, {
|
||||
// orderBy: { createdAt: 'DESC' }
|
||||
// });
|
||||
case PaymentMethodEnum.Online:
|
||||
return this.handleOnlinePayment(ctx);
|
||||
|
||||
// if (!walletTransaction) {
|
||||
// throw new NotFoundException('User wallet not found');
|
||||
// }
|
||||
|
||||
// if (walletTransaction.balance < ctx.amount) {
|
||||
// throw new BadRequestException('Insufficient wallet balance');
|
||||
// }
|
||||
|
||||
// const newBalance = walletTransaction.balance - ctx.amount;
|
||||
default:
|
||||
throw new BadRequestException(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// const payment = await this.getOrCreateLatestPendingPayment(order.id, {
|
||||
// em,
|
||||
// amount: ctx.amount,
|
||||
// method: PaymentMethodEnum.Wallet,
|
||||
// gateway: null,
|
||||
// });
|
||||
private async loadAndValidateOrder(orderId: string, dto: PayOrderDto): Promise<OrderPaymentContext> {
|
||||
const { amount, method, attachments, description, gateway } = dto
|
||||
|
||||
// const newWalletTransaction = em.create(WalletTransaction, {
|
||||
// user: order.user,
|
||||
// restaurant: order.restaurant,
|
||||
// amount: ctx.amount,
|
||||
// type: WalletTransactionType.DEBIT,
|
||||
// reason: WalletTransactionReason.ORDER_PAYMENT,
|
||||
// balance: newBalance,
|
||||
// });
|
||||
const order = await this.orderService.findOneOrFail(orderId)
|
||||
|
||||
// payment.status = PaymentStatusEnum.Paid;
|
||||
// payment.paidAt = new Date();
|
||||
// order.status = OrderStatus.PAID;
|
||||
if (!order) {
|
||||
throw new NotFoundException(OrderMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
// em.persist([payment, order, newWalletTransaction]);
|
||||
// await em.flush();
|
||||
// });
|
||||
// this.eventEmitter.emit(
|
||||
// paymentSucceedEvent.name,
|
||||
// new paymentSucceedEvent(ctx.order.id, ctx.order.restaurant.id, String(ctx.order?.orderNumber) || '', ctx.method, ctx.amount),
|
||||
// );
|
||||
// }
|
||||
if (order.balance == 0) {
|
||||
throw new BadRequestException("you can not pay beause Balance is zero")
|
||||
}
|
||||
|
||||
// private async handleOnlinePayment(ctx: OrderPaymentContext): Promise<{ paymentUrl: string }> {
|
||||
// const gateway = this.gatewayManager.get(ctx.gateway!);
|
||||
|
||||
// const payment = await this.getOrCreateLatestPendingPayment(ctx.order.id, {
|
||||
// amount: ctx.amount,
|
||||
// method: PaymentMethodEnum.Online,
|
||||
// gateway: ctx.gateway!,
|
||||
// });
|
||||
if (amount <= 0) {
|
||||
throw new BadRequestException(PaymentMessage.AMOUNT_MUST_BE_GREATER_THAN_ZERO);
|
||||
}
|
||||
|
||||
// // If we already requested a gateway transaction, just return the same URL (idempotent)
|
||||
// if (payment.transactionId) {
|
||||
// return { paymentUrl: gateway.getPaymentUrl(payment.transactionId) };
|
||||
// }
|
||||
|
||||
// const { transactionId } = await gateway.requestPayment({
|
||||
// amount: ctx.amount,
|
||||
// orderId: ctx.order.id,
|
||||
// merchantId: ctx.merchantId!,
|
||||
// domain: ctx.restaurantDomain!,
|
||||
// });
|
||||
return {
|
||||
user: order.user,
|
||||
order,
|
||||
amount,
|
||||
attachments,
|
||||
description,
|
||||
gateway,
|
||||
method
|
||||
};
|
||||
}
|
||||
|
||||
// payment.gateway = ctx.gateway;
|
||||
// payment.transactionId = transactionId;
|
||||
// payment.status = PaymentStatusEnum.Pending;
|
||||
private async handleCashPayment(ctx: OrderPaymentContext): Promise<void> {
|
||||
const { order, amount, method, attachments, description } = ctx
|
||||
const payment = this.paymentRepository.create({
|
||||
order,
|
||||
amount,
|
||||
method,
|
||||
status: PaymentStatusEnum.Pending,
|
||||
attachments,
|
||||
description
|
||||
})
|
||||
await this.em.persistAndFlush(payment)
|
||||
return
|
||||
}
|
||||
|
||||
// await this.em.persistAndFlush(payment);
|
||||
|
||||
// return {
|
||||
// paymentUrl: gateway.getPaymentUrl(transactionId),
|
||||
// };
|
||||
// }
|
||||
private async handleCreditPayment(ctx: OrderPaymentContext): Promise<void> {
|
||||
const { order, amount, user } = ctx
|
||||
|
||||
// async verifyOnlinePayment(transactionId: string, orderId: string): Promise<Payment> {
|
||||
// const payment = await this.em.transactional(async em => {
|
||||
// const payment = await em.findOne(
|
||||
// Payment,
|
||||
// { transactionId, order: { id: orderId } },
|
||||
// { populate: ['order', 'order.paymentMethod'] },
|
||||
// );
|
||||
await this.em.transactional(async em => {
|
||||
// 1. get User remained Credit
|
||||
const remainedCredit = await this.creditService.getCurrentCredit(user.id)
|
||||
|
||||
// if (!payment) {
|
||||
// throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
|
||||
// }
|
||||
if (remainedCredit < amount) {
|
||||
throw new BadRequestException("You Dont have enough Credit")
|
||||
}
|
||||
|
||||
// if (payment.status === PaymentStatusEnum.Paid) {
|
||||
// return payment;
|
||||
// }
|
||||
const newBalance = remainedCredit - amount;
|
||||
// 2. Create payment record
|
||||
const payment = this.paymentRepository.create({
|
||||
amount: ctx.amount,
|
||||
method: PaymentMethodEnum.Credit,
|
||||
gateway: null,
|
||||
order,
|
||||
status: PaymentStatusEnum.Paid,
|
||||
paidAt: new Date()
|
||||
});
|
||||
// 3. Create Credit transaction record
|
||||
const newCreditTransaction = this.creditRepository.create({
|
||||
user: order.user,
|
||||
amount,
|
||||
type: CreditTransactionType.WITHDRAW,
|
||||
balance: newBalance,
|
||||
orderId: order.id
|
||||
});
|
||||
|
||||
// const pm = payment.order.paymentMethod;
|
||||
// if (!pm?.merchantId || !payment.gateway) {
|
||||
// throw new BadRequestException(PaymentMessage.INVALID_PAYMENT_CONFIGURATION);
|
||||
// }
|
||||
em.persist([payment, newCreditTransaction]);
|
||||
await em.flush();
|
||||
});
|
||||
|
||||
// const gateway = this.gatewayManager.get(payment.gateway);
|
||||
this.eventEmitter.emit(
|
||||
paymentSucceedEvent.name,
|
||||
new paymentSucceedEvent(ctx.order.id, String(ctx.order?.orderNumber) || '', ctx.method, ctx.amount),
|
||||
);
|
||||
}
|
||||
|
||||
// const result = await gateway.verifyPayment({
|
||||
// merchantId: pm.merchantId,
|
||||
// amount: payment.amount,
|
||||
// transactionId: payment.transactionId!,
|
||||
// });
|
||||
private async handleOnlinePayment(ctx: OrderPaymentContext): Promise<{ paymentUrl: string }> {
|
||||
const { amount, order, gateway: requestedGateway } = ctx
|
||||
|
||||
// payment.verifyResponse = result.raw;
|
||||
const gateway = this.gatewayManager.get(requestedGateway!);
|
||||
|
||||
// if (!result.success) {
|
||||
// this.failPayment(payment);
|
||||
// return payment;
|
||||
// }
|
||||
const payment = this.paymentRepository.create({
|
||||
order,
|
||||
amount,
|
||||
method: PaymentMethodEnum.Online,
|
||||
status: PaymentStatusEnum.Pending
|
||||
})
|
||||
|
||||
// this.markPaid(payment, result.referenceId);
|
||||
// if (payment.order.status === OrderStatus.PENDING_PAYMENT) {
|
||||
// payment.order.status = OrderStatus.PAID;
|
||||
// }
|
||||
const { token, paymentUrl } = await gateway.requestPayment({
|
||||
amount: ctx.amount,
|
||||
orderId: ctx.order.id,
|
||||
});
|
||||
|
||||
// await em.flush();
|
||||
// return payment;
|
||||
// });
|
||||
// this.eventEmitter.emit(
|
||||
// onlinePaymentSucceedEvent.name,
|
||||
// new onlinePaymentSucceedEvent(payment.id, payment.order.id, payment.order.restaurant.id, String(payment.order?.orderNumber) || '', payment.amount),
|
||||
// );
|
||||
// return payment;
|
||||
// }
|
||||
payment.gateway = requestedGateway;
|
||||
payment.token = token;
|
||||
payment.status = PaymentStatusEnum.Pending;
|
||||
|
||||
await this.em.persistAndFlush(payment);
|
||||
|
||||
return {
|
||||
paymentUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async verifyOnlinePayment(transactionId: string): Promise<Payment> {
|
||||
const payment = await this.em.transactional(async em => {
|
||||
const payment = await em.findOne(
|
||||
Payment,
|
||||
{ transactionId },
|
||||
{ populate: ['order'] },
|
||||
);
|
||||
|
||||
if (!payment) {
|
||||
throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (payment.status === PaymentStatusEnum.Paid) {
|
||||
return payment;
|
||||
}
|
||||
|
||||
if (!payment.gateway) {
|
||||
throw new NotFoundException("Payment gateway not found");
|
||||
}
|
||||
|
||||
const gateway = this.gatewayManager.get(payment.gateway);
|
||||
|
||||
const { raw, referenceId, success } = await gateway.verifyPayment({
|
||||
amount: payment.amount,
|
||||
token: payment.token!,
|
||||
});
|
||||
|
||||
payment.verifyResponse = raw;
|
||||
|
||||
if (!success) {
|
||||
payment.status = PaymentStatusEnum.Failed;
|
||||
payment.failedAt = new Date();
|
||||
return payment;
|
||||
}
|
||||
|
||||
payment.status = PaymentStatusEnum.Paid;
|
||||
payment.paidAt = new Date();
|
||||
payment.transactionId = referenceId;
|
||||
// TODO : use decimal js
|
||||
// does need persist or not
|
||||
payment.order.paidAmount += payment.amount
|
||||
payment.order.balance = payment.order.total - payment.order.paidAmount
|
||||
|
||||
await em.flush();
|
||||
return payment;
|
||||
});
|
||||
this.eventEmitter.emit(
|
||||
onlinePaymentSucceedEvent.name,
|
||||
new onlinePaymentSucceedEvent(payment.id, payment.order.id, String(payment.order?.orderNumber) || '', payment.amount),
|
||||
);
|
||||
return payment;
|
||||
}
|
||||
|
||||
// findAllPaymentsBy(: string, userId: string) {
|
||||
// return this.em.find(
|
||||
@@ -239,30 +231,33 @@ export class PaymentsService {
|
||||
// );
|
||||
// }
|
||||
|
||||
// async verifyCashPayment(id: string): Promise<Payment> {
|
||||
// const payment = await this.em.transactional(async em => {
|
||||
// const payment = await em.findOne(Payment, id, { populate: ['order'] });
|
||||
// if (!payment) {
|
||||
// throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
|
||||
// }
|
||||
// if (payment.method !== PaymentMethodEnum.Cash) {
|
||||
// throw new BadRequestException(PaymentMessage.PAYMENT_METHOD_NOT_CASH);
|
||||
// }
|
||||
// if (payment.status === PaymentStatusEnum.Paid) {
|
||||
// throw new BadRequestException(PaymentMessage.PAYMENT_ALREADY_PAID);
|
||||
// }
|
||||
// if (payment.order.status === OrderStatus.PENDING_PAYMENT) {
|
||||
// payment.order.status = OrderStatus.PAID;
|
||||
// }
|
||||
// payment.status = PaymentStatusEnum.Paid;
|
||||
// payment.paidAt = new Date();
|
||||
// em.persist(payment);
|
||||
// em.persist(payment.order);
|
||||
// await em.flush();
|
||||
// return payment;
|
||||
// });
|
||||
// return payment;
|
||||
// }
|
||||
async verifyCashPayment(id: string): Promise<Payment> {
|
||||
const payment = await this.em.transactional(async em => {
|
||||
const payment = await em.findOne(Payment, id, { populate: ['order'] });
|
||||
if (!payment) {
|
||||
throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
|
||||
}
|
||||
if (payment.method !== PaymentMethodEnum.Cash) {
|
||||
throw new BadRequestException(PaymentMessage.PAYMENT_METHOD_NOT_CASH);
|
||||
}
|
||||
if (payment.status === PaymentStatusEnum.Paid) {
|
||||
throw new BadRequestException(PaymentMessage.PAYMENT_ALREADY_PAID);
|
||||
}
|
||||
|
||||
payment.status = PaymentStatusEnum.Paid;
|
||||
payment.paidAt = new Date();
|
||||
|
||||
// TODO : use decimal js
|
||||
payment.order.paidAmount += payment.amount
|
||||
payment.order.balance = payment.order.total - payment.order.paidAmount
|
||||
|
||||
em.persist(payment);
|
||||
em.persist(payment.order);
|
||||
await em.flush();
|
||||
return payment;
|
||||
});
|
||||
return payment;
|
||||
}
|
||||
|
||||
// private markPaid(payment: Payment, referenceId: string) {
|
||||
// payment.status = PaymentStatusEnum.Paid;
|
||||
|
||||
Reference in New Issue
Block a user