Browse Source

Merge branch 'dev' into plugins-promotions

pull/240/head
gdlcf88 3 years ago
parent
commit
b6631b56b0
  1. 7
      EShop.sln
  2. 47
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountContext.cs
  3. 15
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountInfoModel.cs
  4. 30
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/CandidateOrderDiscounts.cs
  5. 33
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/HasDynamicDiscountAmountExtensions.cs
  6. 10
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderDiscountDistributor.cs
  7. 11
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderDiscountResolver.cs
  8. 18
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/NewOrderGenerator.cs
  9. 70
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/Order.cs
  10. 28
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountDistributionModel.cs
  11. 66
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountDistributor.cs
  12. 85
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountElectionModel.cs
  13. 98
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountResolver.cs
  14. 16
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountsSchemeModel.cs
  15. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/EShopOrdersWebModule.cs
  16. 2
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/EasyAbp.EShop.Orders.Web.csproj
  17. 4
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/Menus/OrdersMenuContributor.cs
  18. 20
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/DemoOrderDiscountProvider.cs
  19. 161
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDiscountDistributorTests.cs
  20. 74
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs
  21. 2
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/EShopPaymentsWebModule.cs
  22. 2
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/EasyAbp.EShop.Payments.Web.csproj
  23. 4
      modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/Menus/PaymentsMenuContributor.cs
  24. 10
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs
  25. 36
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductViewAppService.cs
  26. 22
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/CandidateProductDiscountInfoModel.cs
  27. 10
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DiscountForProductModels.cs
  28. 47
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DynamicDiscountAmountModel.cs
  29. 146
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/HasDiscountsForProductExtensions.cs
  30. 10
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IDiscountInfo.cs
  31. 6
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasDynamicDiscountAmount.cs
  32. 85
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs
  33. 14
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountInfoModel.cs
  34. 23
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductPriceModel.cs
  35. 21
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/RealTimePriceInfoModel.cs
  36. 30
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/CandidateProductDiscounts.cs
  37. 32
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/HasDynamicDiscountAmountExtensions.cs
  38. 10
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductDiscountResolver.cs
  39. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs
  40. 87
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountElectionModel.cs
  41. 98
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountResolver.cs
  42. 16
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountsSchemeModel.cs
  43. 18
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs
  44. 35
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/DemoProductDiscountProvider.cs
  45. 14
      modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp.EShop.Stores.Web.Shared.csproj
  46. 10
      modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp/EShop/Stores/EShopStoresWebSharedModule.cs
  47. 9
      modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp/EShop/Stores/Stores/IUiDefaultStoreProvider.cs
  48. 22
      modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp/EShop/Stores/Stores/UiDefaultStoreProvider.cs
  49. 3
      modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/FodyWeavers.xml
  50. 30
      modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/FodyWeavers.xsd
  51. 2
      modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/EShopStoresWebModule.cs
  52. 2
      modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/EasyAbp.EShop.Stores.Web.csproj
  53. 4
      modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/Menus/StoresMenuContributor.cs
  54. 5
      plugins/Coupons/src/EasyAbp.EShop.Orders.Plugins.Coupons/EasyAbp/EShop/Orders/Plugins/Coupons/OrderDiscount/CouponOrderDiscountProvider.cs

7
EShop.sln

