import { inject, injectable } from "inversify"; import { ClientSession, startSession } from "mongoose"; import { InsertTrackCodeDTO, InsertTrackCodeGroupDTO } from "./DTO/insertTrackCode.dto"; import { ReceivedOrderItemDTO } from "./DTO/received.dto"; import { SellerOrdersDTO } from "./DTO/sellerOrder.dto"; import { UserOrderDTO } from "./DTO/userOrder.dto"; import { IShipmentAddress } from "./models/Abstraction/IOrder"; import { IOrderItem } from "./models/Abstraction/IOrderItem"; import { OrderItemRepo, OrderRepository } from "./order.repository"; import { AddressMessage, OrderMessage, ShopMessage, UserMessage } from "../../common/enums/message.enum"; import { OrderItemsStatus, OrdersStatus } from "../../common/enums/order.enum"; import { OrderStatusQuery, SellerOrdersQueries } from "../../common/types/query.type"; import { BadRequestError } from "../../core/app/app.errors"; import { IOCTYPES } from "../../IOC/ioc.types"; import { OrderQueue } from "../../queues/order/OrderQueue"; import { EmailService } from "../../utils/email.service"; import { AddressService } from "../address/address.service"; import { ICity } from "../address/models/city.model"; import { IProvince } from "../address/models/province.model"; import { ICartShipmentItem } from "../cart/models/Abstraction/ICartShipmentItem"; import { ICartPayment } from "../payment/models/Abstraction/IPayments"; import { OwnerRef } from "../shop/models/Abstraction/IShop"; import { ShopRepo } from "../shop/shop.repository"; import { IUser } from "../user/models/Abstraction/IUser"; import { WalletService } from "../wallet/wallet.service"; @injectable() class OrderService { @inject(IOCTYPES.OrderRepository) private orderRepo: OrderRepository; @inject(IOCTYPES.OrderItemRepo) private orderItemRepo: OrderItemRepo; @inject(IOCTYPES.ShopRepo) private shopRepo: ShopRepo; @inject(IOCTYPES.AddressService) private addressService: AddressService; @inject(IOCTYPES.WalletService) private walletService: WalletService; //####################################################### //####################################################### async updateOrderStatus(orderId: number, status: OrdersStatus, session: ClientSession) { return await this.orderRepo.model.findByIdAndUpdate(orderId, { orderStatus: status }, { session }); } //####################################################### //####################################################### async getOrderItemByOrderId(orderId: number, session: ClientSession) { return await this.orderItemRepo.model.find({ order: orderId }, null, { session }); } //####################################################### //####################################################### async getOrderByPaymentId(paymentId: string, session: ClientSession) { return await this.orderRepo.model.findOne({ payment: paymentId }).session(session); } //####################################################### //####################################################### async createOrder(user: IUser, payment: ICartPayment, cartShipmentItems: ICartShipmentItem[], session: ClientSession) { if (!user.address) throw new BadRequestError([AddressMessage.UserShouldHaveAddress, AddressMessage.NotFound]); const userAddress = await this.addressService.getUserAddress(user.address.toString()); const city = userAddress.city as unknown as ICity; const province = userAddress.province as unknown as IProvince; const shipmentAddress: IShipmentAddress = { address: userAddress.address, city: city.name, province: province.name, phone: user.phoneNumber, plaque: userAddress.plaque, postalCode: userAddress.postalCode, }; const order = await this.orderRepo.model.create( [ { user: user._id.toString(), payment: payment._id, shipmentAddress, }, ], { session }, ); const orderItemsData: IOrderItem[] = cartShipmentItems.map((item) => ({ order: order[0]._id, shop: item.shop, shipmentItems: item.shipmentItems, shipper: item.shipper, totalSellingPrice: item.totalSellingPrice, totalPaymentPrice: item.totalPaymentPrice, totalRetailPrice: item.totalRetailPrice, totalCouponDiscount: item.totalCouponDiscount, totalShipmentCost: item.totalShipmentCost, itemsCount: item.itemsCount, status: OrderItemsStatus.Processing, })); const orderItems = await this.orderItemRepo.model.create(orderItemsData, { session }); // Add the order to the queue with a 5-minute delay await OrderQueue.addOrderToQueue(order[0]._id, 5 * 60 * 1000); return { order: order[0], orderItems, }; } //####################################################### //####################################################### async getAdminOrders(sellerId: string, queries: SellerOrdersQueries) { const shop = await this.shopRepo.model.findOne({ owner: sellerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const priceRange = await this.orderItemRepo.getOrderPriceRange(shop._id.toString()); const { docs, count } = await this.orderItemRepo.getOrdersForSellerPanel(shop._id.toString(), queries); const orders = docs.map((doc: any) => SellerOrdersDTO.transformOrder(doc)); return { orders, count, priceRange }; } //####################################################### //####################################################### async getSellerOrders(ownerRef: OwnerRef, queries: SellerOrdersQueries) { const shops = await this.shopRepo.model.find({ ownerRef }).select("_id"); const shopIds = shops.map((shop) => shop._id); const priceRange = await this.orderItemRepo.getSellerOrderPriceRange(shopIds); const { docs, count } = await this.orderItemRepo.getSellerOrdersForAdminPanel(shopIds, queries); const orders = docs.map((doc: any) => SellerOrdersDTO.transformOrder(doc)); return { orders, count, priceRange }; } //####################################################### //####################################################### async getOrders(sellerId: string, queries: SellerOrdersQueries, orderIds?: string[] | string) { const shop = await this.shopRepo.model.findOne({ owner: sellerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const priceRange = await this.orderItemRepo.getOrderPriceRange(shop._id.toString()); const { docs, count } = await this.orderItemRepo.getOrdersForSellerPanel(shop._id.toString(), queries, orderIds); const orders = docs.map((doc: any) => SellerOrdersDTO.transformOrder(doc)); return { orders, count, priceRange }; } //####################################################### //####################################################### async insertTrackCode(orderId: number, sellerId: string, insertDto: InsertTrackCodeDTO) { const { orderItem } = await this.validateSellerOrder(orderId, sellerId); orderItem.trackCode = insertDto.trackCode; orderItem.postingDate = insertDto.postingDate; orderItem.postingReceipt = insertDto.postingReceipt; orderItem.status = OrderItemsStatus.Shipped; await orderItem.save(); return { message: OrderMessage.TrackingDetailInserted, orderItem, }; } //####################################################### async insertTrackCodeGroup(sellerId: string, insertDto: InsertTrackCodeGroupDTO) { const session = await startSession(); session.startTransaction(); try { const orderIds = insertDto.trackCodes.map((tc) => tc.orderId); const shop = await this.shopRepo.model.findOne({ owner: sellerId, ownerRef: OwnerRef.SELLER }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const orderItems = await this.orderItemRepo.model.find({ order: { $in: orderIds }, shop: shop._id }).session(session); if (orderItems.length !== orderIds.length) throw new BadRequestError(OrderMessage.OrderItemsNotFound); // Prepare bulk operations const bulkOps = insertDto.trackCodes.map((trackCodeDto) => ({ updateOne: { filter: { order: trackCodeDto.orderId, shop: shop._id, }, update: { $set: { trackCode: trackCodeDto.trackCode, postingDate: trackCodeDto.postingDate, postingReceipt: trackCodeDto.postingReceipt, status: OrderItemsStatus.Shipped, }, }, }, })); // Execute bulk operation await this.orderItemRepo.model.bulkWrite(bulkOps, { session }); await session.commitTransaction(); return { message: OrderMessage.TrackingDetailInserted, }; } catch (error) { await session.abortTransaction(); throw error; } finally { await session.endSession(); } } //####################################################### //####################################################### async insertNativeShopTrackCode(orderId: number, adminId: string, insertDto: InsertTrackCodeDTO) { const { orderItem } = await this.validateSellerOrder(orderId, adminId); orderItem.trackCode = insertDto.trackCode; orderItem.postingDate = insertDto.postingDate; orderItem.postingReceipt = insertDto.postingReceipt; orderItem.status = OrderItemsStatus.Shipped; await orderItem.save(); return { message: OrderMessage.TrackingDetailInserted, orderItem, }; } //####################################################### //####################################################### async trackOrder(orderId: number, userId: string, userEmail: string) { if (isNaN(orderId)) throw new BadRequestError(OrderMessage.NotFound); if (!userEmail) throw new BadRequestError(UserMessage.EmailNotExist); const order = await this.orderRepo.model.findOne({ _id: orderId, user: userId }); if (!order) throw new BadRequestError(OrderMessage.NotFound); const trackingDetails = await this.orderItemRepo.getOrderTrackingDetails(orderId); if (!trackingDetails.length) throw new BadRequestError(OrderMessage.TrackingCodeNotFound); const trackingInfo = trackingDetails.map((item: any) => ({ trackCode: item.trackCode, status: item.status, shipper: item.shipper.name, postingDate: item.postingDate, })); await EmailService.sendOrderTrackingEmail(userEmail, { orderId, trackingInfo }); return { message: OrderMessage.TrackingEmailSent, }; } //####################################################### async setOrderItemReceived(orderId: number, userId: string, receivedOrderItemDto: ReceivedOrderItemDTO) { const session = await startSession(); session.startTransaction(); try { await this.validateUserOrder(orderId, userId); const orderItem = await this.orderItemRepo.model.findById(receivedOrderItemDto.orderItemId).session(session); if (!orderItem) throw new BadRequestError(`OrderItem with ID ${receivedOrderItemDto.orderItemId} not found.`); if (orderItem.status === OrderItemsStatus.Delivered) throw new BadRequestError(OrderMessage.ItemAlreadyReceived); if (orderItem.status !== OrderItemsStatus.Shipped) throw new BadRequestError(OrderMessage.ItemNotShipped); orderItem.status = OrderItemsStatus.Delivered; const shop = await this.shopRepo.model.findById(orderItem.shop.toString()).session(session); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFoundById); if (shop.ownerRef === OwnerRef.SELLER) { await this.walletService.creditSellerWalletForOrder(shop.owner.toString(), orderId, orderItem.totalPaymentPrice, session); } await orderItem.save({ session }); await session.commitTransaction(); await session.endSession(); return { message: OrderMessage.ItemReceived, }; } catch (error) { await session.abortTransaction(); await session.endSession(); throw error; } } //####################################################### //####################################################### async getSellerOrderDetail(orderId: number, sellerId: string) { const { shop } = await this.validateSellerOrder(orderId, sellerId); const doc = await this.orderItemRepo.getSellerOrderDetail(orderId, shop._id.toString()); const order = SellerOrdersDTO.transformOrder(doc[0]); return { order }; } //####################################################### //####################################################### async getUserOrderDetail(orderId: number, userId: string) { await this.validateUserOrder(orderId, userId); const doc = await this.orderRepo.getUserOrderDetail(orderId, userId); const order = UserOrderDTO.transformOrder(doc[0]); return { order }; } async getUserOrderItemsDetail(orderId: number, orderItemId: string, userId: string) { await this.validateUserOrder(orderId, userId); const doc = await this.orderItemRepo.getUserOrderItemsDetail(orderId, orderItemId); const orderItems = UserOrderDTO.transformOrder(doc[0]); return { orderItems }; } //####################################################### //####################################################### async getUserOrderDetailAsAdmin(orderId: number) { const { order } = await this.validateAdminUserOrder(orderId); const doc = await this.orderRepo.getUserOrderDetail(orderId, order.user._id.toString()); const cleanedOrder = SellerOrdersDTO.transformOrder(doc[0]); return { order: cleanedOrder }; } //####################################################### //####################################################### async getSellerOrderDetailAsAdmin(orderId: number) { const { shopIds } = await this.validateAdminSellerOrder(orderId); const doc = await this.orderItemRepo.getSellerOrderDetailForAdminPanel(orderId, shopIds); const cleanedOrder = SellerOrdersDTO.transformOrder(doc[0]); return { order: cleanedOrder }; } //####################################################### //####################################################### async getSellerSalesStats(sellerId: string, queries: { start: string; end: string }) { const shop = await this.shopRepo.model.findOne({ owner: sellerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const saleStats = await this.orderItemRepo.getSellerSalesStats(shop._id.toString(), queries); return { saleStats }; } //####################################################### //####################################################### async getUserOrders(userId: string, statusQuery: OrderStatusQuery) { const docs = await this.orderRepo.getUserOrders(userId, statusQuery); const orders = docs.map((doc) => UserOrderDTO.transformOrder(doc)); const mappedOrders = orders.map((order) => this.mapUserOrderItems(order)); const userOrderCount = await this.orderRepo.getUserOrderCount(userId); return { mappedOrders, orders, userOrderCount }; } //helper methods //####################################################### //####################################################### private async validateSellerOrder(orderId: number, sellerId: string) { const order = await this.orderRepo.findById(orderId); if (!order) throw new BadRequestError(OrderMessage.NotFound); const shop = await this.shopRepo.model.findOne({ owner: sellerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const orderItem = await this.orderItemRepo.model.findOne({ shop: shop._id.toString(), order: order._id }); if (!orderItem) throw new BadRequestError(OrderMessage.OrderItemsNotFound); return { order, orderItem, shop }; } //####################################################### //####################################################### private async validateUserOrder(orderId: number, userId: string) { const order = await this.orderRepo.model.findOne({ _id: orderId, user: userId }); if (!order) throw new BadRequestError(OrderMessage.NotFound); const orderItems = await this.orderItemRepo.model.find({ order: order._id }); if (!orderItems || orderItems.length === 0) throw new BadRequestError(OrderMessage.OrderItemsNotFound); return { order, orderItems }; } //####################################################### //####################################################### private async validateAdminUserOrder(orderId: number) { const order = await this.orderRepo.model.findOne({ _id: orderId }); if (!order) throw new BadRequestError(OrderMessage.NotFound); const orderItems = await this.orderItemRepo.model.find({ order: order._id }); if (!orderItems || orderItems.length === 0) throw new BadRequestError(OrderMessage.OrderItemsNotFound); return { order, orderItems }; } //####################################################### //####################################################### private async validateAdminSellerOrder(orderId: number) { const order = await this.orderRepo.model.findOne({ _id: orderId }).lean(); if (!order) throw new BadRequestError(OrderMessage.NotFound); const orderItems = await this.orderItemRepo.model.find({ order: orderId }).lean(); const shopIds = orderItems.map((item) => item.shop._id); const orderItem = await this.orderItemRepo.model.findOne({ shop: { in: shopIds }, order: order._id }); if (!orderItem) throw new BadRequestError(OrderMessage.OrderItemsNotFound); return { order, orderItem, shopIds }; } //####################################### //####################################### private mapUserOrderItems(userOrder: UserOrderDTO) { const shipmentItems = userOrder.orderItems.map((orderItem) => { return orderItem.shipmentItems; }); return { orderId: userOrder._id, user: userOrder.user, orderItems: shipmentItems.flat(Infinity), payment: userOrder.payment, shipmentAddress: userOrder.shipmentAddress, orderStatus: userOrder.orderStatus, createdAt: userOrder.createdAt, }; } //####################################### //####################################### async getDailySalesReport() { const sales = await this.orderItemRepo.getDailySalesReport(); return sales; } async getWeeklySalesReport() { const sales = await this.orderItemRepo.getWeeklySalesReport(); return sales; } async getMonthlySalesReport() { const sales = await this.orderItemRepo.getMonthlySalesReport(); return sales; } async getAnnualSalesReport() { const sales = await this.orderItemRepo.getAnnualSalesReport(); return sales; } async getDailyShipmentSalesReport() { const sales = await this.orderItemRepo.getDailyShipmentSalesReport(); return sales; } async getWeeklyShipmentSalesReport() { const sales = await this.orderItemRepo.getWeeklyShipmentSalesReport(); return sales; } async getMonthlyShipmentSalesReport() { const sales = await this.orderItemRepo.getMonthlyShipmentSalesReport(); return sales; } async getAnnualShipmentSalesReport() { const sales = await this.orderItemRepo.getAnnualShipmentSalesReport(); return sales; } async getFiveDaysAgoSalesReport() { const sales = await this.orderItemRepo.getFiveDaysAgoSalesReport(); return sales; } async getTopSellingProductsS() { const sales = await this.orderItemRepo.getTopSellingProducts(); return sales; } async getAllProcessingOrders() { const sales = await this.orderItemRepo.getAllProcessingOrders(); return sales; } } export { OrderService };