first
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user