using MeterVault.Core.Analysis; using MeterVault.Core.Analysis.Costing; using MeterVault.Core.Analysis.Totals; using MeterVault.Core.Domain; namespace MeterVault.Core.Tests.Analysis.Costing; /// /// Builders for the cost-calculator tests: real buckets from the period resolver and bucket planner (Berlin), tariffs, /// and quantities per part. Nothing here reads the wall clock. /// internal static class CostingTestData { public const int Electricity = 1; public const int Water = 2; public const int Oil = 3; public const int Grid = 10; public const int Heater = 11; public const int Export = 12; public const int WaterMeter = 20; public const int OilTank = 30; /// A "now" long after every range the tests price, so custom ranges resolve as complete. public static readonly DateTimeOffset FarFuture = AnalysisClock.Utc(2040, 1, 1); public static DateOnly D(int year, int month, int day) => new(year, month, day); /// The buckets of the complete range (inclusive), as the planner cuts them. public static IReadOnlyList Buckets(BucketSize size, DateOnly first, DateOnly last) { var period = PeriodResolver.Resolve(PeriodPreset.Custom, first, last, FarFuture, AnalysisClock.Berlin); return Plan(period, size); } /// The buckets of a to-date preset at (Berlin). public static IReadOnlyList ToDate(PeriodPreset preset, DateTimeOffset now, BucketSize size) => Plan(PeriodResolver.Resolve(preset, null, null, now, AnalysisClock.Berlin), size); /// A bucket spelled out, for the cases a planner never produces (a bucket reaching past today). public static AnalysisBucket Bucket(DateOnly first, DateOnly end, BucketSize size = BucketSize.Month) => new(first, end, AnalysisClock.At(AnalysisClock.Berlin, first.Year, first.Month, first.Day), AnalysisClock.At(AnalysisClock.Berlin, end.Year, end.Month, end.Day), size); public static Tariff Tariff( int id, TariffScope scope, int? scopeId, TariffComponent component, double value, string unit, DateOnly from, DateOnly? to = null) => new() { Id = id, ScopeType = scope, ScopeId = scopeId, Component = component, Value = value, Unit = unit, ValidFrom = from, ValidTo = to, }; /// A unit price of an energy type. public static Tariff TypePrice(int id, int energyTypeId, double value, string unit, DateOnly from, DateOnly? to = null) => Tariff(id, TariffScope.EnergyType, energyTypeId, TariffComponent.UnitPrice, value, unit, from, to); /// A meter's own unit price. public static Tariff MeterPrice(int id, int meterId, double value, string unit, DateOnly from, DateOnly? to = null) => Tariff(id, TariffScope.Meter, meterId, TariffComponent.UnitPrice, value, unit, from, to); public static Tariff BasePrice(int id, TariffScope scope, int? scopeId, double value, string unit, DateOnly from, DateOnly? to = null) => Tariff(id, scope, scopeId, TariffComponent.BasePrice, value, unit, from, to); public static Tariff FeedInPrice(int id, int energyTypeId, double value, string unit, DateOnly from) => Tariff(id, TariffScope.EnergyType, energyTypeId, TariffComponent.FeedIn, value, unit, from); public static TariffBook Book(params Tariff[] tariffs) => TariffBook.Create(tariffs, "EUR"); /// Every part's quantity from a per-day amount: the sum over its days. public static List Daily(IReadOnlyList buckets, Func perDay) => [.. CostCalculator.Parts(buckets).Select(p => CostQuantity.Known(p, DaysOf(p).Sum(perDay)))]; /// Every part's quantity from an amount per whole local month, prorated by days when a part is shorter. public static List Monthly(IReadOnlyList buckets, Func perMonth) => [.. CostCalculator.Parts(buckets).Select(p => perMonth(p.Month) is { } amount ? CostQuantity.Known(p, amount * p.Days / DateTime.DaysInMonth(p.Month.Year, p.Month.Month)) : CostQuantity.Unknown(p))]; /// Every part the same amount. public static List Each(IReadOnlyList buckets, double amount) => [.. CostCalculator.Parts(buckets).Select(p => CostQuantity.Known(p, amount))]; public static CostLine Line( int meterId, int energyTypeId, IReadOnlyList quantities, string unit = "kWh", BillLineKind kind = BillLineKind.UnitPrice, ServicePeriod? service = null) => new(meterId, energyTypeId, kind, unit, quantities, service); public static CostResult Price( IReadOnlyList buckets, TariffBook book, IEnumerable lines, DateOnly? today = null, IEnumerable? standing = null, IEnumerable? manual = null) => CostCalculator.Calculate(new CostRequest( buckets, today ?? D(2039, 12, 31), book, [.. lines], standing is null ? null : [.. standing], manual is null ? null : [.. manual])); public static ManualCost Manual(int id, DateOnly start, double amount, int? categoryId = null, int? meterId = null, DateOnly? end = null, string currency = "EUR") => new() { Id = id, CategoryId = categoryId, MeterId = meterId, PeriodStart = start, PeriodEnd = end ?? start, Amount = amount, Currency = currency, }; /// The cost of the bucket starting on . public static CostAmount At(this IReadOnlyList cells, IReadOnlyList buckets, DateOnly first) { for (var i = 0; i < buckets.Count; i++) { if (buckets[i].FirstDay == first) { return cells[i]; } } throw new InvalidOperationException($"No bucket starts on {first:yyyy-MM-dd}."); } private static IEnumerable DaysOf(CostPart part) => Enumerable.Range(0, part.Days).Select(part.FirstDay.AddDays); private static IReadOnlyList Plan(ResolvedPeriod period, BucketSize size) { var plan = BucketPlanner.Plan(period, size, maxPoints: 10_000); Assert.False(plan.Refused); return plan.Buckets; } }