This commit is contained in:
mahyargdz
2025-09-14 15:15:03 +03:30
commit be82059172
534 changed files with 51310 additions and 0 deletions
+352
View File
@@ -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 its 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 };