| | | 1 | | namespace 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> |
| | | 8 | | public 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 | | { |
| | 8 | 13 | | ArgumentOutOfRangeException.ThrowIfNegative(unitPrice); |
| | 7 | 14 | | ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); |
| | | 15 | | |
| | 5 | 16 | | 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 | | { |
| | 2 | 22 | | ArgumentNullException.ThrowIfNull(items); |
| | | 23 | | |
| | 4 | 24 | | 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 | | { |
| | 5 | 30 | | ArgumentOutOfRangeException.ThrowIfNegative(total); |
| | 5 | 31 | | if (discountPercent is < 0 or > 100) |
| | | 32 | | { |
| | 2 | 33 | | throw new ArgumentOutOfRangeException( |
| | 2 | 34 | | nameof(discountPercent), discountPercent, "Discount must be between 0 and 100 percent."); |
| | | 35 | | } |
| | | 36 | | |
| | 3 | 37 | | 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 | | { |
| | 5 | 46 | | ArgumentException.ThrowIfNullOrWhiteSpace(displayedPrice); |
| | | 47 | | |
| | | 48 | | // Strip currency symbols and whitespace, keep digits and separators. |
| | 38 | 49 | | var cleaned = new string(displayedPrice.Where(c => char.IsDigit(c) || c is '.' or ',').ToArray()); |
| | 5 | 50 | | if (cleaned.Length == 0) |
| | | 51 | | { |
| | 1 | 52 | | throw new FormatException($"'{displayedPrice}' contains no numeric value."); |
| | | 53 | | } |
| | | 54 | | |
| | | 55 | | // Decide which separator is the decimal one: the right-most of '.' and ','. |
| | 4 | 56 | | var lastDot = cleaned.LastIndexOf('.'); |
| | 4 | 57 | | var lastComma = cleaned.LastIndexOf(','); |
| | 4 | 58 | | var decimalSeparator = lastDot > lastComma ? '.' : ','; |
| | 4 | 59 | | var groupSeparator = decimalSeparator == '.' ? ',' : '.'; |
| | | 60 | | |
| | 4 | 61 | | cleaned = cleaned.Replace(groupSeparator.ToString(), string.Empty); |
| | 4 | 62 | | cleaned = cleaned.Replace(decimalSeparator, '.'); |
| | | 63 | | |
| | 4 | 64 | | return decimal.Parse(cleaned, System.Globalization.CultureInfo.InvariantCulture); |
| | | 65 | | } |
| | | 66 | | } |