Webiant Logo Webiant Logo
  1. No results found.

    Try your search with a different keyword or use * as a wildcard.

ShippingService.cs

using Nop.Core;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Shipping;
using Nop.Data;
using Nop.Services.Attributes;
using Nop.Services.Catalog;
using Nop.Services.Common;
using Nop.Services.Customers;
using Nop.Services.Directory;
using Nop.Services.Localization;
using Nop.Services.Logging;
using Nop.Services.Shipping.Pickup;

namespace Nop.Services.Shipping;

/// 
/// Shipping service
/// 
public partial class ShippingService : IShippingService
{
    #region Fields

    protected readonly IAddressService _addressService;
    protected readonly IAttributeParser _checkoutAttributeParser;
    protected readonly ICountryService _countryService;
    protected readonly ICustomerService _customerService;
    protected readonly IGenericAttributeService _genericAttributeService;
    protected readonly ILocalizationService _localizationService;
    protected readonly ILogger _logger;
    protected readonly IPickupPluginManager _pickupPluginManager;
    protected readonly IPriceCalculationService _priceCalculationService;
    protected readonly IProductAttributeParser _productAttributeParser;
    protected readonly IProductService _productService;
    protected readonly IRepository _shippingMethodRepository;
    protected readonly IRepository _shippingMethodCountryMappingRepository;
    protected readonly IRepository _warehouseRepository;
    protected readonly IShippingPluginManager _shippingPluginManager;
    protected readonly IStateProvinceService _stateProvinceService;
    protected readonly IStoreContext _storeContext;
    protected readonly ShippingSettings _shippingSettings;
    protected readonly ShoppingCartSettings _shoppingCartSettings;

    #endregion

    #region Ctor

    public ShippingService(IAddressService addressService,
        IAttributeParser checkoutAttributeParser,
        ICountryService countryService,
        ICustomerService customerService,
        IGenericAttributeService genericAttributeService,
        ILocalizationService localizationService,
        ILogger logger,
        IPickupPluginManager pickupPluginManager,
        IPriceCalculationService priceCalculationService,
        IProductAttributeParser productAttributeParser,
        IProductService productService,
        IRepository shippingMethodRepository,
        IRepository shippingMethodCountryMappingRepository,
        IRepository warehouseRepository,
        IShippingPluginManager shippingPluginManager,
        IStateProvinceService stateProvinceService,
        IStoreContext storeContext,
        ShippingSettings shippingSettings,
        ShoppingCartSettings shoppingCartSettings)
    {
        _addressService = addressService;
        _checkoutAttributeParser = checkoutAttributeParser;
        _countryService = countryService;
        _customerService = customerService;
        _genericAttributeService = genericAttributeService;
        _localizationService = localizationService;
        _logger = logger;
        _pickupPluginManager = pickupPluginManager;
        _priceCalculationService = priceCalculationService;
        _productAttributeParser = productAttributeParser;
        _productService = productService;
        _shippingMethodRepository = shippingMethodRepository;
        _shippingMethodCountryMappingRepository = shippingMethodCountryMappingRepository;
        _warehouseRepository = warehouseRepository;
        _shippingPluginManager = shippingPluginManager;
        _stateProvinceService = stateProvinceService;
        _storeContext = storeContext;
        _shippingSettings = shippingSettings;
        _shoppingCartSettings = shoppingCartSettings;
    }

    #endregion

    #region Utilities

    /// 
    /// Check whether there are multiple package items in the cart for the delivery
    /// 
    /// Package items
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains true if there are multiple items; otherwise false
    /// 
    protected virtual async Task AreMultipleItemsAsync(IList items)
    {
        //no items
        if (!items.Any())
            return false;

        //more than one
        if (items.Count > 1)
            return true;

        //or single item
        var singleItem = items.First();

        //but quantity more than one
        if (singleItem.GetQuantity() > 1)
            return true;

        //one item with quantity is one and without attributes
        if (string.IsNullOrEmpty(singleItem.ShoppingCartItem.AttributesXml))
            return false;

        //find associated products of item
        var associatedAttributeValues = (await _productAttributeParser.ParseProductAttributeValuesAsync(singleItem.ShoppingCartItem.AttributesXml))
            .Where(attributeValue => attributeValue.AttributeValueType == AttributeValueType.AssociatedToProduct);

        //whether to ship associated products
        return await associatedAttributeValues.AnyAwaitAsync(async attributeValue =>
            (await _productService.GetProductByIdAsync(attributeValue.AssociatedProductId))?.IsShipEnabled ?? false);
    }

    /// 
    /// Get dimensions of associated products (for quantity 1)
    /// 
    /// Shopping cart item
    /// Whether to ignore the weight of the products marked as "Free shipping"
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the width. Length. Height
    /// 
    protected virtual async Task<(decimal width, decimal length, decimal height)> GetAssociatedProductDimensionsAsync(ShoppingCartItem shoppingCartItem,
        bool ignoreFreeShippedItems = false)
    {
        ArgumentNullException.ThrowIfNull(shoppingCartItem);

        decimal length;
        decimal height;
        decimal width;

        width = length = height = decimal.Zero;

        //don't consider associated products dimensions
        if (!_shippingSettings.ConsiderAssociatedProductsDimensions)
            return (width, length, height);

        //attributes
        if (string.IsNullOrEmpty(shoppingCartItem.AttributesXml))
            return (width, length, height);

        //bundled products (associated attributes)
        var attributeValues = (await _productAttributeParser.ParseProductAttributeValuesAsync(shoppingCartItem.AttributesXml))
            .Where(x => x.AttributeValueType == AttributeValueType.AssociatedToProduct).ToList();
        foreach (var attributeValue in attributeValues)
        {
            var associatedProduct = await _productService.GetProductByIdAsync(attributeValue.AssociatedProductId);
            if (associatedProduct == null || !associatedProduct.IsShipEnabled || (associatedProduct.IsFreeShipping && ignoreFreeShippedItems))
                continue;

            width += associatedProduct.Width * attributeValue.Quantity;
            length += associatedProduct.Length * attributeValue.Quantity;
            height += associatedProduct.Height * attributeValue.Quantity;
        }

        return (width, length, height);
    }

    #endregion

    #region Methods

    #region Shipping methods

    /// 
    /// Deletes a shipping method
    /// 
    /// The shipping method
    /// A task that represents the asynchronous operation
    public virtual async Task DeleteShippingMethodAsync(ShippingMethod shippingMethod)
    {
        await _shippingMethodRepository.DeleteAsync(shippingMethod);
    }

    /// 
    /// Gets a shipping method
    /// 
    /// The shipping method identifier
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the shipping method
    /// 
    public virtual async Task GetShippingMethodByIdAsync(int shippingMethodId)
    {
        return await _shippingMethodRepository.GetByIdAsync(shippingMethodId, cache => default);
    }

    /// 
    /// Gets all shipping methods
    /// 
    /// The country identifier to filter by
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the shipping methods
    /// 
    public virtual async Task> GetAllShippingMethodsAsync(int? filterByCountryId = null)
    {
        if (filterByCountryId.HasValue && filterByCountryId.Value > 0)
        {
            return await _shippingMethodRepository.GetAllAsync(query =>
            {
                var query1 = from sm in query
                    join smcm in _shippingMethodCountryMappingRepository.Table on sm.Id equals smcm.ShippingMethodId
                    where smcm.CountryId == filterByCountryId.Value
                    select sm.Id;

                query1 = query1.Distinct();

                var query2 = from sm in query
                    where !query1.Contains(sm.Id)
                    orderby sm.DisplayOrder, sm.Id
                    select sm;

                return query2;
            }, cache => cache.PrepareKeyForDefaultCache(NopShippingDefaults.ShippingMethodsAllCacheKey, filterByCountryId));
        }

        return await _shippingMethodRepository.GetAllAsync(query =>
        {
            return from sm in query
                orderby sm.DisplayOrder, sm.Id
                select sm;
        }, cache => default);
    }

    /// 
    /// Inserts a shipping method
    /// 
    /// Shipping method
    /// A task that represents the asynchronous operation
    public virtual async Task InsertShippingMethodAsync(ShippingMethod shippingMethod)
    {
        await _shippingMethodRepository.InsertAsync(shippingMethod);
    }

    /// 
    /// Updates the shipping method
    /// 
    /// Shipping method
    /// A task that represents the asynchronous operation
    public virtual async Task UpdateShippingMethodAsync(ShippingMethod shippingMethod)
    {
        await _shippingMethodRepository.UpdateAsync(shippingMethod);
    }

    /// 
    /// Does country restriction exist
    /// 
    /// Shipping method
    /// Country identifier
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// 
    public virtual async Task CountryRestrictionExistsAsync(ShippingMethod shippingMethod, int countryId)
    {
        ArgumentNullException.ThrowIfNull(shippingMethod);

        var result = await _shippingMethodCountryMappingRepository.Table
            .AnyAsync(smcm => smcm.ShippingMethodId == shippingMethod.Id && smcm.CountryId == countryId);

        return result;
    }

    /// 
    /// Gets shipping country mappings
    /// 
    /// The shipping method identifier
    /// Country identifier
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the shipping country mappings
    /// 
    public virtual async Task> GetShippingMethodCountryMappingAsync(int shippingMethodId,
        int countryId)
    {
        var query = _shippingMethodCountryMappingRepository.Table.Where(smcm =>
            smcm.ShippingMethodId == shippingMethodId && smcm.CountryId == countryId);

        return await query.ToListAsync();
    }

    /// 
    /// Inserts a shipping country mapping
    /// 
    /// Shipping country mapping
    /// A task that represents the asynchronous operation
    public virtual async Task InsertShippingMethodCountryMappingAsync(ShippingMethodCountryMapping shippingMethodCountryMapping)
    {
        await _shippingMethodCountryMappingRepository.InsertAsync(shippingMethodCountryMapping);
    }

    /// 
    /// Delete the shipping country mapping
    /// 
    /// Shipping country mapping
    /// A task that represents the asynchronous operation
    public virtual async Task DeleteShippingMethodCountryMappingAsync(ShippingMethodCountryMapping shippingMethodCountryMapping)
    {
        await _shippingMethodCountryMappingRepository.DeleteAsync(shippingMethodCountryMapping);
    }

    #endregion

    #region Warehouses

    /// 
    /// Deletes a warehouse
    /// 
    /// The warehouse
    /// A task that represents the asynchronous operation
    public virtual async Task DeleteWarehouseAsync(Warehouse warehouse)
    {
        await _warehouseRepository.DeleteAsync(warehouse);
    }

    /// 
    /// Gets a warehouse
    /// 
    /// The warehouse identifier
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the warehouse
    /// 
    public virtual async Task GetWarehouseByIdAsync(int warehouseId)
    {
        return await _warehouseRepository.GetByIdAsync(warehouseId, cache => default);
    }

    /// 
    /// Gets all warehouses
    /// 
    /// Warehouse name
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the warehouses
    /// 
    public virtual async Task> GetAllWarehousesAsync(string name = null)
    {
        var warehouses = await _warehouseRepository.GetAllAsync(query =>
        {
            return from wh in query
                orderby wh.Name
                select wh;
        }, cache => default);

        if (!string.IsNullOrEmpty(name))
            warehouses = warehouses.Where(wh => wh.Name.Contains(name)).ToList();

        return warehouses;
    }

    /// 
    /// Inserts a warehouse
    /// 
    /// Warehouse
    /// A task that represents the asynchronous operation
    public virtual async Task InsertWarehouseAsync(Warehouse warehouse)
    {
        await _warehouseRepository.InsertAsync(warehouse);
    }

    /// 
    /// Updates the warehouse
    /// 
    /// Warehouse
    /// A task that represents the asynchronous operation
    public virtual async Task UpdateWarehouseAsync(Warehouse warehouse)
    {
        await _warehouseRepository.UpdateAsync(warehouse);
    }

    /// 
    /// Get the nearest warehouse for the specified address
    /// 
    /// Address
    /// List of warehouses, if null all warehouses are used.
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the 
    /// 
    public virtual async Task GetNearestWarehouseAsync(Address address, IList warehouses = null)
    {
        warehouses ??= await GetAllWarehousesAsync();

        //no address specified. return any
        if (address == null)
            return warehouses.FirstOrDefault();

        //of course, we should use some better logic to find nearest warehouse
        //but we don't have a built-in geographic database which supports "distance" functionality
        //that's why we simply look for exact matches

        //find by country
        var matchedByCountry = new List();
        foreach (var warehouse in warehouses)
        {
            var warehouseAddress = await _addressService.GetAddressByIdAsync(warehouse.AddressId);
            if (warehouseAddress == null)
                continue;

            if (warehouseAddress.CountryId == address.CountryId)
                matchedByCountry.Add(warehouse);
        }
        //no country matches. return any
        if (!matchedByCountry.Any())
            return warehouses.FirstOrDefault();

        //find by state
        var matchedByState = new List();
        foreach (var warehouse in matchedByCountry)
        {
            var warehouseAddress = await _addressService.GetAddressByIdAsync(warehouse.AddressId);
            if (warehouseAddress == null)
                continue;

            if (warehouseAddress.StateProvinceId == address.StateProvinceId)
                matchedByState.Add(warehouse);
        }

        if (matchedByState.Any())
            return matchedByState.FirstOrDefault();

        //no state matches. return any
        return matchedByCountry.FirstOrDefault();
    }

