Try your search with a different keyword or use * as a wildcard.
using System.Globalization;
using Newtonsoft.Json;
using Nop.Core;
using Nop.Core.Caching;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Directory;
using Nop.Core.Domain.Discounts;
using Nop.Core.Domain.Localization;
using Nop.Core.Domain.Logging;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Payments;
using Nop.Core.Domain.Shipping;
using Nop.Core.Domain.Tax;
using Nop.Core.Domain.Vendors;
using Nop.Core.Events;
using Nop.Services.Affiliates;
using Nop.Services.Catalog;
using Nop.Services.Common;
using Nop.Services.Customers;
using Nop.Services.Directory;
using Nop.Services.Discounts;
using Nop.Services.Localization;
using Nop.Services.Logging;
using Nop.Services.Messages;
using Nop.Services.Payments;
using Nop.Services.Security;
using Nop.Services.Shipping;
using Nop.Services.Stores;
using Nop.Services.Tax;
using Nop.Services.Vendors;
namespace Nop.Services.Orders;
/// <summary>
/// Order processing service
/// </summary>
public partial class OrderProcessingService : IOrderProcessingService
{
#region Fields
protected readonly CurrencySettings _currencySettings;
protected readonly IAddressService _addressService;
protected readonly IAffiliateService _affiliateService;
protected readonly ICheckoutAttributeFormatter _checkoutAttributeFormatter;
protected readonly ICountryService _countryService;
protected readonly ICurrencyService _currencyService;
protected readonly ICustomerActivityService _customerActivityService;
protected readonly ICustomerService _customerService;
protected readonly ICustomNumberFormatter _customNumberFormatter;
protected readonly IDiscountService _discountService;
protected readonly IEncryptionService _encryptionService;
protected readonly IEventPublisher _eventPublisher;
protected readonly IGenericAttributeService _genericAttributeService;
protected readonly IGiftCardService _giftCardService;
protected readonly ILanguageService _languageService;
protected readonly ILocalizationService _localizationService;
protected readonly ILogger _logger;
protected readonly IOrderService _orderService;
protected readonly IOrderTotalCalculationService _orderTotalCalculationService;
protected readonly IPaymentPluginManager _paymentPluginManager;
protected readonly IPaymentService _paymentService;
protected readonly IPdfService _pdfService;
protected readonly IPriceCalculationService _priceCalculationService;
protected readonly IPriceFormatter _priceFormatter;
protected readonly IProductAttributeFormatter _productAttributeFormatter;
protected readonly IProductAttributeParser _productAttributeParser;
protected readonly IProductService _productService;
protected readonly IReturnRequestService _returnRequestService;
protected readonly IRewardPointService _rewardPointService;
protected readonly IShipmentService _shipmentService;
protected readonly IShippingService _shippingService;
protected readonly IShoppingCartService _shoppingCartService;
protected readonly IStateProvinceService _stateProvinceService;
protected readonly IStaticCacheManager _staticCacheManager;
protected readonly IStoreContext _storeContext;
protected readonly IStoreMappingService _storeMappingService;
protected readonly IStoreService _storeService;
protected readonly ITaxService _taxService;
protected readonly IVendorService _vendorService;
protected readonly IWebHelper _webHelper;
protected readonly IWorkContext _workContext;
protected readonly IWorkflowMessageService _workflowMessageService;
protected readonly LocalizationSettings _localizationSettings;
protected readonly OrderSettings _orderSettings;
protected readonly PaymentSettings _paymentSettings;
protected readonly RewardPointsSettings _rewardPointsSettings;
protected readonly ShippingSettings _shippingSettings;
protected readonly TaxSettings _taxSettings;
#endregion
#region Ctor
public OrderProcessingService(CurrencySettings currencySettings,
IAddressService addressService,
IAffiliateService affiliateService,
ICheckoutAttributeFormatter checkoutAttributeFormatter,
ICountryService countryService,
ICurrencyService currencyService,
ICustomerActivityService customerActivityService,
ICustomerService customerService,
ICustomNumberFormatter customNumberFormatter,
IDiscountService discountService,
IEncryptionService encryptionService,
IEventPublisher eventPublisher,
IGenericAttributeService genericAttributeService,
IGiftCardService giftCardService,
ILanguageService languageService,
ILocalizationService localizationService,
ILogger logger,
IOrderService orderService,
IOrderTotalCalculationService orderTotalCalculationService,
IPaymentPluginManager paymentPluginManager,
IPaymentService paymentService,
IPdfService pdfService,
IPriceCalculationService priceCalculationService,
IPriceFormatter priceFormatter,
IProductAttributeFormatter productAttributeFormatter,
IProductAttributeParser productAttributeParser,
IProductService productService,
IReturnRequestService returnRequestService,
IRewardPointService rewardPointService,
IShipmentService shipmentService,
IShippingService shippingService,
IShoppingCartService shoppingCartService,
IStateProvinceService stateProvinceService,
IStaticCacheManager staticCacheManager,
IStoreContext storeContext,
IStoreMappingService storeMappingService,
IStoreService storeService,
ITaxService taxService,
IVendorService vendorService,
IWebHelper webHelper,
IWorkContext workContext,
IWorkflowMessageService workflowMessageService,
LocalizationSettings localizationSettings,
OrderSettings orderSettings,
PaymentSettings paymentSettings,
RewardPointsSettings rewardPointsSettings,
ShippingSettings shippingSettings,
TaxSettings taxSettings)
{
_currencySettings = currencySettings;
_addressService = addressService;
_affiliateService = affiliateService;
_checkoutAttributeFormatter = checkoutAttributeFormatter;
_countryService = countryService;
_currencyService = currencyService;
_customerActivityService = customerActivityService;
_customerService = customerService;
_customNumberFormatter = customNumberFormatter;
_discountService = discountService;
_encryptionService = encryptionService;
_eventPublisher = eventPublisher;
_genericAttributeService = genericAttributeService;
_giftCardService = giftCardService;
_languageService = languageService;
_localizationService = localizationService;
_logger = logger;
_orderService = orderService;
_orderTotalCalculationService = orderTotalCalculationService;
_paymentPluginManager = paymentPluginManager;
_paymentService = paymentService;
_pdfService = pdfService;
_priceCalculationService = priceCalculationService;
_priceFormatter = priceFormatter;
_productAttributeFormatter = productAttributeFormatter;
_productAttributeParser = productAttributeParser;
_productService = productService;
_returnRequestService = returnRequestService;
_rewardPointService = rewardPointService;
_shipmentService = shipmentService;
_shippingService = shippingService;
_shoppingCartService = shoppingCartService;
_stateProvinceService = stateProvinceService;
_staticCacheManager = staticCacheManager;
_storeContext = storeContext;
_storeMappingService = storeMappingService;
_storeService = storeService;
_taxService = taxService;
_vendorService = vendorService;
_webHelper = webHelper;
_workContext = workContext;
_workflowMessageService = workflowMessageService;
_localizationSettings = localizationSettings;
_orderSettings = orderSettings;
_paymentSettings = paymentSettings;
_rewardPointsSettings = rewardPointsSettings;
_shippingSettings = shippingSettings;
_taxSettings = taxSettings;
}
#endregion
#region Utilities
/// <summary>
/// Books the inventory by specified shipment
/// </summary>
/// <param name="shipment">Shipment</param>
/// <param name="message">Message for the stock quantity history</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task BookReservedInventoryAsync(Shipment shipment, string message)
{
foreach (var item in await _shipmentService.GetShipmentItemsByShipmentIdAsync(shipment.Id))
{
var product = await _orderService.GetProductByOrderItemIdAsync(item.OrderItemId);
if (product is null)
continue;
await _productService.BookReservedInventoryAsync(product, item.WarehouseId, -item.Quantity, message);
}
}
/// <summary>
/// Reveres the booked inventory by specified order
/// </summary>
/// <param name="order"></param>
/// <param name="message">Message for the stock quantity history</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task ReverseBookedInventoryAsync(Order order, string message)
{
foreach (var shipment in await _shipmentService.GetShipmentsByOrderIdAsync(order.Id))
{
foreach (var shipmentItem in await _shipmentService.GetShipmentItemsByShipmentIdAsync(shipment.Id))
{
var product = await _orderService.GetProductByOrderItemIdAsync(shipmentItem.OrderItemId);
if (product is null)
continue;
await _productService.ReverseBookedInventoryAsync(product, shipmentItem, message);
}
}
}
/// <summary>
/// Returns the stock by specified order
/// </summary>
/// <param name="order">Order</param>
/// <param name="message">Message for the stock quantity history</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task ReturnOrderStockAsync(Order order, string message)
{
foreach (var orderItem in await _orderService.GetOrderItemsAsync(order.Id))
{
var product = await _productService.GetProductByIdAsync(orderItem.ProductId);
if (product is null)
continue;
await _productService.AdjustInventoryAsync(product, orderItem.Quantity, orderItem.AttributesXml, message);
}
}
/// <summary>
/// Add order note
/// </summary>
/// <param name="order">Order</param>
/// <param name="note">Note text</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task AddOrderNoteAsync(Order order, string note)
{
await _orderService.InsertOrderNoteAsync(new OrderNote
{
OrderId = order.Id,
Note = note,
DisplayToCustomer = false,
CreatedOnUtc = DateTime.UtcNow
});
}
/// <summary>
/// Prepare details to place an order. It also sets some properties to "processPaymentRequest"
/// </summary>
/// <param name="processPaymentRequest">Process payment request</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the details
/// </returns>
protected virtual async Task<PlaceOrderContainer> PreparePlaceOrderDetailsAsync(ProcessPaymentRequest processPaymentRequest)
{
var details = new PlaceOrderContainer();
var currentCurrency = await _workContext.GetWorkingCurrencyAsync();
await PrepareAndValidateCustomerAsync(details, processPaymentRequest, currentCurrency);
await PrepareAndValidateShoppingCartAndCheckoutAttributesAsync(details, processPaymentRequest, currentCurrency);
await PrepareAndValidateBillingAddressAsync(details);
await PrepareAndValidateShippingInfoAsync(details, processPaymentRequest);
await PrepareAndValidateTotalsAsync(details, processPaymentRequest);
//affiliate
var affiliate = await _affiliateService.GetAffiliateByIdAsync(details.Customer.AffiliateId);
if (affiliate != null && affiliate.Active && !affiliate.Deleted)
details.AffiliateId = affiliate.Id;
//tax display type
details.CustomerTaxDisplayType = await _customerService.GetCustomerTaxDisplayTypeAsync(details.Customer);
//recurring or standard shopping cart?
details.IsRecurringShoppingCart = await _shoppingCartService.ShoppingCartIsRecurringAsync(details.Cart);
if (!details.IsRecurringShoppingCart)
return details;
await PrepareAndValidateRecurringShoppingAsync(details, processPaymentRequest);
return details;
}
/// <summary>
/// Prepare and validate recurring shopping cart
/// </summary>
/// <param name="details">PlaceOrder container</param>
/// <param name="processPaymentRequest">payment info holder</param>
/// <returns>A task that represents the asynchronous operation</returns>
/// <exception cref="NopException">Validation problems</exception>
protected virtual async Task PrepareAndValidateRecurringShoppingAsync(PlaceOrderContainer details, ProcessPaymentRequest processPaymentRequest)
{
var (recurringCyclesError, recurringCycleLength, recurringCyclePeriod, recurringTotalCycles) = await _shoppingCartService.GetRecurringCycleInfoAsync(details.Cart);
if (!string.IsNullOrEmpty(recurringCyclesError))
throw new NopException(recurringCyclesError);
processPaymentRequest.RecurringCycleLength = recurringCycleLength;
processPaymentRequest.RecurringCyclePeriod = recurringCyclePeriod;
processPaymentRequest.RecurringTotalCycles = recurringTotalCycles;
}
/// <summary>
/// Prepare and validate all totals
///
/// sub total, shipping total, payment total, tax amount etc.
/// </summary>
/// <param name="details">PlaceOrder container</param>
/// <param name="processPaymentRequest">payment info holder</param>
/// <returns>A task that represents the asynchronous operation</returns>
/// <exception cref="NopException">Validation problems</exception>
protected virtual async Task PrepareAndValidateTotalsAsync(PlaceOrderContainer details, ProcessPaymentRequest processPaymentRequest)
{
var (discountAmountInclTax, discountAmountExclTax, appliedDiscounts, subTotalWithoutDiscountInclTax,
subTotalWithoutDiscountExclTax, _, _, _) =
await _orderTotalCalculationService.GetShoppingCartSubTotalsAsync(details.Cart);
//sub total (incl tax)
details.OrderSubTotalInclTax = subTotalWithoutDiscountInclTax;
details.OrderSubTotalDiscountInclTax = discountAmountInclTax;
//discount history
foreach (var disc in appliedDiscounts)
if (!_discountService.ContainsDiscount(details.AppliedDiscounts, disc))
details.AppliedDiscounts.Add(disc);
//sub total (excl tax)
details.OrderSubTotalExclTax = subTotalWithoutDiscountExclTax;
details.OrderSubTotalDiscountExclTax = discountAmountExclTax;
//shipping total
var (orderShippingTotalInclTax, orderShippingTotalExclTax, _, shippingTotalDiscounts) = await _orderTotalCalculationService.GetShoppingCartShippingTotalsAsync(details.Cart);
if (!orderShippingTotalInclTax.HasValue || !orderShippingTotalExclTax.HasValue)
throw new NopException("Shipping total couldn't be calculated");
details.OrderShippingTotalInclTax = orderShippingTotalInclTax.Value;
details.OrderShippingTotalExclTax = orderShippingTotalExclTax.Value;
foreach (var disc in shippingTotalDiscounts)
if (!_discountService.ContainsDiscount(details.AppliedDiscounts, disc))
details.AppliedDiscounts.Add(disc);
//payment total
var paymentAdditionalFee = await _paymentService.GetAdditionalHandlingFeeAsync(details.Cart, processPaymentRequest.PaymentMethodSystemName);
details.PaymentAdditionalFeeInclTax = (await _taxService.GetPaymentMethodAdditionalFeeAsync(paymentAdditionalFee, true, details.Customer)).price;
details.PaymentAdditionalFeeExclTax = (await _taxService.GetPaymentMethodAdditionalFeeAsync(paymentAdditionalFee, false, details.Customer)).price;
//tax amount
SortedDictionary<decimal, decimal> taxRatesDictionary;
(details.OrderTaxTotal, taxRatesDictionary) = await _orderTotalCalculationService.GetTaxTotalAsync(details.Cart);
//VAT number
if (_taxSettings.EuVatEnabled && details.Customer.VatNumberStatus == VatNumberStatus.Valid)
details.VatNumber = details.Customer.VatNumber;
//tax rates
details.TaxRates = taxRatesDictionary.Aggregate(string.Empty, (current, next) =>
$"{current}{next.Key.ToString(CultureInfo.InvariantCulture)}:{next.Value.ToString(CultureInfo.InvariantCulture)}; ");
//order total (and applied discounts, gift cards, reward points)
var (orderTotal, orderDiscountAmount, orderAppliedDiscounts, appliedGiftCards, redeemedRewardPoints, redeemedRewardPointsAmount) = await _orderTotalCalculationService.GetShoppingCartTotalAsync(details.Cart);
if (!orderTotal.HasValue)
throw new NopException("Order total couldn't be calculated");
details.OrderDiscountAmount = orderDiscountAmount;
details.RedeemedRewardPoints = redeemedRewardPoints;
details.RedeemedRewardPointsAmount = redeemedRewardPointsAmount;
details.AppliedGiftCards = appliedGiftCards;
details.OrderTotal = orderTotal.Value;
//discount history
foreach (var disc in orderAppliedDiscounts)
if (!_discountService.ContainsDiscount(details.AppliedDiscounts, disc))
details.AppliedDiscounts.Add(disc);
processPaymentRequest.OrderTotal = details.OrderTotal;
}
/// <summary>
/// Prepare and validate shipping info
/// </summary>
/// <param name="details">PlaceOrder container</param>
/// <param name="processPaymentRequest">payment info holder</param>
/// <returns>A task that represents the asynchronous operation</returns>
/// <exception cref="NopException">Validation problems</exception>
protected virtual async Task PrepareAndValidateShippingInfoAsync(PlaceOrderContainer details, ProcessPaymentRequest processPaymentRequest)
{
//shipping info
if (await _shoppingCartService.ShoppingCartRequiresShippingAsync(details.Cart))
{
var pickupPoint = await _genericAttributeService.GetAttributeAsync<PickupPoint>(details.Customer,
NopCustomerDefaults.SelectedPickupPointAttribute, processPaymentRequest.StoreId);
if (_shippingSettings.AllowPickupInStore && pickupPoint != null)
{
var country = await _countryService.GetCountryByTwoLetterIsoCodeAsync(pickupPoint.CountryCode);
var state = await _stateProvinceService.GetStateProvinceByAbbreviationAsync(pickupPoint.StateAbbreviation, country?.Id);
details.PickupInStore = true;
details.PickupAddress = new Address
{
Address1 = pickupPoint.Address,
City = pickupPoint.City,
County = pickupPoint.County,
CountryId = country?.Id,
StateProvinceId = state?.Id,
ZipPostalCode = pickupPoint.ZipPostalCode,
CreatedOnUtc = DateTime.UtcNow
};
}
else
{
if (details.Customer.ShippingAddressId == null)
throw new NopException("Shipping address is not provided");
var shippingAddress = await _customerService.GetCustomerShippingAddressAsync(details.Customer);
if (!CommonHelper.IsValidEmail(shippingAddress?.Email))
throw new NopException("Email is not valid");
//clone shipping address
details.ShippingAddress = _addressService.CloneAddress(shippingAddress);
if (await _countryService.GetCountryByAddressAsync(details.ShippingAddress) is Country shippingCountry && !shippingCountry.AllowsShipping)
throw new NopException($"Country '{shippingCountry.Name}' is not allowed for shipping");
}
var shippingOption = await _genericAttributeService.GetAttributeAsync<ShippingOption>(details.Customer,
NopCustomerDefaults.SelectedShippingOptionAttribute, processPaymentRequest.StoreId);
if (shippingOption != null)
{
details.ShippingMethodName = shippingOption.Name;
details.ShippingRateComputationMethodSystemName = shippingOption.ShippingRateComputationMethodSystemName;
}
details.ShippingStatus = ShippingStatus.NotYetShipped;
}
else
details.ShippingStatus = ShippingStatus.ShippingNotRequired;
}
/// <summary>
/// Prepare and validate shopping cart and checkout attributes
/// </summary>
/// <param name="details">PlaceOrder container</param>
/// <param name="processPaymentRequest">payment info holder</param>
/// <param name="currentCurrency">The working currency</param>
/// <returns>A task that represents the asynchronous operation</returns>
/// <exception cref="NopException">Validation problems</exception>
protected virtual async Task PrepareAndValidateShoppingCartAndCheckoutAttributesAsync(PlaceOrderContainer details, ProcessPaymentRequest processPaymentRequest, Currency currentCurrency)
{
//checkout attributes
details.CheckoutAttributesXml = await _genericAttributeService.GetAttributeAsync<string>(details.Customer, NopCustomerDefaults.CheckoutAttributes, processPaymentRequest.StoreId);
details.CheckoutAttributeDescription = await _checkoutAttributeFormatter.FormatAttributesAsync(details.CheckoutAttributesXml, details.Customer);
//load shopping cart
details.Cart = await _shoppingCartService.GetShoppingCartAsync(details.Customer, ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId);
if (!details.Cart.Any())
throw new NopException("Cart is empty");
//validate the entire shopping cart
var warnings = await _shoppingCartService.GetShoppingCartWarningsAsync(details.Cart, details.CheckoutAttributesXml, true);
if (warnings.Any())
throw new NopException(warnings.Aggregate(string.Empty, (current, next) => $"{current}{next};"));
//validate individual cart items
foreach (var sci in details.Cart)
{
var product = await _productService.GetProductByIdAsync(sci.ProductId);
var sciWarnings = await _shoppingCartService.GetShoppingCartItemWarningsAsync(details.Customer,
sci.ShoppingCartType, product, processPaymentRequest.StoreId, sci.AttributesXml,
sci.CustomerEnteredPrice, sci.RentalStartDateUtc, sci.RentalEndDateUtc, sci.Quantity, false, sci.Id);
if (sciWarnings.Any())
throw new NopException(sciWarnings.Aggregate(string.Empty, (current, next) => $"{current}{next};"));
}
//min totals validation
if (!await ValidateMinOrderSubtotalAmountAsync(details.Cart))
{
var minOrderSubtotalAmount = await _currencyService.ConvertFromPrimaryStoreCurrencyAsync(_orderSettings.MinOrderSubtotalAmount, currentCurrency);
throw new NopException(string.Format(await _localizationService.GetResourceAsync("Checkout.MinOrderSubtotalAmount"),
await _priceFormatter.FormatPriceAsync(minOrderSubtotalAmount, true, false)));
}
if (!await ValidateMinOrderTotalAmountAsync(details.Cart))
{
var minOrderTotalAmount = await _currencyService.ConvertFromPrimaryStoreCurrencyAsync(_orderSettings.MinOrderTotalAmount, currentCurrency);
throw new NopException(string.Format(await _localizationService.GetResourceAsync("Checkout.MinOrderTotalAmount"),
await _priceFormatter.FormatPriceAsync(minOrderTotalAmount, true, false)));
}
}
/// <summary>
/// Prepare and validate billing address
/// </summary>
/// <param name="details">PlaceOrder container</param>
/// <returns>A task that represents the asynchronous operation</returns>
/// <exception cref="NopException">Validation problems</exception>
protected virtual async Task PrepareAndValidateBillingAddressAsync(PlaceOrderContainer details)
{
if (details.Customer.BillingAddressId is null)
throw new NopException("Billing address is not provided");
var billingAddress = await _customerService.GetCustomerBillingAddressAsync(details.Customer);
if (!CommonHelper.IsValidEmail(billingAddress?.Email))
throw new NopException("Email is not valid");
details.BillingAddress = _addressService.CloneAddress(billingAddress);
if (await _countryService.GetCountryByAddressAsync(details.BillingAddress) is Country billingCountry && !billingCountry.AllowsBilling)
throw new NopException($"Country '{billingCountry.Name}' is not allowed for billing");
}
/// <summary>
/// Prepare and validate customer
/// </summary>
/// <param name="details">PlaceOrder container</param>
/// <param name="processPaymentRequest">payment info holder</param>
/// <param name="currentCurrency">The working currency</param>
/// <returns>A task that represents the asynchronous operation</returns>
/// <exception cref="NopException">Validation problems</exception>
protected virtual async Task PrepareAndValidateCustomerAsync(PlaceOrderContainer details, ProcessPaymentRequest processPaymentRequest, Currency currentCurrency)
{
details.Customer = await _customerService.GetCustomerByIdAsync(processPaymentRequest.CustomerId);
if (details.Customer == null)
throw new ArgumentException("Customer is not set");
//check whether customer is guest
if (await _customerService.IsGuestAsync(details.Customer) && !_orderSettings.AnonymousCheckoutAllowed)
throw new NopException("Anonymous checkout is not allowed");
//customer currency
var currencyTmp = await _currencyService.GetCurrencyByIdAsync(details.Customer.CurrencyId ?? 0);
var customerCurrency = currencyTmp != null && currencyTmp.Published && await _storeMappingService.AuthorizeAsync(currencyTmp) ? currencyTmp : currentCurrency;
var primaryStoreCurrency = await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId);
details.CustomerCurrencyCode = customerCurrency.CurrencyCode;
details.CustomerCurrencyRate = customerCurrency.Rate / primaryStoreCurrency.Rate;
//customer language
details.CustomerLanguage = await _languageService.GetLanguageByIdAsync(details.Customer.LanguageId ?? 0);
if (details.CustomerLanguage == null || !details.CustomerLanguage.Published || !await _storeMappingService.AuthorizeAsync(details.CustomerLanguage))
details.CustomerLanguage = await _workContext.GetWorkingLanguageAsync();
}
/// <summary>
/// Prepare details to place order based on the recurring payment.
/// </summary>
/// <param name="processPaymentRequest">Process payment request</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the details
/// </returns>
protected virtual async Task<PlaceOrderContainer> PrepareRecurringOrderDetailsAsync(ProcessPaymentRequest processPaymentRequest)
{
var details = new PlaceOrderContainer
{
IsRecurringShoppingCart = true,
//load initial order
InitialOrder = processPaymentRequest.InitialOrder
};
if (details.InitialOrder == null)
throw new ArgumentException("Initial order is not set for recurring payment");
processPaymentRequest.PaymentMethodSystemName = details.InitialOrder.PaymentMethodSystemName;
//customer
details.Customer = await _customerService.GetCustomerByIdAsync(processPaymentRequest.CustomerId);
if (details.Customer == null)
throw new ArgumentException("Customer is not set");
//affiliate
var affiliate = await _affiliateService.GetAffiliateByIdAsync(details.Customer.AffiliateId);
if (affiliate != null && affiliate.Active && !affiliate.Deleted)
details.AffiliateId = affiliate.Id;
//check whether customer is guest
if (await _customerService.IsGuestAsync(details.Customer) && !_orderSettings.AnonymousCheckoutAllowed)
throw new NopException("Anonymous checkout is not allowed");
//customer currency
details.CustomerCurrencyCode = details.InitialOrder.CustomerCurrencyCode;
details.CustomerCurrencyRate = details.InitialOrder.CurrencyRate;
//customer language
details.CustomerLanguage = await _languageService.GetLanguageByIdAsync(details.InitialOrder.CustomerLanguageId);
if (details.CustomerLanguage == null || !details.CustomerLanguage.Published)
details.CustomerLanguage = await _workContext.GetWorkingLanguageAsync();
//billing address
if (details.InitialOrder.BillingAddressId == 0)
throw new NopException("Billing address is not available");
var billingAddress = await _addressService.GetAddressByIdAsync(details.InitialOrder.BillingAddressId);
details.BillingAddress = _addressService.CloneAddress(billingAddress);
if (await _countryService.GetCountryByAddressAsync(billingAddress) is Country billingCountry && !billingCountry.AllowsBilling)
throw new NopException($"Country '{billingCountry.Name}' is not allowed for billing");
//checkout attributes
details.CheckoutAttributesXml = details.InitialOrder.CheckoutAttributesXml;
details.CheckoutAttributeDescription = details.InitialOrder.CheckoutAttributeDescription;
//tax display type
details.CustomerTaxDisplayType = details.InitialOrder.CustomerTaxDisplayType;
//sub total
details.OrderSubTotalInclTax = details.InitialOrder.OrderSubtotalInclTax;
details.OrderSubTotalExclTax = details.InitialOrder.OrderSubtotalExclTax;
details.OrderSubTotalDiscountExclTax = details.InitialOrder.OrderSubTotalDiscountExclTax;
details.OrderSubTotalDiscountInclTax = details.InitialOrder.OrderSubTotalDiscountInclTax;
//shipping info
if (details.InitialOrder.ShippingStatus != ShippingStatus.ShippingNotRequired)
{
details.PickupInStore = details.InitialOrder.PickupInStore;
if (!details.PickupInStore)
{
if (!details.InitialOrder.ShippingAddressId.HasValue || await _addressService.GetAddressByIdAsync(details.InitialOrder.ShippingAddressId.Value) is not Address shippingAddress)
throw new NopException("Shipping address is not available");
//clone shipping address
details.ShippingAddress = _addressService.CloneAddress(shippingAddress);
if (await _countryService.GetCountryByAddressAsync(details.ShippingAddress) is Country shippingCountry && !shippingCountry.AllowsShipping)
throw new NopException($"Country '{shippingCountry.Name}' is not allowed for shipping");
}
else if (details.InitialOrder.PickupAddressId.HasValue && await _addressService.GetAddressByIdAsync(details.InitialOrder.PickupAddressId.Value) is Address pickupAddress)
details.PickupAddress = _addressService.CloneAddress(pickupAddress);
details.ShippingMethodName = details.InitialOrder.ShippingMethod;
details.ShippingRateComputationMethodSystemName = details.InitialOrder.ShippingRateComputationMethodSystemName;
details.ShippingStatus = ShippingStatus.NotYetShipped;
}
else
details.ShippingStatus = ShippingStatus.ShippingNotRequired;
//shipping total
details.OrderShippingTotalInclTax = details.InitialOrder.OrderShippingInclTax;
details.OrderShippingTotalExclTax = details.InitialOrder.OrderShippingExclTax;
//payment total
details.PaymentAdditionalFeeInclTax = details.InitialOrder.PaymentMethodAdditionalFeeInclTax;
details.PaymentAdditionalFeeExclTax = details.InitialOrder.PaymentMethodAdditionalFeeExclTax;
//tax total
details.OrderTaxTotal = details.InitialOrder.OrderTax;
//tax rates
details.TaxRates = details.InitialOrder.TaxRates;
//VAT number
details.VatNumber = details.InitialOrder.VatNumber;
//discount history (the same)
foreach (var duh in await _discountService.GetAllDiscountUsageHistoryAsync(orderId: details.InitialOrder.Id))
{
var d = await _discountService.GetDiscountByIdAsync(duh.DiscountId);
if (d != null)
details.AppliedDiscounts.Add(d);
}
//order total
details.OrderDiscountAmount = details.InitialOrder.OrderDiscount;
details.OrderTotal = details.InitialOrder.OrderTotal;
processPaymentRequest.OrderTotal = details.OrderTotal;
return details;
}
/// <summary>
/// Save order and add order notes
/// </summary>
/// <param name="processPaymentRequest">Process payment request</param>
/// <param name="processPaymentResult">Process payment result</param>
/// <param name="details">Details</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the order
/// </returns>
protected virtual async Task<Order> SaveOrderDetailsAsync(ProcessPaymentRequest processPaymentRequest,
ProcessPaymentResult processPaymentResult, PlaceOrderContainer details)
{
var order = new Order
{
StoreId = processPaymentRequest.StoreId,
OrderGuid = processPaymentRequest.OrderGuid,
CustomerId = details.Customer.Id,
CustomerLanguageId = details.CustomerLanguage.Id,
CustomerTaxDisplayType = details.CustomerTaxDisplayType,
CustomerIp = _webHelper.GetCurrentIpAddress(),
OrderSubtotalInclTax = details.OrderSubTotalInclTax,
OrderSubtotalExclTax = details.OrderSubTotalExclTax,
OrderSubTotalDiscountInclTax = details.OrderSubTotalDiscountInclTax,
OrderSubTotalDiscountExclTax = details.OrderSubTotalDiscountExclTax,
OrderShippingInclTax = details.OrderShippingTotalInclTax,
OrderShippingExclTax = details.OrderShippingTotalExclTax,
PaymentMethodAdditionalFeeInclTax = details.PaymentAdditionalFeeInclTax,
PaymentMethodAdditionalFeeExclTax = details.PaymentAdditionalFeeExclTax,
TaxRates = details.TaxRates,
OrderTax = details.OrderTaxTotal,
OrderTotal = details.OrderTotal,
RefundedAmount = decimal.Zero,
OrderDiscount = details.OrderDiscountAmount,
CheckoutAttributeDescription = details.CheckoutAttributeDescription,
CheckoutAttributesXml = details.CheckoutAttributesXml,
CustomerCurrencyCode = details.CustomerCurrencyCode,
CurrencyRate = details.CustomerCurrencyRate,
AffiliateId = details.AffiliateId,
OrderStatus = OrderStatus.Pending,
AllowStoringCreditCardNumber = processPaymentResult.AllowStoringCreditCardNumber,
CardType = processPaymentResult.AllowStoringCreditCardNumber ? _encryptionService.EncryptText(processPaymentRequest.CreditCardType) : string.Empty,
CardName = processPaymentResult.AllowStoringCreditCardNumber ? _encryptionService.EncryptText(processPaymentRequest.CreditCardName) : string.Empty,
CardNumber = processPaymentResult.AllowStoringCreditCardNumber ? _encryptionService.EncryptText(processPaymentRequest.CreditCardNumber) : string.Empty,
MaskedCreditCardNumber = _encryptionService.EncryptText(_paymentService.GetMaskedCreditCardNumber(processPaymentRequest.CreditCardNumber)),
CardCvv2 = processPaymentResult.AllowStoringCreditCardNumber ? _encryptionService.EncryptText(processPaymentRequest.CreditCardCvv2) : string.Empty,
CardExpirationMonth = processPaymentResult.AllowStoringCreditCardNumber ? _encryptionService.EncryptText(processPaymentRequest.CreditCardExpireMonth.ToString()) : string.Empty,
CardExpirationYear = processPaymentResult.AllowStoringCreditCardNumber ? _encryptionService.EncryptText(processPaymentRequest.CreditCardExpireYear.ToString()) : string.Empty,
PaymentMethodSystemName = processPaymentRequest.PaymentMethodSystemName,
AuthorizationTransactionId = processPaymentResult.AuthorizationTransactionId,
AuthorizationTransactionCode = processPaymentResult.AuthorizationTransactionCode,
AuthorizationTransactionResult = processPaymentResult.AuthorizationTransactionResult,
CaptureTransactionId = processPaymentResult.CaptureTransactionId,
CaptureTransactionResult = processPaymentResult.CaptureTransactionResult,
SubscriptionTransactionId = processPaymentResult.SubscriptionTransactionId,
PaymentStatus = processPaymentResult.NewPaymentStatus,
PaidDateUtc = null,
PickupInStore = details.PickupInStore,
ShippingStatus = details.ShippingStatus,
ShippingMethod = details.ShippingMethodName,
ShippingRateComputationMethodSystemName = details.ShippingRateComputationMethodSystemName,
CustomValuesXml = processPaymentRequest.CustomValues.SerializeToXml(),
VatNumber = details.VatNumber,
CreatedOnUtc = DateTime.UtcNow,
CustomOrderNumber = string.Empty
};
if (details.BillingAddress is null)
throw new NopException("Billing address is not provided");
await _addressService.InsertAddressAsync(details.BillingAddress);
order.BillingAddressId = details.BillingAddress.Id;
if (details.PickupAddress != null)
{
await _addressService.InsertAddressAsync(details.PickupAddress);
order.PickupAddressId = details.PickupAddress.Id;
}
if (details.ShippingAddress != null)
{
await _addressService.InsertAddressAsync(details.ShippingAddress);
order.ShippingAddressId = details.ShippingAddress.Id;
}
await _orderService.InsertOrderAsync(order);
//generate and set custom order number
order.CustomOrderNumber = _customNumberFormatter.GenerateOrderCustomNumber(order);
await _orderService.UpdateOrderAsync(order);
//reward points history
if (details.RedeemedRewardPointsAmount <= decimal.Zero)
return order;
order.RedeemedRewardPointsEntryId = await _rewardPointService.AddRewardPointsHistoryEntryAsync(details.Customer, -details.RedeemedRewardPoints, order.StoreId,
string.Format(await _localizationService.GetResourceAsync("RewardPoints.Message.RedeemedForOrder", order.CustomerLanguageId), order.CustomOrderNumber),
order, details.RedeemedRewardPointsAmount);
await _orderService.UpdateOrderAsync(order);
return order;
}
/// <summary>
/// Send "order placed" notifications and save order notes
/// </summary>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task SendNotificationsAndSaveNotesAsync(Order order)
{
//notes, messages
await AddOrderNoteAsync(order, _workContext.OriginalCustomerIfImpersonated != null
? $"Order placed by a store owner ('{_workContext.OriginalCustomerIfImpersonated.Email}'. ID = {_workContext.OriginalCustomerIfImpersonated.Id}) impersonating the customer."
: "Order placed");
//send email notifications
var orderPlacedStoreOwnerNotificationQueuedEmailIds = await _workflowMessageService.SendOrderPlacedStoreOwnerNotificationAsync(order, _localizationSettings.DefaultAdminLanguageId);
if (orderPlacedStoreOwnerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order placed\" email (to store owner) has been queued. Queued email identifiers: {string.Join(", ", orderPlacedStoreOwnerNotificationQueuedEmailIds)}.");
var orderPlacedAttachmentFilePath = _orderSettings.AttachPdfInvoiceToOrderPlacedEmail ?
(await _pdfService.SaveOrderPdfToDiskAsync(order)) : null;
var orderPlacedAttachmentFileName = _orderSettings.AttachPdfInvoiceToOrderPlacedEmail ?
(string.Format(await _localizationService.GetResourceAsync("PDFInvoice.FileName"), order.CustomOrderNumber) + ".pdf") : null;
var orderPlacedCustomerNotificationQueuedEmailIds = await _workflowMessageService
.SendOrderPlacedCustomerNotificationAsync(order, order.CustomerLanguageId, orderPlacedAttachmentFilePath, orderPlacedAttachmentFileName);
if (orderPlacedCustomerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order placed\" email (to customer) has been queued. Queued email identifiers: {string.Join(", ", orderPlacedCustomerNotificationQueuedEmailIds)}.");
var vendors = await GetVendorsInOrderAsync(order);
foreach (var vendor in vendors)
{
var orderPlacedVendorNotificationQueuedEmailIds = await _workflowMessageService.SendOrderPlacedVendorNotificationAsync(order, vendor, _localizationSettings.DefaultAdminLanguageId);
if (orderPlacedVendorNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order placed\" email (to vendor) has been queued. Queued email identifiers: {string.Join(", ", orderPlacedVendorNotificationQueuedEmailIds)}.");
}
if (order.AffiliateId == 0)
return;
var orderPlacedAffiliateNotificationQueuedEmailIds = await _workflowMessageService.SendOrderPlacedAffiliateNotificationAsync(order, _localizationSettings.DefaultAdminLanguageId);
if (orderPlacedAffiliateNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order placed\" email (to affiliate) has been queued. Queued email identifiers: {string.Join(", ", orderPlacedAffiliateNotificationQueuedEmailIds)}.");
}
/// <summary>
/// Award (earn) reward points (for placing a new order)
/// </summary>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task AwardRewardPointsAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
var totalForRewardPoints = _orderTotalCalculationService
.CalculateApplicableOrderTotalForRewardPoints(order.OrderShippingInclTax, order.OrderTotal);
var points = totalForRewardPoints > decimal.Zero ?
await _orderTotalCalculationService.CalculateRewardPointsAsync(customer, totalForRewardPoints) : 0;
if (points == 0)
return;
//Ensure that reward points were not added (earned) before. We should not add reward points if they were already earned for this order
if (order.RewardPointsHistoryEntryId.HasValue)
return;
//check whether delay is set
DateTime? activatingDate = null;
if (_rewardPointsSettings.ActivationDelay > 0)
{
var delayPeriod = (RewardPointsActivatingDelayPeriod)_rewardPointsSettings.ActivationDelayPeriodId;
var delayInHours = delayPeriod.ToHours(_rewardPointsSettings.ActivationDelay);
activatingDate = DateTime.UtcNow.AddHours(delayInHours);
}
//whether points validity is set
DateTime? endDate = null;
if (_rewardPointsSettings.PurchasesPointsValidity > 0)
endDate = (activatingDate ?? DateTime.UtcNow).AddDays(_rewardPointsSettings.PurchasesPointsValidity.Value);
//add reward points
order.RewardPointsHistoryEntryId = await _rewardPointService.AddRewardPointsHistoryEntryAsync(customer, points, order.StoreId,
string.Format(await _localizationService.GetResourceAsync("RewardPoints.Message.EarnedForOrder"), order.CustomOrderNumber),
activatingDate: activatingDate, endDate: endDate);
await _orderService.UpdateOrderAsync(order);
}
/// <summary>
/// Reduce (cancel) reward points (previously awarded for placing an order)
/// </summary>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task ReduceRewardPointsAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
//ensure that reward points were already earned for this order before
if (!order.RewardPointsHistoryEntryId.HasValue)
return;
//get appropriate history entry
var rewardPointsHistoryEntry = await _rewardPointService.GetRewardPointsHistoryEntryByIdAsync(order.RewardPointsHistoryEntryId.Value);
if (rewardPointsHistoryEntry != null)
{
if (rewardPointsHistoryEntry.CreatedOnUtc > DateTime.UtcNow)
{
//just delete the upcoming entry (points were not granted yet)
await _rewardPointService.DeleteRewardPointsHistoryEntryAsync(rewardPointsHistoryEntry);
}
else
{
var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
//or reduce reward points if the entry already exists
await _rewardPointService.AddRewardPointsHistoryEntryAsync(customer, -rewardPointsHistoryEntry.Points, order.StoreId,
string.Format(await _localizationService.GetResourceAsync("RewardPoints.Message.ReducedForOrder"), order.CustomOrderNumber));
}
}
}
/// <summary>
/// Return back redeemed reward points to a customer (spent when placing an order)
/// </summary>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task ReturnBackRedeemedRewardPointsAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
//were some reward points spend on the order
var allRewardPoints = await _rewardPointService.GetRewardPointsHistoryAsync(order.CustomerId, order.StoreId, orderGuid: order.OrderGuid);
if (allRewardPoints?.Any() == true)
{
foreach (var rewardPoints in allRewardPoints)
{
//return back
await _rewardPointService.AddRewardPointsHistoryEntryAsync(customer, -rewardPoints.Points, order.StoreId,
string.Format(await _localizationService.GetResourceAsync("RewardPoints.Message.ReturnedForOrder"), order.CustomOrderNumber));
}
}
}
/// <summary>
/// Set IsActivated value for purchase gift cards for particular order
/// </summary>
/// <param name="order">Order</param>
/// <param name="activate">A value indicating whether to activate gift cards; true - activate, false - deactivate</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task SetActivatedValueForPurchasedGiftCardsAsync(Order order, bool activate)
{
var giftCards = await _giftCardService.GetAllGiftCardsAsync(order.Id, isGiftCardActivated: !activate);
foreach (var gc in giftCards)
{
if (activate)
{
//activate
var isRecipientNotified = gc.IsRecipientNotified;
if (gc.GiftCardType == GiftCardType.Virtual)
{
//send email for virtual gift card
if (!string.IsNullOrEmpty(gc.RecipientEmail) &&
!string.IsNullOrEmpty(gc.SenderEmail))
{
var customerLang = (await _languageService.GetLanguageByIdAsync(order.CustomerLanguageId) ??
(await _languageService.GetAllLanguagesAsync()).FirstOrDefault())
?? throw new Exception("No languages could be loaded");
var queuedEmailIds = await _workflowMessageService.SendGiftCardNotificationAsync(gc, customerLang.Id);
if (queuedEmailIds.Any())
isRecipientNotified = true;
}
}
gc.IsGiftCardActivated = true;
gc.IsRecipientNotified = isRecipientNotified;
await _giftCardService.UpdateGiftCardAsync(gc);
}
else
{
//deactivate
gc.IsGiftCardActivated = false;
await _giftCardService.UpdateGiftCardAsync(gc);
}
}
}
/// <summary>
/// Sets an order status
/// </summary>
/// <param name="order">Order</param>
/// <param name="os">New order status</param>
/// <param name="notifyCustomer">True to notify customer</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task SetOrderStatusAsync(Order order, OrderStatus os, bool notifyCustomer)
{
ArgumentNullException.ThrowIfNull(order);
var prevOrderStatus = order.OrderStatus;
if (prevOrderStatus == os)
return;
//set and save new order status
order.OrderStatusId = (int)os;
await _orderService.UpdateOrderAsync(order);
//notify and allow event handlers to change status
await _eventPublisher.PublishAsync(new OrderStatusChangedEvent(order, prevOrderStatus));
if (prevOrderStatus == order.OrderStatus)
return;
//order notes, notifications
await AddOrderNoteAsync(order, $"Order status has been changed to {await _localizationService.GetLocalizedEnumAsync(os)}");
if (prevOrderStatus != OrderStatus.Processing &&
os == OrderStatus.Processing
&& notifyCustomer)
{
//notification
var orderProcessingAttachmentFilePath = _orderSettings.AttachPdfInvoiceToOrderProcessingEmail ?
await _pdfService.SaveOrderPdfToDiskAsync(order) : null;
var orderProcessingAttachmentFileName = _orderSettings.AttachPdfInvoiceToOrderProcessingEmail ?
(string.Format(await _localizationService.GetResourceAsync("PDFInvoice.FileName"), order.CustomOrderNumber) + ".pdf") : null;
var orderProcessingCustomerNotificationQueuedEmailIds = await _workflowMessageService
.SendOrderProcessingCustomerNotificationAsync(order, order.CustomerLanguageId, orderProcessingAttachmentFilePath,
orderProcessingAttachmentFileName);
if (orderProcessingCustomerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order processing\" email (to customer) has been queued. Queued email identifiers: {string.Join(", ", orderProcessingCustomerNotificationQueuedEmailIds)}.");
}
if (prevOrderStatus != OrderStatus.Complete &&
os == OrderStatus.Complete
&& notifyCustomer)
{
//notify customer
var orderCompletedAttachmentFilePath = _orderSettings.AttachPdfInvoiceToOrderCompletedEmail ?
await _pdfService.SaveOrderPdfToDiskAsync(order) : null;
var orderCompletedAttachmentFileName = _orderSettings.AttachPdfInvoiceToOrderCompletedEmail ?
(string.Format(await _localizationService.GetResourceAsync("PDFInvoice.FileName"), order.CustomOrderNumber) + ".pdf") : null;
var orderCompletedCustomerNotificationQueuedEmailIds = await _workflowMessageService
.SendOrderCompletedCustomerNotificationAsync(order, order.CustomerLanguageId, orderCompletedAttachmentFilePath,
orderCompletedAttachmentFileName);
if (orderCompletedCustomerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order completed\" email (to customer) has been queued. Queued email identifiers: {string.Join(", ", orderCompletedCustomerNotificationQueuedEmailIds)}.");
//notify store owner
await _workflowMessageService.SendOrderCompletedStoreOwnerNotificationAsync(order, _localizationSettings.DefaultAdminLanguageId);
}
if (prevOrderStatus != OrderStatus.Cancelled &&
os == OrderStatus.Cancelled
&& notifyCustomer)
{
//notification
var orderCancelledCustomerNotificationQueuedEmailIds = await _workflowMessageService.SendOrderCancelledCustomerNotificationAsync(order, order.CustomerLanguageId);
if (orderCancelledCustomerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order cancelled\" email (to customer) has been queued. Queued email identifiers: {string.Join(", ", orderCancelledCustomerNotificationQueuedEmailIds)}.");
var vendors = await GetVendorsInOrderAsync(order);
foreach (var vendor in vendors)
{
var orderCancelVendorNotificationQueuedEmailIds = await _workflowMessageService.SendOrderCancelledVendorNotificationAsync(order, vendor, _localizationSettings.DefaultAdminLanguageId);
if (orderCancelVendorNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order cancelled\" email (to vendor) has been queued. Queued email identifiers: {string.Join(", ", orderCancelVendorNotificationQueuedEmailIds)}.");
}
}
//reward points
if (order.OrderStatus == OrderStatus.Complete)
await AwardRewardPointsAsync(order);
if (order.OrderStatus == OrderStatus.Cancelled)
await ReduceRewardPointsAsync(order);
//gift cards activation
if (_orderSettings.ActivateGiftCardsAfterCompletingOrder && order.OrderStatus == OrderStatus.Complete)
await SetActivatedValueForPurchasedGiftCardsAsync(order, true);
//gift cards deactivation
if (_orderSettings.DeactivateGiftCardsAfterCancellingOrder && order.OrderStatus == OrderStatus.Cancelled)
await SetActivatedValueForPurchasedGiftCardsAsync(order, false);
}
/// <summary>
/// Process order paid status
/// </summary>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task ProcessOrderPaidAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
//raise event
await _eventPublisher.PublishAsync(new OrderPaidEvent(order));
//order paid email notification
if (order.OrderTotal != decimal.Zero)
{
//we should not send it for free ($0 total) orders?
//remove this "if" statement if you want to send it in this case
var orderPaidAttachmentFilePath = _orderSettings.AttachPdfInvoiceToOrderPaidEmail ?
await _pdfService.SaveOrderPdfToDiskAsync(order) : null;
var orderPaidAttachmentFileName = _orderSettings.AttachPdfInvoiceToOrderPaidEmail ?
(string.Format(await _localizationService.GetResourceAsync("PDFInvoice.FileName"), order.CustomOrderNumber) + ".pdf") : null;
var orderPaidCustomerNotificationQueuedEmailIds = await _workflowMessageService.SendOrderPaidCustomerNotificationAsync(order, order.CustomerLanguageId,
orderPaidAttachmentFilePath, orderPaidAttachmentFileName);
if (orderPaidCustomerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order paid\" email (to customer) has been queued. Queued email identifiers: {string.Join(", ", orderPaidCustomerNotificationQueuedEmailIds)}.");
var orderPaidStoreOwnerNotificationQueuedEmailIds = await _workflowMessageService.SendOrderPaidStoreOwnerNotificationAsync(order, _localizationSettings.DefaultAdminLanguageId);
if (orderPaidStoreOwnerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order paid\" email (to store owner) has been queued. Queued email identifiers: {string.Join(", ", orderPaidStoreOwnerNotificationQueuedEmailIds)}.");
var vendors = await GetVendorsInOrderAsync(order);
foreach (var vendor in vendors)
{
var orderPaidVendorNotificationQueuedEmailIds = await _workflowMessageService.SendOrderPaidVendorNotificationAsync(order, vendor, _localizationSettings.DefaultAdminLanguageId);
if (orderPaidVendorNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order paid\" email (to vendor) has been queued. Queued email identifiers: {string.Join(", ", orderPaidVendorNotificationQueuedEmailIds)}.");
}
if (order.AffiliateId != 0)
{
var orderPaidAffiliateNotificationQueuedEmailIds = await _workflowMessageService.SendOrderPaidAffiliateNotificationAsync(order,
_localizationSettings.DefaultAdminLanguageId);
if (orderPaidAffiliateNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order paid\" email (to affiliate) has been queued. Queued email identifiers: {string.Join(", ", orderPaidAffiliateNotificationQueuedEmailIds)}.");
}
}
//customer roles with "purchased with product" specified
await ProcessCustomerRolesWithPurchasedProductSpecifiedAsync(order, true);
}
/// <summary>
/// Process customer roles with "Purchased with Product" property configured
/// </summary>
/// <param name="order">Order</param>
/// <param name="add">A value indicating whether to add configured customer role; true - add, false - remove</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task ProcessCustomerRolesWithPurchasedProductSpecifiedAsync(Order order, bool add)
{
ArgumentNullException.ThrowIfNull(order);
//purchased product identifiers
var purchasedProductIds = new List<int>();
foreach (var orderItem in await _orderService.GetOrderItemsAsync(order.Id))
{
//standard items
purchasedProductIds.Add(orderItem.ProductId);
//bundled (associated) products
var attributeValues = await _productAttributeParser.ParseProductAttributeValuesAsync(orderItem.AttributesXml);
foreach (var attributeValue in attributeValues)
if (attributeValue.AttributeValueType == AttributeValueType.AssociatedToProduct)
purchasedProductIds.Add(attributeValue.AssociatedProductId);
}
//list of customer roles
var customerRoles = (await _customerService
.GetAllCustomerRolesAsync(true))
.Where(cr => purchasedProductIds.Contains(cr.PurchasedWithProductId))
.ToList();
if (!customerRoles.Any())
return;
var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
foreach (var customerRole in customerRoles)
if (!await _customerService.IsInCustomerRoleAsync(customer, customerRole.SystemName))
{
//not in the list yet
if (add)
//add
await _customerService.AddCustomerRoleMappingAsync(new CustomerCustomerRoleMapping { CustomerId = customer.Id, CustomerRoleId = customerRole.Id });
}
else
{
//already in the list
if (!add)
//remove
await _customerService.RemoveCustomerRoleMappingAsync(customer, customerRole);
}
}
/// <summary>
/// Get a list of vendors in order (order items)
/// </summary>
/// <param name="order">Order</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the vendors
/// </returns>
protected virtual async Task<IList<Vendor>> GetVendorsInOrderAsync(Order order)
{
var pIds = (await _orderService.GetOrderItemsAsync(order.Id)).Select(x => x.ProductId).ToArray();
return await _vendorService.GetVendorsByProductIdsAsync(pIds);
}
/// <summary>
/// Create recurring payment (the first payment)
/// </summary>
/// <param name="processPaymentRequest">Process payment request</param>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task CreateFirstRecurringPaymentAsync(ProcessPaymentRequest processPaymentRequest, Order order)
{
var rp = new RecurringPayment
{
CycleLength = processPaymentRequest.RecurringCycleLength,
CyclePeriod = processPaymentRequest.RecurringCyclePeriod,
TotalCycles = processPaymentRequest.RecurringTotalCycles,
StartDateUtc = DateTime.UtcNow,
IsActive = true,
CreatedOnUtc = DateTime.UtcNow,
InitialOrderId = order.Id
};
await _orderService.InsertRecurringPaymentAsync(rp);
switch (await _paymentService.GetRecurringPaymentTypeAsync(processPaymentRequest.PaymentMethodSystemName))
{
case RecurringPaymentType.NotSupported:
//not supported
break;
case RecurringPaymentType.Manual:
await _orderService.InsertRecurringPaymentHistoryAsync(new RecurringPaymentHistory
{
RecurringPaymentId = rp.Id,
CreatedOnUtc = DateTime.UtcNow,
OrderId = order.Id
});
break;
case RecurringPaymentType.Automatic:
//will be created later (process is automated)
break;
default:
break;
}
}
/// <summary>
/// Move shopping cart items to order items
/// </summary>
/// <param name="details">Place order container</param>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task MoveShoppingCartItemsToOrderItemsAsync(PlaceOrderContainer details, Order order)
{
foreach (var sc in details.Cart)
{
var product = await _productService.GetProductByIdAsync(sc.ProductId);
//prices
var scUnitPrice = (await _shoppingCartService.GetUnitPriceAsync(sc, true)).unitPrice;
var (scSubTotal, discountAmount, scDiscounts, _) = await _shoppingCartService.GetSubTotalAsync(sc, true);
var scUnitPriceInclTax = await _taxService.GetProductPriceAsync(product, scUnitPrice, true, details.Customer);
var scUnitPriceExclTax = await _taxService.GetProductPriceAsync(product, scUnitPrice, false, details.Customer);
var scSubTotalInclTax = await _taxService.GetProductPriceAsync(product, scSubTotal, true, details.Customer);
var scSubTotalExclTax = await _taxService.GetProductPriceAsync(product, scSubTotal, false, details.Customer);
var discountAmountInclTax = await _taxService.GetProductPriceAsync(product, discountAmount, true, details.Customer);
var discountAmountExclTax = await _taxService.GetProductPriceAsync(product, discountAmount, false, details.Customer);
foreach (var disc in scDiscounts)
if (!_discountService.ContainsDiscount(details.AppliedDiscounts, disc))
details.AppliedDiscounts.Add(disc);
//attributes
var store = await _storeService.GetStoreByIdAsync(sc.StoreId);
var attributeDescription = await _productAttributeFormatter.FormatAttributesAsync(product, sc.AttributesXml, details.Customer, store);
var itemWeight = await _shippingService.GetShoppingCartItemWeightAsync(sc);
//save order item
var orderItem = new OrderItem
{
OrderItemGuid = Guid.NewGuid(),
OrderId = order.Id,
ProductId = product.Id,
UnitPriceInclTax = scUnitPriceInclTax.price,
UnitPriceExclTax = scUnitPriceExclTax.price,
PriceInclTax = scSubTotalInclTax.price,
PriceExclTax = scSubTotalExclTax.price,
OriginalProductCost = await _priceCalculationService.GetProductCostAsync(product, sc.AttributesXml),
AttributeDescription = attributeDescription,
AttributesXml = sc.AttributesXml,
Quantity = sc.Quantity,
DiscountAmountInclTax = discountAmountInclTax.price,
DiscountAmountExclTax = discountAmountExclTax.price,
DownloadCount = 0,
IsDownloadActivated = false,
LicenseDownloadId = 0,
ItemWeight = itemWeight,
RentalStartDateUtc = sc.RentalStartDateUtc,
RentalEndDateUtc = sc.RentalEndDateUtc
};
await _orderService.InsertOrderItemAsync(orderItem);
//gift cards
await AddGiftCardsAsync(product, sc.AttributesXml, sc.Quantity, orderItem, scUnitPriceExclTax.price);
//inventory
await _productService.AdjustInventoryAsync(product, -sc.Quantity, sc.AttributesXml,
string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.PlaceOrder"), order.Id));
await _eventPublisher.PublishAsync(new ShoppingCartItemMovedToOrderItemEvent(sc, orderItem));
}
await _shoppingCartService.ClearShoppingCartAsync(details.Customer, order.StoreId);
}
/// <summary>
/// Add gift cards
/// </summary>
/// <param name="product">Product</param>
/// <param name="attributesXml">attributes XML</param>
/// <param name="quantity">Quantity</param>
/// <param name="orderItem">Order item</param>
/// <param name="unitPriceExclTax">Unit price exclude tax, it set as amount if not set specific amount and product.OverriddenGiftCardAmount isn't set to</param>
/// <param name="amount">Amount</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task AddGiftCardsAsync(Product product, string attributesXml, int quantity, OrderItem orderItem, decimal? unitPriceExclTax = null, decimal? amount = null)
{
if (!product.IsGiftCard)
return;
_productAttributeParser.GetGiftCardAttribute(attributesXml, out var giftCardRecipientName, out var giftCardRecipientEmail, out var giftCardSenderName, out var giftCardSenderEmail, out var giftCardMessage);
for (var i = 0; i < quantity; i++)
{
await _giftCardService.InsertGiftCardAsync(new GiftCard
{
GiftCardType = product.GiftCardType,
PurchasedWithOrderItemId = orderItem.Id,
Amount = amount ?? product.OverriddenGiftCardAmount ?? unitPriceExclTax ?? 0,
IsGiftCardActivated = false,
GiftCardCouponCode = _giftCardService.GenerateGiftCardCode(),
RecipientName = giftCardRecipientName,
RecipientEmail = giftCardRecipientEmail,
SenderName = giftCardSenderName,
SenderEmail = giftCardSenderEmail,
Message = giftCardMessage,
IsRecipientNotified = false,
CreatedOnUtc = DateTime.UtcNow
});
}
}
/// <summary>
/// Get process payment result
/// </summary>
/// <param name="processPaymentRequest">Process payment request</param>
/// <param name="details">Place order container</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the
/// </returns>
protected virtual async Task<ProcessPaymentResult> GetProcessPaymentResultAsync(ProcessPaymentRequest processPaymentRequest, PlaceOrderContainer details)
{
//process payment
ProcessPaymentResult processPaymentResult;
//check if is payment workflow required
if (await IsPaymentWorkflowRequiredAsync(details.Cart))
{
var customer = await _customerService.GetCustomerByIdAsync(processPaymentRequest.CustomerId);
var paymentMethod = await _paymentPluginManager
.LoadPluginBySystemNameAsync(processPaymentRequest.PaymentMethodSystemName, customer, processPaymentRequest.StoreId)
?? throw new NopException("Payment method couldn't be loaded");
//ensure that payment method is active
if (!_paymentPluginManager.IsPluginActive(paymentMethod))
throw new NopException("Payment method is not active");
if (details.IsRecurringShoppingCart)
{
//recurring cart
processPaymentResult = (await _paymentService.GetRecurringPaymentTypeAsync(processPaymentRequest.PaymentMethodSystemName)) switch
{
RecurringPaymentType.NotSupported => throw new NopException("Recurring payments are not supported by selected payment method"),
RecurringPaymentType.Manual or
RecurringPaymentType.Automatic => await _paymentService.ProcessRecurringPaymentAsync(processPaymentRequest),
_ => throw new NopException("Not supported recurring payment type"),
};
}
else
//standard cart
processPaymentResult = await _paymentService.ProcessPaymentAsync(processPaymentRequest);
}
else
//payment is not required
processPaymentResult = new ProcessPaymentResult { NewPaymentStatus = PaymentStatus.Paid };
return processPaymentResult;
}
/// <summary>
/// Save gift card usage history
/// </summary>
/// <param name="details">Place order container</param>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task SaveGiftCardUsageHistoryAsync(PlaceOrderContainer details, Order order)
{
if (details.AppliedGiftCards == null || !details.AppliedGiftCards.Any())
return;
foreach (var agc in details.AppliedGiftCards)
await _giftCardService.InsertGiftCardUsageHistoryAsync(new GiftCardUsageHistory
{
GiftCardId = agc.GiftCard.Id,
UsedWithOrderId = order.Id,
UsedValue = agc.AmountCanBeUsed,
CreatedOnUtc = DateTime.UtcNow
});
}
/// <summary>
/// Save discount usage history
/// </summary>
/// <param name="details">PlaceOrderContainer</param>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task SaveDiscountUsageHistoryAsync(PlaceOrderContainer details, Order order)
{
if (details.AppliedDiscounts == null || !details.AppliedDiscounts.Any())
return;
foreach (var discount in details.AppliedDiscounts)
{
var d = await _discountService.GetDiscountByIdAsync(discount.Id);
if (d == null)
continue;
await _discountService.InsertDiscountUsageHistoryAsync(new DiscountUsageHistory
{
DiscountId = d.Id,
OrderId = order.Id,
CreatedOnUtc = DateTime.UtcNow
});
}
}
/// <summary>
/// Checks and save order status
/// </summary>
/// <param name="order">Order</param>
/// <param name="needOrderSave">Indicate if we need save order if nothing changed on the order status</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task CheckAndSaveOrderStatusAsync(Order order, bool needOrderSave)
{
ArgumentNullException.ThrowIfNull(order);
var completed = false;
var isOrderSaved = !needOrderSave;
if (order.PaymentStatus == PaymentStatus.Paid)
{
if (!order.PaidDateUtc.HasValue)
{
//ensure that paid date is set
order.PaidDateUtc = DateTime.UtcNow;
isOrderSaved = false;
}
if (order.ShippingStatus == ShippingStatus.ShippingNotRequired)
//shipping is not required
completed = true;
else
//shipping is required
completed = _orderSettings.CompleteOrderWhenDelivered
? order.ShippingStatus == ShippingStatus.Delivered
: order.ShippingStatus == ShippingStatus.Shipped || order.ShippingStatus == ShippingStatus.Delivered;
}
switch (order.OrderStatus)
{
case OrderStatus.Pending:
if (order.PaymentStatus == PaymentStatus.Authorized ||
order.PaymentStatus == PaymentStatus.Paid)
{
await SetOrderStatusAsync(order, OrderStatus.Processing, !completed);
isOrderSaved = true;
}
if (order.ShippingStatus == ShippingStatus.PartiallyShipped ||
order.ShippingStatus == ShippingStatus.Shipped ||
order.ShippingStatus == ShippingStatus.Delivered)
{
await SetOrderStatusAsync(order, OrderStatus.Processing, !completed);
isOrderSaved = true;
}
break;
//is order complete?
case OrderStatus.Cancelled:
case OrderStatus.Complete:
if (!isOrderSaved)
await _orderService.UpdateOrderAsync(order);
return;
}
if (completed)
{
await SetOrderStatusAsync(order, OrderStatus.Complete, true);
isOrderSaved = true;
}
if (!isOrderSaved)
await _orderService.UpdateOrderAsync(order);
}
#endregion
#region Methods
/// <summary>
/// Checks order status
/// </summary>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task CheckOrderStatusAsync(Order order)
{
await CheckAndSaveOrderStatusAsync(order, false);
}
/// <summary>
/// Places an order
/// </summary>
/// <param name="processPaymentRequest">Process payment request</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the place order result
/// </returns>
public virtual async Task<PlaceOrderResult> PlaceOrderAsync(ProcessPaymentRequest processPaymentRequest)
{
ArgumentNullException.ThrowIfNull(processPaymentRequest);
if (processPaymentRequest.OrderGuid == Guid.Empty)
throw new Exception("Order GUID is not generated");
//prepare order details
var details = await PreparePlaceOrderDetailsAsync(processPaymentRequest);
async Task<PlaceOrderResult> placeOrder(PlaceOrderContainer placeOrderContainer)
{
var result = new PlaceOrderResult();
try
{
var processPaymentResult =
await GetProcessPaymentResultAsync(processPaymentRequest, placeOrderContainer)
?? throw new NopException("processPaymentResult is not available");
if (processPaymentResult.Success)
{
var order = await SaveOrderDetailsAsync(processPaymentRequest, processPaymentResult,
placeOrderContainer);
result.PlacedOrder = order;
//move shopping cart items to order items
await MoveShoppingCartItemsToOrderItemsAsync(placeOrderContainer, order);
//discount usage history
await SaveDiscountUsageHistoryAsync(placeOrderContainer, order);
//gift card usage history
await SaveGiftCardUsageHistoryAsync(placeOrderContainer, order);
//recurring orders
if (placeOrderContainer.IsRecurringShoppingCart)
await CreateFirstRecurringPaymentAsync(processPaymentRequest, order);
//notifications
await SendNotificationsAndSaveNotesAsync(order);
//reset checkout data
await _customerService.ResetCheckoutDataAsync(placeOrderContainer.Customer,
processPaymentRequest.StoreId, clearCouponCodes: true, clearCheckoutAttributes: true);
await _customerActivityService.InsertActivityAsync("PublicStore.PlaceOrder",
string.Format(await _localizationService.GetResourceAsync("ActivityLog.PublicStore.PlaceOrder"),
order.Id), order);
//raise event
await _eventPublisher.PublishAsync(new OrderPlacedEvent(order));
//check order status
await CheckOrderStatusAsync(order);
if (order.PaymentStatus == PaymentStatus.Paid)
await ProcessOrderPaidAsync(order);
}
else
foreach (var paymentError in processPaymentResult.Errors)
result.AddError(string.Format(
await _localizationService.GetResourceAsync("Checkout.PaymentError"), paymentError));
}
catch (Exception exc)
{
await _logger.ErrorAsync(exc.Message, exc);
result.AddError(exc.Message);
}
if (result.Success)
return result;
//log errors
var logError = result.Errors.Aggregate("Error while placing order. ",
(current, next) => $"{current}Error {result.Errors.IndexOf(next) + 1}: {next}. ");
var customer = await _customerService.GetCustomerByIdAsync(processPaymentRequest.CustomerId);
await _logger.ErrorAsync(logError, customer: customer);
return result;
}
if (!_orderSettings.PlaceOrderWithLock)
return await placeOrder(details);
PlaceOrderResult result;
var resource = details.Customer.Id.ToString();
//the named mutex helps to avoid creating the same order in different threads,
//and does not decrease performance significantly, because the code is blocked only for the specific cart.
//you should be very careful, mutexes cannot be used in with the await operation
//we can't use semaphore here, because it produces PlatformNotSupportedException exception on UNIX based systems
using var mutex = new Mutex(false, resource);
mutex.WaitOne();
try
{
var cacheKey = _staticCacheManager.PrepareKey(NopOrderDefaults.OrderWithLockCacheKey, resource);
cacheKey.CacheTime = _orderSettings.MinimumOrderPlacementInterval;
var exist = _staticCacheManager.Get(cacheKey, () => false);
if (exist)
{
result = new PlaceOrderResult();
result.Errors.Add(_localizationService.GetResourceAsync("Checkout.MinOrderPlacementInterval").Result);
}
else
{
result = placeOrder(details).Result;
if (result.Success)
_staticCacheManager.SetAsync(cacheKey, true).Wait();
}
}
finally
{
mutex.ReleaseMutex();
}
return result;
}
/// <summary>
/// Update order totals
/// </summary>
/// <param name="updateOrderParameters">Parameters for the updating order</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task UpdateOrderTotalsAsync(UpdateOrderParameters updateOrderParameters)
{
if (!_orderSettings.AutoUpdateOrderTotalsOnEditingOrder)
return;
var updatedOrder = updateOrderParameters.UpdatedOrder;
var updatedOrderItem = updateOrderParameters.UpdatedOrderItem;
//restore shopping cart from order items
var (restoredCart, updatedShoppingCartItem) = await restoreShoppingCartAsync(updatedOrder, updatedOrderItem.Id);
var itemDeleted = updatedShoppingCartItem is null;
//validate shopping cart for warnings
updateOrderParameters.Warnings.AddRange(await _shoppingCartService.GetShoppingCartWarningsAsync(restoredCart, string.Empty, false));
var customer = await _customerService.GetCustomerByIdAsync(updatedOrder.CustomerId);
if (!itemDeleted)
{
var product = await _productService.GetProductByIdAsync(updatedShoppingCartItem.ProductId);
var store = await _storeService.GetStoreByIdAsync(updatedShoppingCartItem.StoreId);
updateOrderParameters.Warnings.AddRange(await _shoppingCartService.GetShoppingCartItemWarningsAsync(customer, updatedShoppingCartItem.ShoppingCartType,
product, updatedOrder.StoreId, updatedShoppingCartItem.AttributesXml, updatedShoppingCartItem.CustomerEnteredPrice,
updatedShoppingCartItem.RentalStartDateUtc, updatedShoppingCartItem.RentalEndDateUtc, updatedShoppingCartItem.Quantity, false, updatedShoppingCartItem.Id));
updatedOrderItem.ItemWeight = await _shippingService.GetShoppingCartItemWeightAsync(updatedShoppingCartItem);
updatedOrderItem.OriginalProductCost = await _priceCalculationService.GetProductCostAsync(product, updatedShoppingCartItem.AttributesXml);
updatedOrderItem.AttributeDescription = await _productAttributeFormatter.FormatAttributesAsync(product,
updatedShoppingCartItem.AttributesXml, customer, store);
//gift cards
await AddGiftCardsAsync(product, updatedShoppingCartItem.AttributesXml, updatedShoppingCartItem.Quantity, updatedOrderItem, updatedOrderItem.UnitPriceExclTax);
}
await _orderTotalCalculationService.UpdateOrderTotalsAsync(updateOrderParameters, restoredCart);
if (!itemDeleted)
await _orderService.UpdateOrderItemAsync(updatedOrderItem);
if (updateOrderParameters.PickupPoint != null)
{
updatedOrder.PickupInStore = true;
var pickupAddress = new Address
{
Address1 = updateOrderParameters.PickupPoint.Address,
City = updateOrderParameters.PickupPoint.City,
County = updateOrderParameters.PickupPoint.County,
CountryId = (await _countryService.GetCountryByTwoLetterIsoCodeAsync(updateOrderParameters.PickupPoint.CountryCode))?.Id,
ZipPostalCode = updateOrderParameters.PickupPoint.ZipPostalCode,
CreatedOnUtc = DateTime.UtcNow
};
await _addressService.InsertAddressAsync(pickupAddress);
updatedOrder.PickupAddressId = pickupAddress.Id;
var shippingMethod = !string.IsNullOrEmpty(updateOrderParameters.PickupPoint.Name) ?
string.Format(await _localizationService.GetResourceAsync("Checkout.PickupPoints.Name"), updateOrderParameters.PickupPoint.Name) :
await _localizationService.GetResourceAsync("Checkout.PickupPoints.NullName");
updatedOrder.ShippingMethod = shippingMethod;
updatedOrder.ShippingRateComputationMethodSystemName = updateOrderParameters.PickupPoint.ProviderSystemName;
}
await CheckAndSaveOrderStatusAsync(updatedOrder, true);
//discount usage history
var discountUsageHistoryForOrder = await _discountService.GetAllDiscountUsageHistoryAsync(null, customer.Id, updatedOrder.Id);
foreach (var discount in updateOrderParameters.AppliedDiscounts)
{
if (discountUsageHistoryForOrder.Any(history => history.DiscountId == discount.Id))
continue;
var d = await _discountService.GetDiscountByIdAsync(discount.Id);
if (d != null)
await _discountService.InsertDiscountUsageHistoryAsync(new DiscountUsageHistory
{
DiscountId = d.Id,
OrderId = updatedOrder.Id,
CreatedOnUtc = DateTime.UtcNow
});
}
async Task<(List<ShoppingCartItem> restoredCart, ShoppingCartItem updatedShoppingCartItem)> restoreShoppingCartAsync(Order order, int updatedOrderItemId)
{
ArgumentNullException.ThrowIfNull(order);
var cart = (await _orderService.GetOrderItemsAsync(order.Id)).Select(item => new ShoppingCartItem
{
Id = item.Id,
AttributesXml = item.AttributesXml,
CustomerId = order.CustomerId,
ProductId = item.ProductId,
Quantity = item.Id == updatedOrderItemId ? updateOrderParameters.Quantity : item.Quantity,
RentalEndDateUtc = item.RentalEndDateUtc,
RentalStartDateUtc = item.RentalStartDateUtc,
ShoppingCartType = ShoppingCartType.ShoppingCart,
StoreId = order.StoreId
}).ToList();
//get shopping cart item which has been updated
var cartItem = cart.FirstOrDefault(shoppingCartItem => shoppingCartItem.Id == updatedOrderItemId);
return (cart, cartItem);
}
}
/// <summary>
/// Deletes an order
/// </summary>
/// <param name="order">The order</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task DeleteOrderAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
//check whether the order wasn't cancelled before
//if it already was cancelled, then there's no need to make the following adjustments
//(such as reward points, inventory, recurring payments)
//they already was done when cancelling the order
if (order.OrderStatus != OrderStatus.Cancelled)
{
//return (add) back redeemded reward points
await ReturnBackRedeemedRewardPointsAsync(order);
//reduce (cancel) back reward points (previously awarded for this order)
await ReduceRewardPointsAsync(order);
//cancel recurring payments
var recurringPayments = await _orderService.SearchRecurringPaymentsAsync(initialOrderId: order.Id);
foreach (var rp in recurringPayments)
await CancelRecurringPaymentAsync(rp);
//Adjust inventory for already shipped shipments
//only products with "use multiple warehouses"
await ReverseBookedInventoryAsync(order, string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.DeleteOrder"), order.Id));
//Adjust inventory
await ReturnOrderStockAsync(order, string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.DeleteOrder"), order.Id));
}
//deactivate gift cards
if (_orderSettings.DeactivateGiftCardsAfterDeletingOrder)
await SetActivatedValueForPurchasedGiftCardsAsync(order, false);
//add a note
await AddOrderNoteAsync(order, "Order has been deleted");
//now delete an order
await _orderService.DeleteOrderAsync(order);
}
/// <summary>
/// Process next recurring payment
/// </summary>
/// <param name="recurringPayment">Recurring payment</param>
/// <param name="paymentResult">Process payment result (info about last payment for automatic recurring payments)</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the collection of errors
/// </returns>
public virtual async Task<IEnumerable<string>> ProcessNextRecurringPaymentAsync(RecurringPayment recurringPayment, ProcessPaymentResult paymentResult = null)
{
ArgumentNullException.ThrowIfNull(recurringPayment);
try
{
if (!recurringPayment.IsActive)
throw new NopException("Recurring payment is not active");
var initialOrder = await _orderService.GetOrderByIdAsync(recurringPayment.InitialOrderId)
?? throw new NopException("Initial order could not be loaded");
var customer = await _customerService.GetCustomerByIdAsync(initialOrder.CustomerId)
?? throw new NopException("Customer could not be loaded");
if (await GetNextPaymentDateAsync(recurringPayment) is null)
throw new NopException("Next payment date could not be calculated");
//payment info
var processPaymentRequest = new ProcessPaymentRequest
{
StoreId = initialOrder.StoreId,
CustomerId = customer.Id,
OrderGuid = Guid.NewGuid(),
InitialOrder = initialOrder,
RecurringCycleLength = recurringPayment.CycleLength,
RecurringCyclePeriod = recurringPayment.CyclePeriod,
RecurringTotalCycles = recurringPayment.TotalCycles
};
processPaymentRequest.CustomValues.FillByXml(initialOrder.CustomValuesXml);
//prepare order details
var details = await PrepareRecurringOrderDetailsAsync(processPaymentRequest);
ProcessPaymentResult processPaymentResult;
//skip payment workflow if order total equals zero
var skipPaymentWorkflow = details.OrderTotal == decimal.Zero;
if (!skipPaymentWorkflow)
{
var paymentMethod = await _paymentPluginManager
.LoadPluginBySystemNameAsync(processPaymentRequest.PaymentMethodSystemName, customer, initialOrder.StoreId)
?? throw new NopException("Payment method couldn't be loaded");
if (!_paymentPluginManager.IsPluginActive(paymentMethod))
throw new NopException("Payment method is not active");
//Old credit card info
if (details.InitialOrder.AllowStoringCreditCardNumber)
{
processPaymentRequest.CreditCardType = _encryptionService.DecryptText(details.InitialOrder.CardType);
processPaymentRequest.CreditCardName = _encryptionService.DecryptText(details.InitialOrder.CardName);
processPaymentRequest.CreditCardNumber = _encryptionService.DecryptText(details.InitialOrder.CardNumber);
processPaymentRequest.CreditCardCvv2 = _encryptionService.DecryptText(details.InitialOrder.CardCvv2);
try
{
processPaymentRequest.CreditCardExpireMonth = Convert.ToInt32(_encryptionService.DecryptText(details.InitialOrder.CardExpirationMonth));
processPaymentRequest.CreditCardExpireYear = Convert.ToInt32(_encryptionService.DecryptText(details.InitialOrder.CardExpirationYear));
}
catch
{
// ignored
}
}
//payment type
processPaymentResult = (await _paymentService.GetRecurringPaymentTypeAsync(processPaymentRequest.PaymentMethodSystemName)) switch
{
RecurringPaymentType.NotSupported => throw new NopException("Recurring payments are not supported by selected payment method"),
RecurringPaymentType.Manual => await _paymentService.ProcessRecurringPaymentAsync(processPaymentRequest),
//payment is processed on payment gateway site, info about last transaction in paymentResult parameter
RecurringPaymentType.Automatic => paymentResult ?? new ProcessPaymentResult(),
_ => throw new NopException("Not supported recurring payment type"),
};
}
else
processPaymentResult = paymentResult ?? new ProcessPaymentResult { NewPaymentStatus = PaymentStatus.Paid };
if (processPaymentResult == null)
throw new NopException("processPaymentResult is not available");
if (processPaymentResult.Success)
{
//save order details
var order = await SaveOrderDetailsAsync(processPaymentRequest, processPaymentResult, details);
foreach (var orderItem in await _orderService.GetOrderItemsAsync(details.InitialOrder.Id))
{
//save item
var newOrderItem = new OrderItem
{
OrderItemGuid = Guid.NewGuid(),
OrderId = order.Id,
ProductId = orderItem.ProductId,
UnitPriceInclTax = orderItem.UnitPriceInclTax,
UnitPriceExclTax = orderItem.UnitPriceExclTax,
PriceInclTax = orderItem.PriceInclTax,
PriceExclTax = orderItem.PriceExclTax,
OriginalProductCost = orderItem.OriginalProductCost,
AttributeDescription = orderItem.AttributeDescription,
AttributesXml = orderItem.AttributesXml,
Quantity = orderItem.Quantity,
DiscountAmountInclTax = orderItem.DiscountAmountInclTax,
DiscountAmountExclTax = orderItem.DiscountAmountExclTax,
DownloadCount = 0,
IsDownloadActivated = false,
LicenseDownloadId = 0,
ItemWeight = orderItem.ItemWeight,
RentalStartDateUtc = orderItem.RentalStartDateUtc,
RentalEndDateUtc = orderItem.RentalEndDateUtc
};
await _orderService.InsertOrderItemAsync(newOrderItem);
var product = await _productService.GetProductByIdAsync(orderItem.ProductId);
//gift cards
await AddGiftCardsAsync(product, orderItem.AttributesXml, orderItem.Quantity, newOrderItem, amount: orderItem.UnitPriceExclTax);
//inventory
await _productService.AdjustInventoryAsync(product, -orderItem.Quantity, orderItem.AttributesXml,
string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.PlaceOrder"), order.Id));
}
//discount usage history
await SaveDiscountUsageHistoryAsync(details, order);
//notifications
await SendNotificationsAndSaveNotesAsync(order);
//raise event
await _eventPublisher.PublishAsync(new OrderPlacedEvent(order));
//check order status
await CheckOrderStatusAsync(order);
if (order.PaymentStatus == PaymentStatus.Paid)
await ProcessOrderPaidAsync(order);
//last payment succeeded
recurringPayment.LastPaymentFailed = false;
//next recurring payment
await _orderService.InsertRecurringPaymentHistoryAsync(new RecurringPaymentHistory
{
RecurringPaymentId = recurringPayment.Id,
CreatedOnUtc = DateTime.UtcNow,
OrderId = order.Id
});
await _orderService.UpdateRecurringPaymentAsync(recurringPayment);
return new List<string>();
}
//log errors
var logError = processPaymentResult.Errors.Aggregate("Error while processing recurring order. ",
(current, next) => $"{current}Error {processPaymentResult.Errors.IndexOf(next) + 1}: {next}. ");
await _logger.ErrorAsync(logError, customer: customer);
if (!processPaymentResult.RecurringPaymentFailed)
return processPaymentResult.Errors;
//set flag that last payment failed
recurringPayment.LastPaymentFailed = true;
await _orderService.UpdateRecurringPaymentAsync(recurringPayment);
if (_paymentSettings.CancelRecurringPaymentsAfterFailedPayment)
{
//cancel recurring payment
var errors = (await CancelRecurringPaymentAsync(recurringPayment)).ToList();
foreach (var error in errors)
{
await _logger.ErrorAsync(error);
}
//notify a customer about cancelled payment
await _workflowMessageService.SendRecurringPaymentCancelledCustomerNotificationAsync(recurringPayment, initialOrder.CustomerLanguageId);
}
else
//notify a customer about failed payment
await _workflowMessageService.SendRecurringPaymentFailedCustomerNotificationAsync(recurringPayment, initialOrder.CustomerLanguageId);
return processPaymentResult.Errors;
}
catch (Exception exc)
{
await _logger.ErrorAsync($"Error while processing recurring order. {exc.Message}", exc);
throw;
}
}
/// <summary>
/// Cancels a recurring payment
/// </summary>
/// <param name="recurringPayment">Recurring payment</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task<IList<string>> CancelRecurringPaymentAsync(RecurringPayment recurringPayment)
{
ArgumentNullException.ThrowIfNull(recurringPayment);
var initialOrder = await _orderService.GetOrderByIdAsync(recurringPayment.InitialOrderId);
if (initialOrder == null)
return new List<string> { "Initial order could not be loaded" };
var request = new CancelRecurringPaymentRequest();
CancelRecurringPaymentResult result = null;
try
{
request.Order = initialOrder;
result = await _paymentService.CancelRecurringPaymentAsync(request);
if (result.Success)
{
//update recurring payment
recurringPayment.IsActive = false;
await _orderService.UpdateRecurringPaymentAsync(recurringPayment);
//add a note
await _orderService.InsertOrderNoteAsync(new OrderNote
{
OrderId = initialOrder.Id,
Note = "Recurring payment has been cancelled",
DisplayToCustomer = false,
CreatedOnUtc = DateTime.UtcNow
});
//notify a store owner
await _workflowMessageService
.SendRecurringPaymentCancelledStoreOwnerNotificationAsync(recurringPayment,
_localizationSettings.DefaultAdminLanguageId);
}
}
catch (Exception exc)
{
result ??= new CancelRecurringPaymentResult();
result.AddError($"Error: {exc.Message}. Full exception: {exc}");
}
//process errors
var error = string.Empty;
for (var i = 0; i < result.Errors.Count; i++)
{
error += $"Error {i}: {result.Errors[i]}";
if (i != result.Errors.Count - 1)
error += ". ";
}
if (string.IsNullOrEmpty(error))
return result.Errors;
//add a note
await _orderService.InsertOrderNoteAsync(new OrderNote
{
OrderId = initialOrder.Id,
Note = $"Unable to cancel recurring payment. {error}",
DisplayToCustomer = false,
CreatedOnUtc = DateTime.UtcNow
});
//log it
var logError = $"Error cancelling recurring payment. Order #{initialOrder.Id}. Error: {error}";
await _logger.InsertLogAsync(LogLevel.Error, logError, logError);
return result.Errors;
}
/// <summary>
/// Gets a value indicating whether a customer can cancel recurring payment
/// </summary>
/// <param name="customerToValidate">Customer</param>
/// <param name="recurringPayment">Recurring Payment</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the value indicating whether a customer can cancel recurring payment
/// </returns>
public virtual async Task<bool> CanCancelRecurringPaymentAsync(Customer customerToValidate, RecurringPayment recurringPayment)
{
if (recurringPayment is null)
return false;
if (customerToValidate is null)
return false;
var initialOrder = await _orderService.GetOrderByIdAsync(recurringPayment.InitialOrderId);
if (initialOrder is null)
return false;
var customer = await _customerService.GetCustomerByIdAsync(initialOrder.CustomerId);
if (customer is null)
return false;
if (initialOrder.OrderStatus == OrderStatus.Cancelled)
return false;
if (!await _customerService.IsAdminAsync(customerToValidate))
if (customer.Id != customerToValidate.Id)
return false;
if (await GetNextPaymentDateAsync(recurringPayment) is null)
return false;
return true;
}
/// <summary>
/// Gets a value indicating whether a customer can retry last failed recurring payment
/// </summary>
/// <param name="customer">Customer</param>
/// <param name="recurringPayment">Recurring Payment</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains true if a customer can retry payment; otherwise false
/// </returns>
public virtual async Task<bool> CanRetryLastRecurringPaymentAsync(Customer customer, RecurringPayment recurringPayment)
{
if (recurringPayment == null || customer == null)
return false;
var order = await _orderService.GetOrderByIdAsync(recurringPayment.InitialOrderId);
if (order is null)
return false;
var orderCustomer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
if (order.OrderStatus == OrderStatus.Cancelled)
return false;
if (!recurringPayment.LastPaymentFailed || await _paymentService.GetRecurringPaymentTypeAsync(order.PaymentMethodSystemName) != RecurringPaymentType.Manual)
return false;
if (orderCustomer == null || (!await _customerService.IsAdminAsync(customer) && orderCustomer.Id != customer.Id))
return false;
return true;
}
/// <summary>
/// Send a shipment
/// </summary>
/// <param name="shipment">Shipment</param>
/// <param name="notifyCustomer">True to notify customer</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task ShipAsync(Shipment shipment, bool notifyCustomer)
{
ArgumentNullException.ThrowIfNull(shipment);
var order = await _orderService.GetOrderByIdAsync(shipment.OrderId) ?? throw new Exception("Order cannot be loaded");
if (order.PickupInStore)
throw new Exception("This shipment is can't be shipped. The order has been placed with 'pickup in store' shipping option.");
if (shipment.ShippedDateUtc.HasValue)
throw new Exception("This shipment is already shipped");
shipment.ShippedDateUtc = DateTime.UtcNow;
await _shipmentService.UpdateShipmentAsync(shipment);
//process products with "Multiple warehouse" support enabled
await BookReservedInventoryAsync(shipment, string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.Ship"), shipment.OrderId));
//check whether we have more items to ship
if (await _orderService.HasItemsToAddToShipmentAsync(order) || await _orderService.HasItemsToShipAsync(order))
order.ShippingStatusId = (int)ShippingStatus.PartiallyShipped;
else
order.ShippingStatusId = (int)ShippingStatus.Shipped;
await _orderService.UpdateOrderAsync(order);
//add a note
await AddOrderNoteAsync(order, $"Shipment# {shipment.Id} has been sent");
if (notifyCustomer)
{
//notify customer
var queuedEmailIds = await _workflowMessageService.SendShipmentSentCustomerNotificationAsync(shipment, order.CustomerLanguageId);
if (queuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Shipped\" email (to customer) has been queued. Queued email identifiers: {string.Join(", ", queuedEmailIds)}.");
}
//event
await _eventPublisher.PublishShipmentSentAsync(shipment);
//check order status
await CheckOrderStatusAsync(order);
}
/// <summary>
/// Marks a shipment as ready for pickup
/// </summary>
/// <param name="shipment">Shipment</param>
/// <param name="notifyCustomer">True to notify customer</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task ReadyForPickupAsync(Shipment shipment, bool notifyCustomer)
{
ArgumentNullException.ThrowIfNull(shipment);
var order = await _orderService.GetOrderByIdAsync(shipment.OrderId) ?? throw new Exception("Order cannot be loaded");
if (!order.PickupInStore)
throw new Exception("This shipment is can't be marked as 'ready for pickup'. The order has been placed without 'pickup in store' shipping option.");
if (shipment.ReadyForPickupDateUtc.HasValue)
throw new Exception("This shipment is already marked as 'ready for pickup'");
shipment.ReadyForPickupDateUtc = DateTime.UtcNow;
await _shipmentService.UpdateShipmentAsync(shipment);
await AddOrderNoteAsync(order, $"Shipment# {shipment.Id} has been ready for pickup");
if (notifyCustomer)
{
var queuedEmailIds = await _workflowMessageService.SendShipmentReadyForPickupNotificationAsync(shipment, order.CustomerLanguageId);
if (queuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Ready for pickup\" email (to customer) has been queued. Queued email identifiers: {string.Join(", ", queuedEmailIds)}.");
}
await _eventPublisher.PublishShipmentReadyForPickupAsync(shipment);
}
/// <summary>
/// Marks a shipment as delivered
/// </summary>
/// <param name="shipment">Shipment</param>
/// <param name="notifyCustomer">True to notify customer</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task DeliverAsync(Shipment shipment, bool notifyCustomer)
{
ArgumentNullException.ThrowIfNull(shipment);
var order = await _orderService.GetOrderByIdAsync(shipment.OrderId) ?? throw new Exception("Order cannot be loaded");
if (!order.PickupInStore && !shipment.ShippedDateUtc.HasValue)
throw new Exception("This shipment is not shipped yet");
if (order.PickupInStore && !shipment.ReadyForPickupDateUtc.HasValue)
throw new Exception("This shipment is not yet ready for pickup");
if (shipment.DeliveryDateUtc.HasValue)
throw new Exception("This shipment is already delivered");
shipment.DeliveryDateUtc = DateTime.UtcNow;
await _shipmentService.UpdateShipmentAsync(shipment);
if (!await _orderService.HasItemsToAddToShipmentAsync(order) &&
!await _orderService.HasItemsToShipAsync(order) &&
!await _orderService.HasItemsToReadyForPickupAsync(order) &&
!await _orderService.HasItemsToDeliverAsync(order))
{
order.ShippingStatusId = (int)ShippingStatus.Delivered;
await _orderService.UpdateOrderAsync(order);
}
//add a note
await AddOrderNoteAsync(order, $"Shipment# {shipment.Id} has been delivered");
if (order.PickupInStore)
{
// Shipment has been collected by customer.
// We must process products with "Multiple warehouse" support enabled.
await BookReservedInventoryAsync(shipment, string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.ReadyForPickupByCustomer"), shipment.OrderId));
}
if (notifyCustomer)
{
//send email notification
var queuedEmailIds = await _workflowMessageService.SendShipmentDeliveredCustomerNotificationAsync(shipment, order.CustomerLanguageId);
if (queuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Delivered\" email (to customer) has been queued. Queued email identifiers: {string.Join(", ", queuedEmailIds)}.");
}
//event
await _eventPublisher.PublishShipmentDeliveredAsync(shipment);
//check order status
await CheckOrderStatusAsync(order);
}
/// <summary>
/// Gets a value indicating whether cancel is allowed
/// </summary>
/// <param name="order">Order</param>
/// <returns>A value indicating whether cancel is allowed</returns>
public virtual bool CanCancelOrder(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (order.OrderStatus == OrderStatus.Cancelled)
return false;
return true;
}
/// <summary>
/// Cancels order
/// </summary>
/// <param name="order">Order</param>
/// <param name="notifyCustomer">True to notify customer</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task CancelOrderAsync(Order order, bool notifyCustomer)
{
ArgumentNullException.ThrowIfNull(order);
if (!CanCancelOrder(order))
throw new NopException("Cannot do cancel for order.");
//cancel order
await SetOrderStatusAsync(order, OrderStatus.Cancelled, notifyCustomer);
//notify store owner
var currentCustomer = await _workContext.GetCurrentCustomerAsync();
if (order.CustomerId == currentCustomer.Id)
await _workflowMessageService.SendOrderCancelledStoreOwnerNotificationAsync(order, _localizationSettings.DefaultAdminLanguageId);
//add a note
await AddOrderNoteAsync(order, "Order has been cancelled");
//return (add) back redeemded reward points
await ReturnBackRedeemedRewardPointsAsync(order);
//delete gift card usage history
if (_orderSettings.DeleteGiftCardUsageHistory)
await _giftCardService.DeleteGiftCardUsageHistoryAsync(order);
//cancel recurring payments
var recurringPayments = await _orderService.SearchRecurringPaymentsAsync(initialOrderId: order.Id);
foreach (var rp in recurringPayments)
await CancelRecurringPaymentAsync(rp);
//Adjust inventory for already shipped shipments
//only products with "use multiple warehouses"
await ReverseBookedInventoryAsync(order, string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.CancelOrder"), order.Id));
//Adjust inventory
await ReturnOrderStockAsync(order, string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.CancelOrder"), order.Id));
}
/// <summary>
/// Gets a value indicating whether order can be marked as authorized
/// </summary>
/// <param name="order">Order</param>
/// <returns>A value indicating whether order can be marked as authorized</returns>
public virtual bool CanMarkOrderAsAuthorized(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (order.OrderStatus == OrderStatus.Cancelled)
return false;
if (order.PaymentStatus == PaymentStatus.Pending)
return true;
return false;
}
/// <summary>
/// Marks order as authorized
/// </summary>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task MarkAsAuthorizedAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
order.PaymentStatusId = (int)PaymentStatus.Authorized;
await _orderService.UpdateOrderAsync(order);
//add a note
await AddOrderNoteAsync(order, "Order has been marked as authorized");
await _eventPublisher.PublishAsync(new OrderAuthorizedEvent(order));
//check order status
await CheckOrderStatusAsync(order);
}
/// <summary>
/// Gets a value indicating whether capture from admin panel is allowed
/// </summary>
/// <param name="order">Order</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains a value indicating whether capture from admin panel is allowed
/// </returns>
public virtual async Task<bool> CanCaptureAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (order.OrderStatus == OrderStatus.Cancelled ||
order.OrderStatus == OrderStatus.Pending)
return false;
if (order.PaymentStatus == PaymentStatus.Authorized &&
await _paymentService.SupportCaptureAsync(order.PaymentMethodSystemName))
return true;
return false;
}
/// <summary>
/// Capture an order (from admin panel)
/// </summary>
/// <param name="order">Order</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains a list of errors; empty list if no errors
/// </returns>
public virtual async Task<IList<string>> CaptureAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (!await CanCaptureAsync(order))
throw new NopException("Cannot do capture for order.");
var request = new CapturePaymentRequest();
CapturePaymentResult result = null;
try
{
//old info from placing order
request.Order = order;
result = await _paymentService.CaptureAsync(request);
if (result.Success)
{
var paidDate = order.PaidDateUtc;
if (result.NewPaymentStatus == PaymentStatus.Paid)
paidDate = DateTime.UtcNow;
order.CaptureTransactionId = result.CaptureTransactionId;
order.CaptureTransactionResult = result.CaptureTransactionResult;
order.PaymentStatus = result.NewPaymentStatus;
order.PaidDateUtc = paidDate;
await _orderService.UpdateOrderAsync(order);
//add a note
await AddOrderNoteAsync(order, "Order has been captured");
await CheckOrderStatusAsync(order);
if (order.PaymentStatus == PaymentStatus.Paid)
await ProcessOrderPaidAsync(order);
}
}
catch (Exception exc)
{
result ??= new CapturePaymentResult();
result.AddError($"Error: {exc.Message}. Full exception: {exc}");
}
//process errors
var error = string.Empty;
for (var i = 0; i < result.Errors.Count; i++)
{
error += $"Error {i}: {result.Errors[i]}";
if (i != result.Errors.Count - 1)
error += ". ";
}
if (string.IsNullOrEmpty(error))
return result.Errors;
//add a note
await AddOrderNoteAsync(order, $"Unable to capture order. {error}");
//log it
var logError = $"Error capturing order #{order.Id}. Error: {error}";
await _logger.InsertLogAsync(LogLevel.Error, logError, logError);
return result.Errors;
}
/// <summary>
/// Gets a value indicating whether order can be marked as paid
/// </summary>
/// <param name="order">Order</param>
/// <returns>A value indicating whether order can be marked as paid</returns>
public virtual bool CanMarkOrderAsPaid(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (order.OrderStatus == OrderStatus.Cancelled)
return false;
if (order.PaymentStatus == PaymentStatus.Paid ||
order.PaymentStatus == PaymentStatus.Refunded ||
order.PaymentStatus == PaymentStatus.Voided)
return false;
return true;
}
/// <summary>
/// Marks order as paid
/// </summary>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task MarkOrderAsPaidAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (!CanMarkOrderAsPaid(order))
throw new NopException("You can't mark this order as paid");
order.PaymentStatusId = (int)PaymentStatus.Paid;
order.PaidDateUtc = DateTime.UtcNow;
await _orderService.UpdateOrderAsync(order);
//add a note
await AddOrderNoteAsync(order, "Order has been marked as paid");
await CheckOrderStatusAsync(order);
if (order.PaymentStatus == PaymentStatus.Paid)
await ProcessOrderPaidAsync(order);
}
/// <summary>
/// Gets a value indicating whether refund from admin panel is allowed
/// </summary>
/// <param name="order">Order</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains a value indicating whether refund from admin panel is allowed
/// </returns>
public virtual async Task<bool> CanRefundAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (order.OrderTotal == decimal.Zero)
return false;
//refund cannot be made if previously a partial refund has been already done. only other partial refund can be made in this case
if (order.RefundedAmount > decimal.Zero)
return false;
//uncomment the lines below in order to disallow this operation for cancelled orders
//if (order.OrderStatus == OrderStatus.Cancelled)
// return false;
if (order.PaymentStatus == PaymentStatus.Paid &&
await _paymentService.SupportRefundAsync(order.PaymentMethodSystemName))
return true;
return false;
}
/// <summary>
/// Refunds an order (from admin panel)
/// </summary>
/// <param name="order">Order</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains a list of errors; empty list if no errors
/// </returns>
public virtual async Task<IList<string>> RefundAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (!await CanRefundAsync(order))
throw new NopException("Cannot do refund for order.");
var request = new RefundPaymentRequest();
RefundPaymentResult result = null;
try
{
request.Order = order;
request.AmountToRefund = order.OrderTotal;
request.IsPartialRefund = false;
result = await _paymentService.RefundAsync(request);
if (result.Success)
{
//total amount refunded
var totalAmountRefunded = order.RefundedAmount + request.AmountToRefund;
//update order info
order.RefundedAmount = totalAmountRefunded;
order.PaymentStatus = result.NewPaymentStatus;
await _orderService.UpdateOrderAsync(order);
//add a note
await AddOrderNoteAsync(order, $"Order has been refunded. Amount = {request.AmountToRefund}");
//raise event
await _eventPublisher.PublishAsync(new OrderRefundedEvent(order, request.AmountToRefund));
//check order status
await CheckOrderStatusAsync(order);
//notifications
var orderRefundedStoreOwnerNotificationQueuedEmailIds = await _workflowMessageService.SendOrderRefundedStoreOwnerNotificationAsync(order, request.AmountToRefund, _localizationSettings.DefaultAdminLanguageId);
if (orderRefundedStoreOwnerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order refunded\" email (to store owner) has been queued. Queued email identifiers: {string.Join(", ", orderRefundedStoreOwnerNotificationQueuedEmailIds)}.");
var orderRefundedCustomerNotificationQueuedEmailIds = await _workflowMessageService.SendOrderRefundedCustomerNotificationAsync(order, request.AmountToRefund, order.CustomerLanguageId);
if (orderRefundedCustomerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order refunded\" email (to customer) has been queued. Queued email identifiers: {string.Join(", ", orderRefundedCustomerNotificationQueuedEmailIds)}.");
}
}
catch (Exception exc)
{
result ??= new RefundPaymentResult();
result.AddError($"Error: {exc.Message}. Full exception: {exc}");
}
//process errors
var error = string.Empty;
for (var i = 0; i < result.Errors.Count; i++)
{
error += $"Error {i}: {result.Errors[i]}";
if (i != result.Errors.Count - 1)
error += ". ";
}
if (string.IsNullOrEmpty(error))
return result.Errors;
//add a note
await AddOrderNoteAsync(order, $"Unable to refund order. {error}");
//log it
var logError = $"Error refunding order #{order.Id}. Error: {error}";
await _logger.InsertLogAsync(LogLevel.Error, logError, logError);
return result.Errors;
}
/// <summary>
/// Gets a value indicating whether order can be marked as refunded
/// </summary>
/// <param name="order">Order</param>
/// <returns>A value indicating whether order can be marked as refunded</returns>
public virtual bool CanRefundOffline(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (order.OrderTotal == decimal.Zero)
return false;
//refund cannot be made if previously a partial refund has been already done. only other partial refund can be made in this case
if (order.RefundedAmount > decimal.Zero)
return false;
//uncomment the lines below in order to disallow this operation for cancelled orders
//if (order.OrderStatus == OrderStatus.Cancelled)
// return false;
if (order.PaymentStatus == PaymentStatus.Paid)
return true;
return false;
}
/// <summary>
/// Refunds an order (offline)
/// </summary>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task RefundOfflineAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (!CanRefundOffline(order))
throw new NopException("You can't refund this order");
//amout to refund
var amountToRefund = order.OrderTotal;
//total amount refunded
var totalAmountRefunded = order.RefundedAmount + amountToRefund;
//update order info
order.RefundedAmount = totalAmountRefunded;
order.PaymentStatus = PaymentStatus.Refunded;
await _orderService.UpdateOrderAsync(order);
//add a note
await AddOrderNoteAsync(order, $"Order has been marked as refunded. Amount = {amountToRefund}");
//raise event
await _eventPublisher.PublishAsync(new OrderRefundedEvent(order, amountToRefund));
//check order status
await CheckOrderStatusAsync(order);
//notifications
var orderRefundedStoreOwnerNotificationQueuedEmailIds = await _workflowMessageService.SendOrderRefundedStoreOwnerNotificationAsync(order, amountToRefund, _localizationSettings.DefaultAdminLanguageId);
if (orderRefundedStoreOwnerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order refunded\" email (to store owner) has been queued. Queued email identifiers: {string.Join(", ", orderRefundedStoreOwnerNotificationQueuedEmailIds)}.");
var orderRefundedCustomerNotificationQueuedEmailIds = await _workflowMessageService.SendOrderRefundedCustomerNotificationAsync(order, amountToRefund, order.CustomerLanguageId);
if (orderRefundedCustomerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order refunded\" email (to customer) has been queued. Queued email identifiers: {string.Join(", ", orderRefundedCustomerNotificationQueuedEmailIds)}.");
}
/// <summary>
/// Gets a value indicating whether partial refund from admin panel is allowed
/// </summary>
/// <param name="order">Order</param>
/// <param name="amountToRefund">Amount to refund</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains a value indicating whether refund from admin panel is allowed
/// </returns>
public virtual async Task<bool> CanPartiallyRefundAsync(Order order, decimal amountToRefund)
{
ArgumentNullException.ThrowIfNull(order);
if (order.OrderTotal == decimal.Zero)
return false;
//uncomment the lines below in order to allow this operation for cancelled orders
//if (order.OrderStatus == OrderStatus.Cancelled)
// return false;
var canBeRefunded = order.OrderTotal - order.RefundedAmount;
if (canBeRefunded <= decimal.Zero)
return false;
if (amountToRefund > canBeRefunded)
return false;
if ((order.PaymentStatus == PaymentStatus.Paid ||
order.PaymentStatus == PaymentStatus.PartiallyRefunded) &&
await _paymentService.SupportPartiallyRefundAsync(order.PaymentMethodSystemName))
return true;
return false;
}
/// <summary>
/// Partially refunds an order (from admin panel)
/// </summary>
/// <param name="order">Order</param>
/// <param name="amountToRefund">Amount to refund</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains a list of errors; empty list if no errors
/// </returns>
public virtual async Task<IList<string>> PartiallyRefundAsync(Order order, decimal amountToRefund)
{
ArgumentNullException.ThrowIfNull(order);
if (!await CanPartiallyRefundAsync(order, amountToRefund))
throw new NopException("Cannot do partial refund for order.");
var request = new RefundPaymentRequest();
RefundPaymentResult result = null;
try
{
request.Order = order;
request.AmountToRefund = amountToRefund;
request.IsPartialRefund = true;
result = await _paymentService.RefundAsync(request);
if (result.Success)
{
//total amount refunded
var totalAmountRefunded = order.RefundedAmount + amountToRefund;
//update order info
order.RefundedAmount = totalAmountRefunded;
//mark payment status as 'Refunded' if the order total amount is fully refunded
order.PaymentStatus = order.OrderTotal == totalAmountRefunded && result.NewPaymentStatus == PaymentStatus.PartiallyRefunded ? PaymentStatus.Refunded : result.NewPaymentStatus;
await _orderService.UpdateOrderAsync(order);
//add a note
await AddOrderNoteAsync(order, $"Order has been partially refunded. Amount = {amountToRefund}");
//raise event
await _eventPublisher.PublishAsync(new OrderRefundedEvent(order, amountToRefund));
//check order status
await CheckOrderStatusAsync(order);
//notifications
var orderRefundedStoreOwnerNotificationQueuedEmailIds = await _workflowMessageService.SendOrderRefundedStoreOwnerNotificationAsync(order, amountToRefund, _localizationSettings.DefaultAdminLanguageId);
if (orderRefundedStoreOwnerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order refunded\" email (to store owner) has been queued. Queued email identifiers: {string.Join(", ", orderRefundedStoreOwnerNotificationQueuedEmailIds)}.");
var orderRefundedCustomerNotificationQueuedEmailIds = await _workflowMessageService.SendOrderRefundedCustomerNotificationAsync(order, amountToRefund, order.CustomerLanguageId);
if (orderRefundedCustomerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order refunded\" email (to customer) has been queued. Queued email identifiers: {string.Join(", ", orderRefundedCustomerNotificationQueuedEmailIds)}.");
}
}
catch (Exception exc)
{
result ??= new RefundPaymentResult();
result.AddError($"Error: {exc.Message}. Full exception: {exc}");
}
//process errors
var error = string.Empty;
for (var i = 0; i < result.Errors.Count; i++)
{
error += $"Error {i}: {result.Errors[i]}";
if (i != result.Errors.Count - 1)
error += ". ";
}
if (string.IsNullOrEmpty(error))
return result.Errors;
//add a note
await AddOrderNoteAsync(order, $"Unable to partially refund order. {error}");
//log it
var logError = $"Error refunding order #{order.Id}. Error: {error}";
await _logger.InsertLogAsync(LogLevel.Error, logError, logError);
return result.Errors;
}
/// <summary>
/// Gets a value indicating whether order can be marked as partially refunded
/// </summary>
/// <param name="order">Order</param>
/// <param name="amountToRefund">Amount to refund</param>
/// <returns>A value indicating whether order can be marked as partially refunded</returns>
public virtual bool CanPartiallyRefundOffline(Order order, decimal amountToRefund)
{
ArgumentNullException.ThrowIfNull(order);
if (order.OrderTotal == decimal.Zero)
return false;
//uncomment the lines below in order to allow this operation for cancelled orders
//if (order.OrderStatus == OrderStatus.Cancelled)
// return false;
var canBeRefunded = order.OrderTotal - order.RefundedAmount;
if (canBeRefunded <= decimal.Zero)
return false;
if (amountToRefund > canBeRefunded)
return false;
if (order.PaymentStatus == PaymentStatus.Paid ||
order.PaymentStatus == PaymentStatus.PartiallyRefunded)
return true;
return false;
}
/// <summary>
/// Partially refunds an order (offline)
/// </summary>
/// <param name="order">Order</param>
/// <param name="amountToRefund">Amount to refund</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task PartiallyRefundOfflineAsync(Order order, decimal amountToRefund)
{
ArgumentNullException.ThrowIfNull(order);
if (!CanPartiallyRefundOffline(order, amountToRefund))
throw new NopException("You can't partially refund (offline) this order");
//total amount refunded
var totalAmountRefunded = order.RefundedAmount + amountToRefund;
//update order info
order.RefundedAmount = totalAmountRefunded;
//mark payment status as 'Refunded' if the order total amount is fully refunded
order.PaymentStatus = order.OrderTotal == totalAmountRefunded ? PaymentStatus.Refunded : PaymentStatus.PartiallyRefunded;
await _orderService.UpdateOrderAsync(order);
//add a note
await AddOrderNoteAsync(order, $"Order has been marked as partially refunded. Amount = {amountToRefund}");
//raise event
await _eventPublisher.PublishAsync(new OrderRefundedEvent(order, amountToRefund));
//check order status
await CheckOrderStatusAsync(order);
//notifications
var orderRefundedStoreOwnerNotificationQueuedEmailIds = await _workflowMessageService.SendOrderRefundedStoreOwnerNotificationAsync(order, amountToRefund, _localizationSettings.DefaultAdminLanguageId);
if (orderRefundedStoreOwnerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order refunded\" email (to store owner) has been queued. Queued email identifiers: {string.Join(", ", orderRefundedStoreOwnerNotificationQueuedEmailIds)}.");
var orderRefundedCustomerNotificationQueuedEmailIds = await _workflowMessageService.SendOrderRefundedCustomerNotificationAsync(order, amountToRefund, order.CustomerLanguageId);
if (orderRefundedCustomerNotificationQueuedEmailIds.Any())
await AddOrderNoteAsync(order, $"\"Order refunded\" email (to customer) has been queued. Queued email identifiers: {string.Join(", ", orderRefundedCustomerNotificationQueuedEmailIds)}.");
}
/// <summary>
/// Gets a value indicating whether void from admin panel is allowed
/// </summary>
/// <param name="order">Order</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains a value indicating whether void from admin panel is allowed
/// </returns>
public virtual async Task<bool> CanVoidAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (order.OrderTotal == decimal.Zero)
return false;
//uncomment the lines below in order to allow this operation for cancelled orders
//if (order.OrderStatus == OrderStatus.Cancelled)
// return false;
if (order.PaymentStatus == PaymentStatus.Authorized &&
await _paymentService.SupportVoidAsync(order.PaymentMethodSystemName))
return true;
return false;
}
/// <summary>
/// Voids order (from admin panel)
/// </summary>
/// <param name="order">Order</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the voided orders
/// </returns>
public virtual async Task<IList<string>> VoidAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (!await CanVoidAsync(order))
throw new NopException("Cannot do void for order.");
var request = new VoidPaymentRequest();
VoidPaymentResult result = null;
try
{
request.Order = order;
result = await _paymentService.VoidAsync(request);
if (result.Success)
{
//update order info
order.PaymentStatus = result.NewPaymentStatus;
await _orderService.UpdateOrderAsync(order);
//add a note
await AddOrderNoteAsync(order, "Order has been voided");
//raise event
await _eventPublisher.PublishAsync(new OrderVoidedEvent(order));
//check order status
await CheckOrderStatusAsync(order);
}
}
catch (Exception exc)
{
result ??= new VoidPaymentResult();
result.AddError($"Error: {exc.Message}. Full exception: {exc}");
}
//process errors
var error = string.Empty;
for (var i = 0; i < result.Errors.Count; i++)
{
error += $"Error {i}: {result.Errors[i]}";
if (i != result.Errors.Count - 1)
error += ". ";
}
if (string.IsNullOrEmpty(error))
return result.Errors;
//add a note
await AddOrderNoteAsync(order, $"Unable to voiding order. {error}");
//log it
var logError = $"Error voiding order #{order.Id}. Error: {error}";
await _logger.InsertLogAsync(LogLevel.Error, logError, logError);
return result.Errors;
}
/// <summary>
/// Gets a value indicating whether order can be marked as voided
/// </summary>
/// <param name="order">Order</param>
/// <returns>A value indicating whether order can be marked as voided</returns>
public virtual bool CanVoidOffline(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (order.OrderTotal == decimal.Zero)
return false;
//uncomment the lines below in order to allow this operation for cancelled orders
//if (order.OrderStatus == OrderStatus.Cancelled)
// return false;
if (order.PaymentStatus == PaymentStatus.Authorized)
return true;
return false;
}
/// <summary>
/// Void order (offline)
/// </summary>
/// <param name="order">Order</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task VoidOfflineAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (!CanVoidOffline(order))
throw new NopException("You can't void this order");
order.PaymentStatusId = (int)PaymentStatus.Voided;
await _orderService.UpdateOrderAsync(order);
//add a note
await AddOrderNoteAsync(order, "Order has been marked as voided");
//raise event
await _eventPublisher.PublishAsync(new OrderVoidedEvent(order));
//check order status
await CheckOrderStatusAsync(order);
}
/// <summary>
/// Place order items in current user shopping cart.
/// </summary>
/// <param name="order">The order</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the warnings
/// </returns>
public virtual async Task<IList<string>> ReOrderAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
var warnings = new List<string>();
//move shopping cart items (if possible)
foreach (var orderItem in await _orderService.GetOrderItemsAsync(order.Id))
{
var product = await _productService.GetProductByIdAsync(orderItem.ProductId);
warnings.AddRange(await _shoppingCartService.AddToCartAsync(customer, product,
ShoppingCartType.ShoppingCart, order.StoreId,
orderItem.AttributesXml,
_taxSettings.PricesIncludeTax ? orderItem.UnitPriceInclTax : orderItem.UnitPriceExclTax,
orderItem.RentalStartDateUtc, orderItem.RentalEndDateUtc,
orderItem.Quantity, false));
}
//set checkout attributes
//comment the code below if you want to disable this functionality
await _genericAttributeService.SaveAttributeAsync(customer, NopCustomerDefaults.CheckoutAttributes, order.CheckoutAttributesXml, order.StoreId);
return warnings;
}
/// <summary>
/// Check whether return request is allowed
/// </summary>
/// <param name="order">Order</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the result
/// </returns>
public virtual async Task<bool> IsReturnRequestAllowedAsync(Order order)
{
if (!_orderSettings.ReturnRequestsEnabled)
return false;
if (order == null || order.Deleted)
return false;
//status should be complete
if (order.OrderStatus != OrderStatus.Complete)
return false;
//validate allowed number of days
if (_orderSettings.NumberOfDaysReturnRequestAvailable > 0)
{
var daysPassed = (DateTime.UtcNow - order.CreatedOnUtc).TotalDays;
if (daysPassed >= _orderSettings.NumberOfDaysReturnRequestAvailable)
return false;
}
var returnRequestAvailability = await _returnRequestService.GetReturnRequestAvailabilityAsync(order.Id);
if (returnRequestAvailability == null)
return false;
return returnRequestAvailability.IsAllowed;
}
/// <summary>
/// Validate minimum order sub-total amount
/// </summary>
/// <param name="cart">Shopping cart</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains true - OK; false - minimum order sub-total amount is not reached
/// </returns>
public virtual async Task<bool> ValidateMinOrderSubtotalAmountAsync(IList<ShoppingCartItem> cart)
{
ArgumentNullException.ThrowIfNull(cart);
//min order amount sub-total validation
if (!cart.Any() || _orderSettings.MinOrderSubtotalAmount <= decimal.Zero)
return true;
//subtotal
var (_, _, subTotalWithoutDiscountBase, _, _) = await _orderTotalCalculationService.GetShoppingCartSubTotalAsync(cart, _orderSettings.MinOrderSubtotalAmountIncludingTax);
if (subTotalWithoutDiscountBase < _orderSettings.MinOrderSubtotalAmount)
return false;
return true;
}
/// <summary>
/// Validate minimum order total amount
/// </summary>
/// <param name="cart">Shopping cart</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains true - OK; false - minimum order total amount is not reached
/// </returns>
public virtual async Task<bool> ValidateMinOrderTotalAmountAsync(IList<ShoppingCartItem> cart)
{
ArgumentNullException.ThrowIfNull(cart);
if (!cart.Any() || _orderSettings.MinOrderTotalAmount <= decimal.Zero)
return true;
var shoppingCartTotalBase = (await _orderTotalCalculationService.GetShoppingCartTotalAsync(cart)).shoppingCartTotal;
if (shoppingCartTotalBase.HasValue && shoppingCartTotalBase.Value < _orderSettings.MinOrderTotalAmount)
return false;
return true;
}
/// <summary>
/// Gets a value indicating whether payment workflow is required
/// </summary>
/// <param name="cart">Shopping cart</param>
/// <param name="useRewardPoints">A value indicating reward points should be used; null to detect current choice of the customer</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the value indicating whether payment workflow is required
/// </returns>
public virtual async Task<bool> IsPaymentWorkflowRequiredAsync(IList<ShoppingCartItem> cart, bool? useRewardPoints = null)
{
ArgumentNullException.ThrowIfNull(cart);
var result = true;
//check whether order total equals zero
var shoppingCartTotalBase = (await _orderTotalCalculationService.GetShoppingCartTotalAsync(cart, useRewardPoints: useRewardPoints, usePaymentMethodAdditionalFee: false)).shoppingCartTotal;
if (shoppingCartTotalBase is decimal.Zero)
result = false;
return result;
}
/// <summary>
/// Gets the next payment date
/// </summary>
/// <param name="recurringPayment">Recurring payment</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task<DateTime?> GetNextPaymentDateAsync(RecurringPayment recurringPayment)
{
ArgumentNullException.ThrowIfNull(recurringPayment);
if (!recurringPayment.IsActive)
return null;
var historyCollection = await _orderService.GetRecurringPaymentHistoryAsync(recurringPayment);
if (historyCollection.Count >= recurringPayment.TotalCycles)
return null;
//result
DateTime? result = null;
//calculate next payment date
if (historyCollection.Any())
{
result = recurringPayment.CyclePeriod switch
{
RecurringProductCyclePeriod.Days => recurringPayment.StartDateUtc.AddDays((double)recurringPayment.CycleLength * historyCollection.Count),
RecurringProductCyclePeriod.Weeks => recurringPayment.StartDateUtc.AddDays((double)(7 * recurringPayment.CycleLength) * historyCollection.Count),
RecurringProductCyclePeriod.Months => recurringPayment.StartDateUtc.AddMonths(recurringPayment.CycleLength * historyCollection.Count),
RecurringProductCyclePeriod.Years => recurringPayment.StartDateUtc.AddYears(recurringPayment.CycleLength * historyCollection.Count),
_ => throw new NopException("Not supported cycle period"),
};
}
else
{
if (recurringPayment.TotalCycles > 0)
result = recurringPayment.StartDateUtc;
}
return result;
}
/// <summary>
/// Gets the cycles remaining
/// </summary>
/// <param name="recurringPayment">Recurring payment</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task<int> GetCyclesRemainingAsync(RecurringPayment recurringPayment)
{
ArgumentNullException.ThrowIfNull(recurringPayment);
var historyCollection = await _orderService.GetRecurringPaymentHistoryAsync(recurringPayment);
var result = recurringPayment.TotalCycles - historyCollection.Count;
if (result < 0)
result = 0;
return result;
}
/// <summary>
/// Gets process payment request
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation
/// The task contains the process payment request
/// </returns>
public virtual async Task<ProcessPaymentRequest> GetProcessPaymentRequestAsync()
{
var customer = await _workContext.GetCurrentCustomerAsync();
var store = await _storeContext.GetCurrentStoreAsync();
var json = await _genericAttributeService.GetAttributeAsync<string>(customer, NopCustomerDefaults.ProcessPaymentRequestAttribute, store.Id);
return string.IsNullOrEmpty(json) ? null : JsonConvert.DeserializeObject<ProcessPaymentRequest>(json);
}
/// <summary>
/// Sets process payment request
/// </summary>
/// <param name="processPaymentRequest">Process payment request. Pass null for delete</param>
/// <param name="useNewOrderGuid">Whether to use new order GUID; pass false to set GUID according to PaymentSettings.RegenerateOrderGuidInterval value</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task SetProcessPaymentRequestAsync(ProcessPaymentRequest processPaymentRequest, bool useNewOrderGuid = false)
{
var customer = await _workContext.GetCurrentCustomerAsync();
var store = await _storeContext.GetCurrentStoreAsync();
if (processPaymentRequest is null)
{
await _genericAttributeService.SaveAttributeAsync<string>(customer, NopCustomerDefaults.ProcessPaymentRequestAttribute, null, store.Id);
return;
}
if (_paymentSettings.RegenerateOrderGuidInterval > 0 && !useNewOrderGuid)
{
//we should use the same GUID for multiple payment attempts
//this way a payment gateway can prevent security issues such as credit card brute-force attacks
//in order to avoid any possible limitations by payment gateway we reset GUID periodically
var previousPaymentRequest = await GetProcessPaymentRequestAsync();
//set previous order GUID (if exists)
if (previousPaymentRequest is { OrderGuidGeneratedOnUtc: not null })
{
var interval = DateTime.UtcNow - previousPaymentRequest.OrderGuidGeneratedOnUtc.Value;
if (interval.TotalSeconds < _paymentSettings.RegenerateOrderGuidInterval)
{
processPaymentRequest.OrderGuid = previousPaymentRequest.OrderGuid;
processPaymentRequest.OrderGuidGeneratedOnUtc = previousPaymentRequest.OrderGuidGeneratedOnUtc;
}
}
}
var json = JsonConvert.SerializeObject(processPaymentRequest);
await _genericAttributeService.SaveAttributeAsync(customer, NopCustomerDefaults.ProcessPaymentRequestAttribute, json, store.Id);
}
#endregion
#region Nested class
/// <summary>
/// PlaceOrder container
/// </summary>
protected partial class PlaceOrderContainer
{
public PlaceOrderContainer()
{
Cart = new List<ShoppingCartItem>();
AppliedDiscounts = new List<Discount>();
AppliedGiftCards = new List<AppliedGiftCard>();
}
/// <summary>
/// Customer
/// </summary>
public Customer Customer { get; set; }
/// <summary>
/// Customer language
/// </summary>
public Language CustomerLanguage { get; set; }
/// <summary>
/// Affiliate identifier
/// </summary>
public int AffiliateId { get; set; }
/// <summary>
/// TAx display type
/// </summary>
public TaxDisplayType CustomerTaxDisplayType { get; set; }
/// <summary>
/// Selected currency
/// </summary>
public string CustomerCurrencyCode { get; set; }
/// <summary>
/// Customer currency rate
/// </summary>
public decimal CustomerCurrencyRate { get; set; }
/// <summary>
/// Billing address
/// </summary>
public Address BillingAddress { get; set; }
/// <summary>
/// Shipping address
/// </summary>
public Address ShippingAddress { get; set; }
/// <summary>
/// Shipping status
/// </summary>
public ShippingStatus ShippingStatus { get; set; }
/// <summary>
/// Selected shipping method
/// </summary>
public string ShippingMethodName { get; set; }
/// <summary>
/// Shipping rate computation method system name
/// </summary>
public string ShippingRateComputationMethodSystemName { get; set; }
/// <summary>
/// Is pickup in store selected?
/// </summary>
public bool PickupInStore { get; set; }
/// <summary>
/// Selected pickup address
/// </summary>
public Address PickupAddress { get; set; }
/// <summary>
/// Is recurring shopping cart
/// </summary>
public bool IsRecurringShoppingCart { get; set; }
/// <summary>
/// Initial order (used with recurring payments)
/// </summary>
public Order InitialOrder { get; set; }
/// <summary>
/// Checkout attributes
/// </summary>
public string CheckoutAttributeDescription { get; set; }
/// <summary>
/// Shopping cart
/// </summary>
public string CheckoutAttributesXml { get; set; }
/// <summary>
///
/// </summary>
public IList<ShoppingCartItem> Cart { get; set; }
/// <summary>
/// Applied discounts
/// </summary>
public List<Discount> AppliedDiscounts { get; set; }
/// <summary>
/// Applied gift cards
/// </summary>
public List<AppliedGiftCard> AppliedGiftCards { get; set; }
/// <summary>
/// Order subtotal (incl tax)
/// </summary>
public decimal OrderSubTotalInclTax { get; set; }
/// <summary>
/// Order subtotal (excl tax)
/// </summary>
public decimal OrderSubTotalExclTax { get; set; }
/// <summary>
/// Subtotal discount (incl tax)
/// </summary>
public decimal OrderSubTotalDiscountInclTax { get; set; }
/// <summary>
/// Subtotal discount (excl tax)
/// </summary>
public decimal OrderSubTotalDiscountExclTax { get; set; }
/// <summary>
/// Shipping (incl tax)
/// </summary>
public decimal OrderShippingTotalInclTax { get; set; }
/// <summary>
/// Shipping (excl tax)
/// </summary>
public decimal OrderShippingTotalExclTax { get; set; }
/// <summary>
/// Payment additional fee (incl tax)
/// </summary>
public decimal PaymentAdditionalFeeInclTax { get; set; }
/// <summary>
/// Payment additional fee (excl tax)
/// </summary>
public decimal PaymentAdditionalFeeExclTax { get; set; }
/// <summary>
/// Tax
/// </summary>
public decimal OrderTaxTotal { get; set; }
/// <summary>
/// VAT number
/// </summary>
public string VatNumber { get; set; }
/// <summary>
/// Tax rates
/// </summary>
public string TaxRates { get; set; }
/// <summary>
/// Order total discount amount
/// </summary>
public decimal OrderDiscountAmount { get; set; }
/// <summary>
/// Redeemed reward points
/// </summary>
public int RedeemedRewardPoints { get; set; }
/// <summary>
/// Redeemed reward points amount
/// </summary>
public decimal RedeemedRewardPointsAmount { get; set; }
/// <summary>
/// Order total
/// </summary>
public decimal OrderTotal { get; set; }
}
#endregion
}