mirror of https://github.com/EasyAbp/EShop.git
committed by
GitHub
49 changed files with 7143 additions and 382 deletions
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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.
|
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -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"); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue