diff --git a/EShop.sln b/EShop.sln index 15b9023c..0125a0b3 100644 --- a/EShop.sln +++ b/EShop.sln @@ -79,6 +79,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyAbp.EShop.Stores.MongoD 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}" 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}" 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}" @@ -607,6 +609,10 @@ Global {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.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.Build.0 = Debug|Any CPU {F11833F9-83D7-44C6-BE12-2402DFF85E54}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1269,6 +1275,7 @@ Global {15A67389-F23B-472C-8D44-1E294C18558B} = {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} + {0102861C-9979-496A-8104-EB33F655983E} = {8CB54A0E-BAAE-4D10-81B5-6F902A7950EB} {83C515D3-9811-4428-9401-17B1AF5406EF} = {7FA8569F-8D62-4C13-9C8A-98DADA1F09A0} {F11833F9-83D7-44C6-BE12-2402DFF85E54} = {BE3E0AEA-37C4-4ED9-B569-444EDE907905} {911CAA20-A5CA-4FFC-B7F2-F5B5B485EACA} = {BE3E0AEA-37C4-4ED9-B569-444EDE907905} diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountContext.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountContext.cs index b6d0413b..b528e533 100644 --- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountContext.cs +++ b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountContext.cs @@ -1,56 +1,33 @@ using System; using System.Collections.Generic; -using System.Linq; using EasyAbp.EShop.Products.Products; +using JetBrains.Annotations; +using Volo.Abp; namespace EasyAbp.EShop.Orders.Orders; +[Serializable] public class OrderDiscountContext { + public DateTime Now { get; } + public IOrder Order { get; } public Dictionary ProductDict { get; } public List CandidateDiscounts { get; } - public OrderDiscountContext(IOrder order, Dictionary productDict) + public OrderDiscountContext(DateTime now, IOrder order, Dictionary productDict, + List candidateDiscounts = null) { - Order = order; + Now = now; + Order = Check.NotNull(order, nameof(order)); ProductDict = productDict ?? new Dictionary(); + CandidateDiscounts = candidateDiscounts ?? new List(); } - public List GetEffectDiscounts() + public OrderDiscountInfoModel FindCandidateDiscount([NotNull] string name, [CanBeNull] string key) { - var effectDiscounts = new List(); - - 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>(); - - foreach (var grouping in CandidateDiscounts.Where(x => !x.EffectGroup.IsNullOrEmpty()) - .GroupBy(x => x.EffectGroup)) - { - var effectGroup = grouping.Key; - affectedOrderLineIdsInEffectGroup[effectGroup] = new List(); - - // 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; + return CandidateDiscounts.Find(x => x.Name == name && x.Key == key); } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountInfoModel.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountInfoModel.cs index 82b38328..6d186f57 100644 --- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountInfoModel.cs +++ b/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 EasyAbp.EShop.Products.Products; using JetBrains.Annotations; +using Volo.Abp; namespace EasyAbp.EShop.Orders.Orders; -public class OrderDiscountInfoModel : IDiscountInfo +public class OrderDiscountInfoModel : IDiscountInfo, IHasDynamicDiscountAmount { public string EffectGroup { get; set; } @@ -17,25 +18,21 @@ public class OrderDiscountInfoModel : IDiscountInfo public List AffectedOrderLineIds { get; set; } = new(); - public decimal DiscountedAmount { get; set; } + public DynamicDiscountAmountModel DynamicDiscountAmount { get; set; } public OrderDiscountInfoModel() { } public OrderDiscountInfoModel(List 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(); EffectGroup = effectGroup; Name = name; Key = key; DisplayName = displayName; - DiscountedAmount = discountedAmount; + DynamicDiscountAmount = Check.NotNull(dynamicDiscountAmount, nameof(dynamicDiscountAmount)); } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/CandidateOrderDiscounts.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/CandidateOrderDiscounts.cs new file mode 100644 index 00000000..d80e8bc8 --- /dev/null +++ b/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 Items { get; } + + public CandidateOrderDiscounts(List candidates) + { + Items = new Dictionary(); + + foreach (var candidate in candidates) + { + Items.Add(Items.Count, candidate); + } + } + + private CandidateOrderDiscounts(Dictionary items) + { + Items = items ?? new Dictionary(); + } + + public object Clone() + { + return new CandidateOrderDiscounts(Items.ToDictionary(x => x.Key, x => x.Value)); + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/HasDynamicDiscountAmountExtensions.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/HasDynamicDiscountAmountExtensions.cs new file mode 100644 index 00000000..a3460086 --- /dev/null +++ b/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 +{ + /// + /// Calculate the real-time discount amount based on the current price. + /// The calculated discount amount has been formatted with NodaMoney and MidpointRounding.ToZero. + /// + 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; + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderDiscountDistributor.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderDiscountDistributor.cs new file mode 100644 index 00000000..1d196908 --- /dev/null +++ b/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 DistributeAsync(IOrder order, Dictionary currentPrices, + OrderDiscountInfoModel discount); +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderDiscountResolver.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderDiscountResolver.cs new file mode 100644 index 00000000..481fdfa4 --- /dev/null +++ b/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> ResolveAsync(Order order, Dictionary productDict); +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/NewOrderGenerator.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/NewOrderGenerator.cs index 8ac1b257..0fe08eb0 100644 --- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/NewOrderGenerator.cs +++ b/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 Volo.Abp; using Volo.Abp.Auditing; -using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Services; using Volo.Abp.ObjectExtending; using Volo.Abp.Settings; @@ -18,17 +17,20 @@ namespace EasyAbp.EShop.Orders.Orders { private readonly ISettingProvider _settingProvider; private readonly IOrderNumberGenerator _orderNumberGenerator; + private readonly IOrderDiscountResolver _orderDiscountResolver; private readonly IProductSkuDescriptionProvider _productSkuDescriptionProvider; private readonly IEnumerable _orderLinePriceOverriders; public NewOrderGenerator( ISettingProvider settingProvider, IOrderNumberGenerator orderNumberGenerator, + IOrderDiscountResolver orderDiscountResolver, IProductSkuDescriptionProvider productSkuDescriptionProvider, IEnumerable orderLinePriceOverriders) { _settingProvider = settingProvider; _orderNumberGenerator = orderNumberGenerator; + _orderDiscountResolver = orderDiscountResolver; _productSkuDescriptionProvider = productSkuDescriptionProvider; _orderLinePriceOverriders = orderLinePriceOverriders; } @@ -107,19 +109,11 @@ namespace EasyAbp.EShop.Orders.Orders protected virtual async Task DiscountOrderAsync(Order order, Dictionary productDict) { - var context = new OrderDiscountContext(order, productDict); + var distributions = await _orderDiscountResolver.ResolveAsync(order, productDict); - foreach (var provider in LazyServiceProvider.LazyGetService>() - .OrderBy(x => x.EffectOrder)) + foreach (var distribution in distributions) { - await provider.DiscountAsync(context); - } - - var effectDiscounts = context.GetEffectDiscounts(); - - foreach (var discount in effectDiscounts) - { - order.AddDiscounts(discount); + order.AddDiscounts(distribution); } } diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/Order.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/Order.cs index 9c1aa3d3..53a168f4 100644 --- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/Order.cs +++ b/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); } - public void AddDiscounts(OrderDiscountInfoModel infoModel) + public void AddDiscounts(OrderDiscountDistributionModel model) { - var affectedOrderLines = infoModel.AffectedOrderLineIds - .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(); - - foreach (var orderLineId in infoModel.AffectedOrderLineIds) + foreach (var (orderLineId, discountAmount) in model.Distributions) { var orderLine = OrderLines.Single(x => x.Id == orderLineId); - var orderLineActualTotalPrice = new Money(orderLine.ActualTotalPrice, Currency); - 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); + orderLine.AddDiscount(discountAmount); - TotalDiscount += orderLineDiscountedAmount.Amount; - ActualTotalPrice -= orderLineDiscountedAmount.Amount; + TotalDiscount += discountAmount; + ActualTotalPrice -= discountAmount; if (ActualTotalPrice < decimal.Zero) { @@ -253,14 +205,16 @@ namespace EasyAbp.EShop.Orders.Orders } if (OrderDiscounts.Any(x => - x.OrderLineId == affectedOrderLine.Id && x.Name == infoModel.Name && - x.Key == infoModel.Key)) + x.OrderLineId == orderLineId && x.Name == model.DiscountInfoModel.Name && + 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, - infoModel.Name, infoModel.Key, infoModel.DisplayName, orderLineDiscountedAmount.Amount); + var orderDiscount = new OrderDiscount(Id, orderLineId, model.DiscountInfoModel.EffectGroup, + model.DiscountInfoModel.Name, model.DiscountInfoModel.Key, model.DiscountInfoModel.DisplayName, + discountAmount); OrderDiscounts.Add(orderDiscount); } diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountDistributionModel.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountDistributionModel.cs new file mode 100644 index 00000000..17cd2260 --- /dev/null +++ b/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; } + + /// + /// OrderLine to discount amount mapping. + /// + public Dictionary Distributions { get; set; } + + public OrderDiscountDistributionModel(OrderDiscountInfoModel discountInfoModel, + Dictionary 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."); + } + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountDistributor.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountDistributor.cs new file mode 100644 index 00000000..fdfea633 --- /dev/null +++ b/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 DistributeAsync(IOrder order, + Dictionary 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(); + 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)); + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountElectionModel.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountElectionModel.cs new file mode 100644 index 00000000..18a57113 --- /dev/null +++ b/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 +{ + /// + /// The search will stop and throw an exception if the number of executions is greater than . + /// = Math.Pow(2, ) + /// + public static int MaxCandidates { get; set; } = 10; + + /// + /// The search will stop and throw an exception if the number of executions is greater than . + /// This is to prevent a dead loop. + /// + private static double MaxDepth { get; set; } = Math.Pow(2, MaxCandidates); + + /// + /// The search will stop and throw an exception if this value is greater than . + /// + public double CurrentDepth { get; private set; } + + public IOrder Order { get; } + + public Dictionary ProductDict { get; } + + public List Schemes { get; } = new(); + + public bool Done => Schemes.Any() && !CandidateDiscountsQueue.Any(); + + private Queue CandidateDiscountsQueue { get; } = new(); + + private HashSet UsedCombinations { get; } = new(); + + public OrderDiscountElectionModel(IOrder order, Dictionary 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(); + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountResolver.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountResolver.cs new file mode 100644 index 00000000..34f49eb7 --- /dev/null +++ b/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(); + + public virtual async Task> ResolveAsync(Order order, + Dictionary productDict) + { + var context = new OrderDiscountContext(order.CreationTime, order, productDict); + + foreach (var provider in LazyServiceProvider.LazyGetService>() + .OrderBy(x => x.EffectOrder)) + { + await provider.DiscountAsync(context); + } + + if (context.CandidateDiscounts.IsNullOrEmpty()) + { + return new List(); + } + + 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>(); + var usedDiscountNameKeyPairs = new HashSet<(string, string)>(); + + var currentPrices = + new Dictionary(order.OrderLines.ToDictionary(x => x, x => x.UnitPrice)); + + var distributionModels = new List(); + + foreach (var (index, discount) in candidateDiscounts.Items) + { + if (!usedDiscountNameKeyPairs.Add(new ValueTuple(discount.Name, discount.Key))) + { + continue; + } + + if (!discount.EffectGroup.IsNullOrEmpty()) + { + var affectedOrderLineIds = + affectedOrderLineIdsInEffectGroup.GetOrAdd(discount.EffectGroup!, () => new List()); + + 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)); + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountsSchemeModel.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscountsSchemeModel.cs new file mode 100644 index 00000000..232a8e82 --- /dev/null +++ b/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 Distributions { get; } + + public decimal TotalDiscountAmount => Distributions.SelectMany(x => x.Distributions).Sum(x => x.Value); + + public OrderDiscountsSchemeModel(List distributions) + { + Distributions = distributions; + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/EShopOrdersWebModule.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/EShopOrdersWebModule.cs index d652dd5b..3e6c1b02 100644 --- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/EShopOrdersWebModule.cs +++ b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/EShopOrdersWebModule.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using EasyAbp.EShop.Orders.Localization; using EasyAbp.EShop.Orders.Web.Menus; +using EasyAbp.EShop.Stores; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AutoMapper; @@ -13,6 +14,7 @@ namespace EasyAbp.EShop.Orders.Web { [DependsOn( typeof(EShopOrdersApplicationContractsModule), + typeof(EShopStoresWebSharedModule), typeof(AbpAspNetCoreMvcUiThemeSharedModule), typeof(AbpAutoMapperModule) )] diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/EasyAbp.EShop.Orders.Web.csproj b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/EasyAbp.EShop.Orders.Web.csproj index c2e2c456..554e79e0 100644 --- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/EasyAbp.EShop.Orders.Web.csproj +++ b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/EasyAbp.EShop.Orders.Web.csproj @@ -18,7 +18,7 @@ - + diff --git a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/Menus/OrdersMenuContributor.cs b/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/Menus/OrdersMenuContributor.cs index a31277c7..a6f6f6aa 100644 --- a/modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Web/Menus/OrdersMenuContributor.cs +++ b/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)) { - var storeAppService = context.ServiceProvider.GetRequiredService(); + var uiDefaultStoreProvider = context.ServiceProvider.GetRequiredService(); - var defaultStore = (await storeAppService.GetDefaultAsync())?.Id; + var defaultStore = (await uiDefaultStoreProvider.GetAsync())?.Id; orderManagementMenuItem.AddItem( new ApplicationMenuItem(OrdersMenus.Order, l["Menu:Order"], "/EShop/Orders/Orders/Order?storeId=" + defaultStore) diff --git a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/DemoOrderDiscountProvider.cs b/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/DemoOrderDiscountProvider.cs index 5d574d4c..a53ed2d4 100644 --- a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/DemoOrderDiscountProvider.cs +++ b/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(); - return Task.FromResult(new List + var models = new List { - new(new List { firstOrderLine.Id }, null, "DemoDiscount1", "1", "Demo Discount 1", 0.01m), - new(new List { firstOrderLine.Id }, "A", "DemoDiscount2", "2", "Demo Discount 2", 0.1m), - new(new List { firstOrderLine.Id }, "A", "DemoDiscount3", "3", "Demo Discount 3", 0.05m), - }); + new(new List { firstOrderLine.Id }, null, "DemoDiscount1", "1", "Demo Discount 1", + new DynamicDiscountAmountModel("USD", 0.01m, 0m, null)), + new(new List { firstOrderLine.Id }, "A", "DemoDiscount2", "2", "Demo Discount 2", + new DynamicDiscountAmountModel("USD", 0.1m, 0m, null)), + new(new List { 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; } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDiscountDistributorTests.cs b/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDiscountDistributorTests.cs new file mode 100644 index 00000000..0a88fc75 --- /dev/null +++ b/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(); + } + + [Fact] + public async Task Should_Fall_Back_Discount_Amount_Using_MidpointRounding_ToZero() + { + var orderGenerator = GetRequiredService(); + + var createOrderInfoModel = new CreateOrderInfoModel(OrderTestData.Store1Id, null, + new List + { + new(OrderTestData.Product1Id, OrderTestData.ProductSku1Id, 1), + } + ); + + var order = await orderGenerator.GenerateAsync(Guid.NewGuid(), createOrderInfoModel, + new Dictionary + { + { + OrderTestData.Product1Id, new ProductEto + { + Id = OrderTestData.Product1Id, + ProductSkus = new List + { + new() + { + Id = OrderTestData.ProductSku1Id, + AttributeOptionIds = new List(), + Price = 0.99m, + Currency = "USD", + OrderMinQuantity = 1, + OrderMaxQuantity = 100, + } + } + } + } + }, new Dictionary()); + + 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(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(); + + var createOrderInfoModel = new CreateOrderInfoModel(OrderTestData.Store1Id, null, + new List + { + new(OrderTestData.Product1Id, OrderTestData.ProductSku1Id, 1), + new(OrderTestData.Product1Id, OrderTestData.ProductSku2Id, 1), + } + ); + + var order = await orderGenerator.GenerateAsync(Guid.NewGuid(), createOrderInfoModel, + new Dictionary + { + { + OrderTestData.Product1Id, new ProductEto + { + Id = OrderTestData.Product1Id, + ProductSkus = new List + { + new() + { + Id = OrderTestData.ProductSku1Id, + AttributeOptionIds = new List(), + Price = 1.7m, + Currency = "USD", + OrderMinQuantity = 1, + OrderMaxQuantity = 100, + }, + new() + { + Id = OrderTestData.ProductSku2Id, + AttributeOptionIds = new List(), + Price = 3.11m, + Currency = "USD", + OrderMinQuantity = 1, + OrderMaxQuantity = 100, + } + } + } + } + }, new Dictionary()); + + 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(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); + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs b/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs index 7e02a2b8..803ffc95 100644 --- a/modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs +++ b/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 { private Order Order1 { get; set; } + private readonly IOrderRepository _orderRepository; public OrderDomainTests() @@ -244,78 +245,5 @@ namespace EasyAbp.EShop.Orders.Orders order.SetReducedInventoryAfterPlacingTime(null); await Should.ThrowAsync(() => orderManager.CancelAsync(order, "my-reason")); } - - [Fact] - public async Task Should_Discount_Multi_OrderLine() - { - var orderGenerator = GetRequiredService(); - - var createOrderInfoModel = new CreateOrderInfoModel(OrderTestData.Store1Id, null, - new List - { - new(OrderTestData.Product1Id, OrderTestData.ProductSku1Id, 1), - new(OrderTestData.Product1Id, OrderTestData.ProductSku2Id, 1), - } - ); - - var order = await orderGenerator.GenerateAsync(Guid.NewGuid(), createOrderInfoModel, - new Dictionary - { - { - OrderTestData.Product1Id, new ProductEto - { - Id = OrderTestData.Product1Id, - ProductSkus = new List - { - new() - { - Id = OrderTestData.ProductSku1Id, - AttributeOptionIds = new List(), - Price = 1.7m, - Currency = "USD", - OrderMinQuantity = 1, - OrderMaxQuantity = 100, - }, - new() - { - Id = OrderTestData.ProductSku2Id, - AttributeOptionIds = new List(), - Price = 3.11m, - Currency = "USD", - OrderMinQuantity = 1, - OrderMaxQuantity = 100, - } - } - } - } - }, new Dictionary()); - - 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); - } } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/EShopPaymentsWebModule.cs b/modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/EShopPaymentsWebModule.cs index a0c0258f..15949109 100644 --- a/modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/EShopPaymentsWebModule.cs +++ b/modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/EShopPaymentsWebModule.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using EasyAbp.EShop.Payments.Localization; using EasyAbp.EShop.Payments.Web.Menus; +using EasyAbp.EShop.Stores; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AutoMapper; @@ -13,6 +14,7 @@ namespace EasyAbp.EShop.Payments.Web { [DependsOn( typeof(EShopPaymentsApplicationContractsModule), + typeof(EShopStoresWebSharedModule), typeof(AbpAspNetCoreMvcUiThemeSharedModule), typeof(AbpAutoMapperModule) )] diff --git a/modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/EasyAbp.EShop.Payments.Web.csproj b/modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/EasyAbp.EShop.Payments.Web.csproj index c6b40bed..44eda16e 100644 --- a/modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/EasyAbp.EShop.Payments.Web.csproj +++ b/modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/EasyAbp.EShop.Payments.Web.csproj @@ -17,7 +17,7 @@ - + diff --git a/modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/Menus/PaymentsMenuContributor.cs b/modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/Menus/PaymentsMenuContributor.cs index 0896b069..30d284c2 100644 --- a/modules/EasyAbp.EShop.Payments/src/EasyAbp.EShop.Payments.Web/Menus/PaymentsMenuContributor.cs +++ b/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 storeAppService = context.ServiceProvider.GetRequiredService(); + var uiDefaultStoreProvider = context.ServiceProvider.GetRequiredService(); - var defaultStore = (await storeAppService.GetDefaultAsync())?.Id; + var defaultStore = (await uiDefaultStoreProvider.GetAsync())?.Id; if (await context.IsGrantedAsync(PaymentsPermissions.Payments.Manage)) { diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs index c4cd5a65..0b85fe90 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs +++ b/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 priceDataModel = await _productManager.GetRealPriceAsync(product, productSku, now); + var realTimePriceInfoModel = await _productManager.GetRealTimePriceAsync(product, productSku, now); - productSkuDto.PriceWithoutDiscount = priceDataModel.PriceWithoutDiscount; - productSkuDto.Price = priceDataModel.GetDiscountedPrice(); - productSkuDto.ProductDiscounts = priceDataModel.ProductDiscounts; - productSkuDto.OrderDiscountPreviews = priceDataModel.OrderDiscountPreviews; + productSkuDto.PriceWithoutDiscount = realTimePriceInfoModel.PriceWithoutDiscount; + productSkuDto.Price = realTimePriceInfoModel.TotalDiscountedPrice; + productSkuDto.ProductDiscounts = realTimePriceInfoModel.Discounts.ProductDiscounts; + productSkuDto.OrderDiscountPreviews = realTimePriceInfoModel.Discounts.OrderDiscountPreviews; } if (productDto.ProductSkus.Count > 0) diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductViewAppService.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductViewAppService.cs index b45c14da..cb3a1899 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductViewAppService.cs +++ b/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) { var overrideProductDiscounts = false; - var priceDataModel = await _productManager.GetRealPriceAsync(product, productSku, now); - var discountedPrice = priceDataModel.GetDiscountedPrice(); + var realTimePrice = await _productManager.GetRealTimePriceAsync(product, productSku, now); + var discountedPrice = realTimePrice.TotalDiscountedPrice; if (min is null || discountedPrice < min.Value) { @@ -219,35 +219,39 @@ namespace EasyAbp.EShop.Products.Products 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, - model.Key, model.DisplayName, model.DiscountedAmount, model.FromTime, model.ToTime)); + discounts.ProductDiscounts.Add(discount); + } + 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, - model.Name, model.Key, model.DisplayName, model.FromTime, model.ToTime)); + discounts.OrderDiscountPreviews.Add(discount); } } } diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/CandidateProductDiscountInfoModel.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/CandidateProductDiscountInfoModel.cs new file mode 100644 index 00000000..16fd528e --- /dev/null +++ b/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)); + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DiscountForProductModels.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DiscountForProductModels.cs index 70986756..8c6d8532 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DiscountForProductModels.cs +++ b/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 OrderDiscountPreviews { get; set; } - public DiscountForProductModels() - { - ProductDiscounts = new List(); - OrderDiscountPreviews = new List(); - } - - public DiscountForProductModels(List productDiscounts, - List orderDiscountPreviews) + public DiscountForProductModels(List productDiscounts = null, + List orderDiscountPreviews = null) { ProductDiscounts = productDiscounts ?? new List(); OrderDiscountPreviews = orderDiscountPreviews ?? new List(); diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DynamicDiscountAmountModel.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DynamicDiscountAmountModel.cs new file mode 100644 index 00000000..6e14b0bc --- /dev/null +++ b/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; } + + /// + /// The absolute discount amount. + /// has a higher priority of effectiveness than . + /// And has a higher priority of effectiveness than . + /// + public decimal DiscountAmount { get; } + + /// + /// The discount rate. This means 10% off if you set it to 0.1. + /// has a higher priority of effectiveness than . + /// And has a higher priority of effectiveness than . + /// + public decimal DiscountRate { get; } + + /// + /// (todo: NOT YET IMPLEMENTED!) + /// The name of the runtime discount calculator. + /// has a higher priority of effectiveness than . + /// And has a higher priority of effectiveness than . + /// + [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; + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/HasDiscountsForProductExtensions.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/HasDiscountsForProductExtensions.cs deleted file mode 100644 index 89e7f57d..00000000 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/HasDiscountsForProductExtensions.cs +++ /dev/null @@ -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(); - - 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; - } - } - } - } - - /// - /// It returns a sum of the amount of the product discounts currently in effect. - /// - public static decimal GetDiscountedAmount(this IHasDiscountsForProduct hasDiscountsForProduct) - { - return hasDiscountsForProduct.ProductDiscounts.Where(x => x.InEffect == true).Sum(x => x.DiscountedAmount); - } - - /// - /// It returns the price minus the product discounts currently in effect. - /// - 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(); - } - } -} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IDiscountInfo.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IDiscountInfo.cs index 2d720807..6a7b88c0 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IDiscountInfo.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IDiscountInfo.cs @@ -7,14 +7,22 @@ public interface IDiscountInfo /// /// 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 ProductDiscounts, the Discount with the highest discounted amount will be applied. + /// For ProductDiscounts, the Discount with the highest discount amount will be applied. /// [CanBeNull] string EffectGroup { get; } + /// + /// If there is more than one Discount with the same and , + /// only the one with the highest discount amount will be applied. + /// [NotNull] string Name { get; } + /// + /// If there is more than one Discount with the same and , + /// only the one with the highest discount amount will be applied. + /// [CanBeNull] string Key { get; } diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasDynamicDiscountAmount.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasDynamicDiscountAmount.cs new file mode 100644 index 00000000..e34a8959 --- /dev/null +++ b/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; } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs index bc0535ec..e4f05456 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs +++ b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs @@ -1,91 +1,34 @@ using System; using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; +using Volo.Abp; namespace EasyAbp.EShop.Products.Products; +[Serializable] public class ProductDiscountContext { public DateTime Now { get; } + public decimal PriceFromPriceProvider { get; } + public IProduct Product { get; } public IProductSku ProductSku { get; } - public decimal PriceWithoutDiscount => PriceModel.PriceWithoutDiscount; - - public IReadOnlyList ProductDiscounts => PriceModel.ProductDiscounts; + public List CandidateProductDiscounts { get; } - public IReadOnlyList OrderDiscountPreviews => PriceModel.OrderDiscountPreviews; + public List OrderDiscountPreviews { get; } - private ProductPriceModel PriceModel { get; } - - public ProductDiscountContext(IProduct product, IProductSku productSku, decimal priceFromPriceProvider, - DateTime now) + public ProductDiscountContext(DateTime now, IProduct product, IProductSku productSku, + decimal priceFromPriceProvider, List candidateProductDiscounts = null, + List orderDiscountPreviews = null) { - Product = product; - ProductSku = productSku; - PriceModel = new ProductPriceModel(priceFromPriceProvider); Now = now; - } - - public ProductDiscountInfoModel FindProductDiscount([NotNull] string name, [CanBeNull] string key) - { - 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); + Product = Check.NotNull(product, nameof(product)); + ProductSku = Check.NotNull(productSku, nameof(productSku)); + PriceFromPriceProvider = priceFromPriceProvider; - PriceModel.FillEffectState(Now); - - 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); - } - - /// - /// It returns a sum of the amount of the product discounts currently in effect. - /// - 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); - } - - /// - /// It returns the price minus the product discounts currently in effect. - /// - public decimal GetDiscountedPrice(string excludingEffectGroup = null) - { - return PriceWithoutDiscount - GetDiscountedAmount(excludingEffectGroup); - } - - public ProductPriceModel ToFinalProductPriceModel() - { - return PriceModel; + CandidateProductDiscounts = candidateProductDiscounts ?? new List(); + OrderDiscountPreviews = orderDiscountPreviews ?? new List(); } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountInfoModel.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountInfoModel.cs index 5d6f9846..dbde1783 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountInfoModel.cs +++ b/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 bool? InEffect { get; set; } + public bool InEffect { get; set; } 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, - [CanBeNull] string displayName, decimal discountedAmount, DateTime? fromTime, DateTime? toTime, - bool? inEffect = null) : base(effectGroup, name, key, displayName, fromTime, toTime) + [CanBeNull] string displayName, decimal discountedAmount, DateTime? fromTime, DateTime? toTime, bool inEffect) : + base(effectGroup, name, key, displayName, fromTime, toTime) { - if (discountedAmount < decimal.Zero) + if (DiscountedAmount < decimal.Zero) { throw new DiscountAmountOverflowException(); } diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductPriceModel.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductPriceModel.cs deleted file mode 100644 index e5bb4f63..00000000 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductPriceModel.cs +++ /dev/null @@ -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 ProductDiscounts { get; } = new(); - - public List OrderDiscountPreviews { get; } = new(); - - public ProductPriceModel(decimal priceWithoutDiscount) - { - if (PriceWithoutDiscount < decimal.Zero) - { - throw new OverflowException(); - } - - PriceWithoutDiscount = priceWithoutDiscount; - } -} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/RealTimePriceInfoModel.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/RealTimePriceInfoModel.cs new file mode 100644 index 00000000..d9385121 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/CandidateProductDiscounts.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/CandidateProductDiscounts.cs new file mode 100644 index 00000000..63f60c9a --- /dev/null +++ b/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 Items { get; } + + public CandidateProductDiscounts(List candidates) + { + Items = new Dictionary(); + + foreach (var candidate in candidates) + { + Items.Add(Items.Count, candidate); + } + } + + private CandidateProductDiscounts(Dictionary items) + { + Items = items ?? new Dictionary(); + } + + public object Clone() + { + return new CandidateProductDiscounts(Items.ToDictionary(x => x.Key, x => x.Value)); + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/HasDynamicDiscountAmountExtensions.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/HasDynamicDiscountAmountExtensions.cs new file mode 100644 index 00000000..5aa77211 --- /dev/null +++ b/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 +{ + /// + /// Calculate the real-time discount amount based on the current price. + /// The calculated discount amount has been formatted with NodaMoney and MidpointRounding.ToZero. + /// + 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; + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductDiscountResolver.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductDiscountResolver.cs new file mode 100644 index 00000000..f8210c44 --- /dev/null +++ b/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 ResolveAsync(IProduct product, IProductSku productSku, + decimal priceFromPriceProvider, DateTime now); +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs index c6c9fbf0..fbed0cc9 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs +++ b/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 TryReduceInventoryAsync(Product product, ProductSku productSku, int quantity, bool increaseSold); - Task GetRealPriceAsync(Product product, ProductSku productSku, DateTime now); + Task GetRealTimePriceAsync(Product product, ProductSku productSku, DateTime now); } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountElectionModel.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountElectionModel.cs new file mode 100644 index 00000000..b00396ec --- /dev/null +++ b/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 +{ + /// + /// The search will stop and throw an exception if the number of executions is greater than . + /// = Math.Pow(2, ) + /// + public static int MaxCandidates { get; set; } = 10; + + /// + /// The search will stop and throw an exception if the number of executions is greater than . + /// This is to prevent a dead loop. + /// + private static double MaxDepth { get; set; } = Math.Pow(2, MaxCandidates); + + /// + /// The search will stop and throw an exception if this value is greater than . + /// + public double CurrentDepth { get; private set; } + + public IProduct Product { get; } + + public IProductSku ProductSku { get; } + + public decimal PriceFromPriceProvider { get; } + + public List Schemes { get; } = new(); + + public bool Done => Schemes.Any() && !CandidateDiscountsQueue.Any(); + + private Queue CandidateDiscountsQueue { get; } = new(); + + private HashSet 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(); + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountResolver.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountResolver.cs new file mode 100644 index 00000000..21b944da --- /dev/null +++ b/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 ResolveAsync(IProduct product, IProductSku productSku, + decimal priceFromPriceProvider, DateTime now) + { + var context = new ProductDiscountContext(now, product, productSku, priceFromPriceProvider); + + foreach (var provider in LazyServiceProvider.LazyGetService>() + .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(); + var usedDiscountNameKeyPairs = new HashSet<(string, string)>(); + + var currentPrice = electionModel.PriceFromPriceProvider; + + var productDiscountInfoModels = new List(); + + 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(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; + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountsSchemeModel.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountsSchemeModel.cs new file mode 100644 index 00000000..082e35c5 --- /dev/null +++ b/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 Discounts { get; } + + public decimal TotalDiscountAmount => Discounts.Where(x => x.InEffect).Sum(x => x.DiscountedAmount); + + public ProductDiscountsSchemeModel(List discounts) + { + Discounts = discounts; + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs b/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs index 6f8a2d93..61834a5b 100644 --- a/modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs +++ b/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 IProductPriceProvider _productPriceProvider; + private readonly IProductDiscountResolver _productDiscountResolver; private readonly IProductDetailRepository _productDetailRepository; private readonly IProductCategoryRepository _productCategoryRepository; private readonly IProductInventoryProviderResolver _productInventoryProviderResolver; @@ -24,6 +25,7 @@ namespace EasyAbp.EShop.Products.Products public ProductManager( IProductRepository productRepository, IProductPriceProvider productPriceProvider, + IProductDiscountResolver productDiscountResolver, IProductDetailRepository productDetailRepository, IProductCategoryRepository productCategoryRepository, IProductInventoryProviderResolver productInventoryProviderResolver, @@ -31,6 +33,7 @@ namespace EasyAbp.EShop.Products.Products { _productRepository = productRepository; _productPriceProvider = productPriceProvider; + _productDiscountResolver = productDiscountResolver; _productDetailRepository = productDetailRepository; _productCategoryRepository = productCategoryRepository; _productInventoryProviderResolver = productInventoryProviderResolver; @@ -268,20 +271,15 @@ namespace EasyAbp.EShop.Products.Products .TryReduceInventoryAsync(model, quantity, increaseSold, isFlashSale); } - public virtual async Task GetRealPriceAsync(Product product, ProductSku productSku, + public virtual async Task GetRealTimePriceAsync(Product product, ProductSku productSku, 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>() - .OrderBy(x => x.EffectOrder)) - { - await provider.DiscountAsync(context); - } - - return context.ToFinalProductPriceModel(); + return new RealTimePriceInfoModel(priceFromPriceProvider, discounts); } } } \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/DemoProductDiscountProvider.cs b/modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/DemoProductDiscountProvider.cs index 849ee28b..d5a6117a 100644 --- a/modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/DemoProductDiscountProvider.cs +++ b/modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/DemoProductDiscountProvider.cs @@ -25,25 +25,34 @@ public class DemoProductDiscountProvider : IProductDiscountProvider return Task.CompletedTask; } - var productDiscountInfoModels = new List + var candidates = new List { // These should take effect: - new(null, "DemoDiscount", "1", "Demo Discount 1", 0.10m, null, null), - new(null, "DemoDiscount", "2", "Demo Discount 2", 0.10m, _clock.Now.AddDays(-1), null), - new(null, "DemoDiscount", "3", "Demo Discount 3", 0.10m, null, _clock.Now.AddDays(1)), - new(null, "DemoDiscount", "4", "Demo Discount 4", 0.10m, _clock.Now.AddDays(-1), _clock.Now.AddDays(1)), + new(null, "DemoDiscount", "1", "Demo Discount 1", + new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, null), + new(null, "DemoDiscount", "2", "Demo Discount 2", + 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: - new(null, "DemoDiscount", "5", "Demo Discount 5", 0.10m, null, _clock.Now.AddDays(-1)), - new(null, "DemoDiscount", "6", "Demo Discount 6", 0.10m, _clock.Now.AddDays(1), null), - new(null, "DemoDiscount", "7", "Demo Discount 7", 0.10m, _clock.Now.AddDays(1), _clock.Now.AddDays(2)), + new(null, "DemoDiscount", "5", "Demo Discount 5", + new DynamicDiscountAmountModel("USD", 0.10m, 0m, null), null, _clock.Now.AddDays(-1)), + 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: - new("A", "DemoDiscount", "8", "Demo Discount 8", 0.10m, null, null), - new("A", "DemoDiscount", "9", "Demo Discount 9", 0.01m, null, null), + new("A", "DemoDiscount", "8", "Demo Discount 8", + 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 @@ -54,7 +63,7 @@ public class DemoProductDiscountProvider : IProductDiscountProvider foreach (var model in orderDiscountPreviewInfoModels) { - context.AddOrUpdateOrderDiscountPreview(model); + context.OrderDiscountPreviews.Add(model); } return Task.CompletedTask; diff --git a/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp.EShop.Stores.Web.Shared.csproj b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp.EShop.Stores.Web.Shared.csproj new file mode 100644 index 00000000..14b12dd6 --- /dev/null +++ b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp.EShop.Stores.Web.Shared.csproj @@ -0,0 +1,14 @@ + + + + + + netstandard2.0 + + + + + + + + diff --git a/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp/EShop/Stores/EShopStoresWebSharedModule.cs b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp/EShop/Stores/EShopStoresWebSharedModule.cs new file mode 100644 index 00000000..04ab0fef --- /dev/null +++ b/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 +{ +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp/EShop/Stores/Stores/IUiDefaultStoreProvider.cs b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp/EShop/Stores/Stores/IUiDefaultStoreProvider.cs new file mode 100644 index 00000000..6d60c9c2 --- /dev/null +++ b/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 GetAsync(); +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp/EShop/Stores/Stores/UiDefaultStoreProvider.cs b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/EasyAbp/EShop/Stores/Stores/UiDefaultStoreProvider.cs new file mode 100644 index 00000000..4735c91b --- /dev/null +++ b/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 GetAsync() + { + return CachedDefaultStore ??= await StoreAppService.GetDefaultAsync(); + } +} \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/FodyWeavers.xml b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/FodyWeavers.xml new file mode 100644 index 00000000..be0de3a9 --- /dev/null +++ b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/FodyWeavers.xsd b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/FodyWeavers.xsd new file mode 100644 index 00000000..3f3946e2 --- /dev/null +++ b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web.Shared/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/EShopStoresWebModule.cs b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/EShopStoresWebModule.cs index d687eb2a..9854520c 100644 --- a/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/EShopStoresWebModule.cs +++ b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/EShopStoresWebModule.cs @@ -14,7 +14,7 @@ using Volo.Abp.VirtualFileSystem; namespace EasyAbp.EShop.Stores.Web { [DependsOn( - typeof(EShopStoresApplicationContractsModule), + typeof(EShopStoresWebSharedModule), typeof(AbpAspNetCoreMvcUiThemeSharedModule), typeof(AbpAutoMapperModule), typeof(AbpIdentityApplicationContractsModule), diff --git a/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/EasyAbp.EShop.Stores.Web.csproj b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/EasyAbp.EShop.Stores.Web.csproj index 62a50120..704be19b 100644 --- a/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/EasyAbp.EShop.Stores.Web.csproj +++ b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/EasyAbp.EShop.Stores.Web.csproj @@ -19,7 +19,7 @@ - + diff --git a/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/Menus/StoresMenuContributor.cs b/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/Menus/StoresMenuContributor.cs index 4d4b4b92..c294d739 100644 --- a/modules/EasyAbp.EShop.Stores/src/EasyAbp.EShop.Stores.Web/Menus/StoresMenuContributor.cs +++ b/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)) { - var storeAppService = context.ServiceProvider.GetRequiredService(); + var uiDefaultStoreProvider = context.ServiceProvider.GetRequiredService(); - var defaultStore = (await storeAppService.GetDefaultAsync())?.Id; + var defaultStore = (await uiDefaultStoreProvider.GetAsync())?.Id; storeManagementMenuItem.AddItem( new ApplicationMenuItem(StoresMenus.Transaction, l["Menu:Transaction"], "/EShop/Stores/Transactions/Transaction?storeId=" + defaultStore)); diff --git a/plugins/Coupons/src/EasyAbp.EShop.Orders.Plugins.Coupons/EasyAbp/EShop/Orders/Plugins/Coupons/OrderDiscount/CouponOrderDiscountProvider.cs b/plugins/Coupons/src/EasyAbp.EShop.Orders.Plugins.Coupons/EasyAbp/EShop/Orders/Plugins/Coupons/OrderDiscount/CouponOrderDiscountProvider.cs index dca69d2d..767d9727 100644 --- a/plugins/Coupons/src/EasyAbp.EShop.Orders.Plugins.Coupons/EasyAbp/EShop/Orders/Plugins/Coupons/OrderDiscount/CouponOrderDiscountProvider.cs +++ b/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 * Math.Floor(totalOrderLineActualTotalPrice.Amount / couponTemplate.ConditionAmount) : couponTemplate.DiscountAmount, - nodaCurrency); + nodaCurrency, MidpointRounding.ToZero); if (totalDiscountedAmount > totalOrderLineActualTotalPrice) { @@ -116,7 +116,8 @@ namespace EasyAbp.EShop.Orders.Plugins.Coupons.OrderDiscount var model = new OrderDiscountInfoModel(orderLinesInScope.Select(x => x.Id).ToList(), 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); }