Files
shop-api/src/modules/shipment/shipment.service.ts
T
mahyargdz be82059172 first
2025-09-14 15:15:03 +03:30

353 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };