307 lines
9.4 KiB
TypeScript
307 lines
9.4 KiB
TypeScript
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
|
import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment';
|
|
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 { 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/user';
|
|
import { Payment } from '../entities/payment.entity';
|
|
import { FindPaymentsDto } from '../dto/find-payments.dto';
|
|
import { Admin } from 'src/modules/admin/entities/admin.entity';
|
|
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
|
|
|
|
|
|
|
@Injectable()
|
|
export class PaymentService {
|
|
private readonly logger = new Logger(PaymentService.name);
|
|
|
|
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,
|
|
private readonly adminRepository: AdminRepository,
|
|
) { }
|
|
|
|
async payOrder(orderId: string, dto: PayOrderDto, adminId?: string): Promise<{ paymentUrl: string | null }> {
|
|
|
|
const ctx = await this.loadAndValidateOrder(orderId, dto, adminId)
|
|
|
|
switch (dto.method) {
|
|
case PaymentMethodEnum.Cash:
|
|
await this.handleCashPayment(ctx);
|
|
return { paymentUrl: null };
|
|
|
|
case PaymentMethodEnum.Credit:
|
|
await this.handleCreditPayment(ctx);
|
|
return { paymentUrl: null };
|
|
|
|
case PaymentMethodEnum.Online:
|
|
return this.handleOnlinePayment(ctx);
|
|
|
|
default:
|
|
throw new BadRequestException(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD);
|
|
}
|
|
}
|
|
|
|
|
|
private async loadAndValidateOrder(orderId: string, dto: PayOrderDto, adminId?: string): Promise<OrderPaymentContext> {
|
|
const { amount, method, attachments, description, gateway } = dto
|
|
|
|
const order = await this.orderService.findOrderOrFail(orderId)
|
|
|
|
if (!order) {
|
|
throw new NotFoundException(OrderMessage.NOT_FOUND);
|
|
}
|
|
|
|
if (!order.balance) {
|
|
throw new NotFoundException('Balance is not set');
|
|
}
|
|
|
|
let admin: null | Admin = null
|
|
if (adminId) {
|
|
admin = await this.adminRepository.findOne({ id: adminId })
|
|
if (!admin) {
|
|
throw new BadRequestException("Admin not found")
|
|
}
|
|
}
|
|
|
|
if (amount <= 0) {
|
|
throw new BadRequestException(PaymentMessage.AMOUNT_MUST_BE_GREATER_THAN_ZERO);
|
|
}
|
|
|
|
if (amount > order.balance) {
|
|
throw new BadRequestException(`You cant pay more than ${order.balance}`);
|
|
}
|
|
|
|
return {
|
|
admin,
|
|
user: order.user,
|
|
order,
|
|
amount,
|
|
attachments,
|
|
description,
|
|
gateway,
|
|
method
|
|
};
|
|
}
|
|
|
|
private async handleCashPayment(ctx: OrderPaymentContext): Promise<void> {
|
|
const { order, amount, method, attachments, description, admin } = ctx
|
|
const payment = this.paymentRepository.create({
|
|
creator: admin,
|
|
order,
|
|
amount,
|
|
method,
|
|
status: PaymentStatusEnum.Pending,
|
|
attachments,
|
|
description
|
|
})
|
|
await this.em.persistAndFlush(payment)
|
|
return
|
|
}
|
|
|
|
|
|
private async handleCreditPayment(ctx: OrderPaymentContext): Promise<void> {
|
|
const { order, amount, user, admin } = ctx
|
|
|
|
await this.em.transactional(async em => {
|
|
// 1. get User remained Credit
|
|
const remainedCredit = await this.creditService.getCurrentCredit(user.id)
|
|
|
|
if (remainedCredit < amount) {
|
|
throw new BadRequestException("You Dont have enough Credit")
|
|
}
|
|
|
|
const newBalance = remainedCredit - amount;
|
|
// 2. Create payment record
|
|
const payment = this.paymentRepository.create({
|
|
creator: admin,
|
|
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,
|
|
});
|
|
|
|
em.persist([payment, newCreditTransaction]);
|
|
await em.flush();
|
|
});
|
|
|
|
this.eventEmitter.emit(
|
|
paymentSucceedEvent.name,
|
|
new paymentSucceedEvent(ctx.order.id, String(ctx.order?.orderNumber) || '', ctx.method, ctx.amount),
|
|
);
|
|
}
|
|
|
|
private async handleOnlinePayment(ctx: OrderPaymentContext): Promise<{ paymentUrl: string }> {
|
|
const { amount, order, gateway: requestedGateway, admin } = ctx
|
|
|
|
const gateway = this.gatewayManager.get(requestedGateway!);
|
|
|
|
const { token, paymentUrl } = await gateway.requestPayment({
|
|
amount: ctx.amount,
|
|
orderId: ctx.order.id,
|
|
});
|
|
|
|
const payment = this.paymentRepository.create({
|
|
creator: admin,
|
|
order,
|
|
amount,
|
|
method: PaymentMethodEnum.Online,
|
|
status: PaymentStatusEnum.Pending,
|
|
gateway: requestedGateway,
|
|
token,
|
|
})
|
|
|
|
await this.em.persistAndFlush(payment);
|
|
|
|
return {
|
|
paymentUrl,
|
|
};
|
|
}
|
|
|
|
async verifyOnlinePayment(token: string): Promise<Payment> {
|
|
const payment = await this.em.transactional(async em => {
|
|
const payment = await this.paymentRepository.findOne(
|
|
{ token },
|
|
{ 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.amount
|
|
|
|
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;
|
|
}
|
|
|
|
findAll(dto: FindPaymentsDto) {
|
|
return this.paymentRepository.findAllPaginated(dto)
|
|
}
|
|
|
|
findAllByUser(dto: FindPaymentsDto, userId: string) {
|
|
return this.paymentRepository.findAllPaginated({ ...dto, userId })
|
|
}
|
|
|
|
async confirmCashPayment(paymentId: string): Promise<Payment> {
|
|
const payment = await this.em.transactional(async em => {
|
|
const payment = await em.findOne(Payment, paymentId, { 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();
|
|
payment.failedAt = null;
|
|
|
|
// TODO : use decimal js
|
|
// payment.order.paidAmount! += payment.amount
|
|
// payment.order.balance! -= payment.amount
|
|
|
|
em.persist(payment);
|
|
em.persist(payment.order);
|
|
await em.flush();
|
|
return payment;
|
|
});
|
|
return payment;
|
|
}
|
|
|
|
async denyCashPayment(paymentId: string): Promise<Payment> {
|
|
const payment = await this.em.transactional(async em => {
|
|
|
|
const payment = await em.findOne(Payment, paymentId, { 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) {
|
|
// payment.order.paidAmount! -= payment.amount
|
|
// payment.order.balance! += payment.amount
|
|
|
|
em.persistAndFlush(payment.order)
|
|
}
|
|
|
|
payment.status = PaymentStatusEnum.Failed;
|
|
payment.failedAt = new Date();
|
|
payment.paidAt = null
|
|
|
|
em.persistAndFlush(payment);
|
|
|
|
return payment;
|
|
});
|
|
return payment;
|
|
}
|
|
|
|
}
|