mirror of https://github.com/EasyAbp/EShop.git
10 changed files with 375 additions and 63 deletions
@ -0,0 +1,86 @@ |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.Eshop.Products.Products; |
|||
using EasyAbp.EShop.Orders.Orders; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using Microsoft.Extensions.Logging; |
|||
using Volo.Abp; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans; |
|||
|
|||
public class FlashSaleOrderCanceledEventHandler : IDistributedEventHandler<OrderCanceledEto>, ITransientDependency |
|||
{ |
|||
protected IFlashSaleResultRepository FlashSaleResultRepository { get; } |
|||
|
|||
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
|||
|
|||
protected IAbpApplication AbpApplication { get; } |
|||
|
|||
protected IFlashSaleInventoryManager FlashSaleInventoryManager { get; } |
|||
|
|||
protected IProductCache ProductCache { get; } |
|||
|
|||
protected IFlashSaleCurrentResultCache FlashSaleCurrentResultCache { get; } |
|||
|
|||
protected ILogger<FlashSaleOrderCanceledEventHandler> Logger { get; } |
|||
|
|||
public FlashSaleOrderCanceledEventHandler( |
|||
IFlashSaleResultRepository flashSaleResultRepository, |
|||
IUnitOfWorkManager unitOfWorkManager, |
|||
IAbpApplication abpApplication, |
|||
IFlashSaleInventoryManager flashSaleInventoryManager, |
|||
IProductCache productCache, |
|||
IFlashSaleCurrentResultCache flashSaleCurrentResultCache, |
|||
ILogger<FlashSaleOrderCanceledEventHandler> logger) |
|||
{ |
|||
FlashSaleResultRepository = flashSaleResultRepository; |
|||
UnitOfWorkManager = unitOfWorkManager; |
|||
AbpApplication = abpApplication; |
|||
FlashSaleInventoryManager = flashSaleInventoryManager; |
|||
ProductCache = productCache; |
|||
FlashSaleCurrentResultCache = flashSaleCurrentResultCache; |
|||
Logger = logger; |
|||
} |
|||
|
|||
[UnitOfWork(true)] |
|||
public virtual async Task HandleEventAsync(OrderCanceledEto eventData) |
|||
{ |
|||
var flashSaleResult = await FlashSaleResultRepository |
|||
.SingleOrDefaultAsync(x => x.Status != FlashSaleResultStatus.Failed && x.StoreId == eventData.Order.StoreId && x.OrderId == eventData.Order.Id); |
|||
if (flashSaleResult == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
flashSaleResult.MarkAsFailed(FlashSaleResultFailedReason.OrderCanceled); |
|||
|
|||
await FlashSaleResultRepository.UpdateAsync(flashSaleResult, autoSave: true); |
|||
|
|||
UnitOfWorkManager.Current.OnCompleted(async () => |
|||
{ |
|||
if (eventData.Order.OrderLines.Count == 0) |
|||
{ |
|||
Logger.LogWarning("OrderCanceled order {orderId} orderLines is empty.", eventData.Order.Id); |
|||
return; |
|||
} |
|||
var productId = eventData.Order.OrderLines[0].ProductId; |
|||
var productSkuId = eventData.Order.OrderLines[0].ProductSkuId; |
|||
var product = await ProductCache.GetAsync(productId); |
|||
// try to roll back the inventory.
|
|||
if (!await FlashSaleInventoryManager.TryRollBackInventoryAsync( |
|||
eventData.TenantId, product.InventoryProviderName, eventData.Order.StoreId, |
|||
productId, productSkuId)) |
|||
{ |
|||
Logger.LogWarning("Failed to roll back the flash sale inventory."); |
|||
return; // avoid to remove cache if the rollback failed.
|
|||
} |
|||
|
|||
// remove the cache so the user can try to order again.
|
|||
await FlashSaleCurrentResultCache.RemoveAsync(flashSaleResult.PlanId, flashSaleResult.UserId); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public interface IProductCache |
|||
{ |
|||
Task<ProductCacheItem> GetAsync(Guid productId); |
|||
|
|||
Task RemoveAsync(Guid productId); |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Products.Products.Dtos; |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.ObjectMapping; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public class ProductCache : IProductCache, ITransientDependency |
|||
{ |
|||
protected IDistributedCache<ProductCacheItem, Guid> DistributedCache { get; } |
|||
|
|||
protected IProductAppService ProductAppService { get; } |
|||
|
|||
protected IObjectMapper ObjectMapper { get; } |
|||
|
|||
protected ICurrentTenant CurrentTenant { get; } |
|||
|
|||
public ProductCache( |
|||
IDistributedCache<ProductCacheItem, Guid> productDistributedCache, |
|||
IProductAppService productAppService, |
|||
IObjectMapper objectMapper, |
|||
ICurrentTenant currentTenant) |
|||
{ |
|||
DistributedCache = productDistributedCache; |
|||
ProductAppService = productAppService; |
|||
ObjectMapper = objectMapper; |
|||
CurrentTenant = currentTenant; |
|||
} |
|||
|
|||
public virtual async Task<ProductCacheItem> GetAsync(Guid productId) |
|||
{ |
|||
return await DistributedCache.GetOrAddAsync(productId, async () => |
|||
{ |
|||
var productDto = await ProductAppService.GetAsync(productId); |
|||
|
|||
var cacheItem = ObjectMapper.Map<ProductDto, ProductCacheItem>(productDto); |
|||
|
|||
if (cacheItem != null) |
|||
{ |
|||
cacheItem.TenantId = CurrentTenant.Id; |
|||
} |
|||
|
|||
return cacheItem; |
|||
}); |
|||
} |
|||
|
|||
public virtual async Task RemoveAsync(Guid productId) |
|||
{ |
|||
await DistributedCache.RemoveAsync(productId); |
|||
} |
|||
} |
|||
@ -0,0 +1,207 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.Eshop.Products.Products; |
|||
using EasyAbp.EShop.Orders.Orders; |
|||
using EasyAbp.EShop.Plugins.FlashSales.FlashSaleResults; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using EasyAbp.EShop.Products.Products.Dtos; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using NSubstitute; |
|||
using Shouldly; |
|||
using Volo.Abp.Users; |
|||
using Xunit; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.FlashSales.FlashSalePlans; |
|||
|
|||
public class FlashSaleOrderCanceledEventHandlerTests : FlashSalesApplicationTestBase |
|||
{ |
|||
protected FlashSaleOrderCanceledEventHandler ResultMarkAsFailedOrderCanceledEventHandler { get; } |
|||
|
|||
protected IFlashSaleCurrentResultCache FlashSaleCurrentResultCache { get; } |
|||
|
|||
protected IFlashSaleInventoryManager FlashSaleInventoryManager { get; } |
|||
|
|||
private ProductDto Product1 { get; set; } |
|||
|
|||
public FlashSaleOrderCanceledEventHandlerTests() |
|||
{ |
|||
ResultMarkAsFailedOrderCanceledEventHandler = GetRequiredService<FlashSaleOrderCanceledEventHandler>(); |
|||
FlashSaleCurrentResultCache = GetRequiredService<IFlashSaleCurrentResultCache>(); |
|||
FlashSaleInventoryManager = GetRequiredService<IFlashSaleInventoryManager>(); |
|||
} |
|||
|
|||
protected override void AfterAddApplication(IServiceCollection services) |
|||
{ |
|||
Product1 = CreateMockProductDto(); |
|||
|
|||
var productAppService = Substitute.For<IProductAppService>(); |
|||
productAppService.GetAsync(FlashSalesTestData.Product1Id).Returns(Task.FromResult(Product1)); |
|||
services.Replace(ServiceDescriptor.Singleton(productAppService)); |
|||
|
|||
var flashSaleInventoryManager = Substitute.For<IFlashSaleInventoryManager>(); |
|||
services.Replace(ServiceDescriptor.Singleton(flashSaleInventoryManager)); |
|||
|
|||
var flashSaleCurrentResultCache = Substitute.For<IFlashSaleCurrentResultCache>(); |
|||
services.Replace(ServiceDescriptor.Singleton(flashSaleCurrentResultCache)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task HandleEventAsync() |
|||
{ |
|||
var flashSaleResult = await CreateFlashSaleResultAsync(); |
|||
var orderCanceledEto = new OrderCanceledEto(new OrderEto() |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
StoreId = flashSaleResult.StoreId, |
|||
OrderLines = new List<OrderLineEto>() |
|||
{ |
|||
new OrderLineEto() |
|||
{ |
|||
ProductId = FlashSalesTestData.Product1Id, |
|||
ProductSkuId = FlashSalesTestData.ProductSku1Id |
|||
} |
|||
} |
|||
}); |
|||
flashSaleResult = await MarkAsSuccessfulAsync(flashSaleResult.Id, orderCanceledEto.Order.Id); |
|||
FlashSaleInventoryManager |
|||
.TryRollBackInventoryAsync(flashSaleResult.TenantId, |
|||
Product1.InventoryProviderName, flashSaleResult.StoreId, |
|||
FlashSalesTestData.Product1Id, FlashSalesTestData.ProductSku1Id) |
|||
.Returns(Task.FromResult(true)); |
|||
|
|||
await ResultMarkAsFailedOrderCanceledEventHandler.HandleEventAsync(orderCanceledEto); |
|||
|
|||
var existFlashSaleResult = await FlashSaleResultRepository.GetAsync(flashSaleResult.Id); |
|||
existFlashSaleResult.Status.ShouldBe(FlashSaleResultStatus.Failed); |
|||
existFlashSaleResult.Reason.ShouldBe(FlashSaleResultFailedReason.OrderCanceled); |
|||
|
|||
await FlashSaleInventoryManager.Received() |
|||
.TryRollBackInventoryAsync(flashSaleResult.TenantId, |
|||
Product1.InventoryProviderName, flashSaleResult.StoreId, |
|||
FlashSalesTestData.Product1Id, FlashSalesTestData.ProductSku1Id); |
|||
await FlashSaleCurrentResultCache.Received() |
|||
.RemoveAsync(flashSaleResult.PlanId, flashSaleResult.UserId); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task HandleEventAsync_Should_Not_RemoveResultCache_When_TryRollBackInventory_Failed() |
|||
{ |
|||
var flashSaleResult = await CreateFlashSaleResultAsync(); |
|||
var orderCanceledEto = new OrderCanceledEto(new OrderEto() |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
StoreId = flashSaleResult.StoreId, |
|||
OrderLines = new List<OrderLineEto>() |
|||
{ |
|||
new OrderLineEto() |
|||
{ |
|||
ProductId = FlashSalesTestData.Product1Id, |
|||
ProductSkuId = FlashSalesTestData.ProductSku1Id |
|||
} |
|||
} |
|||
}); |
|||
flashSaleResult = await MarkAsSuccessfulAsync(flashSaleResult.Id, orderCanceledEto.Order.Id); |
|||
FlashSaleInventoryManager |
|||
.TryRollBackInventoryAsync(flashSaleResult.TenantId, |
|||
Product1.InventoryProviderName, flashSaleResult.StoreId, |
|||
FlashSalesTestData.Product1Id, FlashSalesTestData.ProductSku1Id) |
|||
.Returns(Task.FromResult(false)); |
|||
|
|||
await ResultMarkAsFailedOrderCanceledEventHandler.HandleEventAsync(orderCanceledEto); |
|||
|
|||
var existFlashSaleResult = await FlashSaleResultRepository.GetAsync(flashSaleResult.Id); |
|||
existFlashSaleResult.Status.ShouldBe(FlashSaleResultStatus.Failed); |
|||
existFlashSaleResult.Reason.ShouldBe(FlashSaleResultFailedReason.OrderCanceled); |
|||
|
|||
await FlashSaleInventoryManager.Received() |
|||
.TryRollBackInventoryAsync(flashSaleResult.TenantId, |
|||
Product1.InventoryProviderName, flashSaleResult.StoreId, |
|||
FlashSalesTestData.Product1Id, FlashSalesTestData.ProductSku1Id); |
|||
await FlashSaleCurrentResultCache.DidNotReceive() |
|||
.RemoveAsync(flashSaleResult.PlanId, flashSaleResult.UserId); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task HandleEventAsync_Should_Skip_Handling_If_Result_Does_Not_Exist() |
|||
{ |
|||
var flashSaleResult = await CreateFlashSaleResultAsync(); |
|||
var orderCanceledEto = new OrderCanceledEto(new OrderEto() |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
StoreId = flashSaleResult.StoreId, |
|||
OrderLines = new List<OrderLineEto>() |
|||
{ |
|||
new OrderLineEto() |
|||
{ |
|||
ProductId = FlashSalesTestData.Product1Id, |
|||
ProductSkuId = FlashSalesTestData.ProductSku1Id |
|||
} |
|||
} |
|||
}); |
|||
flashSaleResult = await MarkAsSuccessfulAsync(flashSaleResult.Id, orderCanceledEto.Order.Id); |
|||
flashSaleResult = await MarkAsFailedAsync(flashSaleResult.Id, "UT"); |
|||
FlashSaleInventoryManager |
|||
.TryRollBackInventoryAsync(flashSaleResult.TenantId, |
|||
Product1.InventoryProviderName, flashSaleResult.StoreId, |
|||
FlashSalesTestData.Product1Id, FlashSalesTestData.ProductSku1Id) |
|||
.Returns(Task.FromResult(true)); |
|||
|
|||
await ResultMarkAsFailedOrderCanceledEventHandler.HandleEventAsync(orderCanceledEto); |
|||
|
|||
var existFlashSaleResult = await FlashSaleResultRepository.GetAsync(flashSaleResult.Id); |
|||
existFlashSaleResult.Status.ShouldBe(flashSaleResult.Status); |
|||
existFlashSaleResult.Reason.ShouldBe(flashSaleResult.Reason); |
|||
|
|||
await FlashSaleInventoryManager.DidNotReceive() |
|||
.TryRollBackInventoryAsync(flashSaleResult.TenantId, |
|||
Product1.InventoryProviderName, flashSaleResult.StoreId, |
|||
FlashSalesTestData.Product1Id, FlashSalesTestData.ProductSku1Id); |
|||
await FlashSaleCurrentResultCache.DidNotReceive() |
|||
.RemoveAsync(flashSaleResult.PlanId, flashSaleResult.UserId); |
|||
} |
|||
|
|||
protected async Task<FlashSaleResult> MarkAsSuccessfulAsync(Guid resultId, Guid orderId) |
|||
{ |
|||
return await WithUnitOfWorkAsync(async () => |
|||
{ |
|||
var flashSaleResult = await FlashSaleResultRepository.GetAsync(resultId); |
|||
|
|||
flashSaleResult.MarkAsSuccessful(orderId); |
|||
await FlashSaleResultRepository.UpdateAsync(flashSaleResult, autoSave: true); |
|||
|
|||
return flashSaleResult; |
|||
}); |
|||
} |
|||
|
|||
protected async Task<FlashSaleResult> MarkAsFailedAsync(Guid resultId, string reason) |
|||
{ |
|||
return await WithUnitOfWorkAsync(async () => |
|||
{ |
|||
var flashSaleResult = await FlashSaleResultRepository.GetAsync(resultId); |
|||
|
|||
flashSaleResult.MarkAsFailed(reason); |
|||
await FlashSaleResultRepository.UpdateAsync(flashSaleResult, autoSave: true); |
|||
|
|||
return flashSaleResult; |
|||
}); |
|||
} |
|||
|
|||
protected async Task<FlashSaleResult> CreateFlashSaleResultAsync() |
|||
{ |
|||
return await WithUnitOfWorkAsync(async () => |
|||
{ |
|||
var flashSaleResult = new FlashSaleResult( |
|||
GuidGenerator.Create(), |
|||
null, |
|||
FlashSalesTestData.Store1Id, |
|||
FlashSalesTestData.Plan1Id, |
|||
CurrentUser.GetId(), |
|||
DateTime.Now); |
|||
await FlashSaleResultRepository.InsertAsync(flashSaleResult, autoSave: true); |
|||
|
|||
return flashSaleResult; |
|||
}); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue