< Summary

Information
Class: ShopGuard.Core.Helpers.PriceCalculator
Assembly: ShopGuard.Core
File(s): /home/runner/work/ShopGuard/ShopGuard/ShopGuard/ShopGuard.Core/Helpers/PriceCalculator.cs
Line coverage
100%
Covered lines: 21
Uncovered lines: 0
Coverable lines: 21
Total lines: 66
Line coverage: 100%
Branch coverage
100%
Covered branches: 18
Total branches: 18
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
CalculateLineTotal(...)100%11100%
CalculateCartTotal(...)100%11100%
ApplyDiscount(...)100%66100%
ParseDisplayedPrice(...)100%1212100%

File(s)

/home/runner/work/ShopGuard/ShopGuard/ShopGuard/ShopGuard.Core/Helpers/PriceCalculator.cs

#LineLine coverage
 1namespace ShopGuard.Core.Helpers;
 2
 3/// <summary>
 4/// Price arithmetic used to verify cart and checkout totals shown in the UI.
 5/// All amounts are decimal and rounded to two places (MidpointRounding.ToEven,
 6/// matching the shop's banker's rounding).
 7/// </summary>
 8public static class PriceCalculator
 9{
 10    /// <summary>Calculates the total of a single cart line.</summary>
 11    public static decimal CalculateLineTotal(decimal unitPrice, int quantity)
 12    {
 813        ArgumentOutOfRangeException.ThrowIfNegative(unitPrice);
 714        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity);
 15
 516        return Math.Round(unitPrice * quantity, 2, MidpointRounding.ToEven);
 17    }
 18
 19    /// <summary>Calculates the cart subtotal across all lines.</summary>
 20    public static decimal CalculateCartTotal(IEnumerable<CartItem> items)
 21    {
 222        ArgumentNullException.ThrowIfNull(items);
 23
 424        return items.Sum(item => CalculateLineTotal(item.UnitPrice, item.Quantity));
 25    }
 26
 27    /// <summary>Applies a percentage discount (0–100) to a total.</summary>
 28    public static decimal ApplyDiscount(decimal total, decimal discountPercent)
 29    {
 530        ArgumentOutOfRangeException.ThrowIfNegative(total);
 531        if (discountPercent is < 0 or > 100)
 32        {
 233            throw new ArgumentOutOfRangeException(
 234                nameof(discountPercent), discountPercent, "Discount must be between 0 and 100 percent.");
 35        }
 36
 337        return Math.Round(total * (1 - discountPercent / 100m), 2, MidpointRounding.ToEven);
 38    }
 39
 40    /// <summary>
 41    /// Parses a price string as rendered by the shop UI (e.g. "$1,200.00" or "1.200,00 €")
 42    /// into a decimal value.
 43    /// </summary>
 44    public static decimal ParseDisplayedPrice(string displayedPrice)
 45    {
 546        ArgumentException.ThrowIfNullOrWhiteSpace(displayedPrice);
 47
 48        // Strip currency symbols and whitespace, keep digits and separators.
 3849        var cleaned = new string(displayedPrice.Where(c => char.IsDigit(c) || c is '.' or ',').ToArray());
 550        if (cleaned.Length == 0)
 51        {
 152            throw new FormatException($"'{displayedPrice}' contains no numeric value.");
 53        }
 54
 55        // Decide which separator is the decimal one: the right-most of '.' and ','.
 456        var lastDot = cleaned.LastIndexOf('.');
 457        var lastComma = cleaned.LastIndexOf(',');
 458        var decimalSeparator = lastDot > lastComma ? '.' : ',';
 459        var groupSeparator = decimalSeparator == '.' ? ',' : '.';
 60
 461        cleaned = cleaned.Replace(groupSeparator.ToString(), string.Empty);
 462        cleaned = cleaned.Replace(decimalSeparator, '.');
 63
 464        return decimal.Parse(cleaned, System.Globalization.CultureInfo.InvariantCulture);
 65    }
 66}