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; |
||||
using System.Collections.Generic; |
|
||||
using JetBrains.Annotations; |
|
||||
using Volo.Abp.ObjectExtending; |
|
||||
|
|
||||
namespace EasyAbp.EShop.Payments.Refunds.Dtos |
namespace EasyAbp.EShop.Payments.Refunds.Dtos |
||||
{ |
{ |
||||
[Serializable] |
[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 |
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.Options.ProductGroups; |
||||
|
using EasyAbp.EShop.Products.Products; |
||||
|
using JetBrains.Annotations; |
||||
|
|
||||
namespace EasyAbp.EShop.Products.Options |
namespace EasyAbp.EShop.Products.Options |
||||
{ |
{ |
||||
public class EShopProductsOptions |
public class EShopProductsOptions |
||||
{ |
{ |
||||
public ProductGroupConfigurations Groups { get; } |
public ProductGroupConfigurations Groups { get; } = new(); |
||||
|
|
||||
public Type DefaultFileDownloadProviderType { get; set; } |
public InventoryProviderConfigurations InventoryProviders { get; } = new(); |
||||
|
|
||||
public EShopProductsOptions() |
/// <summary>
|
||||
{ |
/// If the value is <c>null</c>, it will fall back to DefaultProductInventoryProviderName
|
||||
Groups = new ProductGroupConfigurations(); |
/// 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 class ProductGroupConfiguration |
||||
{ |
{ |
||||
public string DisplayName { get; set; } |
public string DisplayName { get; set; } |
||||
|
|
||||
public string Description { 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