From 2ce7dda1681f3a3418431a412fb7a7c7c90be313 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Fri, 19 Dec 2025 19:32:16 +0330 Subject: [PATCH] emit order created event --- src/modules/orders/crone/order.crone.ts | 2 +- .../orders/interface/order.interface.ts | 2 + .../orders/listeners/order.listeners.ts | 179 +++++++++++++++++- .../orders/providers/orders.service.ts | 25 ++- src/modules/payments/events/payment.events.ts | 9 + .../payments/listeners/payment.listeners.ts | 69 +++++++ .../payments/services/payments.service.ts | 2 +- 7 files changed, 281 insertions(+), 7 deletions(-) create mode 100644 src/modules/payments/events/payment.events.ts create mode 100644 src/modules/payments/listeners/payment.listeners.ts diff --git a/src/modules/orders/crone/order.crone.ts b/src/modules/orders/crone/order.crone.ts index 7e5c152..4a0964d 100644 --- a/src/modules/orders/crone/order.crone.ts +++ b/src/modules/orders/crone/order.crone.ts @@ -58,7 +58,7 @@ export class OrdersCrone { payment.failedAt = new Date(); if (payment.order) { - payment.order.status = OrderStatus.FAILED; + payment.order.status = OrderStatus.CANCELED; // prepare restore payload const items = (payment.order as any).items || []; diff --git a/src/modules/orders/interface/order.interface.ts b/src/modules/orders/interface/order.interface.ts index 7d51182..e33af8d 100644 --- a/src/modules/orders/interface/order.interface.ts +++ b/src/modules/orders/interface/order.interface.ts @@ -39,3 +39,5 @@ export interface OrderCouponDetail { maxDiscount?: number; } + +export type StatusTransitionRef = 'user' | 'admin'; diff --git a/src/modules/orders/listeners/order.listeners.ts b/src/modules/orders/listeners/order.listeners.ts index 5363646..87c0b8c 100644 --- a/src/modules/orders/listeners/order.listeners.ts +++ b/src/modules/orders/listeners/order.listeners.ts @@ -1,11 +1,13 @@ import { Injectable, Logger } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; -import { OrderCreatedEvent } from '../events/order.events'; +import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.events'; import { Permission } from 'src/common/enums/permission.enum'; import { NotifTitleEnum } from 'src/modules/notifications/interfaces/notification.interface'; import { NotificationService } from 'src/modules/notifications/services/notification.service'; import { ConfigService } from '@nestjs/config'; +import { OrderRepository } from '../repositories/order.repository'; +import { OrderStatus } from '../interface/order.interface'; @Injectable() export class OrderListeners { @@ -13,6 +15,7 @@ export class OrderListeners { private orderCreatedSmsTemplateId: string; constructor( private readonly adminService: AdminRepository, + private readonly OrderRepository: OrderRepository, private readonly notificationService: NotificationService, private readonly configService: ConfigService, ) { @@ -63,4 +66,178 @@ export class OrderListeners { ); } } + + @OnEvent(OrderStatusChangedEvent.name) + async handleOrderStatusChanged(event: OrderStatusChangedEvent) { + try { + this.logger.log( + `Order status changed event received: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`, + ); + + // const order = await this.OrderRepository.findOneById(event.orderId, { populate: ['user'] }); + // if (!order) { + // this.logger.warn(`Order not found: ${event.orderId}`); + // return; + // } + // if (order.user && order.user.phoneNumber) { + // this.logger.warn(`Order not found: ${event.orderId}`); + // } + + const recipients = [ + { + userId: event.userId, + }, + ]; + await this.notificationService.sendNotification({ + restaurantId: event.restaurantId, + message: this.factory(event.newStatus, event)?.message, + recipients, + metadata: { + priority: 1, + }, + }); + } catch (error) { + this.logger.error( + `Failed to send notification for order status changed event: ${event.restaurantId}`, + error instanceof Error ? error.stack : String(error), + ); + } + } + + private factory(orderStatus: OrderStatus, event: OrderStatusChangedEvent) { + switch (orderStatus) { + case OrderStatus.CONFIRMED: + return { + message: { + title: NotifTitleEnum.ORDER_STATUS_CHANGED, + content: `Order ${event.orderNumber} confirmed .`, + sms: { + templateId: this.orderCreatedSmsTemplateId, + parameters: { + orderNumber: event.orderNumber, + }, + }, + pushNotif: { + title: `Order ${event.orderNumber} has been confirmed successfully`, + content: `Order ${event.orderNumber} has been confirmed successfully`, + icon: `Order ${event.orderNumber} has been confirmed successfully`, + action: { + type: NotifTitleEnum.ORDER_STATUS_CHANGED, + url: ``, + }, + }, + }, + }; + case OrderStatus.PREPARING: + return { + message: { + title: NotifTitleEnum.ORDER_STATUS_CHANGED, + content: `Order ${event.orderNumber} is preparing.`, + sms: { + templateId: this.orderCreatedSmsTemplateId, + parameters: { + orderNumber: event.orderNumber, + }, + }, + pushNotif: { + title: `Order ${event.orderNumber} is preparing`, + content: `Order ${event.orderNumber} is preparing`, + icon: `Order ${event.orderNumber} is preparing`, + action: { + type: NotifTitleEnum.ORDER_STATUS_CHANGED, + url: ``, + }, + }, + }, + }; + case OrderStatus.READY: + return { + message: { + title: NotifTitleEnum.ORDER_STATUS_CHANGED, + content: `Order ${event.orderNumber} is ready.`, + sms: { + templateId: this.orderCreatedSmsTemplateId, + parameters: { + orderNumber: event.orderNumber, + }, + }, + pushNotif: { + title: `Order ${event.orderNumber} is ready`, + content: `Order ${event.orderNumber} is ready`, + icon: `Order ${event.orderNumber} is ready`, + action: { + type: NotifTitleEnum.ORDER_STATUS_CHANGED, + url: ``, + }, + }, + }, + }; + case OrderStatus.SHIPPED: + return { + message: { + title: NotifTitleEnum.ORDER_STATUS_CHANGED, + content: `Order ${event.orderNumber} is shipped.`, + sms: { + templateId: this.orderCreatedSmsTemplateId, + parameters: { + orderNumber: event.orderNumber, + }, + }, + pushNotif: { + title: `Order ${event.orderNumber} is shipped`, + content: `Order ${event.orderNumber} is shipped`, + icon: `Order ${event.orderNumber} is shipped`, + action: { + type: NotifTitleEnum.ORDER_STATUS_CHANGED, + url: ``, + }, + }, + }, + }; + case OrderStatus.CANCELED: + return { + message: { + title: NotifTitleEnum.ORDER_STATUS_CHANGED, + content: `Order ${event.orderNumber} is canceled.`, + sms: { + templateId: this.orderCreatedSmsTemplateId, + parameters: { + orderNumber: event.orderNumber, + }, + }, + pushNotif: { + title: `Order ${event.orderNumber} is canceled`, + content: `Order ${event.orderNumber} is canceled`, + icon: `Order ${event.orderNumber} is canceled`, + action: { + type: NotifTitleEnum.ORDER_STATUS_CHANGED, + url: ``, + }, + }, + }, + }; + default: + return { + message: { + title: NotifTitleEnum.ORDER_STATUS_CHANGED, + content: `Order ${event.orderNumber} is ....`, + sms: { + templateId: this.orderCreatedSmsTemplateId, + parameters: { + orderNumber: event.orderNumber, + }, + }, + pushNotif: { + title: `Order ${event.orderNumber} is ...`, + content: `Order ${event.orderNumber} is ...`, + icon: `Order ${event.orderNumber} is ...`, + action: { + type: NotifTitleEnum.ORDER_STATUS_CHANGED, + url: ``, + }, + }, + }, + }; + } + } } diff --git a/src/modules/orders/providers/orders.service.ts b/src/modules/orders/providers/orders.service.ts index bdeb9ec..98a6af7 100644 --- a/src/modules/orders/providers/orders.service.ts +++ b/src/modules/orders/providers/orders.service.ts @@ -19,7 +19,9 @@ import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { Payment } from 'src/modules/payments/entities/payment.entity'; import { InventoryService } from 'src/modules/inventory/inventory.service'; import { BulkReserveFoodDto } from 'src/modules/inventory/dto/bulk-reserve-food.dto'; - +import { StatusTransitionRef } from '../interface/order.interface'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.events'; type OrderItemData = { food: Food; quantity: number; unitPrice: number; discount: number }; type ValidatedCartForOrder = { @@ -32,8 +34,6 @@ type ValidatedCartForOrder = { orderItemsData: OrderItemData[]; }; -export type StatusTransitionRef = 'user' | 'admin'; - @Injectable() export class OrdersService { private readonly logger = new Logger(OrdersService.name); @@ -55,6 +55,7 @@ export class OrdersService { private readonly orderRepository: OrderRepository, private readonly paymentsService: PaymentsService, private readonly inventoryService: InventoryService, + private readonly eventEmitter: EventEmitter2, ) {} async checkout(userId: string, restaurantId: string) { @@ -123,6 +124,10 @@ export class OrdersService { await this.inventoryService.deductFromInventory(em, bulkReserveFoodDto); await em.flush(); this.logger.debug(`Order ${order.id} created for user ${userId} (restaurant ${restaurantId})`); + this.eventEmitter.emit( + OrderCreatedEvent.name, + new OrderCreatedEvent(order.id, restaurantId, String(order?.orderNumber) || ''), + ); return order; }); @@ -262,9 +267,21 @@ export class OrdersService { order.status = toStatus; await this.em.persistAndFlush(order); + this.eventEmitter.emit( + OrderStatusChangedEvent.name, + new OrderStatusChangedEvent( + orderId, + order.user?.id || '', + String(order?.orderNumber) || '', + restId, + order.status, + toStatus, + ref, + ), + ); return order; } - + /* Helpers */ private assertStatusTransitionAllowed(order: Order, to: OrderStatus, ref: StatusTransitionRef) { const paymentMethod = order.paymentMethod?.method; if (!paymentMethod) { diff --git a/src/modules/payments/events/payment.events.ts b/src/modules/payments/events/payment.events.ts new file mode 100644 index 0000000..41b3d1a --- /dev/null +++ b/src/modules/payments/events/payment.events.ts @@ -0,0 +1,9 @@ + export class paymentSucceedEvent { + constructor( + public readonly orderId: string, + public readonly restaurantId: string, + public readonly orderNumber: string, + ) {} +} + + diff --git a/src/modules/payments/listeners/payment.listeners.ts b/src/modules/payments/listeners/payment.listeners.ts new file mode 100644 index 0000000..2be06d0 --- /dev/null +++ b/src/modules/payments/listeners/payment.listeners.ts @@ -0,0 +1,69 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; +import { paymentSucceedEvent } from '../events/payment.events'; +import { Permission } from 'src/common/enums/permission.enum'; +import { NotifTitleEnum } from 'src/modules/notifications/interfaces/notification.interface'; +import { NotificationService } from 'src/modules/notifications/services/notification.service'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class OrderListeners { + private readonly logger = new Logger(OrderListeners.name); + private orderCreatedSmsTemplateId: string; + constructor( + private readonly adminService: AdminRepository, + private readonly notificationService: NotificationService, + private readonly configService: ConfigService, + // private readonly orderSer + ) { + this.orderCreatedSmsTemplateId = this.configService.get('ORDER_CREATED_SMS_TEMPLATE_ID') ?? '123'; + } + + @OnEvent(paymentSucceedEvent.name) + async handleOrderCreated(event: paymentSucceedEvent) { + try { + this.logger.log( + `Order created event received: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`, + ); + // get admnin os restuaraant that have order permissuins + const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_ORDERS); + // const order=await + const recipients = admins.map(admin => ({ + adminId: admin.id, + })); + await this.notificationService.sendNotification({ + restaurantId: event.restaurantId, + message: { + title: NotifTitleEnum.ORDER_CREATED, + content: `Order ${event.orderNumber} has been created successfully`, + sms: { + templateId: this.orderCreatedSmsTemplateId, + parameters: { + orderNumber: event.orderNumber, + }, + }, + pushNotif: { + title: `Order ${event.orderNumber} has been created successfully`, + content: `Order ${event.orderNumber} has been created successfully`, + icon: `Order ${event.orderNumber} has been created successfully`, + action: { + type: `Order ${event.orderNumber} has been created successfully`, + url: `Order ${event.orderNumber} has been created successfully`, + }, + }, + }, + recipients, + metadata: { + priority: 1, + }, + }); + } catch (error) { + this.logger.error( + `Failed to send notification for order created event: ${event.restaurantId}`, + error instanceof Error ? error.stack : String(error), + ); + } + } + +} diff --git a/src/modules/payments/services/payments.service.ts b/src/modules/payments/services/payments.service.ts index ddeefa8..fd07451 100644 --- a/src/modules/payments/services/payments.service.ts +++ b/src/modules/payments/services/payments.service.ts @@ -221,7 +221,7 @@ export class PaymentsService { private failPayment(payment: Payment) { payment.status = PaymentStatusEnum.Failed; payment.failedAt = new Date(); - payment.order.status = OrderStatus.FAILED; + payment.order.status = OrderStatus.CANCELED; } private confirmOrder(order: Order) {