mirror of https://github.com/EasyAbp/EShop.git
Browse Source
# Conflicts: # Directory.Build.props # EShop.sln # docs/README.md # samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/EShopSampleDbContextModelSnapshot.cspull/170/head
144 changed files with 8256 additions and 810 deletions
@ -0,0 +1,40 @@ |
|||
# EShop.Plugins.Inventories.DaprActors |
|||
|
|||
[](https://abp.io) |
|||
[](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions) |
|||
[](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions) |
|||
[](https://discord.gg/S6QaezrCRq) |
|||
[](https://www.github.com/EasyAbp/EShop) |
|||
|
|||
EShop product-inventory implementation of [Dapr Actors](https://docs.dapr.io/developing-applications/building-blocks/actors/actors-overview). |
|||
|
|||
## Installation |
|||
|
|||
1. Install the following NuGet packages. ([see how](https://github.com/EasyAbp/EasyAbpGuide/blob/master/docs/How-To.md#add-nuget-packages)) |
|||
|
|||
* EasyAbp.EShop.Products.DaprActorsInventory.Domain _(install at EasyAbp.EShop.Products.Domain location)_ |
|||
* EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore _(install at a host project to run Actors)_ |
|||
|
|||
2. Add `DependsOn(typeof(EShopXxxModule))` attribute to configure the module dependencies. ([see how](https://github.com/EasyAbp/EasyAbpGuide/blob/master/docs/How-To.md#add-module-dependencies)) |
|||
|
|||
3. Configure a state store for the inventory actor. ([see how](https://docs.dapr.io/reference/api/state_api/#configuring-state-store-for-actors)) |
|||
|
|||
## Usage |
|||
|
|||
1. Configure the DaprActors inventory provider as default. |
|||
```csharp |
|||
Configure<EShopProductsOptions>(options => |
|||
{ |
|||
// Configure as the default inventory provider |
|||
options.DefaultInventoryProviderName = "DaprActors"; |
|||
|
|||
// Configure as the default inventory provider for MyProductGroup |
|||
options.Groups.Configure<MyProductGroup>(group => |
|||
{ |
|||
group.DefaultInventoryProviderName = "DaprActors"; |
|||
}); |
|||
}); |
|||
``` |
|||
> Better to use `DaprActorsProductInventoryProvider.DaprActorsProductInventoryProviderName` instead of `"DaprActors"` as the provider name. |
|||
|
|||
2. Create a product and set `InventoryProviderName` to `DaprActors`. Then the product is specified to use the Dapr Actors inventory provider. |
|||
@ -0,0 +1,51 @@ |
|||
# EShop.Plugins.Inventories.OrleansGrains |
|||
|
|||
[](https://abp.io) |
|||
[](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions) |
|||
[](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions) |
|||
[](https://discord.gg/S6QaezrCRq) |
|||
[](https://www.github.com/EasyAbp/EShop) |
|||
|
|||
EShop product-inventory implementation of [Orleans Grains](https://docs.microsoft.com/en-us/dotnet/orleans/grains). |
|||
|
|||
## Installation |
|||
|
|||
1. Install the following NuGet packages. ([see how](https://github.com/EasyAbp/EasyAbpGuide/blob/master/docs/How-To.md#add-nuget-packages)) |
|||
|
|||
* EasyAbp.EShop.Products.OrleansGrainsInventory.Domain _(install at EasyAbp.EShop.Products.Domain location)_ |
|||
* EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Silo _(install at a host project to run Grains)_ |
|||
|
|||
2. Add `DependsOn(typeof(EShopXxxModule))` attribute to configure the module dependencies. ([see how](https://github.com/EasyAbp/EasyAbpGuide/blob/master/docs/How-To.md#add-module-dependencies)) |
|||
|
|||
3. Open `Program.cs` in the host project to create an Orleans Silo. (see Microsoft's [document](https://docs.microsoft.com/en-us/dotnet/orleans/host/configuration-guide/server-configuration) for more information) |
|||
|
|||
```csharp |
|||
builder.Host.AddAppSettingsSecretsJson() |
|||
.UseAutofac() |
|||
.UseSerilog() |
|||
.UseOrleans(c => |
|||
{ |
|||
c.UseLocalhostClustering() // for test only |
|||
c.AddMemoryGrainStorage(InventoryGrain.StorageProviderName); // for test only |
|||
}); |
|||
``` |
|||
|
|||
## Usage |
|||
|
|||
1. Configure the OrleansGrains inventory provider as default. |
|||
```csharp |
|||
Configure<EShopProductsOptions>(options => |
|||
{ |
|||
// Configure as the default inventory provider |
|||
options.DefaultInventoryProviderName = "OrleansGrains"; |
|||
|
|||
// Configure as the default inventory provider for MyProductGroup |
|||
options.Groups.Configure<MyProductGroup>(group => |
|||
{ |
|||
group.DefaultInventoryProviderName = "OrleansGrains"; |
|||
}); |
|||
}); |
|||
``` |
|||
> Better to use `OrleansGrainsProductInventoryProvider.OrleansGrainsProductInventoryProviderName` instead of `"OrleansGrains"` as the provider name. |
|||
|
|||
2. Create a product and set `InventoryProviderName` to `OrleansGrains`. Then the product is specified to use the Orleans Grains inventory provider. |
|||
@ -0,0 +1,192 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using NSubstitute; |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace EasyAbp.EShop.Orders.Orders; |
|||
|
|||
public class InventoryReductionResultTests : OrdersDomainTestBase |
|||
{ |
|||
private Order Order1 { get; set; } |
|||
|
|||
protected override void AfterAddApplication(IServiceCollection services) |
|||
{ |
|||
var orderRepository = Substitute.For<IOrderRepository>(); |
|||
Order1 = new Order( |
|||
OrderTestData.Order1Id, |
|||
null, |
|||
OrderTestData.Store1Id, |
|||
Guid.NewGuid(), |
|||
"CNY", |
|||
1m, |
|||
0m, |
|||
1.5m, |
|||
1.5m, |
|||
null, |
|||
null); |
|||
Order1.OrderLines.Add(new OrderLine( |
|||
OrderTestData.OrderLine1Id, |
|||
OrderTestData.Product1Id, |
|||
OrderTestData.ProductSku1Id, |
|||
null, |
|||
DateTime.Now, |
|||
null, |
|||
"Default", |
|||
"Default", |
|||
null, |
|||
"Product 1", |
|||
null, |
|||
null, |
|||
null, |
|||
"CNY", |
|||
0.5m, |
|||
1m, |
|||
0m, |
|||
1m, |
|||
2 |
|||
)); |
|||
Order1.OrderExtraFees.Add(new OrderExtraFee( |
|||
OrderTestData.Order1Id, |
|||
"Name", |
|||
"Key", |
|||
0.3m |
|||
)); |
|||
|
|||
orderRepository.GetAsync(OrderTestData.Order1Id).Returns(Task.FromResult(Order1)); |
|||
|
|||
services.AddTransient(_ => orderRepository); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Cancel_Order_If_Reduction_Failed_After_Placed() |
|||
{ |
|||
typeof(Order).GetProperty(nameof(Order.CanceledTime))!.SetValue(Order1, null); |
|||
typeof(Order).GetProperty(nameof(Order.CancellationReason))!.SetValue(Order1, null); |
|||
Order1.SetReducedInventoryAfterPlacingTime(null); |
|||
Order1.SetReducedInventoryAfterPaymentTime(null); |
|||
Order1.SetOrderStatus(OrderStatus.Pending); |
|||
Order1.SetPaymentId(null); |
|||
Order1.SetPaidTime(null); |
|||
|
|||
var handler = ServiceProvider.GetRequiredService<ProductInventoryReductionEventHandler>(); |
|||
|
|||
await handler.HandleEventAsync(new ProductInventoryReductionAfterOrderPlacedResultEto() |
|||
{ |
|||
TenantId = null, |
|||
OrderId = OrderTestData.Order1Id, |
|||
IsSuccess = false |
|||
}); |
|||
|
|||
Order1.CanceledTime.ShouldNotBeNull(); |
|||
Order1.CancellationReason.ShouldBe(OrdersConsts.InventoryReductionFailedAutoCancellationReason); |
|||
Order1.ReducedInventoryAfterPlacingTime.ShouldBeNull(); |
|||
Order1.ReducedInventoryAfterPaymentTime.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Cancel_Order_If_Reduction_Succeeded_After_Placed() |
|||
{ |
|||
typeof(Order).GetProperty(nameof(Order.CanceledTime))!.SetValue(Order1, null); |
|||
typeof(Order).GetProperty(nameof(Order.CancellationReason))!.SetValue(Order1, null); |
|||
Order1.SetReducedInventoryAfterPlacingTime(null); |
|||
Order1.SetReducedInventoryAfterPaymentTime(null); |
|||
Order1.SetOrderStatus(OrderStatus.Pending); |
|||
Order1.SetPaymentId(null); |
|||
Order1.SetPaidTime(null); |
|||
|
|||
var handler = ServiceProvider.GetRequiredService<ProductInventoryReductionEventHandler>(); |
|||
|
|||
await handler.HandleEventAsync(new ProductInventoryReductionAfterOrderPlacedResultEto() |
|||
{ |
|||
TenantId = null, |
|||
OrderId = OrderTestData.Order1Id, |
|||
IsSuccess = true |
|||
}); |
|||
|
|||
Order1.CanceledTime.ShouldBeNull(); |
|||
Order1.CancellationReason.ShouldBeNull(); |
|||
Order1.ReducedInventoryAfterPlacingTime.ShouldNotBeNull(); |
|||
Order1.ReducedInventoryAfterPaymentTime.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Cancel_Order_And_Refund_If_Reduction_Failed_After_Paid() |
|||
{ |
|||
typeof(Order).GetProperty(nameof(Order.CanceledTime))!.SetValue(Order1, null); |
|||
typeof(Order).GetProperty(nameof(Order.CancellationReason))!.SetValue(Order1, null); |
|||
Order1.SetReducedInventoryAfterPlacingTime(DateTime.Now); |
|||
Order1.SetReducedInventoryAfterPaymentTime(null); |
|||
Order1.SetOrderStatus(OrderStatus.Processing); |
|||
Order1.SetPaymentId(OrderTestData.Payment1Id); |
|||
Order1.SetPaidTime(DateTime.Now); |
|||
|
|||
var handler = ServiceProvider.GetRequiredService<ProductInventoryReductionEventHandler>(); |
|||
|
|||
await handler.HandleEventAsync(new ProductInventoryReductionAfterOrderPaidResultEto() |
|||
{ |
|||
TenantId = null, |
|||
OrderId = OrderTestData.Order1Id, |
|||
IsSuccess = false |
|||
}); |
|||
|
|||
var eventData = TestRefundOrderEventHandler.LastEto; |
|||
TestRefundOrderEventHandler.LastEto = null; |
|||
eventData.ShouldNotBeNull(); |
|||
eventData.DisplayReason.ShouldBe(OrdersConsts.InventoryReductionFailedAutoCancellationReason); |
|||
eventData.StaffRemark.ShouldBe(OrdersConsts.InventoryReductionFailedAutoCancellationReason); |
|||
eventData.CustomerRemark.ShouldBe(OrdersConsts.InventoryReductionFailedAutoCancellationReason); |
|||
eventData.PaymentId.ShouldBe(OrderTestData.Payment1Id); |
|||
eventData.TenantId.ShouldBeNull(); |
|||
eventData.OrderId.ShouldBe(OrderTestData.Order1Id); |
|||
|
|||
eventData.OrderLines.Count.ShouldBe(1); |
|||
var orderLine = eventData.OrderLines[0]; |
|||
orderLine.OrderLineId.ShouldBe(OrderTestData.OrderLine1Id); |
|||
orderLine.Quantity.ShouldBe(2); |
|||
orderLine.TotalAmount.ShouldBe(1m); |
|||
|
|||
eventData.OrderExtraFees.Count.ShouldBe(1); |
|||
var orderExtraFee = eventData.OrderExtraFees[0]; |
|||
orderExtraFee.Name.ShouldBe("Name"); |
|||
orderExtraFee.Key.ShouldBe("Key"); |
|||
orderExtraFee.TotalAmount.ShouldBe(0.3m); |
|||
|
|||
Order1.CanceledTime.ShouldNotBeNull(); |
|||
Order1.CancellationReason.ShouldBe(OrdersConsts.InventoryReductionFailedAutoCancellationReason); |
|||
Order1.ReducedInventoryAfterPlacingTime.ShouldNotBeNull(); |
|||
Order1.ReducedInventoryAfterPaymentTime.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Cancel_And_Refund_Order_If_Reduction_Succeeded_After_Paid() |
|||
{ |
|||
typeof(Order).GetProperty(nameof(Order.CanceledTime))!.SetValue(Order1, null); |
|||
typeof(Order).GetProperty(nameof(Order.CancellationReason))!.SetValue(Order1, null); |
|||
Order1.SetReducedInventoryAfterPlacingTime(DateTime.Now); |
|||
Order1.SetReducedInventoryAfterPaymentTime(null); |
|||
Order1.SetOrderStatus(OrderStatus.Processing); |
|||
Order1.SetPaymentId(OrderTestData.Payment1Id); |
|||
Order1.SetPaidTime(DateTime.Now); |
|||
|
|||
var handler = ServiceProvider.GetRequiredService<ProductInventoryReductionEventHandler>(); |
|||
|
|||
await handler.HandleEventAsync(new ProductInventoryReductionAfterOrderPaidResultEto() |
|||
{ |
|||
TenantId = null, |
|||
OrderId = OrderTestData.Order1Id, |
|||
IsSuccess = true |
|||
}); |
|||
|
|||
var eventData = TestRefundOrderEventHandler.LastEto; |
|||
TestRefundOrderEventHandler.LastEto = null; |
|||
eventData.ShouldBeNull(); |
|||
|
|||
Order1.CanceledTime.ShouldBeNull(); |
|||
Order1.CancellationReason.ShouldBeNull(); |
|||
Order1.ReducedInventoryAfterPlacingTime.ShouldNotBeNull(); |
|||
Order1.ReducedInventoryAfterPaymentTime.ShouldNotBeNull(); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Payments.Refunds; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
|
|||
namespace EasyAbp.EShop.Orders.Orders; |
|||
|
|||
public class TestRefundOrderEventHandler : IDistributedEventHandler<RefundOrderEto>, ITransientDependency |
|||
{ |
|||
public static RefundOrderEto LastEto { get; set; } |
|||
|
|||
public Task HandleEventAsync(RefundOrderEto eventData) |
|||
{ |
|||
LastEto = eventData; |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
@ -1,23 +1,9 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.ObjectExtending; |
|||
|
|||
namespace EasyAbp.EShop.Payments.Refunds.Dtos |
|||
{ |
|||
[Serializable] |
|||
public class CreateEShopRefundItemInput : ExtensibleObject |
|||
public class CreateEShopRefundItemInput : CreateEShopRefundItemInfoModel |
|||
{ |
|||
public Guid OrderId { get; set; } |
|||
|
|||
[CanBeNull] |
|||
public string CustomerRemark { get; set; } |
|||
|
|||
[CanBeNull] |
|||
public string StaffRemark { get; set; } |
|||
|
|||
public List<OrderLineRefundInfoModel> OrderLines { get; set; } = new(); |
|||
|
|||
public List<OrderExtraFeeRefundInfoModel> OrderExtraFees { get; set; } = new(); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.ObjectExtending; |
|||
|
|||
namespace EasyAbp.EShop.Payments.Refunds; |
|||
|
|||
[Serializable] |
|||
public class CreateEShopRefundItemInfoModel : ExtensibleObject |
|||
{ |
|||
public Guid OrderId { get; set; } |
|||
|
|||
[CanBeNull] |
|||
public string CustomerRemark { get; set; } |
|||
|
|||
[CanBeNull] |
|||
public string StaffRemark { get; set; } |
|||
|
|||
public List<OrderLineRefundInfoModel> OrderLines { get; set; } = new(); |
|||
|
|||
public List<OrderExtraFeeRefundInfoModel> OrderExtraFees { get; set; } = new(); |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace EasyAbp.EShop.Payments.Refunds; |
|||
|
|||
[Serializable] |
|||
public class RefundOrderEto : CreateEShopRefundItemInfoModel, IMultiTenant |
|||
{ |
|||
public Guid? TenantId { get; set; } |
|||
|
|||
public Guid StoreId { get; set; } |
|||
|
|||
public Guid PaymentId { get; set; } |
|||
|
|||
[CanBeNull] |
|||
public string DisplayReason { get; set; } |
|||
|
|||
protected RefundOrderEto() |
|||
{ |
|||
} |
|||
|
|||
public RefundOrderEto(Guid? tenantId, Guid orderId, Guid storeId, Guid paymentId, [CanBeNull] string displayReason, |
|||
[CanBeNull] string customerRemark, [CanBeNull] string staffRemark) |
|||
{ |
|||
TenantId = tenantId; |
|||
OrderId = orderId; |
|||
StoreId = storeId; |
|||
PaymentId = paymentId; |
|||
DisplayReason = displayReason; |
|||
CustomerRemark = customerRemark; |
|||
StaffRemark = staffRemark; |
|||
} |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Payments.Payments; |
|||
using EasyAbp.PaymentService.Payments; |
|||
using EasyAbp.PaymentService.Refunds; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.Json; |
|||
|
|||
namespace EasyAbp.EShop.Payments.Refunds; |
|||
|
|||
public class RefundOrderEventHandler : IDistributedEventHandler<RefundOrderEto>, ITransientDependency |
|||
{ |
|||
private readonly IJsonSerializer _jsonSerializer; |
|||
private readonly IPaymentRepository _paymentRepository; |
|||
private readonly IDistributedEventBus _distributedEventBus; |
|||
|
|||
public RefundOrderEventHandler( |
|||
IJsonSerializer jsonSerializer, |
|||
IPaymentRepository paymentRepository, |
|||
IDistributedEventBus distributedEventBus) |
|||
{ |
|||
_jsonSerializer = jsonSerializer; |
|||
_paymentRepository = paymentRepository; |
|||
_distributedEventBus = distributedEventBus; |
|||
} |
|||
|
|||
public virtual async Task HandleEventAsync(RefundOrderEto eventData) |
|||
{ |
|||
var refundAmount = eventData.OrderLines.Sum(x => x.TotalAmount) + |
|||
eventData.OrderExtraFees.Sum(x => x.TotalAmount); |
|||
|
|||
var payment = await _paymentRepository.GetAsync(eventData.PaymentId); |
|||
|
|||
var paymentItem = payment.PaymentItems.Single(x => x.ItemKey == eventData.OrderId.ToString()); |
|||
|
|||
var createRefundItemInput = new CreateRefundItemInput |
|||
{ |
|||
PaymentItemId = paymentItem.Id, |
|||
RefundAmount = refundAmount, |
|||
CustomerRemark = eventData.CustomerRemark, |
|||
StaffRemark = eventData.StaffRemark |
|||
}; |
|||
|
|||
createRefundItemInput.SetProperty(nameof(RefundItem.StoreId), eventData.StoreId); |
|||
createRefundItemInput.SetProperty(nameof(RefundItem.OrderId), eventData.OrderId); |
|||
createRefundItemInput.SetProperty(nameof(RefundItem.OrderLines), |
|||
_jsonSerializer.Serialize(eventData.OrderLines)); |
|||
createRefundItemInput.SetProperty(nameof(RefundItem.OrderExtraFees), |
|||
_jsonSerializer.Serialize(eventData.OrderExtraFees)); |
|||
|
|||
var eto = new RefundPaymentEto(eventData.TenantId, new CreateRefundInput |
|||
{ |
|||
PaymentId = eventData.PaymentId, |
|||
DisplayReason = eventData.DisplayReason, |
|||
CustomerRemark = eventData.CustomerRemark, |
|||
StaffRemark = eventData.StaffRemark, |
|||
RefundItems = new List<CreateRefundItemInput> { createRefundItemInput } |
|||
}); |
|||
|
|||
await _distributedEventBus.PublishAsync(eto); |
|||
} |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Payments.Payments; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using NSubstitute; |
|||
using NSubstitute.Core; |
|||
using Shouldly; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.Json; |
|||
using Xunit; |
|||
|
|||
namespace EasyAbp.EShop.Payments.Refunds; |
|||
|
|||
public class RefundOrderEventHandlerTests : PaymentsDomainTestBase |
|||
{ |
|||
private readonly IJsonSerializer _jsonSerializer; |
|||
|
|||
public RefundOrderEventHandlerTests() |
|||
{ |
|||
_jsonSerializer = ServiceProvider.GetRequiredService<IJsonSerializer>(); |
|||
} |
|||
|
|||
protected override void AfterAddApplication(IServiceCollection services) |
|||
{ |
|||
MockPaymentRepository(services); |
|||
} |
|||
|
|||
private static void MockPaymentRepository(IServiceCollection services) |
|||
{ |
|||
var paymentRepository = Substitute.For<IPaymentRepository>(); |
|||
|
|||
Payment Payment1Returns(CallInfo _) |
|||
{ |
|||
var paymentType = typeof(Payment); |
|||
var paymentItemType = typeof(PaymentItem); |
|||
|
|||
var paymentItem = Activator.CreateInstance(paymentItemType, true) as PaymentItem; |
|||
paymentItem.ShouldNotBeNull(); |
|||
paymentItemType.GetProperty(nameof(PaymentItem.Id))?.SetValue(paymentItem, PaymentsTestData.PaymentItem1); |
|||
paymentItemType.GetProperty(nameof(PaymentItem.ActualPaymentAmount))?.SetValue(paymentItem, 1m); |
|||
paymentItemType.GetProperty(nameof(PaymentItem.ItemType)) |
|||
?.SetValue(paymentItem, PaymentsConsts.PaymentItemType); |
|||
paymentItemType.GetProperty(nameof(PaymentItem.ItemKey)) |
|||
?.SetValue(paymentItem, PaymentsTestData.Order1.ToString()); |
|||
paymentItemType.GetProperty(nameof(PaymentItem.StoreId)) |
|||
?.SetValue(paymentItem, PaymentsTestData.Store1); |
|||
|
|||
var payment = Activator.CreateInstance(paymentType, true) as Payment; |
|||
payment.ShouldNotBeNull(); |
|||
paymentType.GetProperty(nameof(Payment.Id))?.SetValue(payment, PaymentsTestData.Payment1); |
|||
paymentType.GetProperty(nameof(Payment.Currency))?.SetValue(payment, "CNY"); |
|||
paymentType.GetProperty(nameof(Payment.ActualPaymentAmount))?.SetValue(payment, 1m); |
|||
paymentType.GetProperty(nameof(Payment.PaymentItems)) |
|||
?.SetValue(payment, new List<PaymentItem> { paymentItem }); |
|||
|
|||
return payment; |
|||
} |
|||
|
|||
paymentRepository.GetAsync(PaymentsTestData.Payment1).Returns(Payment1Returns); |
|||
paymentRepository.FindAsync(PaymentsTestData.Payment1).Returns(Payment1Returns); |
|||
|
|||
services.AddTransient(_ => paymentRepository); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Refund_Order() |
|||
{ |
|||
var handler = ServiceProvider.GetRequiredService<RefundOrderEventHandler>(); |
|||
|
|||
var eto = new RefundOrderEto(null, PaymentsTestData.Order1, PaymentsTestData.Store1, |
|||
PaymentsTestData.Payment1, "Test", null, null); |
|||
|
|||
eto.OrderLines.Add(new OrderLineRefundInfoModel |
|||
{ |
|||
OrderLineId = PaymentsTestData.OrderLine1, |
|||
Quantity = 2, |
|||
TotalAmount = 0.4m |
|||
}); |
|||
|
|||
eto.OrderExtraFees.Add(new OrderExtraFeeRefundInfoModel |
|||
{ |
|||
Name = "Name", |
|||
Key = "Key", |
|||
TotalAmount = 0.6m |
|||
}); |
|||
|
|||
await handler.HandleEventAsync(eto); |
|||
|
|||
var eventData = TestRefundPaymentEventHandler.LastEto; |
|||
TestRefundPaymentEventHandler.LastEto = null; |
|||
eventData.ShouldNotBeNull(); |
|||
eventData.CreateRefundInput.RefundItems.Count.ShouldBe(1); |
|||
|
|||
var refundItem = eventData.CreateRefundInput.RefundItems[0]; |
|||
refundItem.GetProperty<Guid?>(nameof(RefundItem.OrderId)).ShouldBe(PaymentsTestData.Order1); |
|||
|
|||
var orderLines = |
|||
_jsonSerializer.Deserialize<List<OrderLineRefundInfoModel>>( |
|||
refundItem.GetProperty<string>(nameof(RefundItem.OrderLines))); |
|||
|
|||
orderLines.Count.ShouldBe(1); |
|||
orderLines[0].OrderLineId.ShouldBe(PaymentsTestData.OrderLine1); |
|||
orderLines[0].Quantity.ShouldBe(2); |
|||
orderLines[0].TotalAmount.ShouldBe(0.4m); |
|||
|
|||
var orderExtraFees = |
|||
_jsonSerializer.Deserialize<List<OrderExtraFeeRefundInfoModel>>( |
|||
refundItem.GetProperty<string>(nameof(RefundItem.OrderExtraFees))); |
|||
|
|||
orderExtraFees.Count.ShouldBe(1); |
|||
orderExtraFees[0].Name.ShouldBe("Name"); |
|||
orderExtraFees[0].Key.ShouldBe("Key"); |
|||
orderExtraFees[0].TotalAmount.ShouldBe(0.6m); |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace EasyAbp.EShop.Products.ProductInventories.Dtos |
|||
{ |
|||
[Serializable] |
|||
public class ProductInventoryDto : ExtensibleFullAuditedEntityDto<Guid> |
|||
{ |
|||
public Guid ProductId { get; set; } |
|||
|
|||
public Guid ProductSkuId { get; set; } |
|||
|
|||
public int Inventory { get; set; } |
|||
|
|||
public long Sold { get; set; } |
|||
} |
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
using System; |
|||
using Volo.Abp.ObjectExtending; |
|||
|
|||
namespace EasyAbp.EShop.Products.ProductInventories.Dtos |
|||
{ |
|||
[Serializable] |
|||
public class UpdateProductInventoryDto : ExtensibleObject |
|||
{ |
|||
public Guid ProductId { get; set; } |
|||
|
|||
public Guid ProductSkuId { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Reduce inventory if the value is less than 0
|
|||
/// </summary>
|
|||
public int ChangedInventory { get; set; } |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Products.ProductInventories.Dtos; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace EasyAbp.EShop.Products.ProductInventories |
|||
{ |
|||
public interface IProductInventoryAppService : IApplicationService |
|||
{ |
|||
Task<ProductInventoryDto> GetAsync(Guid productId, Guid productSkuId); |
|||
|
|||
Task<ProductInventoryDto> UpdateAsync(UpdateProductInventoryDto input); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
using Volo.Abp.ObjectExtending; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products.Dtos; |
|||
|
|||
[Serializable] |
|||
public class ChangeProductInventoryDto : ExtensibleObject |
|||
{ |
|||
/// <summary>
|
|||
/// Reduce inventory if the value is less than 0
|
|||
/// </summary>
|
|||
public int ChangedInventory { get; set; } |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
using Volo.Abp.ObjectExtending; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products.Dtos; |
|||
|
|||
[Serializable] |
|||
public class ChangeProductInventoryResultDto : ExtensibleObject |
|||
{ |
|||
public bool Changed { get; set; } |
|||
|
|||
public int ChangedInventory { get; set; } |
|||
|
|||
public int CurrentInventory { get; set; } |
|||
} |
|||
@ -1,103 +0,0 @@ |
|||
using EasyAbp.EShop.Products.Permissions; |
|||
using EasyAbp.EShop.Products.ProductInventories.Dtos; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using EasyAbp.EShop.Stores.Authorization; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Domain.Entities; |
|||
|
|||
namespace EasyAbp.EShop.Products.ProductInventories |
|||
{ |
|||
public class ProductInventoryAppService : ApplicationService, IProductInventoryAppService |
|||
{ |
|||
private readonly IProductRepository _productRepository; |
|||
private readonly IProductInventoryRepository _repository; |
|||
private readonly DefaultProductInventoryProvider _productInventoryProvider; |
|||
|
|||
public ProductInventoryAppService( |
|||
IProductRepository productRepository, |
|||
IProductInventoryRepository repository, |
|||
DefaultProductInventoryProvider productInventoryProvider) |
|||
{ |
|||
_productRepository = productRepository; |
|||
_repository = repository; |
|||
_productInventoryProvider = productInventoryProvider; |
|||
} |
|||
|
|||
[Authorize(ProductsPermissions.ProductInventory.Default)] |
|||
public virtual async Task<ProductInventoryDto> GetAsync(Guid productId, Guid productSkuId) |
|||
{ |
|||
var productInventory = await _repository.FindAsync(x => x.ProductSkuId == productSkuId); |
|||
|
|||
if (productInventory == null) |
|||
{ |
|||
var product = await _productRepository.GetAsync(productId); |
|||
|
|||
if (!product.ProductSkus.Exists(x => x.Id == productSkuId)) |
|||
{ |
|||
throw new EntityNotFoundException(typeof(ProductSku), productSkuId); |
|||
} |
|||
|
|||
productInventory = new ProductInventory(GuidGenerator.Create(), CurrentTenant.Id, productId, |
|||
productSkuId, 0, 0); |
|||
|
|||
await _repository.InsertAsync(productInventory, true); |
|||
} |
|||
|
|||
return ObjectMapper.Map<ProductInventory, ProductInventoryDto>(productInventory); |
|||
} |
|||
|
|||
public virtual async Task<ProductInventoryDto> UpdateAsync(UpdateProductInventoryDto input) |
|||
{ |
|||
var product = await _productRepository.GetAsync(input.ProductId); |
|||
|
|||
if (!product.ProductSkus.Exists(x => x.Id == input.ProductSkuId)) |
|||
{ |
|||
throw new EntityNotFoundException(typeof(ProductSku), input.ProductSkuId); |
|||
} |
|||
|
|||
await AuthorizationService.CheckMultiStorePolicyAsync(product.StoreId, |
|||
ProductsPermissions.ProductInventory.Update, ProductsPermissions.ProductInventory.CrossStore); |
|||
|
|||
var productInventory = await _repository.FindAsync(x => x.ProductSkuId == input.ProductSkuId); |
|||
|
|||
if (productInventory == null) |
|||
{ |
|||
productInventory = |
|||
new ProductInventory(GuidGenerator.Create(), CurrentTenant.Id, input.ProductId, input.ProductSkuId, |
|||
0, 0); |
|||
|
|||
await _repository.InsertAsync(productInventory, true); |
|||
} |
|||
|
|||
await ChangeInventoryAsync(product, productInventory, input.ChangedInventory); |
|||
|
|||
return ObjectMapper.Map<ProductInventory, ProductInventoryDto>(productInventory); |
|||
} |
|||
|
|||
protected virtual async Task ChangeInventoryAsync(Product product, ProductInventory productInventory, |
|||
int changedInventory) |
|||
{ |
|||
if (changedInventory >= 0) |
|||
{ |
|||
if (!await _productInventoryProvider.TryIncreaseInventoryAsync(product, productInventory, |
|||
changedInventory, false)) |
|||
{ |
|||
throw new InventoryChangeFailedException(productInventory.ProductId, productInventory.ProductSkuId, |
|||
productInventory.Inventory, changedInventory); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (!await _productInventoryProvider.TryReduceInventoryAsync(product, productInventory, |
|||
-changedInventory, false)) |
|||
{ |
|||
throw new InventoryChangeFailedException(productInventory.ProductId, productInventory.ProductSkuId, |
|||
productInventory.Inventory, changedInventory); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace EasyAbp.EShop.Products.ProductInventories |
|||
{ |
|||
public interface IProductInventoryProvider |
|||
{ |
|||
string InventoryProviderName { get; } |
|||
|
|||
Task<InventoryDataModel> GetInventoryDataAsync(InventoryQueryModel model); |
|||
|
|||
Task<Dictionary<Guid, InventoryDataModel>> GetSkuIdInventoryDataMappingAsync(IList<InventoryQueryModel> models); |
|||
|
|||
Task<bool> TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, bool decreaseSold); |
|||
|
|||
Task<bool> TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold); |
|||
} |
|||
} |
|||
@ -1,4 +1,4 @@ |
|||
namespace EasyAbp.EShop.Products.Products |
|||
namespace EasyAbp.EShop.Products.ProductInventories |
|||
{ |
|||
public class InventoryDataModel |
|||
{ |
|||
@ -0,0 +1,28 @@ |
|||
using System; |
|||
using EasyAbp.EShop.Stores.Stores; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace EasyAbp.EShop.Products.ProductInventories; |
|||
|
|||
public class InventoryQueryModel : IMultiTenant, IMultiStore |
|||
{ |
|||
public Guid? TenantId { get; set; } |
|||
|
|||
public Guid StoreId { get; set; } |
|||
|
|||
public Guid ProductId { get; set; } |
|||
|
|||
public Guid ProductSkuId { get; set; } |
|||
|
|||
public InventoryQueryModel() |
|||
{ |
|||
} |
|||
|
|||
public InventoryQueryModel(Guid? tenantId, Guid storeId, Guid productId, Guid productSkuId) |
|||
{ |
|||
TenantId = tenantId; |
|||
StoreId = storeId; |
|||
ProductId = productId; |
|||
ProductSkuId = productSkuId; |
|||
} |
|||
} |
|||
@ -1,17 +1,21 @@ |
|||
using System; |
|||
using EasyAbp.EShop.Products.Options.InventoryProviders; |
|||
using EasyAbp.EShop.Products.Options.ProductGroups; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace EasyAbp.EShop.Products.Options |
|||
{ |
|||
public class EShopProductsOptions |
|||
{ |
|||
public ProductGroupConfigurations Groups { get; } |
|||
public ProductGroupConfigurations Groups { get; } = new(); |
|||
|
|||
public Type DefaultFileDownloadProviderType { get; set; } |
|||
|
|||
public EShopProductsOptions() |
|||
{ |
|||
Groups = new ProductGroupConfigurations(); |
|||
} |
|||
public InventoryProviderConfigurations InventoryProviders { get; } = new(); |
|||
|
|||
/// <summary>
|
|||
/// If the value is <c>null</c>, it will fall back to DefaultProductInventoryProviderName
|
|||
/// in the <see cref="DefaultProductInventoryProvider"/>.
|
|||
/// </summary>
|
|||
[CanBeNull] |
|||
public string DefaultInventoryProviderName { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace EasyAbp.EShop.Products.Options.InventoryProviders |
|||
{ |
|||
public interface IInventoryProviderConfigurationProvider |
|||
{ |
|||
InventoryProviderConfiguration Get(string providerName); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
|
|||
namespace EasyAbp.EShop.Products.Options.InventoryProviders |
|||
{ |
|||
public class InventoryProviderConfiguration |
|||
{ |
|||
public string DisplayName { get; set; } |
|||
|
|||
public string Description { get; set; } |
|||
|
|||
public Type ProviderType { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace EasyAbp.EShop.Products.Options.InventoryProviders |
|||
{ |
|||
public class InventoryProviderConfigurationProvider : IInventoryProviderConfigurationProvider, ITransientDependency |
|||
{ |
|||
private readonly EShopProductsOptions _options; |
|||
|
|||
public InventoryProviderConfigurationProvider(IOptions<EShopProductsOptions> options) |
|||
{ |
|||
_options = options.Value; |
|||
} |
|||
|
|||
public InventoryProviderConfiguration Get(string providerName) |
|||
{ |
|||
return _options.InventoryProviders.GetConfiguration(providerName); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp; |
|||
|
|||
namespace EasyAbp.EShop.Products.Options.InventoryProviders |
|||
{ |
|||
public class InventoryProviderConfigurations |
|||
{ |
|||
private readonly Dictionary<string, InventoryProviderConfiguration> _providers; |
|||
|
|||
public InventoryProviderConfigurations() |
|||
{ |
|||
_providers = new Dictionary<string, InventoryProviderConfiguration>(); |
|||
} |
|||
|
|||
public InventoryProviderConfigurations Configure( |
|||
[NotNull] string name, |
|||
[NotNull] Action<InventoryProviderConfiguration> configureAction) |
|||
{ |
|||
Check.NotNullOrWhiteSpace(name, nameof(name)); |
|||
Check.NotNull(configureAction, nameof(configureAction)); |
|||
|
|||
configureAction( |
|||
_providers.GetOrAdd( |
|||
name, |
|||
() => new InventoryProviderConfiguration() |
|||
) |
|||
); |
|||
|
|||
return this; |
|||
} |
|||
|
|||
public InventoryProviderConfigurations ConfigureAll( |
|||
Action<string, InventoryProviderConfiguration> configureAction) |
|||
{ |
|||
foreach (var provider in _providers) |
|||
{ |
|||
configureAction(provider.Key, provider.Value); |
|||
} |
|||
|
|||
return this; |
|||
} |
|||
|
|||
[NotNull] |
|||
public InventoryProviderConfiguration GetConfiguration([NotNull] string name) |
|||
{ |
|||
Check.NotNullOrWhiteSpace(name, nameof(name)); |
|||
|
|||
return _providers.GetOrDefault(name); |
|||
} |
|||
|
|||
[NotNull] |
|||
public Dictionary<string, InventoryProviderConfiguration> GetConfigurationsDictionary() |
|||
{ |
|||
return _providers; |
|||
} |
|||
} |
|||
} |
|||
@ -1,9 +1,18 @@ |
|||
namespace EasyAbp.EShop.Products.Options.ProductGroups |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace EasyAbp.EShop.Products.Options.ProductGroups |
|||
{ |
|||
public class ProductGroupConfiguration |
|||
{ |
|||
public string DisplayName { get; set; } |
|||
|
|||
|
|||
public string Description { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// If the value is <c>null</c>, it will fall back to DefaultInventoryProviderName
|
|||
/// in the <see cref="EShopProductsOptions"/>.
|
|||
/// </summary>
|
|||
[CanBeNull] |
|||
public string DefaultInventoryProviderName { get; set; } |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products |
|||
{ |
|||
public interface IProductInventoryProvider |
|||
{ |
|||
Task<InventoryDataModel> GetInventoryDataAsync(Product product, ProductSku productSku); |
|||
|
|||
Task<Dictionary<Guid, InventoryDataModel>> GetInventoryDataDictionaryAsync(Product product); |
|||
|
|||
Task<bool> TryIncreaseInventoryAsync(Product product, ProductSku productSku, int quantity, bool decreaseSold); |
|||
|
|||
Task<bool> TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold); |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Products.ProductInventories; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public interface IProductInventoryProviderResolver |
|||
{ |
|||
Task<bool> ExistProviderAsync([NotNull] string providerName); |
|||
|
|||
Task<IProductInventoryProvider> GetAsync(Product product); |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
using Volo.Abp; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products |
|||
{ |
|||
public class NonexistentInventoryProviderException : BusinessException |
|||
{ |
|||
public NonexistentInventoryProviderException(string inventoryProviderName) : |
|||
base(ProductsErrorCodes.NonexistentInventoryProvider) |
|||
{ |
|||
WithData(nameof(inventoryProviderName), inventoryProviderName); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Products.Options; |
|||
using EasyAbp.EShop.Products.ProductInventories; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public class ProductInventoryProviderResolver : IProductInventoryProviderResolver, ITransientDependency |
|||
{ |
|||
protected static ConcurrentDictionary<string, Type> NameToProviderTypeMapping { get; } = new(); |
|||
|
|||
protected IServiceProvider ServiceProvider { get; } |
|||
|
|||
public ProductInventoryProviderResolver(IServiceProvider serviceProvider) |
|||
{ |
|||
ServiceProvider = serviceProvider; |
|||
} |
|||
|
|||
public virtual Task<bool> ExistProviderAsync(string providerName) |
|||
{ |
|||
TryBuildNameToProviderTypeMapping(); |
|||
|
|||
return Task.FromResult(NameToProviderTypeMapping.ContainsKey(providerName)); |
|||
} |
|||
|
|||
public virtual Task<IProductInventoryProvider> GetAsync(Product product) |
|||
{ |
|||
if (!product.InventoryProviderName.IsNullOrWhiteSpace()) |
|||
{ |
|||
return Task.FromResult(GetProviderByName(product.InventoryProviderName)); |
|||
} |
|||
|
|||
var options = ServiceProvider.GetRequiredService<IOptions<EShopProductsOptions>>(); |
|||
var productGroupConfiguration = options.Value.Groups.GetConfiguration(product.ProductGroupName); |
|||
|
|||
if (!productGroupConfiguration.DefaultInventoryProviderName.IsNullOrWhiteSpace()) |
|||
{ |
|||
return Task.FromResult(GetProviderByName(productGroupConfiguration.DefaultInventoryProviderName)); |
|||
} |
|||
|
|||
return Task.FromResult(GetProviderByName(options.Value.DefaultInventoryProviderName)); |
|||
} |
|||
|
|||
protected virtual IProductInventoryProvider GetProviderByName([CanBeNull] string providerName) |
|||
{ |
|||
if (providerName.IsNullOrEmpty()) |
|||
{ |
|||
providerName = DefaultProductInventoryProvider.DefaultProductInventoryProviderName; |
|||
} |
|||
|
|||
TryBuildNameToProviderTypeMapping(); |
|||
|
|||
var providerType = NameToProviderTypeMapping[providerName!]; |
|||
|
|||
return (IProductInventoryProvider)ServiceProvider.GetService(providerType); |
|||
} |
|||
|
|||
protected virtual void TryBuildNameToProviderTypeMapping() |
|||
{ |
|||
if (!NameToProviderTypeMapping.IsEmpty) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var options = ServiceProvider.GetRequiredService<IOptions<EShopProductsOptions>>().Value; |
|||
|
|||
foreach (var pair in options.InventoryProviders.GetConfigurationsDictionary()) |
|||
{ |
|||
NameToProviderTypeMapping[pair.Key] = pair.Value.ProviderType; |
|||
} |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Products.ProductInventories.Dtos; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace EasyAbp.EShop.Products.ProductInventories |
|||
{ |
|||
[RemoteService(Name = EShopProductsRemoteServiceConsts.RemoteServiceName)] |
|||
[Route("/api/e-shop/products/product-inventory")] |
|||
public class ProductInventoryController : ProductsController, IProductInventoryAppService |
|||
{ |
|||
private readonly IProductInventoryAppService _service; |
|||
|
|||
public ProductInventoryController(IProductInventoryAppService service) |
|||
{ |
|||
_service = service; |
|||
} |
|||
|
|||
[HttpGet] |
|||
public Task<ProductInventoryDto> GetAsync(Guid productId, Guid productSkuId) |
|||
{ |
|||
return _service.GetAsync(productId, productSkuId); |
|||
} |
|||
|
|||
[HttpPut] |
|||
public Task<ProductInventoryDto> UpdateAsync(UpdateProductInventoryDto input) |
|||
{ |
|||
return _service.UpdateAsync(input); |
|||
} |
|||
} |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
using Shouldly; |
|||
using System.Threading.Tasks; |
|||
using Xunit; |
|||
|
|||
namespace EasyAbp.EShop.Products.ProductInventories |
|||
{ |
|||
public class ProductInventoryAppServiceTests : ProductsApplicationTestBase |
|||
{ |
|||
private readonly IProductInventoryAppService _productInventoryAppService; |
|||
|
|||
public ProductInventoryAppServiceTests() |
|||
{ |
|||
_productInventoryAppService = GetRequiredService<IProductInventoryAppService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Test1() |
|||
{ |
|||
// Arrange
|
|||
|
|||
// Act
|
|||
|
|||
// Assert
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Products.ProductInventories; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace EasyAbp.EShop.Products; |
|||
|
|||
public class FakeProductInventoryProvider : IProductInventoryProvider, ITransientDependency |
|||
{ |
|||
public string InventoryProviderName { get; } = "Fake"; |
|||
|
|||
private static InventoryDataModel Model { get; } = new() |
|||
{ |
|||
Inventory = 9998, |
|||
Sold = 0 |
|||
}; |
|||
|
|||
public Task<InventoryDataModel> GetInventoryDataAsync(InventoryQueryModel model) |
|||
{ |
|||
return Task.FromResult(Model); |
|||
} |
|||
|
|||
public Task<Dictionary<Guid, InventoryDataModel>> GetSkuIdInventoryDataMappingAsync( |
|||
IList<InventoryQueryModel> models) |
|||
{ |
|||
var result = new Dictionary<Guid, InventoryDataModel>(); |
|||
|
|||
foreach (var model in models) |
|||
{ |
|||
result.Add(model.ProductSkuId, Model); |
|||
} |
|||
|
|||
return Task.FromResult(result); |
|||
} |
|||
|
|||
public Task<bool> TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, bool decreaseSold) |
|||
{ |
|||
Model.Inventory++; |
|||
|
|||
if (decreaseSold) |
|||
{ |
|||
Model.Sold--; |
|||
} |
|||
|
|||
return Task.FromResult(true); |
|||
} |
|||
|
|||
public Task<bool> TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold) |
|||
{ |
|||
Model.Inventory--; |
|||
|
|||
if (increaseSold) |
|||
{ |
|||
Model.Sold++; |
|||
} |
|||
|
|||
return Task.FromResult(true); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
../../../docs/plugins/inventories/dapr-actors/README.md |
|||
@ -0,0 +1,18 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Dapr.Actors" Version="$(DaprSdkVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\..\..\..\modules\EasyAbp.EShop.Products\src\EasyAbp.EShop.Products.Domain.Shared\EasyAbp.EShop.Products.Domain.Shared.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,11 @@ |
|||
using EasyAbp.EShop.Products; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Inventories.DaprActors; |
|||
|
|||
[DependsOn( |
|||
typeof(EShopProductsDomainSharedModule) |
|||
)] |
|||
public class EShopPluginsInventoriesDaprActorsAbstractionsModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System.Threading.Tasks; |
|||
using Dapr.Actors; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Inventories.DaprActors; |
|||
|
|||
public interface IInventoryActor : IActor |
|||
{ |
|||
Task<InventoryStateModel> GetInventoryStateAsync(); |
|||
|
|||
Task IncreaseInventoryAsync(int quantity, bool decreaseSold); |
|||
|
|||
Task ReduceInventoryAsync(int quantity, bool increaseSold); |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace EasyAbp.EShop.Plugins.Inventories.DaprActors; |
|||
|
|||
public class InventoryStateModel |
|||
{ |
|||
public int Inventory { get; set; } |
|||
|
|||
public long Sold { get; set; } |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\EasyAbp.EShop.Plugins.Inventories.DaprActors\EasyAbp.EShop.Plugins.Inventories.DaprActors.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.AspNetCore" Version="$(AbpVersion)" /> |
|||
<PackageReference Include="Dapr.Actors.AspNetCore" Version="$(DaprSdkVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,29 @@ |
|||
using EasyAbp.EShop.Products; |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.AspNetCore.Routing; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.AspNetCore; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Inventories.DaprActors; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpAspNetCoreModule), |
|||
typeof(EShopPluginsInventoriesDaprActorsModule) |
|||
)] |
|||
public class EShopPluginsInventoriesDaprActorsAspNetCoreModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddActors(options => { options.Actors.RegisterActor<InventoryActor>(); }); |
|||
|
|||
Configure<AbpEndpointRouterOptions>(options => |
|||
{ |
|||
options.EndpointConfigureActions.Add(ctx => |
|||
{ |
|||
// Register actors handlers that interface with the Dapr runtime.
|
|||
ctx.Endpoints.MapActorsHandlers(); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,18 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions\EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Dapr.Actors" Version="$(DaprSdkVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,10 @@ |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Inventories.DaprActors; |
|||
|
|||
[DependsOn( |
|||
typeof(EShopPluginsInventoriesDaprActorsAbstractionsModule) |
|||
)] |
|||
public class EShopPluginsInventoriesDaprActorsModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
using System.Threading.Tasks; |
|||
using Dapr; |
|||
using Dapr.Actors.Runtime; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Inventories.DaprActors; |
|||
|
|||
public class InventoryActor : Actor, IInventoryActor |
|||
{ |
|||
public static string InventoryStateName { get; set; } = "i"; |
|||
|
|||
public InventoryActor(ActorHost host) : base(host) |
|||
{ |
|||
} |
|||
|
|||
protected override async Task OnActivateAsync() |
|||
{ |
|||
await StateManager.TryAddStateAsync(InventoryStateName, new InventoryStateModel()); |
|||
} |
|||
|
|||
public virtual async Task<InventoryStateModel> GetInventoryStateAsync() |
|||
{ |
|||
return await StateManager.GetStateAsync<InventoryStateModel>(InventoryStateName); |
|||
} |
|||
|
|||
public virtual async Task IncreaseInventoryAsync(int quantity, bool decreaseSold) |
|||
{ |
|||
var state = await GetInventoryStateAsync(); |
|||
|
|||
InternalIncreaseInventory(state, quantity, decreaseSold); |
|||
|
|||
await SetInventoryStateAsync(state); |
|||
} |
|||
|
|||
public async Task ReduceInventoryAsync(int quantity, bool increaseSold) |
|||
{ |
|||
var state = await GetInventoryStateAsync(); |
|||
|
|||
InternalReduceInventory(state, quantity, increaseSold); |
|||
|
|||
await SetInventoryStateAsync(state); |
|||
} |
|||
|
|||
protected virtual async Task SetInventoryStateAsync(InventoryStateModel state) |
|||
{ |
|||
await StateManager.SetStateAsync(InventoryStateName, state); |
|||
} |
|||
|
|||
protected virtual void InternalIncreaseInventory(InventoryStateModel stateModel, int quantity, bool decreaseSold) |
|||
{ |
|||
if (quantity < 0) |
|||
{ |
|||
throw new DaprException("Quantity should not be less than 0."); |
|||
} |
|||
|
|||
if (decreaseSold && stateModel.Sold - quantity < 0) |
|||
{ |
|||
throw new DaprException("Target Sold cannot be less than 0."); |
|||
} |
|||
|
|||
stateModel.Inventory = checked(stateModel.Inventory + quantity); |
|||
|
|||
if (decreaseSold) |
|||
{ |
|||
stateModel.Sold -= quantity; |
|||
} |
|||
} |
|||
|
|||
protected virtual void InternalReduceInventory(InventoryStateModel stateModel, int quantity, bool increaseSold) |
|||
{ |
|||
if (quantity < 0) |
|||
{ |
|||
throw new DaprException("Quantity should not be less than 0."); |
|||
} |
|||
|
|||
if (quantity > stateModel.Inventory) |
|||
{ |
|||
throw new DaprException("Insufficient inventory."); |
|||
} |
|||
|
|||
if (increaseSold) |
|||
{ |
|||
stateModel.Sold = checked(stateModel.Sold + quantity); |
|||
} |
|||
|
|||
stateModel.Inventory -= quantity; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,15 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\..\..\..\modules\EasyAbp.EShop.Products\src\EasyAbp.EShop.Products.Domain\EasyAbp.EShop.Products.Domain.csproj" /> |
|||
<ProjectReference Include="..\EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions\EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,111 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Dapr.Actors; |
|||
using EasyAbp.EShop.Plugins.Inventories.DaprActors; |
|||
using EasyAbp.EShop.Products.ProductInventories; |
|||
using Microsoft.Extensions.Logging; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace EasyAbp.EShop.Products.DaprActorsInventory; |
|||
|
|||
public class DaprActorsProductInventoryProvider : IProductInventoryProvider, ITransientDependency |
|||
{ |
|||
public static string DaprActorsProductInventoryProviderName { get; set; } = "DaprActors"; |
|||
public static string DaprActorsProductInventoryProviderDisplayName { get; set; } = "DaprActors"; |
|||
public static string DaprActorsProductInventoryProviderDescription { get; set; } = "DaprActors"; |
|||
|
|||
public string InventoryProviderName { get; } = DaprActorsProductInventoryProviderName; |
|||
|
|||
private readonly ILogger<DaprActorsProductInventoryProvider> _logger; |
|||
protected IInventoryActorProvider InventoryActorProvider { get; } |
|||
|
|||
public DaprActorsProductInventoryProvider( |
|||
IInventoryActorProvider inventoryActorProvider, |
|||
ILogger<DaprActorsProductInventoryProvider> logger) |
|||
{ |
|||
InventoryActorProvider = inventoryActorProvider; |
|||
_logger = logger; |
|||
} |
|||
|
|||
public virtual async Task<InventoryDataModel> GetInventoryDataAsync(InventoryQueryModel model) |
|||
{ |
|||
var actor = await GetActorAsync(model); |
|||
|
|||
var stateModel = await actor.GetInventoryStateAsync(); |
|||
|
|||
return new InventoryDataModel |
|||
{ |
|||
Inventory = stateModel.Inventory, |
|||
Sold = stateModel.Sold |
|||
}; |
|||
} |
|||
|
|||
public virtual async Task<Dictionary<Guid, InventoryDataModel>> GetSkuIdInventoryDataMappingAsync( |
|||
IList<InventoryQueryModel> models) |
|||
{ |
|||
var result = new Dictionary<Guid, InventoryDataModel>(); |
|||
|
|||
foreach (var model in models) |
|||
{ |
|||
result.Add(model.ProductSkuId, await GetInventoryDataAsync(model)); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public virtual async Task<bool> TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity, |
|||
bool decreaseSold) |
|||
{ |
|||
var actor = await GetActorAsync(model); |
|||
|
|||
try |
|||
{ |
|||
await actor.IncreaseInventoryAsync(quantity, decreaseSold); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
_logger.LogError("Actor threw: {Message}", e.Message); |
|||
|
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public virtual async Task<bool> TryReduceInventoryAsync(InventoryQueryModel model, int quantity, bool increaseSold) |
|||
{ |
|||
var actor = await GetActorAsync(model); |
|||
|
|||
var stateModel = await actor.GetInventoryStateAsync(); |
|||
|
|||
if (stateModel.Inventory < quantity) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
await actor.ReduceInventoryAsync(quantity, increaseSold); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
_logger.LogError("Actor threw: {Message}", e.Message); |
|||
|
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
protected virtual async Task<IInventoryActor> GetActorAsync(InventoryQueryModel model) |
|||
{ |
|||
return await InventoryActorProvider.GetAsync(GetActorId(model)); |
|||
} |
|||
|
|||
protected virtual ActorId GetActorId(InventoryQueryModel model) |
|||
{ |
|||
return new ActorId( |
|||
$"eshop_inventory_{(model.TenantId.HasValue ? model.TenantId.Value : "host")}_{model.ProductSkuId}"); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue