first
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { ArrayMinSize, IsArray, IsNotEmpty, IsNumber, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||
import { IsValidId } from "../../../common/decorator/validation.decorator";
|
||||
import { ShopModel } from "../../shop/models/shop.model";
|
||||
import { ShipmentModel } from "../models/shipment.model";
|
||||
|
||||
class ShipperSelectionDTO {
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@IsValidId(ShopModel)
|
||||
@ApiProperty({ type: "string", description: "shop ID", example: "60d725ebcfc0f308e6f5c92b" })
|
||||
shopId: string;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@IsValidId(ShipmentModel)
|
||||
@ApiProperty({ type: "number", description: "Shipment ID for the selected seller", example: 102 })
|
||||
shipmentId: number;
|
||||
}
|
||||
|
||||
export class SaveSellerShipmentInfoDTO {
|
||||
@ApiProperty({
|
||||
type: "Array",
|
||||
description: "Array of seller IDs and their selected shipment provider",
|
||||
example: [
|
||||
{ shopId: "60d725ebcfc0f308e6f5c92b", shipmentId: 102 },
|
||||
{ shopId: "60d726abcfc0f308e6f5c92d", shipmentId: 104 },
|
||||
],
|
||||
})
|
||||
@Expose()
|
||||
@IsArray()
|
||||
@ArrayMinSize(1, { message: "حداقل باید یک روش ارسال انتخاب شود" })
|
||||
@Type(() => ShipperSelectionDTO)
|
||||
@ValidateNested({ each: true })
|
||||
shipmentsInfo: ShipperSelectionDTO[];
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { ArrayMinSize, IsArray, IsNotEmpty, IsNumber, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||
|
||||
// DTO for Shipment Cost
|
||||
class WeightRangeDTO {
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", description: "Minimum weight in the range in kg", example: 0 })
|
||||
min: number;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", description: "Maximum weight in the range in kg", example: 10 })
|
||||
max: number;
|
||||
}
|
||||
class ShipmentCostDTO {
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@Type(() => WeightRangeDTO)
|
||||
weightRange: WeightRangeDTO;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", description: "Cost of shipping for the given weight range for first items", example: 40000 })
|
||||
cost_first: number;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", description: "Cost of shipping for the given weight range for other items ", example: 40000 })
|
||||
cost_rest: number;
|
||||
}
|
||||
|
||||
// Main DTO for Creating Shipment Providers
|
||||
export class CreateShipProvidersDTO {
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ type: "string", description: "The name of the provider", example: "چاپار" })
|
||||
name: string;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ type: "string", description: "Description of provider", example: "ارائه بهترین راه حل های جهانی لجستیک چاپار" })
|
||||
description: string;
|
||||
|
||||
@Expose()
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ShipmentCostDTO)
|
||||
@ApiProperty({
|
||||
type: "Array",
|
||||
description: "Costs for various weight ranges",
|
||||
example: [
|
||||
{ weightRange: { min: 0, max: 1 }, cost_first: 40000, cost_rest: 2000 },
|
||||
{ weightRange: { min: 1, max: 5 }, cost_first: 80000, cost_rest: 4000 },
|
||||
{ weightRange: { min: 5, max: 10 }, cost_first: 100000, cost_rest: 5000 },
|
||||
],
|
||||
})
|
||||
costs: ShipmentCostDTO[];
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", description: "Delivery time in days", example: 3 })
|
||||
deliveryTime: number;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Expose, Type, plainToInstance } from "class-transformer";
|
||||
|
||||
import { CartItemsDTO } from "../../cart/DTO/cart.dto";
|
||||
|
||||
class ShipperItemsDTO {
|
||||
@Expose()
|
||||
product: number;
|
||||
|
||||
@Expose()
|
||||
variant: string;
|
||||
|
||||
@Expose()
|
||||
quantity: number;
|
||||
|
||||
@Expose()
|
||||
selling_price: number;
|
||||
|
||||
@Expose()
|
||||
retail_price: number;
|
||||
|
||||
@Expose()
|
||||
discount_percent: number;
|
||||
|
||||
@Expose()
|
||||
shippingCost: number;
|
||||
|
||||
@Expose()
|
||||
isFreeShip: boolean;
|
||||
}
|
||||
|
||||
class ShipperInfoDTO {
|
||||
@Expose()
|
||||
shipperId: number;
|
||||
|
||||
@Expose()
|
||||
shipperName: string;
|
||||
|
||||
@Expose()
|
||||
shippingDaysRange: string[];
|
||||
|
||||
@Expose()
|
||||
totalShippingCost: number;
|
||||
|
||||
@Expose()
|
||||
@Type(() => ShipperItemsDTO)
|
||||
items: ShipperItemsDTO[];
|
||||
}
|
||||
|
||||
//***********************************
|
||||
//***********************************
|
||||
|
||||
export class ShippingOptionDTO {
|
||||
@Expose()
|
||||
shopId: string;
|
||||
|
||||
@Expose()
|
||||
shopName: string;
|
||||
|
||||
@Expose()
|
||||
@Type(() => CartItemsDTO)
|
||||
items: CartItemsDTO[];
|
||||
|
||||
@Expose()
|
||||
@Type(() => ShipperInfoDTO)
|
||||
shippers: ShipperInfoDTO[];
|
||||
|
||||
public static transformShipment(data: Record<string, any>): ShippingOptionDTO {
|
||||
const shipmentDTO = plainToInstance(ShippingOptionDTO, data, {
|
||||
excludeExtraneousValues: true,
|
||||
enableImplicitConversion: true,
|
||||
});
|
||||
|
||||
return shipmentDTO;
|
||||
}
|
||||
}
|
||||
|
||||
export class ShipmentMethodDTO {
|
||||
@Expose()
|
||||
_id: number;
|
||||
|
||||
@Expose()
|
||||
name: string;
|
||||
|
||||
@Expose()
|
||||
description: string;
|
||||
|
||||
@Expose()
|
||||
deliveryTime: number;
|
||||
|
||||
@Expose()
|
||||
deliveryType: string;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { PartialType } from "@nestjs/mapped-types";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsEnum, IsNotEmpty, IsOptional } from "class-validator";
|
||||
|
||||
import { CreateShipProvidersDTO } from "./createShipmentProviders.dto";
|
||||
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||
import { DeliveryType } from "../../../common/enums/shipment.enum";
|
||||
|
||||
export class UpdateShipmentProviderDTO extends PartialType(CreateShipProvidersDTO) {
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsEnum(DeliveryType)
|
||||
@ApiProperty({ type: "string" })
|
||||
deliveryType?: DeliveryType;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { DeliveryType } from "../../../../common/enums/shipment.enum";
|
||||
|
||||
export interface IShipmentProviders {
|
||||
_id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
costs: IShipmentCost[];
|
||||
deliveryType: DeliveryType;
|
||||
deliveryTime: number;
|
||||
deleted: boolean;
|
||||
}
|
||||
export interface IShipmentCost {
|
||||
weightRange: { min: number; max: number }; // weight range in kg
|
||||
cost_first: number;
|
||||
cost_rest: number;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import mongoose, { Schema, model } from "mongoose";
|
||||
|
||||
import { IShipmentCost, IShipmentProviders } from "./Abstraction/IShipmentProviders";
|
||||
import { DeliveryType } from "../../../common/enums/shipment.enum";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
|
||||
const AutoIncrement = require("mongoose-sequence")(mongoose);
|
||||
|
||||
const ShipmentCostSchema = new Schema<IShipmentCost>(
|
||||
{
|
||||
weightRange: {
|
||||
min: { type: Number, required: true },
|
||||
max: { type: Number, required: true },
|
||||
},
|
||||
cost_first: { type: Number, required: true },
|
||||
cost_rest: { type: Number, required: true },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const ShipmentSchema = new Schema<IShipmentProviders>(
|
||||
{
|
||||
_id: Number,
|
||||
name: { type: String, unique: true, required: true },
|
||||
description: String,
|
||||
// availableShippingDays: { type: [String], require: true },
|
||||
costs: [ShipmentCostSchema],
|
||||
deliveryType: { type: String, enum: DeliveryType, required: true },
|
||||
deliveryTime: { type: Number, required: true },
|
||||
deleted: { type: Boolean, default: false },
|
||||
},
|
||||
{ timestamps: true, toJSON: { versionKey: false }, _id: false, id: false },
|
||||
);
|
||||
|
||||
ShipmentSchema.plugin(AutoIncrement, { id: "shipment_id", inc_field: "_id", start_seq: 100 });
|
||||
|
||||
const ShipmentModel = model<IShipmentProviders>("Shipment", ShipmentSchema);
|
||||
|
||||
export { ShipmentModel, IShipmentProviders };
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Request } from "express";
|
||||
import { inject } from "inversify";
|
||||
import { controller, httpGet, httpPost, queryParam, request, requestBody } from "inversify-express-utils";
|
||||
|
||||
import { ShipmentService } from "./shipment.service";
|
||||
import { HttpStatus } from "../../common";
|
||||
import { SaveSellerShipmentInfoDTO } from "./DTO/calculateShipCost.dto";
|
||||
import { BaseController } from "../../common/base/controller";
|
||||
import { ApiAuth, ApiModel, ApiOperation, ApiQuery, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs";
|
||||
import { Guard } from "../../core/middlewares/guard.middleware";
|
||||
import { ValidationMiddleware } from "../../core/middlewares/validator.middleware";
|
||||
import { IOCTYPES } from "../../IOC/ioc.types";
|
||||
import { IUser } from "../user/models/Abstraction/IUser";
|
||||
|
||||
@controller("/shipment")
|
||||
@ApiTags("Shipment")
|
||||
class ShipmentController extends BaseController {
|
||||
@inject(IOCTYPES.ShipmentService) shipmentService: ShipmentService;
|
||||
//
|
||||
|
||||
@ApiOperation("get list of all shipment provider")
|
||||
@ApiResponse("successful", HttpStatus.Ok)
|
||||
@ApiQuery("limit", "the limit of return data")
|
||||
@ApiQuery("page", "the page want to get")
|
||||
@httpGet("")
|
||||
public async getShipmentProviders(@queryParam("limit") limit: string, @queryParam("page") page: string) {
|
||||
const queries = {
|
||||
limit: parseInt(limit),
|
||||
page: parseInt(page),
|
||||
};
|
||||
const { count, shipper } = await this.shipmentService.getAllProviders(queries);
|
||||
const { pager } = this.paginate(count);
|
||||
return this.response({ pager, shipper });
|
||||
}
|
||||
|
||||
@ApiOperation("Calculate shipment cost for checkout")
|
||||
@ApiResponse("Successful", HttpStatus.Ok)
|
||||
@ApiAuth()
|
||||
@httpGet("/shipping-cost", Guard.authUser())
|
||||
public async calculateShipCost(@request() req: Request) {
|
||||
const user = req.user as IUser;
|
||||
const data = await this.shipmentService.calculateAllShipmentOptions(user._id.toString());
|
||||
return this.response(data);
|
||||
}
|
||||
|
||||
@ApiOperation("save shipment info for checkout")
|
||||
@ApiResponse("Successful", HttpStatus.Ok)
|
||||
@ApiModel(SaveSellerShipmentInfoDTO)
|
||||
@ApiAuth()
|
||||
@httpPost("/save", Guard.authUser(), ValidationMiddleware.validateInput(SaveSellerShipmentInfoDTO))
|
||||
public async saveShipment(@requestBody() saveShippingInfo: SaveSellerShipmentInfoDTO, @request() req: Request) {
|
||||
const user = req.user as IUser;
|
||||
const data = await this.shipmentService.selectShipmentProvider(saveShippingInfo, user._id.toString());
|
||||
return this.response(data);
|
||||
}
|
||||
}
|
||||
|
||||
export { ShipmentController };
|
||||
@@ -0,0 +1,25 @@
|
||||
import { IShipmentProviders } from "./models/Abstraction/IShipmentProviders";
|
||||
import { ShipmentModel } from "./models/shipment.model";
|
||||
import { BaseRepository } from "../../common/base/repository";
|
||||
|
||||
class ShipmentRepository extends BaseRepository<IShipmentProviders> {
|
||||
constructor() {
|
||||
super(ShipmentModel);
|
||||
}
|
||||
async findAllShipper(queries: { limit: number; page: number }) {
|
||||
const page = queries.page || 1;
|
||||
const limit = queries.limit || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const count = await this.model.countDocuments({ deleted: false });
|
||||
const shipper = await this.model.find({ deleted: false }).skip(skip).limit(limit);
|
||||
|
||||
return { shipper, count };
|
||||
}
|
||||
}
|
||||
|
||||
function createShipmentRepository(): ShipmentRepository {
|
||||
return new ShipmentRepository();
|
||||
}
|
||||
|
||||
export { createShipmentRepository, ShipmentRepository };
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,4 @@
|
||||
import { IProduct } from "../../product/models/Abstraction/IProduct";
|
||||
import { IProductVariant } from "../../product/models/Abstraction/IProductVariant";
|
||||
|
||||
export type Items = { product: IProduct; variant: IProductVariant; quantity: number };
|
||||
Reference in New Issue
Block a user