diff --git a/src/modules/order/dto/create-order.dto.ts b/src/modules/order/dto/create-order.dto.ts index 5246743..6add827 100644 --- a/src/modules/order/dto/create-order.dto.ts +++ b/src/modules/order/dto/create-order.dto.ts @@ -12,6 +12,7 @@ import { IAttachment } from '../interface/order.interface'; export class CreateOrderAsAdminDto { @ApiPropertyOptional({ example: '' }) @IsString() + @IsOptional() userId?: string; @ApiPropertyOptional({ example: '' }) diff --git a/src/modules/payment/gateways/zarinpal.gateway.ts b/src/modules/payment/gateways/zarinpal.gateway.ts index c7539de..132c8bc 100755 --- a/src/modules/payment/gateways/zarinpal.gateway.ts +++ b/src/modules/payment/gateways/zarinpal.gateway.ts @@ -42,17 +42,17 @@ export class ZarinpalGateway implements IPaymentGateway { return { status: error.response.status, data: error.response.data }; } - async requestPayment({ amount, orderId }: IRequestPaymentParams): Promise { + async requestPayment({ amount, invoiceId }: IRequestPaymentParams): Promise { // Transform camelCase to snake_case for Zarinpal API v4 - const callbackUrl = `${this.websiteUrl}/verify/${orderId}`; + const callbackUrl = `${this.websiteUrl}/verify/${invoiceId}`; const zarinpalRequest: IZarinpalRequestPayment = { amount, merchant_id: this.zarinpalMerchantId, - description: `Payment for order #${orderId}`, + description: `Payment for order #${invoiceId}`, callback_url: callbackUrl, currency: 'IRT', metadata: { - order_id: orderId, + order_id: invoiceId, }, }; @@ -81,7 +81,7 @@ export class ZarinpalGateway implements IPaymentGateway { errors, callbackUrl, amount, - orderId, + invoiceId, }), ); throw new BadRequestException(message ?? PaymentMessage.ZARINPAL_PAYMENT_REQUEST_ERROR); @@ -102,7 +102,7 @@ export class ZarinpalGateway implements IPaymentGateway { data: axiosErr.data, callbackUrl, amount, - orderId, + invoiceId, }), ); throw new BadRequestException(PaymentMessage.ZARINPAL_PAYMENT_REQUEST_ERROR); diff --git a/src/modules/payment/interface/gateway.ts b/src/modules/payment/interface/gateway.ts index 249308f..58ef839 100644 --- a/src/modules/payment/interface/gateway.ts +++ b/src/modules/payment/interface/gateway.ts @@ -5,7 +5,7 @@ export interface IPaymentGateway { export interface IRequestPaymentParams { amount: number; - orderId: string; + invoiceId: string; } export interface IRequestPaymentData { diff --git a/src/modules/payment/services/payments.service.ts b/src/modules/payment/services/payments.service.ts index c81384c..114fda7 100644 --- a/src/modules/payment/services/payments.service.ts +++ b/src/modules/payment/services/payments.service.ts @@ -1,301 +1,304 @@ -// import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -// import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment'; -// import { EntityManager } from '@mikro-orm/postgresql'; -// 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'; -// import { CreditTransaction } from 'src/modules/user/entities/credit-transaction.entity'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment'; +import { EntityManager } from '@mikro-orm/postgresql'; + 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'; +import { CreditTransaction } from 'src/modules/user/entities/credit-transaction.entity'; +import { InvoiceService } from 'src/modules/invoice/invoice.service'; -// @Injectable() -// export class PaymentService { -// private readonly logger = new Logger(PaymentService.name); +@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, -// ) { } + 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, + private readonly invoiceService: InvoiceService, + ) { } -// async payOrder(orderId: string, dto: PayOrderDto, adminId?: string): Promise<{ paymentUrl: string | null }> { + async payOrder(invoiceId: string, dto: PayOrderDto, adminId?: string): Promise<{ paymentUrl: string | null }> { -// const ctx = await this.loadAndValidateOrder(orderId, dto, adminId) + const ctx = await this.loadAndValidateOrder(invoiceId, dto, adminId) -// switch (dto.method) { -// case PaymentMethodEnum.Cash: -// await this.handleCashPayment(ctx); -// return { paymentUrl: null }; + 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.Credit: + await this.handleCreditPayment(ctx); + return { paymentUrl: null }; -// case PaymentMethodEnum.Online: -// return this.handleOnlinePayment(ctx); + case PaymentMethodEnum.Online: + return this.handleOnlinePayment(ctx); -// default: -// throw new BadRequestException(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD); -// } -// } + default: + throw new BadRequestException(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD); + } + } -// private async loadAndValidateOrder(orderId: string, dto: PayOrderDto, adminId?: string): Promise { -// const { amount, method, attachments, description, gateway } = dto + private async loadAndValidateOrder(invoiceId: string, dto: PayOrderDto, adminId?: string): Promise { + const { amount, method, attachments, description, gateway } = dto -// const order = await this.orderService.findOrderOrFail(orderId) + // const order = await this.orderService.findOrderOrFail(invoiceId) + const invoice=await this.invoiceService.findOneOrFail(invoiceId) -// if (!order.balance) { -// throw new NotFoundException('Balance is not set'); -// } + if (!invoice.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") -// } -// } + 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 <= 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}`); -// } + if (amount > invoice.balance) { + throw new BadRequestException(`You cant pay more than ${invoice.balance}`); + } -// return { -// admin, -// user: order.user, -// order, -// amount, -// attachments, -// description, -// gateway, -// method -// }; -// } + return { + admin, + user: invoice.user, + invoice, + amount, + attachments, + description, + gateway, + method + }; + } -// private async handleCashPayment(ctx: OrderPaymentContext): Promise { -// 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 handleCashPayment(ctx: OrderPaymentContext): Promise { + const { invoice, amount, method, attachments, description, admin } = ctx + const payment = this.paymentRepository.create({ + creator: admin, + invoice, + amount, + method, + status: PaymentStatusEnum.Pending, + attachments, + description + }) + await this.em.persistAndFlush(payment) + return + } -// private async handleCreditPayment(ctx: OrderPaymentContext): Promise { -// const { order, amount, user, admin } = ctx + private async handleCreditPayment(ctx: OrderPaymentContext): Promise { + const { invoice, amount, user, admin } = ctx -// await this.em.transactional(async em => { -// // 1. get User remained Credit -// const remainedCredit = await this.creditService.getCurrentCredit(user.id) + 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") -// } + if (remainedCredit < amount) { + throw new BadRequestException("You Dont have enough Credit") + } -// const newBalance = remainedCredit - amount; -// // 2. Create payment record -// const payment = em.create(Payment, { -// creator: admin, -// amount: ctx.amount, -// method: PaymentMethodEnum.Credit, -// gateway: null, -// order, -// status: PaymentStatusEnum.Paid, -// paidAt: new Date(), -// }); -// // 3. Create Credit transaction record -// const newCreditTransaction = em.create(CreditTransaction, { -// user: order.user, -// amount, -// type: CreditTransactionType.WITHDRAW, -// balance: newBalance, -// orderId: order.id, -// }); + const newBalance = remainedCredit - amount; + // 2. Create payment record + const payment = em.create(Payment, { + creator: admin, + amount: ctx.amount, + method: PaymentMethodEnum.Credit, + gateway: null, + invoice, + status: PaymentStatusEnum.Paid, + paidAt: new Date(), + }); + // 3. Create Credit transaction record + const newCreditTransaction = em.create(CreditTransaction, { + user: invoice.user, + amount, + type: CreditTransactionType.WITHDRAW, + balance: newBalance, + invoiceId: invoice.id, + }); -// await em.persistAndFlush([payment, newCreditTransaction]); -// }); + await em.persistAndFlush([payment, newCreditTransaction]); + }); -// this.eventEmitter.emit( -// paymentSucceedEvent.name, -// new paymentSucceedEvent(ctx.order.id, String(ctx.order?.orderNumber) || '', ctx.method, ctx.amount), -// ); -// } + this.eventEmitter.emit( + paymentSucceedEvent.name, + new paymentSucceedEvent(ctx.invoice.id, String(ctx.invoice?.invoiceNumber) || '', ctx.method, ctx.amount), + ); + } -// private async handleOnlinePayment(ctx: OrderPaymentContext): Promise<{ paymentUrl: string }> { -// const { amount, order, gateway: requestedGateway, admin } = ctx + private async handleOnlinePayment(ctx: OrderPaymentContext): Promise<{ paymentUrl: string }> { + const { amount, invoice, gateway: requestedGateway, admin } = ctx -// const gateway = this.gatewayManager.get(requestedGateway!); + const gateway = this.gatewayManager.get(requestedGateway!); -// const { token, paymentUrl } = await gateway.requestPayment({ -// amount: ctx.amount, -// orderId: ctx.order.id, -// }); + const { token, paymentUrl } = await gateway.requestPayment({ + amount: ctx.amount, + invoiceId: ctx.invoice.id, + }); -// const payment = this.paymentRepository.create({ -// creator: admin, -// order, -// amount, -// method: PaymentMethodEnum.Online, -// status: PaymentStatusEnum.Pending, -// gateway: requestedGateway, -// token, -// }) + const payment = this.paymentRepository.create({ + creator: admin, + invoice, + amount, + method: PaymentMethodEnum.Online, + status: PaymentStatusEnum.Pending, + gateway: requestedGateway, + token, + }) -// await this.em.persistAndFlush(payment); + await this.em.persistAndFlush(payment); -// return { -// paymentUrl, -// }; -// } + return { + paymentUrl, + }; + } -// async verifyOnlinePayment(token: string): Promise { -// const payment = await this.em.transactional(async em => { -// const payment = await this.paymentRepository.findOne( -// { token }, -// { populate: ['order'] }, -// ); + async verifyOnlinePayment(token: string): Promise { + const payment = await this.em.transactional(async em => { + const payment = await this.paymentRepository.findOne( + { token }, + { populate: ['invoice'] }, + ); -// if (!payment) { -// throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND); -// } + if (!payment) { + throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND); + } -// if (payment.status === PaymentStatusEnum.Paid) { -// return payment; -// } + if (payment.status === PaymentStatusEnum.Paid) { + return payment; + } -// if (!payment.gateway) { -// throw new NotFoundException("Payment gateway not found"); -// } + if (!payment.gateway) { + throw new NotFoundException("Payment gateway not found"); + } -// const gateway = this.gatewayManager.get(payment.gateway); + const gateway = this.gatewayManager.get(payment.gateway); -// const { raw, referenceId, success } = await gateway.verifyPayment({ -// amount: payment.amount, -// token: payment.token!, -// }); + const { raw, referenceId, success } = await gateway.verifyPayment({ + amount: payment.amount, + token: payment.token!, + }); -// payment.verifyResponse = raw; + payment.verifyResponse = raw; -// if (!success) { -// payment.status = PaymentStatusEnum.Failed; -// payment.failedAt = new Date(); -// return payment; -// } + 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 + 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; -// } + await em.flush(); + return payment; + }); + this.eventEmitter.emit( + onlinePaymentSucceedEvent.name, + new onlinePaymentSucceedEvent(payment.id, payment.invoice.id, String(payment.invoice?.invoiceNumber) || '', payment.amount), + ); + return payment; + } -// findAll(dto: FindPaymentsDto) { -// return this.paymentRepository.findAllPaginated(dto) -// } + findAll(dto: FindPaymentsDto) { + return this.paymentRepository.findAllPaginated(dto) + } -// findAllByUser(dto: FindPaymentsDto, userId: string) { -// return this.paymentRepository.findAllPaginated({ ...dto, userId }) -// } + findAllByUser(dto: FindPaymentsDto, userId: string) { + return this.paymentRepository.findAllPaginated({ ...dto, userId }) + } -// async confirmCashPayment(paymentId: string): Promise { -// 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); -// } + async confirmCashPayment(paymentId: string): Promise { + const payment = await this.em.transactional(async em => { + const payment = await em.findOne(Payment, paymentId, { populate: ['invoice'] }); + 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; + 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 + // 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; -// } + em.persist(payment); + em.persist(payment.invoice); + await em.flush(); + return payment; + }); + return payment; + } -// async denyCashPayment(paymentId: string): Promise { -// const payment = await this.em.transactional(async em => { + async denyCashPayment(paymentId: string): Promise { + const payment = await this.em.transactional(async em => { -// const payment = await em.findOne(Payment, paymentId, { populate: ['order'] }); + const payment = await em.findOne(Payment, paymentId, { populate: ['invoice'] }); -// if (!payment) { -// throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND); -// } + if (!payment) { + throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND); + } -// if (payment.method !== PaymentMethodEnum.Cash) { -// throw new BadRequestException(PaymentMessage.PAYMENT_METHOD_NOT_CASH); -// } + 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 + if (payment.status == PaymentStatusEnum.Paid) { + // payment.order.paidAmount! -= payment.amount + // payment.order.balance! += payment.amount -// em.persistAndFlush(payment.order) -// } + em.persistAndFlush(payment.invoice) + } -// payment.status = PaymentStatusEnum.Failed; -// payment.failedAt = new Date(); -// payment.paidAt = null + payment.status = PaymentStatusEnum.Failed; + payment.failedAt = new Date(); + payment.paidAt = null -// em.persistAndFlush(payment); + em.persistAndFlush(payment); -// return payment; -// }); -// return payment; -// } + return payment; + }); + return payment; + } -// } +} diff --git a/src/modules/user/entities/credit-transaction.entity.ts b/src/modules/user/entities/credit-transaction.entity.ts index 04b8b6e..d6b9159 100644 --- a/src/modules/user/entities/credit-transaction.entity.ts +++ b/src/modules/user/entities/credit-transaction.entity.ts @@ -12,7 +12,7 @@ export class CreditTransaction extends BaseEntity { user!: User; @Property() - orderId: string + invoiceId: string @Property({ type: 'decimal', precision: 10, scale: 0 }) amount!: number;