using MeterVault.Core.Analysis.Costing; using MeterVault.Core.Analysis.Quantities; using MeterVault.Core.Costing; using MeterVault.Core.Domain; using static MeterVault.Core.Tests.Analysis.Costing.CostingTestData; namespace MeterVault.Core.Tests.Analysis.Costing; /// /// The tariff book resolves exactly like (meter over type over global, latest start within /// a scope, both ends inclusive) — but deterministically on ties — and answers the D-35/D-38 questions. /// public sealed class CostingTariffBookTests { private static readonly Tariff[] History = [ Tariff(1, TariffScope.Global, null, TariffComponent.UnitPrice, 0.30, "EUR/kWh", D(2023, 1, 1)), Tariff(2, TariffScope.Global, 99, TariffComponent.UnitPrice, 0.31, "EUR/kWh", D(2023, 6, 1)), TypePrice(3, Electricity, 0.40, "EUR/kWh", D(2023, 3, 1), D(2023, 12, 31)), TypePrice(4, Electricity, 0.42, "EUR/kWh", D(2023, 9, 1), D(2023, 10, 31)), MeterPrice(5, Grid, 0.50, "EUR/kWh", D(2023, 5, 1), D(2023, 5, 31)), TypePrice(6, Water, 5, "EUR/m3", D(2023, 1, 1)), Tariff(7, TariffScope.EnergyType, Electricity, TariffComponent.BasePrice, 12, "EUR/Monat", D(2023, 1, 1)), ]; public static TheoryData Dates => new() { { Grid, Electricity, 2, 15 }, { Grid, Electricity, 4, 15 }, { Grid, Electricity, 5, 15 }, { Grid, Electricity, 5, 31 }, { Grid, Electricity, 6, 1 }, { Grid, Electricity, 9, 15 }, { Grid, Electricity, 11, 1 }, { Grid, Electricity, 12, 31 }, { WaterMeter, Water, 7, 15 }, { 77, 5, 7, 15 }, }; [Theory] [MemberData(nameof(Dates))] public void Resolves_like_the_tariff_resolver(int meterId, int energyTypeId, int month, int day) { var book = Book(History); var date = D(2023, month, day); foreach (var component in new[] { TariffComponent.UnitPrice, TariffComponent.BasePrice, TariffComponent.FeedIn }) { var expected = TariffResolver.Resolve(History, component, meterId, energyTypeId, date); Assert.Same(expected, book.Resolve(component, meterId, energyTypeId, date)); } } [Fact] public void A_newer_price_in_the_same_scope_overrides_an_older_one_that_still_covers_the_date() { var book = Book(History); Assert.Equal(4, book.Resolve(TariffComponent.UnitPrice, Grid, Electricity, D(2023, 9, 15))!.Id); Assert.Equal(3, book.Resolve(TariffComponent.UnitPrice, Grid, Electricity, D(2023, 11, 15))!.Id); Assert.Equal(2, book.Resolve(TariffComponent.UnitPrice, Grid, Electricity, D(2024, 1, 15))!.Id); } [Fact] public void A_tie_on_scope_and_start_goes_to_the_later_entry_whatever_the_load_order() { var older = TypePrice(10, Electricity, 0.30, "EUR/kWh", D(2025, 1, 1)); var newer = TypePrice(11, Electricity, 0.35, "EUR/kWh", D(2025, 1, 1)); Assert.Same(newer, Book(older, newer).Resolve(TariffComponent.UnitPrice, Grid, Electricity, D(2025, 3, 15))); Assert.Same(newer, Book(newer, older).Resolve(TariffComponent.UnitPrice, Grid, Electricity, D(2025, 3, 15))); } [Fact] public void Resolving_in_one_scope_never_falls_back_to_another() { var book = Book(History); Assert.Equal(7, book.ResolveInScope(TariffComponent.BasePrice, TariffScope.EnergyType, Electricity, D(2024, 1, 15))!.Id); Assert.Null(book.ResolveInScope(TariffComponent.BasePrice, TariffScope.Global, null, D(2024, 1, 15))); Assert.Null(book.ResolveInScope(TariffComponent.UnitPrice, TariffScope.Meter, Grid, D(2023, 6, 15))); Assert.Equal(2, book.ResolveInScope(TariffComponent.UnitPrice, TariffScope.Global, 12345, D(2024, 1, 15))!.Id); } [Fact] public void A_scope_is_priced_when_it_has_a_tariff_at_any_date() { var book = Book(TypePrice(1, Water, 5, "EUR/m3", D(2030, 1, 1))); Assert.True(book.HasAny(TariffComponent.UnitPrice, WaterMeter, Water)); Assert.False(book.HasAny(TariffComponent.UnitPrice, Grid, Electricity)); Assert.False(book.HasAny(TariffComponent.FeedIn, WaterMeter, Water)); } [Fact] public void A_tariff_that_ends_before_it_starts_is_left_out() { var broken = TypePrice(1, Water, 5, "EUR/m3", D(2025, 6, 1), D(2025, 1, 31)); var book = Book(broken); Assert.Empty(book.Tariffs); Assert.False(book.HasAny(TariffComponent.UnitPrice, WaterMeter, Water)); } [Fact] public void An_own_price_is_known_per_month_by_the_15th() { var book = Book(MeterPrice(1, Heater, 0.22, "EUR/kWh", D(2025, 3, 16)), TypePrice(2, Electricity, 0.30, "EUR/kWh", D(2020, 1, 1))); Assert.True(book.HasMeterScopedUnitPrice(Heater)); Assert.False(book.HasMeterScopedUnitPrice(Grid)); Assert.False(book.HasOwnUnitPrice(Heater, D(2025, 3, 1))); Assert.True(book.HasOwnUnitPrice(Heater, D(2025, 4, 30))); Assert.False(book.HasOwnUnitPrice(Grid, D(2025, 4, 1))); } [Fact] public void Units_are_checked_against_the_instance_currency() { var eur = TypePrice(1, Electricity, 30, "ct/kWh", D(2025, 1, 1)); var chf = TypePrice(2, Electricity, 0.3, "CHF/kWh", D(2025, 1, 1)); var book = TariffBook.Create([eur, chf], "€"); Assert.Equal(0.3, book.Applicability(eur, "kWh").Convert(30), 12); Assert.Equal(TariffUnitIssue.CurrencyMismatch, book.Applicability(chf, "kWh").Issue); Assert.Equal(TariffUnitIssue.IncompatibleUnit, book.Applicability(eur, "m³").Issue); Assert.Same(book.UnitOf(eur), book.UnitOf(eur)); Assert.Equal(BillingPeriod.Month, book.Accrual(Tariff(3, TariffScope.Global, null, TariffComponent.BasePrice, 5, "EUR/Monat", D(2025, 1, 1))).Period); } [Fact] public void The_price_date_is_the_15th_of_the_month() { Assert.Equal(D(2024, 2, 15), TariffBook.PriceDate(D(2024, 2, 29))); Assert.Equal(D(2025, 12, 15), TariffBook.PriceDate(D(2025, 12, 1))); } }