Skip to content

Commit

Permalink
Merge branch 'issue-7325-Allow-enable-lock-for-order-placement' into …
Browse files Browse the repository at this point in the history
…develop
  • Loading branch information
skoshelev committed Oct 29, 2024
2 parents 57f60e7 + faf8ec0 commit 7ed9afa
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 47 deletions.
10 changes: 10 additions & 0 deletions src/Libraries/Nop.Core/Domain/Orders/OrderSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,14 @@ public partial class OrderSettings : ISettings
/// Gets or sets a value indicating whether "Summary" block should be displayed on the order list table
/// </summary>
public bool DisplayOrderSummary { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to place order with lock
/// </summary>
public bool PlaceOrderWithLock { get; set; }

/// <summary>
/// Gets or sets the time in minutes for order lock
/// </summary>
public int PlaceOrderWithLockInterval { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3145,7 +3145,9 @@ await settingService.SaveSettingAsync(new OrderSettings
AllowAdminsToBuyCallForPriceProducts = true,
ShowProductThumbnailInOrderDetailsPage = true,
DisplayCustomerCurrencyOnOrders = false,
DisplayOrderSummary = true
DisplayOrderSummary = true,
PlaceOrderWithLock = false,
PlaceOrderWithLockInterval = 1
});

await settingService.SaveSettingAsync(new SecuritySettings
Expand Down
12 changes: 12 additions & 0 deletions src/Libraries/Nop.Services/Orders/NopOrderDefaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ public static partial class NopOrderDefaults
public static string ShoppingCartItemsByCustomerPrefix => "Nop.shoppingcartitem.all.{0}";


#endregion

#region Perform order with lock

/// <summary>
/// Gets a key for caching
/// </summary>
/// <remarks>
/// {0} : customer identifier
/// </remarks>
public static CacheKey OrderWithLockCacheKey => new("Nop.Order.With.Lock.{0}");

#endregion

#endregion
Expand Down
146 changes: 100 additions & 46 deletions src/Libraries/Nop.Services/Orders/OrderProcessingService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Globalization;
using Nop.Core;
using Nop.Core.Caching;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Customers;
Expand Down Expand Up @@ -71,6 +72,7 @@ public partial class OrderProcessingService : IOrderProcessingService
protected readonly IShippingService _shippingService;
protected readonly IShoppingCartService _shoppingCartService;
protected readonly IStateProvinceService _stateProvinceService;
protected readonly IStaticCacheManager _staticCacheManager;
protected readonly IStoreMappingService _storeMappingService;
protected readonly IStoreService _storeService;
protected readonly ITaxService _taxService;
Expand Down Expand Up @@ -122,6 +124,7 @@ public OrderProcessingService(CurrencySettings currencySettings,
IShippingService shippingService,
IShoppingCartService shoppingCartService,
IStateProvinceService stateProvinceService,
IStaticCacheManager staticCacheManager,
IStoreMappingService storeMappingService,
IStoreService storeService,
ITaxService taxService,
Expand Down Expand Up @@ -169,6 +172,7 @@ public OrderProcessingService(CurrencySettings currencySettings,
_shippingService = shippingService;
_shoppingCartService = shoppingCartService;
_stateProvinceService = stateProvinceService;
_staticCacheManager = staticCacheManager;
_storeMappingService = storeMappingService;
_storeService = storeService;
_taxService = taxService;
Expand Down Expand Up @@ -1528,72 +1532,122 @@ public virtual async Task<PlaceOrderResult> PlaceOrderAsync(ProcessPaymentReques
{
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);

async Task<PlaceOrderResult> placeOrder(PlaceOrderContainer placeOrderContainer)
{
if (processPaymentRequest.OrderGuid == Guid.Empty)
throw new Exception("Order GUID is not generated");
var result = new PlaceOrderResult();

//prepare order details
var details = await PreparePlaceOrderDetailsAsync(processPaymentRequest);
try
{
var processPaymentResult =
await GetProcessPaymentResultAsync(processPaymentRequest, placeOrderContainer)
?? throw new NopException("processPaymentResult is not available");

if (processPaymentResult.Success)
{
var order = await SaveOrderDetailsAsync(processPaymentRequest, processPaymentResult,
placeOrderContainer);
result.PlacedOrder = order;

var processPaymentResult = await GetProcessPaymentResultAsync(processPaymentRequest, details)
?? throw new NopException("processPaymentResult is not available");
//move shopping cart items to order items
await MoveShoppingCartItemsToOrderItemsAsync(placeOrderContainer, order);

if (processPaymentResult.Success)
//discount usage history
await SaveDiscountUsageHistoryAsync(placeOrderContainer, order);

//gift card usage history
await SaveGiftCardUsageHistoryAsync(placeOrderContainer, order);

//recurring orders
if (placeOrderContainer.IsRecurringShoppingCart)
await CreateFirstRecurringPaymentAsync(processPaymentRequest, order);

//notifications
await SendNotificationsAndSaveNotesAsync(order);

//reset checkout data
await _customerService.ResetCheckoutDataAsync(placeOrderContainer.Customer,
processPaymentRequest.StoreId, clearCouponCodes: true, clearCheckoutAttributes: true);
await _customerActivityService.InsertActivityAsync("PublicStore.PlaceOrder",
string.Format(await _localizationService.GetResourceAsync("ActivityLog.PublicStore.PlaceOrder"),
order.Id), order);

//raise event
await _eventPublisher.PublishAsync(new OrderPlacedEvent(order));

//check order status
await CheckOrderStatusAsync(order);

if (order.PaymentStatus == PaymentStatus.Paid)
await ProcessOrderPaidAsync(order);
}
else
foreach (var paymentError in processPaymentResult.Errors)
result.AddError(string.Format(
await _localizationService.GetResourceAsync("Checkout.PaymentError"), paymentError));
}
catch (Exception exc)
{
var order = await SaveOrderDetailsAsync(processPaymentRequest, processPaymentResult, details);
result.PlacedOrder = order;
await _logger.ErrorAsync(exc.Message, exc);
result.AddError(exc.Message);
}

//move shopping cart items to order items
await MoveShoppingCartItemsToOrderItemsAsync(details, order);
if (result.Success)
return result;

//discount usage history
await SaveDiscountUsageHistoryAsync(details, order);
//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);

//gift card usage history
await SaveGiftCardUsageHistoryAsync(details, order);
return result;
}

//recurring orders
if (details.IsRecurringShoppingCart)
await CreateFirstRecurringPaymentAsync(processPaymentRequest, order);
if (!_orderSettings.PlaceOrderWithLock)
return await placeOrder(details);

//notifications
await SendNotificationsAndSaveNotesAsync(order);
PlaceOrderResult result;
var resource = details.Customer.Id.ToString();

//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);
//the named mutex helps to avoid creating the same order in different threads,
//and does not decrease performance significantly, because the code is blocked only for the specific cart.
//you should be very careful, mutexes cannot be used in with the await operation
//we can't use semaphore here, because it produces PlatformNotSupportedException exception on UNIX based systems
using var mutex = new Mutex(false, resource);

//raise event
await _eventPublisher.PublishAsync(new OrderPlacedEvent(order));
mutex.WaitOne();

//check order status
await CheckOrderStatusAsync(order);
try
{
var cacheKey = _staticCacheManager.PrepareKey(NopOrderDefaults.OrderWithLockCacheKey, resource);
cacheKey.CacheTime = _orderSettings.PlaceOrderWithLockInterval;

if (order.PaymentStatus == PaymentStatus.Paid)
await ProcessOrderPaidAsync(order);
var exist = _staticCacheManager.Get(cacheKey, () => false);

if (exist)
{
result = new PlaceOrderResult();
result.Errors.Add(_localizationService.GetResourceAsync("Checkout.MinOrderPlacementInterval").Result);
}
else
foreach (var paymentError in processPaymentResult.Errors)
result.AddError(string.Format(await _localizationService.GetResourceAsync("Checkout.PaymentError"), paymentError));
{
result = placeOrder(details).Result;

if (result.Success)
_staticCacheManager.SetAsync(cacheKey, true).Wait();
}
}
catch (Exception exc)
finally
{
await _logger.ErrorAsync(exc.Message, exc);
result.AddError(exc.Message);
mutex.ReleaseMutex();
}

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FluentMigrator;
using Nop.Core.Domain.Orders;
using Nop.Core.Infrastructure;
using Nop.Data;
using Nop.Data.Migrations;
Expand All @@ -22,6 +23,20 @@ public override void Up()
var displayAttributeCombinationImagesOnly = settingService.GetSetting("producteditorsettings.displayattributecombinationimagesonly");
if (displayAttributeCombinationImagesOnly is not null)
settingService.DeleteSetting(displayAttributeCombinationImagesOnly);

//#7325
var orderSettings = settingService.LoadSetting<OrderSettings>();
if (!settingService.SettingExists(orderSettings, settings => settings.PlaceOrderWithLock))
{
orderSettings.PlaceOrderWithLock = false;
settingService.SaveSetting(orderSettings, settings => settings.PlaceOrderWithLock);
}

if (!settingService.SettingExists(orderSettings, settings => settings.PlaceOrderWithLockInterval))
{
orderSettings.PlaceOrderWithLockInterval = 1;
settingService.SaveSetting(orderSettings, settings => settings.PlaceOrderWithLockInterval);
}
}

public override void Down()
Expand Down

0 comments on commit 7ed9afa

Please sign in to comment.