import { inject, injectable } from "inversify"; import { Types } from "mongoose"; import { SaveSellerShipmentInfoDTO } from "./DTO/calculateShipCost.dto"; import { CreateShipProvidersDTO } from "./DTO/createShipmentProviders.dto"; import { ShippingOptionDTO } from "./DTO/shipment.dto"; import { IShipmentProviders } from "./models/shipment.model"; import { ShipmentRepository } from "./shipment.repository"; import { Items } from "./types/items"; import { CartMessage, CommonMessage, SetShipmentMessage } from "../../common/enums/message.enum"; import { DeliveryType } from "../../common/enums/shipment.enum"; import { BadRequestError } from "../../core/app/app.errors"; import { IOCTYPES } from "../../IOC/ioc.types"; import { CartRepository, CartShipItemRepo } from "../cart/cart.repository"; import { ICart } from "../cart/models/Abstraction/ICart"; import { ProductVariantRepository } from "../product/Repository/productVarinat"; import { SellerRepository } from "../seller/seller.repository"; import { ShopRepo } from "../shop/shop.repository"; import { UpdateShipmentProviderDTO } from "./DTO/updateShipment.dto"; @injectable() class ShipmentService { @inject(IOCTYPES.ShipmentRepository) shipmentRepo: ShipmentRepository; @inject(IOCTYPES.SellerRepository) sellerRepo: SellerRepository; @inject(IOCTYPES.CartRepository) cartRepo: CartRepository; @inject(IOCTYPES.ProductVariantRepository) productVariantRepo: ProductVariantRepository; @inject(IOCTYPES.CartShipItemRepo) cartShipItemRepo: CartShipItemRepo; @inject(IOCTYPES.ShopRepo) shopRepo: ShopRepo; async getAllProviders(queries: { limit: number; page: number }) { const { count, shipper } = await this.shipmentRepo.findAllShipper(queries); return { count, shipper }; } //################################################################### //################################################################### async createShipmentProviderS(createDto: CreateShipProvidersDTO) { const existShipper = await this.shipmentRepo.model.exists({ name: createDto.name }); if (existShipper) throw new BadRequestError(CommonMessage.DuplicateName); const deliveryType = this.getDeliveryType(createDto.deliveryTime); const newProvider = await this.shipmentRepo.model.create({ deliveryType, ...createDto }); return { message: CommonMessage.Created, newProvider, }; } //################################################################### async updateShipmentProvider(shipmentId: number, updateDto: UpdateShipmentProviderDTO) { const existShipper = await this.shipmentRepo.model.exists({ name: updateDto.name, _id: { $ne: shipmentId } }); if (existShipper) throw new BadRequestError(CommonMessage.DuplicateName); if (updateDto.deliveryTime) { updateDto.deliveryType = this.getDeliveryType(updateDto.deliveryTime); } const updatedProvider = await this.shipmentRepo.model.findByIdAndUpdate(shipmentId, updateDto, { new: true }); if (!updatedProvider) throw new BadRequestError(CommonMessage.NotFoundById); return { message: CommonMessage.Updated, updatedProvider, }; } //################################################################### async deleteShipper(shipperId: number) { const shipper = await this.shipmentRepo.model.findByIdAndUpdate(shipperId, { deleted: true }, { new: true }); if (!shipper) throw new BadRequestError(CommonMessage.NotFoundById); return { message: CommonMessage.Deleted, shipper, }; } //################################################################### //################################################################### async calculateAllShipmentOptions(userId: string) { // get the cart for the user const cart = await this.cartRepo.getCartWithPopulate(userId); if (!cart) throw new BadRequestError(CartMessage.CartNotFound); console.log({ cart }); const shipping = []; const cartItems = cart.items as unknown as Items[]; // Group items by shop const itemsByShop = cartItems.reduce>((acc, item) => { const shopId = item.variant.shop._id.toString(); if (!acc[shopId]) { acc[shopId] = []; } acc[shopId].push({ product: item.product, variant: item.variant, quantity: item.quantity, }); return acc; }, {}); console.log({ itemsByShop }); // iterate through each shops items for (const [shopId, items] of Object.entries(itemsByShop)) { const shop = await this.shopRepo.model.findById(shopId).lean(); console.log(shop); if (!shop) { continue; } const shopShippers = []; // for each shop, check all available shippers for (const shipperId of shop.shipmentMethod) { const shipmentProvider = await this.shipmentRepo.model.findById(shipperId).lean(); console.log({ shipmentProvider }); if (!shipmentProvider) { console.log("Shipment provider not found for shipper ID:", shipperId); continue; } // group items by product and variant const itemsByProductVariant = items.reduce>((acc, item) => { const key = `${item.product}-${item.variant._id}`; if (!acc[key]) { acc[key] = { product: item.product, variant: item.variant, quantity: 0, weight: 0 }; } acc[key].quantity += item.quantity; acc[key].weight += item.variant.dimensions.package_weight * item.quantity; return acc; }, {}); let totalShippingCost = 0; const shipperItems = []; console.log({ itemsByProductVariant }); // calculate shipping cost and gather item details for each shipper for (const { product, variant, quantity, weight } of Object.values(itemsByProductVariant)) { const shippingCostForItem = await this.calculateShippingCost(shipmentProvider, weight, quantity); totalShippingCost += shippingCostForItem; shipperItems.push({ product: product._id, variant: variant._id, quantity, selling_price: variant.price.selling_price, retail_price: variant.price.retailPrice, discount_percent: variant.price.discount_percent, shippingCost: variant.isFreeShip ? 0 : shippingCostForItem, isFreeShip: variant.isFreeShip, }); } // push shipper details for this seller shopShippers.push({ shipperId: shipmentProvider._id, shipperName: shipmentProvider.name, shippingDaysRange: this.calculateShippingDays(shipmentProvider.deliveryType, shipmentProvider.deliveryTime), totalShippingCost, items: shipperItems, }); } // push seller info and their shippers shipping.push({ shopId, shopName: shop.shopName, items, shippers: shopShippers, }); console.log({ shipping }); } return { shipping: shipping.map((ship) => ShippingOptionDTO.transformShipment(ship)) }; } //################################################################### //################################################################### async selectShipmentProvider(saveShippingInfo: SaveSellerShipmentInfoDTO, userId: string) { const cart = (await this.cartRepo.getCartWithAggregate(userId))[0] as ICart; if (!cart) throw new BadRequestError(CartMessage.CartNotFound); const { shipping: availableShipmentOptions } = await this.calculateAllShipmentOptions(userId); if (!availableShipmentOptions.length) throw new BadRequestError(SetShipmentMessage.ShipmentOptionsUnavailable); const { shipmentsInfo } = saveShippingInfo; // ensure shipment info matches the number of sellers in the cart const shopIds = availableShipmentOptions.map((shipperOption) => shipperOption.shopId); const shipmentShopIds = shipmentsInfo.map((shipInfo) => shipInfo.shopId); // validate that the selected shop IDs match the cart shop IDs const unmatchedSellers = shipmentShopIds.filter((id) => !shopIds.includes(id)); if (unmatchedSellers.length > 0) throw new BadRequestError([SetShipmentMessage.InvalidShipmentItems, unmatchedSellers.join(",")]); // process each seller's shipment info for (const shipInfo of shipmentsInfo) { const shopShipmentOptions = availableShipmentOptions.find((option) => option.shopId === shipInfo.shopId); if (!shopShipmentOptions) { throw new BadRequestError([ SetShipmentMessage.InvalidShipmentSellers, `shop with ID ${shipInfo.shopId} does not have shipment options.`, ]); } const selectedShipper = shopShipmentOptions.shippers.find((shipper) => shipper.shipperId === shipInfo.shipmentId); if (!selectedShipper) { throw new BadRequestError([ SetShipmentMessage.ShipperNotAvailableForSeller, `Shipper with ID ${shipInfo.shipmentId} is not available for shop ${shipInfo.shopId}.`, ]); } // find existing CartShipmentItem for this seller or create a new one const existingCartShipmentItem = await this.cartShipItemRepo.model.findOne({ cart: cart._id.toString(), shop: new Types.ObjectId(shipInfo.shopId), }); const shipmentItems = selectedShipper.items.map((item) => ({ product: item.product, variant: new Types.ObjectId(item.variant), quantity: item.quantity, selling_price: item.selling_price, retail_price: item.retail_price, discount_percent: item.discount_percent, shipmentCost: item.shippingCost, isFreeShip: item.isFreeShip, })); if (existingCartShipmentItem) { // update the existing CartShipmentItem existingCartShipmentItem.shipmentItems = shipmentItems; existingCartShipmentItem.shipper = selectedShipper.shipperId; await existingCartShipmentItem.save(); } else { const totalSellingPrice = shipmentItems.reduce((acc, item) => acc + item.selling_price * item.quantity, 0); const totalRetailPrice = shipmentItems.reduce((acc, item) => acc + item.retail_price * item.quantity, 0); const totalShipmentCost = shipmentItems.reduce((acc, item) => acc + item.shipmentCost, 0); // create a new CartShipmentItem await this.cartShipItemRepo.model.create({ cart: cart._id.toString(), shop: new Types.ObjectId(shipInfo.shopId), shipmentItems, shipper: selectedShipper.shipperId, totalSellingPrice, totalRetailPrice, totalShipmentCost, totalPaymentPrice: totalShipmentCost + totalSellingPrice, itemsCount: shipmentItems.reduce((acc, item) => acc + item.quantity, 0), }); } } // calculate the total shipping cost const cartShipmentItems = await this.cartShipItemRepo.model.find({ cart: cart._id.toString() }).lean(); const totalShippingCost = cartShipmentItems.reduce( (sum, item) => sum + item.shipmentItems.reduce((subSum, shipmentItem) => subSum + shipmentItem.shipmentCost, 0), 0, ); return { cartShipmentItems: cartShipmentItems, shippingCost: totalShippingCost, processCost: totalShippingCost, payable_price: cart.payable_price + totalShippingCost * 2, }; } //===============================> helper method // Helper function to calculate shipping days range private calculateShippingDays(deliveryType: DeliveryType, deliveryTime: number): string[] { const currentDate = new Date(); let minDeliveryDays = 0; let maxDeliveryDays = 0; switch (deliveryType) { case DeliveryType.SameDay: minDeliveryDays = 0; maxDeliveryDays = 1; break; case DeliveryType.Express: minDeliveryDays = 1; maxDeliveryDays = 2; break; case DeliveryType.Standard: minDeliveryDays = 3; maxDeliveryDays = 5; break; default: minDeliveryDays = deliveryTime - 1; maxDeliveryDays = deliveryTime; break; } const shippingDaysRange: string[] = []; let processedDays = 0; const totalDays = maxDeliveryDays - minDeliveryDays + 1; let i = minDeliveryDays; while (processedDays < totalDays) { const estimatedDate = new Date(currentDate); estimatedDate.setDate(currentDate.getDate() + i); // check if the day is Thursday (4) or Friday (5) const dayNumber = estimatedDate.getDay(); // skip Thursday (4) and Friday (5) if (dayNumber === 4 || dayNumber === 5) { i++; continue; } const options = { day: "numeric", weekday: "long", month: "long" }; const dayName = estimatedDate.toLocaleString("fa-IR", options as any); shippingDaysRange.push(dayName); processedDays++; // only count this day if it’s a valid shipping day i++; // move to the next day } return shippingDaysRange; } // Helper function to calculate the shipping cost for a product variant based on shipment provider private async calculateShippingCost(shipmentProvider: IShipmentProviders, package_weight: number, quantity: number): Promise { console.log({ package_weight }); console.dir({ shipmentProvider: shipmentProvider.costs }, { depth: null }); const cost = shipmentProvider.costs.find( (cost) => package_weight / 1000 >= cost.weightRange.min && package_weight / 1000 <= cost.weightRange.max, ); console.log({ cost }); // if (!cost) throw new BadRequestError("No shipping cost available for this weight range"); const defaulCost = cost ?? { cost_first: 100_000, cost_rest: 10_000 }; const mainCost = defaulCost.cost_first; const restCost = defaulCost.cost_rest * (quantity - 1); // multiply the rest cost by the remaining quantity return mainCost + restCost; } //helper function to calculate the delivery type based on time private getDeliveryType(deliveryTime: number): DeliveryType { if (deliveryTime <= 1) return DeliveryType.SameDay; if (deliveryTime <= 2) return DeliveryType.Express; return DeliveryType.Standard; } } export { ShipmentService };