    #endregion

    #region Workflow

    /// 
    /// Gets shopping cart item weight (of one item)
    /// 
    /// Shopping cart item
    /// Whether to ignore the weight of the products marked as "Free shipping"
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the shopping cart item weight
    /// 
    public virtual async Task GetShoppingCartItemWeightAsync(ShoppingCartItem shoppingCartItem, bool ignoreFreeShippedItems = false)
    {
        ArgumentNullException.ThrowIfNull(shoppingCartItem);

        var product = await _productService.GetProductByIdAsync(shoppingCartItem.ProductId);

        return await GetShoppingCartItemWeightAsync(product, shoppingCartItem.AttributesXml, ignoreFreeShippedItems);
    }

    /// 
    /// Gets product item weight (of one item)
    /// 
    /// Product
    /// Selected product attributes in XML
    /// Whether to ignore the weight of the products marked as "Free shipping"
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the item weight
    /// 
    public virtual async Task GetShoppingCartItemWeightAsync(Product product, string attributesXml, bool ignoreFreeShippedItems = false)
    {
        if (product == null)
            return decimal.Zero;

        //product weight
        var productWeight = !product.IsFreeShipping || !ignoreFreeShippedItems ? product.Weight : decimal.Zero;

        //attribute weight
        var attributesTotalWeight = decimal.Zero;

        if (!_shippingSettings.ConsiderAssociatedProductsDimensions || string.IsNullOrEmpty(attributesXml))
            return productWeight + attributesTotalWeight;

        var attributeValues = await _productAttributeParser.ParseProductAttributeValuesAsync(attributesXml);
        foreach (var attributeValue in attributeValues)
        {
            switch (attributeValue.AttributeValueType)
            {
                case AttributeValueType.Simple:
                    //simple attribute
                    attributesTotalWeight += attributeValue.WeightAdjustment;
                    break;
                case AttributeValueType.AssociatedToProduct:
                    //bundled product
                    var associatedProduct = await _productService.GetProductByIdAsync(attributeValue.AssociatedProductId);
                    if (associatedProduct != null && associatedProduct.IsShipEnabled && (!associatedProduct.IsFreeShipping || !ignoreFreeShippedItems))
                        attributesTotalWeight += associatedProduct.Weight * attributeValue.Quantity;
                    break;
            }
        }

        return productWeight + attributesTotalWeight;
    }

    /// 
    /// Gets shopping cart weight
    /// 
    /// Request
    /// A value indicating whether we should calculate weights of selected checkotu attributes
    /// Whether to ignore the weight of the products marked as "Free shipping"
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the otal weight
    /// 
    public virtual async Task GetTotalWeightAsync(GetShippingOptionRequest request,
        bool includeCheckoutAttributes = true, bool ignoreFreeShippedItems = false)
    {
        ArgumentNullException.ThrowIfNull(request);

        var totalWeight = decimal.Zero;

        //shopping cart items
        foreach (var packageItem in request.Items)
            totalWeight += await GetShoppingCartItemWeightAsync(packageItem.ShoppingCartItem, ignoreFreeShippedItems) * packageItem.GetQuantity();

        //checkout attributes
        if (request.Customer is null || !includeCheckoutAttributes)
            return totalWeight;
        var store = await _storeContext.GetCurrentStoreAsync();
        var checkoutAttributesXml = await _genericAttributeService.GetAttributeAsync(request.Customer, NopCustomerDefaults.CheckoutAttributes, store.Id);
        if (string.IsNullOrEmpty(checkoutAttributesXml))
            return totalWeight;
        var attributeValues = _checkoutAttributeParser.ParseAttributeValues(checkoutAttributesXml);
        foreach (var attributeValue in await attributeValues.SelectMany(x => x.values).ToListAsync())
            totalWeight += attributeValue.WeightAdjustment;

        return totalWeight;
    }

