Browse Source

Merge branch 'dev' into booking-plugin

# Conflicts:
#	Directory.Build.props
#	EShop.sln
#	docs/README.md
#	samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/EShopSampleDbContextModelSnapshot.cs
pull/170/head
gdlcf88 4 years ago
parent
commit
be8a132812
  1. 2
      Directory.Build.props
  2. 91
      EShop.sln
  3. 1
      EShop.sln.DotSettings
  4. 5
      docs/README.md
  5. 40
      docs/plugins/inventories/dapr-actors/README.md
  6. 51
      docs/plugins/inventories/orleans-grains/README.md
  7. 3
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/OrdersConsts.cs
  8. 8
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderAutoCancelOnCreatedHandler.cs
  9. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderAutoCancelOnUpdatedHandler.cs
  10. 1
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderManager.cs
  11. 72
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/ProductInventoryReductionEventHandler.cs
  12. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/UnpaidOrderAutoCancelJob.cs
  13. 31
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderAppServiceTests.cs
  14. 6
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/TestOrderLinePriceOverrider.cs
  15. 192
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/InventoryReductionResultTests.cs
  16. 18
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/TestRefundOrderEventHandler.cs
  17. 18
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.TestBase/OrderTestData.cs
  18. 16
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application.Contracts/EasyAbp/EShop/Payments/Refunds/Dtos/CreateEShopRefundItemInput.cs
  19. 22
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Refunds/CreateEShopRefundItemInfoModel.cs
  20. 34
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Refunds/RefundOrderEto.cs
  21. 65
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain/EasyAbp/EShop/Payments/Refunds/RefundOrderEventHandler.cs
  22. 12
      modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Application.Tests/Refunds/RefundAppServiceTests.cs
  23. 116
      modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Domain.Tests/Refunds/RefundOrderEventHandlerTests.cs
  24. 13
      modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Domain.Tests/Refunds/TestRefundPaymentEventHandler.cs
  25. 17
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/ProductInventories/Dtos/ProductInventoryDto.cs
  26. 18
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/ProductInventories/Dtos/UpdateProductInventoryDto.cs
  27. 15
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/ProductInventories/IProductInventoryAppService.cs
  28. 13
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ChangeProductInventoryDto.cs
  29. 14
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ChangeProductInventoryResultDto.cs
  30. 5
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/CreateUpdateProductDto.cs
  31. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ProductDto.cs
  32. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ProductViewDto.cs
  33. 11
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/IProductAppService.cs
  34. 103
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/ProductInventories/ProductInventoryAppService.cs
  35. 133
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs
  36. 3
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/ProductsApplicationAutoMapperProfile.cs
  37. 3
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/cs.json
  38. 3
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/en.json
  39. 3
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/pl.json
  40. 3
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/pt-BR.json
  41. 3
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/sl.json
  42. 3
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/tr.json
  43. 3
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/vi.json
  44. 3
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/zh-Hans.json
  45. 5
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/zh-Hant.json
  46. 19
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs
  47. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryDataModel.cs
  48. 28
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryQueryModel.cs
  49. 15
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IProduct.cs
  50. 11
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductEto.cs
  51. 1
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductsErrorCodes.cs
  52. 16
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/EShopProductsDomainModule.cs
  53. 20
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Options/EShopProductsOptions.cs
  54. 7
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Options/InventoryProviders/IInventoryProviderConfigurationProvider.cs
  55. 13
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Options/InventoryProviders/InventoryProviderConfiguration.cs
  56. 20
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Options/InventoryProviders/InventoryProviderConfigurationProvider.cs
  57. 59
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Options/InventoryProviders/InventoryProviderConfigurations.cs
  58. 13
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Options/ProductGroups/ProductGroupConfiguration.cs
  59. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/ProductInventories/IProductInventoryRepository.cs
  60. 23
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ConsumeInventoryModel.cs
  61. 67
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs
  62. 17
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProvider.cs
  63. 12
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProviderResolver.cs
  64. 1
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs
  65. 14
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/NonexistentInventoryProviderException.cs
  66. 45
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs
  67. 55
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs
  68. 43
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/Product.cs
  69. 78
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductInventoryProviderResolver.cs
  70. 86
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs
  71. 24
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductView.cs
  72. 5
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/ProductInventories/ProductInventoryRepository.cs
  73. 33
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.HttpApi/EasyAbp/EShop/Products/ProductInventories/ProductInventoryController.cs
  74. 8
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.HttpApi/EasyAbp/EShop/Products/Products/ProductController.cs
  75. 6
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Web/Pages/EShop/Products/Products/Product/ViewModels/CreateEditProductViewModel.cs
  76. 21
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Web/Pages/EShop/Products/Products/ProductSku/ChangeInventoryModal.cshtml.cs
  77. 22
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Web/Pages/EShop/Products/Products/ProductSku/CreateModal.cshtml.cs
  78. 26
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/ProductInventories/ProductInventoryAppServiceTests.cs
  79. 48
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Domain.Tests/Products/ProductDomainTests.cs
  80. 16
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/EShopProductsTestBaseModule.cs
  81. 60
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/FakeProductInventoryProvider.cs
  82. 2
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/ProductsTestDataBuilder.cs
  83. 1
      plugins/Inventories/DaprActors/README.md
  84. 18
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions.csproj
  85. 11
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions/EasyAbp/EShop/Plugins/Inventories/DaprActors/EShopPluginsInventoriesDaprActorsAbstractionsModule.cs
  86. 13
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions/EasyAbp/EShop/Plugins/Inventories/DaprActors/IInventoryActor.cs
  87. 8
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions/EasyAbp/EShop/Plugins/Inventories/DaprActors/InventoryStateModel.cs
  88. 3
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions/FodyWeavers.xml
  89. 30
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions/FodyWeavers.xsd
  90. 19
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore/EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore.csproj
  91. 29
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore/EasyAbp/EShop/Plugins/Inventories/DaprActors/EShopPluginsInventoriesDaprActorsAspNetCoreModule.cs
  92. 3
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore/FodyWeavers.xml
  93. 30
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore/FodyWeavers.xsd
  94. 18
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors/EasyAbp.EShop.Plugins.Inventories.DaprActors.csproj
  95. 10
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors/EasyAbp/EShop/Plugins/Inventories/DaprActors/EShopPluginsInventoriesDaprActorsModule.cs
  96. 87
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors/EasyAbp/EShop/Plugins/Inventories/DaprActors/InventoryActor.cs
  97. 3
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors/FodyWeavers.xml
  98. 30
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors/FodyWeavers.xsd
  99. 15
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Products.DaprActorsInventory.Domain/EasyAbp.EShop.Products.DaprActorsInventory.Domain.csproj
  100. 111
      plugins/Inventories/DaprActors/src/EasyAbp.EShop.Products.DaprActorsInventory.Domain/EasyAbp/EShop/Products/DaprActorsInventory/DaprActorsProductInventoryProvider.cs

2
Directory.Build.props

@ -5,6 +5,8 @@
<EasyAbpAbpTreesModuleVersion>2.5.3</EasyAbpAbpTreesModuleVersion> <EasyAbpAbpTreesModuleVersion>2.5.3</EasyAbpAbpTreesModuleVersion>
<EasyAbpPaymentServiceModuleVersion>2.0.11</EasyAbpPaymentServiceModuleVersion> <EasyAbpPaymentServiceModuleVersion>2.0.11</EasyAbpPaymentServiceModuleVersion>
<EasyAbpAbpTagHelperPlusModuleVersion>1.0.0</EasyAbpAbpTagHelperPlusModuleVersion> <EasyAbpAbpTagHelperPlusModuleVersion>1.0.0</EasyAbpAbpTagHelperPlusModuleVersion>
<DaprSdkVersion>1.7.0</DaprSdkVersion>
<OrleansVersion>3.6.2</OrleansVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

91
EShop.sln

@ -341,6 +341,40 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupo
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Orders.Plugins.Coupons", "plugins\Coupons\src\EasyAbp.EShop.Orders.Plugins.Coupons\EasyAbp.EShop.Orders.Plugins.Coupons.csproj", "{3C385657-8365-470F-9F4F-30F31F9FCA42}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Orders.Plugins.Coupons", "plugins\Coupons\src\EasyAbp.EShop.Orders.Plugins.Coupons\EasyAbp.EShop.Orders.Plugins.Coupons.csproj", "{3C385657-8365-470F-9F4F-30F31F9FCA42}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Inventories", "Inventories", "{9AC27747-E175-487F-92C9-434DEE543273}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DaprActors", "DaprActors", "{6E6FE4B9-4117-4F57-B219-EE47E4046096}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Products.DaprActorsInventory.Domain", "plugins\Inventories\DaprActors\src\EasyAbp.EShop.Products.DaprActorsInventory.Domain\EasyAbp.EShop.Products.DaprActorsInventory.Domain.csproj", "{6CD1A8B5-8AB7-4A31-8333-024A7FB602D1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions", "plugins\Inventories\DaprActors\src\EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions\EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions.csproj", "{CF4DE32D-9629-4C48-9BE8-5B83A1C27291}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Inventories.DaprActors", "plugins\Inventories\DaprActors\src\EasyAbp.EShop.Plugins.Inventories.DaprActors\EasyAbp.EShop.Plugins.Inventories.DaprActors.csproj", "{B6F3ACD5-463E-4455-A094-057A82941A94}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F468A386-5660-4888-981A-6ECF15182D32}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{485204B1-7603-4EA0-B3A4-73CB89B0D5BC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Products.DaprActorsInventory.Domain.Tests", "plugins\Inventories\DaprActors\test\EasyAbp.EShop.Products.DaprActorsInventory.Domain.Tests\EasyAbp.EShop.Products.DaprActorsInventory.Domain.Tests.csproj", "{733C51A3-19C8-45C4-8B22-3FD40CAF4EFB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore", "plugins\Inventories\DaprActors\src\EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore\EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore.csproj", "{3F0EA314-CCF4-4BB2-A8C1-79FAE4442884}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Inventories.OrleansGrains", "plugins\Inventories\OrleansGrains\src\EasyAbp.EShop.Plugins.Inventories.OrleansGrains\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.csproj", "{83F6434F-74DC-4389-870D-46510E28C029}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OrleansGrains", "OrleansGrains", "{88D17635-75D7-48A1-B622-E6FB3DCACEF8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8E978749-7972-4703-8A94-6A90080C78DE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions", "plugins\Inventories\OrleansGrains\src\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions.csproj", "{AB3477DB-3457-4167-A086-BAD104D69604}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Silo", "plugins\Inventories\OrleansGrains\src\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Silo\EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Silo.csproj", "{0D613460-A0AD-4EAF-B719-785FE65E97E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Products.OrleansGrainsInventory.Domain", "plugins\Inventories\OrleansGrains\src\EasyAbp.EShop.Products.OrleansGrainsInventory.Domain\EasyAbp.EShop.Products.OrleansGrainsInventory.Domain.csproj", "{DB1C55BF-0C0D-488C-9AFC-992A3DED2EAD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F58B6EEF-5AFF-4B79-BC71-A2D8C71F5E77}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Products.OrleansGrainsInventory.Domain.Tests", "plugins\Inventories\OrleansGrains\test\EasyAbp.EShop.Products.OrleansGrainsInventory.Domain.Tests\EasyAbp.EShop.Products.OrleansGrainsInventory.Domain.Tests.csproj", "{D652EBF0-27CA-44C2-BB78-F446B87377C7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Booking", "Booking", "{CE945F1D-6636-47D5-A619-C16C4E14CF8C}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Booking", "Booking", "{CE945F1D-6636-47D5-A619-C16C4E14CF8C}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A8C4583C-034E-47AF-B7EC-1A34EE288E2F}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A8C4583C-034E-47AF-B7EC-1A34EE288E2F}"
@ -943,6 +977,46 @@ Global
{3C385657-8365-470F-9F4F-30F31F9FCA42}.Debug|Any CPU.Build.0 = Debug|Any CPU {3C385657-8365-470F-9F4F-30F31F9FCA42}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C385657-8365-470F-9F4F-30F31F9FCA42}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C385657-8365-470F-9F4F-30F31F9FCA42}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3C385657-8365-470F-9F4F-30F31F9FCA42}.Release|Any CPU.Build.0 = Release|Any CPU {3C385657-8365-470F-9F4F-30F31F9FCA42}.Release|Any CPU.Build.0 = Release|Any CPU
{6CD1A8B5-8AB7-4A31-8333-024A7FB602D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6CD1A8B5-8AB7-4A31-8333-024A7FB602D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6CD1A8B5-8AB7-4A31-8333-024A7FB602D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6CD1A8B5-8AB7-4A31-8333-024A7FB602D1}.Release|Any CPU.Build.0 = Release|Any CPU
{CF4DE32D-9629-4C48-9BE8-5B83A1C27291}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF4DE32D-9629-4C48-9BE8-5B83A1C27291}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF4DE32D-9629-4C48-9BE8-5B83A1C27291}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF4DE32D-9629-4C48-9BE8-5B83A1C27291}.Release|Any CPU.Build.0 = Release|Any CPU
{B6F3ACD5-463E-4455-A094-057A82941A94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B6F3ACD5-463E-4455-A094-057A82941A94}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6F3ACD5-463E-4455-A094-057A82941A94}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6F3ACD5-463E-4455-A094-057A82941A94}.Release|Any CPU.Build.0 = Release|Any CPU
{733C51A3-19C8-45C4-8B22-3FD40CAF4EFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{733C51A3-19C8-45C4-8B22-3FD40CAF4EFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{733C51A3-19C8-45C4-8B22-3FD40CAF4EFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{733C51A3-19C8-45C4-8B22-3FD40CAF4EFB}.Release|Any CPU.Build.0 = Release|Any CPU
{3F0EA314-CCF4-4BB2-A8C1-79FAE4442884}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F0EA314-CCF4-4BB2-A8C1-79FAE4442884}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F0EA314-CCF4-4BB2-A8C1-79FAE4442884}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F0EA314-CCF4-4BB2-A8C1-79FAE4442884}.Release|Any CPU.Build.0 = Release|Any CPU
{83F6434F-74DC-4389-870D-46510E28C029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{83F6434F-74DC-4389-870D-46510E28C029}.Debug|Any CPU.Build.0 = Debug|Any CPU
{83F6434F-74DC-4389-870D-46510E28C029}.Release|Any CPU.ActiveCfg = Release|Any CPU
{83F6434F-74DC-4389-870D-46510E28C029}.Release|Any CPU.Build.0 = Release|Any CPU
{AB3477DB-3457-4167-A086-BAD104D69604}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB3477DB-3457-4167-A086-BAD104D69604}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB3477DB-3457-4167-A086-BAD104D69604}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB3477DB-3457-4167-A086-BAD104D69604}.Release|Any CPU.Build.0 = Release|Any CPU
{0D613460-A0AD-4EAF-B719-785FE65E97E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D613460-A0AD-4EAF-B719-785FE65E97E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D613460-A0AD-4EAF-B719-785FE65E97E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D613460-A0AD-4EAF-B719-785FE65E97E8}.Release|Any CPU.Build.0 = Release|Any CPU
{DB1C55BF-0C0D-488C-9AFC-992A3DED2EAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DB1C55BF-0C0D-488C-9AFC-992A3DED2EAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DB1C55BF-0C0D-488C-9AFC-992A3DED2EAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DB1C55BF-0C0D-488C-9AFC-992A3DED2EAD}.Release|Any CPU.Build.0 = Release|Any CPU
{D652EBF0-27CA-44C2-BB78-F446B87377C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D652EBF0-27CA-44C2-BB78-F446B87377C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D652EBF0-27CA-44C2-BB78-F446B87377C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D652EBF0-27CA-44C2-BB78-F446B87377C7}.Release|Any CPU.Build.0 = Release|Any CPU
{10C98582-61EB-49B9-9E6B-83E90CA3795D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {10C98582-61EB-49B9-9E6B-83E90CA3795D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10C98582-61EB-49B9-9E6B-83E90CA3795D}.Debug|Any CPU.Build.0 = Debug|Any CPU {10C98582-61EB-49B9-9E6B-83E90CA3795D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10C98582-61EB-49B9-9E6B-83E90CA3795D}.Release|Any CPU.ActiveCfg = Release|Any CPU {10C98582-61EB-49B9-9E6B-83E90CA3795D}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -1193,6 +1267,23 @@ Global
{86CAD303-A0E5-42C9-89A5-61D9AAA4AD8F} = {4001814E-A67B-490D-9E13-2FB9A34B0A0B} {86CAD303-A0E5-42C9-89A5-61D9AAA4AD8F} = {4001814E-A67B-490D-9E13-2FB9A34B0A0B}
{B076C103-DF0B-464B-A9CB-4BE5CAFEE067} = {4001814E-A67B-490D-9E13-2FB9A34B0A0B} {B076C103-DF0B-464B-A9CB-4BE5CAFEE067} = {4001814E-A67B-490D-9E13-2FB9A34B0A0B}
{3C385657-8365-470F-9F4F-30F31F9FCA42} = {72F34527-9295-4F29-923E-4B075A4F31A2} {3C385657-8365-470F-9F4F-30F31F9FCA42} = {72F34527-9295-4F29-923E-4B075A4F31A2}
{9AC27747-E175-487F-92C9-434DEE543273} = {94CC5A11-DA0F-413C-96CA-01DB0FC426E0}
{6E6FE4B9-4117-4F57-B219-EE47E4046096} = {9AC27747-E175-487F-92C9-434DEE543273}
{F468A386-5660-4888-981A-6ECF15182D32} = {6E6FE4B9-4117-4F57-B219-EE47E4046096}
{B6F3ACD5-463E-4455-A094-057A82941A94} = {F468A386-5660-4888-981A-6ECF15182D32}
{CF4DE32D-9629-4C48-9BE8-5B83A1C27291} = {F468A386-5660-4888-981A-6ECF15182D32}
{6CD1A8B5-8AB7-4A31-8333-024A7FB602D1} = {F468A386-5660-4888-981A-6ECF15182D32}
{485204B1-7603-4EA0-B3A4-73CB89B0D5BC} = {6E6FE4B9-4117-4F57-B219-EE47E4046096}
{733C51A3-19C8-45C4-8B22-3FD40CAF4EFB} = {485204B1-7603-4EA0-B3A4-73CB89B0D5BC}
{3F0EA314-CCF4-4BB2-A8C1-79FAE4442884} = {F468A386-5660-4888-981A-6ECF15182D32}
{88D17635-75D7-48A1-B622-E6FB3DCACEF8} = {9AC27747-E175-487F-92C9-434DEE543273}
{8E978749-7972-4703-8A94-6A90080C78DE} = {88D17635-75D7-48A1-B622-E6FB3DCACEF8}
{83F6434F-74DC-4389-870D-46510E28C029} = {8E978749-7972-4703-8A94-6A90080C78DE}
{AB3477DB-3457-4167-A086-BAD104D69604} = {8E978749-7972-4703-8A94-6A90080C78DE}
{0D613460-A0AD-4EAF-B719-785FE65E97E8} = {8E978749-7972-4703-8A94-6A90080C78DE}
{DB1C55BF-0C0D-488C-9AFC-992A3DED2EAD} = {8E978749-7972-4703-8A94-6A90080C78DE}
{F58B6EEF-5AFF-4B79-BC71-A2D8C71F5E77} = {88D17635-75D7-48A1-B622-E6FB3DCACEF8}
{D652EBF0-27CA-44C2-BB78-F446B87377C7} = {F58B6EEF-5AFF-4B79-BC71-A2D8C71F5E77}
{CE945F1D-6636-47D5-A619-C16C4E14CF8C} = {94CC5A11-DA0F-413C-96CA-01DB0FC426E0} {CE945F1D-6636-47D5-A619-C16C4E14CF8C} = {94CC5A11-DA0F-413C-96CA-01DB0FC426E0}
{A8C4583C-034E-47AF-B7EC-1A34EE288E2F} = {CE945F1D-6636-47D5-A619-C16C4E14CF8C} {A8C4583C-034E-47AF-B7EC-1A34EE288E2F} = {CE945F1D-6636-47D5-A619-C16C4E14CF8C}
{10C98582-61EB-49B9-9E6B-83E90CA3795D} = {A8C4583C-034E-47AF-B7EC-1A34EE288E2F} {10C98582-61EB-49B9-9E6B-83E90CA3795D} = {A8C4583C-034E-47AF-B7EC-1A34EE288E2F}

1
EShop.sln.DotSettings

@ -21,4 +21,5 @@
<s:String x:Key="/Default/CodeStyle/Generate/=Overrides/Options/=Mutable/@EntryIndexedValue">False</s:String> <s:String x:Key="/Default/CodeStyle/Generate/=Overrides/Options/=Mutable/@EntryIndexedValue">False</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SQL/@EntryIndexedValue">SQL</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SQL/@EntryIndexedValue">SQL</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Authorizer/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Authorizer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dapr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Skus/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=Skus/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

5
docs/README.md

@ -48,7 +48,7 @@ We have launched an online demo for this module: [https://eshop.samples.easyabp.
* Define a Product Group (optional) * Define a Product Group (optional)
* Product group is used to classify different types of products, so we can customize different behavior for them, for example, products of the "GiftCard" product group could automatically send the card number and password to the customer's mailbox. * Product group is used to classify different types of products, so we can customize different behavior for them, for example, products of the "GiftCard" product group could automatically send the card number and password to the customer's mailbox.
* EShop provides a [default product group](https://github.com/EasyAbp/EShop/blob/master/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/EShopProductsDomainModule.cs#L29-L36). * EShop provides a [default product group](https://github.com/EasyAbp/EShop/blob/master/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/EShopProductsDomainModule.cs#L33-L37).
* Refer to the configuration of the default product group and define a new product group. * Refer to the configuration of the default product group and define a new product group.
* Create a Product * Create a Product
@ -102,6 +102,9 @@ We can customize some features to use EShop in complex application scenarios.
* Plugin modules * Plugin modules
* Baskets * Baskets
* Coupons * Coupons
* Inventories
* [DaprActors](https://github.com/EasyAbp/EShop/tree/dev/plugins/Inventories/DaprActors)
* [OrleansGrains](https://github.com/EasyAbp/EShop/tree/dev/plugins/Inventories/OrleansGrains)
* [Booking](https://github.com/EasyAbp/EShop/tree/dev/plugins/Booking) * [Booking](https://github.com/EasyAbp/EShop/tree/dev/plugins/Booking)
## Roadmap ## Roadmap

40
docs/plugins/inventories/dapr-actors/README.md

@ -0,0 +1,40 @@
# EShop.Plugins.Inventories.DaprActors
[![ABP version](https://img.shields.io/badge/dynamic/xml?style=flat-square&color=yellow&label=abp&query=%2F%2FProject%2FPropertyGroup%2FAbpVersion&url=https%3A%2F%2Fraw.githubusercontent.com%2FEasyAbp%2FEShop%2Fmaster%2FDirectory.Build.props)](https://abp.io)
[![NuGet](https://img.shields.io/nuget/v/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions)
[![NuGet Download](https://img.shields.io/nuget/dt/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions)
[![Discord online](https://badgen.net/discord/online-members/S6QaezrCRq?label=Discord)](https://discord.gg/S6QaezrCRq)
[![GitHub stars](https://img.shields.io/github/stars/EasyAbp/EShop?style=social)](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.

51
docs/plugins/inventories/orleans-grains/README.md

@ -0,0 +1,51 @@
# EShop.Plugins.Inventories.OrleansGrains
[![ABP version](https://img.shields.io/badge/dynamic/xml?style=flat-square&color=yellow&label=abp&query=%2F%2FProject%2FPropertyGroup%2FAbpVersion&url=https%3A%2F%2Fraw.githubusercontent.com%2FEasyAbp%2FEShop%2Fmaster%2FDirectory.Build.props)](https://abp.io)
[![NuGet](https://img.shields.io/nuget/v/EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions)
[![NuGet Download](https://img.shields.io/nuget/dt/EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EasyAbp.EShop.Plugins.Inventories.OrleansGrains.Abstractions)
[![Discord online](https://badgen.net/discord/online-members/S6QaezrCRq?label=Discord)](https://discord.gg/S6QaezrCRq)
[![GitHub stars](https://img.shields.io/github/stars/EasyAbp/EShop?style=social)](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.

3
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/OrdersConsts.cs

@ -2,6 +2,7 @@
{ {
public static class OrdersConsts public static class OrdersConsts
{ {
public static string CancellationReason = "Order payment timed out and not paid"; public static string UnpaidAutoCancellationReason = "Order payment timed out and not paid";
public static string InventoryReductionFailedAutoCancellationReason = "Insufficient inventory";
} }
} }

8
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderAutoCancelOnCreatedHandler.cs

@ -3,6 +3,7 @@ using Volo.Abp.BackgroundJobs;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.EventBus.Distributed; using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Timing;
namespace EasyAbp.EShop.Orders.Orders namespace EasyAbp.EShop.Orders.Orders
{ {
@ -10,14 +11,17 @@ namespace EasyAbp.EShop.Orders.Orders
IDistributedEventHandler<EntityCreatedEto<OrderEto>>, IDistributedEventHandler<EntityCreatedEto<OrderEto>>,
ITransientDependency ITransientDependency
{ {
private readonly IClock _clock;
private readonly IBackgroundJobManager _backgroundJobManager; private readonly IBackgroundJobManager _backgroundJobManager;
public OrderAutoCancelOnCreatedHandler( public OrderAutoCancelOnCreatedHandler(
IClock clock,
IBackgroundJobManager backgroundJobManager) IBackgroundJobManager backgroundJobManager)
{ {
_clock = clock;
_backgroundJobManager = backgroundJobManager; _backgroundJobManager = backgroundJobManager;
} }
public virtual async Task HandleEventAsync(EntityCreatedEto<OrderEto> eventData) public virtual async Task HandleEventAsync(EntityCreatedEto<OrderEto> eventData)
{ {
if (!eventData.Entity.PaymentExpiration.HasValue) if (!eventData.Entity.PaymentExpiration.HasValue)
@ -33,7 +37,7 @@ namespace EasyAbp.EShop.Orders.Orders
await _backgroundJobManager.EnqueueAsync( await _backgroundJobManager.EnqueueAsync(
args: args, args: args,
delay: eventData.Entity.PaymentExpiration.Value.Subtract(eventData.Entity.CreationTime) // Todo: use a absolute time. delay: eventData.Entity.PaymentExpiration.Value.Subtract(_clock.Now) // Todo: use a absolute time.
); );
} }
} }

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderAutoCancelOnUpdatedHandler.cs

@ -32,7 +32,7 @@ namespace EasyAbp.EShop.Orders.Orders
if (_clock.Now > eventData.Entity.PaymentExpiration.Value && !eventData.Entity.CanceledTime.HasValue) if (_clock.Now > eventData.Entity.PaymentExpiration.Value && !eventData.Entity.CanceledTime.HasValue)
{ {
await _orderManager.CancelAsync(eventData.Entity, OrdersConsts.CancellationReason); await _orderManager.CancelAsync(eventData.Entity, OrdersConsts.UnpaidAutoCancellationReason);
} }
} }
} }

1
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderManager.cs

@ -56,7 +56,6 @@ namespace EasyAbp.EShop.Orders.Orders
return order; return order;
} }
// Todo: should handler the inventory rollback.
[UnitOfWork] [UnitOfWork]
public virtual async Task<Order> CancelAsync(Order order, string cancellationReason) public virtual async Task<Order> CancelAsync(Order order, string cancellationReason)
{ {

72
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/ProductInventoryReductionEventHandler.cs

@ -1,6 +1,12 @@
using System.Threading.Tasks; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.EShop.Payments.Refunds;
using EasyAbp.EShop.Products.Products; using EasyAbp.EShop.Products.Products;
using EasyAbp.PaymentService.Refunds;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.Timing; using Volo.Abp.Timing;
using Volo.Abp.Uow; using Volo.Abp.Uow;
@ -11,18 +17,24 @@ namespace EasyAbp.EShop.Orders.Orders
{ {
private readonly IClock _clock; private readonly IClock _clock;
private readonly ICurrentTenant _currentTenant; private readonly ICurrentTenant _currentTenant;
private readonly IOrderManager _orderManager;
private readonly IOrderRepository _orderRepository; private readonly IOrderRepository _orderRepository;
private readonly IDistributedEventBus _distributedEventBus;
public ProductInventoryReductionEventHandler( public ProductInventoryReductionEventHandler(
IClock clock, IClock clock,
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IOrderRepository orderRepository) IOrderManager orderManager,
IOrderRepository orderRepository,
IDistributedEventBus distributedEventBus)
{ {
_clock = clock; _clock = clock;
_currentTenant = currentTenant; _currentTenant = currentTenant;
_orderManager = orderManager;
_orderRepository = orderRepository; _orderRepository = orderRepository;
_distributedEventBus = distributedEventBus;
} }
[UnitOfWork(true)] [UnitOfWork(true)]
public virtual async Task HandleEventAsync(ProductInventoryReductionAfterOrderPlacedResultEto eventData) public virtual async Task HandleEventAsync(ProductInventoryReductionAfterOrderPlacedResultEto eventData)
{ {
@ -37,10 +49,11 @@ namespace EasyAbp.EShop.Orders.Orders
if (!eventData.IsSuccess) if (!eventData.IsSuccess)
{ {
// Todo: Cancel order. await _orderManager.CancelAsync(order, OrdersConsts.InventoryReductionFailedAutoCancellationReason);
return; return;
} }
order.SetReducedInventoryAfterPlacingTime(_clock.Now); order.SetReducedInventoryAfterPlacingTime(_clock.Now);
await _orderRepository.UpdateAsync(order, true); await _orderRepository.UpdateAsync(order, true);
@ -61,15 +74,58 @@ namespace EasyAbp.EShop.Orders.Orders
if (!eventData.IsSuccess) if (!eventData.IsSuccess)
{ {
// Todo: Refund. var refundOrderEto = CreateRefundOrderEto(order);
// Todo: Cancel order.
await _orderManager.CancelAsync(order, OrdersConsts.InventoryReductionFailedAutoCancellationReason);
await RefundOrderAsync(refundOrderEto);
return; return;
} }
order.SetReducedInventoryAfterPaymentTime(_clock.Now); order.SetReducedInventoryAfterPaymentTime(_clock.Now);
await _orderRepository.UpdateAsync(order, true); await _orderRepository.UpdateAsync(order, true);
} }
} }
[UnitOfWork(true)]
protected virtual async Task RefundOrderAsync(RefundOrderEto refundOrderEto)
{
await _distributedEventBus.PublishAsync(refundOrderEto);
}
protected virtual RefundOrderEto CreateRefundOrderEto(Order order)
{
if (!order.PaymentId.HasValue)
{
throw new OrderIsInWrongStageException(order.Id);
}
var eto = new RefundOrderEto(
order.TenantId,
order.Id,
order.StoreId,
order.PaymentId.Value,
OrdersConsts.InventoryReductionFailedAutoCancellationReason,
OrdersConsts.InventoryReductionFailedAutoCancellationReason,
OrdersConsts.InventoryReductionFailedAutoCancellationReason);
eto.OrderLines.AddRange(order.OrderLines.Select(x => new OrderLineRefundInfoModel
{
OrderLineId = x.Id,
Quantity = x.Quantity - x.RefundedQuantity,
TotalAmount = x.ActualTotalPrice - x.RefundAmount
}));
eto.OrderExtraFees.AddRange(order.OrderExtraFees.Select(x => new OrderExtraFeeRefundInfoModel
{
Name = x.Name,
Key = x.Key,
TotalAmount = x.Fee - x.RefundAmount
}));
return eto;
}
} }
} }

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/UnpaidOrderAutoCancelJob.cs

@ -46,7 +46,7 @@ namespace EasyAbp.EShop.Orders.Orders
} }
else else
{ {
await _orderManager.CancelAsync(order, OrdersConsts.CancellationReason); await _orderManager.CancelAsync(order, OrdersConsts.UnpaidAutoCancellationReason);
} }
} }
} }

31
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/OrderAppServiceTests.cs

@ -64,6 +64,17 @@ namespace EasyAbp.EShop.Orders.Orders
Price = 2m, Price = 2m,
Currency = "CNY", Currency = "CNY",
ProductDetailId = OrderTestData.ProductDetail2Id ProductDetailId = OrderTestData.ProductDetail2Id
},
new ProductSkuDto
{
Id = OrderTestData.ProductSku3Id,
Name = "My SKU 3",
OrderMinQuantity = 0,
OrderMaxQuantity = 100,
AttributeOptionIds = new List<Guid>(),
Price = 3m,
Currency = "CNY",
ProductDetailId = OrderTestData.ProductDetail2Id
} }
}, },
InventoryStrategy = InventoryStrategy.NoNeed, InventoryStrategy = InventoryStrategy.NoNeed,
@ -278,7 +289,7 @@ namespace EasyAbp.EShop.Orders.Orders
response.PaymentExpiration.ShouldBe(now); response.PaymentExpiration.ShouldBe(now);
response.OrderStatus.ShouldBe(OrderStatus.Canceled); response.OrderStatus.ShouldBe(OrderStatus.Canceled);
response.CanceledTime.ShouldNotBeNull(); response.CanceledTime.ShouldNotBeNull();
response.CancellationReason.ShouldBe(OrdersConsts.CancellationReason); response.CancellationReason.ShouldBe(OrdersConsts.UnpaidAutoCancellationReason);
UsingDbContext(db => UsingDbContext(db =>
{ {
@ -288,7 +299,7 @@ namespace EasyAbp.EShop.Orders.Orders
order.PaymentExpiration.ShouldBe(now); order.PaymentExpiration.ShouldBe(now);
order.OrderStatus.ShouldBe(OrderStatus.Canceled); order.OrderStatus.ShouldBe(OrderStatus.Canceled);
order.CanceledTime.ShouldNotBeNull(); order.CanceledTime.ShouldNotBeNull();
order.CancellationReason.ShouldBe(OrdersConsts.CancellationReason); order.CancellationReason.ShouldBe(OrdersConsts.UnpaidAutoCancellationReason);
}); });
} }
@ -392,7 +403,7 @@ namespace EasyAbp.EShop.Orders.Orders
response.PaymentExpiration.ShouldBe(now); response.PaymentExpiration.ShouldBe(now);
response.OrderStatus.ShouldBe(OrderStatus.Canceled); response.OrderStatus.ShouldBe(OrderStatus.Canceled);
response.CanceledTime.ShouldNotBeNull(); response.CanceledTime.ShouldNotBeNull();
response.CancellationReason.ShouldBe(OrdersConsts.CancellationReason); response.CancellationReason.ShouldBe(OrdersConsts.UnpaidAutoCancellationReason);
UsingDbContext(db => UsingDbContext(db =>
{ {
@ -402,7 +413,7 @@ namespace EasyAbp.EShop.Orders.Orders
order.PaymentExpiration.ShouldBe(now); order.PaymentExpiration.ShouldBe(now);
order.OrderStatus.ShouldBe(OrderStatus.Canceled); order.OrderStatus.ShouldBe(OrderStatus.Canceled);
order.CanceledTime.ShouldNotBeNull(); order.CanceledTime.ShouldNotBeNull();
order.CancellationReason.ShouldBe(OrdersConsts.CancellationReason); order.CancellationReason.ShouldBe(OrdersConsts.UnpaidAutoCancellationReason);
}); });
} }
@ -449,7 +460,7 @@ namespace EasyAbp.EShop.Orders.Orders
response.PaymentExpiration.ShouldBe(now); response.PaymentExpiration.ShouldBe(now);
response.OrderStatus.ShouldBe(OrderStatus.Canceled); response.OrderStatus.ShouldBe(OrderStatus.Canceled);
response.CanceledTime.ShouldNotBeNull(); response.CanceledTime.ShouldNotBeNull();
response.CancellationReason.ShouldBe(OrdersConsts.CancellationReason); response.CancellationReason.ShouldBe(OrdersConsts.UnpaidAutoCancellationReason);
UsingDbContext(db => UsingDbContext(db =>
{ {
@ -459,7 +470,7 @@ namespace EasyAbp.EShop.Orders.Orders
order.PaymentExpiration.ShouldBe(now); order.PaymentExpiration.ShouldBe(now);
order.OrderStatus.ShouldBe(OrderStatus.Canceled); order.OrderStatus.ShouldBe(OrderStatus.Canceled);
order.CanceledTime.ShouldNotBeNull(); order.CanceledTime.ShouldNotBeNull();
order.CancellationReason.ShouldBe(OrdersConsts.CancellationReason); order.CancellationReason.ShouldBe(OrdersConsts.UnpaidAutoCancellationReason);
}); });
} }
@ -481,7 +492,7 @@ namespace EasyAbp.EShop.Orders.Orders
new() new()
{ {
ProductId = OrderTestData.Product1Id, ProductId = OrderTestData.Product1Id,
ProductSkuId = OrderTestData.ProductSku2Id, ProductSkuId = OrderTestData.ProductSku3Id,
Quantity = 2 Quantity = 2
} }
} }
@ -490,11 +501,11 @@ namespace EasyAbp.EShop.Orders.Orders
await WithUnitOfWorkAsync(async () => await WithUnitOfWorkAsync(async () =>
{ {
var order = await _orderAppService.CreateAsync(createOrderDto); var order = await _orderAppService.CreateAsync(createOrderDto);
var orderLine = order.OrderLines.Find(x => x.ProductSkuId == OrderTestData.ProductSku2Id); var orderLine = order.OrderLines.Find(x => x.ProductSkuId == OrderTestData.ProductSku3Id);
order.ProductTotalPrice.ShouldBe(10 * 1m + 2 * TestOrderLinePriceOverrider.Sku2UnitPrice); order.ProductTotalPrice.ShouldBe(10 * 1m + 2 * TestOrderLinePriceOverrider.Sku3UnitPrice);
orderLine.ShouldNotBeNull(); orderLine.ShouldNotBeNull();
orderLine.UnitPrice.ShouldBe(TestOrderLinePriceOverrider.Sku2UnitPrice); orderLine.UnitPrice.ShouldBe(TestOrderLinePriceOverrider.Sku3UnitPrice);
orderLine.TotalPrice.ShouldBe(orderLine.Quantity * orderLine.UnitPrice); orderLine.TotalPrice.ShouldBe(orderLine.Quantity * orderLine.UnitPrice);
}); });
} }

