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>
<EasyAbpPaymentServiceModuleVersion>2.0.11</EasyAbpPaymentServiceModuleVersion>
<EasyAbpAbpTagHelperPlusModuleVersion>1.0.0</EasyAbpAbpTagHelperPlusModuleVersion>
<DaprSdkVersion>1.7.0</DaprSdkVersion>
<OrleansVersion>3.6.2</OrleansVersion>
</PropertyGroup>
</Project>

91
EShop.sln

@ -341,6 +341,40 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Plugins.Coupo
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}"
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}"
EndProject
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}.Release|Any CPU.ActiveCfg = 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.Build.0 = Debug|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}
{B076C103-DF0B-464B-A9CB-4BE5CAFEE067} = {4001814E-A67B-490D-9E13-2FB9A34B0A0B}
{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}
{A8C4583C-034E-47AF-B7EC-1A34EE288E2F} = {CE945F1D-6636-47D5-A619-C16C4E14CF8C}
{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/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/=Dapr/@EntryIndexedValue">True</s:Boolean>
<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)
* 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.
* Create a Product
@ -102,6 +102,9 @@ We can customize some features to use EShop in complex application scenarios.
* Plugin modules
* Baskets
* 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)
## 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 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.Domain.Entities.Events.Distributed;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Timing;
namespace EasyAbp.EShop.Orders.Orders
{
@ -10,14 +11,17 @@ namespace EasyAbp.EShop.Orders.Orders
IDistributedEventHandler<EntityCreatedEto<OrderEto>>,
ITransientDependency
{
private readonly IClock _clock;
private readonly IBackgroundJobManager _backgroundJobManager;
public OrderAutoCancelOnCreatedHandler(
IClock clock,
IBackgroundJobManager backgroundJobManager)
{
_clock = clock;
_backgroundJobManager = backgroundJobManager;
}
public virtual async Task HandleEventAsync(EntityCreatedEto<OrderEto> eventData)
{
if (!eventData.Entity.PaymentExpiration.HasValue)
@ -33,7 +37,7 @@ namespace EasyAbp.EShop.Orders.Orders
await _backgroundJobManager.EnqueueAsync(
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)
{
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;
}
// Todo: should handler the inventory rollback.
[UnitOfWork]
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.PaymentService.Refunds;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Timing;
using Volo.Abp.Uow;
@ -11,18 +17,24 @@ namespace EasyAbp.EShop.Orders.Orders
{
private readonly IClock _clock;
private readonly ICurrentTenant _currentTenant;
private readonly IOrderManager _orderManager;
private readonly IOrderRepository _orderRepository;
private readonly IDistributedEventBus _distributedEventBus;
public ProductInventoryReductionEventHandler(
IClock clock,
ICurrentTenant currentTenant,
IOrderRepository orderRepository)
IOrderManager orderManager,
IOrderRepository orderRepository,
IDistributedEventBus distributedEventBus)
{
_clock = clock;
_currentTenant = currentTenant;
_orderManager = orderManager;
_orderRepository = orderRepository;
_distributedEventBus = distributedEventBus;
}
[UnitOfWork(true)]
public virtual async Task HandleEventAsync(ProductInventoryReductionAfterOrderPlacedResultEto eventData)
{
@ -37,10 +49,11 @@ namespace EasyAbp.EShop.Orders.Orders
if (!eventData.IsSuccess)
{
// Todo: Cancel order.
await _orderManager.CancelAsync(order, OrdersConsts.InventoryReductionFailedAutoCancellationReason);
return;
}
order.SetReducedInventoryAfterPlacingTime(_clock.Now);
await _orderRepository.UpdateAsync(order, true);
@ -61,15 +74,58 @@ namespace EasyAbp.EShop.Orders.Orders
if (!eventData.IsSuccess)
{
// Todo: Refund.
// Todo: Cancel order.
var refundOrderEto = CreateRefundOrderEto(order);
await _orderManager.CancelAsync(order, OrdersConsts.InventoryReductionFailedAutoCancellationReason);
await RefundOrderAsync(refundOrderEto);
return;
}
order.SetReducedInventoryAfterPaymentTime(_clock.Now);
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
{
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,
Currency = "CNY",
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,
@ -278,7 +289,7 @@ namespace EasyAbp.EShop.Orders.Orders
response.PaymentExpiration.ShouldBe(now);
response.OrderStatus.ShouldBe(OrderStatus.Canceled);
response.CanceledTime.ShouldNotBeNull();
response.CancellationReason.ShouldBe(OrdersConsts.CancellationReason);
response.CancellationReason.ShouldBe(OrdersConsts.UnpaidAutoCancellationReason);
UsingDbContext(db =>
{
@ -288,7 +299,7 @@ namespace EasyAbp.EShop.Orders.Orders
order.PaymentExpiration.ShouldBe(now);
order.OrderStatus.ShouldBe(OrderStatus.Canceled);
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.OrderStatus.ShouldBe(OrderStatus.Canceled);
response.CanceledTime.ShouldNotBeNull();
response.CancellationReason.ShouldBe(OrdersConsts.CancellationReason);
response.CancellationReason.ShouldBe(OrdersConsts.UnpaidAutoCancellationReason);
UsingDbContext(db =>
{
@ -402,7 +413,7 @@ namespace EasyAbp.EShop.Orders.Orders
order.PaymentExpiration.ShouldBe(now);
order.OrderStatus.ShouldBe(OrderStatus.Canceled);
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.OrderStatus.ShouldBe(OrderStatus.Canceled);
response.CanceledTime.ShouldNotBeNull();
response.CancellationReason.ShouldBe(OrdersConsts.CancellationReason);
response.CancellationReason.ShouldBe(OrdersConsts.UnpaidAutoCancellationReason);
UsingDbContext(db =>
{
@ -459,7 +470,7 @@ namespace EasyAbp.EShop.Orders.Orders
order.PaymentExpiration.ShouldBe(now);
order.OrderStatus.ShouldBe(OrderStatus.Canceled);
order.CanceledTime.ShouldNotBeNull();
order.CancellationReason.ShouldBe(OrdersConsts.CancellationReason);
order.CancellationReason.ShouldBe(OrdersConsts.UnpaidAutoCancellationReason);
});
}
@ -481,7 +492,7 @@ namespace EasyAbp.EShop.Orders.Orders
new()
{
ProductId = OrderTestData.Product1Id,
ProductSkuId = OrderTestData.ProductSku2Id,
ProductSkuId = OrderTestData.ProductSku3Id,
Quantity = 2
}
}
@ -490,11 +501,11 @@ namespace EasyAbp.EShop.Orders.Orders
await WithUnitOfWorkAsync(async () =>
{
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.UnitPrice.ShouldBe(TestOrderLinePriceOverrider.Sku2UnitPrice);
orderLine.UnitPrice.ShouldBe(TestOrderLinePriceOverrider.Sku3UnitPrice);
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 static decimal Sku2UnitPrice { get; set; } = 100m;
public static decimal Sku3UnitPrice { get; set; } = 100m;
public async Task<decimal?> GetUnitPriceOrNullAsync(CreateOrderDto input, CreateOrderLineDto inputOrderLine,
ProductDto product, ProductSkuDto productSku)
{
if (inputOrderLine.ProductSkuId == OrderTestData.ProductSku2Id)
if (inputOrderLine.ProductSkuId == OrderTestData.ProductSku3Id)
{
return Sku2UnitPrice;
return Sku3UnitPrice;
}
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 static Guid Order1Id { get; } = Guid.NewGuid();
public static Guid OrderLine1Id { get; } = Guid.NewGuid();
public static Guid Payment1Id { get; } = Guid.NewGuid();
public static Guid Store1Id { get; } = Guid.NewGuid();
public static Guid Product1Id { get; } = Guid.NewGuid();
public static Guid ProductSku1Id { 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 ProductDetail2Id { get; } = Guid.NewGuid();
public static DateTime ProductLastModificationTime { 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.Collections.Generic;
using JetBrains.Annotations;
using Volo.Abp.ObjectExtending;
namespace EasyAbp.EShop.Payments.Refunds.Dtos
{
[Serializable]
public class CreateEShopRefundItemInput : ExtensibleObject
public class CreateEShopRefundItemInput : CreateEShopRefundItemInfoModel
{
public Guid OrderId { get; set; }
[CanBeNull]
public string CustomerRemark { get; set; }
[CanBeNull]
public string StaffRemark { get; set; }
public List<OrderLineRefundInfoModel> OrderLines { get; set; } = new();
public List<OrderExtraFeeRefundInfoModel> OrderExtraFees { get; set; } = new();
}
}

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.ItemType))?.SetValue(paymentItem, PaymentsConsts.PaymentItemType);
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;
payment.ShouldNotBeNull();
@ -75,7 +76,9 @@ namespace EasyAbp.EShop.Payments.Refunds
?.SetValue(paymentItem, PaymentsConsts.PaymentItemType);
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;
payment.ShouldNotBeNull();
@ -182,10 +185,9 @@ namespace EasyAbp.EShop.Payments.Refunds
// Act & Assert
await _refundAppService.CreateAsync(request);
_testRefundPaymentEventHandler.IsEventPublished.ShouldBe(true);
var eventData = _testRefundPaymentEventHandler.EventData;
var eventData = TestRefundPaymentEventHandler.LastEto;
TestRefundPaymentEventHandler.LastEto = null;
eventData.ShouldNotBeNull();
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
{
public class TestRefundPaymentEventHandler : IDistributedEventHandler<RefundPaymentEto>, ISingletonDependency
public class TestRefundPaymentEventHandler : IDistributedEventHandler<RefundPaymentEto>, ITransientDependency
{
public bool IsEventPublished { get; protected set; }
public RefundPaymentEto EventData { get; protected set; }
public static RefundPaymentEto LastEto { get; set; }
public Task HandleEventAsync(RefundPaymentEto eventData)
{
IsEventPublished = true;
EventData = eventData;
LastEto = eventData;
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")]
public InventoryStrategy InventoryStrategy { get; set; }
[DisplayName("ProductInventoryProviderName")]
public string InventoryProviderName { get; set; }
[DisplayName("ProductDisplayOrder")]
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 string InventoryProviderName { get; set; }
public string MediaResources { 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 string InventoryProviderName { get; set; }
public string MediaResources { 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
{
public interface IProductAppService :
ICrudAppService<
ProductDto,
Guid,
ICrudAppService<
ProductDto,
Guid,
GetProductListInput,
CreateUpdateProductDto,
CreateUpdateProductDto>
@ -19,9 +19,12 @@ namespace EasyAbp.EShop.Products.Products
Task<ProductDto> UpdateSkuAsync(Guid productId, Guid productSkuId, UpdateProductSkuDto input);
Task<ProductDto> DeleteSkuAsync(Guid productId, Guid productSkuId);
Task<ProductDto> GetByUniqueNameAsync(Guid storeId, string uniqueName);
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.Threading.Tasks;
using EasyAbp.EShop.Products.Options;
using EasyAbp.EShop.Products.ProductInventories;
using EasyAbp.EShop.Products.Products.CacheItems;
using EasyAbp.EShop.Stores.Stores;
using Microsoft.Extensions.Options;
@ -14,7 +15,9 @@ using Volo.Abp.Domain.Entities;
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 DeletePolicyName { get; set; } = ProductsPermissions.Products.Delete;
@ -26,7 +29,7 @@ namespace EasyAbp.EShop.Products.Products
private readonly IProductManager _productManager;
private readonly IDistributedCache<ProductViewCacheItem> _cache;
private readonly EShopProductsOptions _options;
private readonly IProductInventoryProvider _productInventoryProvider;
private readonly IProductInventoryProviderResolver _productInventoryProviderResolver;
private readonly IProductViewCacheKeyProvider _productViewCacheKeyProvider;
private readonly IAttributeOptionIdsSerializer _attributeOptionIdsSerializer;
private readonly IProductRepository _repository;
@ -35,7 +38,7 @@ namespace EasyAbp.EShop.Products.Products
IProductManager productManager,
IOptions<EShopProductsOptions> options,
IDistributedCache<ProductViewCacheItem> cache,
IProductInventoryProvider productInventoryProvider,
IProductInventoryProviderResolver productInventoryProviderResolver,
IProductViewCacheKeyProvider productViewCacheKeyProvider,
IAttributeOptionIdsSerializer attributeOptionIdsSerializer,
IProductRepository repository) : base(repository)
@ -43,7 +46,7 @@ namespace EasyAbp.EShop.Products.Products
_productManager = productManager;
_cache = cache;
_options = options.Value;
_productInventoryProvider = productInventoryProvider;
_productInventoryProviderResolver = productInventoryProviderResolver;
_productViewCacheKeyProvider = productViewCacheKeyProvider;
_attributeOptionIdsSerializer = attributeOptionIdsSerializer;
_repository = repository;
@ -74,14 +77,11 @@ namespace EasyAbp.EShop.Products.Products
await _productManager.CreateAsync(product, input.CategoryIds);
var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto);
await LoadDtosProductGroupDisplayNameAsync(new[] {dto});
UnitOfWorkManager.Current.OnCompleted(async () =>
{
await ClearProductViewCacheAsync(product.StoreId);
});
await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
return dto;
}
@ -94,14 +94,14 @@ namespace EasyAbp.EShop.Products.Products
public override async Task<ProductDto> UpdateAsync(Guid id, CreateUpdateProductDto input)
{
var product = await GetEntityByIdAsync(id);
await CheckMultiStorePolicyAsync(product.StoreId, UpdatePolicyName);
if (input.StoreId != product.StoreId)
{
await CheckMultiStorePolicyAsync(input.StoreId, UpdatePolicyName);
}
CheckProductIsNotStatic(product);
await MapToEntityAsync(input, product);
@ -111,15 +111,12 @@ namespace EasyAbp.EShop.Products.Products
await _productManager.UpdateAsync(product, input.CategoryIds);
var dto = await MapToGetOutputDtoAsync(product);
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;
}
@ -130,10 +127,10 @@ namespace EasyAbp.EShop.Products.Products
var usedAttributeOptionIds = new HashSet<Guid>();
foreach (var serializedAttributeOptionIds in product.ProductSkus.Select(sku =>
sku.SerializedAttributeOptionIds))
sku.SerializedAttributeOptionIds))
{
foreach (var attributeOptionId in await _attributeOptionIdsSerializer.DeserializeAsync(
serializedAttributeOptionIds))
serializedAttributeOptionIds))
{
usedAttributeOptionIds.Add(attributeOptionId);
}
@ -175,9 +172,9 @@ namespace EasyAbp.EShop.Products.Products
.Except(attributeDto.ProductAttributeOptions.Select(o => o.DisplayName)).ToList();
if (!isProductSkusEmpty && removedOptionNames.Any() && usedAttributeOptionIds
.Intersect(attribute.ProductAttributeOptions
.Where(option => removedOptionNames.Contains(option.DisplayName))
.Select(option => option.Id)).Any())
.Intersect(attribute.ProductAttributeOptions
.Where(option => removedOptionNames.Contains(option.DisplayName))
.Select(option => option.Id)).Any())
{
throw new ProductAttributeOptionsDeletionFailedException();
}
@ -210,7 +207,7 @@ namespace EasyAbp.EShop.Products.Products
var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto);
await LoadDtosProductGroupDisplayNameAsync(new[] {dto});
await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
return dto;
}
@ -241,7 +238,7 @@ namespace EasyAbp.EShop.Products.Products
var dto = await MapToGetOutputDtoAsync(product);
await LoadDtoExtraDataAsync(product, dto);
await LoadDtosProductGroupDisplayNameAsync(new[] {dto});
await LoadDtosProductGroupDisplayNameAsync(new[] { dto });
return dto;
}
@ -283,7 +280,12 @@ namespace EasyAbp.EShop.Products.Products
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;
@ -306,7 +308,7 @@ namespace EasyAbp.EShop.Products.Products
return productDto;
}
protected virtual async Task<ProductDto> LoadDtoPriceDataAsync(Product product, ProductDto productDto)
{
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 priceDataModel = await _productManager.GetRealPriceAsync(product, productSku);
productSkuDto.Price = priceDataModel.Price;
productSkuDto.DiscountedPrice = priceDataModel.DiscountedPrice;
}
@ -331,17 +333,14 @@ namespace EasyAbp.EShop.Products.Products
public override async Task DeleteAsync(Guid id)
{
var product = await GetEntityByIdAsync(id);
await CheckMultiStorePolicyAsync(product.StoreId, DeletePolicyName);
CheckProductIsNotStatic(product);
await _productManager.DeleteAsync(product);
UnitOfWorkManager.Current.OnCompleted(async () =>
{
await ClearProductViewCacheAsync(product.StoreId);
});
UnitOfWorkManager.Current.OnCompleted(async () => { await ClearProductViewCacheAsync(product.StoreId); });
}
private static void CheckProductIsNotStatic(Product product)
@ -355,9 +354,9 @@ namespace EasyAbp.EShop.Products.Products
public async Task<ProductDto> CreateSkuAsync(Guid productId, CreateProductSkuDto input)
{
var product = await GetEntityByIdAsync(productId);
await CheckMultiStorePolicyAsync(product.StoreId, UpdatePolicyName);
CheckProductIsNotStatic(product);
var sku = ObjectMapper.Map<CreateProductSkuDto, ProductSku>(input);
@ -367,15 +366,12 @@ namespace EasyAbp.EShop.Products.Products
await _productManager.CreateSkuAsync(product, sku);
var dto = await MapToGetOutputDtoAsync(product);
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;
}
@ -394,15 +390,12 @@ namespace EasyAbp.EShop.Products.Products
await _productManager.UpdateSkuAsync(product, sku);
var dto = await MapToGetOutputDtoAsync(product);
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;
}
@ -419,15 +412,12 @@ namespace EasyAbp.EShop.Products.Products
await _productManager.DeleteSkuAsync(product, sku);
var dto = await MapToGetOutputDtoAsync(product);
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;
}
@ -445,6 +435,29 @@ namespace EasyAbp.EShop.Products.Products
).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)
{
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.ProductHistories;
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.Dtos;
using System.Linq;
@ -65,7 +63,6 @@ namespace EasyAbp.EShop.Products
CreateMap<ProductCategory, ProductCategoryDto>();
CreateMap<ProductHistory, ProductHistoryDto>();
CreateMap<ProductDetailHistory, ProductDetailHistoryDto>();
CreateMap<ProductInventory, ProductInventoryDto>();
CreateMap<ProductView, ProductViewDto>();
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",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -85,6 +87,7 @@
"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: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:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate 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",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -86,6 +88,7 @@
"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: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:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate 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",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -85,6 +87,7 @@
"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: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:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate 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",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -85,6 +87,7 @@
"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: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:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate 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",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -86,6 +88,7 @@
"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: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:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate 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",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -86,6 +88,7 @@
"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: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:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate 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",
"ProductSkuDeletionConfirmationMessage": "Are you sure to delete the product SKU {0}?",
"ProductInventoryStrategy": "Inventory strategy",
"ProductInventoryProviderName": "Inventory provider",
"ProductInventoryProviderNamePlaceholder": "Keep it empty if you don't understand",
"InventoryStrategy.NoNeed": "No need",
"InventoryStrategy.ReduceAfterPlacing": "Reduce inventory after placing",
"InventoryStrategy.ReduceAfterPayment": "Reduce inventory after payment",
@ -85,6 +87,7 @@
"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: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:ProductSkuDuplicated": "Sku {serializedAttributeOptionIds} is duplicate 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": "编辑",
"ProductSkuDeletionConfirmationMessage": "确认删除 SKU {0}?",
"ProductInventoryStrategy": "库存扣减策略",
"ProductInventoryProviderName": "库存提供者",
"ProductInventoryProviderNamePlaceholder": "如果你不理解它的意义,请留空",
"InventoryStrategy.NoNeed": "无需库存",
"InventoryStrategy.ReduceAfterPlacing": "下单后减库存",
"InventoryStrategy.ReduceAfterPayment": "支付后减库存",
@ -83,6 +85,7 @@
"EasyAbp.EShop.Products:DuplicatedProductUniqueName": "唯一的产品名称'{uniqueName}'重复",
"EasyAbp.EShop.Products:InventoryChangeFailed": "产品{productId} (SKU: {productSkuId})的库存不能由{originalInventory}中的{changedInventory}更改",
"EasyAbp.EShop.Products:NonexistentProductGroup": "指定的产品组({productGroupName})不存在",
"EasyAbp.EShop.Products:NonexistentInventoryProvider": "指定的库存提供者({inventoryProviderName})不存在",
"EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "商品{productId}的Sku代码{code}重复",
"EasyAbp.EShop.Products:ProductSkuDuplicated": "商品{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": "編輯",
"ProductSkuDeletionConfirmationMessage": "確認刪除 SKU {0}?",
"ProductInventoryStrategy": "庫存扣減策略",
"InventoryStrategy.NoNeed": "No need",
"ProductInventoryProviderName": "庫存提供者",
"ProductInventoryProviderNamePlaceholder": "如果你不理解它的意義,請留空",
"InventoryStrategy.NoNeed": "無需庫存",
"InventoryStrategy.ReduceAfterPlacing": "下單後減庫存",
"InventoryStrategy.ReduceAfterPayment": "支付後減庫存",
"ProductIsPublished": "是否發布",
@ -83,6 +85,7 @@
"EasyAbp.EShop.Products:DuplicatedProductUniqueName": "唯一的產品名稱'{uniqueName}'重復",
"EasyAbp.EShop.Products:InventoryChangeFailed": "產品{productId} (SKU: {productSkuId})的庫存不能由{originalInventory}中的{changedInventory}更改",
"EasyAbp.EShop.Products:NonexistentProductGroup": "指定的產品組({productGroupName})不存在",
"EasyAbp.EShop.Products:NonexistentInventoryProvider": "指定的庫存提供者({inventoryProviderName})不存在",
"EasyAbp.EShop.Products:ProductSkuCodeDuplicated": "商品{productId}的Sku代碼{code}重復",
"EasyAbp.EShop.Products:ProductSkuDuplicated": "商品{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
{

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 EasyAbp.EShop.Stores.Stores;
using JetBrains.Annotations;
using Volo.Abp.Data;
namespace EasyAbp.EShop.Products.Products
@ -7,23 +8,25 @@ namespace EasyAbp.EShop.Products.Products
public interface IProduct : IHasExtraProperties, IMultiStore
{
string ProductGroupName { get; }
Guid? ProductDetailId { get; }
string UniqueName { get; }
string DisplayName { get; }
InventoryStrategy InventoryStrategy { get; }
[CanBeNull] string InventoryProviderName { get; }
string MediaResources { get; }
int DisplayOrder { get; }
bool IsPublished { get; }
bool IsStatic { 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.Collections.Generic;
using Volo.Abp.Data;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectExtending;
@ -11,11 +10,11 @@ namespace EasyAbp.EShop.Products.Products
public Guid? TenantId { get; set; }
public Guid Id { get; set; }
public Guid StoreId { get; set; }
public string ProductGroupName { get; set; }
public Guid? ProductDetailId { get; set; }
public string UniqueName { get; set; }
@ -24,6 +23,8 @@ namespace EasyAbp.EShop.Products.Products
public InventoryStrategy InventoryStrategy { get; set; }
public string InventoryProviderName { get; set; }
public string MediaResources { get; set; }
public int DisplayOrder { get; set; }
@ -33,9 +34,9 @@ namespace EasyAbp.EShop.Products.Products
public bool IsStatic { get; set; }
public bool IsHidden { get; set; }
public List<ProductAttributeEto> ProductAttributes { 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 InventoryChangeFailed = "EasyAbp.EShop.Products:InventoryChangeFailed";
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 ProductSkuDuplicated = "EasyAbp.EShop.Products:ProductSkuDuplicated";
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 =>
{
options.EtoMappings.Add<Product, ProductEto>();
options.AutoEventSelectors.Add<Product>();
});
Configure<EShopProductsOptions>(options =>
{
options.Groups.Configure<DefaultProductGroup>(group =>
@ -35,6 +35,16 @@ namespace EasyAbp.EShop.Products
group.DisplayName = ProductsConsts.DefaultProductGroupDisplayName;
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.Products;
using JetBrains.Annotations;
namespace EasyAbp.EShop.Products.Options
{
public class EShopProductsOptions
{
public ProductGroupConfigurations Groups { get; }
public ProductGroupConfigurations Groups { get; } = new();
public Type DefaultFileDownloadProviderType { get; set; }
public EShopProductsOptions()
{
Groups = new ProductGroupConfigurations();
}
public InventoryProviderConfigurations InventoryProviders { get; } = new();
/// <summary>
/// If the value is <c>null</c>, it will fall back to DefaultProductInventoryProviderName
/// in the <see cref="DefaultProductInventoryProvider"/>.
/// </summary>
[CanBeNull]
public string DefaultInventoryProviderName { get; set; }
}
}

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 string DisplayName { get; set; }
public string Description { get; set; }
/// <summary>
/// If the value is <c>null</c>, it will fall back to DefaultInventoryProviderName
/// in the <see cref="EShopProductsOptions"/>.
/// </summary>
[CanBeNull]
public string DefaultInventoryProviderName { get; set; }
}
}

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<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 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 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.
private readonly IGuidGenerator _guidGenerator;
private readonly ICurrentTenant _currentTenant;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IDistributedEventBus _distributedEventBus;
private readonly IProductInventoryRepository _productInventoryRepository;
public DefaultProductInventoryProvider(
IGuidGenerator guidGenerator,
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
IDistributedEventBus distributedEventBus,
IProductInventoryRepository productInventoryRepository)
{
_guidGenerator = guidGenerator;
_currentTenant = currentTenant;
_unitOfWorkManager = unitOfWorkManager;
_distributedEventBus = distributedEventBus;
_productInventoryRepository = productInventoryRepository;
}
[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]
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
.Select(sku => sku.Id).ToList());
var dict = await _productInventoryRepository.GetSkuIdInventoryDataMappingAsync(
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;
}
[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);
return await TryIncreaseInventoryAsync(product, productInventory, quantity, decreaseSold);
var productInventory = await GetOrCreateProductInventoryAsync(model.ProductId, model.ProductSkuId);
return await TryIncreaseInventoryAsync(model, productInventory, quantity, decreaseSold);
}
[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);
return await TryReduceInventoryAsync(product, productInventory, quantity, increaseSold);
var productInventory = await GetOrCreateProductInventoryAsync(model.ProductId, model.ProductSkuId);
return await TryReduceInventoryAsync(model, productInventory, quantity, increaseSold);
}
[UnitOfWork]
protected virtual async Task<ProductInventory> GetOrCreateProductInventoryAsync(Guid productId, Guid productSkuId)
protected virtual async Task<ProductInventory> GetOrCreateProductInventoryAsync(Guid productId,
Guid productSkuId)
{
var productInventory =
await _productInventoryRepository.FindAsync(x =>
x.ProductId == productId && x.ProductSkuId == productSkuId);
if (productInventory is null)
{
productInventory = new ProductInventory(_guidGenerator.Create(), _currentTenant.Id, productId,
@ -87,15 +94,16 @@ namespace EasyAbp.EShop.Products.Products
return productInventory;
}
[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)
{
return false;
}
var originalInventory = productInventory.Inventory;
if (!productInventory.TryIncreaseInventory(quantity, decreaseSold))
@ -105,15 +113,16 @@ namespace EasyAbp.EShop.Products.Products
await _productInventoryRepository.UpdateAsync(productInventory, true);
await PublishInventoryChangedEventAsync(product.TenantId, product.StoreId,
await PublishInventoryChangedEventAsync(model.TenantId, model.StoreId,
productInventory.ProductId, productInventory.ProductSkuId, originalInventory,
productInventory.Inventory, productInventory.Sold);
return 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)
{
@ -129,7 +138,7 @@ namespace EasyAbp.EShop.Products.Products
await _productInventoryRepository.UpdateAsync(productInventory, true);
await PublishInventoryChangedEventAsync(product.TenantId, product.StoreId,
await PublishInventoryChangedEventAsync(model.TenantId, model.StoreId,
productInventory.ProductId, productInventory.ProductSkuId, originalInventory,
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.Collections.Generic;
using System.Threading.Tasks;
using EasyAbp.EShop.Products.ProductInventories;
using Volo.Abp.Domain.Services;
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.Threading.Tasks;
using EasyAbp.EShop.Orders.Orders;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.EventBus.Distributed;
@ -15,6 +16,7 @@ namespace EasyAbp.EShop.Products.Products
private readonly ICurrentTenant _currentTenant;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IDistributedEventBus _distributedEventBus;
private readonly ILogger<OrderCreatedEventHandler> _logger;
private readonly IProductRepository _productRepository;
private readonly IProductManager _productManager;
@ -22,12 +24,14 @@ namespace EasyAbp.EShop.Products.Products
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
IDistributedEventBus distributedEventBus,
ILogger<OrderCreatedEventHandler> logger,
IProductRepository productRepository,
IProductManager productManager)
{
_currentTenant = currentTenant;
_unitOfWorkManager = unitOfWorkManager;
_distributedEventBus = distributedEventBus;
_logger = logger;
_productRepository = productRepository;
_productManager = productManager;
}
@ -65,33 +69,52 @@ namespace EasyAbp.EShop.Products.Products
return;
}
models.Add(new ConsumeInventoryModel
{
Product = product,
ProductSku = productSku,
StoreId = eventData.Entity.StoreId,
Quantity = orderLine.Quantity
});
models.Add(new ConsumeInventoryModel(
product, productSku, eventData.Entity.StoreId, orderLine.Quantity));
}
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;
}
// Todo: should release unused inventory since (external) inventory providers may not be transactional.
await TryRollbackInventoriesAsync(models);
await _unitOfWorkManager.Current.RollbackAsync();
await PublishInventoryReductionResultEventAsync(eventData, false, true);
return;
}
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)
{
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.Threading.Tasks;
using EasyAbp.EShop.Orders.Orders;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.MultiTenancy;
@ -13,6 +14,7 @@ namespace EasyAbp.EShop.Products.Products
{
private readonly ICurrentTenant _currentTenant;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly ILogger<OrderPaidEventHandler> _logger;
private readonly IDistributedEventBus _distributedEventBus;
private readonly IProductRepository _productRepository;
private readonly IProductManager _productManager;
@ -20,24 +22,26 @@ namespace EasyAbp.EShop.Products.Products
public OrderPaidEventHandler(
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
ILogger<OrderPaidEventHandler> logger,
IDistributedEventBus distributedEventBus,
IProductRepository productRepository,
IProductManager productManager)
{
_currentTenant = currentTenant;
_unitOfWorkManager = unitOfWorkManager;
_logger = logger;
_distributedEventBus = distributedEventBus;
_productRepository = productRepository;
_productManager = productManager;
}
[UnitOfWork(true)]
public virtual async Task HandleEventAsync(OrderPaidEto eventData)
{
using var changeTenant = _currentTenant.Change(eventData.Order.TenantId);
var models = new List<ConsumeInventoryModel>();
foreach (var orderLine in eventData.Order.OrderLines)
{
// Todo: Should use ProductHistory.
@ -48,10 +52,10 @@ namespace EasyAbp.EShop.Products.Products
if (productSku == null)
{
await PublishInventoryReductionResultEventAsync(eventData, false);
return;
}
if (product.InventoryStrategy != InventoryStrategy.ReduceAfterPayment)
{
continue;
@ -60,37 +64,56 @@ namespace EasyAbp.EShop.Products.Products
if (!await _productManager.IsInventorySufficientAsync(product, productSku, orderLine.Quantity))
{
await PublishInventoryReductionResultEventAsync(eventData, false);
return;
}
models.Add(new ConsumeInventoryModel
{
Product = product,
ProductSku = productSku,
StoreId = eventData.Order.StoreId,
Quantity = orderLine.Quantity
});
models.Add(new ConsumeInventoryModel(product, productSku, eventData.Order.StoreId, orderLine.Quantity));
}
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;
}
await TryRollbackInventoriesAsync(models);
await _unitOfWorkManager.Current.RollbackAsync();
await PublishInventoryReductionResultEventAsync(eventData, false, true);
return;
}
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
{

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

@ -1,6 +1,7 @@
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using EasyAbp.EShop.Products.Options.ProductGroups;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
@ -9,37 +10,39 @@ namespace EasyAbp.EShop.Products.Products
public class Product : FullAuditedAggregateRoot<Guid>, IProduct, IMultiTenant
{
public virtual Guid? TenantId { get; protected set; }
public virtual Guid StoreId { get; protected set; }
[NotNull]
public virtual string ProductGroupName { get; protected set; }
[NotNull] public virtual string ProductGroupName { get; protected set; }
public virtual Guid? ProductDetailId { get; protected set; }
[CanBeNull]
public virtual string UniqueName { get; protected set; }
[CanBeNull] 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; }
[CanBeNull]
public virtual string MediaResources { get; protected set; }
/// <summary>
/// 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 bool IsPublished { get; protected set; }
public virtual bool IsStatic { get; protected set; }
public virtual bool IsHidden { get; protected set; }
public virtual TimeSpan? PaymentExpireIn { get; protected set; }
public virtual List<ProductAttribute> ProductAttributes { get; protected set; }
public virtual List<ProductSku> ProductSkus { get; protected set; }
protected Product()
@ -55,6 +58,7 @@ namespace EasyAbp.EShop.Products.Products
[CanBeNull] string uniqueName,
[NotNull] string displayName,
InventoryStrategy inventoryStrategy,
[CanBeNull] string inventoryProviderName,
bool isPublished,
bool isStatic,
bool isHidden,
@ -70,13 +74,14 @@ namespace EasyAbp.EShop.Products.Products
UniqueName = uniqueName?.Trim();
DisplayName = displayName;
InventoryStrategy = inventoryStrategy;
InventoryProviderName = inventoryProviderName;
IsPublished = isPublished;
IsStatic = isStatic;
IsHidden = isHidden;
PaymentExpireIn = paymentExpireIn;
MediaResources = mediaResources;
DisplayOrder = displayOrder;
ProductAttributes = new List<ProductAttribute>();
ProductSkus = new List<ProductSku>();
}
@ -92,4 +97,4 @@ namespace EasyAbp.EShop.Products.Products
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.ProductCategories;
using EasyAbp.EShop.Products.ProductDetails;
using Microsoft.Extensions.DependencyInjection;
using EasyAbp.EShop.Products.ProductInventories;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Services;
using Volo.Abp.Uow;
@ -18,7 +18,7 @@ namespace EasyAbp.EShop.Products.Products
private readonly IProductPriceProvider _productPriceProvider;
private readonly IProductDetailRepository _productDetailRepository;
private readonly IProductCategoryRepository _productCategoryRepository;
private readonly IProductInventoryProvider _productInventoryProvider;
private readonly IProductInventoryProviderResolver _productInventoryProviderResolver;
private readonly IAttributeOptionIdsSerializer _attributeOptionIdsSerializer;
private readonly IProductGroupConfigurationProvider _productGroupConfigurationProvider;
@ -27,7 +27,7 @@ namespace EasyAbp.EShop.Products.Products
IProductPriceProvider productPriceProvider,
IProductDetailRepository productDetailRepository,
IProductCategoryRepository productCategoryRepository,
IProductInventoryProvider productInventoryProvider,
IProductInventoryProviderResolver productInventoryProviderResolver,
IAttributeOptionIdsSerializer attributeOptionIdsSerializer,
IProductGroupConfigurationProvider productGroupConfigurationProvider)
{
@ -35,7 +35,7 @@ namespace EasyAbp.EShop.Products.Products
_productPriceProvider = productPriceProvider;
_productDetailRepository = productDetailRepository;
_productCategoryRepository = productCategoryRepository;
_productInventoryProvider = productInventoryProvider;
_productInventoryProviderResolver = productInventoryProviderResolver;
_attributeOptionIdsSerializer = attributeOptionIdsSerializer;
_productGroupConfigurationProvider = productGroupConfigurationProvider;
}
@ -44,9 +44,11 @@ namespace EasyAbp.EShop.Products.Products
public virtual async Task<Product> CreateAsync(Product product, IEnumerable<Guid> categoryIds = null)
{
product.TrimUniqueName();
await CheckProductGroupNameAsync(product);
await CheckInventoryProviderNameAsync(product);
await CheckProductUniqueNameAsync(product);
await _productRepository.InsertAsync(product, autoSave: true);
@ -64,15 +66,30 @@ namespace EasyAbp.EShop.Products.Products
{
throw new NonexistentProductGroupException(product.ProductGroupName);
}
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)]
public virtual async Task<Product> UpdateAsync(Product product, IEnumerable<Guid> categoryIds = null)
{
await CheckProductGroupNameAsync(product);
await CheckInventoryProviderNameAsync(product);
await CheckProductUniqueNameAsync(product);
await _productRepository.UpdateAsync(product, autoSave: true);
@ -91,7 +108,7 @@ namespace EasyAbp.EShop.Products.Products
await _productRepository.DeleteAsync(product, true);
}
[UnitOfWork(true)]
public virtual async Task DeleteAsync(Guid id)
{
@ -108,13 +125,13 @@ namespace EasyAbp.EShop.Products.Products
await CheckSkuAttributeOptionsAsync(product, productSku);
await CheckProductSkuNameUniqueAsync(product, productSku);
productSku.TrimName();
product.ProductSkus.AddIfNotContains(productSku);
await CheckProductDetailAsync(product);
return await _productRepository.UpdateAsync(product, true);
}
@ -124,9 +141,9 @@ namespace EasyAbp.EShop.Products.Products
{
return Task.CompletedTask;
}
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);
}
@ -169,7 +186,7 @@ namespace EasyAbp.EShop.Products.Products
public virtual async Task<Product> DeleteSkuAsync(Product product, ProductSku productSku)
{
product.ProductSkus.Remove(productSku);
return await _productRepository.UpdateAsync(product, true);
}
@ -178,20 +195,20 @@ namespace EasyAbp.EShop.Products.Products
{
await _productRepository.CheckUniqueNameAsync(product);
}
protected virtual async Task CheckProductDetailAsync(Product product)
{
if (product.ProductDetailId.HasValue)
{
await CheckProductDetailExistAsync(product.ProductDetailId.Value, product.StoreId);
}
foreach (var sku in product.ProductSkus.Where(x => x.ProductDetailId.HasValue))
{
await CheckProductDetailExistAsync(sku.ProductDetailId!.Value, product.StoreId);
}
}
[UnitOfWork]
protected virtual async Task CheckProductDetailExistAsync(Guid productDetailId, Guid storeId)
{
@ -202,7 +219,7 @@ namespace EasyAbp.EShop.Products.Products
throw new EntityNotFoundException(typeof(ProductDetail), productDetailId);
}
}
[UnitOfWork(true)]
protected virtual async Task UpdateProductCategoriesAsync(Guid productId, IEnumerable<Guid> categoryIds)
{
@ -212,7 +229,7 @@ namespace EasyAbp.EShop.Products.Products
{
return;
}
foreach (var categoryId in categoryIds)
{
await _productCategoryRepository.InsertAsync(
@ -222,24 +239,37 @@ namespace EasyAbp.EShop.Products.Products
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;
}
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)
@ -247,7 +277,7 @@ namespace EasyAbp.EShop.Products.Products
var price = await _productPriceProvider.GetPriceAsync(product, productSku);
var discountedPrice = price;
// Todo: provider execution ordering.
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 string InventoryProviderName { get; protected set; }
public virtual string MediaResources { 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; }
#endregion
public virtual string ProductGroupDisplayName { get; protected set; }
public virtual decimal? MinimumPrice { get; protected set; }
public virtual decimal? MaximumPrice { get; protected set; }
public virtual long Sold { get; protected set; }
protected ProductView()
{
}
public ProductView(
Guid id,
Guid? tenantId,
@ -55,6 +57,7 @@ namespace EasyAbp.EShop.Products.Products
string uniqueName,
string displayName,
InventoryStrategy inventoryStrategy,
string inventoryProviderName,
bool isPublished,
bool isStatic,
bool isHidden,
@ -73,27 +76,28 @@ namespace EasyAbp.EShop.Products.Products
UniqueName = uniqueName?.Trim();
DisplayName = displayName;
InventoryStrategy = inventoryStrategy;
InventoryProviderName = inventoryProviderName;
IsPublished = isPublished;
IsStatic = isStatic;
IsHidden = isHidden;
MediaResources = mediaResources;
DisplayOrder = displayOrder;
ProductGroupDisplayName = productGroupDisplayName;
MinimumPrice = minimumPrice;
MaximumPrice = maximumPrice;
Sold = sold;
}
public void SetSold(long sold)
{
Sold = sold;
}
public void SetPrices(decimal? minimumPrice, decimal? maximumPrice)
{
MinimumPrice = minimumPrice;
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.Tasks;
using EasyAbp.EShop.Products.EntityFrameworkCore;
using EasyAbp.EShop.Products.Products;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
@ -29,9 +28,9 @@ namespace EasyAbp.EShop.Products.ProductInventories
.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))
.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();
}
[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")]
public InventoryStrategy InventoryStrategy { get; set; }
[Placeholder("ProductInventoryProviderNamePlaceholder")]
[Display(Name = "ProductInventoryProviderName")]
public string InventoryProviderName { get; set; }
[Display(Name = "ProductDisplayOrder")]
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.Threading.Tasks;
using EasyAbp.EShop.Products.ProductInventories;
using EasyAbp.EShop.Products.ProductInventories.Dtos;
using EasyAbp.EShop.Products.Products;
using EasyAbp.EShop.Products.Products.Dtos;
using EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku.ViewModels;
using Microsoft.AspNetCore.Mvc;
@ -12,7 +12,7 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid ProductId { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid ProductSkuId { get; set; }
@ -20,17 +20,18 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku
[BindProperty]
public ChangeProductInventoryViewModel ViewModel { get; set; }
private readonly IProductInventoryAppService _service;
private readonly IProductAppService _service;
public ChangeInventoryModal(IProductInventoryAppService service)
public ChangeInventoryModal(IProductAppService service)
{
_service = service;
}
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
{
ChangedInventory = 0,
@ -40,15 +41,13 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku
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
? ViewModel.ChangedInventory
: -ViewModel.ChangedInventory
});
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 EasyAbp.EShop.Products.ProductDetails;
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.Dtos;
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]
[BindProperty(SupportsGet = true)]
public Guid ProductId { get; set; }
[BindProperty]
public CreateProductSkuViewModel ProductSku { get; set; } = new CreateProductSkuViewModel();
[BindProperty]
public Dictionary<string, Guid> SelectedAttributeOptionIdDict { get; set; }
public Dictionary<string, List<SelectListItem>> Attributes { get; set; }
private readonly IProductInventoryAppService _productInventoryAppService;
private readonly IProductDetailAppService _productDetailAppService;
private readonly IProductAppService _productAppService;
public CreateModalModel(
IProductInventoryAppService productInventoryAppService,
IProductDetailAppService productDetailAppService,
IProductAppService productAppService)
{
_productInventoryAppService = productInventoryAppService;
_productDetailAppService = productDetailAppService;
_productAppService = productAppService;
}
@ -47,7 +42,7 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku
var product = await _productAppService.GetAsync(ProductId);
Attributes = new Dictionary<string, List<SelectListItem>>();
foreach (var attribute in product.ProductAttributes.ToList())
{
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());
}
}
public virtual async Task<IActionResult> OnPostAsync()
{
var createDto = ObjectMapper.Map<CreateProductSkuViewModel, CreateProductSkuDto>(ProductSku);
createDto.AttributeOptionIds = SelectedAttributeOptionIdDict.Values.ToList();
if (ProductSku.ProductDetail.HasContent())
{
var detail = await _productDetailAppService.CreateAsync(
@ -70,15 +64,13 @@ namespace EasyAbp.EShop.Products.Web.Pages.EShop.Products.Products.ProductSku
createDto.ProductDetailId = detail.Id;
}
var product = await _productAppService.CreateSkuAsync(ProductId, createDto);
var productSku = product.ProductSkus
.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
});

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()
{
var product2 = new Product(ProductsTestData.Product2Id, null, ProductsTestData.Store1Id, "Default",
ProductsTestData.ProductDetails2Id, "Ball", "Ball", InventoryStrategy.NoNeed, true, false, false, null,
null, 0);
ProductsTestData.ProductDetails2Id, "Ball", "Ball", InventoryStrategy.NoNeed, null, true, false, false,
null, null, 0);
await ProductManager.CreateAsync(product2);
product2 = await ProductRepository.GetAsync(product2.Id);
product2.ProductDetailId.ShouldBe(ProductsTestData.ProductDetails2Id);
}
@ -42,9 +41,9 @@ namespace EasyAbp.EShop.Products.Products
{
var product1 = await ProductRepository.GetAsync(ProductsTestData.Product1Id);
var sku1 = product1.ProductSkus.Single(x => x.Id == ProductsTestData.Product1Sku1Id);
sku1.ProductDetailId.ShouldBeNull();
typeof(ProductSku).GetProperty(nameof(ProductSku.ProductDetailId))!.SetValue(sku1,
ProductsTestData.ProductDetails1Id);
@ -65,20 +64,20 @@ namespace EasyAbp.EShop.Products.Products
product1.ProductDetailId.ShouldBe(ProductsTestData.ProductDetails1Id);
var product2 = new Product(ProductsTestData.Product2Id, null, ProductsTestData.Store1Id, "Default",
ProductsTestData.ProductDetails2Id, "Ball", "Ball", InventoryStrategy.NoNeed, true, false, false, null,
null, 0);
ProductsTestData.ProductDetails2Id, "Ball", "Ball", InventoryStrategy.NoNeed, null, true, false, false,
null, null, 0);
await ProductManager.CreateAsync(product2);
product2 = await ProductRepository.GetAsync(product2.Id);
product2.ProductDetailId.ShouldBe(ProductsTestData.ProductDetails2Id);
typeof(Product).GetProperty(nameof(Product.ProductDetailId))!.SetValue(product2,
ProductsTestData.ProductDetails1Id);
await ProductManager.UpdateAsync(product2);
product2 = await ProductRepository.GetAsync(product2.Id);
product2.ProductDetailId.ShouldBe(ProductsTestData.ProductDetails1Id);
@ -88,17 +87,17 @@ namespace EasyAbp.EShop.Products.Products
public async Task Should_Remove_ProductDetailId()
{
await Should_Set_ProductDetailId();
var product2 = await ProductRepository.GetAsync(ProductsTestData.Product2Id);
product2.ProductDetailId.ShouldNotBeNull();
typeof(Product).GetProperty(nameof(Product.ProductDetailId))!.SetValue(product2, null);
await ProductManager.UpdateAsync(product2);
product2 = await ProductRepository.GetAsync(product2.Id);
product2.ProductDetailId.ShouldBeNull();
}
@ -135,7 +134,7 @@ namespace EasyAbp.EShop.Products.Products
ProductsTestData.Product1Attribute1Option4Id,
ProductsTestData.Product1Attribute2Option2Id
};
await Should.NotThrowAsync(async () =>
{
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)
{
return new ProductSku(Guid.NewGuid(), await AttributeOptionIdsSerializer.SerializeAsync(attributeOptionIds),
"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.Authorization;
using Volo.Abp.Autofac;
@ -19,6 +22,17 @@ namespace EasyAbp.EShop.Products
public override void ConfigureServices(ServiceConfigurationContext context)
{
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)

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