@ -79,6 +79,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Stores.MongoD
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Stores.Web", "modules\EasyAbp.EShop.Stores\src\EasyAbp.EShop.Stores.Web\EasyAbp.EShop.Stores.Web.csproj", "{20E6571D-9BAF-4BE7-B7CC-7F5F32D682F8}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Stores.Web", "modules\EasyAbp.EShop.Stores\src\EasyAbp.EShop.Stores.Web\EasyAbp.EShop.Stores.Web.csproj", "{20E6571D-9BAF-4BE7-B7CC-7F5F32D682F8}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyAbp.EShop.Stores.Web.Shared", "modules\EasyAbp.Eshop.Stores\src\EasyAbp.EShop.Stores.Web.Shared\EasyAbp.EShop.Stores.Web.Shared.csproj", "{0102861C-9979-496A-8104-EB33F655983E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{83C515D3-9811-4428-9401-17B1AF5406EF}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{83C515D3-9811-4428-9401-17B1AF5406EF}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Application", "modules\EasyAbp.EShop.Plugins\src\EasyAbp.EShop.Plugins.Application\EasyAbp.EShop.Plugins.Application.csproj", "{F11833F9-83D7-44C6-BE12-2402DFF85E54}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Plugins.Application", "modules\EasyAbp.EShop.Plugins\src\EasyAbp.EShop.Plugins.Application\EasyAbp.EShop.Plugins.Application.csproj", "{F11833F9-83D7-44C6-BE12-2402DFF85E54}"
@ -645,6 +647,10 @@ Global
{20E6571D-9BAF-4BE7-B7CC-7F5F32D682F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {20E6571D-9BAF-4BE7-B7CC-7F5F32D682F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{20E6571D-9BAF-4BE7-B7CC-7F5F32D682F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {20E6571D-9BAF-4BE7-B7CC-7F5F32D682F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{20E6571D-9BAF-4BE7-B7CC-7F5F32D682F8}.Release|Any CPU.Build.0 = Release|Any CPU {20E6571D-9BAF-4BE7-B7CC-7F5F32D682F8}.Release|Any CPU.Build.0 = Release|Any CPU
{0102861C-9979-496A-8104-EB33F655983E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0102861C-9979-496A-8104-EB33F655983E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0102861C-9979-496A-8104-EB33F655983E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0102861C-9979-496A-8104-EB33F655983E}.Release|Any CPU.Build.0 = Release|Any CPU
{F11833F9-83D7-44C6-BE12-2402DFF85E54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F11833F9-83D7-44C6-BE12-2402DFF85E54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F11833F9-83D7-44C6-BE12-2402DFF85E54}.Debug|Any CPU.Build.0 = Debug|Any CPU {F11833F9-83D7-44C6-BE12-2402DFF85E54}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F11833F9-83D7-44C6-BE12-2402DFF85E54}.Release|Any CPU.ActiveCfg = Release|Any CPU {F11833F9-83D7-44C6-BE12-2402DFF85E54}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -1371,6 +1377,7 @@ Global
{15A67389-F23B-472C-8D44-1E294C18558B} = {8CB54A0E-BAAE-4D10-81B5-6F902A7950EB} {15A67389-F23B-472C-8D44-1E294C18558B} = {8CB54A0E-BAAE-4D10-81B5-6F902A7950EB}
{6897396E-A709-4934-95E2-50644DF3FD44} = {8CB54A0E-BAAE-4D10-81B5-6F902A7950EB} {6897396E-A709-4934-95E2-50644DF3FD44} = {8CB54A0E-BAAE-4D10-81B5-6F902A7950EB}
{20E6571D-9BAF-4BE7-B7CC-7F5F32D682F8} = {8CB54A0E-BAAE-4D10-81B5-6F902A7950EB} {20E6571D-9BAF-4BE7-B7CC-7F5F32D682F8} = {8CB54A0E-BAAE-4D10-81B5-6F902A7950EB}
{0102861C-9979-496A-8104-EB33F655983E} = {8CB54A0E-BAAE-4D10-81B5-6F902A7950EB}
{83C515D3-9811-4428-9401-17B1AF5406EF} = {7FA8569F-8D62-4C13-9C8A-98DADA1F09A0} {83C515D3-9811-4428-9401-17B1AF5406EF} = {7FA8569F-8D62-4C13-9C8A-98DADA1F09A0}
{F11833F9-83D7-44C6-BE12-2402DFF85E54} = {BE3E0AEA-37C4-4ED9-B569-444EDE907905} {F11833F9-83D7-44C6-BE12-2402DFF85E54} = {BE3E0AEA-37C4-4ED9-B569-444EDE907905}
{911CAA20-A5CA-4FFC-B7F2-F5B5B485EACA} = {BE3E0AEA-37C4-4ED9-B569-444EDE907905} {911CAA20-A5CA-4FFC-B7F2-F5B5B485EACA} = {BE3E0AEA-37C4-4ED9-B569-444EDE907905}

47
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountContext.cs

@ -1,56 +1,33 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using EasyAbp.EShop.Products.Products; using EasyAbp.EShop.Products.Products;
using JetBrains.Annotations;
using Volo.Abp;
namespace EasyAbp.EShop.Orders.Orders; namespace EasyAbp.EShop.Orders.Orders;
[Serializable]
public class OrderDiscountContext public class OrderDiscountContext
{ {
public DateTime Now { get; }
public IOrder Order { get; } public IOrder Order { get; }
public Dictionary<Guid, IProduct> ProductDict { get; } public Dictionary<Guid, IProduct> ProductDict { get; }
public List<OrderDiscountInfoModel> CandidateDiscounts { get; } public List<OrderDiscountInfoModel> CandidateDiscounts { get; }
public OrderDiscountContext(IOrder order, Dictionary<Guid, IProduct> productDict) public OrderDiscountContext(DateTime now, IOrder order, Dictionary<Guid, IProduct> productDict,
List<OrderDiscountInfoModel> candidateDiscounts = null)
{ {
Order = order; Now = now;
Order = Check.NotNull(order, nameof(order));
ProductDict = productDict ?? new Dictionary<Guid, IProduct>(); ProductDict = productDict ?? new Dictionary<Guid, IProduct>();
CandidateDiscounts = candidateDiscounts ?? new List<OrderDiscountInfoModel>();
} }
public List<OrderDiscountInfoModel> GetEffectDiscounts() public OrderDiscountInfoModel FindCandidateDiscount([NotNull] string name, [CanBeNull] string key)
{ {
var effectDiscounts = new List<OrderDiscountInfoModel>(); return CandidateDiscounts.Find(x => x.Name == name && x.Key == key);
foreach (var discount in CandidateDiscounts.Where(x => x.EffectGroup.IsNullOrEmpty()))
{
effectDiscounts.Add(discount);
}
// Make sure that each OrderLine can only be affected by one discount with the same EffectGroup.
var affectedOrderLineIdsInEffectGroup = new Dictionary<string, List<Guid>>();
foreach (var grouping in CandidateDiscounts.Where(x => !x.EffectGroup.IsNullOrEmpty())
.GroupBy(x => x.EffectGroup))
{
var effectGroup = grouping.Key;
affectedOrderLineIdsInEffectGroup[effectGroup] = new List<Guid>();
// todo: can be improved to find the best discount combo.
foreach (var discount in grouping)
{
if (discount.AffectedOrderLineIds.Any(x => affectedOrderLineIdsInEffectGroup[effectGroup].Contains(x)))
{
continue;
}
affectedOrderLineIdsInEffectGroup[effectGroup].AddRange(discount.AffectedOrderLineIds);
effectDiscounts.Add(discount);
}
}
return effectDiscounts;
} }
} }

15
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountInfoModel.cs

@ -2,10 +2,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using EasyAbp.EShop.Products.Products; using EasyAbp.EShop.Products.Products;
using JetBrains.Annotations; using JetBrains.Annotations;
using Volo.Abp;
namespace EasyAbp.EShop.Orders.Orders; namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscountInfoModel : IDiscountInfo public class OrderDiscountInfoModel : IDiscountInfo, IHasDynamicDiscountAmount
{ {
public string EffectGroup { get; set; } public string EffectGroup { get; set; }
@ -17,25 +18,21 @@ public class OrderDiscountInfoModel : IDiscountInfo
public List<Guid> AffectedOrderLineIds { get; set; } = new(); public List<Guid> AffectedOrderLineIds { get; set; } = new();
public decimal DiscountedAmount { get; set; } public DynamicDiscountAmountModel DynamicDiscountAmount { get; set; }
public OrderDiscountInfoModel() public OrderDiscountInfoModel()
{ {
} }
public OrderDiscountInfoModel(List<Guid> affectedOrderLineIds, [CanBeNull] string effectGroup, public OrderDiscountInfoModel(List<Guid> affectedOrderLineIds, [CanBeNull] string effectGroup,
[NotNull] string name, [CanBeNull] string key, [CanBeNull] string displayName, decimal discountedAmount) [NotNull] string name, [CanBeNull] string key, [CanBeNull] string displayName,
[NotNull] DynamicDiscountAmountModel dynamicDiscountAmount)
{ {
if (discountedAmount < decimal.Zero)
{
throw new DiscountAmountOverflowException();
}
AffectedOrderLineIds = affectedOrderLineIds ?? new List<Guid>(); AffectedOrderLineIds = affectedOrderLineIds ?? new List<Guid>();
EffectGroup = effectGroup; EffectGroup = effectGroup;
Name = name; Name = name;
Key = key; Key = key;
DisplayName = displayName; DisplayName = displayName;
DiscountedAmount = discountedAmount; DynamicDiscountAmount = Check.NotNull(dynamicDiscountAmount, nameof(dynamicDiscountAmount));
} }
} }

30
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/CandidateOrderDiscounts.cs

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace EasyAbp.EShop.Orders.Orders;
public class CandidateOrderDiscounts : ICloneable
{
public Dictionary<int, OrderDiscountInfoModel> Items { get; }
public CandidateOrderDiscounts(List<OrderDiscountInfoModel> candidates)
{
Items = new Dictionary<int, OrderDiscountInfoModel>();
foreach (var candidate in candidates)
{
Items.Add(Items.Count, candidate);
}
}
private CandidateOrderDiscounts(Dictionary<int, OrderDiscountInfoModel> items)
{
Items = items ?? new Dictionary<int, OrderDiscountInfoModel>();
}
public object Clone()
{
return new CandidateOrderDiscounts(Items.ToDictionary(x => x.Key, x => x.Value));
}
}

33
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/HasDynamicDiscountAmountExtensions.cs

@ -0,0 +1,33 @@
using System;
using EasyAbp.EShop.Products.Products;
using NodaMoney;
namespace EasyAbp.EShop.Orders.Orders;
public static class HasDynamicDiscountAmountExtensions
{
/// <summary>
/// Calculate the real-time discount amount based on the current price.
/// The calculated discount amount has been formatted with NodaMoney and MidpointRounding.ToZero.
/// </summary>
public static decimal CalculateDiscountAmount(this IHasDynamicDiscountAmount value, decimal currentPrice,
string currency)
{
var money = new Money(
value.DynamicDiscountAmount.DiscountAmount > decimal.Zero
? value.DynamicDiscountAmount.DiscountAmount
: value.DynamicDiscountAmount.DiscountRate * currentPrice, currency, MidpointRounding.ToZero);
if (money.Amount < decimal.Zero)
{
throw new DiscountAmountOverflowException();
}
if (money.Amount > currentPrice)
{
money = new Money(currentPrice, currency);
}
return money.Amount;
}
}

10
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderDiscountDistributor.cs

@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace EasyAbp.EShop.Orders.Orders;
public interface IOrderDiscountDistributor
{
Task<OrderDiscountDistributionModel> DistributeAsync(IOrder order, Dictionary<IOrderLine, decimal> currentPrices,
OrderDiscountInfoModel discount);
}

11
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderDiscountResolver.cs

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using EasyAbp.EShop.Products.Products;
namespace EasyAbp.EShop.Orders.Orders;
public interface IOrderDiscountResolver
{
Task<List<OrderDiscountDistributionModel>> ResolveAsync(Order order, Dictionary<Guid, IProduct> productDict);
}

18
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/NewOrderGenerator.cs

@ -7,7 +7,6 @@ using EasyAbp.EShop.Products.Products;
using NodaMoney; using NodaMoney;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Auditing; using Volo.Abp.Auditing;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
using Volo.Abp.ObjectExtending; using Volo.Abp.ObjectExtending;
using Volo.Abp.Settings; using Volo.Abp.Settings;
@ -18,17 +17,20 @@ namespace EasyAbp.EShop.Orders.Orders
{ {
private readonly ISettingProvider _settingProvider; private readonly ISettingProvider _settingProvider;
private readonly IOrderNumberGenerator _orderNumberGenerator; private readonly IOrderNumberGenerator _orderNumberGenerator;
private readonly IOrderDiscountResolver _orderDiscountResolver;
private readonly IProductSkuDescriptionProvider _productSkuDescriptionProvider; private readonly IProductSkuDescriptionProvider _productSkuDescriptionProvider;
private readonly IEnumerable<IOrderLinePriceOverrider> _orderLinePriceOverriders; private readonly IEnumerable<IOrderLinePriceOverrider> _orderLinePriceOverriders;
public NewOrderGenerator( public NewOrderGenerator(
ISettingProvider settingProvider, ISettingProvider settingProvider,
IOrderNumberGenerator orderNumberGenerator, IOrderNumberGenerator orderNumberGenerator,
IOrderDiscountResolver orderDiscountResolver,
IProductSkuDescriptionProvider productSkuDescriptionProvider, IProductSkuDescriptionProvider productSkuDescriptionProvider,
IEnumerable<IOrderLinePriceOverrider> orderLinePriceOverriders) IEnumerable<IOrderLinePriceOverrider> orderLinePriceOverriders)
{ {
_settingProvider = settingProvider; _settingProvider = settingProvider;
_orderNumberGenerator = orderNumberGenerator; _orderNumberGenerator = orderNumberGenerator;
_orderDiscountResolver = orderDiscountResolver;
_productSkuDescriptionProvider = productSkuDescriptionProvider; _productSkuDescriptionProvider = productSkuDescriptionProvider;
_orderLinePriceOverriders = orderLinePriceOverriders; _orderLinePriceOverriders = orderLinePriceOverriders;
} }
@ -107,19 +109,11 @@ namespace EasyAbp.EShop.Orders.Orders
protected virtual async Task DiscountOrderAsync(Order order, Dictionary<Guid, IProduct> productDict) protected virtual async Task DiscountOrderAsync(Order order, Dictionary<Guid, IProduct> productDict)
{ {
var context = new OrderDiscountContext(order, productDict); var distributions = await _orderDiscountResolver.ResolveAsync(order, productDict);
foreach (var provider in LazyServiceProvider.LazyGetService<IEnumerable<IOrderDiscountProvider>>() foreach (var distribution in distributions)
.OrderBy(x => x.EffectOrder))
{ {
await provider.DiscountAsync(context); order.AddDiscounts(distribution);
}
var effectDiscounts = context.GetEffectDiscounts();
foreach (var discount in effectDiscounts)
{
order.AddDiscounts(discount);
} }
} }

70
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/Order.cs

@ -188,64 +188,16 @@ namespace EasyAbp.EShop.Orders.Orders
return !(!PaymentId.HasValue || PaidTime.HasValue); return !(!PaymentId.HasValue || PaidTime.HasValue);
} }
public void AddDiscounts(OrderDiscountInfoModel infoModel) public void AddDiscounts(OrderDiscountDistributionModel model)
{ {
var affectedOrderLines = infoModel.AffectedOrderLineIds foreach (var (orderLineId, discountAmount) in model.Distributions)
.Select(orderLineId => OrderLines.Single(x => x.Id == orderLineId))
.ToList();
var remainingDiscountedAmount = new Money(infoModel.DiscountedAmount, Currency);
var totalOrderLineActualTotalPrice = new Money(affectedOrderLines.Sum(x => x.ActualTotalPrice), Currency);
var orderLineDiscounts = new Dictionary<OrderLine, Money>();
foreach (var orderLineId in infoModel.AffectedOrderLineIds)
{ {
var orderLine = OrderLines.Single(x => x.Id == orderLineId); var orderLine = OrderLines.Single(x => x.Id == orderLineId);
var orderLineActualTotalPrice = new Money(orderLine.ActualTotalPrice, Currency); orderLine.AddDiscount(discountAmount);
var maxDiscountAmount =
new Money(
orderLineActualTotalPrice.Amount / totalOrderLineActualTotalPrice.Amount *
infoModel.DiscountedAmount, Currency, MidpointRounding.ToZero);
var discountAmount = maxDiscountAmount > totalOrderLineActualTotalPrice
? orderLineActualTotalPrice
: maxDiscountAmount;
orderLineDiscounts[orderLine] = discountAmount;
remainingDiscountedAmount -= discountAmount;
}
foreach (var orderLine in affectedOrderLines.OrderByDescending(x => x.ActualTotalPrice))
{
if (remainingDiscountedAmount == decimal.Zero)
{
break;
}
var discountAmount = remainingDiscountedAmount > totalOrderLineActualTotalPrice
? totalOrderLineActualTotalPrice
: remainingDiscountedAmount;
orderLineDiscounts[orderLine] += discountAmount;
remainingDiscountedAmount -= discountAmount;
}
if (remainingDiscountedAmount.Amount != decimal.Zero)
{
throw new ApplicationException();
}
foreach (var affectedOrderLine in affectedOrderLines)
{
var orderLineDiscountedAmount = orderLineDiscounts[affectedOrderLine];
affectedOrderLine.AddDiscount(orderLineDiscountedAmount.Amount);
TotalDiscount += orderLineDiscountedAmount.Amount; TotalDiscount += discountAmount;
ActualTotalPrice -= orderLineDiscountedAmount.Amount; ActualTotalPrice -= discountAmount;
if (ActualTotalPrice < decimal.Zero) if (ActualTotalPrice < decimal.Zero)
{ {
@ -253,14 +205,16 @@ namespace EasyAbp.EShop.Orders.Orders
} }
if (OrderDiscounts.Any(x => if (OrderDiscounts.Any(x =>
x.OrderLineId == affectedOrderLine.Id && x.Name == infoModel.Name && x.OrderLineId == orderLineId && x.Name == model.DiscountInfoModel.Name &&
x.Key == infoModel.Key)) x.Key == model.DiscountInfoModel.Key))
{ {
throw new DuplicateOrderDiscountException(affectedOrderLine.Id, infoModel.Name, infoModel.Key); throw new DuplicateOrderDiscountException(orderLineId, model.DiscountInfoModel.Name,
model.DiscountInfoModel.Key);
} }
var orderDiscount = new OrderDiscount(Id, affectedOrderLine.Id, infoModel.EffectGroup, var orderDiscount = new OrderDiscount(Id, orderLineId, model.DiscountInfoModel.EffectGroup,
infoModel.Name, infoModel.Key, infoModel.DisplayName, orderLineDiscountedAmount.Amount); model.DiscountInfoModel.Name, model.DiscountInfoModel.Key, model.DiscountInfoModel.DisplayName,
discountAmount);
OrderDiscounts.Add(orderDiscount); OrderDiscounts.Add(orderDiscount);
} }

28
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountDistributionModel.cs

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp;
namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscountDistributionModel
{
public OrderDiscountInfoModel DiscountInfoModel { get; set; }
/// <summary>
/// OrderLine to discount amount mapping.
/// </summary>
public Dictionary<Guid, decimal> Distributions { get; set; }
public OrderDiscountDistributionModel(OrderDiscountInfoModel discountInfoModel,
Dictionary<Guid, decimal> distributions)
{
DiscountInfoModel = Check.NotNull(discountInfoModel, nameof(discountInfoModel));
Distributions = Check.NotNull(distributions, nameof(distributions));
if (DiscountInfoModel.AffectedOrderLineIds.Any(x => !Distributions.ContainsKey(x)))
{
throw new AbpException("The OrderDiscountDistributionModel got incorrect distributions.");
}
}
}

66
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountDistributor.cs

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.EShop.Products.Products;
using NodaMoney;
using Volo.Abp.DependencyInjection;
namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscountDistributor : IOrderDiscountDistributor, ITransientDependency
{
public virtual Task<OrderDiscountDistributionModel> DistributeAsync(IOrder order,
Dictionary<IOrderLine, decimal> currentPrices, OrderDiscountInfoModel discount)
{
var affectedOrderLines = discount.AffectedOrderLineIds
.Select(orderLineId => order.OrderLines.Single(x => x.Id == orderLineId))
.ToList();
var affectedOrderLinesCurrentPrice =
new Money(affectedOrderLines.Sum(x => currentPrices[x]), order.Currency);
var totalDiscountAmount = discount.CalculateDiscountAmount(affectedOrderLinesCurrentPrice.Amount, order.Currency);
var distributions = new Dictionary<Guid, decimal>();
var remainingDiscountAmount = totalDiscountAmount;
foreach (var orderLine in affectedOrderLines)
{
var calculatedDiscountAmount = new Money(
currentPrices[orderLine] / affectedOrderLinesCurrentPrice.Amount *
totalDiscountAmount, order.Currency, MidpointRounding.ToZero);
var discountAmount = calculatedDiscountAmount.Amount > currentPrices[orderLine]
? currentPrices[orderLine]
: calculatedDiscountAmount.Amount;
distributions[orderLine.Id] = discountAmount;
currentPrices[orderLine] -= discountAmount;
remainingDiscountAmount -= discountAmount;
}
foreach (var orderLine in affectedOrderLines.OrderByDescending(x => currentPrices[x]))
{
if (remainingDiscountAmount == decimal.Zero)
{
break;
}
var discountAmount = remainingDiscountAmount > currentPrices[orderLine]
? currentPrices[orderLine]
: remainingDiscountAmount;
distributions[orderLine.Id] += discountAmount;
currentPrices[orderLine] -= discountAmount;
remainingDiscountAmount -= discountAmount;
}
if (remainingDiscountAmount != decimal.Zero)
{
throw new ApplicationException("The OrderDiscountDistributor failed to distribute the remaining");
}
return Task.FromResult(new OrderDiscountDistributionModel(discount, distributions));
}
}

85
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountElectionModel.cs

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using EasyAbp.EShop.Products.Products;
using Volo.Abp;
namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscountElectionModel
{
/// <summary>
/// The search will stop and throw an exception if the number of executions is greater than <see cref="MaxDepth"/>.
/// <see cref="MaxDepth"/> = Math.Pow(2, <see cref="MaxCandidates"/>)
/// </summary>
public static int MaxCandidates { get; set; } = 10;
/// <summary>
/// The search will stop and throw an exception if the number of executions is greater than <see cref="MaxDepth"/>.
/// This is to prevent a dead loop.
/// </summary>
private static double MaxDepth { get; set; } = Math.Pow(2, MaxCandidates);
/// <summary>
/// The search will stop and throw an exception if this value is greater than <see cref="MaxDepth"/>.
/// </summary>
public double CurrentDepth { get; private set; }
public IOrder Order { get; }
public Dictionary<Guid, IProduct> ProductDict { get; }
public List<OrderDiscountsSchemeModel> Schemes { get; } = new();
public bool Done => Schemes.Any() && !CandidateDiscountsQueue.Any();
private Queue<CandidateOrderDiscounts> CandidateDiscountsQueue { get; } = new();
private HashSet<string> UsedCombinations { get; } = new();
public OrderDiscountElectionModel(IOrder order, Dictionary<Guid, IProduct> productDict)
{
Order = order;
ProductDict = productDict;
}
public OrderDiscountsSchemeModel GetBestScheme()
{
if (!Done)
{
throw new AbpException("The OrderDiscountElectionModel is in an incorrect state.");
}
return Schemes.MaxBy(x => x.TotalDiscountAmount);
}
public bool TryEnqueue(CandidateOrderDiscounts candidateDiscounts)
{
if (candidateDiscounts.Items.Count > MaxCandidates)
{
throw new AbpException(
$"The OrderDiscountElectionModel cannot handle a CandidateOrderDiscounts with the number of candidate discounts > {MaxCandidates}.");
}
CurrentDepth++;
if (CurrentDepth > MaxDepth || CurrentDepth < 0)
{
throw new AbpException("The OrderDiscountElectionModel's search exceeded maximum depth.");
}
if (!UsedCombinations.Add(candidateDiscounts.Items.Select(pair => pair.Key).JoinAsString(";")))
{
return false;
}
CandidateDiscountsQueue.Enqueue(candidateDiscounts);
return true;
}
public CandidateOrderDiscounts Dequeue()
{
return CandidateDiscountsQueue.Dequeue();
}
}

98
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountResolver.cs

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.EShop.Products.Products;
using Volo.Abp.DependencyInjection;
namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscountResolver : IOrderDiscountResolver, ITransientDependency
{
public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
protected IOrderDiscountDistributor OrderDiscountDistributor =>
LazyServiceProvider.LazyGetRequiredService<IOrderDiscountDistributor>();
public virtual async Task<List<OrderDiscountDistributionModel>> ResolveAsync(Order order,
Dictionary<Guid, IProduct> productDict)
{
var context = new OrderDiscountContext(order.CreationTime, order, productDict);
foreach (var provider in LazyServiceProvider.LazyGetService<IEnumerable<IOrderDiscountProvider>>()
.OrderBy(x => x.EffectOrder))
{
await provider.DiscountAsync(context);
}
if (context.CandidateDiscounts.IsNullOrEmpty())
{
return new List<OrderDiscountDistributionModel>();
}
var electionModel = new OrderDiscountElectionModel(context.Order, context.ProductDict);
electionModel.TryEnqueue(new CandidateOrderDiscounts(context.CandidateDiscounts));
while (!electionModel.Done)
{
await EvolveAsync(electionModel);
}
return electionModel.GetBestScheme().Distributions;
}
protected virtual async Task EvolveAsync(OrderDiscountElectionModel electionModel)
{
if (electionModel.Done)
{
return;
}
var order = electionModel.Order;
var candidateDiscounts = electionModel.Dequeue();
// Make sure that each OrderLine can only be affected by one discount with the same EffectGroup.
var affectedOrderLineIdsInEffectGroup = new Dictionary<string, List<Guid>>();
var usedDiscountNameKeyPairs = new HashSet<(string, string)>();
var currentPrices =
new Dictionary<IOrderLine, decimal>(order.OrderLines.ToDictionary(x => x, x => x.UnitPrice));
var distributionModels = new List<OrderDiscountDistributionModel>();
foreach (var (index, discount) in candidateDiscounts.Items)
{
if (!usedDiscountNameKeyPairs.Add(new ValueTuple<string, string>(discount.Name, discount.Key)))
{
continue;
}
if (!discount.EffectGroup.IsNullOrEmpty())
{
var affectedOrderLineIds =
affectedOrderLineIdsInEffectGroup.GetOrAdd(discount.EffectGroup!, () => new List<Guid>());
if (discount.AffectedOrderLineIds.Any(x => affectedOrderLineIds.Contains(x)))
{
continue;
}
affectedOrderLineIdsInEffectGroup[discount.EffectGroup!].AddRange(discount.AffectedOrderLineIds);
}
if (candidateDiscounts.Items.Values.Any(x => x.EffectGroup == discount.EffectGroup && x != discount))
{
// remove the current discount and try again to find a better scheme
var newCandidateDiscounts = (CandidateOrderDiscounts)candidateDiscounts.Clone();
newCandidateDiscounts.Items.Remove(index);
electionModel.TryEnqueue(newCandidateDiscounts);
}
var distributionResult = await OrderDiscountDistributor.DistributeAsync(order, currentPrices, discount);
distributionModels.Add(new OrderDiscountDistributionModel(discount, distributionResult.Distributions));
}
electionModel.Schemes.Add(new OrderDiscountsSchemeModel(distributionModels));
}
}

16
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountsSchemeModel.cs

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Linq;
namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscountsSchemeModel
{
public List<OrderDiscountDistributionModel> Distributions { get; }
public decimal TotalDiscountAmount => Distributions.SelectMany(x => x.Distributions).Sum(x => x.Value);
public OrderDiscountsSchemeModel(List<OrderDiscountDistributionModel> distributions)
{
Distributions = distributions;
}
}

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/EShopOrdersWebModule.cs

@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EasyAbp.EShop.Orders.Localization; using EasyAbp.EShop.Orders.Localization;
using EasyAbp.EShop.Orders.Web.Menus; using EasyAbp.EShop.Orders.Web.Menus;
using EasyAbp.EShop.Stores;
using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.Localization;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared;
using Volo.Abp.AutoMapper; using Volo.Abp.AutoMapper;
@ -13,6 +14,7 @@ namespace EasyAbp.EShop.Orders.Web
{ {
[DependsOn( [DependsOn(
typeof(EShopOrdersApplicationContractsModule), typeof(EShopOrdersApplicationContractsModule),
typeof(EShopStoresWebSharedModule),
typeof(AbpAspNetCoreMvcUiThemeSharedModule), typeof(AbpAspNetCoreMvcUiThemeSharedModule),
typeof(AbpAutoMapperModule) typeof(AbpAutoMapperModule)
)] )]

2
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/EasyAbp.EShop.Orders.Web.csproj

@ -18,7 +18,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\EasyAbp.EShop.Orders.Application.Contracts\EasyAbp.EShop.Orders.Application.Contracts.csproj" /> <ProjectReference Include="..\EasyAbp.EShop.Orders.Application.Contracts\EasyAbp.EShop.Orders.Application.Contracts.csproj" />
<ProjectReference Include="..\..\..\EasyAbp.EShop.Stores\src\EasyAbp.EShop.Stores.Application.Contracts\EasyAbp.EShop.Stores.Application.Contracts.csproj" /> <ProjectReference Include="..\..\..\EasyAbp.EShop.Stores\src\EasyAbp.EShop.Stores.Web.Shared\EasyAbp.EShop.Stores.Web.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

4
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/Menus/OrdersMenuContributor.cs

@ -26,9 +26,9 @@ namespace EasyAbp.EShop.Orders.Web.Menus
if (await context.IsGrantedAsync(OrdersPermissions.Orders.Manage)) if (await context.IsGrantedAsync(OrdersPermissions.Orders.Manage))
{ {
var storeAppService = context.ServiceProvider.GetRequiredService<IStoreAppService>(); var uiDefaultStoreProvider = context.ServiceProvider.GetRequiredService<IUiDefaultStoreProvider>();
var defaultStore = (await storeAppService.GetDefaultAsync())?.Id; var defaultStore = (await uiDefaultStoreProvider.GetAsync())?.Id;
orderManagementMenuItem.AddItem( orderManagementMenuItem.AddItem(
new ApplicationMenuItem(OrdersMenus.Order, l["Menu:Order"], "/EShop/Orders/Orders/Order?storeId=" + defaultStore) new ApplicationMenuItem(OrdersMenus.Order, l["Menu:Order"], "/EShop/Orders/Orders/Order?storeId=" + defaultStore)

20
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/DemoOrderDiscountProvider.cs

@ -16,11 +16,21 @@ public class DemoOrderDiscountProvider : IOrderDiscountProvider
{ {
var firstOrderLine = context.Order.OrderLines.First(); var firstOrderLine = context.Order.OrderLines.First();
return Task.FromResult(new List<OrderDiscountInfoModel> var models = new List<OrderDiscountInfoModel>
{ {
new(new List<Guid> { firstOrderLine.Id }, null, "DemoDiscount1", "1", "Demo Discount 1", 0.01m), new(new List<Guid> { firstOrderLine.Id }, null, "DemoDiscount1", "1", "Demo Discount 1",
new(new List<Guid> { firstOrderLine.Id }, "A", "DemoDiscount2", "2", "Demo Discount 2", 0.1m), new DynamicDiscountAmountModel("USD", 0.01m, 0m, null)),
new(new List<Guid> { firstOrderLine.Id }, "A", "DemoDiscount3", "3", "Demo Discount 3", 0.05m), new(new List<Guid> { firstOrderLine.Id }, "A", "DemoDiscount2", "2", "Demo Discount 2",
}); new DynamicDiscountAmountModel("USD", 0.1m, 0m, null)),
new(new List<Guid> { firstOrderLine.Id }, "A", "DemoDiscount3", "3", "Demo Discount 3",
new DynamicDiscountAmountModel("USD", 0.05m, 0m, null)),
};
foreach (var model in models)
{
context.CandidateDiscounts.Add(model);
}
return Task.CompletedTask;
} }
} }

161
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDiscountDistributorTests.cs

@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EasyAbp.EShop.Products.Products;
using Shouldly;
using Xunit;
namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscountDistributorTests : OrdersDomainTestBase
{
private IOrderDiscountDistributor OrderDiscountDistributor { get; }
public OrderDiscountDistributorTests()
{
OrderDiscountDistributor = GetRequiredService<IOrderDiscountDistributor>();
}
[Fact]
public async Task Should_Fall_Back_Discount_Amount_Using_MidpointRounding_ToZero()
{
var orderGenerator = GetRequiredService<INewOrderGenerator>();
var createOrderInfoModel = new CreateOrderInfoModel(OrderTestData.Store1Id, null,
new List<CreateOrderLineInfoModel>
{
new(OrderTestData.Product1Id, OrderTestData.ProductSku1Id, 1),
}
);
var order = await orderGenerator.GenerateAsync(Guid.NewGuid(), createOrderInfoModel,
new Dictionary<Guid, IProduct>
{
{
OrderTestData.Product1Id, new ProductEto
{
Id = OrderTestData.Product1Id,
ProductSkus = new List<ProductSkuEto>
{
new()
{
Id = OrderTestData.ProductSku1Id,
AttributeOptionIds = new List<Guid>(),
Price = 0.99m,
Currency = "USD",
OrderMinQuantity = 1,
OrderMaxQuantity = 100,
}
}
}
}
}, new Dictionary<Guid, DateTime>());
const decimal discountRate = 0.50m;
var discount = new OrderDiscountInfoModel(order.OrderLines.Select(x => x.Id).ToList(), null, "Test", null,
null, new DynamicDiscountAmountModel("USD", 0m, discountRate, null));
var currentPrices =
new Dictionary<IOrderLine, decimal>(order.OrderLines.ToDictionary(x => (IOrderLine)x,
x => x.UnitPrice));
var distributionResult = await OrderDiscountDistributor.DistributeAsync(order, currentPrices, discount);
order.AddDiscounts(distributionResult);
var orderLine1 = order.OrderLines[0];
var discountedAmount1 = Math.Round(orderLine1.TotalPrice * discountRate, 2, MidpointRounding.ToZero);
discountedAmount1.ShouldBe(0.49m);
var discount1 = order.OrderDiscounts.Find(x => x.OrderLineId == orderLine1.Id);
order.OrderDiscounts.Count.ShouldBe(1);
discount1.ShouldNotBeNull();
discount1.DiscountedAmount.ShouldBe(discountedAmount1);
}
[Fact]
public async Task Should_Discount_Multi_OrderLine()
{
var orderGenerator = GetRequiredService<INewOrderGenerator>();
var createOrderInfoModel = new CreateOrderInfoModel(OrderTestData.Store1Id, null,
new List<CreateOrderLineInfoModel>
{
new(OrderTestData.Product1Id, OrderTestData.ProductSku1Id, 1),
new(OrderTestData.Product1Id, OrderTestData.ProductSku2Id, 1),
}
);
var order = await orderGenerator.GenerateAsync(Guid.NewGuid(), createOrderInfoModel,
new Dictionary<Guid, IProduct>
{
{
OrderTestData.Product1Id, new ProductEto
{
Id = OrderTestData.Product1Id,
ProductSkus = new List<ProductSkuEto>
{
new()
{
Id = OrderTestData.ProductSku1Id,
AttributeOptionIds = new List<Guid>(),
Price = 1.7m,
Currency = "USD",
OrderMinQuantity = 1,
OrderMaxQuantity = 100,
},
new()
{
Id = OrderTestData.ProductSku2Id,
AttributeOptionIds = new List<Guid>(),
Price = 3.11m,
Currency = "USD",
OrderMinQuantity = 1,
OrderMaxQuantity = 100,
}
}
}
}
}, new Dictionary<Guid, DateTime>());
const decimal discountedAmount = 0.9m;
var discount = new OrderDiscountInfoModel(order.OrderLines.Select(x => x.Id).ToList(), null, "Test", null,
null, new DynamicDiscountAmountModel("USD", discountedAmount, 0m, null));
var currentPrices =
new Dictionary<IOrderLine, decimal>(order.OrderLines.ToDictionary(x => (IOrderLine)x,
x => x.UnitPrice));
var distributionResult = await OrderDiscountDistributor.DistributeAsync(order, currentPrices, discount);
order.AddDiscounts(distributionResult);
var orderLine1 = order.OrderLines[0];
var orderLine2 = order.OrderLines[1];
var discountedAmount1 = Math.Round(discountedAmount * (orderLine1.TotalPrice / order.TotalPrice), 2,
MidpointRounding.ToZero);
var discountedAmount2 = Math.Round(discountedAmount * (orderLine2.TotalPrice / order.TotalPrice), 2,
MidpointRounding.ToZero);
var fraction = discountedAmount - discountedAmount1 - discountedAmount2;
discountedAmount1.ShouldBe(0.31m);
discountedAmount2.ShouldBe(0.58m);
fraction.ShouldBe(0.01m);
var discount1 = order.OrderDiscounts.Find(x => x.OrderLineId == orderLine1.Id);
var discount2 = order.OrderDiscounts.Find(x => x.OrderLineId == orderLine2.Id);
order.OrderDiscounts.Count.ShouldBe(2);
discount1.ShouldNotBeNull();
discount2.ShouldNotBeNull();
discount1.DiscountedAmount.ShouldBe(discountedAmount1);
discount2.DiscountedAmount.ShouldBe(discountedAmount2 + fraction);
}
}

74
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs

@ -14,6 +14,7 @@ namespace EasyAbp.EShop.Orders.Orders
public class OrderDomainTests : OrdersDomainTestBase public class OrderDomainTests : OrdersDomainTestBase
{ {
private Order Order1 { get; set; } private Order Order1 { get; set; }
private readonly IOrderRepository _orderRepository; private readonly IOrderRepository _orderRepository;
public OrderDomainTests() public OrderDomainTests()
@ -244,78 +245,5 @@ namespace EasyAbp.EShop.Orders.Orders
order.SetReducedInventoryAfterPlacingTime(null); order.SetReducedInventoryAfterPlacingTime(null);
await Should.ThrowAsync<OrderIsInWrongStageException>(() => orderManager.CancelAsync(order, "my-reason")); await Should.ThrowAsync<OrderIsInWrongStageException>(() => orderManager.CancelAsync(order, "my-reason"));
} }
[Fact]
public async Task Should_Discount_Multi_OrderLine()
{
var orderGenerator = GetRequiredService<INewOrderGenerator>();
var createOrderInfoModel = new CreateOrderInfoModel(OrderTestData.Store1Id, null,
new List<CreateOrderLineInfoModel>
{
new(OrderTestData.Product1Id, OrderTestData.ProductSku1Id, 1),
new(OrderTestData.Product1Id, OrderTestData.ProductSku2Id, 1),
}
);
var order = await orderGenerator.GenerateAsync(Guid.NewGuid(), createOrderInfoModel,
new Dictionary<Guid, IProduct>
{
{
OrderTestData.Product1Id, new ProductEto
{
Id = OrderTestData.Product1Id,
ProductSkus = new List<ProductSkuEto>
{
new()
{
Id = OrderTestData.ProductSku1Id,
AttributeOptionIds = new List<Guid>(),
Price = 1.7m,
Currency = "USD",
OrderMinQuantity = 1,
OrderMaxQuantity = 100,
},
new()
{
Id = OrderTestData.ProductSku2Id,
AttributeOptionIds = new List<Guid>(),
Price = 3.11m,
Currency = "USD",
OrderMinQuantity = 1,
OrderMaxQuantity = 100,
}
}
}
}
}, new Dictionary<Guid, DateTime>());
const decimal discountedAmount = 0.9m;
order.AddDiscounts(new OrderDiscountInfoModel(order.OrderLines.Select(x => x.Id).ToList(), null, "Test",
null, null, discountedAmount));
var orderLine1 = order.OrderLines[0];
var orderLine2 = order.OrderLines[1];
var discountedAmount1 = Math.Round(discountedAmount * (orderLine1.TotalPrice / order.TotalPrice), 2,
MidpointRounding.ToZero);
var discountedAmount2 = Math.Round(discountedAmount * (orderLine2.TotalPrice / order.TotalPrice), 2,
MidpointRounding.ToZero);
var fraction = discountedAmount - discountedAmount1 - discountedAmount2;
discountedAmount1.ShouldBe(0.31m);
discountedAmount2.ShouldBe(0.58m);
fraction.ShouldBe(0.01m);
var discount1 = order.OrderDiscounts.Find(x => x.OrderLineId == orderLine1.Id);
var discount2 = order.OrderDiscounts.Find(x => x.OrderLineId == orderLine2.Id);
order.OrderDiscounts.Count.ShouldBe(2);
discount1.ShouldNotBeNull();
discount2.ShouldNotBeNull();
discount1.DiscountedAmount.ShouldBe(discountedAmount1);
discount2.DiscountedAmount.ShouldBe(discountedAmount2 + fraction);
}
} }
} }

2
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/EShopPaymentsWebModule.cs

@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EasyAbp.EShop.Payments.Localization; using EasyAbp.EShop.Payments.Localization;
using EasyAbp.EShop.Payments.Web.Menus; using EasyAbp.EShop.Payments.Web.Menus;
using EasyAbp.EShop.Stores;
using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.Localization;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared;
using Volo.Abp.AutoMapper; using Volo.Abp.AutoMapper;
@ -13,6 +14,7 @@ namespace EasyAbp.EShop.Payments.Web
{ {
[DependsOn( [DependsOn(
typeof(EShopPaymentsApplicationContractsModule), typeof(EShopPaymentsApplicationContractsModule),
typeof(EShopStoresWebSharedModule),
typeof(AbpAspNetCoreMvcUiThemeSharedModule), typeof(AbpAspNetCoreMvcUiThemeSharedModule),
typeof(AbpAutoMapperModule) typeof(AbpAutoMapperModule)
)] )]

2
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/EasyAbp.EShop.Payments.Web.csproj

@ -17,7 +17,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\EasyAbp.EShop.Stores\src\EasyAbp.EShop.Stores.Application.Contracts\EasyAbp.EShop.Stores.Application.Contracts.csproj" /> <ProjectReference Include="..\..\..\EasyAbp.EShop.Stores\src\EasyAbp.EShop.Stores.Web.Shared\EasyAbp.EShop.Stores.Web.Shared.csproj" />
<ProjectReference Include="..\EasyAbp.EShop.Payments.Application.Contracts\EasyAbp.EShop.Payments.Application.Contracts.csproj" /> <ProjectReference Include="..\EasyAbp.EShop.Payments.Application.Contracts\EasyAbp.EShop.Payments.Application.Contracts.csproj" />
</ItemGroup> </ItemGroup>

4
modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/Menus/PaymentsMenuContributor.cs

@ -24,9 +24,9 @@ namespace EasyAbp.EShop.Payments.Web.Menus
var paymentManagementMenuItem = new ApplicationMenuItem(PaymentsMenus.Prefix, l["Menu:PaymentManagement"]); var paymentManagementMenuItem = new ApplicationMenuItem(PaymentsMenus.Prefix, l["Menu:PaymentManagement"]);
var storeAppService = context.ServiceProvider.GetRequiredService<IStoreAppService>(); var uiDefaultStoreProvider = context.ServiceProvider.GetRequiredService<IUiDefaultStoreProvider>();
var defaultStore = (await storeAppService.GetDefaultAsync())?.Id; var defaultStore = (await uiDefaultStoreProvider.GetAsync())?.Id;
if (await context.IsGrantedAsync(PaymentsPermissions.Payments.Manage)) if (await context.IsGrantedAsync(PaymentsPermissions.Payments.Manage))
{ {

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

@ -313,12 +313,12 @@ namespace EasyAbp.EShop.Products.Products
{ {
var productSkuDto = productDto.ProductSkus.First(x => x.Id == productSku.Id); var productSkuDto = productDto.ProductSkus.First(x => x.Id == productSku.Id);
var priceDataModel = await _productManager.GetRealPriceAsync(product, productSku, now); var realTimePriceInfoModel = await _productManager.GetRealTimePriceAsync(product, productSku, now);
productSkuDto.PriceWithoutDiscount = priceDataModel.PriceWithoutDiscount; productSkuDto.PriceWithoutDiscount = realTimePriceInfoModel.PriceWithoutDiscount;
productSkuDto.Price = priceDataModel.GetDiscountedPrice(); productSkuDto.Price = realTimePriceInfoModel.TotalDiscountedPrice;
productSkuDto.ProductDiscounts = priceDataModel.ProductDiscounts; productSkuDto.ProductDiscounts = realTimePriceInfoModel.Discounts.ProductDiscounts;
productSkuDto.OrderDiscountPreviews = priceDataModel.OrderDiscountPreviews; productSkuDto.OrderDiscountPreviews = realTimePriceInfoModel.Discounts.OrderDiscountPreviews;
} }
if (productDto.ProductSkus.Count > 0) if (productDto.ProductSkus.Count > 0)

36
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductViewAppService.cs

@ -205,8 +205,8 @@ namespace EasyAbp.EShop.Products.Products
foreach (var productSku in product.ProductSkus) foreach (var productSku in product.ProductSkus)
{ {
var overrideProductDiscounts = false; var overrideProductDiscounts = false;
var priceDataModel = await _productManager.GetRealPriceAsync(product, productSku, now); var realTimePrice = await _productManager.GetRealTimePriceAsync(product, productSku, now);
var discountedPrice = priceDataModel.GetDiscountedPrice(); var discountedPrice = realTimePrice.TotalDiscountedPrice;
if (min is null || discountedPrice < min.Value) if (min is null || discountedPrice < min.Value)
{ {
@ -219,35 +219,39 @@ namespace EasyAbp.EShop.Products.Products
max = discountedPrice; max = discountedPrice;
} }
if (minWithoutDiscount is null || priceDataModel.PriceWithoutDiscount < minWithoutDiscount.Value) if (minWithoutDiscount is null || realTimePrice.PriceWithoutDiscount < minWithoutDiscount.Value)
{ {
minWithoutDiscount = priceDataModel.PriceWithoutDiscount; minWithoutDiscount = realTimePrice.PriceWithoutDiscount;
} }
if (maxWithoutDiscount is null || priceDataModel.PriceWithoutDiscount > maxWithoutDiscount.Value) if (maxWithoutDiscount is null || realTimePrice.PriceWithoutDiscount > maxWithoutDiscount.Value)
{ {
maxWithoutDiscount = priceDataModel.PriceWithoutDiscount; maxWithoutDiscount = realTimePrice.PriceWithoutDiscount;
} }
foreach (var model in priceDataModel.ProductDiscounts) foreach (var discount in realTimePrice.Discounts.ProductDiscounts)
{ {
var discount = discounts.FindProductDiscount(model.Name, model.Key); var existingDiscount =
discounts.ProductDiscounts.Find(x => x.Name == discount.Name && x.Key == discount.Key);
if (discount is null || overrideProductDiscounts) if (existingDiscount is null)
{ {
discounts.AddOrUpdateProductDiscount(new ProductDiscountInfoModel(model.EffectGroup, model.Name, discounts.ProductDiscounts.Add(discount);
model.Key, model.DisplayName, model.DiscountedAmount, model.FromTime, model.ToTime)); }
else if (overrideProductDiscounts)
{
discounts.ProductDiscounts.ReplaceOne(existingDiscount, discount);
} }
} }
foreach (var model in priceDataModel.OrderDiscountPreviews) foreach (var discount in realTimePrice.Discounts.OrderDiscountPreviews)
{ {
var discount = discounts.FindOrderDiscountPreview(model.Name, model.Key); var existingDiscount =
discounts.OrderDiscountPreviews.Find(x => x.Name == discount.Name && x.Key == discount.Key);
if (discount is null) if (existingDiscount is null)
{ {
discounts.AddOrUpdateOrderDiscountPreview(new OrderDiscountPreviewInfoModel(model.EffectGroup, discounts.OrderDiscountPreviews.Add(discount);
model.Name, model.Key, model.DisplayName, model.FromTime, model.ToTime));
} }
} }
} }

22
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/CandidateProductDiscountInfoModel.cs

@ -0,0 +1,22 @@
using System;
using JetBrains.Annotations;
using Volo.Abp;
namespace EasyAbp.EShop.Products.Products;
[Serializable]
public class CandidateProductDiscountInfoModel : DiscountInfoModel, IHasDynamicDiscountAmount
{
public DynamicDiscountAmountModel DynamicDiscountAmount { get; set; }
public CandidateProductDiscountInfoModel()
{
}
public CandidateProductDiscountInfoModel([CanBeNull] string effectGroup, [NotNull] string name,
[CanBeNull] string key, [CanBeNull] string displayName, DynamicDiscountAmountModel dynamicDiscountAmount,
DateTime? fromTime, DateTime? toTime) : base(effectGroup, name, key, displayName, fromTime, toTime)
{
DynamicDiscountAmount = Check.NotNull(dynamicDiscountAmount, nameof(dynamicDiscountAmount));
}
}

10
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DiscountForProductModels.cs

@ -8,14 +8,8 @@ public class DiscountForProductModels : IHasDiscountsForProduct
public List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; set; } public List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; set; }
public DiscountForProductModels() public DiscountForProductModels(List<ProductDiscountInfoModel> productDiscounts = null,
{ List<OrderDiscountPreviewInfoModel> orderDiscountPreviews = null)
ProductDiscounts = new List<ProductDiscountInfoModel>();
OrderDiscountPreviews = new List<OrderDiscountPreviewInfoModel>();
}
public DiscountForProductModels(List<ProductDiscountInfoModel> productDiscounts,
List<OrderDiscountPreviewInfoModel> orderDiscountPreviews)
{ {
ProductDiscounts = productDiscounts ?? new List<ProductDiscountInfoModel>(); ProductDiscounts = productDiscounts ?? new List<ProductDiscountInfoModel>();
OrderDiscountPreviews = orderDiscountPreviews ?? new List<OrderDiscountPreviewInfoModel>(); OrderDiscountPreviews = orderDiscountPreviews ?? new List<OrderDiscountPreviewInfoModel>();

47
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DynamicDiscountAmountModel.cs

@ -0,0 +1,47 @@
using System;
using JetBrains.Annotations;
namespace EasyAbp.EShop.Products.Products;
[Serializable]
public class DynamicDiscountAmountModel
{
public string Currency { get; }
/// <summary>
/// The absolute discount amount.
/// <see cref="DiscountAmount"/> has a higher priority of effectiveness than <see cref="DiscountRate"/>.
/// And <see cref="DiscountRate"/> has a higher priority of effectiveness than <see cref="CalculatorName"/>.
/// </summary>
public decimal DiscountAmount { get; }
/// <summary>
/// The discount rate. This means 10% off if you set it to <value>0.1</value>.
/// <see cref="DiscountAmount"/> has a higher priority of effectiveness than <see cref="DiscountRate"/>.
/// And <see cref="DiscountRate"/> has a higher priority of effectiveness than <see cref="CalculatorName"/>.
/// </summary>
public decimal DiscountRate { get; }
/// <summary>
/// (todo: NOT YET IMPLEMENTED!)
/// The name of the runtime discount calculator.
/// <see cref="DiscountAmount"/> has a higher priority of effectiveness than <see cref="DiscountRate"/>.
/// And <see cref="DiscountRate"/> has a higher priority of effectiveness than <see cref="CalculatorName"/>.
/// </summary>
[CanBeNull]
public string CalculatorName { get; }
public DynamicDiscountAmountModel(string currency, decimal discountAmount, decimal discountRate,
[CanBeNull] string calculatorName)
{
if (discountAmount < decimal.Zero || discountRate < decimal.Zero)
{
throw new DiscountAmountOverflowException();
}
Currency = currency;
DiscountAmount = discountAmount;
DiscountRate = discountRate;
CalculatorName = calculatorName;
}
}

146
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/HasDiscountsForProductExtensions.cs

@ -1,146 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
namespace EasyAbp.EShop.Products.Products;
public static class HasDiscountsForProductExtensions
{
public static void FillEffectState(this IHasDiscountsForProduct hasDiscountsForProduct, DateTime now)
{
var effectGroupsBestModel = new Dictionary<string, ProductDiscountInfoModel>();
foreach (var model in hasDiscountsForProduct.ProductDiscounts.Where(model =>
(!model.FromTime.HasValue || model.FromTime <= now) &&
(!model.ToTime.HasValue || model.ToTime >= now)))
{
if (model.EffectGroup.IsNullOrEmpty())
{
model.InEffect = true;
}
else
{
effectGroupsBestModel.TryGetValue(model.EffectGroup!, out var existing);
if (existing is null)
{
model.InEffect = true;
effectGroupsBestModel[model.EffectGroup] = model;
}
else if (effectGroupsBestModel[model.EffectGroup].DiscountedAmount < model.DiscountedAmount)
{
effectGroupsBestModel[model.EffectGroup].InEffect = false;
model.InEffect = true;
effectGroupsBestModel[model.EffectGroup] = model;
}
}
}
}
/// <summary>
/// It returns a sum of the amount of the product discounts currently in effect.
/// </summary>
public static decimal GetDiscountedAmount(this IHasDiscountsForProduct hasDiscountsForProduct)
{
return hasDiscountsForProduct.ProductDiscounts.Where(x => x.InEffect == true).Sum(x => x.DiscountedAmount);
}
/// <summary>
/// It returns the price minus the product discounts currently in effect.
/// </summary>
public static decimal GetDiscountedPrice(this IHasFullDiscountsForProduct hasFullDiscountsForProduct)
{
return hasFullDiscountsForProduct.PriceWithoutDiscount - hasFullDiscountsForProduct.GetDiscountedAmount();
}
public static ProductDiscountInfoModel FindProductDiscount(this IHasDiscountsForProduct hasDiscountsForProduct,
[NotNull] string name, [CanBeNull] string key)
{
return hasDiscountsForProduct.ProductDiscounts.Find(x => x.Name == name && x.Key == key);
}
public static void AddOrUpdateProductDiscount(this IHasDiscountsForProduct hasDiscountsForProduct,
ProductDiscountInfoModel model)
{
var found = hasDiscountsForProduct.FindProductDiscount(model.Name, model.Key);
if (found is null)
{
hasDiscountsForProduct.ProductDiscounts.Add(model);
}
else
{
hasDiscountsForProduct.ProductDiscounts.ReplaceOne(found, model);
}
hasDiscountsForProduct.CheckDiscountedAmount();
}
public static bool TryRemoveProductDiscount(this IHasDiscountsForProduct hasDiscountsForProduct,
[NotNull] string name,
[CanBeNull] string key)
{
var found = hasDiscountsForProduct.FindProductDiscount(name, key);
if (found is null)
{
return false;
}
hasDiscountsForProduct.ProductDiscounts.Remove(found);
hasDiscountsForProduct.CheckDiscountedAmount();
return true;
}
public static OrderDiscountPreviewInfoModel FindOrderDiscountPreview(
this IHasDiscountsForProduct hasDiscountsForProduct, [NotNull] string name, [CanBeNull] string key)
{
return hasDiscountsForProduct.OrderDiscountPreviews.Find(x => x.Name == name && x.Key == key);
}
public static void AddOrUpdateOrderDiscountPreview(this IHasDiscountsForProduct hasDiscountsForProduct,
OrderDiscountPreviewInfoModel model)
{
var found = hasDiscountsForProduct.FindOrderDiscountPreview(model.Name, model.Key);
if (found is null)
{
hasDiscountsForProduct.OrderDiscountPreviews.Add(model);
}
else
{
hasDiscountsForProduct.OrderDiscountPreviews.ReplaceOne(found, model);
}
hasDiscountsForProduct.CheckDiscountedAmount();
}
public static bool TryRemoveOrderDiscountPreview(this IHasDiscountsForProduct hasDiscountsForProduct,
[NotNull] string name,
[CanBeNull] string key)
{
var found = hasDiscountsForProduct.FindOrderDiscountPreview(name, key);
if (found is null)
{
return false;
}
hasDiscountsForProduct.OrderDiscountPreviews.Remove(found);
hasDiscountsForProduct.CheckDiscountedAmount();
return true;
}
private static void CheckDiscountedAmount(this IHasDiscountsForProduct hasDiscountsForProduct)
{
if (hasDiscountsForProduct.ProductDiscounts.Any(x => x.DiscountedAmount < decimal.Zero))
{
throw new DiscountAmountOverflowException();
}
}
}

10
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IDiscountInfo.cs

@ -7,14 +7,22 @@ public interface IDiscountInfo
/// <summary> /// <summary>
/// If you set this value, only one Discount in the same EffectGroup will be applied. /// If you set this value, only one Discount in the same EffectGroup will be applied.
/// For OrderDiscounts, each OrderLine can only be affected by one discount with the same EffectGroup. /// For OrderDiscounts, each OrderLine can only be affected by one discount with the same EffectGroup.
/// For ProductDiscounts, the Discount with the highest discounted amount will be applied. /// For ProductDiscounts, the Discount with the highest discount amount will be applied.
/// </summary> /// </summary>
[CanBeNull] [CanBeNull]
string EffectGroup { get; } string EffectGroup { get; }
/// <summary>
/// If there is more than one Discount with the same <see cref="Name"/> and <see cref="Key"/>,
/// only the one with the highest discount amount will be applied.
/// </summary>
[NotNull] [NotNull]
string Name { get; } string Name { get; }
/// <summary>
/// If there is more than one Discount with the same <see cref="Name"/> and <see cref="Key"/>,
/// only the one with the highest discount amount will be applied.
/// </summary>
[CanBeNull] [CanBeNull]
string Key { get; } string Key { get; }

6
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasDynamicDiscountAmount.cs

@ -0,0 +1,6 @@
namespace EasyAbp.EShop.Products.Products;
public interface IHasDynamicDiscountAmount
{
DynamicDiscountAmountModel DynamicDiscountAmount { get; }
}

85
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs

@ -1,91 +1,34 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using Volo.Abp;
using JetBrains.Annotations;
namespace EasyAbp.EShop.Products.Products; namespace EasyAbp.EShop.Products.Products;
[Serializable]
public class ProductDiscountContext public class ProductDiscountContext
{ {
public DateTime Now { get; } public DateTime Now { get; }
public decimal PriceFromPriceProvider { get; }
public IProduct Product { get; } public IProduct Product { get; }
public IProductSku ProductSku { get; } public IProductSku ProductSku { get; }
public decimal PriceWithoutDiscount => PriceModel.PriceWithoutDiscount; public List<CandidateProductDiscountInfoModel> CandidateProductDiscounts { get; }
public IReadOnlyList<ProductDiscountInfoModel> ProductDiscounts => PriceModel.ProductDiscounts;
public IReadOnlyList<OrderDiscountPreviewInfoModel> OrderDiscountPreviews => PriceModel.OrderDiscountPreviews; public List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; }
private ProductPriceModel PriceModel { get; } public ProductDiscountContext(DateTime now, IProduct product, IProductSku productSku,
decimal priceFromPriceProvider, List<CandidateProductDiscountInfoModel> candidateProductDiscounts = null,
public ProductDiscountContext(IProduct product, IProductSku productSku, decimal priceFromPriceProvider, List<OrderDiscountPreviewInfoModel> orderDiscountPreviews = null)
DateTime now)
{ {
Product = product;
ProductSku = productSku;
PriceModel = new ProductPriceModel(priceFromPriceProvider);
Now = now; Now = now;
} Product = Check.NotNull(product, nameof(product));
ProductSku = Check.NotNull(productSku, nameof(productSku));
public ProductDiscountInfoModel FindProductDiscount([NotNull] string name, [CanBeNull] string key) PriceFromPriceProvider = priceFromPriceProvider;
{
return PriceModel.FindProductDiscount(name, key);
}
public void AddOrUpdateProductDiscount(ProductDiscountInfoModel model)
{
PriceModel.AddOrUpdateProductDiscount(model);
PriceModel.FillEffectState(Now);
}
public bool TryRemoveProductDiscount([NotNull] string name, [CanBeNull] string key)
{
var result = PriceModel.TryRemoveProductDiscount(name, key);
PriceModel.FillEffectState(Now); CandidateProductDiscounts = candidateProductDiscounts ?? new List<CandidateProductDiscountInfoModel>();
OrderDiscountPreviews = orderDiscountPreviews ?? new List<OrderDiscountPreviewInfoModel>();
return result;
}
public OrderDiscountPreviewInfoModel FindOrderDiscountPreview([NotNull] string name, [CanBeNull] string key)
{
return PriceModel.FindOrderDiscountPreview(name, key);
}
public void AddOrUpdateOrderDiscountPreview(OrderDiscountPreviewInfoModel model)
{
PriceModel.AddOrUpdateOrderDiscountPreview(model);
}
public bool TryRemoveOrderDiscountPreview([NotNull] string name, [CanBeNull] string key)
{
return PriceModel.TryRemoveOrderDiscountPreview(name, key);
}
/// <summary>
/// It returns a sum of the amount of the product discounts currently in effect.
/// </summary>
public decimal GetDiscountedAmount(string excludingEffectGroup = null)
{
return PriceModel.ProductDiscounts
.Where(x => x.InEffect == true)
.WhereIf(excludingEffectGroup != null, x => x.EffectGroup != excludingEffectGroup)
.Sum(x => x.DiscountedAmount);
}
/// <summary>
/// It returns the price minus the product discounts currently in effect.
/// </summary>
public decimal GetDiscountedPrice(string excludingEffectGroup = null)
{
return PriceWithoutDiscount - GetDiscountedAmount(excludingEffectGroup);
}
public ProductPriceModel ToFinalProductPriceModel()
{
return PriceModel;
} }
} }

14
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountInfoModel.cs

@ -8,17 +8,23 @@ public class ProductDiscountInfoModel : DiscountInfoModel, ICloneable
{ {
public decimal DiscountedAmount { get; set; } public decimal DiscountedAmount { get; set; }
public bool? InEffect { get; set; } public bool InEffect { get; set; }
public ProductDiscountInfoModel() public ProductDiscountInfoModel()
{ {
} }
public ProductDiscountInfoModel(CandidateProductDiscountInfoModel candidate, decimal discountedAmount,
bool inEffect) : this(candidate.EffectGroup, candidate.Name, candidate.Key, candidate.DisplayName,
discountedAmount, candidate.FromTime, candidate.ToTime, inEffect)
{
}
public ProductDiscountInfoModel([CanBeNull] string effectGroup, [NotNull] string name, [CanBeNull] string key, public ProductDiscountInfoModel([CanBeNull] string effectGroup, [NotNull] string name, [CanBeNull] string key,
[CanBeNull] string displayName, decimal discountedAmount, DateTime? fromTime, DateTime? toTime, [CanBeNull] string displayName, decimal discountedAmount, DateTime? fromTime, DateTime? toTime, bool inEffect) :
bool? inEffect = null) : base(effectGroup, name, key, displayName, fromTime, toTime) base(effectGroup, name, key, displayName, fromTime, toTime)
{ {
if (discountedAmount < decimal.Zero) if (DiscountedAmount < decimal.Zero)
{ {
throw new DiscountAmountOverflowException(); throw new DiscountAmountOverflowException();
} }

23
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductPriceModel.cs

@ -1,23 +0,0 @@
using System;
using System.Collections.Generic;
namespace EasyAbp.EShop.Products.Products;
public class ProductPriceModel : IHasFullDiscountsForProduct
{
public decimal PriceWithoutDiscount { get; }
public List<ProductDiscountInfoModel> ProductDiscounts { get; } = new();
public List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; } = new();
public ProductPriceModel(decimal priceWithoutDiscount)
{
if (PriceWithoutDiscount < decimal.Zero)
{
throw new OverflowException();
}
PriceWithoutDiscount = priceWithoutDiscount;
}
}

21
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/RealTimePriceInfoModel.cs

@ -0,0 +1,21 @@
using System.Linq;
namespace EasyAbp.EShop.Products.Products;
public class RealTimePriceInfoModel
{
public decimal PriceWithoutDiscount { get; }
public DiscountForProductModels Discounts { get; }
public decimal TotalDiscountAmount =>
Discounts.ProductDiscounts.Where(x => x.InEffect).Sum(x => x.DiscountedAmount);
public decimal TotalDiscountedPrice => PriceWithoutDiscount - TotalDiscountAmount;
public RealTimePriceInfoModel(decimal priceWithoutDiscount, DiscountForProductModels discounts)
{
PriceWithoutDiscount = priceWithoutDiscount;
Discounts = discounts;
}
}

30
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/CandidateProductDiscounts.cs

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace EasyAbp.EShop.Products.Products;
public class CandidateProductDiscounts : ICloneable
{
public Dictionary<int, CandidateProductDiscountInfoModel> Items { get; }
public CandidateProductDiscounts(List<CandidateProductDiscountInfoModel> candidates)
{
Items = new Dictionary<int, CandidateProductDiscountInfoModel>();
foreach (var candidate in candidates)
{
Items.Add(Items.Count, candidate);
}
}
private CandidateProductDiscounts(Dictionary<int, CandidateProductDiscountInfoModel> items)
{
Items = items ?? new Dictionary<int, CandidateProductDiscountInfoModel>();
}
public object Clone()
{
return new CandidateProductDiscounts(Items.ToDictionary(x => x.Key, x => x.Value));
}
}

32
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/HasDynamicDiscountAmountExtensions.cs

@ -0,0 +1,32 @@
using System;
using NodaMoney;
namespace EasyAbp.EShop.Products.Products;
public static class HasDynamicDiscountAmountExtensions
{
/// <summary>
/// Calculate the real-time discount amount based on the current price.
/// The calculated discount amount has been formatted with NodaMoney and MidpointRounding.ToZero.
/// </summary>
public static decimal CalculateDiscountAmount(this IHasDynamicDiscountAmount value, decimal currentPrice,
string currency)
{
var money = new Money(
value.DynamicDiscountAmount.DiscountAmount > decimal.Zero
? value.DynamicDiscountAmount.DiscountAmount
: value.DynamicDiscountAmount.DiscountRate * currentPrice, currency, MidpointRounding.ToZero);
if (money.Amount < decimal.Zero)
{
throw new DiscountAmountOverflowException();
}
if (money.Amount > currentPrice)
{
money = new Money(currentPrice, currency);
}
return money.Amount;
}
}

10
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductDiscountResolver.cs

@ -0,0 +1,10 @@
using System;
using System.Threading.Tasks;
namespace EasyAbp.EShop.Products.Products;
public interface IProductDiscountResolver
{
Task<DiscountForProductModels> ResolveAsync(IProduct product, IProductSku productSku,
decimal priceFromPriceProvider, DateTime now);
}

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

@ -30,6 +30,6 @@ namespace EasyAbp.EShop.Products.Products
Task<bool> TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold); Task<bool> TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold);
Task<ProductPriceModel> GetRealPriceAsync(Product product, ProductSku productSku, DateTime now); Task<RealTimePriceInfoModel> GetRealTimePriceAsync(Product product, ProductSku productSku, DateTime now);
} }
} }

87
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountElectionModel.cs

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp;
namespace EasyAbp.EShop.Products.Products;
public class ProductDiscountElectionModel
{
/// <summary>
/// The search will stop and throw an exception if the number of executions is greater than <see cref="MaxDepth"/>.
/// <see cref="MaxDepth"/> = Math.Pow(2, <see cref="MaxCandidates"/>)
/// </summary>
public static int MaxCandidates { get; set; } = 10;
/// <summary>
/// The search will stop and throw an exception if the number of executions is greater than <see cref="MaxDepth"/>.
/// This is to prevent a dead loop.
/// </summary>
private static double MaxDepth { get; set; } = Math.Pow(2, MaxCandidates);
/// <summary>
/// The search will stop and throw an exception if this value is greater than <see cref="MaxDepth"/>.
/// </summary>
public double CurrentDepth { get; private set; }
public IProduct Product { get; }
public IProductSku ProductSku { get; }
public decimal PriceFromPriceProvider { get; }
public List<ProductDiscountsSchemeModel> Schemes { get; } = new();
public bool Done => Schemes.Any() && !CandidateDiscountsQueue.Any();
private Queue<CandidateProductDiscounts> CandidateDiscountsQueue { get; } = new();
private HashSet<string> UsedCombinations { get; } = new();
public ProductDiscountElectionModel(IProduct product, IProductSku productSku, decimal priceFromPriceProvider)
{
Product = product;
ProductSku = productSku;
PriceFromPriceProvider = priceFromPriceProvider;
}
public ProductDiscountsSchemeModel GetBestScheme()
{
if (!Done)
{
throw new AbpException("The ProductDiscountElectionModel is in an incorrect state.");
}
return Schemes.MaxBy(x => x.TotalDiscountAmount);
}
public bool TryEnqueue(CandidateProductDiscounts candidateDiscounts)
{
if (candidateDiscounts.Items.Count > MaxCandidates)
{
throw new AbpException(
$"The ProductDiscountElectionModel cannot handle a CandidateProductDiscounts with the number of candidate discounts > {MaxCandidates}.");
}
CurrentDepth++;
if (CurrentDepth > MaxDepth || CurrentDepth < 0)
{
throw new AbpException("The ProductDiscountElectionModel's search exceeded maximum depth.");
}
if (!UsedCombinations.Add(candidateDiscounts.Items.Select(pair => pair.Key).JoinAsString(";")))
{
return false;
}
CandidateDiscountsQueue.Enqueue(candidateDiscounts);
return true;
}
public CandidateProductDiscounts Dequeue()
{
return CandidateDiscountsQueue.Dequeue();
}
}

98
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountResolver.cs

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace EasyAbp.EShop.Products.Products;
public class ProductDiscountResolver : IProductDiscountResolver, ITransientDependency
{
public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
public virtual async Task<DiscountForProductModels> ResolveAsync(IProduct product, IProductSku productSku,
decimal priceFromPriceProvider, DateTime now)
{
var context = new ProductDiscountContext(now, product, productSku, priceFromPriceProvider);
foreach (var provider in LazyServiceProvider.LazyGetService<IEnumerable<IProductDiscountProvider>>()
.OrderBy(x => x.EffectOrder))
{
await provider.DiscountAsync(context);
}
if (context.CandidateProductDiscounts.IsNullOrEmpty())
{
return new DiscountForProductModels(null, context.OrderDiscountPreviews);
}
var electionModel =
new ProductDiscountElectionModel(context.Product, context.ProductSku, context.PriceFromPriceProvider);
electionModel.TryEnqueue(new CandidateProductDiscounts(context.CandidateProductDiscounts));
while (!electionModel.Done)
{
await EvolveAsync(electionModel, now);
}
return new DiscountForProductModels(electionModel.GetBestScheme().Discounts, context.OrderDiscountPreviews);
}
protected virtual Task EvolveAsync(ProductDiscountElectionModel electionModel, DateTime now)
{
if (electionModel.Done)
{
return Task.CompletedTask;
}
var candidateDiscounts = electionModel.Dequeue();
var usedEffectGroup = new HashSet<string>();
var usedDiscountNameKeyPairs = new HashSet<(string, string)>();
var currentPrice = electionModel.PriceFromPriceProvider;
var productDiscountInfoModels = new List<ProductDiscountInfoModel>();
foreach (var (index, candidate) in candidateDiscounts.Items)
{
var discount = new ProductDiscountInfoModel(candidate, 0m, false);
productDiscountInfoModels.Add(discount);
if (candidate.FromTime.HasValue && now < candidate.FromTime ||
candidate.ToTime.HasValue && now > candidate.ToTime)
{
continue;
}
if (!usedDiscountNameKeyPairs.Add(new ValueTuple<string, string>(candidate.Name, candidate.Key)))
{
continue;
}
if (!candidate.EffectGroup.IsNullOrEmpty() && !usedEffectGroup.Add(candidate.EffectGroup))
{
continue;
}
if (candidateDiscounts.Items.Values.Any(x => x.EffectGroup == candidate.EffectGroup && x != candidate))
{
// remove the current discount and try again to find a better scheme
var newCandidateDiscounts = (CandidateProductDiscounts)candidateDiscounts.Clone();
newCandidateDiscounts.Items.Remove(index);
electionModel.TryEnqueue(newCandidateDiscounts);
}
discount.InEffect = true;
discount.DiscountedAmount =
candidate.CalculateDiscountAmount(currentPrice, electionModel.ProductSku.Currency);
currentPrice -= discount.DiscountedAmount;
}
electionModel.Schemes.Add(new ProductDiscountsSchemeModel(productDiscountInfoModels));
return Task.CompletedTask;
}
}

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

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Linq;
namespace EasyAbp.EShop.Products.Products;
public class ProductDiscountsSchemeModel
{
public List<ProductDiscountInfoModel> Discounts { get; }
public decimal TotalDiscountAmount => Discounts.Where(x => x.InEffect).Sum(x => x.DiscountedAmount);
public ProductDiscountsSchemeModel(List<ProductDiscountInfoModel> discounts)
{
Discounts = discounts;
}
}

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

@ -16,6 +16,7 @@ namespace EasyAbp.EShop.Products.Products
{ {
private readonly IProductRepository _productRepository; private readonly IProductRepository _productRepository;
private readonly IProductPriceProvider _productPriceProvider; private readonly IProductPriceProvider _productPriceProvider;
private readonly IProductDiscountResolver _productDiscountResolver;
private readonly IProductDetailRepository _productDetailRepository; private readonly IProductDetailRepository _productDetailRepository;
private readonly IProductCategoryRepository _productCategoryRepository; private readonly IProductCategoryRepository _productCategoryRepository;
private readonly IProductInventoryProviderResolver _productInventoryProviderResolver; private readonly IProductInventoryProviderResolver _productInventoryProviderResolver;
@ -24,6 +25,7 @@ namespace EasyAbp.EShop.Products.Products
public ProductManager( public ProductManager(
IProductRepository productRepository, IProductRepository productRepository,
IProductPriceProvider productPriceProvider, IProductPriceProvider productPriceProvider,
IProductDiscountResolver productDiscountResolver,
IProductDetailRepository productDetailRepository, IProductDetailRepository productDetailRepository,
IProductCategoryRepository productCategoryRepository, IProductCategoryRepository productCategoryRepository,
IProductInventoryProviderResolver productInventoryProviderResolver, IProductInventoryProviderResolver productInventoryProviderResolver,
@ -31,6 +33,7 @@ namespace EasyAbp.EShop.Products.Products
{ {
_productRepository = productRepository; _productRepository = productRepository;
_productPriceProvider = productPriceProvider; _productPriceProvider = productPriceProvider;
_productDiscountResolver = productDiscountResolver;
_productDetailRepository = productDetailRepository; _productDetailRepository = productDetailRepository;
_productCategoryRepository = productCategoryRepository; _productCategoryRepository = productCategoryRepository;
_productInventoryProviderResolver = productInventoryProviderResolver; _productInventoryProviderResolver = productInventoryProviderResolver;
@ -268,20 +271,15 @@ namespace EasyAbp.EShop.Products.Products
.TryReduceInventoryAsync(model, quantity, increaseSold, isFlashSale); .TryReduceInventoryAsync(model, quantity, increaseSold, isFlashSale);
} }
public virtual async Task<ProductPriceModel> GetRealPriceAsync(Product product, ProductSku productSku, public virtual async Task<RealTimePriceInfoModel> GetRealTimePriceAsync(Product product, ProductSku productSku,
DateTime now) DateTime now)
{ {
var price = await _productPriceProvider.GetPriceAsync(product, productSku); var priceFromPriceProvider = await _productPriceProvider.GetPriceAsync(product, productSku);
var context = new ProductDiscountContext(product, productSku, price, now); var discounts =
await _productDiscountResolver.ResolveAsync(product, productSku, priceFromPriceProvider, now);
foreach (var provider in LazyServiceProvider.LazyGetService<IEnumerable<IProductDiscountProvider>>() return new RealTimePriceInfoModel(priceFromPriceProvider, discounts);
.OrderBy(x => x.EffectOrder))
{
await provider.DiscountAsync(context);
}
return context.ToFinalProductPriceModel();
} }
} }
} }

35
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/DemoProductDiscountProvider.cs

@ -25,25 +25,34 @@ public class DemoProductDiscountProvider : IProductDiscountProvider
return Task.CompletedTask; return Task.CompletedTask;
} }
var productDiscountInfoModels = new List<ProductDiscountInfoModel> var candidates = new List<CandidateProductDiscountInfoModel>
{ {
// These should take effect: // These should take effect:
new(null, "DemoDiscount", "1", "Demo Discount 1", 0.10m, null, null), new(null, "DemoDiscount", "1", "Demo Discount 1",
new(null, "DemoDiscount", "2", "Demo Discount 2", 0.10m, _clock.Now.AddDays(-1), null), new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, null),
new(null, "DemoDiscount", "3", "Demo Discount 3", 0.10m, null, _clock.Now.AddDays(1)), new(null, "DemoDiscount", "2", "Demo Discount 2",
new(null, "DemoDiscount", "4", "Demo Discount 4", 0.10m, _clock.Now.AddDays(-1), _clock.Now.AddDays(1)), new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), _clock.Now.AddDays(-1), null),
new(null, "DemoDiscount", "3", "Demo Discount 3",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, _clock.Now.AddDays(1)),
new(null, "DemoDiscount", "4", "Demo Discount 4",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), _clock.Now.AddDays(-1), _clock.Now.AddDays(1)),
// These should not take effect: // These should not take effect:
new(null, "DemoDiscount", "5", "Demo Discount 5", 0.10m, null, _clock.Now.AddDays(-1)), new(null, "DemoDiscount", "5", "Demo Discount 5",
new(null, "DemoDiscount", "6", "Demo Discount 6", 0.10m, _clock.Now.AddDays(1), null), new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, _clock.Now.AddDays(-1)),
new(null, "DemoDiscount", "7", "Demo Discount 7", 0.10m, _clock.Now.AddDays(1), _clock.Now.AddDays(2)), new(null, "DemoDiscount", "6", "Demo Discount 6",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), _clock.Now.AddDays(1), null),
new(null, "DemoDiscount", "7", "Demo Discount 7",
new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), _clock.Now.AddDays(1), _clock.Now.AddDays(2)),
// Only the one with the highest discount amount should take effect: // Only the one with the highest discount amount should take effect:
new("A", "DemoDiscount", "8", "Demo Discount 8", 0.10m, null, null), new("A", "DemoDiscount", "8", "Demo Discount 8",
new("A", "DemoDiscount", "9", "Demo Discount 9", 0.01m, null, null), new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, null),
new("A", "DemoDiscount", "9", "Demo Discount 9",
new DynamicDiscountAmountModel("USD", 0.01m, 0m, null), null, null),
}; };
foreach (var model in productDiscountInfoModels) foreach (var model in candidates)
{ {
context.AddOrUpdateProductDiscount(model); context.CandidateProductDiscounts.Add(model);
} }
var orderDiscountPreviewInfoModels = new List<OrderDiscountPreviewInfoModel> var orderDiscountPreviewInfoModels = new List<OrderDiscountPreviewInfoModel>
@ -54,7 +63,7 @@ public class DemoProductDiscountProvider : IProductDiscountProvider
foreach (var model in orderDiscountPreviewInfoModels) foreach (var model in orderDiscountPreviewInfoModels)
{ {
context.AddOrUpdateOrderDiscountPreview(model); context.OrderDiscountPreviews.Add(model);
} }
return Task.CompletedTask; return Task.CompletedTask;

