import { Injectable, BadRequestException, Logger } 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 { CreateOrderDto, CreateOrderItemAsUserDto, CreateOrderItemDtoAsAdmin } from '../dto/create-order.dto'; import { UserService } from 'src/modules/user/providers/user.service'; import { CreateOrderItemParam, CreateOrderParam, OrderStatusEnum, UpdateOrderItem, UpdateOrderParam } from '../interface/order.interface'; import { OrderItemRepository } from '../repositories/order-item.repository'; import { ProductService } from 'src/modules/product/providers/product.service'; import { ProductRepository } from 'src/modules/product/repositories/product.repository'; import { TicketService } from 'src/modules/ticket/providers/tickets.service'; import { TicketRepository } from 'src/modules/ticket/repositories/tickets.repository'; import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; import { Order } from '../entities/order.entity'; import { PaymentRepository } from 'src/modules/payment/repositories/payment.repository'; import { UpdateOrderItemDtoAsAdmin, UpdateOrderItemDtoAsUser } from '../dto/update-order-item.dto'; import { Admin } from 'src/modules/admin/entities/admin.entity'; import { Product } from 'src/modules/product/entities/product.entity'; import { AdminService } from 'src/modules/admin/providers/admin.service'; import { OrderItem } from '../entities/order-item.entity'; @Injectable() export class OrderService { private readonly logger = new Logger(OrderService.name); constructor( private readonly em: EntityManager, private readonly orderRepository: OrderRepository, private readonly orderItemRepository: OrderItemRepository, private readonly userService: UserService, private readonly adminService: AdminService, private readonly productService: ProductService, private readonly productRepository: ProductRepository, private readonly ticketRepository: TicketRepository, private readonly ticketService: TicketService, private readonly eventEmitter: EventEmitter2, private readonly adminRepository: AdminRepository, private readonly paymentRepository: PaymentRepository, ) { } async createOrderAsAdmin(adminId: string, dto: CreateOrderParam) { const order = await this.createOrder({ ...dto, adminId }) // await this.calculateOrder(order) // this.em.flush() this.logger.log("Order created as admin") return order } async createOrderAsUser(userId: string, dto: CreateOrderDto) { const order = await this.createOrder({ ...dto, userId, status: OrderStatusEnum.CREATED }) this.logger.log("Order created as admin") return order } async updateOrderAsAdmin(orderId: string, param: UpdateOrderParam) { const order = await this.findOrderOrFail(orderId) const updateOrder = await this.updateOrder(order, param) // const updateOrder = await this.calculateOrder(order) // await this.em.flush() this.logger.log("Order updated as admin") return updateOrder } async addOrderItemAsUser(userId: string, orderId: string, dto: CreateOrderItemAsUserDto) { const order = await this.findOrderOrFail(orderId) if (order.status !== OrderStatusEnum.CREATED) { throw new BadRequestException(`You can not update when status is ${order.status}`) } if (order.user.id !== userId) { throw new BadRequestException(`This order doest belongs to you!`) } const product = await this.productService.findOneOrFail(dto.productId) const orderItem = this.createOrderItem(order, product, dto) await this.em.flush() return orderItem } async addOrderItemAsAdmin(orderId: string, dto: CreateOrderItemDtoAsAdmin) { const order = await this.findOrderOrFail(orderId) const product = await this.productService.findOneOrFail(dto.productId) const orderItem = this.createOrderItem(order, product, dto) await this.em.flush() // await this.calculateOrder(undefined, orderId) // await this.em.flush() return orderItem } async updateOrderItemAsUser(userId: string, orderId: string, itemId: number, dto: UpdateOrderItemDtoAsUser) { const order = await this.findOrderOrFail(orderId) if (order.user.id !== userId) { throw new BadRequestException(`This order doesnt belongs to you`) } if (order.status !== OrderStatusEnum.CREATED) { throw new BadRequestException(`You can not update when status is ${order.status}`) } const orderItem = await this.updateOrderItem(itemId, dto) return orderItem } async updateOrderItemAsAdmin(orderId: string, itemId: number, dto: UpdateOrderItemDtoAsAdmin) { await this.findOrderItemOrFail(orderId, itemId) const orderItem = await this.updateOrderItem(itemId, dto) // await this.calculateOrder(undefined, orderId) // await this.em.flush() return orderItem } async removeOrderItemAsAdmin(orderId: string, itemId: number) { const order = await this.findOrderOrFail(orderId) await this.removeOrderItem(itemId) // await this.calculateOrder(undefined, orderId) // await this.em.flush() return true } async removeOrderItemAsUser(userId: string, orderId: string, itemId: number) { const order = await this.findOrderOrFail(orderId) if (order.user.id !== userId) { throw new BadRequestException(`this order is not belongs to you`) } if (order.status !== OrderStatusEnum.CREATED) { throw new BadRequestException(`You can not update when status is ${order.status}`) } await this.removeOrderItem(itemId) return true } async findUserOrders(userId: string, dto: FindOrdersDto) { const orders = await this.orderRepository.findAllPaginated({ userId, ...dto }) return orders } // async calculateOrder(inputOrder?: Order, orderId?: string) { // let order: undefined | Order = inputOrder // if (!order && orderId) { // order = await this.findOneOrFail(orderId) // } // if (!order) { // throw new BadRequestException("Order not found") // } // // calculate order financials // const subTotal = order.items.reduce((sum, item) => { // return sum + item.subTotal // }, 0) // const totalDiscount = order.items.reduce((sum, item) => { // return sum + Number(item.discount) // }, 0) // const totalBeforeTax = subTotal - totalDiscount // let tax = 0 // if (order.enableTax) { // tax = 0.1 * totalBeforeTax // } // const total = totalBeforeTax + tax // // Update Order financial values // // order.subTotal = subTotal // // order.discount = totalDiscount // // order.taxAmount = tax // // order.total = total // // order.balance = total - paidAmount // return order // } async confirmOrderItem(userId: string, orderId: string, orderItemId: number) { const orderItem = await this.orderItemRepository.findOne({ id: orderItemId, order: { id: orderId } }, { populate: ['order', 'order.user'] }) if (!orderItem) { throw new BadRequestException("Order Item not found") } if (orderItem.order.user.id !== userId) { throw new BadRequestException("Order Item does not belong to you") } orderItem.confirmedAt = new Date() await this.em.persistAndFlush(orderItem) return orderItem } async assignDesigner(orderId: string, designerId: string) { const order = await this.orderRepository.findOne({ id: orderId }) if (!order) { throw new BadRequestException("Order not found") } const designer = await this.adminRepository.findOne({ id: designerId }) if (!designer) { throw new BadRequestException("designer not found") } order.designer = designer // order.status = OrderStatusEnum.IN_DESIGN await this.em.persistAndFlush(order) return order } async removeOrderAsUser(userId: string, orderId: string) { const order = await this.findOrderOrFail(orderId) if (order.user.id !== userId) { throw new BadRequestException("this order doesnt belongs to you") } if (![OrderStatusEnum.CREATED].includes(order.status)) { throw new BadRequestException("Order can not be deleted") } return this.hardDeleteOrder(orderId) } async updateStatus(orderId: string, newStatus: OrderStatusEnum) { const order = await this.findOrderOrFail(orderId) order.status = newStatus await this.em.flush() return order } async createInvoice(orderId: string) { const order = await this.findOrderOrFail(orderId) await this.updateStatus(orderId, OrderStatusEnum.INVOICED) // await this.calculateOrder(order) await this.em.flush() } async getOrderAsUser(userId: string, orderId: string) { const order = await this.findOrderOrFail(orderId) if (order.user.id !== userId) { throw new BadRequestException("This order is not belongs to you") } return order } // ==================== Entity Methods ==================== private async createOrderItem(order: Order, product: Product, dto: CreateOrderItemParam, em?: EntityManager) { const { attributes, description, quantity, attachments, discount, unitPrice } = dto const eM = em ?? this.em return eM.create( OrderItem, { order, attributes, description, quantity, attachments, discount: discount ?? undefined, unitPrice: unitPrice ?? undefined, product }) } // ==================== Core Logic ==================== async createOrder(dto: CreateOrderParam) { const { items, userId, attachments, designerId, enableTax, estimatedDays, paymentMethod, status, adminId } = dto // Validate and fetch all required entities const user = await this.userService.findOrFail(userId) const admin = adminId ? await this.adminService.findOrFail(adminId) : undefined const designer = designerId ? await this.adminService.findOrFail(designerId) : undefined const products = await this.productService.findProductsByIds(items.map(item => item.productId)) const productMap = new Map( products.map(p => [Number(p.id), p]) ); return this.em.transactional(async (em) => { const order = em.create(Order, { creator: admin, user, attachments, designer, enableTax, estimatedDays, paymentMethod, status, }) em.persist(order) for (const item of items) { const product = productMap.get(Number(item.productId)) if (!product) { throw new BadRequestException("Product not found!") } const orderItem = await this.createOrderItem(order, product, item, em) order.items.add(orderItem) } // TODO : calculation must be done after create order // await this.calculateOrder(order) await em.flush() return order }) } async updateOrder(order: Order, param: UpdateOrderParam) { const { attachments, adminId, designerId, enableTax, estimatedDays, paymentMethod, status, userId } = param if (userId) { const user = await this.userService.findOrFail(userId) order.user = user } if (adminId != undefined && adminId !== null) { const admin = await this.adminService.findOrFail(adminId) order.creator = admin } if (designerId != undefined && designerId != undefined) { const designer = await this.adminService.findOrFail(designerId) order.designer = designer } if (attachments != undefined) { order.attachments = attachments } if (enableTax != undefined) { order.enableTax = enableTax } if (estimatedDays != undefined) { order.estimatedDays = estimatedDays } if (paymentMethod != undefined) { order.paymentMethod = paymentMethod } if (status != undefined) { order.status = status } await this.em.persistAndFlush(order) return order } async updateOrderItem(itemId: number, dto: UpdateOrderItem) { const { attributes, description, productId, quantity, attachments, discount, unitPrice } = dto const orderItem = await this.orderItemRepository.findOne({ id: itemId }) if (!orderItem) { throw new BadRequestException(`orderItem not found`) } // product is changed if (productId && Number(orderItem.product.id) !== productId) { const product = await this.productRepository.findOne({ id: productId }) if (!product) { throw new BadRequestException(`product not found`) } orderItem.product = product } if (attributes) { orderItem.attributes = attributes } if (description) { orderItem.description = description } if (quantity) { orderItem.quantity = quantity } if (attachments) { orderItem.attachments = attachments } if (discount) { orderItem.discount = discount } if (unitPrice) { orderItem.unitPrice = unitPrice } await this.em.flush() return orderItem } async removeOrderItem(itemId: number) { const orderItem = await this.orderItemRepository.findOne({ id: itemId }) if (!orderItem) { throw new BadRequestException(`orderItem not found`) } await this.em.removeAndFlush(orderItem) return true } async hardDeleteOrder(orderId: string) { const order = await this.findOrderOrFail(orderId) if (order.payments.length > 0) { throw new BadRequestException("order with payments can not be deleted!") } await this.em.transactional(async (em) => { await this.orderRepository.nativeDelete(order) await this.orderItemRepository.nativeDelete({ order: { id: orderId } }) await this.ticketRepository.nativeDelete({ order: { id: orderId } }) await em.flush() }) return { message: "Order deleted successfully" } } // ==================== Helper Methods ==================== async findOrderOrFail(orderId: string) { const order = await this.orderRepository.findOne({ id: orderId }, { populate: ['items', 'items.product', 'payments', 'user'] }) if (!order) { throw new BadRequestException('Order not found') } return order } async findOrderItemOrFail(orderId: string, itemId: number) { const orderItem = await this.orderItemRepository.findOne({ id: itemId, order: { id: orderId } }, { populate: [] }) if (!orderItem) { throw new BadRequestException('Order item not found') } return orderItem } }