6
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Application.Tests/Orders/TestOrderLinePriceOverrider.cs

@ -7,14 +7,14 @@ namespace EasyAbp.EShop.Orders.Orders;
public class TestOrderLinePriceOverrider : IOrderLinePriceOverrider, ITransientDependency public class TestOrderLinePriceOverrider : IOrderLinePriceOverrider, ITransientDependency
{ {
public static decimal Sku2UnitPrice { get; set; } = 100m; public static decimal Sku3UnitPrice { get; set; } = 100m;
public async Task<decimal?> GetUnitPriceOrNullAsync(CreateOrderDto input, CreateOrderLineDto inputOrderLine, public async Task<decimal?> GetUnitPriceOrNullAsync(CreateOrderDto input, CreateOrderLineDto inputOrderLine,
ProductDto product, ProductSkuDto productSku) ProductDto product, ProductSkuDto productSku)
{ {
if (inputOrderLine.ProductSkuId == OrderTestData.ProductSku2Id) if (inputOrderLine.ProductSkuId == OrderTestData.ProductSku3Id)
{ {
return Sku2UnitPrice; return Sku3UnitPrice;
} }
return null; return null;

192
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/InventoryReductionResultTests.cs

@ -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();
}
}

18
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/TestRefundOrderEventHandler.cs

@ -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;
}
}

18
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.TestBase/OrderTestData.cs

@ -6,25 +6,27 @@ namespace EasyAbp.EShop.Orders
public class OrderTestData public class OrderTestData
{ {
public static Guid Order1Id { get; } = Guid.NewGuid(); public static Guid Order1Id { get; } = Guid.NewGuid();
public static Guid OrderLine1Id { get; } = Guid.NewGuid(); public static Guid OrderLine1Id { get; } = Guid.NewGuid();
public static Guid Payment1Id { get; } = Guid.NewGuid(); public static Guid Payment1Id { get; } = Guid.NewGuid();
public static Guid Store1Id { get; } = Guid.NewGuid(); public static Guid Store1Id { get; } = Guid.NewGuid();
public static Guid Product1Id { get; } = Guid.NewGuid(); public static Guid Product1Id { get; } = Guid.NewGuid();
public static Guid ProductSku1Id { get; } = Guid.NewGuid(); public static Guid ProductSku1Id { get; } = Guid.NewGuid();
public static Guid ProductSku2Id { get; } = Guid.NewGuid(); public static Guid ProductSku2Id { get; } = Guid.NewGuid();
public static Guid ProductSku3Id { get; } = Guid.NewGuid();
public static Guid ProductDetail1Id { get; } = Guid.NewGuid(); public static Guid ProductDetail1Id { get; } = Guid.NewGuid();
public static Guid ProductDetail2Id { get; } = Guid.NewGuid(); public static Guid ProductDetail2Id { get; } = Guid.NewGuid();
public static DateTime ProductLastModificationTime { get; } = DateTime.Today; public static DateTime ProductLastModificationTime { get; } = DateTime.Today;
public static DateTime ProductDetailLastModificationTime { get; } = DateTime.Today; public static DateTime ProductDetailLastModificationTime { get; } = DateTime.Today;
} }
} }

16
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Application.Contracts/EasyAbp/EShop/Payments/Refunds/Dtos/CreateEShopRefundItemInput.cs

@ -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();
} }
} }

22
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Refunds/CreateEShopRefundItemInfoModel.cs

@ -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();
}

34
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain.Shared/EasyAbp/EShop/Payments/Refunds/RefundOrderEto.cs

@ -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;
}
}

65
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Domain/EasyAbp/EShop/Payments/Refunds/RefundOrderEventHandler.cs

@ -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);
}
}

12
modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Application.Tests/Refunds/RefundAppServiceTests.cs

@ -47,7 +47,8 @@ namespace EasyAbp.EShop.Payments.Refunds
paymentItemType.GetProperty(nameof(PaymentItem.ActualPaymentAmount))?.SetValue(paymentItem, 1m); paymentItemType.GetProperty(nameof(PaymentItem.ActualPaymentAmount))?.SetValue(paymentItem, 1m);
paymentItemType.GetProperty(nameof(PaymentItem.ItemType))?.SetValue(paymentItem, PaymentsConsts.PaymentItemType); paymentItemType.GetProperty(nameof(PaymentItem.ItemType))?.SetValue(paymentItem, PaymentsConsts.PaymentItemType);
paymentItemType.GetProperty(nameof(PaymentItem.ItemKey))?.SetValue(paymentItem, PaymentsTestData.Order1.ToString()); paymentItemType.GetProperty(nameof(PaymentItem.ItemKey))?.SetValue(paymentItem, PaymentsTestData.Order1.ToString());
paymentItem.ExtraProperties.Add(nameof(paymentItem.StoreId), PaymentsTestData.Store1); paymentItemType.GetProperty(nameof(PaymentItem.StoreId))?.SetValue(paymentItem, PaymentsTestData.Store1);
// paymentItem.ExtraProperties.Add(nameof(paymentItem.StoreId), PaymentsTestData.Store1);
var payment = Activator.CreateInstance(paymentType, true) as Payment; var payment = Activator.CreateInstance(paymentType, true) as Payment;
payment.ShouldNotBeNull(); payment.ShouldNotBeNull();
@ -75,7 +76,9 @@ namespace EasyAbp.EShop.Payments.Refunds
?.SetValue(paymentItem, PaymentsConsts.PaymentItemType); ?.SetValue(paymentItem, PaymentsConsts.PaymentItemType);
paymentItemType.GetProperty(nameof(PaymentItem.ItemKey)) paymentItemType.GetProperty(nameof(PaymentItem.ItemKey))
?.SetValue(paymentItem, PaymentsTestData.Order1.ToString()); ?.SetValue(paymentItem, PaymentsTestData.Order1.ToString());
paymentItem.ExtraProperties.Add(nameof(paymentItem.StoreId), PaymentsTestData.Store1); paymentItemType.GetProperty(nameof(PaymentItem.StoreId))
?.SetValue(paymentItem, PaymentsTestData.Store1);
// paymentItem.ExtraProperties.Add(nameof(paymentItem.StoreId), PaymentsTestData.Store1);
var payment = Activator.CreateInstance(paymentType, true) as Payment; var payment = Activator.CreateInstance(paymentType, true) as Payment;
payment.ShouldNotBeNull(); payment.ShouldNotBeNull();
@ -182,10 +185,9 @@ namespace EasyAbp.EShop.Payments.Refunds
// Act & Assert // Act & Assert
await _refundAppService.CreateAsync(request); await _refundAppService.CreateAsync(request);
_testRefundPaymentEventHandler.IsEventPublished.ShouldBe(true);
var eventData = _testRefundPaymentEventHandler.EventData; var eventData = TestRefundPaymentEventHandler.LastEto;
TestRefundPaymentEventHandler.LastEto = null;
eventData.ShouldNotBeNull(); eventData.ShouldNotBeNull();
eventData.CreateRefundInput.RefundItems.Count.ShouldBe(1); eventData.CreateRefundInput.RefundItems.Count.ShouldBe(1);

116
modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Domain.Tests/Refunds/RefundOrderEventHandlerTests.cs

@ -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);
}
}

13
modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Application.Tests/Refunds/TestRefundPaymentEventHandler.cs → modules/EasyAbp.EShop.Payments/test/EasyAbp.EShop.Payments.Domain.Tests/Refunds/TestRefundPaymentEventHandler.cs

@ -5,17 +5,14 @@ using Volo.Abp.EventBus.Distributed;
namespace EasyAbp.EShop.Payments.Refunds namespace EasyAbp.EShop.Payments.Refunds
{ {
public class TestRefundPaymentEventHandler : IDistributedEventHandler<RefundPaymentEto>, ISingletonDependency public class TestRefundPaymentEventHandler : IDistributedEventHandler<RefundPaymentEto>, ITransientDependency
{ {
public bool IsEventPublished { get; protected set; } public static RefundPaymentEto LastEto { get; set; }
public RefundPaymentEto EventData { get; protected set; }
public Task HandleEventAsync(RefundPaymentEto eventData) public Task HandleEventAsync(RefundPaymentEto eventData)
{ {
IsEventPublished = true; LastEto = eventData;
EventData = eventData;
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

17
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/ProductInventories/Dtos/ProductInventoryDto.cs

@ -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; }
}
}

18
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/ProductInventories/Dtos/UpdateProductInventoryDto.cs

@ -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; }
}
}

15
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/ProductInventories/IProductInventoryAppService.cs

@ -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);
}
}

13
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ChangeProductInventoryDto.cs

@ -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; }
}

14
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ChangeProductInventoryResultDto.cs

@ -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; }
}

5
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/CreateUpdateProductDto.cs

@ -34,7 +34,10 @@ namespace EasyAbp.EShop.Products.Products.Dtos
[DisplayName("ProductInventoryStrategy")] [DisplayName("ProductInventoryStrategy")]
public InventoryStrategy InventoryStrategy { get; set; } public InventoryStrategy InventoryStrategy { get; set; }
[DisplayName("ProductInventoryProviderName")]
public string InventoryProviderName { get; set; }
[DisplayName("ProductDisplayOrder")] [DisplayName("ProductDisplayOrder")]
public int DisplayOrder { get; set; } public int DisplayOrder { get; set; }

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ProductDto.cs

@ -22,6 +22,8 @@ namespace EasyAbp.EShop.Products.Products.Dtos
public InventoryStrategy InventoryStrategy { get; set; } public InventoryStrategy InventoryStrategy { get; set; }
public string InventoryProviderName { get; set; }
public string MediaResources { get; set; } public string MediaResources { get; set; }
public int DisplayOrder { get; set; } public int DisplayOrder { get; set; }

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ProductViewDto.cs

@ -19,6 +19,8 @@ namespace EasyAbp.EShop.Products.Products.Dtos
public InventoryStrategy InventoryStrategy { get; set; } public InventoryStrategy InventoryStrategy { get; set; }
public string InventoryProviderName { get; set; }
public string MediaResources { get; set; } public string MediaResources { get; set; }
public int DisplayOrder { get; set; } public int DisplayOrder { get; set; }

11
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/IProductAppService.cs

@ -7,9 +7,9 @@ using Volo.Abp.Application.Services;
namespace EasyAbp.EShop.Products.Products namespace EasyAbp.EShop.Products.Products
{ {
public interface IProductAppService : public interface IProductAppService :
ICrudAppService< ICrudAppService<
ProductDto, ProductDto,
Guid, Guid,
GetProductListInput, GetProductListInput,
CreateUpdateProductDto, CreateUpdateProductDto,
CreateUpdateProductDto> CreateUpdateProductDto>
@ -19,9 +19,12 @@ namespace EasyAbp.EShop.Products.Products
Task<ProductDto> UpdateSkuAsync(Guid productId, Guid productSkuId, UpdateProductSkuDto input); Task<ProductDto> UpdateSkuAsync(Guid productId, Guid productSkuId, UpdateProductSkuDto input);
Task<ProductDto> DeleteSkuAsync(Guid productId, Guid productSkuId); Task<ProductDto> DeleteSkuAsync(Guid productId, Guid productSkuId);
Task<ProductDto> GetByUniqueNameAsync(Guid storeId, string uniqueName); Task<ProductDto> GetByUniqueNameAsync(Guid storeId, string uniqueName);
Task<ListResultDto<ProductGroupDto>> GetProductGroupListAsync(); Task<ListResultDto<ProductGroupDto>> GetProductGroupListAsync();
Task<ChangeProductInventoryResultDto> ChangeInventoryAsync(Guid id, Guid productSkuId,
ChangeProductInventoryDto input);
} }
} }

103
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/ProductInventories/ProductInventoryAppService.cs

@ -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);
}
}
}
}
}

133
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using EasyAbp.EShop.Products.Options; using EasyAbp.EShop.Products.Options;
using EasyAbp.EShop.Products.ProductInventories;
using EasyAbp.EShop.Products.Products.CacheItems; using EasyAbp.EShop.Products.Products.CacheItems;
using EasyAbp.EShop.Stores.Stores; using EasyAbp.EShop.Stores.Stores;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -14,7 +15,9 @@ using Volo.Abp.Domain.Entities;
namespace EasyAbp.EShop.Products.Products namespace EasyAbp.EShop.Products.Products
{ {
public class ProductAppService : MultiStoreCrudAppService<Product, ProductDto, Guid, GetProductListInput, CreateUpdateProductDto, CreateUpdateProductDto>, IProductAppService public class ProductAppService :
MultiStoreCrudAppService<Product, ProductDto, Guid, GetProductListInput, CreateUpdateProductDto,
CreateUpdateProductDto>, IProductAppService
{ {
protected override string CreatePolicyName { get; set; } = ProductsPermissions.Products.Create; protected override string CreatePolicyName { get; set; } = ProductsPermissions.Products.Create;
protected override string DeletePolicyName { get; set; } = ProductsPermissions.Products.Delete; protected override string DeletePolicyName { get; set; } = ProductsPermissions.Products.Delete;
@ -26,7 +29,7 @@ namespace EasyAbp.EShop.Products.Products
private readonly IProductManager _productManager; private readonly IProductManager _productManager;
private readonly IDistributedCache<ProductViewCacheItem> _cache; private readonly IDistributedCache<ProductViewCacheItem> _cache;
private readonly EShopProductsOptions _options; private readonly EShopProductsOptions _options;
private readonly IProductInventoryProvider _productInventoryProvider; private readonly IProductInventoryProviderResolver _productInventoryProviderResolver;
private readonly IProductViewCacheKeyProvider _productViewCacheKeyProvider; private readonly IProductViewCacheKeyProvider _productViewCacheKeyProvider;
private readonly IAttributeOptionIdsSerializer _attributeOptionIdsSerializer; private readonly IAttributeOptionIdsSerializer _attributeOptionIdsSerializer;
private readonly IProductRepository _repository; private readonly IProductRepository _repository;
@ -35,7 +38,7 @@ namespace EasyAbp.EShop.Products.Products
IProductManager productManager, IProductManager productManager,
IOptions<EShopProductsOptions> options, IOptions<EShopProductsOptions> options,
IDistributedCache<ProductViewCacheItem> cache, IDistributedCache<ProductViewCacheItem> cache,
IProductInventoryProvider productInventoryProvider, IProductInventoryProviderResolver productInventoryProviderResolver,
IProductViewCacheKeyProvider productViewCacheKeyProvider, IProductViewCacheKeyProvider productViewCacheKeyProvider,
IAttributeOptionIdsSerializer attributeOptionIdsSerializer, IAttributeOptionIdsSerializer attributeOptionIdsSerializer,
IProductRepository repository) : base(repository) IProductRepository repository) : base(repository)
@ -43,7 +46,7 @@ namespace EasyAbp.EShop.Products.Products
_productManager = productManager; _productManager = productManager;
_cache = cache; _cache = cache;
_options = options.Value; _options = options.Value;
_productInventoryProvider = productInventoryProvider; _productInventoryProviderResolver = productInventoryProviderResolver;
_productViewCacheKeyProvider = productViewCacheKeyProvider; _productViewCacheKeyProvider = productViewCacheKeyProvider;
_attributeOptionIdsSerializer = attributeOptionIdsSerializer; _attributeOptionIdsSerializer = attributeOptionIdsSerializer;
_repository = repository; _repository = repository;
@ -74,14 +77,11 @@ namespace EasyAbp.EShop.Products.Products
await _productManager.CreateAsync(product, input.CategoryIds); await _productManager.CreateAsync(product, input.CategoryIds);
var dto = await MapToGetOutputDtoAsync(product); var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto); await LoadDtoExtraDataAsync(product, dto);
await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
UnitOfWorkManager.Current.OnCompleted(async () => UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
{
await ClearProductViewCacheAsync(product.StoreId);
});
return dto; return dto;
} }
@ -94,14 +94,14 @@ namespace EasyAbp.EShop.Products.Products
public override async Task<ProductDto> UpdateAsync(Guid id, CreateUpdateProductDto input) public override async Task<ProductDto> UpdateAsync(Guid id, CreateUpdateProductDto input)
{ {
var product = await GetEntityByIdAsync(id); var product = await GetEntityByIdAsync(id);
await CheckMultiStorePolicyAsync(product.StoreId, UpdatePolicyName); await CheckMultiStorePolicyAsync(product.StoreId, UpdatePolicyName);
if (input.StoreId != product.StoreId) if (input.StoreId != product.StoreId)
{ {
await CheckMultiStorePolicyAsync(input.StoreId, UpdatePolicyName); await CheckMultiStorePolicyAsync(input.StoreId, UpdatePolicyName);
} }
CheckProductIsNotStatic(product); CheckProductIsNotStatic(product);
await MapToEntityAsync(input, product); await MapToEntityAsync(input, product);
@ -111,15 +111,12 @@ namespace EasyAbp.EShop.Products.Products
await _productManager.UpdateAsync(product, input.CategoryIds); await _productManager.UpdateAsync(product, input.CategoryIds);
var dto = await MapToGetOutputDtoAsync(product); var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto); await LoadDtoExtraDataAsync(product, dto);
await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
UnitOfWorkManager.Current.OnCompleted(async () =>
{
await ClearProductViewCacheAsync(product.StoreId);
});
return dto; return dto;
} }
@ -130,10 +127,10 @@ namespace EasyAbp.EShop.Products.Products
var usedAttributeOptionIds = new HashSet<Guid>(); var usedAttributeOptionIds = new HashSet<Guid>();
foreach (var serializedAttributeOptionIds in product.ProductSkus.Select(sku => foreach (var serializedAttributeOptionIds in product.ProductSkus.Select(sku =>
sku.SerializedAttributeOptionIds)) sku.SerializedAttributeOptionIds))
{ {
foreach (var attributeOptionId in await _attributeOptionIdsSerializer.DeserializeAsync( foreach (var attributeOptionId in await _attributeOptionIdsSerializer.DeserializeAsync(
serializedAttributeOptionIds)) serializedAttributeOptionIds))
{ {
usedAttributeOptionIds.Add(attributeOptionId); usedAttributeOptionIds.Add(attributeOptionId);
} }
@ -175,9 +172,9 @@ namespace EasyAbp.EShop.Products.Products
.Except(attributeDto.ProductAttributeOptions.Select(o => o.DisplayName)).ToList(); .Except(attributeDto.ProductAttributeOptions.Select(o => o.DisplayName)).ToList();
if (!isProductSkusEmpty && removedOptionNames.Any() && usedAttributeOptionIds if (!isProductSkusEmpty && removedOptionNames.Any() && usedAttributeOptionIds
.Intersect(attribute.ProductAttributeOptions .Intersect(attribute.ProductAttributeOptions
.Where(option => removedOptionNames.Contains(option.DisplayName)) .Where(option => removedOptionNames.Contains(option.DisplayName))
.Select(option => option.Id)).Any()) .Select(option => option.Id)).Any())
{ {
throw new ProductAttributeOptionsDeletionFailedException(); throw new ProductAttributeOptionsDeletionFailedException();
} }
@ -210,7 +207,7 @@ namespace EasyAbp.EShop.Products.Products
var dto = await MapToGetOutputDtoAsync(product); var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto); await LoadDtoExtraDataAsync(product, dto);
await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
return dto; return dto;
} }
@ -241,7 +238,7 @@ namespace EasyAbp.EShop.Products.Products
var dto = await MapToGetOutputDtoAsync(product); var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto); await LoadDtoExtraDataAsync(product, dto);
await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
return dto; return dto;
} }
@ -283,7 +280,12 @@ namespace EasyAbp.EShop.Products.Products
protected virtual async Task<ProductDto> LoadDtoInventoryDataAsync(Product product, ProductDto productDto) protected virtual async Task<ProductDto> LoadDtoInventoryDataAsync(Product product, ProductDto productDto)
{ {
var inventoryDataDict = await _productInventoryProvider.GetInventoryDataDictionaryAsync(product); var models = product.ProductSkus.Select(x =>
new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, x.Id)).ToList();
var inventoryProvider = await _productInventoryProviderResolver.GetAsync(product);
var inventoryDataDict = await inventoryProvider.GetSkuIdInventoryDataMappingAsync(models);
productDto.Sold = 0; productDto.Sold = 0;
@ -306,7 +308,7 @@ namespace EasyAbp.EShop.Products.Products
return productDto; return productDto;
} }
protected virtual async Task<ProductDto> LoadDtoPriceDataAsync(Product product, ProductDto productDto) protected virtual async Task<ProductDto> LoadDtoPriceDataAsync(Product product, ProductDto productDto)
{ {
foreach (var productSku in product.ProductSkus) foreach (var productSku in product.ProductSkus)
@ -314,7 +316,7 @@ namespace EasyAbp.EShop.Products.Products
var productSkuDto = productDto.ProductSkus.First(x => x.Id == productSku.Id); var productSkuDto = productDto.ProductSkus.First(x => x.Id == productSku.Id);
var priceDataModel = await _productManager.GetRealPriceAsync(product, productSku); var priceDataModel = await _productManager.GetRealPriceAsync(product, productSku);
productSkuDto.Price = priceDataModel.Price; productSkuDto.Price = priceDataModel.Price;
productSkuDto.DiscountedPrice = priceDataModel.DiscountedPrice; productSkuDto.DiscountedPrice = priceDataModel.DiscountedPrice;
} }
@ -331,17 +333,14 @@ namespace EasyAbp.EShop.Products.Products
public override async Task DeleteAsync(Guid id) public override async Task DeleteAsync(Guid id)
{ {
var product = await GetEntityByIdAsync(id); var product = await GetEntityByIdAsync(id);
await CheckMultiStorePolicyAsync(product.StoreId, DeletePolicyName); await CheckMultiStorePolicyAsync(product.StoreId, DeletePolicyName);
CheckProductIsNotStatic(product); CheckProductIsNotStatic(product);
await _productManager.DeleteAsync(product); await _productManager.DeleteAsync(product);
UnitOfWorkManager.Current.OnCompleted(async () => UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
{
await ClearProductViewCacheAsync(product.StoreId);
});
} }
private static void CheckProductIsNotStatic(Product product) private static void CheckProductIsNotStatic(Product product)
@ -355,9 +354,9 @@ namespace EasyAbp.EShop.Products.Products
public async Task<ProductDto> CreateSkuAsync(Guid productId, CreateProductSkuDto input) public async Task<ProductDto> CreateSkuAsync(Guid productId, CreateProductSkuDto input)
{ {
var product = await GetEntityByIdAsync(productId); var product = await GetEntityByIdAsync(productId);
await CheckMultiStorePolicyAsync(product.StoreId, UpdatePolicyName); await CheckMultiStorePolicyAsync(product.StoreId, UpdatePolicyName);
CheckProductIsNotStatic(product); CheckProductIsNotStatic(product);
var sku = ObjectMapper.Map<CreateProductSkuDto, ProductSku>(input); var sku = ObjectMapper.Map<CreateProductSkuDto, ProductSku>(input);
@ -367,15 +366,12 @@ namespace EasyAbp.EShop.Products.Products
await _productManager.CreateSkuAsync(product, sku); await _productManager.CreateSkuAsync(product, sku);
var dto = await MapToGetOutputDtoAsync(product); var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto); await LoadDtoExtraDataAsync(product, dto);
await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
UnitOfWorkManager.Current.OnCompleted(async () =>
{
await ClearProductViewCacheAsync(product.StoreId);
});
return dto; return dto;
} }
@ -394,15 +390,12 @@ namespace EasyAbp.EShop.Products.Products
await _productManager.UpdateSkuAsync(product, sku); await _productManager.UpdateSkuAsync(product, sku);
var dto = await MapToGetOutputDtoAsync(product); var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto); await LoadDtoExtraDataAsync(product, dto);
await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
UnitOfWorkManager.Current.OnCompleted(async () =>
{
await ClearProductViewCacheAsync(product.StoreId);
});
return dto; return dto;
} }
@ -419,15 +412,12 @@ namespace EasyAbp.EShop.Products.Products
await _productManager.DeleteSkuAsync(product, sku); await _productManager.DeleteSkuAsync(product, sku);
var dto = await MapToGetOutputDtoAsync(product); var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto); await LoadDtoExtraDataAsync(product, dto);
await LoadDtosProductGroupDisplayNameAsync(new[] {dto}); await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
UnitOfWorkManager.Current.OnCompleted(async () =>
{
await ClearProductViewCacheAsync(product.StoreId);
});
return dto; return dto;
} }
@ -445,6 +435,29 @@ namespace EasyAbp.EShop.Products.Products
).ToList())); ).ToList()));
} }
public virtual async Task<ChangeProductInventoryResultDto> ChangeInventoryAsync(Guid id, Guid productSkuId,
ChangeProductInventoryDto input)
{
var product = await GetEntityByIdAsync(id);
var sku = product.ProductSkus.Single(x => x.Id == productSkuId);
var changed = input.ChangedInventory switch
{
> 0 => await _productManager.TryIncreaseInventoryAsync(product, sku, input.ChangedInventory, false),
< 0 => await _productManager.TryReduceInventoryAsync(product, sku, -1 * input.ChangedInventory, false),
_ => false
};
var model = await _productManager.GetInventoryDataAsync(product, sku);
return new ChangeProductInventoryResultDto
{
Changed = changed,
ChangedInventory = input.ChangedInventory,
CurrentInventory = model.Inventory
};
}
protected override ProductDto MapToGetOutputDto(Product entity) protected override ProductDto MapToGetOutputDto(Product entity)
{ {
var productDto = base.MapToGetOutputDto(entity); var productDto = base.MapToGetOutputDto(entity);

3
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/ProductsApplicationAutoMapperProfile.cs

@ -9,8 +9,6 @@ using EasyAbp.EShop.Products.ProductDetails;
using EasyAbp.EShop.Products.ProductDetails.Dtos; using EasyAbp.EShop.Products.ProductDetails.Dtos;
using EasyAbp.EShop.Products.ProductHistories; using EasyAbp.EShop.Products.ProductHistories;
using EasyAbp.EShop.Products.ProductHistories.Dtos; using EasyAbp.EShop.Products.ProductHistories.Dtos;
using EasyAbp.EShop.Products.ProductInventories;
using EasyAbp.EShop.Products.ProductInventories.Dtos;
using EasyAbp.EShop.Products.Products; using EasyAbp.EShop.Products.Products;
using EasyAbp.EShop.Products.Products.Dtos; using EasyAbp.EShop.Products.Products.Dtos;
using System.Linq; using System.Linq;
@ -65,7 +63,6 @@ namespace EasyAbp.EShop.Products
CreateMap<ProductCategory, ProductCategoryDto>(); CreateMap<ProductCategory, ProductCategoryDto>();
CreateMap<ProductHistory, ProductHistoryDto>(); CreateMap<ProductHistory, ProductHistoryDto>();
CreateMap<ProductDetailHistory, ProductDetailHistoryDto>(); CreateMap<ProductDetailHistory, ProductDetailHistoryDto>();
CreateMap<ProductInventory, ProductInventoryDto>();
CreateMap<ProductView, ProductViewDto>(); CreateMap<ProductView, ProductViewDto>();
CreateMap<Product, ProductView>(MemberList.Destination); CreateMap<Product, ProductView>(MemberList.Destination);
} }

3
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/cs.json

@ -40,6 +40,8 @@
"EditProductSku": "Edit", "EditProductSku": "Edit",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?", "ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy", "ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need", "InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing", "InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment", "InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -85,6 +87,7 @@
"EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.", "EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.",
"EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}", "EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}",
"EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.", "EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.",
"EasyAbp.EShop.Products:NonexistentInventoryProvider": "The specified inventory provider ({inventoryProviderName}) is nonexistent.",
"EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}", "EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}",

3
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/en.json

@ -41,6 +41,8 @@
"EditProductSku": "Edit", "EditProductSku": "Edit",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?", "ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy", "ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need", "InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing", "InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment", "InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -86,6 +88,7 @@
"EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.", "EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.",
"EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}", "EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}",
"EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.", "EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.",
"EasyAbp.EShop.Products:NonexistentInventoryProvider": "The specified inventory provider ({inventoryProviderName}) is nonexistent.",
"EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}", "EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}",

3
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/pl.json

@ -40,6 +40,8 @@
"EditProductSku": "Edit", "EditProductSku": "Edit",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?", "ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy", "ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need", "InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing", "InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment", "InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -85,6 +87,7 @@
"EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.", "EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.",
"EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}", "EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}",
"EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.", "EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.",
"EasyAbp.EShop.Products:NonexistentInventoryProvider": "The specified inventory provider ({inventoryProviderName}) is nonexistent.",
"EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}", "EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}",

3
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/pt-BR.json

@ -40,6 +40,8 @@
"EditProductSku": "Edit", "EditProductSku": "Edit",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?", "ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy", "ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need", "InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing", "InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment", "InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -85,6 +87,7 @@
"EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.", "EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.",
"EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}", "EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}",
"EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.", "EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.",
"EasyAbp.EShop.Products:NonexistentInventoryProvider": "The specified inventory provider ({inventoryProviderName}) is nonexistent.",
"EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}", "EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}",

3
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/sl.json

@ -41,6 +41,8 @@
"EditProductSku": "Edit", "EditProductSku": "Edit",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?", "ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy", "ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need", "InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing", "InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment", "InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -86,6 +88,7 @@
"EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.", "EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.",
"EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}", "EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}",
"EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.", "EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.",
"EasyAbp.EShop.Products:NonexistentInventoryProvider": "The specified inventory provider ({inventoryProviderName}) is nonexistent.",
"EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}", "EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}",

3
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/tr.json

@ -41,6 +41,8 @@
"EditProductSku": "Edit", "EditProductSku": "Edit",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?", "ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy", "ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need", "InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing", "InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment", "InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -86,6 +88,7 @@
"EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.", "EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.",
"EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}", "EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}",
"EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.", "EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.",
"EasyAbp.EShop.Products:NonexistentInventoryProvider": "The specified inventory provider ({inventoryProviderName}) is nonexistent.",
"EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}", "EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}",

3
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/vi.json

@ -40,6 +40,8 @@
"EditProductSku": "Edit", "EditProductSku": "Edit",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?", "ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy", "ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need", "InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing", "InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment", "InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -85,6 +87,7 @@
"EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.", "EasyAbp.EShop.Products:DuplicatedProductUniqueName": "The product unique name '{uniqueName}' is duplicated.",
"EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}", "EasyAbp.EShop.Products:InventoryChangeFailed": "Inventory of product {productId} (SKU: {productSkuId}) cannot be changed by {changedInventory} from {originalInventory}",
"EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.", "EasyAbp.EShop.Products:NonexistentProductGroup": "The specified product group ({productGroupName}) is nonexistent.",
"EasyAbp.EShop.Products:NonexistentInventoryProvider": "The specified inventory provider ({inventoryProviderName}) is nonexistent.",
"EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "Sku code {code} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}", "EasyAbp.EShop.Products:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate for the product {productId}",
"EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}", "EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "Sku {serializedAttributeOptionIds} is incorrect for the product {productId}",

3
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/zh-Hans.json