14
modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp.EShop.Stores.Web.Shared.csproj

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\EasyAbp.EShop.Stores.Application.Contracts\EasyAbp.EShop.Stores.Application.Contracts.csproj" />
</ItemGroup>
</Project>

10
modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp/EShop/Stores/EShopStoresWebSharedModule.cs

@ -0,0 +1,10 @@
using Volo.Abp.Modularity;
namespace EasyAbp.EShop.Stores;
[DependsOn(
typeof(EShopStoresApplicationContractsModule)
)]
public class EShopStoresWebSharedModule : AbpModule
{
}

9
modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp/EShop/Stores/Stores/IUiDefaultStoreProvider.cs

@ -0,0 +1,9 @@
using System.Threading.Tasks;
using EasyAbp.EShop.Stores.Stores.Dtos;
namespace EasyAbp.EShop.Stores.Stores;
public interface IUiDefaultStoreProvider
{
Task<StoreDto> GetAsync();
}

22
modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp/EShop/Stores/Stores/UiDefaultStoreProvider.cs

@ -0,0 +1,22 @@
using System.Threading.Tasks;
using EasyAbp.EShop.Stores.Stores.Dtos;
using Volo.Abp.DependencyInjection;
namespace EasyAbp.EShop.Stores.Stores;
public class UiDefaultStoreProvider : IUiDefaultStoreProvider, IScopedDependency
{
private StoreDto CachedDefaultStore { get; set; }
protected IStoreAppService StoreAppService { get; }
public UiDefaultStoreProvider(IStoreAppService storeAppService)
{
StoreAppService = storeAppService;
}
public virtual async Task<StoreDto> GetAsync()
{
return CachedDefaultStore ??= await StoreAppService.GetDefaultAsync();
}
}