    /// 
    /// Get total dimensions
    /// 
    /// Package items
    /// Whether to ignore the weight of the products marked as "Free shipping"
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the width. Length. Height
    /// 
    public virtual async Task<(decimal width, decimal length, decimal height)> GetDimensionsAsync(IList packageItems, bool ignoreFreeShippedItems = false)
    {
        ArgumentNullException.ThrowIfNull(packageItems);

        decimal length;
        decimal height;
        decimal width;

        //calculate cube root of volume, in case if the number of items more than 1
        if (_shippingSettings.UseCubeRootMethod && await AreMultipleItemsAsync(packageItems))
        {
            //find max dimensions of the shipped items
            var maxWidth = packageItems.Max(item => !item.Product.IsFreeShipping || !ignoreFreeShippedItems
                ? item.Product.Width : decimal.Zero);
            var maxLength = packageItems.Max(item => !item.Product.IsFreeShipping || !ignoreFreeShippedItems
                ? item.Product.Length : decimal.Zero);
            var maxHeight = packageItems.Max(item => !item.Product.IsFreeShipping || !ignoreFreeShippedItems
                ? item.Product.Height : decimal.Zero);

            //get total volume of the shipped items
            var totalVolume = await packageItems.SumAwaitAsync(async packageItem =>
            {
                //product volume
                var productVolume = !packageItem.Product.IsFreeShipping || !ignoreFreeShippedItems ?
                    packageItem.Product.Width * packageItem.Product.Length * packageItem.Product.Height : decimal.Zero;

                //associated products volume
                if (_shippingSettings.ConsiderAssociatedProductsDimensions && !string.IsNullOrEmpty(packageItem.ShoppingCartItem.AttributesXml))
                {
                    productVolume += await (await _productAttributeParser.ParseProductAttributeValuesAsync(packageItem.ShoppingCartItem.AttributesXml))
                        .Where(attributeValue => attributeValue.AttributeValueType == AttributeValueType.AssociatedToProduct).SumAwaitAsync(async attributeValue =>
                        {
                            var associatedProduct = await _productService.GetProductByIdAsync(attributeValue.AssociatedProductId);
                            if (associatedProduct == null || !associatedProduct.IsShipEnabled || (associatedProduct.IsFreeShipping && ignoreFreeShippedItems))
                                return 0;

                            //adjust max dimensions
                            maxWidth = Math.Max(maxWidth, associatedProduct.Width);
                            maxLength = Math.Max(maxLength, associatedProduct.Length);
                            maxHeight = Math.Max(maxHeight, associatedProduct.Height);

                            return attributeValue.Quantity * associatedProduct.Width * associatedProduct.Length * associatedProduct.Height;
                        });
                }

                //total volume of item
                return productVolume * packageItem.GetQuantity();
            });

            //set dimensions as cube root of volume
            width = length = height = Convert.ToDecimal(Math.Pow(Convert.ToDouble(totalVolume), 1.0 / 3.0));

            //sometimes we have products with sizes like 1x1x20
            //that's why let's ensure that a maximum dimension is always preserved
            //otherwise, shipping rate computation methods can return low rates
            width = Math.Max(width, maxWidth);
            length = Math.Max(length, maxLength);
            height = Math.Max(height, maxHeight);
        }
        else
        {
            //summarize all values (very inaccurate with multiple items)
            width = length = height = decimal.Zero;
            foreach (var packageItem in packageItems)
            {
                var productWidth = decimal.Zero;
                var productLength = decimal.Zero;
                var productHeight = decimal.Zero;
                if (!packageItem.Product.IsFreeShipping || !ignoreFreeShippedItems)
                {
                    productWidth = packageItem.Product.Width;
                    productLength = packageItem.Product.Length;
                    productHeight = packageItem.Product.Height;
                }

                //associated products
                var (associatedProductsWidth, associatedProductsLength, associatedProductsHeight) = await GetAssociatedProductDimensionsAsync(packageItem.ShoppingCartItem);

                var quantity = packageItem.GetQuantity();
                width += (productWidth + associatedProductsWidth) * quantity;
                length += (productLength + associatedProductsLength) * quantity;
                height += (productHeight + associatedProductsHeight) * quantity;
            }
        }

        return (width, length, height);
    }