@ -41,6 +41,8 @@
"EditProductSku": "编辑", "EditProductSku": "编辑",
"ProductSkuDeletionConfirmationMessage": "确认删除 SKU {0}?", "ProductSkuDeletionConfirmationMessage": "确认删除 SKU {0}?",
"ProductInventoryStrategy": "库存扣减策略", "ProductInventoryStrategy": "库存扣减策略",
"ProductInventoryProviderName": "库存提供者",
"ProductInventoryProviderNamePlaceholder": "如果你不理解它的意义,请留空",
"InventoryStrategy.NoNeed": "无需库存", "InventoryStrategy.NoNeed": "无需库存",
"InventoryStrategy.ReduceAfterPlacing": "下单后减库存", "InventoryStrategy.ReduceAfterPlacing": "下单后减库存",
"InventoryStrategy.ReduceAfterPayment": "支付后减库存", "InventoryStrategy.ReduceAfterPayment": "支付后减库存",
@ -83,6 +85,7 @@
"EasyAbp.EShop.Products:DuplicatedProductUniqueName": "唯一的产品名称'{uniqueName}'重复", "EasyAbp.EShop.Products:DuplicatedProductUniqueName": "唯一的产品名称'{uniqueName}'重复",
"EasyAbp.EShop.Products:InventoryChangeFailed": "产品{productId} (SKU: {productSkuId})的库存不能由{originalInventory}中的{changedInventory}更改", "EasyAbp.EShop.Products:InventoryChangeFailed": "产品{productId} (SKU: {productSkuId})的库存不能由{originalInventory}中的{changedInventory}更改",
"EasyAbp.EShop.Products:NonexistentProductGroup": "指定的产品组({productGroupName})不存在", "EasyAbp.EShop.Products:NonexistentProductGroup": "指定的产品组({productGroupName})不存在",
"EasyAbp.EShop.Products:NonexistentInventoryProvider": "指定的库存提供者({inventoryProviderName})不存在",
"EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "商品{productId}的Sku代码{code}重复", "EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "商品{productId}的Sku代码{code}重复",
"EasyAbp.EShop.Products:ProductSkuDuplicated": "商品{productId}的Sku{serializedAttributeOptionIds}重复", "EasyAbp.EShop.Products:ProductSkuDuplicated": "商品{productId}的Sku{serializedAttributeOptionIds}重复",
"EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "商品{productId}的Sku{serializedAttributeOptionIds}不正确", "EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "商品{productId}的Sku{serializedAttributeOptionIds}不正确",

5
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Localization/Products/zh-Hant.json

@ -41,7 +41,9 @@
"EditProductSku": "編輯", "EditProductSku": "編輯",
"ProductSkuDeletionConfirmationMessage": "確認刪除 SKU {0}?", "ProductSkuDeletionConfirmationMessage": "確認刪除 SKU {0}?",
"ProductInventoryStrategy": "庫存扣減策略", "ProductInventoryStrategy": "庫存扣減策略",
"InventoryStrategy.NoNeed": "No need", "ProductInventoryProviderName": "庫存提供者",
"ProductInventoryProviderNamePlaceholder": "如果你不理解它的意義,請留空",
"InventoryStrategy.NoNeed": "無需庫存",
"InventoryStrategy.ReduceAfterPlacing": "下單後減庫存", "InventoryStrategy.ReduceAfterPlacing": "下單後減庫存",
"InventoryStrategy.ReduceAfterPayment": "支付後減庫存", "InventoryStrategy.ReduceAfterPayment": "支付後減庫存",
"ProductIsPublished": "是否發布", "ProductIsPublished": "是否發布",
@ -83,6 +85,7 @@
"EasyAbp.EShop.Products:DuplicatedProductUniqueName": "唯一的產品名稱'{uniqueName}'重復", "EasyAbp.EShop.Products:DuplicatedProductUniqueName": "唯一的產品名稱'{uniqueName}'重復",
"EasyAbp.EShop.Products:InventoryChangeFailed": "產品{productId} (SKU: {productSkuId})的庫存不能由{originalInventory}中的{changedInventory}更改", "EasyAbp.EShop.Products:InventoryChangeFailed": "產品{productId} (SKU: {productSkuId})的庫存不能由{originalInventory}中的{changedInventory}更改",
"EasyAbp.EShop.Products:NonexistentProductGroup": "指定的產品組({productGroupName})不存在", "EasyAbp.EShop.Products:NonexistentProductGroup": "指定的產品組({productGroupName})不存在",
"EasyAbp.EShop.Products:NonexistentInventoryProvider": "指定的庫存提供者({inventoryProviderName})不存在",
"EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "商品{productId}的Sku代碼{code}重復", "EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "商品{productId}的Sku代碼{code}重復",
"EasyAbp.EShop.Products:ProductSkuDuplicated": "商品{productId}的Sku{serializedAttributeOptionIds}重復", "EasyAbp.EShop.Products:ProductSkuDuplicated": "商品{productId}的Sku{serializedAttributeOptionIds}重復",
"EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "商品{productId}的Sku{serializedAttributeOptionIds}不正確", "EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions": "商品{productId}的Sku{serializedAttributeOptionIds}不正確",

19
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/IProductInventoryProvider.cs

@ -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);
}
}

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/InventoryDataModel.cs → modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryDataModel.cs

@ -1,4 +1,4 @@
namespace EasyAbp.EShop.Products.Products namespace EasyAbp.EShop.Products.ProductInventories
{ {
public class InventoryDataModel public class InventoryDataModel
{ {

28
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductInventories/InventoryQueryModel.cs

@ -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;
}
}

15
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IProduct.cs

@ -1,5 +1,6 @@
using System; using System;
using EasyAbp.EShop.Stores.Stores; using EasyAbp.EShop.Stores.Stores;
using JetBrains.Annotations;
using Volo.Abp.Data; using Volo.Abp.Data;
namespace EasyAbp.EShop.Products.Products namespace EasyAbp.EShop.Products.Products
@ -7,23 +8,25 @@ namespace EasyAbp.EShop.Products.Products
public interface IProduct : IHasExtraProperties, IMultiStore public interface IProduct : IHasExtraProperties, IMultiStore
{ {
string ProductGroupName { get; } string ProductGroupName { get; }
Guid? ProductDetailId { get; } Guid? ProductDetailId { get; }
string UniqueName { get; } string UniqueName { get; }
string DisplayName { get; } string DisplayName { get; }
InventoryStrategy InventoryStrategy { get; } InventoryStrategy InventoryStrategy { get; }
[CanBeNull] string InventoryProviderName { get; }
string MediaResources { get; } string MediaResources { get; }
int DisplayOrder { get; } int DisplayOrder { get; }
bool IsPublished { get; } bool IsPublished { get; }
bool IsStatic { get; } bool IsStatic { get; }
bool IsHidden { get; } bool IsHidden { get; }
} }
} }

11
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductEto.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Volo.Abp.Data;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectExtending; using Volo.Abp.ObjectExtending;
@ -11,11 +10,11 @@ namespace EasyAbp.EShop.Products.Products
public Guid? TenantId { get; set; } public Guid? TenantId { get; set; }
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid StoreId { get; set; } public Guid StoreId { get; set; }
public string ProductGroupName { get; set; } public string ProductGroupName { get; set; }
public Guid? ProductDetailId { get; set; } public Guid? ProductDetailId { get; set; }
public string UniqueName { get; set; } public string UniqueName { get; set; }
@ -24,6 +23,8 @@ namespace EasyAbp.EShop.Products.Products
public InventoryStrategy InventoryStrategy { get; set; } public InventoryStrategy InventoryStrategy { get; set; }
public string InventoryProviderName { get; set; }
public string MediaResources { get; set; } public string MediaResources { get; set; }
public int DisplayOrder { get; set; } public int DisplayOrder { get; set; }
@ -33,9 +34,9 @@ namespace EasyAbp.EShop.Products.Products
public bool IsStatic { get; set; } public bool IsStatic { get; set; }
public bool IsHidden { get; set; } public bool IsHidden { get; set; }
public List<ProductAttributeEto> ProductAttributes { get; set; } public List<ProductAttributeEto> ProductAttributes { get; set; }
public List<ProductSkuEto> ProductSkus { get; set; } public List<ProductSkuEto> ProductSkus { get; set; }
} }
} }

1
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/ProductsErrorCodes.cs

@ -6,6 +6,7 @@
public const string DuplicatedProductUniqueName = "EasyAbp.EShop.Products:DuplicatedProductUniqueName"; public const string DuplicatedProductUniqueName = "EasyAbp.EShop.Products:DuplicatedProductUniqueName";
public const string InventoryChangeFailed = "EasyAbp.EShop.Products:InventoryChangeFailed"; public const string InventoryChangeFailed = "EasyAbp.EShop.Products:InventoryChangeFailed";
public const string NonexistentProductGroup = "EasyAbp.EShop.Products:NonexistentProductGroup"; public const string NonexistentProductGroup = "EasyAbp.EShop.Products:NonexistentProductGroup";
public const string NonexistentInventoryProvider = "EasyAbp.EShop.Products:NonexistentInventoryProvider";
public const string ProductSkuCodeDuplicated = "EasyAbp.EShop.Products:ProductSkuCodeDuplicated"; public const string ProductSkuCodeDuplicated = "EasyAbp.EShop.Products:ProductSkuCodeDuplicated";
public const string ProductSkuDuplicated = "EasyAbp.EShop.Products:ProductSkuDuplicated"; public const string ProductSkuDuplicated = "EasyAbp.EShop.Products:ProductSkuDuplicated";
public const string ProductSkuIncorrectAttributeOptions = "EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions"; public const string ProductSkuIncorrectAttributeOptions = "EasyAbp.EShop.Products:ProductSkuIncorrectAttributeOptions";

16
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/EShopProductsDomainModule.cs

@ -24,10 +24,10 @@ namespace EasyAbp.EShop.Products
Configure<AbpDistributedEntityEventOptions>(options => Configure<AbpDistributedEntityEventOptions>(options =>
{ {
options.EtoMappings.Add<Product, ProductEto>(); options.EtoMappings.Add<Product, ProductEto>();
options.AutoEventSelectors.Add<Product>(); options.AutoEventSelectors.Add<Product>();
}); });
Configure<EShopProductsOptions>(options => Configure<EShopProductsOptions>(options =>
{ {
options.Groups.Configure<DefaultProductGroup>(group => options.Groups.Configure<DefaultProductGroup>(group =>
@ -35,6 +35,16 @@ namespace EasyAbp.EShop.Products
group.DisplayName = ProductsConsts.DefaultProductGroupDisplayName; group.DisplayName = ProductsConsts.DefaultProductGroupDisplayName;
group.Description = ProductsConsts.DefaultProductGroupDescription; group.Description = ProductsConsts.DefaultProductGroupDescription;
}); });
options.InventoryProviders.Configure(
DefaultProductInventoryProvider.DefaultProductInventoryProviderName, provider =>
{
provider.DisplayName =
DefaultProductInventoryProvider.DefaultProductInventoryProviderDisplayName;
provider.Description =
DefaultProductInventoryProvider.DefaultProductInventoryProviderDescription;
provider.ProviderType = typeof(DefaultProductInventoryProvider);
});
}); });
} }
@ -48,4 +58,4 @@ namespace EasyAbp.EShop.Products
}); });
} }
} }
} }

20
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Options/EShopProductsOptions.cs

@ -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; }
} }
} }

7
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Options/InventoryProviders/IInventoryProviderConfigurationProvider.cs

@ -0,0 +1,7 @@
namespace EasyAbp.EShop.Products.Options.InventoryProviders
{
public interface IInventoryProviderConfigurationProvider
{
InventoryProviderConfiguration Get(string providerName);
}
}

13
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Options/InventoryProviders/InventoryProviderConfiguration.cs

@ -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; }
}
}

20
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Options/InventoryProviders/InventoryProviderConfigurationProvider.cs

@ -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);
}
}
}

59
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Options/InventoryProviders/InventoryProviderConfigurations.cs

@ -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;
}
}
}

13
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Options/ProductGroups/ProductGroupConfiguration.cs

@ -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; }
} }
} }

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/ProductInventories/IProductInventoryRepository.cs

@ -11,6 +11,6 @@ namespace EasyAbp.EShop.Products.ProductInventories
{ {
Task<InventoryDataModel> GetInventoryDataAsync(Guid productSkuId, CancellationToken cancellationToken = default); Task<InventoryDataModel> GetInventoryDataAsync(Guid productSkuId, CancellationToken cancellationToken = default);
Task<Dictionary<Guid, InventoryDataModel>> GetInventoryDataDictionaryAsync(List<Guid> productSkuIds, CancellationToken cancellationToken = default); Task<Dictionary<Guid, InventoryDataModel>> GetSkuIdInventoryDataMappingAsync(List<Guid> productSkuIds, CancellationToken cancellationToken = default);
} }
} }

23
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ConsumeInventoryModel.cs

@ -4,12 +4,27 @@ namespace EasyAbp.EShop.Products.Products
{ {
public class ConsumeInventoryModel public class ConsumeInventoryModel
{ {
public Product Product { get; set; } public Product Product { get; }
public ProductSku ProductSku { get; set; } public ProductSku ProductSku { get; }
public Guid StoreId { get; set; } public Guid StoreId { get; }
public int Quantity { get; set; } public int Quantity { get; }
public bool Consumed { get; private set;}
public ConsumeInventoryModel(Product product, ProductSku productSku, Guid storeId, int quantity)
{
Product = product;
ProductSku = productSku;
StoreId = storeId;
Quantity = quantity;
}
public void SetConsumed(bool consumed)
{
Consumed = consumed;
}
} }
} }

67
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs

@ -13,70 +13,77 @@ namespace EasyAbp.EShop.Products.Products
{ {
public class DefaultProductInventoryProvider : IProductInventoryProvider, ITransientDependency public class DefaultProductInventoryProvider : IProductInventoryProvider, ITransientDependency
{ {
public static string DefaultProductInventoryProviderName { get; set; } = "Default";
public static string DefaultProductInventoryProviderDisplayName { get; set; } = "Default";
public static string DefaultProductInventoryProviderDescription { get; set; } = "Default";
public string InventoryProviderName { get; } = DefaultProductInventoryProviderName;
// Todo: should use IProductInventoryStore. // Todo: should use IProductInventoryStore.
private readonly IGuidGenerator _guidGenerator; private readonly IGuidGenerator _guidGenerator;
private readonly ICurrentTenant _currentTenant; private readonly ICurrentTenant _currentTenant;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IDistributedEventBus _distributedEventBus; private readonly IDistributedEventBus _distributedEventBus;
private readonly IProductInventoryRepository _productInventoryRepository; private readonly IProductInventoryRepository _productInventoryRepository;
public DefaultProductInventoryProvider( public DefaultProductInventoryProvider(
IGuidGenerator guidGenerator, IGuidGenerator guidGenerator,
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
IDistributedEventBus distributedEventBus, IDistributedEventBus distributedEventBus,
IProductInventoryRepository productInventoryRepository) IProductInventoryRepository productInventoryRepository)
{ {
_guidGenerator = guidGenerator; _guidGenerator = guidGenerator;
_currentTenant = currentTenant; _currentTenant = currentTenant;
_unitOfWorkManager = unitOfWorkManager;
_distributedEventBus = distributedEventBus; _distributedEventBus = distributedEventBus;
_productInventoryRepository = productInventoryRepository; _productInventoryRepository = productInventoryRepository;
} }
[UnitOfWork] [UnitOfWork]
public virtual async Task<InventoryDataModel> GetInventoryDataAsync(Product product, ProductSku productSku) public virtual async Task<InventoryDataModel> GetInventoryDataAsync(InventoryQueryModel model)
{ {
return await _productInventoryRepository.GetInventoryDataAsync(productSku.Id); return await _productInventoryRepository.GetInventoryDataAsync(model.ProductSkuId);
} }
[UnitOfWork] [UnitOfWork]
public virtual async Task<Dictionary<Guid, InventoryDataModel>> GetInventoryDataDictionaryAsync(Product product) public virtual async Task<Dictionary<Guid, InventoryDataModel>> GetSkuIdInventoryDataMappingAsync(
IList<InventoryQueryModel> models)
{ {
var dict = await _productInventoryRepository.GetInventoryDataDictionaryAsync(product.ProductSkus var dict = await _productInventoryRepository.GetSkuIdInventoryDataMappingAsync(
.Select(sku => sku.Id).ToList()); models.Select(x => x.ProductSkuId).ToList());
foreach (var sku in product.ProductSkus) foreach (var model in models)
{ {
dict.GetOrAdd(sku.Id, () => new InventoryDataModel()); dict.GetOrAdd(model.ProductSkuId, () => new InventoryDataModel());
} }
return dict; return dict;
} }
[UnitOfWork(true)] [UnitOfWork(true)]
public virtual async Task<bool> TryIncreaseInventoryAsync(Product product, ProductSku productSku, int quantity, bool decreaseSold) public virtual async Task<bool> TryIncreaseInventoryAsync(InventoryQueryModel model, int quantity,
bool decreaseSold)
{ {
var productInventory = await GetOrCreateProductInventoryAsync(product.Id, productSku.Id); var productInventory = await GetOrCreateProductInventoryAsync(model.ProductId, model.ProductSkuId);
return await TryIncreaseInventoryAsync(product, productInventory, quantity, decreaseSold); return await TryIncreaseInventoryAsync(model, productInventory, quantity, decreaseSold);
} }
[UnitOfWork(true)] [UnitOfWork(true)]
public virtual async Task<bool> TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold) public virtual async Task<bool> TryReduceInventoryAsync(InventoryQueryModel model, int quantity,
bool increaseSold)
{ {
var productInventory = await GetOrCreateProductInventoryAsync(product.Id, productSku.Id); var productInventory = await GetOrCreateProductInventoryAsync(model.ProductId, model.ProductSkuId);
return await TryReduceInventoryAsync(product, productInventory, quantity, increaseSold); return await TryReduceInventoryAsync(model, productInventory, quantity, increaseSold);
} }
[UnitOfWork] [UnitOfWork]
protected virtual async Task<ProductInventory> GetOrCreateProductInventoryAsync(Guid productId, Guid productSkuId) protected virtual async Task<ProductInventory> GetOrCreateProductInventoryAsync(Guid productId,
Guid productSkuId)
{ {
var productInventory = var productInventory =
await _productInventoryRepository.FindAsync(x => await _productInventoryRepository.FindAsync(x =>
x.ProductId == productId && x.ProductSkuId == productSkuId); x.ProductId == productId && x.ProductSkuId == productSkuId);
if (productInventory is null) if (productInventory is null)
{ {
productInventory = new ProductInventory(_guidGenerator.Create(), _currentTenant.Id, productId, productInventory = new ProductInventory(_guidGenerator.Create(), _currentTenant.Id, productId,
@ -87,15 +94,16 @@ namespace EasyAbp.EShop.Products.Products
return productInventory; return productInventory;
} }
[UnitOfWork(true)] [UnitOfWork(true)]
public virtual async Task<bool> TryIncreaseInventoryAsync(Product product, ProductInventory productInventory, int quantity, bool decreaseSold) protected virtual async Task<bool> TryIncreaseInventoryAsync(InventoryQueryModel model,
ProductInventory productInventory, int quantity, bool decreaseSold)
{ {
if (quantity < 0) if (quantity < 0)
{ {
return false; return false;
} }
var originalInventory = productInventory.Inventory; var originalInventory = productInventory.Inventory;
if (!productInventory.TryIncreaseInventory(quantity, decreaseSold)) if (!productInventory.TryIncreaseInventory(quantity, decreaseSold))
@ -105,15 +113,16 @@ namespace EasyAbp.EShop.Products.Products
await _productInventoryRepository.UpdateAsync(productInventory, true); await _productInventoryRepository.UpdateAsync(productInventory, true);
await PublishInventoryChangedEventAsync(product.TenantId, product.StoreId, await PublishInventoryChangedEventAsync(model.TenantId, model.StoreId,
productInventory.ProductId, productInventory.ProductSkuId, originalInventory, productInventory.ProductId, productInventory.ProductSkuId, originalInventory,
productInventory.Inventory, productInventory.Sold); productInventory.Inventory, productInventory.Sold);
return true; return true;
} }
[UnitOfWork(true)] [UnitOfWork(true)]
public virtual async Task<bool> TryReduceInventoryAsync(Product product, ProductInventory productInventory, int quantity, bool increaseSold) protected virtual async Task<bool> TryReduceInventoryAsync(InventoryQueryModel model,
ProductInventory productInventory, int quantity, bool increaseSold)
{ {
if (quantity < 0) if (quantity < 0)
{ {
@ -129,7 +138,7 @@ namespace EasyAbp.EShop.Products.Products
await _productInventoryRepository.UpdateAsync(productInventory, true); await _productInventoryRepository.UpdateAsync(productInventory, true);
await PublishInventoryChangedEventAsync(product.TenantId, product.StoreId, await PublishInventoryChangedEventAsync(model.TenantId, model.StoreId,
productInventory.ProductId, productInventory.ProductSkuId, originalInventory, productInventory.ProductId, productInventory.ProductSkuId, originalInventory,
productInventory.Inventory, productInventory.Sold); productInventory.Inventory, productInventory.Sold);

17
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProvider.cs

@ -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);
}
}

12
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductInventoryProviderResolver.cs

@ -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);
}

1
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using EasyAbp.EShop.Products.ProductInventories;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
namespace EasyAbp.EShop.Products.Products namespace EasyAbp.EShop.Products.Products

14
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/NonexistentInventoryProviderException.cs

@ -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);
}
}
}

45
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderCreatedEventHandler.cs

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using EasyAbp.EShop.Orders.Orders; using EasyAbp.EShop.Orders.Orders;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.EventBus.Distributed; using Volo.Abp.EventBus.Distributed;
@ -15,6 +16,7 @@ namespace EasyAbp.EShop.Products.Products
private readonly ICurrentTenant _currentTenant; private readonly ICurrentTenant _currentTenant;
private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IDistributedEventBus _distributedEventBus; private readonly IDistributedEventBus _distributedEventBus;
private readonly ILogger<OrderCreatedEventHandler> _logger;
private readonly IProductRepository _productRepository; private readonly IProductRepository _productRepository;
private readonly IProductManager _productManager; private readonly IProductManager _productManager;
@ -22,12 +24,14 @@ namespace EasyAbp.EShop.Products.Products
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager, IUnitOfWorkManager unitOfWorkManager,
IDistributedEventBus distributedEventBus, IDistributedEventBus distributedEventBus,
ILogger<OrderCreatedEventHandler> logger,
IProductRepository productRepository, IProductRepository productRepository,
IProductManager productManager) IProductManager productManager)
{ {
_currentTenant = currentTenant; _currentTenant = currentTenant;
_unitOfWorkManager = unitOfWorkManager; _unitOfWorkManager = unitOfWorkManager;
_distributedEventBus = distributedEventBus; _distributedEventBus = distributedEventBus;
_logger = logger;
_productRepository = productRepository; _productRepository = productRepository;
_productManager = productManager; _productManager = productManager;
} }
@ -65,33 +69,52 @@ namespace EasyAbp.EShop.Products.Products
return; return;
} }
models.Add(new ConsumeInventoryModel models.Add(new ConsumeInventoryModel(
{ product, productSku, eventData.Entity.StoreId, orderLine.Quantity));
Product = product,
ProductSku = productSku,
StoreId = eventData.Entity.StoreId,
Quantity = orderLine.Quantity
});
} }
foreach (var model in models) foreach (var model in models)
{ {
if (await _productManager.TryReduceInventoryAsync(model.Product, model.ProductSku, model.Quantity, true)) if (await _productManager.TryReduceInventoryAsync(
model.Product, model.ProductSku, model.Quantity, true))
{ {
continue; continue;
} }
// Todo: should release unused inventory since (external) inventory providers may not be transactional. await TryRollbackInventoriesAsync(models);
await _unitOfWorkManager.Current.RollbackAsync(); await _unitOfWorkManager.Current.RollbackAsync();
await PublishInventoryReductionResultEventAsync(eventData, false, true); await PublishInventoryReductionResultEventAsync(eventData, false, true);
return; return;
} }
await PublishInventoryReductionResultEventAsync(eventData, true); await PublishInventoryReductionResultEventAsync(eventData, true);
} }
protected virtual async Task<bool> TryRollbackInventoriesAsync(IEnumerable<ConsumeInventoryModel> models)
{
var result = true;
foreach (var model in models.Where(x => x.Consumed))
{
if (await _productManager.TryIncreaseInventoryAsync(
model.Product, model.ProductSku, model.Quantity, true))
{
continue;
}
result = false;
_logger.LogWarning(
"OrderCreatedEventHandler: inventory rollback failed! productId = {productId}, productSkuId = {productSkuId}, quantity = {quantity}, reduceSold = {reduceSold}",
model.Product.Id, model.ProductSku.Id, model.Quantity, true);
}
return result;
}
protected virtual async Task PublishInventoryReductionResultEventAsync(EntityCreatedEto<OrderEto> orderCreatedEto, bool isSuccess, bool publishNow = false) protected virtual async Task PublishInventoryReductionResultEventAsync(EntityCreatedEto<OrderEto> orderCreatedEto, bool isSuccess, bool publishNow = false)
{ {
await _distributedEventBus.PublishAsync(new ProductInventoryReductionAfterOrderPlacedResultEto await _distributedEventBus.PublishAsync(new ProductInventoryReductionAfterOrderPlacedResultEto

55
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/OrderPaidEventHandler.cs

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using EasyAbp.EShop.Orders.Orders; using EasyAbp.EShop.Orders.Orders;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed; using Volo.Abp.EventBus.Distributed;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
@ -13,6 +14,7 @@ namespace EasyAbp.EShop.Products.Products
{ {
private readonly ICurrentTenant _currentTenant; private readonly ICurrentTenant _currentTenant;
private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly ILogger<OrderPaidEventHandler> _logger;
private readonly IDistributedEventBus _distributedEventBus; private readonly IDistributedEventBus _distributedEventBus;
private readonly IProductRepository _productRepository; private readonly IProductRepository _productRepository;
private readonly IProductManager _productManager; private readonly IProductManager _productManager;
@ -20,24 +22,26 @@ namespace EasyAbp.EShop.Products.Products
public OrderPaidEventHandler( public OrderPaidEventHandler(
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager, IUnitOfWorkManager unitOfWorkManager,
ILogger<OrderPaidEventHandler> logger,
IDistributedEventBus distributedEventBus, IDistributedEventBus distributedEventBus,
IProductRepository productRepository, IProductRepository productRepository,
IProductManager productManager) IProductManager productManager)
{ {
_currentTenant = currentTenant; _currentTenant = currentTenant;
_unitOfWorkManager = unitOfWorkManager; _unitOfWorkManager = unitOfWorkManager;
_logger = logger;
_distributedEventBus = distributedEventBus; _distributedEventBus = distributedEventBus;
_productRepository = productRepository; _productRepository = productRepository;
_productManager = productManager; _productManager = productManager;
} }
[UnitOfWork(true)] [UnitOfWork(true)]
public virtual async Task HandleEventAsync(OrderPaidEto eventData) public virtual async Task HandleEventAsync(OrderPaidEto eventData)
{ {
using var changeTenant = _currentTenant.Change(eventData.Order.TenantId); using var changeTenant = _currentTenant.Change(eventData.Order.TenantId);
var models = new List<ConsumeInventoryModel>(); var models = new List<ConsumeInventoryModel>();
foreach (var orderLine in eventData.Order.OrderLines) foreach (var orderLine in eventData.Order.OrderLines)
{ {
// Todo: Should use ProductHistory. // Todo: Should use ProductHistory.
@ -48,10 +52,10 @@ namespace EasyAbp.EShop.Products.Products
if (productSku == null) if (productSku == null)
{ {
await PublishInventoryReductionResultEventAsync(eventData, false); await PublishInventoryReductionResultEventAsync(eventData, false);
return; return;
} }
if (product.InventoryStrategy != InventoryStrategy.ReduceAfterPayment) if (product.InventoryStrategy != InventoryStrategy.ReduceAfterPayment)
{ {
continue; continue;
@ -60,37 +64,56 @@ namespace EasyAbp.EShop.Products.Products
if (!await _productManager.IsInventorySufficientAsync(product, productSku, orderLine.Quantity)) if (!await _productManager.IsInventorySufficientAsync(product, productSku, orderLine.Quantity))
{ {
await PublishInventoryReductionResultEventAsync(eventData, false); await PublishInventoryReductionResultEventAsync(eventData, false);
return; return;
} }
models.Add(new ConsumeInventoryModel models.Add(new ConsumeInventoryModel(product, productSku, eventData.Order.StoreId, orderLine.Quantity));
{
Product = product,
ProductSku = productSku,
StoreId = eventData.Order.StoreId,
Quantity = orderLine.Quantity
});
} }
foreach (var model in models) foreach (var model in models)
{ {
if (await _productManager.TryReduceInventoryAsync(model.Product, model.ProductSku, model.Quantity, true)) if (await _productManager.TryReduceInventoryAsync(
model.Product, model.ProductSku, model.Quantity, true))
{ {
continue; continue;
} }
await TryRollbackInventoriesAsync(models);
await _unitOfWorkManager.Current.RollbackAsync(); await _unitOfWorkManager.Current.RollbackAsync();
await PublishInventoryReductionResultEventAsync(eventData, false, true); await PublishInventoryReductionResultEventAsync(eventData, false, true);
return; return;
} }
await PublishInventoryReductionResultEventAsync(eventData, true); await PublishInventoryReductionResultEventAsync(eventData, true);
} }
protected virtual async Task PublishInventoryReductionResultEventAsync(OrderPaidEto orderPaidEto, bool isSuccess, bool publishNow = false) protected virtual async Task<bool> TryRollbackInventoriesAsync(IEnumerable<ConsumeInventoryModel> models)
{
var result = true;
foreach (var model in models.Where(x => x.Consumed))
{
if (await _productManager.TryIncreaseInventoryAsync(
model.Product, model.ProductSku, model.Quantity, true))
{
continue;
}
result = false;
_logger.LogWarning(
"OrderPaidEventHandler: inventory rollback failed! productId = {productId}, productSkuId = {productSkuId}, quantity = {quantity}, reduceSold = {reduceSold}",
model.Product.Id, model.ProductSku.Id, model.Quantity, true);
}
return result;
}
protected virtual async Task PublishInventoryReductionResultEventAsync(OrderPaidEto orderPaidEto,
bool isSuccess, bool publishNow = false)
{ {
await _distributedEventBus.PublishAsync(new ProductInventoryReductionAfterOrderPaidResultEto await _distributedEventBus.PublishAsync(new ProductInventoryReductionAfterOrderPaidResultEto
{ {

43
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/Product.cs

@ -1,6 +1,7 @@
using JetBrains.Annotations; using JetBrains.Annotations;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using EasyAbp.EShop.Products.Options.ProductGroups;
using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
@ -9,37 +10,39 @@ namespace EasyAbp.EShop.Products.Products
public class Product : FullAuditedAggregateRoot<Guid>, IProduct, IMultiTenant public class Product : FullAuditedAggregateRoot<Guid>, IProduct, IMultiTenant
{ {
public virtual Guid? TenantId { get; protected set; } public virtual Guid? TenantId { get; protected set; }
public virtual Guid StoreId { get; protected set; } public virtual Guid StoreId { get; protected set; }
[NotNull] [NotNull] public virtual string ProductGroupName { get; protected set; }
public virtual string ProductGroupName { get; protected set; }
public virtual Guid? ProductDetailId { get; protected set; } public virtual Guid? ProductDetailId { get; protected set; }
[CanBeNull] [CanBeNull] public virtual string UniqueName { get; protected set; }
public virtual string UniqueName { get; protected set; }
[NotNull] public virtual string DisplayName { get; protected set; }
[NotNull]
public virtual string DisplayName { get; protected set; }
public virtual InventoryStrategy InventoryStrategy { get; protected set; } public virtual InventoryStrategy InventoryStrategy { get; protected set; }
[CanBeNull] /// <summary>
public virtual string MediaResources { get; protected set; } /// If the value is <c>null</c>, it will fall back to DefaultInventoryProviderName
/// in the <see cref="ProductGroupConfiguration"/>.
/// </summary>
public virtual string InventoryProviderName { get; protected set; }
[CanBeNull] public virtual string MediaResources { get; protected set; }
public virtual int DisplayOrder { get; protected set; } public virtual int DisplayOrder { get; protected set; }
public virtual bool IsPublished { get; protected set; } public virtual bool IsPublished { get; protected set; }
public virtual bool IsStatic { get; protected set; } public virtual bool IsStatic { get; protected set; }
public virtual bool IsHidden { get; protected set; } public virtual bool IsHidden { get; protected set; }
public virtual TimeSpan? PaymentExpireIn { get; protected set; } public virtual TimeSpan? PaymentExpireIn { get; protected set; }
public virtual List<ProductAttribute> ProductAttributes { get; protected set; } public virtual List<ProductAttribute> ProductAttributes { get; protected set; }
public virtual List<ProductSku> ProductSkus { get; protected set; } public virtual List<ProductSku> ProductSkus { get; protected set; }
protected Product() protected Product()
@ -55,6 +58,7 @@ namespace EasyAbp.EShop.Products.Products
[CanBeNull] string uniqueName, [CanBeNull] string uniqueName,
[NotNull] string displayName, [NotNull] string displayName,
InventoryStrategy inventoryStrategy, InventoryStrategy inventoryStrategy,
[CanBeNull] string inventoryProviderName,
bool isPublished, bool isPublished,
bool isStatic, bool isStatic,
bool isHidden, bool isHidden,
@ -70,13 +74,14 @@ namespace EasyAbp.EShop.Products.Products
UniqueName = uniqueName?.Trim(); UniqueName = uniqueName?.Trim();
DisplayName = displayName; DisplayName = displayName;
InventoryStrategy = inventoryStrategy; InventoryStrategy = inventoryStrategy;
InventoryProviderName = inventoryProviderName;
IsPublished = isPublished; IsPublished = isPublished;
IsStatic = isStatic; IsStatic = isStatic;
IsHidden = isHidden; IsHidden = isHidden;
PaymentExpireIn = paymentExpireIn; PaymentExpireIn = paymentExpireIn;
MediaResources = mediaResources; MediaResources = mediaResources;
DisplayOrder = displayOrder; DisplayOrder = displayOrder;
ProductAttributes = new List<ProductAttribute>(); ProductAttributes = new List<ProductAttribute>();
ProductSkus = new List<ProductSku>(); ProductSkus = new List<ProductSku>();
} }
@ -92,4 +97,4 @@ namespace EasyAbp.EShop.Products.Products
UniqueName = UniqueName?.Trim(); UniqueName = UniqueName?.Trim();
} }
} }
} }

78
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductInventoryProviderResolver.cs

@ -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;
}
}
}

86
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs

@ -5,7 +5,7 @@ using System.Threading.Tasks;
using EasyAbp.EShop.Products.Options.ProductGroups; using EasyAbp.EShop.Products.Options.ProductGroups;
using EasyAbp.EShop.Products.ProductCategories; using EasyAbp.EShop.Products.ProductCategories;
using EasyAbp.EShop.Products.ProductDetails; using EasyAbp.EShop.Products.ProductDetails;
using Microsoft.Extensions.DependencyInjection; using EasyAbp.EShop.Products.ProductInventories;
using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
using Volo.Abp.Uow; using Volo.Abp.Uow;
@ -18,7 +18,7 @@ namespace EasyAbp.EShop.Products.Products
private readonly IProductPriceProvider _productPriceProvider; private readonly IProductPriceProvider _productPriceProvider;
private readonly IProductDetailRepository _productDetailRepository; private readonly IProductDetailRepository _productDetailRepository;
private readonly IProductCategoryRepository _productCategoryRepository; private readonly IProductCategoryRepository _productCategoryRepository;
private readonly IProductInventoryProvider _productInventoryProvider; private readonly IProductInventoryProviderResolver _productInventoryProviderResolver;
private readonly IAttributeOptionIdsSerializer _attributeOptionIdsSerializer; private readonly IAttributeOptionIdsSerializer _attributeOptionIdsSerializer;
private readonly IProductGroupConfigurationProvider _productGroupConfigurationProvider; private readonly IProductGroupConfigurationProvider _productGroupConfigurationProvider;
@ -27,7 +27,7 @@ namespace EasyAbp.EShop.Products.Products
IProductPriceProvider productPriceProvider, IProductPriceProvider productPriceProvider,
IProductDetailRepository productDetailRepository, IProductDetailRepository productDetailRepository,
IProductCategoryRepository productCategoryRepository, IProductCategoryRepository productCategoryRepository,
IProductInventoryProvider productInventoryProvider, IProductInventoryProviderResolver productInventoryProviderResolver,
IAttributeOptionIdsSerializer attributeOptionIdsSerializer, IAttributeOptionIdsSerializer attributeOptionIdsSerializer,
IProductGroupConfigurationProvider productGroupConfigurationProvider) IProductGroupConfigurationProvider productGroupConfigurationProvider)
{ {
@ -35,7 +35,7 @@ namespace EasyAbp.EShop.Products.Products
_productPriceProvider = productPriceProvider; _productPriceProvider = productPriceProvider;
_productDetailRepository = productDetailRepository; _productDetailRepository = productDetailRepository;
_productCategoryRepository = productCategoryRepository; _productCategoryRepository = productCategoryRepository;
_productInventoryProvider = productInventoryProvider; _productInventoryProviderResolver = productInventoryProviderResolver;
_attributeOptionIdsSerializer = attributeOptionIdsSerializer; _attributeOptionIdsSerializer = attributeOptionIdsSerializer;
_productGroupConfigurationProvider = productGroupConfigurationProvider; _productGroupConfigurationProvider = productGroupConfigurationProvider;
} }
@ -44,9 +44,11 @@ namespace EasyAbp.EShop.Products.Products
public virtual async Task<Product> CreateAsync(Product product, IEnumerable<Guid> categoryIds = null) public virtual async Task<Product> CreateAsync(Product product, IEnumerable<Guid> categoryIds = null)
{ {
product.TrimUniqueName(); product.TrimUniqueName();
await CheckProductGroupNameAsync(product); await CheckProductGroupNameAsync(product);
await CheckInventoryProviderNameAsync(product);
await CheckProductUniqueNameAsync(product); await CheckProductUniqueNameAsync(product);
await _productRepository.InsertAsync(product, autoSave: true); await _productRepository.InsertAsync(product, autoSave: true);
@ -64,15 +66,30 @@ namespace EasyAbp.EShop.Products.Products
{ {
throw new NonexistentProductGroupException(product.ProductGroupName); throw new NonexistentProductGroupException(product.ProductGroupName);
} }
return Task.CompletedTask; return Task.CompletedTask;
} }
protected virtual async Task CheckInventoryProviderNameAsync(Product product)
{
if (product.InventoryProviderName.IsNullOrEmpty())
{
return;
}
if (!await _productInventoryProviderResolver.ExistProviderAsync(product.InventoryProviderName!))
{
throw new NonexistentInventoryProviderException(product.InventoryProviderName);
}
}
[UnitOfWork(true)] [UnitOfWork(true)]
public virtual async Task<Product> UpdateAsync(Product product, IEnumerable<Guid> categoryIds = null) public virtual async Task<Product> UpdateAsync(Product product, IEnumerable<Guid> categoryIds = null)
{ {
await CheckProductGroupNameAsync(product); await CheckProductGroupNameAsync(product);
await CheckInventoryProviderNameAsync(product);
await CheckProductUniqueNameAsync(product); await CheckProductUniqueNameAsync(product);
await _productRepository.UpdateAsync(product, autoSave: true); await _productRepository.UpdateAsync(product, autoSave: true);
@ -91,7 +108,7 @@ namespace EasyAbp.EShop.Products.Products
await _productRepository.DeleteAsync(product, true); await _productRepository.DeleteAsync(product, true);
} }
[UnitOfWork(true)] [UnitOfWork(true)]
public virtual async Task DeleteAsync(Guid id) public virtual async Task DeleteAsync(Guid id)
{ {
@ -108,13 +125,13 @@ namespace EasyAbp.EShop.Products.Products
await CheckSkuAttributeOptionsAsync(product, productSku); await CheckSkuAttributeOptionsAsync(product, productSku);
await CheckProductSkuNameUniqueAsync(product, productSku); await CheckProductSkuNameUniqueAsync(product, productSku);
productSku.TrimName(); productSku.TrimName();
product.ProductSkus.AddIfNotContains(productSku); product.ProductSkus.AddIfNotContains(productSku);
await CheckProductDetailAsync(product); await CheckProductDetailAsync(product);
return await _productRepository.UpdateAsync(product, true); return await _productRepository.UpdateAsync(product, true);
} }
@ -124,9 +141,9 @@ namespace EasyAbp.EShop.Products.Products
{ {
return Task.CompletedTask; return Task.CompletedTask;
} }
if (product.ProductSkus.Where(sku => sku.Id != productSku.Id) if (product.ProductSkus.Where(sku => sku.Id != productSku.Id)
.FirstOrDefault(sku => sku.Name == productSku.Name) != null) .FirstOrDefault(sku => sku.Name == productSku.Name) != null)
{ {
throw new ProductSkuCodeDuplicatedException(product.Id, productSku.Name); throw new ProductSkuCodeDuplicatedException(product.Id, productSku.Name);
} }
@ -169,7 +186,7 @@ namespace EasyAbp.EShop.Products.Products
public virtual async Task<Product> DeleteSkuAsync(Product product, ProductSku productSku) public virtual async Task<Product> DeleteSkuAsync(Product product, ProductSku productSku)
{ {
product.ProductSkus.Remove(productSku); product.ProductSkus.Remove(productSku);
return await _productRepository.UpdateAsync(product, true); return await _productRepository.UpdateAsync(product, true);
} }
@ -178,20 +195,20 @@ namespace EasyAbp.EShop.Products.Products
{ {
await _productRepository.CheckUniqueNameAsync(product); await _productRepository.CheckUniqueNameAsync(product);
} }
protected virtual async Task CheckProductDetailAsync(Product product) protected virtual async Task CheckProductDetailAsync(Product product)
{ {
if (product.ProductDetailId.HasValue) if (product.ProductDetailId.HasValue)
{ {
await CheckProductDetailExistAsync(product.ProductDetailId.Value, product.StoreId); await CheckProductDetailExistAsync(product.ProductDetailId.Value, product.StoreId);
} }
foreach (var sku in product.ProductSkus.Where(x => x.ProductDetailId.HasValue)) foreach (var sku in product.ProductSkus.Where(x => x.ProductDetailId.HasValue))
{ {
await CheckProductDetailExistAsync(sku.ProductDetailId!.Value, product.StoreId); await CheckProductDetailExistAsync(sku.ProductDetailId!.Value, product.StoreId);
} }
} }
[UnitOfWork] [UnitOfWork]
protected virtual async Task CheckProductDetailExistAsync(Guid productDetailId, Guid storeId) protected virtual async Task CheckProductDetailExistAsync(Guid productDetailId, Guid storeId)
{ {
@ -202,7 +219,7 @@ namespace EasyAbp.EShop.Products.Products
throw new EntityNotFoundException(typeof(ProductDetail), productDetailId); throw new EntityNotFoundException(typeof(ProductDetail), productDetailId);
} }
} }
[UnitOfWork(true)] [UnitOfWork(true)]
protected virtual async Task UpdateProductCategoriesAsync(Guid productId, IEnumerable<Guid> categoryIds) protected virtual async Task UpdateProductCategoriesAsync(Guid productId, IEnumerable<Guid> categoryIds)
{ {
@ -212,7 +229,7 @@ namespace EasyAbp.EShop.Products.Products
{ {
return; return;
} }
foreach (var categoryId in categoryIds) foreach (var categoryId in categoryIds)
{ {
await _productCategoryRepository.InsertAsync( await _productCategoryRepository.InsertAsync(
@ -222,24 +239,37 @@ namespace EasyAbp.EShop.Products.Products
public virtual async Task<bool> IsInventorySufficientAsync(Product product, ProductSku productSku, int quantity) public virtual async Task<bool> IsInventorySufficientAsync(Product product, ProductSku productSku, int quantity)
{ {
var inventoryData = await _productInventoryProvider.GetInventoryDataAsync(product, productSku); var model = new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, productSku.Id);
var inventoryData =
await (await _productInventoryProviderResolver.GetAsync(product)).GetInventoryDataAsync(model);
return product.InventoryStrategy == InventoryStrategy.NoNeed || inventoryData.Inventory - quantity >= 0; return product.InventoryStrategy == InventoryStrategy.NoNeed || inventoryData.Inventory - quantity >= 0;
} }
public virtual async Task<InventoryDataModel> GetInventoryDataAsync(Product product, ProductSku productSku) public virtual async Task<InventoryDataModel> GetInventoryDataAsync(Product product, ProductSku productSku)
{ {
return await _productInventoryProvider.GetInventoryDataAsync(product, productSku); var model = new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, productSku.Id);
return await (await _productInventoryProviderResolver.GetAsync(product)).GetInventoryDataAsync(model);
} }
public virtual async Task<bool> TryIncreaseInventoryAsync(Product product, ProductSku productSku, int quantity, bool reduceSold) public virtual async Task<bool> TryIncreaseInventoryAsync(Product product, ProductSku productSku, int quantity,
bool reduceSold)
{ {
return await _productInventoryProvider.TryIncreaseInventoryAsync(product, productSku, quantity, reduceSold); var model = new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, productSku.Id);
return await (await _productInventoryProviderResolver.GetAsync(product))
.TryIncreaseInventoryAsync(model, quantity, reduceSold);
} }
public virtual async Task<bool> TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold) public virtual async Task<bool> TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity,
bool increaseSold)
{ {
return await _productInventoryProvider.TryReduceInventoryAsync(product, productSku, quantity, increaseSold); var model = new InventoryQueryModel(product.TenantId, product.StoreId, product.Id, productSku.Id);
return await (await _productInventoryProviderResolver.GetAsync(product))
.TryReduceInventoryAsync(model, quantity, increaseSold);
} }
public virtual async Task<PriceDataModel> GetRealPriceAsync(Product product, ProductSku productSku) public virtual async Task<PriceDataModel> GetRealPriceAsync(Product product, ProductSku productSku)
@ -247,7 +277,7 @@ namespace EasyAbp.EShop.Products.Products
var price = await _productPriceProvider.GetPriceAsync(product, productSku); var price = await _productPriceProvider.GetPriceAsync(product, productSku);
var discountedPrice = price; var discountedPrice = price;
// Todo: provider execution ordering. // Todo: provider execution ordering.
foreach (var provider in LazyServiceProvider.LazyGetService<IEnumerable<IProductDiscountProvider>>()) foreach (var provider in LazyServiceProvider.LazyGetService<IEnumerable<IProductDiscountProvider>>())
{ {

24
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductView.cs

@ -22,6 +22,8 @@ namespace EasyAbp.EShop.Products.Products
public virtual InventoryStrategy InventoryStrategy { get; protected set; } public virtual InventoryStrategy InventoryStrategy { get; protected set; }
public string InventoryProviderName { get; protected set; }
public virtual string MediaResources { get; protected set; } public virtual string MediaResources { get; protected set; }
public virtual int DisplayOrder { get; protected set; } public virtual int DisplayOrder { get; protected set; }
@ -33,19 +35,19 @@ namespace EasyAbp.EShop.Products.Products
public virtual bool IsHidden { get; protected set; } public virtual bool IsHidden { get; protected set; }
#endregion #endregion
public virtual string ProductGroupDisplayName { get; protected set; } public virtual string ProductGroupDisplayName { get; protected set; }
public virtual decimal? MinimumPrice { get; protected set; } public virtual decimal? MinimumPrice { get; protected set; }
public virtual decimal? MaximumPrice { get; protected set; } public virtual decimal? MaximumPrice { get; protected set; }
public virtual long Sold { get; protected set; } public virtual long Sold { get; protected set; }
protected ProductView() protected ProductView()
{ {
} }
public ProductView( public ProductView(
Guid id, Guid id,
Guid? tenantId, Guid? tenantId,
@ -55,6 +57,7 @@ namespace EasyAbp.EShop.Products.Products
string uniqueName, string uniqueName,
string displayName, string displayName,
InventoryStrategy inventoryStrategy, InventoryStrategy inventoryStrategy,
string inventoryProviderName,
bool isPublished, bool isPublished,
bool isStatic, bool isStatic,
bool isHidden, bool isHidden,
@ -73,27 +76,28 @@ namespace EasyAbp.EShop.Products.Products
UniqueName = uniqueName?.Trim(); UniqueName = uniqueName?.Trim();
DisplayName = displayName; DisplayName = displayName;
InventoryStrategy = inventoryStrategy; InventoryStrategy = inventoryStrategy;
InventoryProviderName = inventoryProviderName;
IsPublished = isPublished; IsPublished = isPublished;
IsStatic = isStatic; IsStatic = isStatic;
IsHidden = isHidden; IsHidden = isHidden;
MediaResources = mediaResources; MediaResources = mediaResources;
DisplayOrder = displayOrder; DisplayOrder = displayOrder;
ProductGroupDisplayName = productGroupDisplayName; ProductGroupDisplayName = productGroupDisplayName;
MinimumPrice = minimumPrice; MinimumPrice = minimumPrice;
MaximumPrice = maximumPrice; MaximumPrice = maximumPrice;
Sold = sold; Sold = sold;
} }
public void SetSold(long sold) public void SetSold(long sold)
{ {
Sold = sold; Sold = sold;
} }
public void SetPrices(decimal? minimumPrice, decimal? maximumPrice) public void SetPrices(decimal? minimumPrice, decimal? maximumPrice)
{ {
MinimumPrice = minimumPrice; MinimumPrice = minimumPrice;
MaximumPrice = maximumPrice; MaximumPrice = maximumPrice;
} }
} }
} }

