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