Browse Source

Merge pull request #254 from EasyAbp/effect-group

Introduce `EffectGroup` and refactor product/order discounting
pull/255/head
Super 3 years ago
committed by GitHub
parent
commit
95c005b2bd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/en.json
  2. 1
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hans.json
  3. 1
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hant.json
  4. 56
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountContext.cs
  5. 26
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Orders/OrderDiscountInfoModel.cs
  6. 9
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderDiscountProvider.cs
  7. 20
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/NewOrderGenerator.cs
  8. 80
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/Order.cs
  9. 10
      modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/OrderDiscount.cs
  10. 13
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/DemoOrderDiscountProvider.cs
  11. 2
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDiscountProviderTests.cs
  12. 77
      modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDomainTests.cs
  13. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ProductSkuDto.cs
  14. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductAppService.cs
  15. 22
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Application/EasyAbp/EShop/Products/Products/ProductViewAppService.cs
  16. 6
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DiscountForProductModels.cs
  17. 12
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DiscountInfoModel.cs
  18. 146
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/HasDiscountsForProductExtensions.cs
  19. 108
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/HasDiscountsInfoExtensions.cs
  20. 23
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IDiscountInfo.cs
  21. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasDiscountsForProduct.cs
  22. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasFullDiscountsForProduct.cs
  23. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IProductView.cs
  24. 33
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/OrderDiscountPreviewInfoModel.cs
  25. 91
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs
  26. 21
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductDiscountInfoModel.cs
  27. 23
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/ProductPriceModel.cs
  28. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/DefaultProductInventoryProvider.cs
  29. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductDiscountProvider.cs
  30. 2
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/IProductManager.cs
  31. 36
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/PriceDataModel.cs
  32. 19
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs
  33. 8
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductManager.cs
  34. 6
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductView.cs
  35. 6
      modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/EntityFrameworkCore/ValueMappings/EShopProductsEntityTypeBuilderExtensions.cs
  36. 49
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/DemoProductDiscountProvider.cs
  37. 14
      modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/ProductDiscountTests.cs
  38. 2
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/BasketItems/IProductData.cs
  39. 6
      plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.EntityFrameworkCore/EasyAbp/EShop/Plugins/Baskets/EntityFrameworkCore/ValueMappings/EShopProductsEntityTypeBuilderExtensions.cs
  40. 103
      plugins/Coupons/src/EasyAbp.EShop.Orders.Plugins.Coupons/EasyAbp/EShop/Orders/Plugins/Coupons/OrderDiscount/CouponOrderDiscountProvider.cs
  41. 2
      plugins/Coupons/test/EasyAbp.EShop.Plugins.Coupons.Application.Tests/EasyAbp.EShop.Plugins.Coupons.Application.Tests.csproj
  42. 2
      plugins/Coupons/test/EasyAbp.EShop.Plugins.Coupons.Domain.Tests/EasyAbp.EShop.Plugins.Coupons.Domain.Tests.csproj
  43. 2
      plugins/Coupons/test/EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore.Tests/EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore.Tests.csproj
  44. 2
      plugins/Coupons/test/EasyAbp.EShop.Plugins.Coupons.HttpApi.Client.ConsoleTestApp/EasyAbp.EShop.Plugins.Coupons.HttpApi.Client.ConsoleTestApp.csproj
  45. 2
      plugins/Coupons/test/EasyAbp.EShop.Plugins.Coupons.MongoDB.Tests/EasyAbp.EShop.Plugins.Coupons.MongoDB.Tests.csproj
  46. 2
      plugins/Coupons/test/EasyAbp.EShop.Plugins.Coupons.TestBase/EasyAbp.EShop.Plugins.Coupons.TestBase.csproj
  47. 6424
      samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/20230412195849_AddedEffectGroup.Designer.cs
  48. 28
      samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/20230412195849_AddedEffectGroup.cs
  49. 15
      samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/EShopSampleDbContextModelSnapshot.cs

1
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/en.json

@ -48,6 +48,7 @@
"OrderDiscount": "Order discount",
"OrderDiscountOrderId": "Order ID",
"OrderDiscountOrderLineId": "Order line ID",
"OrderDiscountEffectGroup": "Effect group",
"OrderDiscountName": "Name",
"OrderDiscountKey": "Key",
"OrderDiscountDisplayName": "Display name",

1
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hans.json

@ -48,6 +48,7 @@
"OrderDiscount": "订单折扣项",
"OrderDiscountOrderId": "订单 ID",
"OrderDiscountOrderLineId": "订单项 ID",
"OrderDiscountEffectGroup": "生效组",
"OrderDiscountName": "折扣项名称",
"OrderDiscountKey": "折扣项 Key",
"OrderDiscountDisplayName": "显示名称",

1
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain.Shared/EasyAbp/EShop/Orders/Localization/Orders/zh-Hant.json

@ -48,6 +48,7 @@
"OrderDiscount": "訂單折扣項",
"OrderDiscountOrderId": "訂單 ID",
"OrderDiscountOrderLineId": "訂單項 ID",
"OrderDiscountEffectGroup": "生效組",
"OrderDiscountName": "折扣項名稱",
"OrderDiscountKey": "折扣項 Key",
"OrderDiscountDisplayName": "顯示名稱",

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

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using EasyAbp.EShop.Products.Products;
namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscountContext
{
public IOrder Order { get; }
public Dictionary<Guid, IProduct> ProductDict { get; }
public List<OrderDiscountInfoModel> CandidateDiscounts { get; }
public OrderDiscountContext(IOrder order, Dictionary<Guid, IProduct> productDict)
{
Order = order;
ProductDict = productDict ?? new Dictionary<Guid, IProduct>();
}
public List<OrderDiscountInfoModel> GetEffectDiscounts()
{
var effectDiscounts = new List<OrderDiscountInfoModel>();
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;
}
}

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

@ -1,28 +1,38 @@
using System;
using System.Collections.Generic;
using EasyAbp.EShop.Products.Products;
using JetBrains.Annotations;
namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscountInfoModel
public class OrderDiscountInfoModel : IDiscountInfo
{
public Guid OrderLineId { get; set; }
public string EffectGroup { get; set; }
[NotNull]
public string Name { get; set; }
[CanBeNull]
public string Key { get; set; }
[CanBeNull]
public string DisplayName { get; set; }
public List<Guid> AffectedOrderLineIds { get; set; } = new();
public decimal DiscountedAmount { get; set; }
public OrderDiscountInfoModel(Guid orderLineId, [NotNull] string name, [CanBeNull] string key,
[CanBeNull] string displayName, decimal discountedAmount)
public OrderDiscountInfoModel()
{
OrderLineId = orderLineId;
}
public OrderDiscountInfoModel(List<Guid> affectedOrderLineIds, [CanBeNull] string effectGroup,
[NotNull] string name, [CanBeNull] string key, [CanBeNull] string displayName, decimal discountedAmount)
{
if (discountedAmount < decimal.Zero)
{
throw new DiscountAmountOverflowException();
}
AffectedOrderLineIds = affectedOrderLineIds ?? new List<Guid>();
EffectGroup = effectGroup;
Name = name;
Key = key;
DisplayName = displayName;

9
modules/EasyAbp.EShop.Orders/src/EasyAbp.EShop.Orders.Domain/EasyAbp/EShop/Orders/Orders/IOrderDiscountProvider.cs

@ -1,12 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using EasyAbp.EShop.Products.Products;
using System.Threading.Tasks;
namespace EasyAbp.EShop.Orders.Orders
{
public interface IOrderDiscountProvider
{
Task<List<OrderDiscountInfoModel>> GetAllAsync(Order order, Dictionary<Guid, IProduct> productDict);
int EffectOrder { get; }
Task DiscountAsync(OrderDiscountContext context);
}
}

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

@ -14,7 +14,7 @@ using Volo.Abp.Settings;
namespace EasyAbp.EShop.Orders.Orders
{
public class NewOrderGenerator : DomainService, INewOrderGenerator, ITransientDependency
public class NewOrderGenerator : DomainService, INewOrderGenerator
{
private readonly ISettingProvider _settingProvider;
private readonly IOrderNumberGenerator _orderNumberGenerator;
@ -107,15 +107,19 @@ namespace EasyAbp.EShop.Orders.Orders
protected virtual async Task DiscountOrderAsync(Order order, Dictionary<Guid, IProduct> productDict)
{
foreach (var provider in LazyServiceProvider.LazyGetService<IEnumerable<IOrderDiscountProvider>>())
var context = new OrderDiscountContext(order, productDict);
foreach (var provider in LazyServiceProvider.LazyGetService<IEnumerable<IOrderDiscountProvider>>()
.OrderBy(x => x.EffectOrder))
{
var discounts = await provider.GetAllAsync(order, productDict);
await provider.DiscountAsync(context);
}
foreach (var discount in discounts)
{
order.AddDiscount(discount.OrderLineId, discount.Name, discount.Key, discount.DisplayName,
discount.DiscountedAmount);
}
var effectDiscounts = context.GetEffectDiscounts();
foreach (var discount in effectDiscounts)
{
order.AddDiscounts(discount);
}
}

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

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using EasyAbp.EShop.Products.Products;
using JetBrains.Annotations;
using NodaMoney;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
@ -186,30 +188,82 @@ namespace EasyAbp.EShop.Orders.Orders
return !(!PaymentId.HasValue || PaidTime.HasValue);
}
public void AddDiscount(Guid orderLineId, [NotNull] string discountName, [CanBeNull] string discountKey,
[CanBeNull] string discountDisplayName, decimal discountedAmount)
public void AddDiscounts(OrderDiscountInfoModel infoModel)
{
var orderLine = OrderLines.Single(x => x.Id == orderLineId);
var affectedOrderLines = infoModel.AffectedOrderLineIds
.Select(orderLineId => OrderLines.Single(x => x.Id == orderLineId))
.ToList();
var remainingDiscountedAmount = new Money(infoModel.DiscountedAmount, Currency);
orderLine.AddDiscount(discountedAmount);
var totalOrderLineActualTotalPrice = new Money(affectedOrderLines.Sum(x => x.ActualTotalPrice), Currency);
TotalDiscount += discountedAmount;
ActualTotalPrice -= discountedAmount;
var orderLineDiscounts = new Dictionary<OrderLine, Money>();
if (ActualTotalPrice < decimal.Zero)
foreach (var orderLineId in infoModel.AffectedOrderLineIds)
{
throw new DiscountAmountOverflowException();
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;
}
if (OrderDiscounts.Any(x => x.OrderLineId == orderLineId && x.Name == discountName && x.Key == discountKey))
foreach (var orderLine in affectedOrderLines.OrderByDescending(x => x.ActualTotalPrice))
{
throw new DuplicateOrderDiscountException(orderLineId, discountName, discountKey);
if (remainingDiscountedAmount == decimal.Zero)
{
break;
}
var discountAmount = remainingDiscountedAmount > totalOrderLineActualTotalPrice
? totalOrderLineActualTotalPrice
: remainingDiscountedAmount;
orderLineDiscounts[orderLine] += discountAmount;
remainingDiscountedAmount -= discountAmount;
}
var orderDiscount = new OrderDiscount(
Id, orderLineId, discountName, discountKey, discountDisplayName, discountedAmount);
if (remainingDiscountedAmount.Amount != decimal.Zero)
{
throw new ApplicationException();
}
OrderDiscounts.Add(orderDiscount);
foreach (var affectedOrderLine in affectedOrderLines)
{
var orderLineDiscountedAmount = orderLineDiscounts[affectedOrderLine];
affectedOrderLine.AddDiscount(orderLineDiscountedAmount.Amount);
TotalDiscount += orderLineDiscountedAmount.Amount;
ActualTotalPrice -= orderLineDiscountedAmount.Amount;
if (ActualTotalPrice < decimal.Zero)
{
throw new DiscountAmountOverflowException();
}
if (OrderDiscounts.Any(x =>
x.OrderLineId == affectedOrderLine.Id && x.Name == infoModel.Name &&
x.Key == infoModel.Key))
{
throw new DuplicateOrderDiscountException(affectedOrderLine.Id, infoModel.Name, infoModel.Key);
}
var orderDiscount = new OrderDiscount(Id, affectedOrderLine.Id, infoModel.EffectGroup,
infoModel.Name, infoModel.Key, infoModel.DisplayName, orderLineDiscountedAmount.Amount);
OrderDiscounts.Add(orderDiscount);
}
}
public void AddOrderExtraFee(decimal extraFee, [NotNull] string extraFeeName, [CanBeNull] string extraFeeKey,

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

@ -1,22 +1,22 @@
using System;
using EasyAbp.EShop.Products.Products;
using JetBrains.Annotations;
using Volo.Abp.Domain.Entities;
namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscount : Entity
public class OrderDiscount : Entity, IDiscountInfo
{
public virtual Guid OrderId { get; protected set; }
public virtual Guid OrderLineId { get; protected set; }
[NotNull]
public virtual string EffectGroup { get; protected set; }
public virtual string Name { get; protected set; }
[CanBeNull]
public virtual string Key { get; protected set; }
[CanBeNull]
public virtual string DisplayName { get; protected set; }
public virtual decimal DiscountedAmount { get; protected set; }
@ -28,6 +28,7 @@ public class OrderDiscount : Entity
public OrderDiscount(
Guid orderId,
Guid orderLineId,
[CanBeNull] string effectGroup,
[NotNull] string name,
[CanBeNull] string key,
[CanBeNull] string displayName,
@ -35,6 +36,7 @@ public class OrderDiscount : Entity
{
OrderId = orderId;
OrderLineId = orderLineId;
EffectGroup = effectGroup;
Name = name;
Key = key;
DisplayName = displayName;

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

@ -8,12 +8,19 @@ namespace EasyAbp.EShop.Orders.Orders;
public class DemoOrderDiscountProvider : IOrderDiscountProvider
{
public Task<List<OrderDiscountInfoModel>> GetAllAsync(Order order, Dictionary<Guid, IProduct> productDict)
public static int DemoOrderDiscountEffectOrder { get; set; } = 10000;
public int EffectOrder => DemoOrderDiscountEffectOrder;
public Task DiscountAsync(OrderDiscountContext context)
{
var firstOrderLine = context.Order.OrderLines.First();
return Task.FromResult(new List<OrderDiscountInfoModel>
{
new(order.OrderLines.First().Id, "DemoDiscount1", "1", "Demo Discount 1", 0.01m),
new(order.OrderLines.First().Id, "DemoDiscount2", "2", "Demo Discount 2", 0.1m),
new(new List<Guid> { firstOrderLine.Id }, null, "DemoDiscount1", "1", "Demo Discount 1", 0.01m),
new(new List<Guid> { firstOrderLine.Id }, "A", "DemoDiscount2", "2", "Demo Discount 2", 0.1m),
new(new List<Guid> { firstOrderLine.Id }, "A", "DemoDiscount3", "3", "Demo Discount 3", 0.05m),
});
}
}

2
modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDiscountTests.cs → modules/EasyAbp.EShop.Orders/test/EasyAbp.EShop.Orders.Domain.Tests/Orders/OrderDiscountProviderTests.cs

@ -9,7 +9,7 @@ using Xunit;
namespace EasyAbp.EShop.Orders.Orders;
public class OrderDiscountTests : OrdersDomainTestBase
public class OrderDiscountProviderTests : OrdersDomainTestBase
{
protected override void AfterAddApplication(IServiceCollection services)
{

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

@ -121,7 +121,7 @@ namespace EasyAbp.EShop.Orders.Orders
Order1.SetPaidTime(DateTime.Now);
Order1.RefundAmount.ShouldBe(0.3m);
var orderLine1 = Order1.OrderLines.Single(x => x.Id == OrderTestData.OrderLine1Id);
orderLine1.RefundAmount.ShouldBe(0.2m);
orderLine1.RefundedQuantity.ShouldBe(1);
@ -244,5 +244,78 @@ namespace EasyAbp.EShop.Orders.Orders
order.SetReducedInventoryAfterPlacingTime(null);
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.Products/src/EasyAbp.EShop.Products.Application.Contracts/EasyAbp/EShop/Products/Products/Dtos/ProductSkuDto.cs

@ -5,7 +5,7 @@ using Volo.Abp.Application.Dtos;
namespace EasyAbp.EShop.Products.Products.Dtos
{
[Serializable]
public class ProductSkuDto : ExtensibleFullAuditedEntityDto<Guid>, IProductSku, IHasFullDiscountsInfo
public class ProductSkuDto : ExtensibleFullAuditedEntityDto<Guid>, IProductSku, IHasFullDiscountsForProduct
{
public List<Guid> AttributeOptionIds { get; set; }

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

@ -316,7 +316,7 @@ namespace EasyAbp.EShop.Products.Products
var priceDataModel = await _productManager.GetRealPriceAsync(product, productSku, now);
productSkuDto.PriceWithoutDiscount = priceDataModel.PriceWithoutDiscount;
productSkuDto.Price = priceDataModel.DiscountedPrice;
productSkuDto.Price = priceDataModel.GetDiscountedPrice();
productSkuDto.ProductDiscounts = priceDataModel.ProductDiscounts;
productSkuDto.OrderDiscountPreviews = priceDataModel.OrderDiscountPreviews;
}

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

@ -200,22 +200,23 @@ namespace EasyAbp.EShop.Products.Products
decimal? min = null, max = null;
decimal? minWithoutDiscount = null, maxWithoutDiscount = null;
var discounts = new DiscountsInfoModel();
var discounts = new DiscountForProductModels();
foreach (var productSku in product.ProductSkus)
{
var overrideProductDiscounts = false;
var priceDataModel = await _productManager.GetRealPriceAsync(product, productSku, now);
var discountedPrice = priceDataModel.GetDiscountedPrice();
if (min is null || priceDataModel.DiscountedPrice < min.Value)
if (min is null || discountedPrice < min.Value)
{
min = priceDataModel.DiscountedPrice;
min = discountedPrice;
overrideProductDiscounts = true;
}
if (max is null || priceDataModel.DiscountedPrice > max.Value)
if (max is null || discountedPrice > max.Value)
{
max = priceDataModel.DiscountedPrice;
max = discountedPrice;
}
if (minWithoutDiscount is null || priceDataModel.PriceWithoutDiscount < minWithoutDiscount.Value)
@ -234,20 +235,19 @@ namespace EasyAbp.EShop.Products.Products
if (discount is null || overrideProductDiscounts)
{
discounts.AddOrUpdateProductDiscount(new ProductDiscountInfoModel(model.Name, model.Key,
model.DisplayName, model.DiscountedAmount, model.FromTime, model.ToTime));
discounts.AddOrUpdateProductDiscount(new ProductDiscountInfoModel(model.EffectGroup, model.Name,
model.Key, model.DisplayName, model.DiscountedAmount, model.FromTime, model.ToTime));
}
}
foreach (var model in priceDataModel.OrderDiscountPreviews)
{
var discount = discounts.FindOrderDiscount(model.Name, model.Key);
var discount = discounts.FindOrderDiscountPreview(model.Name, model.Key);
if (discount is null)
{
discounts.AddOrUpdateOrderDiscountPreview(new OrderDiscountPreviewInfoModel(model.Name,
model.Key, model.DisplayName, model.MinDiscountedAmount, model.MaxDiscountedAmount,
model.FromTime, model.ToTime));
discounts.AddOrUpdateOrderDiscountPreview(new OrderDiscountPreviewInfoModel(model.EffectGroup,
model.Name, model.Key, model.DisplayName, model.FromTime, model.ToTime));
}
}
}

6
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DiscountsInfoModel.cs → modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DiscountForProductModels.cs

@ -2,19 +2,19 @@ using System.Collections.Generic;
namespace EasyAbp.EShop.Products.Products;
public class DiscountsInfoModel : IHasDiscountsInfo
public class DiscountForProductModels : IHasDiscountsForProduct
{
public List<ProductDiscountInfoModel> ProductDiscounts { get; set; }
public List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; set; }
public DiscountsInfoModel()
public DiscountForProductModels()
{
ProductDiscounts = new List<ProductDiscountInfoModel>();
OrderDiscountPreviews = new List<OrderDiscountPreviewInfoModel>();
}
public DiscountsInfoModel(List<ProductDiscountInfoModel> productDiscounts,
public DiscountForProductModels(List<ProductDiscountInfoModel> productDiscounts,
List<OrderDiscountPreviewInfoModel> orderDiscountPreviews)
{
ProductDiscounts = productDiscounts ?? new List<ProductDiscountInfoModel>();

12
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/DiscountInfoModel.cs

@ -3,15 +3,14 @@ using JetBrains.Annotations;
namespace EasyAbp.EShop.Products.Products;
public abstract class DiscountInfoModel
public abstract class DiscountInfoModel : IDiscountInfo
{
[NotNull]
public string EffectGroup { get; set; }
public string Name { get; set; }
[CanBeNull]
public string Key { get; set; }
[CanBeNull]
public string DisplayName { get; set; }
/// <summary>
@ -28,14 +27,15 @@ public abstract class DiscountInfoModel
{
}
public DiscountInfoModel([NotNull] string name, [CanBeNull] string key, [CanBeNull] string displayName,
DateTime? fromTime, DateTime? toTime)
public DiscountInfoModel([CanBeNull] string effectGroup, [NotNull] string name, [CanBeNull] string key,
[CanBeNull] string displayName, DateTime? fromTime, DateTime? toTime)
{
if (fromTime > toTime)
{
throw new InvalidTimePeriodException();
}
EffectGroup = effectGroup;
Name = name;
Key = key;
DisplayName = displayName;

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

@ -0,0 +1,146 @@
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();
}
}
}

108
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/HasDiscountsInfoExtensions.cs

@ -1,108 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
namespace EasyAbp.EShop.Products.Products;
public static class HasDiscountsInfoExtensions
{
public static decimal GetProductDiscountsDiscountedAmount(this IHasDiscountsInfo hasDiscountsInfo, DateTime now)
{
return hasDiscountsInfo.ProductDiscounts
.Where(x => !x.FromTime.HasValue || x.FromTime <= now)
.Where(x => !x.ToTime.HasValue || now <= x.ToTime)
.Sum(x => x.DiscountedAmount);
}
public static void AddOrUpdateProductDiscount(this IHasDiscountsInfo hasDiscountsInfo,
ProductDiscountInfoModel model)
{
var found = hasDiscountsInfo.FindProductDiscount(model.Name, model.Key);
if (found is null)
{
hasDiscountsInfo.ProductDiscounts.Add(model);
}
else
{
hasDiscountsInfo.ProductDiscounts.ReplaceOne(found, model);
}
hasDiscountsInfo.CheckDiscountedAmount();
}
public static ProductDiscountInfoModel FindProductDiscount(this IHasDiscountsInfo hasDiscountsInfo,
[NotNull] string name, [CanBeNull] string key)
{
return hasDiscountsInfo.ProductDiscounts.Find(x => x.Name == name && x.Key == key);
}
public static bool TryRemoveProductDiscount(this IHasDiscountsInfo hasDiscountsInfo, [NotNull] string name,
[CanBeNull] string key)
{
var found = hasDiscountsInfo.FindProductDiscount(name, key);
if (found is null)
{
return false;
}
hasDiscountsInfo.ProductDiscounts.Remove(found);
hasDiscountsInfo.CheckDiscountedAmount();
return true;
}
public static void AddOrUpdateOrderDiscountPreview(this IHasDiscountsInfo hasDiscountsInfo,
OrderDiscountPreviewInfoModel model)
{
var found = hasDiscountsInfo.FindOrderDiscount(model.Name, model.Key);
if (found is null)
{
hasDiscountsInfo.OrderDiscountPreviews.Add(model);
}
else
{
hasDiscountsInfo.OrderDiscountPreviews.ReplaceOne(found, model);
}
hasDiscountsInfo.CheckDiscountedAmount();
}
public static OrderDiscountPreviewInfoModel FindOrderDiscount(this IHasDiscountsInfo hasDiscountsInfo,
[NotNull] string name, [CanBeNull] string key)
{
return hasDiscountsInfo.OrderDiscountPreviews.Find(x => x.Name == name && x.Key == key);
}
public static bool TryRemoveOrderDiscountPreview(this IHasDiscountsInfo hasDiscountsInfo, [NotNull] string name,
[CanBeNull] string key)
{
var found = hasDiscountsInfo.FindOrderDiscount(name, key);
if (found is null)
{
return false;
}
hasDiscountsInfo.OrderDiscountPreviews.Remove(found);
hasDiscountsInfo.CheckDiscountedAmount();
return true;
}
private static void CheckDiscountedAmount(this IHasDiscountsInfo hasDiscountsInfo)
{
if (hasDiscountsInfo.ProductDiscounts.Any(x => x.DiscountedAmount < decimal.Zero) ||
hasDiscountsInfo.OrderDiscountPreviews.Any(x =>
x.MinDiscountedAmount < decimal.Zero || x.MaxDiscountedAmount < decimal.Zero ||
x.MinDiscountedAmount > x.MaxDiscountedAmount))
{
throw new DiscountAmountOverflowException();
}
}
}

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

@ -0,0 +1,23 @@
using JetBrains.Annotations;
namespace EasyAbp.EShop.Products.Products;
public interface IDiscountInfo
{
/// <summary>
/// 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.
/// </summary>
[CanBeNull]
string EffectGroup { get; }
[NotNull]
string Name { get; }
[CanBeNull]
string Key { get; }
[CanBeNull]
string DisplayName { get; }
}

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasDiscountsInfo.cs → modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasDiscountsForProduct.cs

@ -2,7 +2,7 @@ using System.Collections.Generic;
namespace EasyAbp.EShop.Products.Products;
public interface IHasDiscountsInfo
public interface IHasDiscountsForProduct
{
/// <summary>
/// The Price of the ProductSku has been subtracted from these product discounts.

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasFullDiscountsInfo.cs → modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IHasFullDiscountsForProduct.cs

@ -1,6 +1,6 @@
namespace EasyAbp.EShop.Products.Products;
public interface IHasFullDiscountsInfo : IHasDiscountsInfo
public interface IHasFullDiscountsForProduct : IHasDiscountsForProduct
{
/// <summary>
/// The realtime price without subtracting the discount amount.

2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/IProductView.cs

@ -1,6 +1,6 @@
namespace EasyAbp.EShop.Products.Products;
public interface IProductView : IProductBase, IHasDiscountsInfo, IHasProductGroupDisplayName
public interface IProductView : IProductBase, IHasDiscountsForProduct, IHasProductGroupDisplayName
{
decimal? MinimumPrice { get; }

33
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain.Shared/EasyAbp/EShop/Products/Products/OrderDiscountPreviewInfoModel.cs

@ -6,32 +6,19 @@ namespace EasyAbp.EShop.Products.Products;
[Serializable]
public class OrderDiscountPreviewInfoModel : DiscountInfoModel, ICloneable
{
public decimal MinDiscountedAmount { get; set; }
public decimal MaxDiscountedAmount { get; set; }
public OrderDiscountPreviewInfoModel()
{
}
public OrderDiscountPreviewInfoModel([NotNull] string name, [CanBeNull] string key, [CanBeNull] string displayName,
decimal minDiscountedAmount, decimal maxDiscountedAmount, DateTime? fromTime, DateTime? toTime) : base(name,
key, displayName, fromTime, toTime)
public OrderDiscountPreviewInfoModel([CanBeNull] string effectGroup, [NotNull] string name, [CanBeNull] string key,
[CanBeNull] string displayName, DateTime? fromTime, DateTime? toTime) : base(effectGroup, name, key,
displayName, fromTime, toTime)
{
if (minDiscountedAmount < decimal.Zero || maxDiscountedAmount < decimal.Zero ||
minDiscountedAmount > maxDiscountedAmount)
{
throw new DiscountAmountOverflowException();
}
MinDiscountedAmount = minDiscountedAmount;
MaxDiscountedAmount = maxDiscountedAmount;
}
public object Clone()
public virtual object Clone()
{
return new OrderDiscountPreviewInfoModel(Name, Key, DisplayName, MinDiscountedAmount, MaxDiscountedAmount,
FromTime, ToTime);
return new OrderDiscountPreviewInfoModel(EffectGroup, Name, Key, DisplayName, FromTime, ToTime);
}
public override bool Equals(object obj)
@ -39,13 +26,12 @@ public class OrderDiscountPreviewInfoModel : DiscountInfoModel, ICloneable
return obj is OrderDiscountPreviewInfoModel other && Equals(other);
}
protected bool Equals(OrderDiscountPreviewInfoModel other)
private bool Equals(OrderDiscountPreviewInfoModel other)
{
return Name == other.Name &&
return EffectGroup == other.EffectGroup &&
Name == other.Name &&
Key == other.Key &&
DisplayName == other.DisplayName &&
MinDiscountedAmount == other.MinDiscountedAmount &&
MaxDiscountedAmount == other.MaxDiscountedAmount &&
Nullable.Equals(FromTime, other.FromTime) &&
Nullable.Equals(ToTime, other.ToTime);
}
@ -55,10 +41,9 @@ public class OrderDiscountPreviewInfoModel : DiscountInfoModel, ICloneable
unchecked
{
var hashCode = Name.GetHashCode();
hashCode = (hashCode * 397) ^ (EffectGroup != null ? EffectGroup.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (Key != null ? Key.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (DisplayName != null ? DisplayName.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ MinDiscountedAmount.GetHashCode();
hashCode = (hashCode * 397) ^ MaxDiscountedAmount.GetHashCode();
hashCode = (hashCode * 397) ^ FromTime.GetHashCode();
hashCode = (hashCode * 397) ^ ToTime.GetHashCode();
return hashCode;

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

@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
namespace EasyAbp.EShop.Products.Products;
public class ProductDiscountContext
{
public DateTime Now { get; }
public IProduct Product { get; }
public IProductSku ProductSku { get; }
public decimal PriceWithoutDiscount => PriceModel.PriceWithoutDiscount;
public IReadOnlyList<ProductDiscountInfoModel> ProductDiscounts => PriceModel.ProductDiscounts;
public IReadOnlyList<OrderDiscountPreviewInfoModel> OrderDiscountPreviews => PriceModel.OrderDiscountPreviews;
private ProductPriceModel PriceModel { get; }
public ProductDiscountContext(IProduct product, IProductSku productSku, decimal priceFromPriceProvider,
DateTime now)
{
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);
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);
}
/// <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;
}
}

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

@ -8,12 +8,15 @@ public class ProductDiscountInfoModel : DiscountInfoModel, ICloneable
{
public decimal DiscountedAmount { get; set; }
public bool? InEffect { get; set; }
public ProductDiscountInfoModel()
{
}
public ProductDiscountInfoModel([NotNull] string name, [CanBeNull] string key, [CanBeNull] string displayName,
decimal discountedAmount, DateTime? fromTime, DateTime? toTime) : base(name, key, displayName, fromTime, toTime)
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)
{
if (discountedAmount < decimal.Zero)
{
@ -21,11 +24,13 @@ public class ProductDiscountInfoModel : DiscountInfoModel, ICloneable
}
DiscountedAmount = discountedAmount;
InEffect = inEffect;
}
public object Clone()
public virtual object Clone()
{
return new ProductDiscountInfoModel(Name, Key, DisplayName, DiscountedAmount, FromTime, ToTime);
return new ProductDiscountInfoModel(
EffectGroup, Name, Key, DisplayName, DiscountedAmount, FromTime, ToTime, InEffect);
}
public override bool Equals(object obj)
@ -33,12 +38,14 @@ public class ProductDiscountInfoModel : DiscountInfoModel, ICloneable
return obj is ProductDiscountInfoModel other && Equals(other);
}
protected bool Equals(ProductDiscountInfoModel other)
private bool Equals(ProductDiscountInfoModel other)
{
return Name == other.Name &&
return EffectGroup == other.EffectGroup &&
Name == other.Name &&
Key == other.Key &&
DisplayName == other.DisplayName &&
DiscountedAmount == other.DiscountedAmount &&
Nullable.Equals(InEffect, other.InEffect) &&
Nullable.Equals(FromTime, other.FromTime) &&
Nullable.Equals(ToTime, other.ToTime);
}
@ -48,9 +55,11 @@ public class ProductDiscountInfoModel : DiscountInfoModel, ICloneable
unchecked
{
var hashCode = Name.GetHashCode();
hashCode = (hashCode * 397) ^ (EffectGroup != null ? EffectGroup.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (Key != null ? Key.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (DisplayName != null ? DisplayName.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ DiscountedAmount.GetHashCode();
hashCode = (hashCode * 397) ^ InEffect.GetHashCode();
hashCode = (hashCode * 397) ^ FromTime.GetHashCode();
hashCode = (hashCode * 397) ^ ToTime.GetHashCode();
return hashCode;

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

@ -0,0 +1,23 @@
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;
}
}

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

@ -54,7 +54,7 @@ namespace EasyAbp.EShop.Products.Products
}
[UnitOfWork]
public virtual async Task<InventoryDataModel> GetInventoryDataAsync(InventoryQueryModel model)
public virtual async Task<InventoryDataModel> GetInventoryDataAsync(InventoryQueryModel model)
{
return await _productInventoryRepository.GetInventoryDataAsync(model.ProductSkuId) ??
new InventoryDataModel();

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

@ -4,5 +4,7 @@ namespace EasyAbp.EShop.Products.Products;
public interface IProductDiscountProvider
{
int EffectOrder { get; }
Task DiscountAsync(ProductDiscountContext context);
}

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<PriceDataModel> GetRealPriceAsync(Product product, ProductSku productSku, DateTime now);
Task<ProductPriceModel> GetRealPriceAsync(Product product, ProductSku productSku, DateTime now);
}
}

36
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/PriceDataModel.cs

@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
namespace EasyAbp.EShop.Products.Products;
public class PriceDataModel : IHasFullDiscountsInfo
{
public DateTime Now { get; }
public decimal PriceWithoutDiscount { get; }
/// <summary>
/// It's a sum of the amount of product discounts which in effect at the current time (this.<see cref="Now"/>).
/// </summary>
public decimal DiscountedAmount => this.GetProductDiscountsDiscountedAmount(Now);
/// <summary>
/// It has been subtracted from the product discounts which in effect at the current time (this.<see cref="Now"/>).
/// </summary>
public decimal DiscountedPrice => PriceWithoutDiscount - DiscountedAmount;
public List<ProductDiscountInfoModel> ProductDiscounts { get; } = new();
public List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; } = new();
public PriceDataModel(decimal priceWithoutDiscount, DateTime now)
{
if (PriceWithoutDiscount < decimal.Zero)
{
throw new OverflowException();
}
Now = now;
PriceWithoutDiscount = priceWithoutDiscount;
}
}

19
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.Domain/EasyAbp/EShop/Products/Products/ProductDiscountContext.cs

@ -1,19 +0,0 @@
using System;
namespace EasyAbp.EShop.Products.Products;
public class ProductDiscountContext
{
public Product Product { get; }
public ProductSku ProductSku { get; }
public PriceDataModel PriceDataModel { get; }
public ProductDiscountContext(Product product, ProductSku productSku, decimal priceFromPriceProvider, DateTime now)
{
Product = product;
ProductSku = productSku;
PriceDataModel = new PriceDataModel(priceFromPriceProvider, now);
}
}

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

@ -268,20 +268,20 @@ namespace EasyAbp.EShop.Products.Products
.TryReduceInventoryAsync(model, quantity, increaseSold, isFlashSale);
}
public virtual async Task<PriceDataModel> GetRealPriceAsync(Product product, ProductSku productSku,
public virtual async Task<ProductPriceModel> GetRealPriceAsync(Product product, ProductSku productSku,
DateTime now)
{
var price = await _productPriceProvider.GetPriceAsync(product, productSku);
var context = new ProductDiscountContext(product, productSku, price, now);
// Todo: provider execution ordering.
foreach (var provider in LazyServiceProvider.LazyGetService<IEnumerable<IProductDiscountProvider>>())
foreach (var provider in LazyServiceProvider.LazyGetService<IEnumerable<IProductDiscountProvider>>()
.OrderBy(x => x.EffectOrder))
{
await provider.DiscountAsync(context);
}
return context.PriceDataModel;
return context.ToFinalProductPriceModel();
}
}
}

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

@ -127,10 +127,10 @@ namespace EasyAbp.EShop.Products.Products
MaximumPriceWithoutDiscount = maxWithoutDiscount;
}
public void SetDiscounts(IHasDiscountsInfo discountsInfo)
public void SetDiscounts(IHasDiscountsForProduct discountsForProduct)
{
ProductDiscounts = discountsInfo.ProductDiscounts ?? new List<ProductDiscountInfoModel>();
OrderDiscountPreviews = discountsInfo.OrderDiscountPreviews ?? new List<OrderDiscountPreviewInfoModel>();
ProductDiscounts = discountsForProduct.ProductDiscounts ?? new List<ProductDiscountInfoModel>();
OrderDiscountPreviews = discountsForProduct.OrderDiscountPreviews ?? new List<OrderDiscountPreviewInfoModel>();
}
}
}

6
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/EntityFrameworkCore/ValueMappings/EShopProductsEntityTypeBuilderExtensions.cs

@ -18,12 +18,12 @@ public static class EShopProductsEntityTypeBuilderExtensions
public static void TryConfigureDiscountsInfo(this EntityTypeBuilder b)
{
if (b.Metadata.ClrType.IsAssignableTo<IHasDiscountsInfo>())
if (b.Metadata.ClrType.IsAssignableTo<IHasDiscountsForProduct>())
{
b.Property(nameof(IHasDiscountsInfo.ProductDiscounts))
b.Property(nameof(IHasDiscountsForProduct.ProductDiscounts))
.HasConversion<ProductDiscountsInfoValueConverter>()
.Metadata.SetValueComparer(new ProductDiscountsInfoValueComparer());
b.Property(nameof(IHasDiscountsInfo.OrderDiscountPreviews))
b.Property(nameof(IHasDiscountsForProduct.OrderDiscountPreviews))
.HasConversion<OrderDiscountPreviewsInfoValueConverter>()
.Metadata.SetValueComparer(new OrderDiscountPreviewsInfoValueComparer());
}

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

@ -13,6 +13,10 @@ public class DemoProductDiscountProvider : IProductDiscountProvider
_clock = clock;
}
public static int DemoProductDiscountEffectOrder { get; set; } = 10000;
public int EffectOrder => DemoProductDiscountEffectOrder;
public Task DiscountAsync(ProductDiscountContext context)
{
if (context.Product.Id != ProductsTestData.Product1Id ||
@ -21,24 +25,37 @@ public class DemoProductDiscountProvider : IProductDiscountProvider
return Task.CompletedTask;
}
context.PriceDataModel.ProductDiscounts.AddRange(new List<ProductDiscountInfoModel>
var productDiscountInfoModels = new List<ProductDiscountInfoModel>
{
// These should affect:
new("DemoDiscount", "1", "Demo Discount 1", 0.01m, null, null),
new("DemoDiscount", "2", "Demo Discount 2", 0.01m, _clock.Now.AddDays(-1), null),
new("DemoDiscount", "3", "Demo Discount 3", 0.01m, null, _clock.Now.AddDays(1)),
new("DemoDiscount", "4", "Demo Discount 4", 0.01m, _clock.Now.AddDays(-1), _clock.Now.AddDays(1)),
// These should not affect: 0.01m,
new("DemoDiscount", "5", "Demo Discount 5", 0.01m, null, _clock.Now.AddDays(-1)),
new("DemoDiscount", "6", "Demo Discount 6", 0.01m, _clock.Now.AddDays(1), null),
new("DemoDiscount", "7", "Demo Discount 7", 0.01m, _clock.Now.AddDays(1), _clock.Now.AddDays(2)),
});
context.PriceDataModel.OrderDiscountPreviews.AddRange(new List<OrderDiscountPreviewInfoModel>
// 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)),
// 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)),
// 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),
};
foreach (var model in productDiscountInfoModels)
{
new("DemoDiscount", "1", "Demo Discount 1", 0.01m, 0.01m, null, null),
new("DemoDiscount", "2", "Demo Discount 2", 0.01m, 0.01m, _clock.Now.AddDays(-1), _clock.Now.AddDays(1)),
});
context.AddOrUpdateProductDiscount(model);
}
var orderDiscountPreviewInfoModels = new List<OrderDiscountPreviewInfoModel>
{
new(null, "DemoDiscount", "1", "Demo Discount 1", null, null),
new(null, "DemoDiscount", "2", "Demo Discount 2", _clock.Now.AddDays(-1), _clock.Now.AddDays(1)),
};
foreach (var model in orderDiscountPreviewInfoModels)
{
context.AddOrUpdateOrderDiscountPreview(model);
}
return Task.CompletedTask;
}

14
modules/EasyAbp.EShop.Products/test/EasyAbp.EShop.Products.Application.Tests/Products/ProductDiscountTests.cs

@ -23,15 +23,17 @@ public class ProductDiscountTests : ProductsApplicationTestBase
var sku1 = (ProductSkuDto)product1.GetSkuById(ProductsTestData.Product1Sku1Id);
var sku2 = (ProductSkuDto)product1.GetSkuById(ProductsTestData.Product1Sku2Id);
sku1.Price.ShouldBe(1m - 0.01m * 4);
sku1.ProductDiscounts.Count.ShouldBe(7);
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "1");
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "2");
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "3");
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "4");
sku1.Price.ShouldBe(1m - 0.1m * 5);
sku1.ProductDiscounts.Count.ShouldBe(9);
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "1"); // in effect
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "2"); // in effect
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "3"); // in effect
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "4"); // in effect
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "5");
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "6");
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "7");
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "8"); // in effect
sku1.ProductDiscounts.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "9");
sku1.OrderDiscountPreviews.Count.ShouldBe(2);
sku1.OrderDiscountPreviews.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "1");
sku1.OrderDiscountPreviews.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "2");

2
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.Domain.Shared/EasyAbp/EShop/Plugins/Baskets/BasketItems/IProductData.cs

@ -2,7 +2,7 @@
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems
{
public interface IProductData : IHasFullDiscountsInfo
public interface IProductData : IHasFullDiscountsForProduct
{
string MediaResources { get; }

6
plugins/Baskets/src/EasyAbp.EShop.Plugins.Baskets.EntityFrameworkCore/EasyAbp/EShop/Plugins/Baskets/EntityFrameworkCore/ValueMappings/EShopProductsEntityTypeBuilderExtensions.cs

@ -8,12 +8,12 @@ public static class EShopProductsEntityTypeBuilderExtensions
{
public static void TryConfigureDiscountsInfo(this EntityTypeBuilder b)
{
if (b.Metadata.ClrType.IsAssignableTo<IHasDiscountsInfo>())
if (b.Metadata.ClrType.IsAssignableTo<IHasDiscountsForProduct>())
{
b.Property(nameof(IHasDiscountsInfo.ProductDiscounts))
b.Property(nameof(IHasDiscountsForProduct.ProductDiscounts))
.HasConversion<ProductDiscountsInfoValueConverter>()
.Metadata.SetValueComparer(new ProductDiscountsInfoValueComparer());
b.Property(nameof(IHasDiscountsInfo.OrderDiscountPreviews))
b.Property(nameof(IHasDiscountsForProduct.OrderDiscountPreviews))
.HasConversion<OrderDiscountPreviewsInfoValueConverter>()
.Metadata.SetValueComparer(new OrderDiscountPreviewsInfoValueComparer());
}

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

@ -18,6 +18,11 @@ namespace EasyAbp.EShop.Orders.Plugins.Coupons.OrderDiscount
public class CouponOrderDiscountProvider : IOrderDiscountProvider, ITransientDependency
{
public static string OrderDiscountName { get; set; } = "Coupon";
public static string OrderDiscountEffectGroup { get; set; } = "Coupon";
public static int CouponOrderDiscountEffectOrder { get; set; } = 5000;
public int EffectOrder => CouponOrderDiscountEffectOrder;
private readonly IClock _clock;
private readonly ICouponAppService _couponAppService;
@ -36,13 +41,13 @@ namespace EasyAbp.EShop.Orders.Plugins.Coupons.OrderDiscount
_couponTemplateLookupService = couponTemplateLookupService;
}
public virtual async Task<List<OrderDiscountInfoModel>> GetAllAsync(Order order,
Dictionary<Guid, IProduct> productDict)
public virtual async Task DiscountAsync(OrderDiscountContext context)
{
var couponId = order.GetProperty<Guid?>(CouponsConsts.OrderCouponIdPropertyName);
var couponId = context.Order.GetProperty<Guid?>(CouponsConsts.OrderCouponIdPropertyName);
if (couponId is null)
{
return new List<OrderDiscountInfoModel>();
return;
}
var now = _clock.Now;
@ -63,27 +68,27 @@ namespace EasyAbp.EShop.Orders.Plugins.Coupons.OrderDiscount
if (couponTemplate == null ||
!IsInUsableTime(couponTemplate, now) ||
!IsCurrencyExpected(couponTemplate, order))
!IsCurrencyExpected(couponTemplate, context.Order))
{
throw new CouponTemplateNotFoundOrUnavailableException();
}
var orderLinesInScope = GetOrderLinesInScope(couponTemplate, order, productDict);
var orderLinesInScope = GetOrderLinesInScope(couponTemplate, context);
var models = await DiscountOrderLinesAsync(couponTemplate, coupon, order, orderLinesInScope);
var model = await CreateDiscountModelAsync(context, couponTemplate, coupon, orderLinesInScope);
await _couponAppService.OccupyAsync(coupon.Id, new OccupyCouponInput { OrderId = order.Id });
context.CandidateDiscounts.Add(model);
return models;
await _couponAppService.OccupyAsync(coupon.Id, new OccupyCouponInput { OrderId = context.Order.Id });
}
protected virtual bool IsCurrencyExpected(CouponTemplateData couponTemplate, Order order)
protected virtual bool IsCurrencyExpected(CouponTemplateData couponTemplate, IOrder order)
{
return couponTemplate.Currency == order.Currency;
}
protected virtual Task<List<OrderDiscountInfoModel>> DiscountOrderLinesAsync(CouponTemplateData couponTemplate,
CouponData coupon, Order order, List<OrderLine> orderLinesInScope)
protected virtual Task<OrderDiscountInfoModel> CreateDiscountModelAsync(OrderDiscountContext context,
CouponTemplateData couponTemplate, CouponData coupon, List<IOrderLine> orderLinesInScope)
{
// Todo: support Custom coupon.
if (couponTemplate.CouponType == CouponType.Custom)
@ -91,7 +96,7 @@ namespace EasyAbp.EShop.Orders.Plugins.Coupons.OrderDiscount
throw new NotSupportedException();
}
var nodaCurrency = Currency.FromCode(order.Currency);
var nodaCurrency = Currency.FromCode(context.Order.Currency);
var totalOrderLineActualTotalPrice =
new Money(orderLinesInScope.Sum(x => x.ActualTotalPrice), nodaCurrency);
@ -107,71 +112,30 @@ namespace EasyAbp.EShop.Orders.Plugins.Coupons.OrderDiscount
totalDiscountedAmount = totalOrderLineActualTotalPrice;
}
var remainingDiscountedAmount = totalDiscountedAmount;
var orderLineDiscounts = new Dictionary<OrderLine, Money>();
foreach (var orderLine in orderLinesInScope)
{
var orderLineActualTotalPrice = new Money(orderLine.ActualTotalPrice, nodaCurrency);
var maxDiscountAmount =
new Money(
orderLineActualTotalPrice.Amount / totalOrderLineActualTotalPrice.Amount *
totalDiscountedAmount.Amount, nodaCurrency, MidpointRounding.ToZero);
var discountAmount = maxDiscountAmount > totalOrderLineActualTotalPrice
? orderLineActualTotalPrice
: maxDiscountAmount;
orderLineDiscounts[orderLine] = discountAmount;
remainingDiscountedAmount -= discountAmount;
}
foreach (var orderLine in orderLinesInScope.OrderByDescending(x => x.ActualTotalPrice))
{
if (remainingDiscountedAmount == decimal.Zero)
{
break;
}
var discountAmount = remainingDiscountedAmount > totalOrderLineActualTotalPrice
? totalOrderLineActualTotalPrice
: remainingDiscountedAmount;
orderLineDiscounts[orderLine] += discountAmount;
remainingDiscountedAmount -= discountAmount;
}
if (remainingDiscountedAmount != decimal.Zero)
{
throw new ApplicationException();
}
order.SetProperty(CouponsConsts.OrderCouponDiscountAmountPropertyName, totalDiscountedAmount);
context.Order.SetProperty(CouponsConsts.OrderCouponDiscountAmountPropertyName, totalDiscountedAmount);
var models = orderLinesInScope.Select(orderLine =>
new OrderDiscountInfoModel(orderLine.Id, OrderDiscountName, coupon.Id.ToString(),
couponTemplate.DisplayName, orderLineDiscounts[orderLine].Amount)
).ToList();
var model = new OrderDiscountInfoModel(orderLinesInScope.Select(x => x.Id).ToList(),
OrderDiscountEffectGroup, OrderDiscountName, coupon.Id.ToString(), couponTemplate.DisplayName,
totalDiscountedAmount.Amount);
return Task.FromResult(models);
return Task.FromResult(model);
}
protected virtual List<OrderLine> GetOrderLinesInScope(CouponTemplateData couponTemplate, Order order,
Dictionary<Guid, IProduct> productDict)
protected virtual List<IOrderLine> GetOrderLinesInScope(CouponTemplateData couponTemplate,
OrderDiscountContext context)
{
if (couponTemplate.IsUnscoped)
{
return order.OrderLines;
return context.Order.OrderLines.ToList();
}
var expectedOrderLines = new List<OrderLine>();
var expectedOrderLines = new List<IOrderLine>();
foreach (var scope in couponTemplate.Scopes.Where(scope => scope.StoreId == order.StoreId))
foreach (var scope in couponTemplate.Scopes.Where(scope => scope.StoreId == context.Order.StoreId))
{
expectedOrderLines.AddRange(order.OrderLines
expectedOrderLines.AddRange(context.Order.OrderLines
.WhereIf(scope.ProductGroupName != null,
x => productDict[x.ProductId].ProductGroupName == scope.ProductGroupName)
x => context.ProductDict[x.ProductId].ProductGroupName == scope.ProductGroupName)
.WhereIf(scope.ProductId.HasValue, x => x.ProductId == scope.ProductId)
.WhereIf(scope.ProductSkuId.HasValue, x => x.ProductSkuId == scope.ProductSkuId));
}
@ -181,7 +145,7 @@ namespace EasyAbp.EShop.Orders.Plugins.Coupons.OrderDiscount
throw new OrderDoesNotMeetCouponUsageConditionException();
}
if (expectedOrderLines.Sum(x => GetOrderLineProductPrice(x, productDict) * x.Quantity) <
if (expectedOrderLines.Sum(x => GetOrderLineProductPrice(x, context) * x.Quantity) <
couponTemplate.ConditionAmount)
{
throw new OrderDoesNotMeetCouponUsageConditionException();
@ -190,10 +154,9 @@ namespace EasyAbp.EShop.Orders.Plugins.Coupons.OrderDiscount
return expectedOrderLines;
}
protected virtual decimal GetOrderLineProductPrice(OrderLine orderLine,
Dictionary<Guid, IProduct> productDict)
protected virtual decimal GetOrderLineProductPrice(IOrderLine orderLine, OrderDiscountContext context)
{
return productDict[orderLine.ProductId].GetSkuById(orderLine.ProductSkuId).Price;
return context.ProductDict[orderLine.ProductId].GetSkuById(orderLine.ProductSkuId).Price;
}
protected virtual bool IsInUsableTime(ICouponTemplate couponTemplate, DateTime now)

2
plugins/Coupons/test/EasyAbp.EShop.Plugins.Coupons.Application.Tests/EasyAbp.EShop.Plugins.Coupons.Application.Tests.csproj

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace />
<RootNamespace>EasyAbp.EShop.Plugins.Coupons</RootNamespace>
</PropertyGroup>
<ItemGroup>

2
plugins/Coupons/test/EasyAbp.EShop.Plugins.Coupons.Domain.Tests/EasyAbp.EShop.Plugins.Coupons.Domain.Tests.csproj

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace />
<RootNamespace>EasyAbp.EShop.Plugins.Coupons</RootNamespace>
</PropertyGroup>
<ItemGroup>

2
plugins/Coupons/test/EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore.Tests/EasyAbp.EShop.Plugins.Coupons.EntityFrameworkCore.Tests.csproj

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace />
<RootNamespace>EasyAbp.EShop.Plugins.Coupons</RootNamespace>
</PropertyGroup>
<ItemGroup>

2
plugins/Coupons/test/EasyAbp.EShop.Plugins.Coupons.HttpApi.Client.ConsoleTestApp/EasyAbp.EShop.Plugins.Coupons.HttpApi.Client.ConsoleTestApp.csproj

@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace />
<RootNamespace>EasyAbp.EShop.Plugins.Coupons</RootNamespace>
</PropertyGroup>
<ItemGroup>

2
plugins/Coupons/test/EasyAbp.EShop.Plugins.Coupons.MongoDB.Tests/EasyAbp.EShop.Plugins.Coupons.MongoDB.Tests.csproj

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace />
<RootNamespace>EasyAbp.EShop.Plugins.Coupons</RootNamespace>
</PropertyGroup>
<ItemGroup>

2
plugins/Coupons/test/EasyAbp.EShop.Plugins.Coupons.TestBase/EasyAbp.EShop.Plugins.Coupons.TestBase.csproj

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace />
<RootNamespace>EasyAbp.EShop.Plugins.Coupons</RootNamespace>
</PropertyGroup>
<ItemGroup>

6424
samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/20230412195849_AddedEffectGroup.Designer.cs

File diff suppressed because it is too large

28
samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/20230412195849_AddedEffectGroup.cs

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EShopSample.Migrations
{
/// <inheritdoc />
public partial class AddedEffectGroup : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "EffectGroup",
table: "EasyAbpEShopOrdersOrderDiscounts",
type: "nvarchar(max)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "EffectGroup",
table: "EasyAbpEShopOrdersOrderDiscounts");
}
}
}

