using MeterVault.Core.Analysis; using MeterVault.Core.Analysis.Coverage; using MeterVault.Core.Domain; using MeterVault.Core.Normalization; using MeterVault.Infrastructure.Import; using static MeterVault.Integration.Tests.Reconciliation.ReconciliationSupport; namespace MeterVault.Integration.Tests.Reconciliation; /// /// Coverage runs (D-13) of the seeded reference sheets, in the instance timezone. These are the shapes /// the analysis pages meet first: monthly sheets that resolve months exactly, a burner read once in twelve /// years before its monthly rows begin, and a tank whose deliveries go back to 1997 but whose draw is only /// known from the first dipstick in 2022. /// public sealed class CoverageOfFixturesTests { private static readonly TimeZoneInfo Berlin = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin"); private static IReadOnlyList Coverage(StagedImport staged, MeterConfig config) => CoverageBuilder.Build( NormalizationEngine.CreateDefault().Normalize(new NormalizationContext { Meter = config, Readings = staged.Readings.Where(r => r.MeterId == config.MeterId).ToList(), Events = staged.Events.Where(e => e.MeterId == config.MeterId).ToList(), TimeZone = Berlin, }), Berlin); [Theory] [InlineData(ReferenceProfiles.Haus, MeterMode.CumulativeCounter)] [InlineData(ReferenceProfiles.Netz, MeterMode.CumulativeCounter)] [InlineData(ReferenceProfiles.Auto, MeterMode.CumulativeCounter)] [InlineData(ReferenceProfiles.Solar1, MeterMode.GenerationCounter)] [InlineData(ReferenceProfiles.Solar2, MeterMode.GenerationCounter)] public void Every_electricity_meter_is_one_month_run_divided_at_months(int meterId, MeterMode mode) { var staged = Stage(ReferenceProfiles.Electricity(), Electricity); var readings = staged.Readings.Where(r => r.MeterId == meterId).OrderBy(r => r.Time).ToList(); var run = Assert.Single(Coverage(staged, new MeterConfig { MeterId = meterId, Mode = mode, Unit = "kWh" })); Assert.Equal(GapAttribution.LabelMonthStart(readings[0], Berlin), run.From); Assert.Equal(GapAttribution.EffectiveTime(readings[^1], Berlin), run.To); Assert.Equal(GapAttribution.LabelMonthStart(readings[^1], Berlin), run.LastIntervalStart); Assert.Equal(ResolutionClass.Month, run.Resolution); Assert.True(run.DividedAtMonths); Assert.False(run.IsGap); } [Fact] public void Capping_the_electricity_sheet_inside_its_last_month_gives_up_exactly_that_month() { // A-04: the sheet's last row describes its whole month. A reader whose now falls in that month // leaves the row out of actuals and its month uncovered, and keeps every month before it. var staged = Stage(ReferenceProfiles.Electricity(), Electricity); var readings = staged.Readings.Where(r => r.MeterId == ReferenceProfiles.Haus).OrderBy(r => r.Time).ToList(); var lastMonth = GapAttribution.LabelMonthStart(readings[^1], Berlin); var stored = Assert.Single(Coverage(staged, new MeterConfig { MeterId = ReferenceProfiles.Haus, Mode = MeterMode.CumulativeCounter, Unit = "kWh" })); var capped = Assert.Single(CoverageRuns.CapAt([stored], lastMonth.AddDays(10), Berlin)); Assert.Equal(stored.From, capped.From); Assert.Equal(lastMonth, capped.To); } [Fact] public void The_water_meter_is_covered_across_its_swap() { var staged = Stage(ReferenceProfiles.Water(), Water); var run = Assert.Single(Coverage(staged, new MeterConfig { MeterId = ReferenceProfiles.Wasser, Mode = MeterMode.CumulativeCounter, Unit = "m3", InitialBaseline = 820, })); Assert.Equal(ResolutionClass.Month, run.Resolution); Assert.True(run.DividedAtMonths); } [Fact] public void The_seeded_burner_starts_with_its_twelve_year_interval_as_its_own_coarse_run() { var staged = Stage(ReferenceProfiles.HeatingOil(), Oil); var readings = staged.Readings.Where(r => r.MeterId == ReferenceProfiles.Burner).OrderBy(r => r.Time).ToList(); var runs = Coverage(staged, new MeterConfig { MeterId = ReferenceProfiles.Burner, Mode = MeterMode.RuntimeCounter, Unit = "h" }); // 18.10.2010 → 14.10.2022: the first reading is an opening balance, the next closes twelve years. Assert.Equal( new CoverageRun(Utc(2010, 10, 18), Utc(2022, 10, 14), ResolutionClass.Coarse, DividedAtMonths: false, LastIntervalStart: Utc(2010, 10, 18)), runs[0]); Assert.Equal(readings[^1].Time, runs[^1].To); Assert.Equal(ResolutionClass.Month, runs[^1].Resolution); Assert.DoesNotContain(runs, r => r.IsGap); for (var i = 1; i < runs.Count; i++) { Assert.Equal(runs[i - 1].To, runs[i].From); } } [Fact] public void The_seeded_tank_is_covered_from_its_first_dipstick_not_its_first_delivery() { var staged = Stage(ReferenceProfiles.HeatingOil(), Oil); var levels = staged.Events .Where(e => e.MeterId == ReferenceProfiles.OilTank && e.EventType == MeterEventType.TankLevel) .OrderBy(e => e.Time) .ToList(); Assert.Contains(staged.Events, e => e.MeterId == ReferenceProfiles.OilTank && e.EventType == MeterEventType.Delivery && e.Time.Year == 1997); var runs = Coverage(staged, new MeterConfig { MeterId = ReferenceProfiles.OilTank, Mode = MeterMode.ConsumableBalance, Unit = "L", Tank = new TankConfig { Capacity = 7000, Calibration = new CalibrationCurve(ReferenceProfiles.OilLitresPerCm) }, }); Assert.Equal(Utc(2022, 9, 8), runs[0].From); Assert.Equal(levels[^1].Time, runs[^1].To); Assert.DoesNotContain(runs, r => r.IsGap); } private static DateTimeOffset Utc(int year, int month, int day) => new(year, month, day, 0, 0, 0, TimeSpan.Zero); }