    /// 
    /// Create shipment packages (requests) from shopping cart
    /// 
    /// Shopping cart
    /// Shipping address
    /// Load records allowed only in a specified store; pass 0 to load all records
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the shipment packages (requests). Value indicating whether shipping is done from multiple locations (warehouses)
    /// 
    public virtual async Task<(IList shipmentPackages, bool shippingFromMultipleLocations)> CreateShippingOptionRequestsAsync(IList cart,
        Address shippingAddress, int storeId)
    {
        //if we always ship from the default shipping origin, then there's only one request
        //if we ship from warehouses ("ShippingSettings.UseWarehouseLocation" enabled),
        //then there could be several requests

        //key - warehouse identifier (0 - default shipping origin)
        //value - request
        var requests = new Dictionary();

        //a list of requests with products which should be shipped separately
        var separateRequests = new List();

        foreach (var sci in cart)
        {
            if (!await IsShipEnabledAsync(sci))
                continue;

            var product = await _productService.GetProductByIdAsync(sci.ProductId);

            if (product == null || !product.IsShipEnabled)
            {
                var associatedProducts = await (await _productAttributeParser.ParseProductAttributeValuesAsync(sci.AttributesXml))
                    .Where(attributeValue => attributeValue.AttributeValueType == AttributeValueType.AssociatedToProduct)
                    .SelectAwait(async attributeValue => await _productService.GetProductByIdAsync(attributeValue.AssociatedProductId)).ToListAsync();
                product = associatedProducts.FirstOrDefault(associatedProduct => associatedProduct != null && associatedProduct.IsShipEnabled);
            }

            if (product == null)
                continue;

            //warehouses
            Warehouse warehouse = null;
            if (_shippingSettings.UseWarehouseLocation)
            {
                if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock &&
                    product.UseMultipleWarehouses)
                {
                    var allWarehouses = new List();
                    //multiple warehouses supported
                    foreach (var pwi in await _productService.GetAllProductWarehouseInventoryRecordsAsync(product.Id))
                    {
                        var tmpWarehouse = await GetWarehouseByIdAsync(pwi.WarehouseId);
                        if (tmpWarehouse != null)
                            allWarehouses.Add(tmpWarehouse);
                    }

                    warehouse = await GetNearestWarehouseAsync(shippingAddress, allWarehouses);
                }
                else
                {
                    //multiple warehouses are not supported
                    warehouse = await GetWarehouseByIdAsync(product.WarehouseId);
                }
            }

            var warehouseId = warehouse?.Id ?? 0;

            //add item to existing request
            if (requests.TryGetValue(warehouseId, out var value) && !product.ShipSeparately)
            {
                value.Items.Add(new GetShippingOptionRequest.PackageItem(sci, product));
            }
            else
            {
                //create a new request
                var request = new GetShippingOptionRequest
                {
                    //store
                    StoreId = storeId,
                    //customer
                    Customer = await _customerService.GetShoppingCartCustomerAsync(cart),

                    //ship to
                    ShippingAddress = shippingAddress
                };
                //ship from
                Address originAddress = null;
                if (warehouse != null)
                {
                    //warehouse address
                    originAddress = await _addressService.GetAddressByIdAsync(warehouse.AddressId);
                    request.WarehouseFrom = warehouse;
                }

                //no warehouse address. in this case use the default shipping origin
                originAddress ??= await _addressService.GetAddressByIdAsync(_shippingSettings.ShippingOriginAddressId);

                if (originAddress != null)
                {
                    request.CountryFrom = await _countryService.GetCountryByAddressAsync(originAddress);
                    request.StateProvinceFrom = await _stateProvinceService.GetStateProvinceByAddressAsync(originAddress);
                    request.ZipPostalCodeFrom = originAddress.ZipPostalCode;
                    request.CountyFrom = originAddress.County;
                    request.CityFrom = originAddress.City;
                    request.AddressFrom = originAddress.Address1;
                }

                //whether this product should be shipped separately from other ones
                if (product.ShipSeparately)
                {
                    //whether product items should be shipped separately
                    if (_shippingSettings.ShipSeparatelyOneItemEach)
                    {
                        //add item with overridden quantity 1
                        request.Items.Add(new GetShippingOptionRequest.PackageItem(sci, product, 1));

                        //create separate requests for all product quantity
                        for (var i = 0; i < sci.Quantity; i++)
                        {
                            separateRequests.Add(request);
                        }
                    }
                    else
                    {
                        //all of product items should be shipped in a single box, so create the single separate request 
                        request.Items.Add(new GetShippingOptionRequest.PackageItem(sci, product));
                        separateRequests.Add(request);
                    }
                }
                else
                {
                    //usual request
                    request.Items.Add(new GetShippingOptionRequest.PackageItem(sci, product));
                    requests.Add(warehouseId, request);
                }
            }
        }

        //multiple locations?
        //currently we just compare warehouses
        //but we should also consider cases when several warehouses are located in the same address
        var shippingFromMultipleLocations = requests.Select(x => x.Key).Distinct().Count() > 1;

        var result = requests.Values.ToList();
        result.AddRange(separateRequests);

