From 2b40b0bb71776411ae53b17b8664f2e52305b298 Mon Sep 17 00:00:00 2001 From: inflop Date: Tue, 28 Apr 2026 19:59:30 +0200 Subject: [PATCH] refactor: deduplicate net/discount calculation in line item strategies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce CalculateNetAndDiscount and CalculateGrossAndNetDiscount on InvoiceLineItem to compute both values in a single pass, avoiding a redundant second invocation of TotalNetWith / TotalGrossWith. All three calculation strategies now use these combined methods. Remove the default Calculate(lineItems, rounding) overload from IVatCalculationStrategy — callers must provide explicit discount behavior. Also: - Change Quantity.One from a computed property to a static readonly field to avoid unnecessary allocations - Multi-target test project: net8.0;net9.0;net10.0 Co-authored-by: Copilot --- .../FromSumOfGrossValuesStrategy.cs | 3 +- .../Calculation/FromSumOfNetValuesStrategy.cs | 3 +- .../Calculation/IVatCalculationStrategy.cs | 3 -- .../SumOfLineItemVatAmountsStrategy.cs | 3 +- .../ValueObjects/InvoiceLineItem.cs | 28 +++++++++++++++++-- src/Inflop.VatSharp/ValueObjects/Quantity.cs | 2 +- .../Inflop.VatSharp.Tests.csproj | 2 +- 7 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/Inflop.VatSharp/Strategies/Calculation/FromSumOfGrossValuesStrategy.cs b/src/Inflop.VatSharp/Strategies/Calculation/FromSumOfGrossValuesStrategy.cs index 229fd2b..6481a22 100644 --- a/src/Inflop.VatSharp/Strategies/Calculation/FromSumOfGrossValuesStrategy.cs +++ b/src/Inflop.VatSharp/Strategies/Calculation/FromSumOfGrossValuesStrategy.cs @@ -50,8 +50,7 @@ public DocumentAmounts Calculate(IReadOnlyList lineItems, IRoun foreach (var (item, originalIndex) in group) { - var itemGross = item.TotalGrossWith(discountBehavior, rounding).Round(rounding); - var itemDiscount = item.DiscountAmountNetWith(discountBehavior, rounding).Round(rounding); + var (itemGross, itemDiscount) = item.CalculateGrossAndNetDiscount(discountBehavior, rounding); sumGross += itemGross; sumDiscount += itemDiscount; diff --git a/src/Inflop.VatSharp/Strategies/Calculation/FromSumOfNetValuesStrategy.cs b/src/Inflop.VatSharp/Strategies/Calculation/FromSumOfNetValuesStrategy.cs index c7b90dd..43b3bce 100644 --- a/src/Inflop.VatSharp/Strategies/Calculation/FromSumOfNetValuesStrategy.cs +++ b/src/Inflop.VatSharp/Strategies/Calculation/FromSumOfNetValuesStrategy.cs @@ -36,8 +36,7 @@ public DocumentAmounts Calculate(IReadOnlyList lineItems, IRoun foreach (var (item, originalIndex) in group) { - var itemNet = item.TotalNetWith(discountBehavior, rounding).Round(rounding); - var itemDiscount = item.DiscountAmountNetWith(discountBehavior, rounding).Round(rounding); + var (itemNet, itemDiscount) = item.CalculateNetAndDiscount(discountBehavior, rounding); sumNet += itemNet; sumDiscount += itemDiscount; diff --git a/src/Inflop.VatSharp/Strategies/Calculation/IVatCalculationStrategy.cs b/src/Inflop.VatSharp/Strategies/Calculation/IVatCalculationStrategy.cs index 80878c9..0b798c4 100644 --- a/src/Inflop.VatSharp/Strategies/Calculation/IVatCalculationStrategy.cs +++ b/src/Inflop.VatSharp/Strategies/Calculation/IVatCalculationStrategy.cs @@ -18,9 +18,6 @@ internal interface IVatCalculationStrategy DocumentAmounts Calculate(IReadOnlyList lineItems, IRoundingStrategy rounding, IAbsoluteDiscountBehavior discountBehavior); - DocumentAmounts Calculate(IReadOnlyList lineItems, IRoundingStrategy rounding) - => Calculate(lineItems, rounding, FromTotalAbsoluteDiscountBehavior.Instance); - /// /// Converts a per-rate FCY summary to base currency using this strategy's authoritative field. /// Each strategy knows which field drives the others, so the conversion mirrors that hierarchy. diff --git a/src/Inflop.VatSharp/Strategies/Calculation/SumOfLineItemVatAmountsStrategy.cs b/src/Inflop.VatSharp/Strategies/Calculation/SumOfLineItemVatAmountsStrategy.cs index 8f4c166..e9c5b33 100644 --- a/src/Inflop.VatSharp/Strategies/Calculation/SumOfLineItemVatAmountsStrategy.cs +++ b/src/Inflop.VatSharp/Strategies/Calculation/SumOfLineItemVatAmountsStrategy.cs @@ -28,8 +28,7 @@ public DocumentAmounts Calculate(IReadOnlyList lineItems, IRoun var itemAmounts = lineItems .Select(item => { - var net = item.TotalNetWith(discountBehavior, rounding).Round(rounding); - var discount = item.DiscountAmountNetWith(discountBehavior, rounding).Round(rounding); + var (net, discount) = item.CalculateNetAndDiscount(discountBehavior, rounding); return LineItemAmounts.FromNet(net, item.VatRate, rounding, discount); }) .ToList(); diff --git a/src/Inflop.VatSharp/ValueObjects/InvoiceLineItem.cs b/src/Inflop.VatSharp/ValueObjects/InvoiceLineItem.cs index 3934541..7174ca1 100644 --- a/src/Inflop.VatSharp/ValueObjects/InvoiceLineItem.cs +++ b/src/Inflop.VatSharp/ValueObjects/InvoiceLineItem.cs @@ -95,10 +95,34 @@ internal Money TotalGrossWith(IAbsoluteDiscountBehavior discountBehavior, IRound : VatRate.GrossFromNet(totalInInputType); } + /// + /// Returns rounded net and net-equivalent discount in a single call, + /// avoiding a redundant second invocation of . + /// + internal (Money Net, Money Discount) CalculateNetAndDiscount(IAbsoluteDiscountBehavior discountBehavior, IRoundingStrategy rounding) + { + var net = TotalNetWith(discountBehavior, rounding); + var discount = Discount.HasValue ? TotalNetBeforeDiscount - net : Money.Zero; + return (net.Round(rounding), discount.Round(rounding)); + } + + /// + /// Returns rounded gross and net-equivalent discount in a single call, + /// avoiding a redundant second invocation of via + /// . + /// + internal (Money Gross, Money NetDiscount) CalculateGrossAndNetDiscount(IAbsoluteDiscountBehavior discountBehavior, IRoundingStrategy rounding) + { + var gross = TotalGrossWith(discountBehavior, rounding); + var netDiscount = Discount.HasValue + ? TotalNetBeforeDiscount - VatRate.NetFromGross(gross) + : Money.Zero; + return (gross.Round(rounding), netDiscount.Round(rounding)); + } + internal LineItemAmounts Calculate(IRoundingStrategy rounding, IAbsoluteDiscountBehavior discountBehavior) { - var net = TotalNetWith(discountBehavior, rounding).Round(rounding); - var discount = DiscountAmountNetWith(discountBehavior, rounding).Round(rounding); + var (net, discount) = CalculateNetAndDiscount(discountBehavior, rounding); return LineItemAmounts.FromNet(net, VatRate, rounding, discount); } diff --git a/src/Inflop.VatSharp/ValueObjects/Quantity.cs b/src/Inflop.VatSharp/ValueObjects/Quantity.cs index 758d1a2..d8444f5 100644 --- a/src/Inflop.VatSharp/ValueObjects/Quantity.cs +++ b/src/Inflop.VatSharp/ValueObjects/Quantity.cs @@ -28,7 +28,7 @@ public sealed record Quantity : IComparable public static Quantity Of(int value) => new(value); /// A quantity of exactly one. - public static Quantity One => new(1m); + public static readonly Quantity One = Of(1); /// public int CompareTo(Quantity? other) diff --git a/tests/Inflop.VatSharp.Tests/Inflop.VatSharp.Tests.csproj b/tests/Inflop.VatSharp.Tests/Inflop.VatSharp.Tests.csproj index 5fff8cf..78ed3cd 100644 --- a/tests/Inflop.VatSharp.Tests/Inflop.VatSharp.Tests.csproj +++ b/tests/Inflop.VatSharp.Tests/Inflop.VatSharp.Tests.csproj @@ -1,7 +1,7 @@ - net10.0 + net8.0;net9.0;net10.0 false false