import { Injectable, BadRequestException, Logger, NotFoundException, ForbiddenException } from '@nestjs/common'; import { EntityManager } from '@mikro-orm/postgresql'; import { OrderRepository } from '../repositories/order.repository'; import { FindOrdersDto } from '../dto/find-orders.dto'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { CreateOrderAsAdminDto, } from '../dto/create-order.dto'; import { UserService } from 'src/modules/user/providers/user.service'; import { OrderStatusEnum, IOrderPrintResponse } from '../interface/order.interface'; import { Order } from '../entities/order.entity'; import { AdminService } from 'src/modules/admin/providers/admin.service'; import { UpdateOrderAsAdminDto } from '../dto/update-order.dto'; import { PermissionEnum } from 'src/common/enums/permission.enum'; import { User } from 'src/modules/user/entities/user.entity'; import { Product } from 'src/modules/product/entities/product.entity'; import { Category } from 'src/modules/product/entities/category.entity'; import { InvoiceItem } from 'src/modules/invoice/entities/invoice-item.entity'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { UpdateStatusDto } from '../dto/update-status.dto'; import { CreateOrderPrintDto } from '../dto/create-order-print.dto'; import { OrderPrintValueDto } from '../dto/order-print-value.dto'; import { OrderPrint } from '../entities/order-print.entity'; import { OrderPrintValue } from '../entities/order-print-value.entity'; import { OrderPrintRepository } from '../repositories/order-print.repository'; import { Field } from 'src/modules/form-builder/entities/field.entity'; import { Request } from 'src/modules/request/entities/request.entity'; import { Invoice } from 'src/modules/invoice/entities/invoice.entity'; import { OrderCreatedEvent } from '../events/order.events'; import { InvoiceConfirmStatusEnum } from 'src/modules/invoice/enum/invoice-confirm-status.enum'; @Injectable() export class OrderService { private readonly logger = new Logger(OrderService.name); constructor( private readonly em: EntityManager, private readonly orderRepository: OrderRepository, private readonly userService: UserService, private readonly adminService: AdminService, private readonly orderPrintRepository: OrderPrintRepository, private readonly eventEmitter: EventEmitter2, ) {} async createOrderAsAdmin(adminId: string, dto: CreateOrderAsAdminDto): Promise { let invoiceItem: null | InvoiceItem = null let user: User | null = null if (dto.invoiceItemId) { invoiceItem = await this.em.findOne(InvoiceItem, { id: dto.invoiceItemId }, { populate: ['invoice', 'invoice.user', 'product'] }); if (!invoiceItem) { throw new NotFoundException(`Invoice item with ID ${dto.invoiceItemId} not found.`); } if (!invoiceItem.confirmedAt ) { throw new BadRequestException(`Invoice item is not confirmed`); } user = invoiceItem.invoice.user } if (!dto.invoiceItemId) { if (!dto.userId) { throw new NotFoundException(`UserId is required`); } user = await this.userService.findById(dto.userId); if (!user) { throw new NotFoundException(`User with ID ${dto.userId} not found.`); } } if (!user) { throw new NotFoundException(`User not found`); } const creator = await this.adminService.findOrFail(adminId); const type = await this.em.findOne(Category, { id: dto.typeId }); if (!type) { throw new NotFoundException(`Order type with ID ${dto.typeId} not found.`); } let product: Product | null = null; if (dto.productId) { product = await this.em.findOne(Product, { id: dto.productId }); if (!product) { throw new NotFoundException(`Product with ID ${dto.productId} not found.`); } } else if (invoiceItem?.product) { product = invoiceItem.product; } let designer = null; if (dto.designerId) { designer = await this.adminService.findOrFail(dto.designerId); } const order = this.em.create(Order, { user, creator, designer, type, product: product ?? null, title: dto.title, status: OrderStatusEnum.CREATED, estimatedDays: dto.estimatedDays, invoiceItem, attachments: dto.attachments, }); await this.em.persistAndFlush(order); this.eventEmitter.emit( OrderCreatedEvent.name, new OrderCreatedEvent(order.id, user.id, order.orderNumber), ); return order; } async findOrdersAsUser(userId: string, dto: FindOrdersDto): Promise> { return this.orderRepository.findAllPaginated({ userId, ...dto }) } async findOrdersAsAdmin(adminId: string, dto: FindOrdersDto): Promise> { const permissions = await this.adminService.ownedPermissions(adminId, [PermissionEnum.VIEW_ORDERS, PermissionEnum.VIEW_ASSIGNED_ORDERS]) if (permissions.includes(PermissionEnum.VIEW_ORDERS)) { return this.orderRepository.findAllPaginated(dto) } else if (permissions.includes(PermissionEnum.VIEW_ASSIGNED_ORDERS)) { return this.orderRepository.findAllPaginated({ ...dto, designerId: adminId, }) } else { throw new ForbiddenException("You don't have permission to view orders") } } async getOrderAsUser(userId: string, orderId: string): Promise { const order = await this.findOrderOrFail(orderId) if (order.user.id !== userId) { throw new BadRequestException("this order doesnt belongs to you") } return order } async updateOrderAsAdmin(id: string, dto: UpdateOrderAsAdminDto): Promise { const order = await this.findOrderOrFail(id); if (dto.userId !== undefined) { const user = await this.userService.findById(dto.userId); if (!user) { throw new NotFoundException(`User with ID ${dto.userId} not found.`); } order.user = user as User; } if (dto.typeId !== undefined) { const type = await this.em.findOne(Category, { id: dto.typeId }); if (!type) { throw new NotFoundException(`Order type with ID ${dto.typeId} not found.`); } order.type = type; } if (dto.estimatedDays !== undefined) { order.estimatedDays = dto.estimatedDays; } if (dto.title !== undefined) { order.title = dto.title; } if (dto.productId !== undefined) { if (dto.productId == null || dto.productId === '') { order.product = null; } else { const product = await this.em.findOne(Product, { id: dto.productId }); if (!product) { throw new NotFoundException(`Product with ID ${dto.productId} not found.`); } order.product = product; } } await this.em.flush(); return order; } async softDeleteOrder(orderId: string): Promise { const order = await this.findOrderOrFail(orderId); order.deletedAt = new Date(); await this.em.flush(); } async updateStatus(orderId: string, adminId: string, dto: UpdateStatusDto): Promise { const permissions = await this.adminService.ownedPermissions(adminId, [PermissionEnum.VIEW_ORDERS, PermissionEnum.VIEW_ASSIGNED_ORDERS]) if (permissions.includes(PermissionEnum.VIEW_ORDERS)) { const order = await this.findOrderOrFail(orderId); order.status = dto.status; await this.em.flush(); return order } if (permissions.includes(PermissionEnum.VIEW_ASSIGNED_ORDERS)) { //TODO const order = await this.findOrderOrFailAssignedOrder(orderId, adminId); order.status = dto.status; await this.em.flush(); return order } throw new ForbiddenException("You don't have permission to update order status") } async assignDesigner(orderId: string, designerId: string) { const order = await this.findOrderOrFail(orderId) const designer = await this.adminService.findOrFail(designerId) order.designer = designer await this.em.flush() return order } async createPrint(orderId: string, dto: CreateOrderPrintDto): Promise { const order = await this.em.findOne(Order, { id: orderId }, { populate: ['print'] }); if (!order) { throw new NotFoundException(`Order with ID ${orderId} not found.`); } const fieldMap = await this.resolvePrintFields(dto.fields); let print = order.print; if (print) { await this.em.populate(print, ['values']); print.values.removeAll(); } else { print = this.em.create(OrderPrint, { order }); } this.assignPrintValues(print, dto.fields, fieldMap); await this.em.persistAndFlush(print); await this.em.populate(print, ['values', 'values.field']); return this.serializeOrderPrint(print); } async getPrint(orderId: string): Promise { const orderPrint = await this.orderPrintRepository.findOne( { order: { id: orderId } }, { populate: ['values', 'values.field'] }, ); if (!orderPrint) { throw new NotFoundException(`Print with ID ${orderId} not found.`); } return this.serializeOrderPrint(orderPrint); } private serializeOrderPrint(print: OrderPrint): IOrderPrintResponse { return { id: print.id, createdAt: print.createdAt, deletedAt: print.deletedAt, fileds: print.values.getItems().map((item) => ({ fieldId: item.field.id, value: item.value, })), }; } private async resolvePrintFields(dtoFields: OrderPrintValueDto[]): Promise> { const fieldIds = [...new Set(dtoFields.map((field) => field.fieldId))]; const fields = await this.em.find(Field, { id: { $in: fieldIds } }); if (fields.length !== fieldIds.length) { throw new NotFoundException('One or more fields not found.'); } return new Map(fields.map((field) => [field.id, field])); } private assignPrintValues( print: OrderPrint, dtoFields: OrderPrintValueDto[], fieldMap: Map, ): void { for (const dtoField of dtoFields) { print.values.add( this.em.create(OrderPrintValue, { orderPrint: print, field: fieldMap.get(dtoField.fieldId)!, value: dtoField.value, }), ); } } // -----------Hrlper Methods ----------- async findOrderOrFail(id: string): Promise { const order = await this.em.findOne(Order, { id }, { populate: ['user', 'type', 'product', 'creator', 'designer'] }); if (!order) { throw new NotFoundException(`Order with ID ${id} not found.`); } return order; } async findOrderOrFailAssignedOrder(id: string, adminId: string): Promise { const order = await this.em.findOne(Order, { id }, { populate: ['user', 'type', 'product', 'creator'] }); if (!order) { throw new NotFoundException(`Order with ID ${id} not found.`); } if (order?.designer?.id !== adminId) { throw new ForbiddenException("You don't have permission to view this order") } return order; } async getStats(userId: string) { const [requestsCount, invoicesCount, inProgressOrdersCount, completedOrdersCount] = await Promise.all([ this.em.count(Request, { user: { id: userId } }), this.em.count(Invoice, { user: { id: userId } }), this.em.count(Order, { user: { id: userId }, status: { $nin: [OrderStatusEnum.FINISHED, OrderStatusEnum.CANCELED] }, }), this.em.count(Order, { user: { id: userId }, status: OrderStatusEnum.FINISHED }), ]); return { requestsCount, invoicesCount, inProgressOrdersCount, completedOrdersCount, }; } async getAdminStats() { const [requestsCount, invoicesCount, inProgressOrdersCount, completedOrdersCount] = await Promise.all([ this.em.count(Request), this.em.count(Invoice), this.em.count(Order, { status: { $nin: [OrderStatusEnum.FINISHED, OrderStatusEnum.CANCELED] }, }), this.em.count(Order, { status: OrderStatusEnum.FINISHED }), ]); return { requestsCount, invoicesCount, inProgressOrdersCount, completedOrdersCount, }; } }