353 lines
14 KiB
TypeScript
353 lines
14 KiB
TypeScript
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<Record<string, Items[]>>((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<Record<string, Items & { weight: number }>>((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<number> {
|
||
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 };
|