From b0f693aeec8a058afed8f38a4f67256bc6ce7c52 Mon Sep 17 00:00:00 2001 From: Artem Dudarev Date: Thu, 19 Dec 2024 17:08:38 +0200 Subject: [PATCH] VCST-2305: Add listTotal and showPlacedPrice to LineItem (#25) * Update ApplyRewards() * Fix sonar issue * Update dependencies * Add listTotal * Update dependencies --- .../Extensions/RewardExtensions.cs | 56 ++++++----- .../Schemas/LineItemType.cs | 9 ++ .../VirtoCommerce.XCart.Core.csproj | 12 +-- .../Services/CartAvailMethodsService.cs | 12 +-- src/VirtoCommerce.XCart.Web/module.manifest | 6 +- .../Aggregates/CartAggregateTests.cs | 98 +++++++++++++++++++ .../Helpers/XCartMoqHelper.cs | 31 +++++- .../VirtoCommerce.XCart.Tests.csproj | 1 + 8 files changed, 178 insertions(+), 47 deletions(-) diff --git a/src/VirtoCommerce.XCart.Core/Extensions/RewardExtensions.cs b/src/VirtoCommerce.XCart.Core/Extensions/RewardExtensions.cs index 844f69a..f66d704 100644 --- a/src/VirtoCommerce.XCart.Core/Extensions/RewardExtensions.cs +++ b/src/VirtoCommerce.XCart.Core/Extensions/RewardExtensions.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using VirtoCommerce.CartModule.Core.Model; using VirtoCommerce.CoreModule.Core.Common; +using VirtoCommerce.CoreModule.Core.Currency; using VirtoCommerce.MarketingModule.Core.Model.Promotions; using VirtoCommerce.PaymentModule.Core.Model; using VirtoCommerce.Platform.Core.Common; @@ -13,19 +14,19 @@ namespace VirtoCommerce.XCart.Core.Extensions { public static class RewardExtensions { - public static void ApplyRewards(this PaymentMethod paymentMethod, ICollection rewards) + public static void ApplyRewards(this PaymentMethod paymentMethod, Currency currency, ICollection rewards) => paymentMethod.DiscountAmount = rewards .Where(r => r.IsValid) .OfType() .Where(r => r.PaymentMethod.IsNullOrEmpty() || r.PaymentMethod.EqualsInvariant(paymentMethod.Code)) - .Sum(reward => reward.GetRewardAmount(paymentMethod.Price - paymentMethod.DiscountAmount, 1)); + .Sum(reward => reward.GetTotalAmount(paymentMethod.Price - paymentMethod.DiscountAmount, 1, currency)); - public static void ApplyRewards(this ShippingRate shippingRate, ICollection rewards) + public static void ApplyRewards(this ShippingRate shippingRate, Currency currency, ICollection rewards) => shippingRate.DiscountAmount = rewards .Where(r => r.IsValid) .OfType() .Where(r => r.ShippingMethod.IsNullOrEmpty() || shippingRate.ShippingMethod != null && r.ShippingMethod.EqualsInvariant(shippingRate.ShippingMethod.Code)) - .Sum(reward => reward.GetRewardAmount(shippingRate.Rate, 1)); + .Sum(reward => reward.GetTotalAmount(shippingRate.Rate, 1, currency)); public static void ApplyRewards(this CartAggregate aggregate, ICollection rewards) { @@ -46,7 +47,7 @@ public static void ApplyRewards(this CartAggregate aggregate, ICollection rewards) + public static void ApplyRewards(this LineItem lineItem, Currency currency, IEnumerable rewards) { var lineItemRewards = rewards .Where(r => r.IsValid) @@ -54,6 +55,7 @@ public static void ApplyRewards(this LineItem lineItem, string currency, IEnumer lineItem.Discounts?.Clear(); lineItem.DiscountAmount = Math.Max(0, lineItem.ListPrice - lineItem.SalePrice); + lineItem.IsDiscountAmountRounded = true; if (lineItem.Quantity == 0) { @@ -65,14 +67,14 @@ public static void ApplyRewards(this LineItem lineItem, string currency, IEnumer var discount = new Discount { Coupon = reward.Coupon, - Currency = currency, + Currency = currency.Code, PromotionId = reward.PromotionId ?? reward.Promotion?.Id, Name = reward.Promotion?.Name, Description = reward.Promotion?.Description, - DiscountAmount = reward.GetRewardAmount(lineItem.ListPrice - lineItem.DiscountAmount, lineItem.Quantity), + DiscountAmount = reward.GetAmountPerItem(lineItem.ListPrice - lineItem.DiscountAmount, lineItem.Quantity, currency), }; - // Pass invalid discounts + // Skip invalid discounts if (discount.DiscountAmount <= 0) { continue; @@ -81,10 +83,11 @@ public static void ApplyRewards(this LineItem lineItem, string currency, IEnumer lineItem.Discounts ??= new List(); lineItem.Discounts.Add(discount); lineItem.DiscountAmount += discount.DiscountAmount; + lineItem.IsDiscountAmountRounded &= reward.RoundAmountPerItem; } } - public static void ApplyRewards(this Shipment shipment, string currency, IEnumerable rewards) + public static void ApplyRewards(this Shipment shipment, Currency currency, IEnumerable rewards) { var shipmentRewards = rewards .Where(r => r.IsValid) @@ -98,11 +101,11 @@ public static void ApplyRewards(this Shipment shipment, string currency, IEnumer var discount = new Discount { Coupon = reward.Coupon, - Currency = currency, + Currency = currency.Code, PromotionId = reward.PromotionId ?? reward.Promotion?.Id, Name = reward.Promotion?.Name, Description = reward.Promotion?.Description, - DiscountAmount = reward.GetRewardAmount(shipment.Price - shipment.DiscountAmount, 1), + DiscountAmount = reward.GetTotalAmount(shipment.Price - shipment.DiscountAmount, 1, currency), }; // Pass invalid discounts @@ -116,7 +119,7 @@ public static void ApplyRewards(this Shipment shipment, string currency, IEnumer } } - public static void ApplyRewards(this Payment payment, string currency, IEnumerable rewards) + public static void ApplyRewards(this Payment payment, Currency currency, IEnumerable rewards) { var paymentRewards = rewards .Where(r => r.IsValid) @@ -130,11 +133,11 @@ public static void ApplyRewards(this Payment payment, string currency, IEnumerab var discount = new Discount { Coupon = reward.Coupon, - Currency = currency, + Currency = currency.Code, PromotionId = reward.PromotionId ?? reward.Promotion?.Id, Name = reward.Promotion?.Name, Description = reward.Promotion?.Description, - DiscountAmount = reward.GetRewardAmount(payment.Price - payment.DiscountAmount, 1), + DiscountAmount = reward.GetTotalAmount(payment.Price - payment.DiscountAmount, 1, currency), }; // Pass invalid discounts @@ -167,13 +170,12 @@ public static async Task ApplyRewardsAsync(this CartAggregate aggregate, ICollec // automatically add gift rewards to line items if the setting is enabled if (aggregate.IsSelectedForCheckout) { - var availableGifts = await aggregate.GetAvailableGiftsAsync(rewards); + var availableGifts = (await aggregate.GetAvailableGiftsAsync(rewards)).ToList(); - if (availableGifts.Any()) + if (availableGifts.Count > 0) { - var newGiftItems = availableGifts.Where(x => !x.HasLineItem).ToList(); //get new items - var newGiftItemIds = newGiftItems.Select(x => x.Id).ToList(); - await aggregate.AddGiftItemsAsync(newGiftItemIds, availableGifts.ToList()); //add new items to cart + var newGiftItemIds = availableGifts.Where(x => !x.HasLineItem).Select(x => x.Id).ToList(); + await aggregate.AddGiftItemsAsync(newGiftItemIds, availableGifts); //add new items to cart } } @@ -184,22 +186,22 @@ private static void ApplyCartRewardsInternal(CartAggregate aggregate, ICollectio { var shoppingCart = aggregate.Cart; - var lineItemRewards = rewards.OfType(); - foreach (var lineItem in aggregate.LineItems ?? Enumerable.Empty()) + var lineItemRewards = rewards.OfType().ToList(); + foreach (var lineItem in aggregate.LineItems ?? []) { - lineItem.ApplyRewards(shoppingCart.Currency, lineItemRewards); + lineItem.ApplyRewards(aggregate.Currency, lineItemRewards); } - var shipmentRewards = rewards.OfType(); + var shipmentRewards = rewards.OfType().ToList(); foreach (var shipment in shoppingCart.Shipments ?? Enumerable.Empty()) { - shipment.ApplyRewards(shoppingCart.Currency, shipmentRewards); + shipment.ApplyRewards(aggregate.Currency, shipmentRewards); } - var paymentRewards = rewards.OfType(); + var paymentRewards = rewards.OfType().ToList(); foreach (var payment in shoppingCart.Payments ?? Enumerable.Empty()) { - payment.ApplyRewards(shoppingCart.Currency, paymentRewards); + payment.ApplyRewards(aggregate.Currency, paymentRewards); } var subTotalExcludeDiscount = shoppingCart.Items.Where(li => li.SelectedForCheckout).Sum(li => (li.ListPrice - li.DiscountAmount) * li.Quantity); @@ -217,7 +219,7 @@ private static void ApplyCartRewardsInternal(CartAggregate aggregate, ICollectio PromotionId = reward.PromotionId ?? reward.Promotion?.Id, Name = reward.Promotion?.Name, Description = reward.Promotion?.Description, - DiscountAmount = reward.GetRewardAmount(subTotalExcludeDiscount, 1), + DiscountAmount = reward.GetTotalAmount(subTotalExcludeDiscount, 1, aggregate.Currency), }; shoppingCart.Discounts ??= new List(); diff --git a/src/VirtoCommerce.XCart.Core/Schemas/LineItemType.cs b/src/VirtoCommerce.XCart.Core/Schemas/LineItemType.cs index 8b2ff2f..9c6a05d 100644 --- a/src/VirtoCommerce.XCart.Core/Schemas/LineItemType.cs +++ b/src/VirtoCommerce.XCart.Core/Schemas/LineItemType.cs @@ -144,6 +144,15 @@ public LineItemType( Field>("listPriceWithTax", "List price with tax", resolve: context => context.Source.ListPriceWithTax.ToMoney(context.GetCart().Currency)); + Field>("listTotal", + "List total", + resolve: context => context.Source.ListTotal.ToMoney(context.GetCart().Currency)); + Field>("listTotalWithTax", + "List total with tax", + resolve: context => context.Source.ListTotalWithTax.ToMoney(context.GetCart().Currency)); + Field>("showPlacedPrice", + "Indicates whether the PlacedPrice should be visible to the customer", + resolve: context => context.Source.IsDiscountAmountRounded); Field>("placedPrice", "Placed price", resolve: context => context.Source.PlacedPrice.ToMoney(context.GetCart().Currency)); diff --git a/src/VirtoCommerce.XCart.Core/VirtoCommerce.XCart.Core.csproj b/src/VirtoCommerce.XCart.Core/VirtoCommerce.XCart.Core.csproj index e70ca11..df12ae5 100644 --- a/src/VirtoCommerce.XCart.Core/VirtoCommerce.XCart.Core.csproj +++ b/src/VirtoCommerce.XCart.Core/VirtoCommerce.XCart.Core.csproj @@ -10,13 +10,13 @@ - - - - - + - + + + + + \ No newline at end of file diff --git a/src/VirtoCommerce.XCart.Data/Services/CartAvailMethodsService.cs b/src/VirtoCommerce.XCart.Data/Services/CartAvailMethodsService.cs index 904a2fb..d927d66 100644 --- a/src/VirtoCommerce.XCart.Data/Services/CartAvailMethodsService.cs +++ b/src/VirtoCommerce.XCart.Data/Services/CartAvailMethodsService.cs @@ -53,7 +53,7 @@ public async Task> GetAvailableShippingRatesAsync(Cart { if (cartAggregate == null) { - return Enumerable.Empty(); + return []; } //Request available shipping rates @@ -75,7 +75,7 @@ public async Task> GetAvailableShippingRatesAsync(Cart if (availableShippingRates.IsNullOrEmpty()) { - return Enumerable.Empty(); + return []; } //Evaluate promotions cart and apply rewards for available shipping methods @@ -90,7 +90,7 @@ public async Task> GetAvailableShippingRatesAsync(Cart var promoEvalResult = await cartAggregate.EvaluatePromotionsAsync(evalContextCartMap.PromotionEvaluationContext); foreach (var shippingRate in availableShippingRates) { - shippingRate.ApplyRewards(promoEvalResult.Rewards); + shippingRate.ApplyRewards(cartAggregate.Currency, promoEvalResult.Rewards); } var taxProvider = await GetActiveTaxProviderAsync(cartAggregate); @@ -115,7 +115,7 @@ public async Task> GetAvailablePaymentMethodsAsync(Ca { if (cartAggregate == null) { - return Enumerable.Empty(); + return []; } var criteria = new PaymentMethodsSearchCriteria @@ -128,7 +128,7 @@ public async Task> GetAvailablePaymentMethodsAsync(Ca var result = await _paymentMethodsSearchService.SearchAsync(criteria); if (result.Results.IsNullOrEmpty()) { - return Enumerable.Empty(); + return []; } var evalContext = AbstractTypeFactory.TryCreateInstance(); @@ -143,7 +143,7 @@ public async Task> GetAvailablePaymentMethodsAsync(Ca foreach (var paymentMethod in result.Results) { - paymentMethod.ApplyRewards(promoResult.Rewards); + paymentMethod.ApplyRewards(cartAggregate.Currency, promoResult.Rewards); } //Evaluate taxes for available payments diff --git a/src/VirtoCommerce.XCart.Web/module.manifest b/src/VirtoCommerce.XCart.Web/module.manifest index 6bb594a..f9f5d00 100644 --- a/src/VirtoCommerce.XCart.Web/module.manifest +++ b/src/VirtoCommerce.XCart.Web/module.manifest @@ -6,14 +6,14 @@ 3.867.0 - + - + - + Cart Experience API diff --git a/tests/VirtoCommerce.XCart.Tests/Aggregates/CartAggregateTests.cs b/tests/VirtoCommerce.XCart.Tests/Aggregates/CartAggregateTests.cs index dc3e74f..501bdb7 100644 --- a/tests/VirtoCommerce.XCart.Tests/Aggregates/CartAggregateTests.cs +++ b/tests/VirtoCommerce.XCart.Tests/Aggregates/CartAggregateTests.cs @@ -7,6 +7,7 @@ using Moq; using VirtoCommerce.CartModule.Core.Model; using VirtoCommerce.CatalogModule.Core.Model; +using VirtoCommerce.CoreModule.Core.Currency; using VirtoCommerce.MarketingModule.Core.Model.Promotions; using VirtoCommerce.PaymentModule.Core.Model; using VirtoCommerce.Platform.Core.Common; @@ -823,6 +824,103 @@ public async Task RecalculateAsync_HasPromoRewards_CalculateTotalsCalled() _shoppingCartTotalsCalculatorMock.Verify(x => x.CalculateTotals(It.Is(x => x == cartAggregate.Cart)), Times.Exactly(2)); } + public static IEnumerable Data => + [ + // Expected Expected Expected Expected + // MidpointRounding, ListPrice, Quantity, RewardAmount, Round, DiscountAmount, SubTotal, DiscountTotal, Total + [MidpointRounding.AwayFromZero, 49.95m, 10, 10m, false, 4.995m, 499.50m, 49.95m, 449.55m], + [MidpointRounding.ToZero, 49.95m, 10, 10m, false, 4.995m, 499.50m, 49.95m, 449.55m], + [MidpointRounding.AwayFromZero, 49.95m, 10, 10m, true, 5.000m, 499.50m, 50.00m, 449.50m], + [MidpointRounding.ToZero, 49.95m, 10, 10m, true, 4.990m, 499.50m, 49.90m, 449.60m], + + [MidpointRounding.AwayFromZero, 0.01m, 10, 10m, false, 0.001m, 0.10m, 0.01m, 0.09m], + [MidpointRounding.ToZero, 0.01m, 10, 10m, false, 0.001m, 0.10m, 0.01m, 0.09m], + [MidpointRounding.AwayFromZero, 0.01m, 10, 10m, true, 0.000m, 0.10m, 0.00m, 0.10m], + [MidpointRounding.ToZero, 0.01m, 10, 10m, true, 0.000m, 0.10m, 0.00m, 0.10m], + + [MidpointRounding.AwayFromZero, 7.50m, 1, 3m, false, 0.225m, 7.50m, 0.23m, 7.27m], + [MidpointRounding.ToZero, 7.50m, 1, 3m, false, 0.225m, 7.50m, 0.22m, 7.28m], + [MidpointRounding.AwayFromZero, 7.50m, 1, 3m, true, 0.230m, 7.50m, 0.23m, 7.27m], + [MidpointRounding.ToZero, 7.50m, 1, 3m, true, 0.220m, 7.50m, 0.22m, 7.28m], + + [MidpointRounding.AwayFromZero, 422.50m, 1, 45m, false, 190.125m, 422.50m, 190.13m, 232.37m], + [MidpointRounding.ToZero, 422.50m, 1, 45m, false, 190.125m, 422.50m, 190.12m, 232.38m], + [MidpointRounding.AwayFromZero, 422.50m, 10, 45m, false, 190.125m, 4225.00m, 1901.25m, 2323.75m], + [MidpointRounding.ToZero, 422.50m, 10, 45m, false, 190.125m, 4225.00m, 1901.25m, 2323.75m], + [MidpointRounding.AwayFromZero, 422.50m, 1, 45m, true, 190.130m, 422.50m, 190.13m, 232.37m], + [MidpointRounding.ToZero, 422.50m, 1, 45m, true, 190.120m, 422.50m, 190.12m, 232.38m], + [MidpointRounding.AwayFromZero, 422.50m, 10, 45m, true, 190.130m, 4225.00m, 1901.30m, 2323.70m], + [MidpointRounding.ToZero, 422.50m, 10, 45m, true, 190.120m, 4225.00m, 1901.20m, 2323.80m], + ]; + + [Theory] + [MemberData(nameof(Data))] + public async Task RecalculateAsync_DiscountAppliedProperly( + MidpointRounding midpointRounding, + decimal listPrice, + int quantity, + decimal rewardAmount, + bool roundRewardAmountPerItem, + decimal expectedDiscountAmount, + decimal expectedSubTotal, + decimal expectedDiscountTotal, + decimal expectedTotal) + { + // Arrange + var currency = _fixture.Create(); + currency.MidpointRounding = midpointRounding.ToString(); + + var lineItem = new LineItem + { + Currency = currency.Code, + ListPrice = listPrice, + SalePrice = listPrice, + Quantity = quantity, + }; + + var cart = new ShoppingCart + { + Currency = currency.Code, + Items = new List { lineItem }, + }; + + var cartAggregate = GetValidCartAggregate(cart, currency); + + var context = new PromotionEvaluationContext(); + + var promotionResult = new PromotionResult(); + var reward = new CatalogItemAmountReward + { + Amount = rewardAmount, + AmountType = RewardAmountType.Relative, + RoundAmountPerItem = roundRewardAmountPerItem, + IsValid = true, + }; + promotionResult.Rewards.Add(reward); + + _mapperMock + .Setup(x => x.Map(It.IsAny())) + .Returns(context); + + _marketingPromoEvaluatorMock + .Setup(x => x.EvaluatePromotionAsync(It.IsAny())) + .ReturnsAsync(promotionResult); + + // Act + await cartAggregate.RecalculateAsync(); + + // Assert + Assert.Equal(expectedSubTotal, cart.SubTotal); + Assert.Equal(expectedDiscountTotal, cart.DiscountTotal); + Assert.Equal(expectedTotal, cart.Total); + + Assert.Equal(expectedSubTotal, lineItem.ListTotal); + Assert.Equal(expectedDiscountAmount, lineItem.DiscountAmount); + Assert.Equal(expectedDiscountTotal, lineItem.DiscountTotal); + Assert.Equal(expectedTotal, lineItem.ExtendedPrice); + Assert.Equal(roundRewardAmountPerItem, lineItem.IsDiscountAmountRounded); + } + #endregion RecalculateAsync #region AddCartAddressAsync diff --git a/tests/VirtoCommerce.XCart.Tests/Helpers/XCartMoqHelper.cs b/tests/VirtoCommerce.XCart.Tests/Helpers/XCartMoqHelper.cs index 5b37a4b..d81b6aa 100644 --- a/tests/VirtoCommerce.XCart.Tests/Helpers/XCartMoqHelper.cs +++ b/tests/VirtoCommerce.XCart.Tests/Helpers/XCartMoqHelper.cs @@ -6,6 +6,7 @@ using Moq; using VirtoCommerce.CartModule.Core.Model; using VirtoCommerce.CartModule.Core.Services; +using VirtoCommerce.CartModule.Data.Services; using VirtoCommerce.CatalogModule.Core.Model; using VirtoCommerce.CoreModule.Core.Common; using VirtoCommerce.CoreModule.Core.Currency; @@ -226,13 +227,11 @@ protected NewCartItem BuildNewCartItem( return newCartItem; } - protected CartAggregate GetValidCartAggregate() + protected CartAggregate GetValidCartAggregate(ShoppingCart cart = null, Currency currency = null) { - var cart = GetCart(); - var aggregate = new CartAggregate( _marketingPromoEvaluatorMock.Object, - _shoppingCartTotalsCalculatorMock.Object, + GeTotalsCalculator(currency), _taxProviderSearchServiceMock.Object, _cartProductServiceMock.Object, _dynamicPropertyUpdaterService.Object, @@ -240,9 +239,31 @@ protected CartAggregate GetValidCartAggregate() _memberService.Object, _genericPipelineLauncherMock.Object); - aggregate.GrabCart(cart, new Store(), GetMember(), GetCurrency()); + aggregate.GrabCart(cart ?? GetCart(), new Store(), GetMember(), currency ?? GetCurrency()); return aggregate; } + + private IShoppingCartTotalsCalculator GeTotalsCalculator(Currency currency) + { + IShoppingCartTotalsCalculator totalsCalculator; + + if (currency != null) + { + var currencyServiceMock = new Mock(); + + currencyServiceMock + .Setup(x => x.GetAllCurrenciesAsync()) + .ReturnsAsync([currency]); + + totalsCalculator = new DefaultShoppingCartTotalsCalculator(currencyServiceMock.Object); + } + else + { + totalsCalculator = _shoppingCartTotalsCalculatorMock.Object; + } + + return totalsCalculator; + } } } diff --git a/tests/VirtoCommerce.XCart.Tests/VirtoCommerce.XCart.Tests.csproj b/tests/VirtoCommerce.XCart.Tests/VirtoCommerce.XCart.Tests.csproj index c821876..4b0412b 100644 --- a/tests/VirtoCommerce.XCart.Tests/VirtoCommerce.XCart.Tests.csproj +++ b/tests/VirtoCommerce.XCart.Tests/VirtoCommerce.XCart.Tests.csproj @@ -12,6 +12,7 @@ + all