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.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
}