        return (result, shippingFromMultipleLocations);
    }

    /// 
    ///  Gets available shipping options
    /// 
    /// Shopping cart
    /// Shipping address
    /// Load records allowed only to a specified customer; pass null to ignore ACL permissions
    /// Filter by shipping rate computation method identifier; null to load shipping options of all shipping rate computation methods
    /// Load records allowed only in a specified store; pass 0 to load all records
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the shipping options
    /// 
    public virtual async Task GetShippingOptionsAsync(IList cart,
        Address shippingAddress, Customer customer = null, string allowedShippingRateComputationMethodSystemName = "",
        int storeId = 0)
    {
        ArgumentNullException.ThrowIfNull(cart);

        var result = new GetShippingOptionResponse();

        //create a package
        var (shippingOptionRequests, shippingFromMultipleLocations) = await CreateShippingOptionRequestsAsync(cart, shippingAddress, storeId);
        result.ShippingFromMultipleLocations = shippingFromMultipleLocations;

        var shippingRateComputationMethods = await _shippingPluginManager
            .LoadActivePluginsAsync(customer, storeId, allowedShippingRateComputationMethodSystemName);
        if (!shippingRateComputationMethods.Any())
            return result;

        //request shipping options from each shipping rate computation methods
        foreach (var srcm in shippingRateComputationMethods)
        {
            //request shipping options (separately for each package-request)
            IList srcmShippingOptions = null;
            foreach (var shippingOptionRequest in shippingOptionRequests)
            {
                var getShippingOptionResponse = await srcm.GetShippingOptionsAsync(shippingOptionRequest);

                if (getShippingOptionResponse.Success)
                {
                    //success
                    if (srcmShippingOptions == null)
                    {
                        //first shipping option request
                        srcmShippingOptions = getShippingOptionResponse.ShippingOptions;
                    }
                    else
                    {
                        //get shipping options which already exist for prior requested packages for this scrm (i.e. common options)
                        srcmShippingOptions = srcmShippingOptions
                            .Where(existingso => getShippingOptionResponse.ShippingOptions.Any(newso => newso.Name == existingso.Name))
                            .ToList();

                        //and sum the rates
                        foreach (var existingso in srcmShippingOptions)
                        {
                            existingso.Rate += getShippingOptionResponse
                                .ShippingOptions
                                .First(newso => newso.Name == existingso.Name)
                                .Rate;
                        }
                    }
                }
                else
                {
                    //errors
                    foreach (var error in getShippingOptionResponse.Errors)
                    {
                        result.AddError(error);
                        await _logger.WarningAsync($"Shipping ({srcm.PluginDescriptor.FriendlyName}). {error}");
                    }
                    //clear the shipping options in this case
                    srcmShippingOptions = new List();
                    break;
                }
            }

            //add this scrm's options to the result
            if (srcmShippingOptions == null)
                continue;

            foreach (var so in srcmShippingOptions)
            {
                //set system name if not set yet
                if (string.IsNullOrEmpty(so.ShippingRateComputationMethodSystemName))
                    so.ShippingRateComputationMethodSystemName = srcm.PluginDescriptor.SystemName;
                if (_shoppingCartSettings.RoundPricesDuringCalculation)
                    so.Rate = await _priceCalculationService.RoundPriceAsync(so.Rate);
                result.ShippingOptions.Add(so);
            }
        }

        if (_shippingSettings.ReturnValidOptionsIfThereAreAny)
        {
            //return valid options if there are any (no matter of the errors returned by other shipping rate computation methods).
            if (result.ShippingOptions.Any() && result.Errors.Any())
                result.Errors.Clear();
        }

        //no shipping options loaded
        if (!result.ShippingOptions.Any() && !result.Errors.Any())
            result.Errors.Add(await _localizationService.GetResourceAsync("Checkout.ShippingOptionCouldNotBeLoaded"));

        return result;
    }

    /// 
    /// Gets available pickup points
    /// 
    /// Shopping Cart
    /// Address
    /// Load records allowed only to a specified customer; pass null to ignore ACL permissions
    /// Filter by provider identifier; null to load pickup points of all providers
    /// Load records allowed only in a specified store; pass 0 to load all records
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the pickup points
    /// 
    public virtual async Task GetPickupPointsAsync(IList cart, Address address,
        Customer customer = null, string providerSystemName = null, int storeId = 0)
    {
        var result = new GetPickupPointsResponse();

        var pickupPointsProviders = await _pickupPluginManager.LoadActivePluginsAsync(customer, storeId, providerSystemName);
        if (!pickupPointsProviders.Any())
            return result;

        var allPickupPoints = new List();
        foreach (var provider in pickupPointsProviders)
        {
            var pickPointsResponse = await provider.GetPickupPointsAsync(cart, address);
            if (pickPointsResponse.Success)
                allPickupPoints.AddRange(pickPointsResponse.PickupPoints);
            else
            {
                foreach (var error in pickPointsResponse.Errors)
                {
                    result.AddError(error);
                    await _logger.WarningAsync($"PickupPoints ({provider.PluginDescriptor.FriendlyName}). {error}");
                }
            }
        }

        //any pickup points is enough
        if (allPickupPoints.Count <= 0)
            return result;

        result.Errors.Clear();
        result.PickupPoints = allPickupPoints.OrderBy(point => point.DisplayOrder).ThenBy(point => point.Name).ToList();

        return result;
    }

    /// 
    /// Whether the shopping cart item is ship enabled
    /// 
    /// Shopping cart item
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains true if the shopping cart item requires shipping; otherwise false
    /// 
    public virtual async Task IsShipEnabledAsync(ShoppingCartItem shoppingCartItem)
    {
        //whether the product requires shipping
        if (shoppingCartItem.ProductId != 0 && (await _productService.GetProductByIdAsync(shoppingCartItem.ProductId))?.IsShipEnabled == true)
            return true;

        if (string.IsNullOrEmpty(shoppingCartItem.AttributesXml))
            return false;

        //or whether associated products of the shopping cart item require shipping
        return await (await _productAttributeParser.ParseProductAttributeValuesAsync(shoppingCartItem.AttributesXml))
            .Where(attributeValue => attributeValue.AttributeValueType == AttributeValueType.AssociatedToProduct)
            .AnyAwaitAsync(async attributeValue => (await _productService.GetProductByIdAsync(attributeValue.AssociatedProductId))?.IsShipEnabled ?? false);
    }

    /// 
    /// Whether the shopping cart item is free shipping
    /// 
    /// Shopping cart item
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains true if the shopping cart item is free shipping; otherwise false
    /// 
    public virtual async Task IsFreeShippingAsync(ShoppingCartItem shoppingCartItem)
    {
        //first, check whether shipping is required
        if (!await IsShipEnabledAsync(shoppingCartItem))
            return true;

        //then whether the product is free shipping
        if (shoppingCartItem.ProductId != 0 && !(await _productService.GetProductByIdAsync(shoppingCartItem.ProductId)).IsFreeShipping)
            return false;

        if (string.IsNullOrEmpty(shoppingCartItem.AttributesXml))
            return true;

        //and whether associated products of the shopping cart item is free shipping
        return await (await _productAttributeParser.ParseProductAttributeValuesAsync(shoppingCartItem.AttributesXml))
            .Where(attributeValue => attributeValue.AttributeValueType == AttributeValueType.AssociatedToProduct)
            .AllAwaitAsync(async attributeValue => (await _productService.GetProductByIdAsync(attributeValue.AssociatedProductId))?.IsFreeShipping ?? true);
    }

    /// 
    /// Get the additional shipping charge
    ///  
    /// Shopping cart item
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the additional shipping charge of the shopping cart item
    /// 
    public virtual async Task GetAdditionalShippingChargeAsync(ShoppingCartItem shoppingCartItem)
    {
        //first, check whether shipping is free
        if (await IsFreeShippingAsync(shoppingCartItem))
            return decimal.Zero;

        //get additional shipping charge of the product
        var additionalShippingCharge = ((await _productService.GetProductByIdAsync(shoppingCartItem.ProductId))?.AdditionalShippingCharge ?? decimal.Zero) * shoppingCartItem.Quantity;

        if (string.IsNullOrEmpty(shoppingCartItem.AttributesXml))
            return additionalShippingCharge;

        //and sum with associated products additional shipping charges
        additionalShippingCharge += await (await _productAttributeParser.ParseProductAttributeValuesAsync(shoppingCartItem.AttributesXml))
            .Where(attributeValue => attributeValue.AttributeValueType == AttributeValueType.AssociatedToProduct)
            .SumAwaitAsync(async attributeValue => (await _productService.GetProductByIdAsync(attributeValue.AssociatedProductId))?.AdditionalShippingCharge ?? decimal.Zero);

        return additionalShippingCharge;
    }

    #endregion

    #endregion
}