using MeterVault.Core.Domain; using MeterVault.Infrastructure.Costing; using MeterVault.Infrastructure.Dashboard; using MeterVault.Infrastructure.Options; using MeterVault.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace MeterVault.Integration.Tests; /// /// The meter-detail headline numbers: month/year totals bucketed in the instance timezone, and the /// generation-vs-consumption split that decides which of the two a meter reports. /// [Collection("Timescale")] public sealed class MeterPeriodServiceTests(TimescaleFixture fx) { [Fact] public async Task Buckets_by_local_month_and_compares_with_the_previous_one() { await using var db = fx.CreateContext(); var meterId = await SetupAsync(db, MeterMode.CumulativeCounter); var today = DateOnly.FromDateTime(DateTime.UtcNow); var thisMonth = new DateOnly(today.Year, today.Month, 1); var lastMonth = thisMonth.AddMonths(-1); await AddConsumptionAsync(db, meterId, ConsumptionKind.Consumption, thisMonth.AddDays(1), 30); await AddConsumptionAsync(db, meterId, ConsumptionKind.Consumption, lastMonth.AddDays(3), 100); var view = await NewService().GetAsync(meterId); Assert.NotNull(view); Assert.Equal("Consumption", view!.Label); Assert.Equal(30d, view.MonthToDate, 3); Assert.Equal(100d, view.LastMonth, 3); Assert.Equal(130d, view.YearToDate, 3); // Projection scales the partial month up, so it must be at least what has already happened. Assert.True(view.MonthProjected >= view.MonthToDate); await CleanupAsync(db, meterId); } [Fact] public async Task A_generation_counter_reports_generation_not_consumption() { // Regression: a PV meter showed "0 kWh consumption", which is true and useless. await using var db = fx.CreateContext(); var meterId = await SetupAsync(db, MeterMode.GenerationCounter); var today = DateOnly.FromDateTime(DateTime.UtcNow); await AddConsumptionAsync( db, meterId, ConsumptionKind.Generation, new DateOnly(today.Year, today.Month, 1).AddDays(1), 42); var view = await NewService().GetAsync(meterId); Assert.NotNull(view); Assert.Equal("Generation", view!.Label); Assert.Equal(42d, view.MonthToDate, 3); await CleanupAsync(db, meterId); } [Fact] public async Task A_meter_with_no_consumption_yields_an_empty_history_rather_than_a_flat_line() { await using var db = fx.CreateContext(); var meterId = await SetupAsync(db, MeterMode.CumulativeCounter); var view = await NewService().GetAsync(meterId); Assert.NotNull(view); Assert.False(view!.HasHistory); Assert.Empty(view.Last12Months); Assert.Null(view.MonthChange); // no previous month to divide by await CleanupAsync(db, meterId); } private MeterPeriodService NewService() { var options = Microsoft.Extensions.Options.Options.Create( new MeterVaultOptions { TimeZone = "Europe/Berlin", Currency = "EUR" }); return new MeterPeriodService(fx, new CostService(fx), options); } private static async Task SetupAsync(MeterVaultDbContext db, MeterMode mode) { await DatabaseSeeder.SeedAsync(db); var type = await db.EnergyTypes.FirstAsync(t => t.Key == "electricity"); var meter = new Meter { Name = $"period-{Guid.NewGuid():N}", EnergyTypeId = type.Id, Mode = mode, Unit = "kWh", }; db.Meters.Add(meter); await db.SaveChangesAsync(); return meter.Id; } private static async Task AddConsumptionAsync( MeterVaultDbContext db, int meterId, ConsumptionKind kind, DateOnly day, double amount) { db.Consumption.Add(new Consumption { MeterId = meterId, // Midday local, so the row cannot drift into an adjacent month through the UTC offset. Time = new DateTimeOffset(day.Year, day.Month, day.Day, 12, 0, 0, TimeSpan.Zero), Kind = kind, Amount = amount, Quality = ReadingQuality.Measured, }); await db.SaveChangesAsync(); } private static async Task CleanupAsync(MeterVaultDbContext db, int meterId) { await db.Consumption.Where(c => c.MeterId == meterId).ExecuteDeleteAsync(); await db.Meters.Where(m => m.Id == meterId).ExecuteDeleteAsync(); } }