diff --git a/src/app.module.ts b/src/app.module.ts index 6c27ef6..4baf777 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,7 +12,7 @@ import { ScheduleModule } from '@nestjs/schedule'; import { productModule } from './modules/product/product.module'; import { RolesModule } from './modules/roles/roles.module'; import { PaymentModule } from './modules/payment/payments.module'; -import { OrderModule } from './modules/order/orders.module'; +import { OrderModule } from './modules/order/order.module'; import { NotificationsModule } from './modules/notification/notifications.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { CacheModule } from '@nestjs/cache-manager'; diff --git a/src/modules/order/controllers/orders.controller.ts b/src/modules/order/controllers/orders.controller.ts index 70d952a..d85c491 100644 --- a/src/modules/order/controllers/orders.controller.ts +++ b/src/modules/order/controllers/orders.controller.ts @@ -34,22 +34,7 @@ export class OrdersController { return orders } - @Post('admin/orders/:orderId/create-invoice') - @UseGuards(AuthGuard) - @ApiOperation({ summary: 'Create invoice for new order' }) - createInvoice(@Param('orderId') orderId: string, @Body() body: CreateInvoiceDto) { - return this.ordersService.createInvoice(orderId, body); - } - - // @UseGuards(AuthGuard) - // @Patch('public/orders/:id/:status') - // @ApiParam({ - // name: 'status', - // description: 'Order status', - // enum: OrderStatus, - // }) - - @Post('admin/orders/:orderId/items/:orderItemId') + @Post('public/orders/:orderId/items/:orderItemId') @UseGuards(AuthGuard) @ApiOperation({ summary: 'Confirm Invoice Item By User' }) confirmOrderItem( @@ -60,7 +45,17 @@ export class OrdersController { return this.ordersService.confirmOrderItem(userId, orderId, +orderItemId); } - // /******************** Admin Routes **********************/ + + + + + /*========================== Admin Routes =====================*/ + @Post('admin/orders/:orderId/create-invoice') + @UseGuards(AuthGuard) + @ApiOperation({ summary: 'Create invoice for new order' }) + createInvoice(@Param('orderId') orderId: string, @Body() body: CreateInvoiceDto) { + return this.ordersService.createInvoice(orderId, body); + } // @UseGuards(AdminAuthGuard) // @Permissions(Permission.MANAGE_ORDERS) // @Get('admin/orders') diff --git a/src/modules/order/orders.module.ts b/src/modules/order/order.module.ts similarity index 100% rename from src/modules/order/orders.module.ts rename to src/modules/order/order.module.ts diff --git a/src/modules/order/providers/orders.service.ts b/src/modules/order/providers/order.service.ts similarity index 98% rename from src/modules/order/providers/orders.service.ts rename to src/modules/order/providers/order.service.ts index 718940c..82e861f 100644 --- a/src/modules/order/providers/orders.service.ts +++ b/src/modules/order/providers/order.service.ts @@ -25,8 +25,8 @@ import { CreateInvoiceDto } from '../dto/create-invoice.dto'; @Injectable() -export class OrdersService { - private readonly logger = new Logger(OrdersService.name); +export class OrderService { + private readonly logger = new Logger(OrderService.name); constructor( private readonly em: EntityManager, diff --git a/src/modules/payment/controllers/payments.controller.ts b/src/modules/payment/controllers/payments.controller.ts index 31789d0..a834077 100644 --- a/src/modules/payment/controllers/payments.controller.ts +++ b/src/modules/payment/controllers/payments.controller.ts @@ -10,7 +10,7 @@ import { ApiHeader, } from '@nestjs/swagger'; import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; - import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto'; +import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto'; import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto'; import { VerifyPaymentDto } from '../dto/verify-payment.dto'; import { PaymentChartDto } from '../dto/payment-chart.dto'; @@ -19,27 +19,23 @@ import { AuthGuard } from 'src/modules/auth/guards/auth.guard'; import { Permissions } from 'src/common/decorators/permissions.decorator'; import { Permission } from 'src/common/enums/permission.enum'; +import { PayOrderDto } from '../dto/pay-order.dto'; @ApiTags('payments') @Controller() export class PaymentsController { constructor( private readonly paymentsService: PaymentsService, - ) { } + ) { } - - - - // @UseGuards(AuthGuard) - // @ApiBearerAuth() - // @Get('public/payments/pay-order/:orderId') - // @ApiOperation({ summary: 'Pay for an order' }) - // @ApiParam({ name: 'orderId', type: 'string', description: 'Order ID' }) - - // payAnOrder(@Param('orderId') orderId: string) { - // return this.paymentsService.payOrder(orderId); - // } + @Get('public/payments/pay-order/:orderId') + @UseGuards(AuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Pay for an order' }) + payAnOrder(@Param('orderId') orderId: string,@Body() dto :PayOrderDto) { + return this.paymentsService.payOrder(orderId,dto); + } // @Post('public/payments/verify') // @ApiOperation({ summary: 'Verify a payment' }) @@ -50,7 +46,7 @@ export class PaymentsController { // return this.paymentsService.verifyOnlinePayment(verifyPaymentDto.authority, verifyPaymentDto.orderId); // } - + // @UseGuards(AdminAuthGuard) // @Permissions(Permission.MANAGE_PAYMENTS) diff --git a/src/modules/payment/dto/pay-order.dto.ts b/src/modules/payment/dto/pay-order.dto.ts new file mode 100644 index 0000000..6d2d283 --- /dev/null +++ b/src/modules/payment/dto/pay-order.dto.ts @@ -0,0 +1,31 @@ +import { IsString, IsOptional, IsBoolean, IsNumber, IsEnum, IsArray } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { PaymentMethodEnum, PaymentGatewayEnum } from '../interface/payment'; + +export class PayOrderDto { + + @ApiProperty({ description: 'amount'}) + @IsNumber() + amount!: number; + + @ApiProperty({ description: 'Payment method', enum: PaymentMethodEnum }) + @IsEnum(PaymentMethodEnum) + method!: PaymentMethodEnum; + + @ApiProperty({ description: 'Payment gateway', enum: PaymentGatewayEnum, required: false }) + @IsOptional() + @IsEnum(PaymentGatewayEnum) + gateway?: PaymentGatewayEnum ; + + @ApiProperty({ description: 'Payment method description', required: false }) + @IsOptional() + @IsString() + description?: string; + + + @ApiProperty({ description: 'Merchant ID', required: false }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + attachments?: string[]; +} diff --git a/src/modules/payment/entities/payment.entity.ts b/src/modules/payment/entities/payment.entity.ts index 4199543..c8f1c7e 100644 --- a/src/modules/payment/entities/payment.entity.ts +++ b/src/modules/payment/entities/payment.entity.ts @@ -6,9 +6,9 @@ import { ulid } from 'ulid'; @Entity({ tableName: 'payments' }) export class Payment extends BaseEntity { - @PrimaryKey({ type: 'string', columnType:'char(26)' }) - id: string=ulid() - + @PrimaryKey({ type: 'string', columnType: 'char(26)' }) + id: string = ulid() + @ManyToOne(() => Order) @Index() order!: Order; @@ -40,4 +40,10 @@ export class Payment extends BaseEntity { @Property({ nullable: true }) failedAt?: Date | null = null; + @Property({ nullable: true }) + attachments?: string[] + + @Property({ nullable: true }) + description?: string + } diff --git a/src/modules/payment/events/payment.events.ts b/src/modules/payment/events/payment.events.ts index 7d4e0b3..664a193 100644 --- a/src/modules/payment/events/payment.events.ts +++ b/src/modules/payment/events/payment.events.ts @@ -3,7 +3,6 @@ import { PaymentMethodEnum } from "../interface/payment"; export class paymentSucceedEvent { constructor( public readonly orderId: string, - public readonly: string, public readonly orderNumber: string, public readonly paymentMethod: PaymentMethodEnum, public readonly total: number @@ -16,7 +15,6 @@ export class onlinePaymentSucceedEvent { constructor( public readonly paymentId: string, public readonly orderId: string, - public readonly: string, public readonly orderNumber: string, public readonly total: number, ) { } diff --git a/src/modules/payment/gateways/zarinpal.gateway.ts b/src/modules/payment/gateways/zarinpal.gateway.ts index 5b26c6e..c7539de 100755 --- a/src/modules/payment/gateways/zarinpal.gateway.ts +++ b/src/modules/payment/gateways/zarinpal.gateway.ts @@ -20,6 +20,8 @@ export class ZarinpalGateway implements IPaymentGateway { private readonly zarinpalRequestUrl: string; private readonly zarinpalVerifyUrl: string; private readonly zarinpalPaymentBaseUrl: string; + private readonly zarinpalMerchantId: string; + private readonly websiteUrl: string = 'https://negareh.ir' private readonly axiosConfig = { timeout: 10_000, headers: { @@ -32,6 +34,7 @@ export class ZarinpalGateway implements IPaymentGateway { this.zarinpalPaymentBaseUrl = zarinpalBaseUrl; this.zarinpalRequestUrl = `${zarinpalBaseUrl}/pg/v4/payment/request.json`; this.zarinpalVerifyUrl = `${zarinpalBaseUrl}/pg/v4/payment/verify.json`; + this.zarinpalMerchantId = this.configService.getOrThrow('ZARINPAL_BASE_MERCHANT') } private getAxiosErrorData(error: unknown): { status?: number; data?: unknown } | null { @@ -39,12 +42,12 @@ export class ZarinpalGateway implements IPaymentGateway { return { status: error.response.status, data: error.response.data }; } - async requestPayment({ amount, merchantId, orderId, domain }: IRequestPaymentParams): Promise { + async requestPayment({ amount, orderId }: IRequestPaymentParams): Promise { // Transform camelCase to snake_case for Zarinpal API v4 - const callbackUrl = `${domain}/verify/${orderId}`; + const callbackUrl = `${this.websiteUrl}/verify/${orderId}`; const zarinpalRequest: IZarinpalRequestPayment = { amount, - merchant_id: merchantId, + merchant_id: this.zarinpalMerchantId, description: `Payment for order #${orderId}`, callback_url: callbackUrl, currency: 'IRT', @@ -84,7 +87,9 @@ export class ZarinpalGateway implements IPaymentGateway { throw new BadRequestException(message ?? PaymentMessage.ZARINPAL_PAYMENT_REQUEST_ERROR); } - return { transactionId: authority }; + const paymentUrl = `${this.zarinpalPaymentBaseUrl}/pg/StartPay/${authority}` + + return { token: authority, paymentUrl }; } catch (error) { if (error instanceof BadRequestException) throw error; @@ -109,13 +114,13 @@ export class ZarinpalGateway implements IPaymentGateway { } } - async verifyPayment({ merchantId, amount, transactionId }: IPaymentVerifyParams): Promise { + async verifyPayment({ amount, token }: IPaymentVerifyParams): Promise { try { // Transform camelCase to snake_case for Zarinpal API v4 const zarinpalVerifyRequest: IZarinpalVerifyRequest = { - merchant_id: merchantId, + merchant_id: this.zarinpalMerchantId, amount, - authority: transactionId, + authority: token, }; const res = await axios.post( @@ -152,7 +157,5 @@ export class ZarinpalGateway implements IPaymentGateway { } } - getPaymentUrl(authority: string): string { - return `${this.zarinpalPaymentBaseUrl}/pg/StartPay/${authority}`; - } + } diff --git a/src/modules/payment/interface/gateway.ts b/src/modules/payment/interface/gateway.ts index f336f2c..249308f 100644 --- a/src/modules/payment/interface/gateway.ts +++ b/src/modules/payment/interface/gateway.ts @@ -1,24 +1,21 @@ export interface IPaymentGateway { requestPayment(requestPaymentParams: IRequestPaymentParams): Promise; verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise; - getPaymentUrl(authority: string): string; } export interface IRequestPaymentParams { amount: number; - merchantId: string; - domain: string; orderId: string; } export interface IRequestPaymentData { - transactionId: string; + token: string; + paymentUrl:string } export interface IPaymentVerifyParams { amount: number; - merchantId: string; - transactionId: string; + token: string; } export interface IPaymentVerifyData { diff --git a/src/modules/payment/interface/payment.ts b/src/modules/payment/interface/payment.ts index fe6645c..d58ef06 100644 --- a/src/modules/payment/interface/payment.ts +++ b/src/modules/payment/interface/payment.ts @@ -1,9 +1,10 @@ import type { Order } from 'src/modules/order/entities/order.entity'; +import { User } from 'src/modules/user/entities/user.entity'; export enum PaymentMethodEnum { Online = 'Online', Cash = 'Cash', - Wallet = 'Wallet', + Credit = 'Credit', } export enum PaymentStatusEnum { Pending = 'pending', @@ -24,10 +25,11 @@ export interface ICreatePayment { } export interface OrderPaymentContext { + user:User; order: Order; amount: number; - method: PaymentMethodEnum; - gateway: PaymentGatewayEnum | null; - merchantId: string | null; - restaurantDomain: string | null; + method: PaymentMethodEnum + attachments?: string[] + description?: string + gateway?: PaymentGatewayEnum } diff --git a/src/modules/payment/payments.module.ts b/src/modules/payment/payments.module.ts index b787413..80781c1 100644 --- a/src/modules/payment/payments.module.ts +++ b/src/modules/payment/payments.module.ts @@ -11,7 +11,7 @@ import { PaymentRepository } from './repositories/payment.repository'; import { PaymentListeners } from './listeners/payment.listeners'; import { AdminModule } from '../admin/admin.module'; import { NotificationsModule } from '../notification/notifications.module'; -import { OrderModule } from '../order/orders.module'; +import { OrderModule } from '../order/order.module'; @Module({ imports: [MikroOrmModule.forFeature([Payment]), diff --git a/src/modules/payment/services/payments.service.ts b/src/modules/payment/services/payments.service.ts index 2ddb18e..3e69ea3 100644 --- a/src/modules/payment/services/payments.service.ts +++ b/src/modules/payment/services/payments.service.ts @@ -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 { - // 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 { - // 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 { - // 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 { + 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 { + 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 { + const { order, amount, user } = ctx - // async verifyOnlinePayment(transactionId: string, orderId: string): Promise { - // 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 { + 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 { - // 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 { + 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; diff --git a/src/modules/user/controllers/users.controller.ts b/src/modules/user/controllers/users.controller.ts index 7e7689a..e2d98c9 100644 --- a/src/modules/user/controllers/users.controller.ts +++ b/src/modules/user/controllers/users.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, UseGuards, Patch, Body} from '@nestjs/common'; +import { Controller, Get, UseGuards, Patch, Body } from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody, ApiOkResponse } from '@nestjs/swagger'; import { AuthGuard } from 'src/modules/auth/guards/auth.guard'; import { UserService } from '../providers/user.service'; @@ -14,14 +14,14 @@ import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; import { UserSuccessMessage } from 'src/common/enums/message.enum'; import { Permissions } from 'src/common/decorators/permissions.decorator'; import { Permission } from 'src/common/enums/permission.enum'; -import { WalletService } from '../providers/wallet.service'; +import { CreditService } from '../providers/credit.service'; @ApiTags('User') @Controller() export class UsersController { constructor( private readonly userService: UserService, - private readonly walletService: WalletService, + private readonly walletService: CreditService, ) { } @UseGuards(AuthGuard) diff --git a/src/modules/user/providers/wallet.service.ts b/src/modules/user/providers/credit.service.ts similarity index 87% rename from src/modules/user/providers/wallet.service.ts rename to src/modules/user/providers/credit.service.ts index a9a73f9..7c86599 100644 --- a/src/modules/user/providers/wallet.service.ts +++ b/src/modules/user/providers/credit.service.ts @@ -1,19 +1,40 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { FilterQuery } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/postgresql'; -import { CreditTransactionRepository } from '../repositories/credit-transaction.repository'; +import { CreditRepository } from '../repositories/credit.repository'; import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { CreditTransaction } from '../entities/credit-transaction.entity'; +import { UserRepository } from '../repositories/user.repository'; @Injectable() -export class WalletService { +export class CreditService { constructor( - // private readonly walletTransactionRepository: CreditTransactionRepository, - + private readonly creditRepository: CreditRepository, + private readonly userRepository: UserRepository, + ) { } + async getCurrentCredit(userId: string,): Promise { + const user = await this.userRepository.findOne(userId) + if (!user) { + throw new BadRequestException("User not found") + } + const maxCredit = user.maxCredit + + const latestTransaction = await this.creditRepository.findOne( + { + user: { id: userId }, + }, + { orderBy: { createdAt: 'desc' } }, + ); + const balance = latestTransaction?.balance || 0 + + const remained = maxCredit - balance + + return remained + } // async getUserWalletTransactions( // userId: string, // dto: FindWalletTransactionsDto, diff --git a/src/modules/user/repositories/credit-transaction.repository.ts b/src/modules/user/repositories/credit-transaction.repository.ts deleted file mode 100644 index b0a1d46..0000000 --- a/src/modules/user/repositories/credit-transaction.repository.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; -import { CreditTransaction } from '../entities/credit-transaction.entity'; - -@Injectable() -export class CreditTransactionRepository extends EntityRepository { - constructor(readonly em: EntityManager) { - super(em, CreditTransaction); - } - async getCurrentCredit(userId: string,): Promise { - const lastRow = await this.em.findOne( - CreditTransaction, - { - user: { id: userId }, - }, - { orderBy: { createdAt: 'desc' } }, - ); - return lastRow?.balance || 0; - } -} diff --git a/src/modules/user/repositories/credit.repository.ts b/src/modules/user/repositories/credit.repository.ts new file mode 100644 index 0000000..7ed7f3c --- /dev/null +++ b/src/modules/user/repositories/credit.repository.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; +import { CreditTransaction } from '../entities/credit-transaction.entity'; + +@Injectable() +export class CreditRepository extends EntityRepository { + constructor(readonly em: EntityManager) { + super(em, CreditTransaction); + } + +} diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index 17267cf..8143787 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -6,17 +6,28 @@ import { User } from './entities/user.entity'; import { UserAddress } from './entities/user-address.entity'; import { JwtModule } from '@nestjs/jwt'; import { UserRepository } from './repositories/user.repository'; -import { CreditTransactionRepository } from './repositories/credit-transaction.repository'; -import { WalletService } from './providers/wallet.service'; +import { CreditRepository } from './repositories/credit.repository'; +import { CreditService } from './providers/credit.service'; import { CreditTransaction } from './entities/credit-transaction.entity'; @Module({ - providers: [UserService, WalletService, UserRepository, CreditTransactionRepository, CreditTransactionRepository], + providers: [ + UserService, + CreditService, + UserRepository, + CreditRepository, + ], controllers: [UsersController], imports: [ MikroOrmModule.forFeature([User, UserAddress, CreditTransaction]), JwtModule, ], - exports: [UserService, WalletService, UserRepository, CreditTransactionRepository, CreditTransactionRepository], + exports: [ + UserService, + CreditService, + UserRepository, + CreditRepository, + CreditRepository + ], }) export class UserModule { }