15
samples/EShopSample/aspnet-core/src/EShopSample.EntityFrameworkCore/Migrations/EShopSampleDbContextModelSnapshot.cs

@ -627,6 +627,9 @@ namespace EShopSample.Migrations
b.Property<string>("DisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("EffectGroup")
.HasColumnType("nvarchar(max)");
b.HasKey("OrderId", "OrderLineId", "Name", "Key");
b.ToTable("EasyAbpEShopOrdersOrderDiscounts", (string)null);
@ -5757,7 +5760,7 @@ namespace EShopSample.Migrations
.WithMany("Children")
.HasForeignKey("ParentId");
b.OwnsOne("EasyAbp.BookingService.TimeInAdvance", "TimeInAdvance", b1 =>
b.OwnsOne("EasyAbp.BookingService.AssetCategories.AssetCategory.TimeInAdvance#EasyAbp.BookingService.TimeInAdvance", "TimeInAdvance", b1 =>
{
b1.Property<Guid>("AssetCategoryId")
.HasColumnType("uniqueidentifier");
@ -5776,7 +5779,7 @@ namespace EShopSample.Migrations
b1.HasKey("AssetCategoryId");
b1.ToTable("EasyAbpBookingServiceAssetCategories");
b1.ToTable("EasyAbpBookingServiceAssetCategories", (string)null);
b1.WithOwner()
.HasForeignKey("AssetCategoryId");
@ -5789,7 +5792,7 @@ namespace EShopSample.Migrations
modelBuilder.Entity("EasyAbp.BookingService.AssetSchedules.AssetSchedule", b =>
{
b.OwnsOne("EasyAbp.BookingService.TimeInAdvance", "TimeInAdvance", b1 =>
b.OwnsOne("EasyAbp.BookingService.AssetSchedules.AssetSchedule.TimeInAdvance#EasyAbp.BookingService.TimeInAdvance", "TimeInAdvance", b1 =>
{
b1.Property<Guid>("AssetScheduleId")
.HasColumnType("uniqueidentifier");
@ -5808,7 +5811,7 @@ namespace EShopSample.Migrations
b1.HasKey("AssetScheduleId");
b1.ToTable("EasyAbpBookingServiceAssetSchedules");
b1.ToTable("EasyAbpBookingServiceAssetSchedules", (string)null);
b1.WithOwner()
.HasForeignKey("AssetScheduleId");
@ -5819,7 +5822,7 @@ namespace EShopSample.Migrations
modelBuilder.Entity("EasyAbp.BookingService.Assets.Asset", b =>
{
b.OwnsOne("EasyAbp.BookingService.TimeInAdvance", "TimeInAdvance", b1 =>
b.OwnsOne("EasyAbp.BookingService.Assets.Asset.TimeInAdvance#EasyAbp.BookingService.TimeInAdvance", "TimeInAdvance", b1 =>
{
b1.Property<Guid>("AssetId")
.HasColumnType("uniqueidentifier");
@ -5838,7 +5841,7 @@ namespace EShopSample.Migrations
b1.HasKey("AssetId");
b1.ToTable("EasyAbpBookingServiceAssets");
b1.ToTable("EasyAbpBookingServiceAssets", (string)null);
b1.WithOwner()
.HasForeignKey("AssetId");

Loading…
Cancel
Save