Try your search with a different keyword or use * as a wildcard.
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.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;
/// <summary>
/// Shipping service
/// </summary>
public partial class ShippingService : IShippingService
{
#region Fields
protected readonly IAddressService _addressService;
protected readonly IAttributeParser<CheckoutAttribute, CheckoutAttributeValue> _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 IShippingPluginManager _shippingPluginManager;
protected readonly IStateProvinceService _stateProvinceService;
protected readonly IStoreContext _storeContext;
protected readonly IWarehouseService _warehouseService;
protected readonly ShippingSettings _shippingSettings;
protected readonly ShoppingCartSettings _shoppingCartSettings;
#endregion
#region Ctor
public ShippingService(IAddressService addressService,
IAttributeParser<CheckoutAttribute, CheckoutAttributeValue> checkoutAttributeParser,
ICountryService countryService,
ICustomerService customerService,
IGenericAttributeService genericAttributeService,
ILocalizationService localizationService,
ILogger logger,
IPickupPluginManager pickupPluginManager,
IPriceCalculationService priceCalculationService,
IProductAttributeParser productAttributeParser,
IProductService productService,
IShippingPluginManager shippingPluginManager,
IStateProvinceService stateProvinceService,
IStoreContext storeContext,
IWarehouseService warehouseService,
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;
_shippingPluginManager = shippingPluginManager;
_stateProvinceService = stateProvinceService;
_storeContext = storeContext;
_warehouseService = warehouseService;
_shippingSettings = shippingSettings;
_shoppingCartSettings = shoppingCartSettings;
}
#endregion
#region Utilities
/// <summary>
/// Check whether there are multiple package items in the cart for the delivery
/// </summary>
/// <param name="items">Package items</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains true if there are multiple items; otherwise false
/// </returns>
protected virtual async Task<bool> AreMultipleItemsAsync(IList<GetShippingOptionRequest.PackageItem> 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);
}
/// <summary>
/// Get dimensions of associated products (for quantity 1)
/// </summary>
/// <param name="shoppingCartItem">Shopping cart item</param>
/// <param name="ignoreFreeShippedItems">Whether to ignore the weight of the products marked as "Free shipping"</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the width. Length. Height
/// </returns>
protected virtual async Task<(decimal width, decimal length, decimal height)> GetAssociatedProductDimensionsAsync(ShoppingCartItem shoppingCartItem,
bool ignoreFreeShippedItems = false)
{
ArgumentNullException.ThrowIfNull(shoppingCartItem);
decimal length;
decimal height;
var 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
/// <summary>
/// Gets shopping cart item weight (of one item)
/// </summary>
/// <param name="shoppingCartItem">Shopping cart item</param>
/// <param name="ignoreFreeShippedItems">Whether to ignore the weight of the products marked as "Free shipping"</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the shopping cart item weight
/// </returns>
public virtual async Task<decimal> GetShoppingCartItemWeightAsync(ShoppingCartItem shoppingCartItem, bool ignoreFreeShippedItems = false)
{
ArgumentNullException.ThrowIfNull(shoppingCartItem);
var product = await _productService.GetProductByIdAsync(shoppingCartItem.ProductId);
return await GetShoppingCartItemWeightAsync(product, shoppingCartItem.AttributesXml, ignoreFreeShippedItems);
}
/// <summary>
/// Gets product item weight (of one item)
/// </summary>
/// <param name="product">Product</param>
/// <param name="attributesXml">Selected product attributes in XML</param>
/// <param name="ignoreFreeShippedItems">Whether to ignore the weight of the products marked as "Free shipping"</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the item weight
/// </returns>
public virtual async Task<decimal> 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;
}
/// <summary>
/// Gets shopping cart weight
/// </summary>
/// <param name="request">Request</param>
/// <param name="includeCheckoutAttributes">A value indicating whether we should calculate weights of selected checkotu attributes</param>
/// <param name="ignoreFreeShippedItems">Whether to ignore the weight of the products marked as "Free shipping"</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the otal weight
/// </returns>
public virtual async Task<decimal> 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<string>(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;
}
/// <summary>
/// Get total dimensions
/// </summary>
/// <param name="packageItems">Package items</param>
/// <param name="ignoreFreeShippedItems">Whether to ignore the weight of the products marked as "Free shipping"</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the width. Length. Height
/// </returns>
public virtual async Task<(decimal width, decimal length, decimal height)> GetDimensionsAsync(IList<GetShippingOptionRequest.PackageItem> 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);
}
/// <summary>
/// Create shipment packages (requests) from shopping cart
/// </summary>
/// <param name="cart">Shopping cart</param>
/// <param name="shippingAddress">Shipping address</param>
/// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param>
/// <returns>
/// 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)
/// </returns>
public virtual async Task<(IList<GetShippingOptionRequest> shipmentPackages, bool shippingFromMultipleLocations)> CreateShippingOptionRequestsAsync(IList<ShoppingCartItem> 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<int, GetShippingOptionRequest>();
//a list of requests with products which should be shipped separately
var separateRequests = new List<GetShippingOptionRequest>();
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<Warehouse>();
//multiple warehouses supported
foreach (var pwi in await _productService.GetAllProductWarehouseInventoryRecordsAsync(product.Id))
{
var tmpWarehouse = await _warehouseService.GetWarehouseByIdAsync(pwi.WarehouseId);
if (tmpWarehouse != null)
allWarehouses.Add(tmpWarehouse);
}
warehouse = await _warehouseService.GetNearestWarehouseAsync(shippingAddress, allWarehouses);
}
else
{
//multiple warehouses are not supported
warehouse = await _warehouseService.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);
}
/// <summary>
/// Gets available shipping options
/// </summary>
/// <param name="cart">Shopping cart</param>
/// <param name="shippingAddress">Shipping address</param>
/// <param name="customer">Load records allowed only to a specified customer; pass null to ignore ACL permissions</param>
/// <param name="allowedShippingRateComputationMethodSystemName">Filter by shipping rate computation method identifier; null to load shipping options of all shipping rate computation methods</param>
/// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the shipping options
/// </returns>
public virtual async Task<GetShippingOptionResponse> GetShippingOptionsAsync(IList<ShoppingCartItem> 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<ShippingOption> 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<ShippingOption>();
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;
}
/// <summary>
/// Gets available pickup points
/// </summary>
/// <param name="cart">Shopping Cart</param>
/// <param name="address">Address</param>
/// <param name="customer">Load records allowed only to a specified customer; pass null to ignore ACL permissions</param>
/// <param name="providerSystemName">Filter by provider identifier; null to load pickup points of all providers</param>
/// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the pickup points
/// </returns>
public virtual async Task<GetPickupPointsResponse> GetPickupPointsAsync(IList<ShoppingCartItem> 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<PickupPoint>();
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;
}
/// <summary>
/// Whether the shopping cart item is ship enabled
/// </summary>
/// <param name="shoppingCartItem">Shopping cart item</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains true if the shopping cart item requires shipping; otherwise false
/// </returns>
public virtual async Task<bool> 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);
}
/// <summary>
/// Whether the shopping cart item is free shipping
/// </summary>
/// <param name="shoppingCartItem">Shopping cart item</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains true if the shopping cart item is free shipping; otherwise false
/// </returns>
public virtual async Task<bool> 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);
}
/// <summary>
/// Get the additional shipping charge
/// </summary>
/// <param name="shoppingCartItem">Shopping cart item</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the additional shipping charge of the shopping cart item
/// </returns>
public virtual async Task<decimal> 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
}