diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderManager.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderManager.cs index 6f4bfc61..2ca6052d 100644 --- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderManager.cs +++ b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderManager.cs @@ -8,6 +8,6 @@ namespace EasyAbp.EShop.Orders.Orders { Task CompleteAsync(Order order); - Task CancelAsync(Order order, string cancellationReason); + Task CancelAsync(Order order, string cancellationReason, bool forceCancel = false); } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/Order.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/Order.cs index 663c02d0..06fa086c 100644 --- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/Order.cs +++ b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/Order.cs @@ -226,5 +226,11 @@ namespace EasyAbp.EShop.Orders.Orders { StaffRemark = staffRemark; } + + public bool IsInInventoryDeductionStage() + { + return !ReducedInventoryAfterPlacingTime.HasValue || + PaidTime.HasValue && !ReducedInventoryAfterPaymentTime.HasValue; + } } } diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderManager.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderManager.cs index 912b968f..a242be5c 100644 --- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderManager.cs +++ b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderManager.cs @@ -57,14 +57,14 @@ namespace EasyAbp.EShop.Orders.Orders } [UnitOfWork] - public virtual async Task CancelAsync(Order order, string cancellationReason) + public virtual async Task CancelAsync(Order order, string cancellationReason, bool forceCancel = false) { if (order.CanceledTime.HasValue) { throw new OrderIsInWrongStageException(order.Id); } - if (order.IsInPayment()) + if (!forceCancel && (order.IsInPayment() || order.IsInInventoryDeductionStage())) { throw new OrderIsInWrongStageException(order.Id); } diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/PaymentCanceledEventHandler.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/PaymentCanceledEventHandler.cs index 827ad846..a66c8c69 100644 --- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/PaymentCanceledEventHandler.cs +++ b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/PaymentCanceledEventHandler.cs @@ -40,6 +40,7 @@ namespace EasyAbp.EShop.Orders.Orders order.SetPaymentId(null); + // OrderAutoCancelOnUpdatedHandler may auto cancel the unpaid order. await _orderRepository.UpdateAsync(order, true); } } diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/ProductInventoryReductionEventHandler.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/ProductInventoryReductionEventHandler.cs index 2e126164..4098a3bd 100644 --- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/ProductInventoryReductionEventHandler.cs +++ b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/ProductInventoryReductionEventHandler.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using EasyAbp.EShop.Payments.Refunds; using EasyAbp.EShop.Products.Products; -using EasyAbp.PaymentService.Refunds; -using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus.Distributed; using Volo.Abp.MultiTenancy; @@ -49,7 +46,8 @@ namespace EasyAbp.EShop.Orders.Orders if (!eventData.IsSuccess) { - await _orderManager.CancelAsync(order, OrdersConsts.InventoryReductionFailedAutoCancellationReason); + await _orderManager.CancelAsync( + order, OrdersConsts.InventoryReductionFailedAutoCancellationReason, true); return; } @@ -76,7 +74,8 @@ namespace EasyAbp.EShop.Orders.Orders { var refundOrderEto = CreateRefundOrderEto(order); - await _orderManager.CancelAsync(order, OrdersConsts.InventoryReductionFailedAutoCancellationReason); + await _orderManager.CancelAsync( + order, OrdersConsts.InventoryReductionFailedAutoCancellationReason, true); await RefundOrderAsync(refundOrderEto); diff --git a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs b/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs index e28f5e3c..6836ab02 100644 --- a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs +++ b/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs @@ -64,9 +64,7 @@ namespace EasyAbp.EShop.Orders.Orders "Key", 0.3m )); - Order1.SetPaymentId(OrderTestData.Payment1Id); - Order1.SetPaidTime(DateTime.Now); - + orderRepository.GetAsync(OrderTestData.Order1Id).Returns(Task.FromResult(Order1)); services.AddTransient(_ => orderRepository); @@ -117,7 +115,10 @@ namespace EasyAbp.EShop.Orders.Orders } } }); - + + Order1.SetPaymentId(OrderTestData.Payment1Id); + Order1.SetPaidTime(DateTime.Now); + Order1.RefundAmount.ShouldBe(0.3m); var orderLine1 = Order1.OrderLines.Single(x => x.Id == OrderTestData.OrderLine1Id); @@ -133,6 +134,9 @@ namespace EasyAbp.EShop.Orders.Orders { var handler = ServiceProvider.GetRequiredService(); + Order1.SetPaymentId(OrderTestData.Payment1Id); + Order1.SetPaidTime(DateTime.Now); + await Should.ThrowAsync(async () => { await handler.HandleEventAsync(new EShopRefundCompletedEto @@ -174,6 +178,9 @@ namespace EasyAbp.EShop.Orders.Orders { var handler = ServiceProvider.GetRequiredService(); + Order1.SetPaymentId(OrderTestData.Payment1Id); + Order1.SetPaidTime(DateTime.Now); + await Should.ThrowAsync(async () => { await handler.HandleEventAsync(new EShopRefundCompletedEto @@ -209,5 +216,32 @@ namespace EasyAbp.EShop.Orders.Orders }); }); } + + [Fact] + public async Task Should_Forbid_Canceling_Order_During_Payment_State() + { + var orderManager = ServiceProvider.GetRequiredService(); + var order = await _orderRepository.GetAsync(OrderTestData.Order1Id); + + order.SetPaymentId(Guid.NewGuid()); + order.SetPaidTime(null); + await Should.ThrowAsync(() => orderManager.CancelAsync(order, "my-reason")); + } + + [Fact] + public async Task Should_Forbid_Canceling_Order_During_Inventory_Reduction_State() + { + var orderManager = ServiceProvider.GetRequiredService(); + var order = await _orderRepository.GetAsync(OrderTestData.Order1Id); + + order.SetReducedInventoryAfterPlacingTime(null); + await Should.ThrowAsync(() => orderManager.CancelAsync(order, "my-reason")); + + order.SetReducedInventoryAfterPlacingTime(DateTime.Now); + order.SetPaymentId(Guid.NewGuid()); + order.SetPaidTime(DateTime.Now); + order.SetReducedInventoryAfterPlacingTime(null); + await Should.ThrowAsync(() => orderManager.CancelAsync(order, "my-reason")); + } } } diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryRollbackOrderCanceledEventHandler.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryRollbackOrderCanceledEventHandler.cs index 386575b5..38a22766 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryRollbackOrderCanceledEventHandler.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryRollbackOrderCanceledEventHandler.cs @@ -28,7 +28,7 @@ namespace EasyAbp.EShop.Products.Products [UnitOfWork(true)] public virtual async Task HandleEventAsync(OrderCanceledEto eventData) { - if (eventData.Order.PaidTime.HasValue) + if (eventData.Order.PaidTime.HasValue || !eventData.Order.ReducedInventoryAfterPlacingTime.HasValue) { return; } diff --git a/modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Domain.Tests/Products/InventoryRollbackTests.cs b/modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Domain.Tests/Products/InventoryRollbackTests.cs index 6a93e3bc..a4383ae3 100644 --- a/modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Domain.Tests/Products/InventoryRollbackTests.cs +++ b/modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Domain.Tests/Products/InventoryRollbackTests.cs @@ -25,40 +25,62 @@ namespace EasyAbp.EShop.Products.Products [Fact] public async Task Should_Roll_Back_ReduceAfterPlacing_Inventory_If_Order_Is_Not_Paid() { - await TestAsync(InventoryStrategy.ReduceAfterPlacing, false, true); + await TestAsync(InventoryStrategy.ReduceAfterPlacing, false, true, true); } [Fact] public async Task Should_Not_Roll_Back_ReduceAfterPlacing_Inventory_If_Order_Is_Paid() { - await TestAsync(InventoryStrategy.ReduceAfterPlacing, true, false); + await TestAsync(InventoryStrategy.ReduceAfterPlacing, true, true, false); } [Fact] public async Task Should_Not_Roll_Back_ReduceAfterPayment_Inventory_If_Order_Is_Not_Paid() { - await TestAsync(InventoryStrategy.ReduceAfterPayment, false, false); + await TestAsync(InventoryStrategy.ReduceAfterPayment, false, true, false); } [Fact] public async Task Should_Not_Roll_Back_ReduceAfterPayment_Inventory_If_Order_Is_Paid() { - await TestAsync(InventoryStrategy.ReduceAfterPayment, true, false); + await TestAsync(InventoryStrategy.ReduceAfterPayment, true, true, false); } [Fact] public async Task Should_Not_Roll_Back_NoNeed_Inventory_If_Order_Is_Not_Paid() { - await TestAsync(InventoryStrategy.NoNeed, false, false); + await TestAsync(InventoryStrategy.NoNeed, false, true, false); } [Fact] public async Task Should_Not_Roll_Back_NoNeed_Inventory_If_Order_Is_Paid() { - await TestAsync(InventoryStrategy.NoNeed, true, false); + await TestAsync(InventoryStrategy.NoNeed, true, true, false); } - protected async Task TestAsync(InventoryStrategy inventoryStrategy, bool orderPaid, bool expectRollback) + [Fact] + public async Task Should_Roll_Back_FlashSales_Inventory_If_Order_Is_Not_Paid() + { + await TestAsync(InventoryStrategy.FlashSales, false, true, true); + } + + [Fact] + public async Task Should_Not_Roll_Back_FlashSales_Inventory_If_Order_Is_Paid() + { + await TestAsync(InventoryStrategy.FlashSales, true, true, false); + } + + [Fact] + public async Task Should_Not_Roll_Back_Inventory_If_ReducedInventoryAfterPlacingTime_Is_Null() + { + await TestAsync(InventoryStrategy.NoNeed, true, false, false); + await TestAsync(InventoryStrategy.ReduceAfterPlacing, true, false, false); + await TestAsync(InventoryStrategy.ReduceAfterPayment, true, false, false); + await TestAsync(InventoryStrategy.FlashSales, true, false, false); + } + + protected async Task TestAsync(InventoryStrategy inventoryStrategy, bool orderPaid, + bool hasReducedInventoryAfterPlacingTime, bool expectRollback) { var product = await ProductRepository.GetAsync(ProductsTestData.Product1Id); var productSku = product.ProductSkus.Single(x => x.Id == ProductsTestData.Product1Sku1Id); @@ -98,7 +120,7 @@ namespace EasyAbp.EShop.Products.Products CompletionTime = null, CanceledTime = null, CancellationReason = null, - ReducedInventoryAfterPlacingTime = null, + ReducedInventoryAfterPlacingTime = hasReducedInventoryAfterPlacingTime ? DateTime.Now : null, ReducedInventoryAfterPaymentTime = null, PaymentExpiration = null, OrderLines = new List