Try your search with a different keyword or use * as a wildcard.
using System.Globalization;
using Nop.Core;
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;
///
/// Order processing service
///
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 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,
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;
_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
///
/// Books the inventory by specified shipment
///
/// Shipment
/// Message for the stock quantity history
/// A task that represents the asynchronous operation
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);
}
}
///
/// Reveres the booked inventory by specified order
///
///
/// Message for the stock quantity history
/// A task that represents the asynchronous operation
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);
}
}
}
///
/// Returns the stock by specified order
///
/// Order
/// Message for the stock quantity history
/// A task that represents the asynchronous operation
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);
}
}
///
/// Add order note
///
/// Order
/// Note text
/// A task that represents the asynchronous operation
protected virtual async Task AddOrderNoteAsync(Order order, string note)
{
await _orderService.InsertOrderNoteAsync(new OrderNote
{
OrderId = order.Id,
Note = note,
DisplayToCustomer = false,
CreatedOnUtc = DateTime.UtcNow
});
}
///
/// Prepare details to place an order. It also sets some properties to "processPaymentRequest"
///
/// Process payment request
///
/// A task that represents the asynchronous operation
/// The task result contains the details
///
protected virtual async Task 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;
}
///
/// Prepare and validate recurring shopping cart
///
/// PlaceOrder container
/// payment info holder
/// A task that represents the asynchronous operation
/// Validation problems
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;
}
///
/// Prepare and validate all totals
///
/// sub total, shipping total, payment total, tax amount etc.
///
/// PlaceOrder container
/// payment info holder
/// A task that represents the asynchronous operation
/// Validation problems
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 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;
}
///
/// Prepare and validate shipping info
///
/// PlaceOrder container
/// payment info holder
/// A task that represents the asynchronous operation
/// Validation problems
protected virtual async Task PrepareAndValidateShippingInfoAsync(PlaceOrderContainer details, ProcessPaymentRequest processPaymentRequest)
{
//shipping info
if (await _shoppingCartService.ShoppingCartRequiresShippingAsync(details.Cart))
{
var pickupPoint = await _genericAttributeService.GetAttributeAsync(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(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;
}
///
/// Prepare and validate shopping cart and checkout attributes
///
/// PlaceOrder container
/// payment info holder
/// The working currency
/// A task that represents the asynchronous operation
/// Validation problems
protected virtual async Task PrepareAndValidateShoppingCartAndCheckoutAttributesAsync(PlaceOrderContainer details, ProcessPaymentRequest processPaymentRequest, Currency currentCurrency)
{
//checkout attributes
details.CheckoutAttributesXml = await _genericAttributeService.GetAttributeAsync(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)));
}
}
///
/// Prepare and validate billing address
///
/// PlaceOrder container
/// A task that represents the asynchronous operation
/// Validation problems
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");
}
///
/// Prepare and validate customer
///
/// PlaceOrder container
/// payment info holder
/// The working currency
/// A task that represents the asynchronous operation
/// Validation problems
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();
}
///
/// Prepare details to place order based on the recurring payment.
///
/// Process payment request
///
/// A task that represents the asynchronous operation
/// The task result contains the details
///
protected virtual async Task 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;
}
///
/// Save order and add order notes
///
/// Process payment request
/// Process payment result
/// Details
///
/// A task that represents the asynchronous operation
/// The task result contains the order
///
protected virtual async Task 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 = _paymentService.SerializeCustomValues(processPaymentRequest),
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;
}
///
/// Send "order placed" notifications and save order notes
///
/// Order
/// A task that represents the asynchronous operation
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)}.");
}
///
/// Award (earn) reward points (for placing a new order)
///
/// Order
/// A task that represents the asynchronous operation
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);
}
///
/// Reduce (cancel) reward points (previously awarded for placing an order)
///
/// Order
/// A task that represents the asynchronous operation
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));
}
}
}
///
/// Return back redeemed reward points to a customer (spent when placing an order)
///
/// Order
/// A task that represents the asynchronous operation
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));
}
}
}
///
/// Set IsActivated value for purchase gift cards for particular order
///
/// Order
/// A value indicating whether to activate gift cards; true - activate, false - deactivate
/// A task that represents the asynchronous operation
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);
}
}
}
///
/// Sets an order status
///
/// Order
/// New order status
/// True to notify customer
/// A task that represents the asynchronous operation
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)
{
//notification
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)}.");
}
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)}.");
}
//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);
}
///
/// Process order paid status
///
/// Order
/// A task that represents the asynchronous operation
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);
}
///
/// Process customer roles with "Purchased with Product" property configured
///
/// Order
/// A value indicating whether to add configured customer role; true - add, false - remove
/// A task that represents the asynchronous operation
protected virtual async Task ProcessCustomerRolesWithPurchasedProductSpecifiedAsync(Order order, bool add)
{
ArgumentNullException.ThrowIfNull(order);
//purchased product identifiers
var purchasedProductIds = new List();
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);
}
}
///
/// Get a list of vendors in order (order items)
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains the vendors
///
protected virtual async Task> GetVendorsInOrderAsync(Order order)
{
var pIds = (await _orderService.GetOrderItemsAsync(order.Id)).Select(x => x.ProductId).ToArray();
return await _vendorService.GetVendorsByProductIdsAsync(pIds);
}
///
/// Create recurring payment (the first payment)
///
/// Process payment request
/// Order
/// A task that represents the asynchronous operation
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;
}
}
///
/// Move shopping cart items to order items
///
/// Place order container
/// Order
/// A task that represents the asynchronous operation
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 _shoppingCartService.ClearShoppingCartAsync(details.Customer, order.StoreId);
}
///
/// Add gift cards
///
/// Product
/// attributes XML
/// Quantity
/// Order item
/// Unit price exclude tax, it set as amount if not set specific amount and product.OverriddenGiftCardAmount isn't set to
/// Amount
/// A task that represents the asynchronous operation
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
});
}
}
///
/// Get process payment result
///
/// Process payment request
/// Place order container
///
/// A task that represents the asynchronous operation
/// The task result contains the
///
protected virtual async Task 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;
}
///
/// Save gift card usage history
///
/// Place order container
/// Order
/// A task that represents the asynchronous operation
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
});
}
///
/// Save discount usage history
///
/// PlaceOrderContainer
/// Order
/// A task that represents the asynchronous operation
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
});
}
}
///
/// Checks and save order status
///
/// Order
/// Indicate if we need save order if nothing changed on the order status
/// A task that represents the asynchronous operation
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
///
/// Checks order status
///
/// Order
/// A task that represents the asynchronous operation
public virtual async Task CheckOrderStatusAsync(Order order)
{
await CheckAndSaveOrderStatusAsync(order, false);
}
///
/// Places an order
///
/// Process payment request
///
/// A task that represents the asynchronous operation
/// The task result contains the place order result
///
public virtual async Task PlaceOrderAsync(ProcessPaymentRequest processPaymentRequest)
{
ArgumentNullException.ThrowIfNull(processPaymentRequest);
var result = new PlaceOrderResult();
try
{
if (processPaymentRequest.OrderGuid == Guid.Empty)
throw new Exception("Order GUID is not generated");
//prepare order details
var details = await PreparePlaceOrderDetailsAsync(processPaymentRequest);
var processPaymentResult = await GetProcessPaymentResultAsync(processPaymentRequest, details)
?? throw new NopException("processPaymentResult is not available");
if (processPaymentResult.Success)
{
var order = await SaveOrderDetailsAsync(processPaymentRequest, processPaymentResult, details);
result.PlacedOrder = order;
//move shopping cart items to order items
await MoveShoppingCartItemsToOrderItemsAsync(details, order);
//discount usage history
await SaveDiscountUsageHistoryAsync(details, order);
//gift card usage history
await SaveGiftCardUsageHistoryAsync(details, order);
//recurring orders
if (details.IsRecurringShoppingCart)
await CreateFirstRecurringPaymentAsync(processPaymentRequest, order);
//notifications
await SendNotificationsAndSaveNotesAsync(order);
//reset checkout data
await _customerService.ResetCheckoutDataAsync(details.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;
}
///
/// Update order totals
///
/// Parameters for the updating order
/// A task that represents the asynchronous operation
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 (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 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);
}
}
///
/// Deletes an order
///
/// The order
/// A task that represents the asynchronous operation
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);
}
///
/// Process next recurring payment
///
/// Recurring payment
/// Process payment result (info about last payment for automatic recurring payments)
///
/// A task that represents the asynchronous operation
/// The task result contains the collection of errors
///
public virtual async Task> 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,
CustomValues = _paymentService.DeserializeCustomValues(initialOrder)
};
//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();
}
//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;
}
}
///
/// Cancels a recurring payment
///
/// Recurring payment
/// A task that represents the asynchronous operation
public virtual async Task> CancelRecurringPaymentAsync(RecurringPayment recurringPayment)
{
ArgumentNullException.ThrowIfNull(recurringPayment);
var initialOrder = await _orderService.GetOrderByIdAsync(recurringPayment.InitialOrderId);
if (initialOrder == null)
return new List { "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;
}
///
/// Gets a value indicating whether a customer can cancel recurring payment
///
/// Customer
/// Recurring Payment
///
/// A task that represents the asynchronous operation
/// The task result contains the value indicating whether a customer can cancel recurring payment
///
public virtual async Task 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;
}
///
/// Gets a value indicating whether a customer can retry last failed recurring payment
///
/// Customer
/// Recurring Payment
///
/// A task that represents the asynchronous operation
/// The task result contains true if a customer can retry payment; otherwise false
///
public virtual async Task 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;
}
///
/// Send a shipment
///
/// Shipment
/// True to notify customer
/// A task that represents the asynchronous operation
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);
}
///
/// Marks a shipment as ready for pickup
///
/// Shipment
/// True to notify customer
/// A task that represents the asynchronous operation
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);
}
///
/// Marks a shipment as delivered
///
/// Shipment
/// True to notify customer
/// A task that represents the asynchronous operation
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);
}
///
/// Gets a value indicating whether cancel is allowed
///
/// Order
/// A value indicating whether cancel is allowed
public virtual bool CanCancelOrder(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (order.OrderStatus == OrderStatus.Cancelled)
return false;
return true;
}
///
/// Cancels order
///
/// Order
/// True to notify customer
/// A task that represents the asynchronous operation
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);
//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));
}
///
/// Gets a value indicating whether order can be marked as authorized
///
/// Order
/// A value indicating whether order can be marked as authorized
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;
}
///
/// Marks order as authorized
///
/// Order
/// A task that represents the asynchronous operation
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);
}
///
/// Gets a value indicating whether capture from admin panel is allowed
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains a value indicating whether capture from admin panel is allowed
///
public virtual async Task 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;
}
///
/// Capture an order (from admin panel)
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains a list of errors; empty list if no errors
///
public virtual async Task> 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;
}
///
/// Gets a value indicating whether order can be marked as paid
///
/// Order
/// A value indicating whether order can be marked as paid
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;
}
///
/// Marks order as paid
///
/// Order
/// A task that represents the asynchronous operation
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);
}
///
/// Gets a value indicating whether refund from admin panel is allowed
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains a value indicating whether refund from admin panel is allowed
///
public virtual async Task 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;
}
///
/// Refunds an order (from admin panel)
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains a list of errors; empty list if no errors
///
public virtual async Task> 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;
}
///
/// Gets a value indicating whether order can be marked as refunded
///
/// Order
/// A value indicating whether order can be marked as refunded
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;
}
///
/// Refunds an order (offline)
///
/// Order
/// A task that represents the asynchronous operation
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)}.");
}
///
/// Gets a value indicating whether partial refund from admin panel is allowed
///
/// Order
/// Amount to refund
///
/// A task that represents the asynchronous operation
/// The task result contains a value indicating whether refund from admin panel is allowed
///
public virtual async Task 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;
}
///
/// Partially refunds an order (from admin panel)
///
/// Order
/// Amount to refund
///
/// A task that represents the asynchronous operation
/// The task result contains a list of errors; empty list if no errors
///
public virtual async Task> 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;
}
///
/// Gets a value indicating whether order can be marked as partially refunded
///
/// Order
/// Amount to refund
/// A value indicating whether order can be marked as partially refunded
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;
}
///
/// Partially refunds an order (offline)
///
/// Order
/// Amount to refund
/// A task that represents the asynchronous operation
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)}.");
}
///
/// Gets a value indicating whether void from admin panel is allowed
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains a value indicating whether void from admin panel is allowed
///
public virtual async Task 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;
}
///
/// Voids order (from admin panel)
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains the voided orders
///
public virtual async Task> 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;
}
///
/// Gets a value indicating whether order can be marked as voided
///
/// Order
/// A value indicating whether order can be marked as voided
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;
}
///
/// Void order (offline)
///
/// Order
/// A task that represents the asynchronous operation
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);
}
///
/// Place order items in current user shopping cart.
///
/// The order
///
/// A task that represents the asynchronous operation
/// The task result contains the warnings
///
public virtual async Task> ReOrderAsync(Order order)
{
ArgumentNullException.ThrowIfNull(order);
var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
var warnings = new List();
//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, 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;
}
///
/// Check whether return request is allowed
///
/// Order
///
/// A task that represents the asynchronous operation
/// The task result contains the result
///
public virtual async Task 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;
}
///
/// Validate minimum order sub-total amount
///
/// Shopping cart
///
/// A task that represents the asynchronous operation
/// The task result contains true - OK; false - minimum order sub-total amount is not reached
///
public virtual async Task ValidateMinOrderSubtotalAmountAsync(IList 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;
}
///
/// Validate minimum order total amount
///
/// Shopping cart
///
/// A task that represents the asynchronous operation
/// The task result contains true - OK; false - minimum order total amount is not reached
///
public virtual async Task ValidateMinOrderTotalAmountAsync(IList 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;
}
///
/// Gets a value indicating whether payment workflow is required
///
/// Shopping cart
/// A value indicating reward points should be used; null to detect current choice of the customer
///
/// A task that represents the asynchronous operation
/// The task result contains the value indicating whether payment workflow is required
///
public virtual async Task IsPaymentWorkflowRequiredAsync(IList 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;
}
///
/// Gets the next payment date
///
/// Recurring payment
/// A task that represents the asynchronous operation
public virtual async Task 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;
}
///
/// Gets the cycles remaining
///
/// Recurring payment
/// A task that represents the asynchronous operation
public virtual async Task 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;
}
#endregion
#region Nested class
///
/// PlaceOrder container
///
protected partial class PlaceOrderContainer
{
public PlaceOrderContainer()
{
Cart = new List();
AppliedDiscounts = new List();
AppliedGiftCards = new List();
}
///
/// Customer
///
public Customer Customer { get; set; }
///
/// Customer language
///
public Language CustomerLanguage { get; set; }
///
/// Affiliate identifier
///
public int AffiliateId { get; set; }
///
/// TAx display type
///
public TaxDisplayType CustomerTaxDisplayType { get; set; }
///
/// Selected currency
///
public string CustomerCurrencyCode { get; set; }
///
/// Customer currency rate
///
public decimal CustomerCurrencyRate { get; set; }
///
/// Billing address
///
public Address BillingAddress { get; set; }
///
/// Shipping address
///
public Address ShippingAddress { get; set; }
///
/// Shipping status
///
public ShippingStatus ShippingStatus { get; set; }
///
/// Selected shipping method
///
public string ShippingMethodName { get; set; }
///
/// Shipping rate computation method system name
///
public string ShippingRateComputationMethodSystemName { get; set; }
///
/// Is pickup in store selected?
///
public bool PickupInStore { get; set; }
///
/// Selected pickup address
///
public Address PickupAddress { get; set; }
///
/// Is recurring shopping cart
///
public bool IsRecurringShoppingCart { get; set; }
///
/// Initial order (used with recurring payments)
///
public Order InitialOrder { get; set; }
///
/// Checkout attributes
///
public string CheckoutAttributeDescription { get; set; }
///
/// Shopping cart
///
public string CheckoutAttributesXml { get; set; }
///
///
///
public IList Cart { get; set; }
///
/// Applied discounts
///
public List AppliedDiscounts { get; set; }
///
/// Applied gift cards
///
public List AppliedGiftCards { get; set; }
///
/// Order subtotal (incl tax)
///
public decimal OrderSubTotalInclTax { get; set; }
///
/// Order subtotal (excl tax)
///
public decimal OrderSubTotalExclTax { get; set; }
///
/// Subtotal discount (incl tax)
///
public decimal OrderSubTotalDiscountInclTax { get; set; }
///
/// Subtotal discount (excl tax)
///
public decimal OrderSubTotalDiscountExclTax { get; set; }
///
/// Shipping (incl tax)
///
public decimal OrderShippingTotalInclTax { get; set; }
///
/// Shipping (excl tax)
///
public decimal OrderShippingTotalExclTax { get; set; }
///
/// Payment additional fee (incl tax)
///
public decimal PaymentAdditionalFeeInclTax { get; set; }
///
/// Payment additional fee (excl tax)
///
public decimal PaymentAdditionalFeeExclTax { get; set; }
///
/// Tax
///
public decimal OrderTaxTotal { get; set; }
///
/// VAT number
///
public string VatNumber { get; set; }
///
/// Tax rates
///
public string TaxRates { get; set; }
///
/// Order total discount amount
///
public decimal OrderDiscountAmount { get; set; }
///
/// Redeemed reward points
///
public int RedeemedRewardPoints { get; set; }
///
/// Redeemed reward points amount
///
public decimal RedeemedRewardPointsAmount { get; set; }
///
/// Order total
///
public decimal OrderTotal { get; set; }
}
#endregion
}