3
modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/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
modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/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>

2
modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/EShopStoresWebModule.cs

@ -14,7 +14,7 @@ using Volo.Abp.VirtualFileSystem;
namespace EasyAbp.EShop.Stores.Web namespace EasyAbp.EShop.Stores.Web
{ {
[DependsOn( [DependsOn(
typeof(EShopStoresApplicationContractsModule), typeof(EShopStoresWebSharedModule),
typeof(AbpAspNetCoreMvcUiThemeSharedModule), typeof(AbpAspNetCoreMvcUiThemeSharedModule),
typeof(AbpAutoMapperModule), typeof(AbpAutoMapperModule),
typeof(AbpIdentityApplicationContractsModule), typeof(AbpIdentityApplicationContractsModule),

2
modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/EasyAbp.EShop.Stores.Web.csproj

@ -19,7 +19,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\EasyAbp.EShop.Stores.Application.Contracts\EasyAbp.EShop.Stores.Application.Contracts.csproj" /> <ProjectReference Include="..\EasyAbp.EShop.Stores.Web.Shared\EasyAbp.EShop.Stores.Web.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

4
modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/Menus/StoresMenuContributor.cs

@ -40,9 +40,9 @@ namespace EasyAbp.EShop.Stores.Web.Menus
if (await context.IsGrantedAsync(StoresPermissions.Transaction.Default)) if (await context.IsGrantedAsync(StoresPermissions.Transaction.Default))
{ {
var storeAppService = context.ServiceProvider.GetRequiredService<IStoreAppService>(); var uiDefaultStoreProvider = context.ServiceProvider.GetRequiredService<IUiDefaultStoreProvider>();
var defaultStore = (await storeAppService.GetDefaultAsync())?.Id; var defaultStore = (await uiDefaultStoreProvider.GetAsync())?.Id;
storeManagementMenuItem.AddItem( storeManagementMenuItem.AddItem(
new ApplicationMenuItem(StoresMenus.Transaction, l["Menu:Transaction"], "/EShop/Stores/Transactions/Transaction?storeId=" + defaultStore)); new ApplicationMenuItem(StoresMenus.Transaction, l["Menu:Transaction"], "/EShop/Stores/Transactions/Transaction?storeId=" + defaultStore));

5
plugins/Coupons/src/EasyAbp.EShop.Orders.Plugins.Coupons/EasyAbp/EShop/Orders/Plugins/Coupons/OrderDiscount/CouponOrderDiscountProvider.cs

@ -105,7 +105,7 @@ namespace EasyAbp.EShop.Orders.Plugins.Coupons.OrderDiscount
? couponTemplate.DiscountAmount * ? couponTemplate.DiscountAmount *
Math.Floor(totalOrderLineActualTotalPrice.Amount / couponTemplate.ConditionAmount) Math.Floor(totalOrderLineActualTotalPrice.Amount / couponTemplate.ConditionAmount)
: couponTemplate.DiscountAmount, : couponTemplate.DiscountAmount,
nodaCurrency); nodaCurrency, MidpointRounding.ToZero);
if (totalDiscountedAmount > totalOrderLineActualTotalPrice) if (totalDiscountedAmount > totalOrderLineActualTotalPrice)
{ {
@ -116,7 +116,8 @@ namespace EasyAbp.EShop.Orders.Plugins.Coupons.OrderDiscount
var model = new OrderDiscountInfoModel(orderLinesInScope.Select(x => x.Id).ToList(), var model = new OrderDiscountInfoModel(orderLinesInScope.Select(x => x.Id).ToList(),
OrderDiscountEffectGroup, OrderDiscountName, coupon.Id.ToString(), couponTemplate.DisplayName, OrderDiscountEffectGroup, OrderDiscountName, coupon.Id.ToString(), couponTemplate.DisplayName,
totalDiscountedAmount.Amount); new DynamicDiscountAmountModel(context.Order.Currency, totalDiscountedAmount.Amount, 0m, null));
// todo: discount rate for coupons.
return Task.FromResult(model); return Task.FromResult(model);
} }

Loading…
Cancel
Save