mirror of https://github.com/EasyAbp/EShop.git
78 changed files with 7616 additions and 266 deletions
@ -1,36 +1,40 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Data; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products.Dtos |
|||
{ |
|||
[Serializable] |
|||
public class ProductSkuDto : ExtensibleFullAuditedEntityDto<Guid>, IProductSku |
|||
public class ProductSkuDto : ExtensibleFullAuditedEntityDto<Guid>, IProductSku, IHasFullDiscountsInfo |
|||
{ |
|||
public List<Guid> AttributeOptionIds { get; set; } |
|||
|
|||
public string Name { get; set; } |
|||
|
|||
|
|||
public string Currency { get; set; } |
|||
|
|||
|
|||
public decimal? OriginalPrice { get; set; } |
|||
|
|||
|
|||
public decimal PriceWithoutDiscount { get; set; } |
|||
|
|||
public decimal Price { get; set; } |
|||
|
|||
public decimal DiscountedPrice { get; set; } |
|||
|
|||
|
|||
public List<ProductDiscountInfoModel> ProductDiscounts { get; set; } = new(); |
|||
|
|||
public List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; set; } = new(); |
|||
|
|||
public int Inventory { get; set; } |
|||
|
|||
|
|||
public long Sold { get; set; } |
|||
|
|||
|
|||
public int OrderMinQuantity { get; set; } |
|||
|
|||
|
|||
public int OrderMaxQuantity { get; set; } |
|||
|
|||
|
|||
public TimeSpan? PaymentExpireIn { get; set; } |
|||
|
|||
public string MediaResources { get; set; } |
|||
|
|||
|
|||
public Guid? ProductDetailId { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using Volo.Abp; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public class DiscountAmountOverflowException : BusinessException |
|||
{ |
|||
public DiscountAmountOverflowException() : base(ProductsErrorCodes.DiscountAmountOverflow) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public abstract class DiscountInfoModel |
|||
{ |
|||
[NotNull] |
|||
public string Name { get; set; } |
|||
|
|||
[CanBeNull] |
|||
public string Key { get; set; } |
|||
|
|||
[CanBeNull] |
|||
public string DisplayName { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// When the discount begins to take effect.
|
|||
/// </summary>
|
|||
public DateTime? FromTime { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// When the discount ends.
|
|||
/// </summary>
|
|||
public DateTime? ToTime { get; set; } |
|||
|
|||
public DiscountInfoModel() |
|||
{ |
|||
} |
|||
|
|||
public DiscountInfoModel([NotNull] string name, [CanBeNull] string key, [CanBeNull] string displayName, |
|||
DateTime? fromTime, DateTime? toTime) |
|||
{ |
|||
if (fromTime > toTime) |
|||
{ |
|||
throw new InvalidTimePeriodException(); |
|||
} |
|||
|
|||
Name = name; |
|||
Key = key; |
|||
DisplayName = displayName; |
|||
FromTime = fromTime; |
|||
ToTime = toTime; |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public class DiscountsInfoModel : IHasDiscountsInfo |
|||
{ |
|||
public List<ProductDiscountInfoModel> ProductDiscounts { get; set; } |
|||
|
|||
public List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; set; } |
|||
|
|||
public DiscountsInfoModel() |
|||
{ |
|||
ProductDiscounts = new List<ProductDiscountInfoModel>(); |
|||
OrderDiscountPreviews = new List<OrderDiscountPreviewInfoModel>(); |
|||
} |
|||
|
|||
public DiscountsInfoModel(List<ProductDiscountInfoModel> productDiscounts, |
|||
List<OrderDiscountPreviewInfoModel> orderDiscountPreviews) |
|||
{ |
|||
ProductDiscounts = productDiscounts ?? new List<ProductDiscountInfoModel>(); |
|||
OrderDiscountPreviews = orderDiscountPreviews ?? new List<OrderDiscountPreviewInfoModel>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
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,16 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public interface IHasDiscountsInfo |
|||
{ |
|||
/// <summary>
|
|||
/// The Price of the ProductSku has been subtracted from these product discounts.
|
|||
/// </summary>
|
|||
List<ProductDiscountInfoModel> ProductDiscounts { get; } |
|||
|
|||
/// <summary>
|
|||
/// These order discount previews do not change the Price. They will be effective after placing an order.
|
|||
/// </summary>
|
|||
List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; } |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public interface IHasFullDiscountsInfo : IHasDiscountsInfo |
|||
{ |
|||
/// <summary>
|
|||
/// The realtime price without subtracting the discount amount.
|
|||
/// </summary>
|
|||
decimal PriceWithoutDiscount { get; } |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public interface IProductView : IProductBase, IHasDiscountsInfo, IHasProductGroupDisplayName |
|||
{ |
|||
decimal? MinimumPrice { get; } |
|||
|
|||
decimal? MaximumPrice { get; } |
|||
|
|||
decimal? MinimumPriceWithoutDiscount { get; } |
|||
|
|||
decimal? MaximumPriceWithoutDiscount { get; } |
|||
|
|||
long Sold { get; } |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using Volo.Abp; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public class InvalidTimePeriodException : BusinessException |
|||
{ |
|||
public InvalidTimePeriodException() : base(ProductsErrorCodes.InvalidTimePeriod) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
|
|||
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) |
|||
{ |
|||
if (minDiscountedAmount < decimal.Zero || maxDiscountedAmount < decimal.Zero || |
|||
minDiscountedAmount > maxDiscountedAmount) |
|||
{ |
|||
throw new DiscountAmountOverflowException(); |
|||
} |
|||
|
|||
MinDiscountedAmount = minDiscountedAmount; |
|||
MaxDiscountedAmount = maxDiscountedAmount; |
|||
} |
|||
|
|||
public object Clone() |
|||
{ |
|||
return new OrderDiscountPreviewInfoModel(Name, Key, DisplayName, MinDiscountedAmount, MaxDiscountedAmount, |
|||
FromTime, ToTime); |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
return obj is OrderDiscountPreviewInfoModel other && Equals(other); |
|||
} |
|||
|
|||
protected bool Equals(OrderDiscountPreviewInfoModel other) |
|||
{ |
|||
return 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); |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
unchecked |
|||
{ |
|||
var hashCode = Name.GetHashCode(); |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
[Serializable] |
|||
public class ProductDiscountInfoModel : DiscountInfoModel, ICloneable |
|||
{ |
|||
public decimal DiscountedAmount { 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) |
|||
{ |
|||
if (discountedAmount < decimal.Zero) |
|||
{ |
|||
throw new DiscountAmountOverflowException(); |
|||
} |
|||
|
|||
DiscountedAmount = discountedAmount; |
|||
} |
|||
|
|||
public object Clone() |
|||
{ |
|||
return new ProductDiscountInfoModel(Name, Key, DisplayName, DiscountedAmount, FromTime, ToTime); |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
return obj is ProductDiscountInfoModel other && Equals(other); |
|||
} |
|||
|
|||
protected bool Equals(ProductDiscountInfoModel other) |
|||
{ |
|||
return Name == other.Name && |
|||
Key == other.Key && |
|||
DisplayName == other.DisplayName && |
|||
DiscountedAmount == other.DiscountedAmount && |
|||
Nullable.Equals(FromTime, other.FromTime) && |
|||
Nullable.Equals(ToTime, other.ToTime); |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
unchecked |
|||
{ |
|||
var hashCode = Name.GetHashCode(); |
|||
hashCode = (hashCode * 397) ^ (Key != null ? Key.GetHashCode() : 0); |
|||
hashCode = (hashCode * 397) ^ (DisplayName != null ? DisplayName.GetHashCode() : 0); |
|||
hashCode = (hashCode * 397) ^ DiscountedAmount.GetHashCode(); |
|||
hashCode = (hashCode * 397) ^ FromTime.GetHashCode(); |
|||
hashCode = (hashCode * 397) ^ ToTime.GetHashCode(); |
|||
return hashCode; |
|||
} |
|||
} |
|||
} |
|||
@ -1,10 +1,8 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products |
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public interface IProductDiscountProvider |
|||
{ |
|||
public interface IProductDiscountProvider |
|||
{ |
|||
Task<decimal> GetDiscountedPriceAsync(Product product, ProductSku productSku, decimal currentPrice); |
|||
} |
|||
Task DiscountAsync(ProductDiscountContext context); |
|||
} |
|||
@ -1,9 +1,37 @@ |
|||
namespace EasyAbp.EShop.Products.Products |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Timing; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public class PriceDataModel : IHasFullDiscountsInfo |
|||
{ |
|||
public class PriceDataModel |
|||
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, IClock clock) |
|||
{ |
|||
public decimal Price { get; set; } |
|||
|
|||
public decimal DiscountedPrice { get; set; } |
|||
if (PriceWithoutDiscount < decimal.Zero) |
|||
{ |
|||
throw new OverflowException(); |
|||
} |
|||
|
|||
Now = clock.Now; |
|||
PriceWithoutDiscount = priceWithoutDiscount; |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using Volo.Abp.Timing; |
|||
|
|||
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, IClock clock) |
|||
{ |
|||
Product = product; |
|||
ProductSku = productSku; |
|||
PriceDataModel = new PriceDataModel(priceFromPriceProvider, clock); |
|||
} |
|||
} |
|||
@ -1,20 +0,0 @@ |
|||
using System; |
|||
using EasyAbp.EShop.Products.EntityFrameworkCore.AttributeOptionIds; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
|||
|
|||
namespace EasyAbp.EShop.Products.EntityFrameworkCore; |
|||
|
|||
public static class EShopProductsEntityTypeBuilderExtensions |
|||
{ |
|||
public static void TryConfigureAttributeOptionIds(this EntityTypeBuilder b) |
|||
{ |
|||
if (b.Metadata.ClrType.IsAssignableTo<IHasAttributeOptionIds>()) |
|||
{ |
|||
b.Property(nameof(IHasAttributeOptionIds.AttributeOptionIds)) |
|||
.HasConversion<AttributeOptionIdsValueConverter>() |
|||
.Metadata.SetValueComparer(new AttributeOptionIdsValueComparer()); |
|||
} |
|||
} |
|||
} |
|||
2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/EntityFrameworkCore/AttributeOptionIds/AttributeOptionIdsValueComparer.cs → modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/EntityFrameworkCore/ValueMappings/AttributeOptionIdsValueComparer.cs
2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/EntityFrameworkCore/AttributeOptionIds/AttributeOptionIdsValueComparer.cs → modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/EntityFrameworkCore/ValueMappings/AttributeOptionIdsValueComparer.cs
2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/EntityFrameworkCore/AttributeOptionIds/AttributeOptionIdsValueConverter.cs → modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/EntityFrameworkCore/ValueMappings/AttributeOptionIdsValueConverter.cs
2
modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/EntityFrameworkCore/AttributeOptionIds/AttributeOptionIdsValueConverter.cs → modules/EasyAbp.EShop.Products/src/EasyAbp.EShop.Products.EntityFrameworkCore/EasyAbp/EShop/Products/EntityFrameworkCore/ValueMappings/AttributeOptionIdsValueConverter.cs
@ -0,0 +1,29 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using Microsoft.EntityFrameworkCore.ChangeTracking; |
|||
|
|||
namespace EasyAbp.EShop.Products.EntityFrameworkCore.ValueMappings; |
|||
|
|||
public class ProductDiscountsInfoValueComparer : ValueComparer<List<ProductDiscountInfoModel>> |
|||
{ |
|||
public ProductDiscountsInfoValueComparer() |
|||
: base( |
|||
(d1, d2) => d1.SequenceEqual(d2), |
|||
d => d.Aggregate(0, (k, v) => HashCode.Combine(k, v.GetHashCode())), |
|||
d => d.Select(x => (ProductDiscountInfoModel)x.Clone()).ToList()) |
|||
{ |
|||
} |
|||
} |
|||
|
|||
public class OrderDiscountPreviewsInfoValueComparer : ValueComparer<List<OrderDiscountPreviewInfoModel>> |
|||
{ |
|||
public OrderDiscountPreviewsInfoValueComparer() |
|||
: base( |
|||
(d1, d2) => d1.SequenceEqual(d2), |
|||
d => d.Aggregate(0, (k, v) => HashCode.Combine(k, v.GetHashCode())), |
|||
d => new List<OrderDiscountPreviewInfoModel>(d)) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using System.Collections.Generic; |
|||
using System.Text.Json; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |
|||
|
|||
namespace EasyAbp.EShop.Products.EntityFrameworkCore.ValueMappings; |
|||
|
|||
public class ProductDiscountsInfoValueConverter : ValueConverter<List<ProductDiscountInfoModel>, string> |
|||
{ |
|||
public ProductDiscountsInfoValueConverter() : base( |
|||
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null), |
|||
v => JsonSerializer.Deserialize<List<ProductDiscountInfoModel>>(v, (JsonSerializerOptions)null)) |
|||
{ |
|||
} |
|||
} |
|||
|
|||
public class OrderDiscountPreviewsInfoValueConverter : ValueConverter<List<OrderDiscountPreviewInfoModel>, string> |
|||
{ |
|||
public OrderDiscountPreviewsInfoValueConverter() : base( |
|||
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null), |
|||
v => JsonSerializer.Deserialize<List<OrderDiscountPreviewInfoModel>>(v, (JsonSerializerOptions)null)) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using System; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
|||
|
|||
namespace EasyAbp.EShop.Products.EntityFrameworkCore.ValueMappings; |
|||
|
|||
public static class EShopProductsEntityTypeBuilderExtensions |
|||
{ |
|||
public static void TryConfigureAttributeOptionIds(this EntityTypeBuilder b) |
|||
{ |
|||
if (b.Metadata.ClrType.IsAssignableTo<IHasAttributeOptionIds>()) |
|||
{ |
|||
b.Property(nameof(IHasAttributeOptionIds.AttributeOptionIds)) |
|||
.HasConversion<AttributeOptionIdsValueConverter>() |
|||
.Metadata.SetValueComparer(new AttributeOptionIdsValueComparer()); |
|||
} |
|||
} |
|||
|
|||
public static void TryConfigureDiscountsInfo(this EntityTypeBuilder b) |
|||
{ |
|||
if (b.Metadata.ClrType.IsAssignableTo<IHasDiscountsInfo>()) |
|||
{ |
|||
b.Property(nameof(IHasDiscountsInfo.ProductDiscounts)) |
|||
.HasConversion<ProductDiscountsInfoValueConverter>() |
|||
.Metadata.SetValueComparer(new ProductDiscountsInfoValueComparer()); |
|||
b.Property(nameof(IHasDiscountsInfo.OrderDiscountPreviews)) |
|||
.HasConversion<OrderDiscountPreviewsInfoValueConverter>() |
|||
.Metadata.SetValueComparer(new OrderDiscountPreviewsInfoValueComparer()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Timing; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public class DemoProductDiscountProvider : IProductDiscountProvider |
|||
{ |
|||
private readonly IClock _clock; |
|||
|
|||
public DemoProductDiscountProvider(IClock clock) |
|||
{ |
|||
_clock = clock; |
|||
} |
|||
|
|||
public Task DiscountAsync(ProductDiscountContext context) |
|||
{ |
|||
if (context.Product.Id != ProductsTestData.Product1Id || |
|||
context.ProductSku.Id != ProductsTestData.Product1Sku1Id) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
context.PriceDataModel.ProductDiscounts.AddRange(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> |
|||
{ |
|||
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)), |
|||
}); |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System.Threading.Tasks; |
|||
using EasyAbp.EShop.Products.Products.Dtos; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace EasyAbp.EShop.Products.Products; |
|||
|
|||
public class ProductDiscountTests : ProductsApplicationTestBase |
|||
{ |
|||
protected override void AfterAddApplication(IServiceCollection services) |
|||
{ |
|||
services.AddTransient<IProductDiscountProvider, DemoProductDiscountProvider>(); |
|||
base.AfterAddApplication(services); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_Product_With_Discount() |
|||
{ |
|||
var productAppService = GetRequiredService<IProductAppService>(); |
|||
|
|||
var product1 = await productAppService.GetAsync(ProductsTestData.Product1Id); |
|||
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.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.OrderDiscountPreviews.Count.ShouldBe(2); |
|||
sku1.OrderDiscountPreviews.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "1"); |
|||
sku1.OrderDiscountPreviews.ShouldContain(x => x.Name == "DemoDiscount" && x.Key == "2"); |
|||
|
|||
sku2.Price.ShouldBe(2m); |
|||
sku2.ProductDiscounts.ShouldBeEmpty(); |
|||
sku2.OrderDiscountPreviews.ShouldBeEmpty(); |
|||
} |
|||
} |
|||
@ -1,25 +1,28 @@ |
|||
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems |
|||
using System.Collections.Generic; |
|||
using EasyAbp.EShop.Products.Products; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems |
|||
{ |
|||
public class ProductDataModel : IProductData |
|||
{ |
|||
public string MediaResources { get; set; } |
|||
|
|||
|
|||
public string ProductUniqueName { get; set; } |
|||
|
|||
|
|||
public string ProductDisplayName { get; set; } |
|||
|
|||
|
|||
public string SkuName { get; set; } |
|||
|
|||
|
|||
public string SkuDescription { get; set; } |
|||
|
|||
|
|||
public string Currency { get; set; } |
|||
|
|||
public decimal UnitPrice { get; set; } |
|||
|
|||
public decimal TotalPrice { get; set; } |
|||
|
|||
public decimal TotalDiscount { get; set; } |
|||
|
|||
|
|||
public decimal PriceWithoutDiscount { get; set; } |
|||
|
|||
public List<ProductDiscountInfoModel> ProductDiscounts { get; set; } = new(); |
|||
|
|||
public List<OrderDiscountPreviewInfoModel> OrderDiscountPreviews { get; set; } = new(); |
|||
|
|||
public int Inventory { get; set; } |
|||
} |
|||
} |
|||
@ -1,27 +1,8 @@ |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.Data; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems; |
|||
|
|||
public interface IBasketItem : IProductData, IHasExtraProperties |
|||
public interface IBasketItem : IBasketItemInfo |
|||
{ |
|||
Guid Id { get; } |
|||
|
|||
[NotNull] |
|||
string BasketName { get; } |
|||
|
|||
Guid StoreId { get; } |
|||
|
|||
Guid ProductId { get; } |
|||
|
|||
Guid ProductSkuId { get; } |
|||
|
|||
int Quantity { get; } |
|||
|
|||
bool IsInvalid { get; } |
|||
|
|||
void SetIsInvalid(bool isInvalid); |
|||
|
|||
void UpdateProductData(int quantity, IProductData productData); |
|||
void Update(int quantity, IProductData productData); |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.Data; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems; |
|||
|
|||
public interface IBasketItemInfo : IProductData, IHasExtraProperties |
|||
{ |
|||
Guid Id { get; } |
|||
|
|||
[NotNull] |
|||
string BasketName { get; } |
|||
|
|||
Guid StoreId { get; } |
|||
|
|||
Guid ProductId { get; } |
|||
|
|||
Guid ProductSkuId { get; } |
|||
|
|||
int Quantity { get; } |
|||
|
|||
/// <summary>
|
|||
/// PriceWithoutDiscount * Quantity
|
|||
/// </summary>
|
|||
decimal TotalPriceWithoutDiscount { get; } |
|||
|
|||
bool IsInvalid { get; } |
|||
} |
|||
@ -1,25 +1,21 @@ |
|||
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems |
|||
using EasyAbp.EShop.Products.Products; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems |
|||
{ |
|||
public interface IProductData |
|||
public interface IProductData : IHasFullDiscountsInfo |
|||
{ |
|||
string MediaResources { get; } |
|||
|
|||
|
|||
string ProductUniqueName { get; } |
|||
|
|||
|
|||
string ProductDisplayName { get; } |
|||
|
|||
|
|||
string SkuName { get; } |
|||
|
|||
|
|||
string SkuDescription { get; } |
|||
|
|||
string Currency { get; } |
|||
|
|||
decimal UnitPrice { get; } |
|||
|
|||
decimal TotalPrice { get; } |
|||
|
|||
decimal TotalDiscount { get; } |
|||
|
|||
|
|||
int Inventory { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using System; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Baskets.BasketItems; |
|||
|
|||
public interface IServerSideBasketItemInfo : IBasketItemInfo |
|||
{ |
|||
Guid UserId { get; } |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using Microsoft.EntityFrameworkCore.ChangeTracking; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Baskets.EntityFrameworkCore.ValueMappings; |
|||
|
|||
public class ProductDiscountsInfoValueComparer : ValueComparer<List<ProductDiscountInfoModel>> |
|||
{ |
|||
public ProductDiscountsInfoValueComparer() |
|||
: base( |
|||
(d1, d2) => d1.SequenceEqual(d2), |
|||
d => d.Aggregate(0, (k, v) => HashCode.Combine(k, v.GetHashCode())), |
|||
d => d.Select(x => (ProductDiscountInfoModel)x.Clone()).ToList()) |
|||
{ |
|||
} |
|||
} |
|||
|
|||
public class OrderDiscountPreviewsInfoValueComparer : ValueComparer<List<OrderDiscountPreviewInfoModel>> |
|||
{ |
|||
public OrderDiscountPreviewsInfoValueComparer() |
|||
: base( |
|||
(d1, d2) => d1.SequenceEqual(d2), |
|||
d => d.Aggregate(0, (k, v) => HashCode.Combine(k, v.GetHashCode())), |
|||
d => new List<OrderDiscountPreviewInfoModel>(d)) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using System.Collections.Generic; |
|||
using System.Text.Json; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Baskets.EntityFrameworkCore.ValueMappings; |
|||
|
|||
public class ProductDiscountsInfoValueConverter : ValueConverter<List<ProductDiscountInfoModel>, string> |
|||
{ |
|||
public ProductDiscountsInfoValueConverter() : base( |
|||
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null), |
|||
v => JsonSerializer.Deserialize<List<ProductDiscountInfoModel>>(v, (JsonSerializerOptions)null)) |
|||
{ |
|||
} |
|||
} |
|||
|
|||
public class OrderDiscountPreviewsInfoValueConverter : ValueConverter<List<OrderDiscountPreviewInfoModel>, string> |
|||
{ |
|||
public OrderDiscountPreviewsInfoValueConverter() : base( |
|||
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null), |
|||
v => JsonSerializer.Deserialize<List<OrderDiscountPreviewInfoModel>>(v, (JsonSerializerOptions)null)) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using System; |
|||
using EasyAbp.EShop.Products.Products; |
|||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
|||
|
|||
namespace EasyAbp.EShop.Plugins.Baskets.EntityFrameworkCore.ValueMappings; |
|||
|
|||
public static class EShopProductsEntityTypeBuilderExtensions |
|||
{ |
|||
public static void TryConfigureDiscountsInfo(this EntityTypeBuilder b) |
|||
{ |
|||
if (b.Metadata.ClrType.IsAssignableTo<IHasDiscountsInfo>()) |
|||
{ |
|||
b.Property(nameof(IHasDiscountsInfo.ProductDiscounts)) |
|||
.HasConversion<ProductDiscountsInfoValueConverter>() |
|||
.Metadata.SetValueComparer(new ProductDiscountsInfoValueComparer()); |
|||
b.Property(nameof(IHasDiscountsInfo.OrderDiscountPreviews)) |
|||
.HasConversion<OrderDiscountPreviewsInfoValueConverter>() |
|||
.Metadata.SetValueComparer(new OrderDiscountPreviewsInfoValueComparer()); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,109 @@ |
|||
using Microsoft.EntityFrameworkCore.Migrations; |
|||
|
|||
#nullable disable |
|||
|
|||
namespace EShopSample.Migrations |
|||
{ |
|||
/// <inheritdoc />
|
|||
public partial class ImplementedProductDiscounts : Migration |
|||
{ |
|||
/// <inheritdoc />
|
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropColumn( |
|||
name: "TotalDiscount", |
|||
table: "EasyAbpEShopPluginsBasketsBasketItems"); |
|||
|
|||
migrationBuilder.RenameColumn( |
|||
name: "UnitPrice", |
|||
table: "EasyAbpEShopPluginsBasketsBasketItems", |
|||
newName: "TotalPriceWithoutDiscount"); |
|||
|
|||
migrationBuilder.RenameColumn( |
|||
name: "TotalPrice", |
|||
table: "EasyAbpEShopPluginsBasketsBasketItems", |
|||
newName: "PriceWithoutDiscount"); |
|||
|
|||
migrationBuilder.AddColumn<decimal>( |
|||
name: "MaximumPriceWithoutDiscount", |
|||
table: "EasyAbpEShopProductsProductViews", |
|||
type: "decimal(20,8)", |
|||
nullable: true); |
|||
|
|||
migrationBuilder.AddColumn<decimal>( |
|||
name: "MinimumPriceWithoutDiscount", |
|||
table: "EasyAbpEShopProductsProductViews", |
|||
type: "decimal(20,8)", |
|||
nullable: true); |
|||
|
|||
migrationBuilder.AddColumn<string>( |
|||
name: "OrderDiscountPreviews", |
|||
table: "EasyAbpEShopProductsProductViews", |
|||
type: "nvarchar(max)", |
|||
nullable: true); |
|||
|
|||
migrationBuilder.AddColumn<string>( |
|||
name: "ProductDiscounts", |
|||
table: "EasyAbpEShopProductsProductViews", |
|||
type: "nvarchar(max)", |
|||
nullable: true); |
|||
|
|||
migrationBuilder.AddColumn<string>( |
|||
name: "OrderDiscountPreviews", |
|||
table: "EasyAbpEShopPluginsBasketsBasketItems", |
|||
type: "nvarchar(max)", |
|||
nullable: true); |
|||
|
|||
migrationBuilder.AddColumn<string>( |
|||
name: "ProductDiscounts", |
|||
table: "EasyAbpEShopPluginsBasketsBasketItems", |
|||
type: "nvarchar(max)", |
|||
nullable: true); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropColumn( |
|||
name: "MaximumPriceWithoutDiscount", |
|||
table: "EasyAbpEShopProductsProductViews"); |
|||
|
|||
migrationBuilder.DropColumn( |
|||
name: "MinimumPriceWithoutDiscount", |
|||
table: "EasyAbpEShopProductsProductViews"); |
|||
|
|||
migrationBuilder.DropColumn( |
|||
name: "OrderDiscountPreviews", |
|||
table: "EasyAbpEShopProductsProductViews"); |
|||
|
|||
migrationBuilder.DropColumn( |
|||
name: "ProductDiscounts", |
|||
table: "EasyAbpEShopProductsProductViews"); |
|||
|
|||
migrationBuilder.DropColumn( |
|||
name: "OrderDiscountPreviews", |
|||
table: "EasyAbpEShopPluginsBasketsBasketItems"); |
|||
|
|||
migrationBuilder.DropColumn( |
|||
name: "ProductDiscounts", |
|||
table: "EasyAbpEShopPluginsBasketsBasketItems"); |
|||
|
|||
migrationBuilder.RenameColumn( |
|||
name: "TotalPriceWithoutDiscount", |
|||
table: "EasyAbpEShopPluginsBasketsBasketItems", |
|||
newName: "UnitPrice"); |
|||
|
|||
migrationBuilder.RenameColumn( |
|||
name: "PriceWithoutDiscount", |
|||
table: "EasyAbpEShopPluginsBasketsBasketItems", |
|||
newName: "TotalPrice"); |
|||
|
|||
migrationBuilder.AddColumn<decimal>( |
|||
name: "TotalDiscount", |
|||
table: "EasyAbpEShopPluginsBasketsBasketItems", |
|||
type: "decimal(20,8)", |
|||
nullable: false, |
|||
defaultValue: 0m); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue