online payment

This commit is contained in:
2026-01-18 17:13:06 +03:30
parent 27631907cf
commit a10b6129b9
18 changed files with 345 additions and 299 deletions
+201 -206
View File
@@ -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;