import { Injectable, BadRequestException, Logger } from '@nestjs/common'; import { EntityManager } from '@mikro-orm/postgresql'; import { OrderItem } from '../entities/order-item.entity'; import { OrderRepository } from '../repositories/order.repository'; import { FindOrdersDto } from '../dto/find-orders.dto'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { CreateOrderDto, CreateOrderItemAsUserDto, CreateOrderItemDtoAsAdmin, CreateOrderAsAdminDto } from '../dto/create-order.dto'; import { UserService } from 'src/modules/user/providers/user.service'; import { IAddOrderItem, ICreateOrder, OrderStatusEnum, IUpdateOrderItem, IUpdateOrder } 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'; @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 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: CreateOrderAsAdminDto) { const order = await this.createOrder({ ...dto, adminId }) await this.calculateOrder(order) this.em.flush() return order } async createOrderAsUser(userId: string, dto: CreateOrderDto) { const order = await this.createOrder({ ...dto, userId, status: OrderStatusEnum.CREATED }) return order } async createOrder(dto: ICreateOrder) { const { items, userId, attachments, designerId, enableTax, estimatedDays, paymentMethod, status, adminId } = dto const user = await this.userService.findById(userId) if (!user) { throw new BadRequestException("User not found!") } let admin: null | Admin = null if (adminId) { admin = await this.adminRepository.findOne({ id: adminId }) if (!admin) { throw new BadRequestException("Admin not found") } } let designer: null | Admin = null if (designerId) { designer = await this.adminRepository.findOne({ id: designerId }) if (!designer) { throw new BadRequestException("designer not found") } } const order = await this.em.transactional(async (em) => { const order = this.orderRepository.create({ creator: admin, user, attachments, designer, enableTax, estimatedDays, paymentMethod, status, }) em.persist(order) const productIds = items.map(item => item.productId) const products = await this.productRepository.find({ id: { $in: productIds } }) if (productIds.length !== products.length) { throw new BadRequestException("some products not found") } items.forEach(item => { this.persistOrderItem(order, item) }); // TODO : calculation must be done after create order await this.calculateOrder(order) // await em.flush() return order }) return order } async updateOrder(orderId: string, dto: IUpdateOrder) { const { attachments, adminId, designerId, enableTax, estimatedDays, paymentMethod, status, userId } = dto const order = await this.findOneOrFail(orderId) if (userId) { const user = await this.userService.findById(userId) if (!user) { throw new BadRequestException("User not found!") } order.user = user } if (adminId) { const admin = await this.adminRepository.findOne({ id: adminId }) if (!admin) { throw new BadRequestException("Admin not found") } order.creator = admin } if (designerId) { const designer = await this.adminRepository.findOne({ id: designerId }) if (!designer) { throw new BadRequestException("designer not found") } order.designer = designer } if (attachments) { order.attachments = attachments } if (typeof enableTax !== 'undefined') { order.enableTax = enableTax } if (estimatedDays) { order.estimatedDays = estimatedDays } if (paymentMethod) { order.paymentMethod = paymentMethod } if (status) { order.status = status } this.em.persist(order) await this.calculateOrder(order) await this.em.flush() return order } async addOrderItemAsUser(userId: string, orderId: string, dto: CreateOrderItemAsUserDto) { const order = await this.findOneOrFail(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 orderItem = this.persistOrderItem(order, dto) await this.em.flush() return orderItem } async addOrderItemAsAdmin(orderId: string, dto: CreateOrderItemDtoAsAdmin) { const order = await this.findOneOrFail(orderId) const orderItem = this.persistOrderItem(order, dto) await this.em.flush() await this.calculateOrder(undefined, orderId) await this.em.flush() return orderItem } private async persistOrderItem(order: Order, dto: IAddOrderItem) { const { attributes, description, productId, quantity, attachments, discount, unitPrice } = dto const found = order.items.find(it => it.product.id == productId) if (found) { throw new BadRequestException(`Product already exists`) } const product = await this.productRepository.findOne({ id: productId }) if (!product) { throw new BadRequestException(`Product not found`) } const orderItem = this.orderItemRepository.create({ order, attributes, description, quantity, attachments, subTotal: 0, discount: discount ?? 0, total: 0, unitPrice: unitPrice ?? 0, product }) this.em.persist(orderItem) return orderItem } async updateOrderItemAsUser(userId: string, orderId: string, itemId: string, dto: UpdateOrderItemDtoAsUser) { const order = await this.findOneOrFail(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: string, dto: UpdateOrderItemDtoAsAdmin) { const order = await this.findOneOrFail(orderId) const orderItem = await this.updateOrderItem(itemId, dto) await this.calculateOrder(undefined, orderId) await this.em.flush() return orderItem } async updateOrderItem(itemId: string, dto: IUpdateOrderItem) { 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 && 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 removeOrderItemAsAdmin(orderId: string, itemId: string) { const order = await this.findOneOrFail(orderId) await this.removeOrderItem(itemId) await this.calculateOrder(undefined, orderId) await this.em.flush() return true } async removeOrderItemAsUser(userId: string, orderId: string, itemId: string) { const order = await this.findOneOrFail(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 removeOrderItem(itemId: string) { const orderItem = await this.orderItemRepository.findOne({ id: itemId }) if (!orderItem) { throw new BadRequestException(`orderItem not found`) } await this.em.removeAndFlush(orderItem) return true } async findUserOrders(userId: string, dto: FindOrdersDto) { const orders = await this.orderRepository.findAllPaginated({ userId, ...dto }) return orders } async findOneOrFail(orderId: string) { const order = await this.orderRepository.findOne({ id: orderId }, { populate: ['items', 'items.product', 'payments'] }) if (!order) { throw new BadRequestException('Order not found') } return order } async calculateOrder(inputOrder?: Order, orderId?: string) { let order: undefined | Order = undefined if (orderId) { order = await this.findOneOrFail(orderId) } if (order) { order = inputOrder } if (!order) { throw new BadRequestException("Order not found") } // calculate order financials let subTotal = 0 let totalDiscount = 0 // TODO : use reduce for (let orderItem of order.items) { subTotal += orderItem.total totalDiscount += Number(orderItem.discount) } const totalBeforeTax = subTotal - totalDiscount let tax = 0 if (order.enableTax) { tax = 0.1 * totalBeforeTax } const total = totalBeforeTax + tax const paidAmount = await this.paymentRepository.getActualPaidAmount(order.id) // 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 hardDeleteOrder(orderId: string) { const order = await this.orderRepository.findOne({ id: orderId }) if (!order) { throw new BadRequestException("Order not found") } if (![OrderStatusEnum.CREATED, OrderStatusEnum.INVOICED].includes(order.status)) { throw new BadRequestException("Order status must be of of Drfat or Invoiced") } 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" } } async removeOrderAsUser(orderId: string) { const order = await this.orderRepository.findOne({ id: orderId }) if (!order) { throw new BadRequestException("Order not found") } 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.orderRepository.findOne({ id: orderId }) if (!order) { throw new BadRequestException("Order not found") } order.status = newStatus await this.em.flush() return order } async createInvoice(orderId: string) { const order = await this.findOneOrFail(orderId) await this.updateStatus(orderId, OrderStatusEnum.INVOICED) await this.calculateOrder(order) await this.em.flush() } }