5
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/ProductInventories/ProductInventoryRepository.cs

@ -4,7 +4,6 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using EasyAbp.EShop.Products.EntityFrameworkCore; using EasyAbp.EShop.Products.EntityFrameworkCore;
using EasyAbp.EShop.Products.Products;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore;
@ -29,9 +28,9 @@ namespace EasyAbp.EShop.Products.ProductInventories
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
} }
public async Task<Dictionary<Guid, InventoryDataModel>> GetInventoryDataDictionaryAsync(List<Guid> productSkuIds, CancellationToken cancellationToken = default) public async Task<Dictionary<Guid, InventoryDataModel>> GetSkuIdInventoryDataMappingAsync(List<Guid> productSkuIds, CancellationToken cancellationToken = default)
{ {
return await GetQueryable() return await (await GetQueryableAsync())
.Where(x => productSkuIds.Contains(x.ProductSkuId)) .Where(x => productSkuIds.Contains(x.ProductSkuId))
.ToDictionaryAsync(x => x.ProductSkuId, x => new InventoryDataModel .ToDictionaryAsync(x => x.ProductSkuId, x => new InventoryDataModel
{ {

33
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.HttpApi/EasyAbp/EShop/Products/ProductInventories/ProductInventoryController.cs

@ -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);
}
}
}

8
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.HttpApi/EasyAbp/EShop/Products/Products/ProductController.cs

@ -93,5 +93,13 @@ namespace EasyAbp.EShop.Products.Products
{ {
return _service.GetProductGroupListAsync(); return _service.GetProductGroupListAsync();
} }
[HttpPost]
[Route("{id}/sku/{productSkuId}/change-inventory")]
public Task<ChangeProductInventoryResultDto> ChangeInventoryAsync(Guid id, Guid productSkuId,
ChangeProductInventoryDto input)
{
return _service.ChangeInventoryAsync(id, productSkuId, input);
}
} }
} }

6
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Web/Pages/EShop/Products/Products/Product/ViewModels/CreateEditProductViewModel.cs

@ -50,7 +50,11 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.Product.ViewM
[Display(Name = "ProductInventoryStrategy")] [Display(Name = "ProductInventoryStrategy")]
public InventoryStrategy InventoryStrategy { get; set; } public InventoryStrategy InventoryStrategy { get; set; }
[Placeholder("ProductInventoryProviderNamePlaceholder")]
[Display(Name = "ProductInventoryProviderName")]
public string InventoryProviderName { get; set; }
[Display(Name = "ProductDisplayOrder")] [Display(Name = "ProductDisplayOrder")]
public int DisplayOrder { get; set; } public int DisplayOrder { get; set; }

21
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Web/Pages/EShop/Products/Products/ProductSku/ChangeInventoryModal.cshtml.cs

@ -1,7 +1,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using EasyAbp.EShop.Products.ProductInventories; using EasyAbp.EShop.Products.Products;
using EasyAbp.EShop.Products.ProductInventories.Dtos; using EasyAbp.EShop.Products.Products.Dtos;
using EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku.ViewModels; using EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku.ViewModels;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -12,7 +12,7 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku
[HiddenInput] [HiddenInput]
[BindProperty(SupportsGet = true)] [BindProperty(SupportsGet = true)]
public Guid ProductId { get; set; } public Guid ProductId { get; set; }
[HiddenInput] [HiddenInput]
[BindProperty(SupportsGet = true)] [BindProperty(SupportsGet = true)]
public Guid ProductSkuId { get; set; } public Guid ProductSkuId { get; set; }
@ -20,17 +20,18 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku
[BindProperty] [BindProperty]
public ChangeProductInventoryViewModel ViewModel { get; set; } public ChangeProductInventoryViewModel ViewModel { get; set; }
private readonly IProductInventoryAppService _service; private readonly IProductAppService _service;
public ChangeInventoryModal(IProductInventoryAppService service) public ChangeInventoryModal(IProductAppService service)
{ {
_service = service; _service = service;
} }
public virtual async Task OnGetAsync() public virtual async Task OnGetAsync()
{ {
var dto = await _service.GetAsync(ProductId, ProductSkuId); var product = await _service.GetAsync(ProductId);
product.GetSkuById(ProductSkuId); // ensure the specified sku exists.
ViewModel = new ChangeProductInventoryViewModel ViewModel = new ChangeProductInventoryViewModel
{ {
ChangedInventory = 0, ChangedInventory = 0,
@ -40,15 +41,13 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku
public virtual async Task<IActionResult> OnPostAsync() public virtual async Task<IActionResult> OnPostAsync()
{ {
await _service.UpdateAsync(new UpdateProductInventoryDto await _service.ChangeInventoryAsync(ProductId, ProductSkuId, new ChangeProductInventoryDto
{ {
ProductId = ProductId,
ProductSkuId = ProductSkuId,
ChangedInventory = ViewModel.ProductInventoryChangeType == InventoryChangeType.IncreaseInventory ChangedInventory = ViewModel.ProductInventoryChangeType == InventoryChangeType.IncreaseInventory
? ViewModel.ChangedInventory ? ViewModel.ChangedInventory
: -ViewModel.ChangedInventory : -ViewModel.ChangedInventory
}); });
return NoContent(); return NoContent();
} }
} }

22
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Web/Pages/EShop/Products/Products/ProductSku/CreateModal.cshtml.cs

@ -4,8 +4,6 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using EasyAbp.EShop.Products.ProductDetails; using EasyAbp.EShop.Products.ProductDetails;
using EasyAbp.EShop.Products.ProductDetails.Dtos; using EasyAbp.EShop.Products.ProductDetails.Dtos;
using EasyAbp.EShop.Products.ProductInventories;
using EasyAbp.EShop.Products.ProductInventories.Dtos;
using EasyAbp.EShop.Products.Products; using EasyAbp.EShop.Products.Products;
using EasyAbp.EShop.Products.Products.Dtos; using EasyAbp.EShop.Products.Products.Dtos;
using EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku.ViewModels; using EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku.ViewModels;
@ -19,25 +17,22 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku
[HiddenInput] [HiddenInput]
[BindProperty(SupportsGet = true)] [BindProperty(SupportsGet = true)]
public Guid ProductId { get; set; } public Guid ProductId { get; set; }
[BindProperty] [BindProperty]
public CreateProductSkuViewModel ProductSku { get; set; } = new CreateProductSkuViewModel(); public CreateProductSkuViewModel ProductSku { get; set; } = new CreateProductSkuViewModel();
[BindProperty] [BindProperty]
public Dictionary<string, Guid> SelectedAttributeOptionIdDict { get; set; } public Dictionary<string, Guid> SelectedAttributeOptionIdDict { get; set; }
public Dictionary<string, List<SelectListItem>> Attributes { get; set; } public Dictionary<string, List<SelectListItem>> Attributes { get; set; }
private readonly IProductInventoryAppService _productInventoryAppService;
private readonly IProductDetailAppService _productDetailAppService; private readonly IProductDetailAppService _productDetailAppService;
private readonly IProductAppService _productAppService; private readonly IProductAppService _productAppService;
public CreateModalModel( public CreateModalModel(
IProductInventoryAppService productInventoryAppService,
IProductDetailAppService productDetailAppService, IProductDetailAppService productDetailAppService,
IProductAppService productAppService) IProductAppService productAppService)
{ {
_productInventoryAppService = productInventoryAppService;
_productDetailAppService = productDetailAppService; _productDetailAppService = productDetailAppService;
_productAppService = productAppService; _productAppService = productAppService;
} }
@ -47,7 +42,7 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku
var product = await _productAppService.GetAsync(ProductId); var product = await _productAppService.GetAsync(ProductId);
Attributes = new Dictionary<string, List<SelectListItem>>(); Attributes = new Dictionary<string, List<SelectListItem>>();
foreach (var attribute in product.ProductAttributes.ToList()) foreach (var attribute in product.ProductAttributes.ToList())
{ {
Attributes.Add(attribute.DisplayName, Attributes.Add(attribute.DisplayName,
@ -55,13 +50,12 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku
.Select(dto => new SelectListItem(dto.DisplayName, dto.Id.ToString())).ToList()); .Select(dto => new SelectListItem(dto.DisplayName, dto.Id.ToString())).ToList());
} }
} }
public virtual async Task<IActionResult> OnPostAsync() public virtual async Task<IActionResult> OnPostAsync()
{ {
var createDto = ObjectMapper.Map<CreateProductSkuViewModel, CreateProductSkuDto>(ProductSku); var createDto = ObjectMapper.Map<CreateProductSkuViewModel, CreateProductSkuDto>(ProductSku);
createDto.AttributeOptionIds = SelectedAttributeOptionIdDict.Values.ToList(); createDto.AttributeOptionIds = SelectedAttributeOptionIdDict.Values.ToList();
if (ProductSku.ProductDetail.HasContent()) if (ProductSku.ProductDetail.HasContent())
{ {
var detail = await _productDetailAppService.CreateAsync( var detail = await _productDetailAppService.CreateAsync(
@ -70,15 +64,13 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku
createDto.ProductDetailId = detail.Id; createDto.ProductDetailId = detail.Id;
} }
var product = await _productAppService.CreateSkuAsync(ProductId, createDto); var product = await _productAppService.CreateSkuAsync(ProductId, createDto);
var productSku = product.ProductSkus var productSku = product.ProductSkus
.Single(x => !x.AttributeOptionIds.Except(createDto.AttributeOptionIds).Any()); .Single(x => !x.AttributeOptionIds.Except(createDto.AttributeOptionIds).Any());
await _productInventoryAppService.UpdateAsync(new UpdateProductInventoryDto await _productAppService.ChangeInventoryAsync(product.Id, productSku.Id, new ChangeProductInventoryDto
{ {
ProductId = product.Id,
ProductSkuId = productSku.Id,
ChangedInventory = ProductSku.Inventory ChangedInventory = ProductSku.Inventory
}); });

26
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/ProductInventories/ProductInventoryAppServiceTests.cs

@ -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
}
}
}

48
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Domain.Tests/Products/ProductDomainTests.cs

@ -25,13 +25,12 @@ namespace EasyAbp.EShop.Products.Products
public async Task Should_Set_ProductDetailId() public async Task Should_Set_ProductDetailId()
{ {
var product2 = new Product(ProductsTestData.Product2Id, null, ProductsTestData.Store1Id, "Default", var product2 = new Product(ProductsTestData.Product2Id, null, ProductsTestData.Store1Id, "Default",
ProductsTestData.ProductDetails2Id, "Ball", "Ball", InventoryStrategy.NoNeed, true, false, false, null, ProductsTestData.ProductDetails2Id, "Ball", "Ball", InventoryStrategy.NoNeed, null, true, false, false,
null, 0); null, null, 0);
await ProductManager.CreateAsync(product2); await ProductManager.CreateAsync(product2);
product2 = await ProductRepository.GetAsync(product2.Id); product2 = await ProductRepository.GetAsync(product2.Id);
product2.ProductDetailId.ShouldBe(ProductsTestData.ProductDetails2Id); product2.ProductDetailId.ShouldBe(ProductsTestData.ProductDetails2Id);
} }
@ -42,9 +41,9 @@ namespace EasyAbp.EShop.Products.Products
{ {
var product1 = await ProductRepository.GetAsync(ProductsTestData.Product1Id); var product1 = await ProductRepository.GetAsync(ProductsTestData.Product1Id);
var sku1 = product1.ProductSkus.Single(x => x.Id == ProductsTestData.Product1Sku1Id); var sku1 = product1.ProductSkus.Single(x => x.Id == ProductsTestData.Product1Sku1Id);
sku1.ProductDetailId.ShouldBeNull(); sku1.ProductDetailId.ShouldBeNull();
typeof(ProductSku).GetProperty(nameof(ProductSku.ProductDetailId))!.SetValue(sku1, typeof(ProductSku).GetProperty(nameof(ProductSku.ProductDetailId))!.SetValue(sku1,
ProductsTestData.ProductDetails1Id); ProductsTestData.ProductDetails1Id);
@ -65,20 +64,20 @@ namespace EasyAbp.EShop.Products.Products
product1.ProductDetailId.ShouldBe(ProductsTestData.ProductDetails1Id); product1.ProductDetailId.ShouldBe(ProductsTestData.ProductDetails1Id);
var product2 = new Product(ProductsTestData.Product2Id, null, ProductsTestData.Store1Id, "Default", var product2 = new Product(ProductsTestData.Product2Id, null, ProductsTestData.Store1Id, "Default",
ProductsTestData.ProductDetails2Id, "Ball", "Ball", InventoryStrategy.NoNeed, true, false, false, null, ProductsTestData.ProductDetails2Id, "Ball", "Ball", InventoryStrategy.NoNeed, null, true, false, false,
null, 0); null, null, 0);
await ProductManager.CreateAsync(product2); await ProductManager.CreateAsync(product2);
product2 = await ProductRepository.GetAsync(product2.Id); product2 = await ProductRepository.GetAsync(product2.Id);
product2.ProductDetailId.ShouldBe(ProductsTestData.ProductDetails2Id); product2.ProductDetailId.ShouldBe(ProductsTestData.ProductDetails2Id);
typeof(Product).GetProperty(nameof(Product.ProductDetailId))!.SetValue(product2, typeof(Product).GetProperty(nameof(Product.ProductDetailId))!.SetValue(product2,
ProductsTestData.ProductDetails1Id); ProductsTestData.ProductDetails1Id);
await ProductManager.UpdateAsync(product2); await ProductManager.UpdateAsync(product2);
product2 = await ProductRepository.GetAsync(product2.Id); product2 = await ProductRepository.GetAsync(product2.Id);
product2.ProductDetailId.ShouldBe(ProductsTestData.ProductDetails1Id); product2.ProductDetailId.ShouldBe(ProductsTestData.ProductDetails1Id);
@ -88,17 +87,17 @@ namespace EasyAbp.EShop.Products.Products
public async Task Should_Remove_ProductDetailId() public async Task Should_Remove_ProductDetailId()
{ {
await Should_Set_ProductDetailId(); await Should_Set_ProductDetailId();
var product2 = await ProductRepository.GetAsync(ProductsTestData.Product2Id); var product2 = await ProductRepository.GetAsync(ProductsTestData.Product2Id);
product2.ProductDetailId.ShouldNotBeNull(); product2.ProductDetailId.ShouldNotBeNull();
typeof(Product).GetProperty(nameof(Product.ProductDetailId))!.SetValue(product2, null); typeof(Product).GetProperty(nameof(Product.ProductDetailId))!.SetValue(product2, null);
await ProductManager.UpdateAsync(product2); await ProductManager.UpdateAsync(product2);
product2 = await ProductRepository.GetAsync(product2.Id); product2 = await ProductRepository.GetAsync(product2.Id);
product2.ProductDetailId.ShouldBeNull(); product2.ProductDetailId.ShouldBeNull();
} }
@ -135,7 +134,7 @@ namespace EasyAbp.EShop.Products.Products
ProductsTestData.Product1Attribute1Option4Id, ProductsTestData.Product1Attribute1Option4Id,
ProductsTestData.Product1Attribute2Option2Id ProductsTestData.Product1Attribute2Option2Id
}; };
await Should.NotThrowAsync(async () => await Should.NotThrowAsync(async () =>
{ {
await ProductManager.CreateSkuAsync(product1, await CreateTestSkuAsync(attributeOptionIds)); await ProductManager.CreateSkuAsync(product1, await CreateTestSkuAsync(attributeOptionIds));
@ -178,10 +177,27 @@ namespace EasyAbp.EShop.Products.Products
}); });
} }
[Fact]
public async Task Should_Use_Fake_Inventory_Provider()
{
var product2 = new Product(ProductsTestData.Product2Id, null, ProductsTestData.Store1Id, "Default",
ProductsTestData.ProductDetails2Id, "Ball", "Ball", InventoryStrategy.NoNeed, "Fake", true, false,
false, null, null, 0);
await ProductManager.CreateAsync(product2);
var fakeSku = new ProductSku(Guid.NewGuid(), "", null, "", null, 0m, 1, 1, null, null, null);
var inventoryDataModel = await ProductManager.GetInventoryDataAsync(product2, fakeSku);
inventoryDataModel.ShouldNotBeNull();
inventoryDataModel.Inventory.ShouldBe(9998);
}
private async Task<ProductSku> CreateTestSkuAsync(IEnumerable<Guid> attributeOptionIds) private async Task<ProductSku> CreateTestSkuAsync(IEnumerable<Guid> attributeOptionIds)
{ {
return new ProductSku(Guid.NewGuid(), await AttributeOptionIdsSerializer.SerializeAsync(attributeOptionIds), return new ProductSku(Guid.NewGuid(), await AttributeOptionIdsSerializer.SerializeAsync(attributeOptionIds),
"test-sku", "CNY", null, 0m, 1, 10, null, null, null); "test-sku", "CNY", null, 0m, 1, 10, null, null, null);
} }
} }
} }

16
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/EShopProductsTestBaseModule.cs

@ -1,4 +1,7 @@
using Microsoft.Extensions.DependencyInjection; using EasyAbp.EShop.Products.Options;
using EasyAbp.EShop.Products.Options.ProductGroups;
using EasyAbp.EShop.Products.Products;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Authorization; using Volo.Abp.Authorization;
using Volo.Abp.Autofac; using Volo.Abp.Autofac;
@ -19,6 +22,17 @@ namespace EasyAbp.EShop.Products
public override void ConfigureServices(ServiceConfigurationContext context) public override void ConfigureServices(ServiceConfigurationContext context)
{ {
context.Services.AddAlwaysAllowAuthorization(); context.Services.AddAlwaysAllowAuthorization();
Configure<EShopProductsOptions>(options =>
{
options.InventoryProviders.Configure(
"Fake", provider =>
{
provider.DisplayName = "Fake";
provider.Description = "For tests";
provider.ProviderType = typeof(FakeProductInventoryProvider);
});
});
} }
public override void OnApplicationInitialization(ApplicationInitializationContext context) public override void OnApplicationInitialization(ApplicationInitializationContext context)

60
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/FakeProductInventoryProvider.cs

@ -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);
}
}

2
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.TestBase/ProductsTestDataBuilder.cs

@ -45,7 +45,7 @@ namespace EasyAbp.EShop.Products
"Product details for store 1"), true); "Product details for store 1"), true);
var product = new Product(ProductsTestData.Product1Id, null, ProductsTestData.Store1Id, "Default", var product = new Product(ProductsTestData.Product1Id, null, ProductsTestData.Store1Id, "Default",
productDetail1.Id, "Cake", "Cake", InventoryStrategy.NoNeed, true, false, false, null, null, 0); productDetail1.Id, "Cake", "Cake", InventoryStrategy.NoNeed, null, true, false, false, null, null, 0);
var attribute1 = new ProductAttribute(ProductsTestData.Product1Attribute1Id, "Size", null, 2); var attribute1 = new ProductAttribute(ProductsTestData.Product1Attribute1Id, "Size", null, 2);
var attribute2 = new ProductAttribute(ProductsTestData.Product1Attribute2Id, "Color", null, 1); var attribute2 = new ProductAttribute(ProductsTestData.Product1Attribute2Id, "Color", null, 1);

1
plugins/Inventories/DaprActors/README.md

@ -0,0 +1 @@
../../../docs/plugins/inventories/dapr-actors/README.md

18
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions.csproj

@ -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>

11
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions/EasyAbp/EShop/Plugins/Inventories/DaprActors/EShopPluginsInventoriesDaprActorsAbstractionsModule.cs

@ -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
{
}

13
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions/EasyAbp/EShop/Plugins/Inventories/DaprActors/IInventoryActor.cs

@ -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);
}

8
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions/EasyAbp/EShop/Plugins/Inventories/DaprActors/InventoryStateModel.cs

@ -0,0 +1,8 @@
namespace EasyAbp.EShop.Plugins.Inventories.DaprActors;
public class InventoryStateModel
{
public int Inventory { get; set; }
public long Sold { get; set; }
}

3
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.Abstractions/FodyWeavers.xsd

@ -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>

19
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore/EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore.csproj

@ -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>

29
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore/EasyAbp/EShop/Plugins/Inventories/DaprActors/EShopPluginsInventoriesDaprActorsAspNetCoreModule.cs

@ -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();
});
});
}
}

3
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors.AspNetCore/FodyWeavers.xsd

@ -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>

18
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors/EasyAbp.EShop.Plugins.Inventories.DaprActors.csproj

@ -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>

10
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors/EasyAbp/EShop/Plugins/Inventories/DaprActors/EShopPluginsInventoriesDaprActorsModule.cs

@ -0,0 +1,10 @@
using Volo.Abp.Modularity;
namespace EasyAbp.EShop.Plugins.Inventories.DaprActors;
[DependsOn(
typeof(EShopPluginsInventoriesDaprActorsAbstractionsModule)
)]
public class EShopPluginsInventoriesDaprActorsModule : AbpModule
{
}

87
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors/EasyAbp/EShop/Plugins/Inventories/DaprActors/InventoryActor.cs

@ -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;
}
}

3
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Plugins.Inventories.DaprActors/FodyWeavers.xsd

@ -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>

15
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Products.DaprActorsInventory.Domain/EasyAbp.EShop.Products.DaprActorsInventory.Domain.csproj

@ -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>

111
plugins/Inventories/DaprActors/src/EasyAbp.EShop.Products.DaprActorsInventory.Domain/EasyAbp/EShop/Products/DaprActorsInventory/DaprActorsProductInventoryProvider.cs

@ -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…
Cancel
Save