using MeterVault.Core.Domain; using MeterVault.Core.Normalization; using MeterVault.Infrastructure.Import; using MeterVault.Infrastructure.Normalization; using MeterVault.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using static MeterVault.Integration.Tests.Reconciliation.ReconciliationSupport; namespace MeterVault.Integration.Tests; /// /// End-to-end persistence: stage the water CSV → commit as a batch → normalized consumption lands /// in the hypertable → revert removes everything and rebases consumption (SDD §6.3, FR-6). /// [Collection("Timescale")] public sealed class ImportRoundTripTests(TimescaleFixture fx) { [Fact] public async Task Commit_persists_consumption_and_revert_removes_it() { await using var db = fx.CreateContext(); await DatabaseSeeder.SeedAsync(db); var waterType = await db.EnergyTypes.FirstAsync(t => t.Key == "water"); var meter = new Meter { Name = "Zähler Wasser (round-trip test)", EnergyTypeId = waterType.Id, Mode = MeterMode.CumulativeCounter, Unit = "m3", InitialBaseline = 820, }; db.Meters.Add(meter); await db.SaveChangesAsync(); var profile = new MappingProfile { Name = "test-water", DateColumn = 0, DateKind = DateKind.MonthName, FirstDataRowIndex = 1, DetectCumulativeSwaps = true, Columns = [ new ColumnMapping { Index = 1, Role = MappingRole.Reading, MeterId = meter.Id, Unit = "m3", SwapConsumptionColumn = 2, }, ], }; StagedImport staged; using (var reader = new StreamReader(FixturePath(Water))) { staged = new CsvImporter().Stage(profile, reader); } var normalization = new NormalizationService(db, NormalizationEngine.CreateDefault()); var service = new ImportService(db, normalization); // Commit. var batchId = await service.CommitAsync(staged, sourceName: "Wasser.csv", mappingJson: null); Assert.True(await db.Readings.AnyAsync(r => r.MeterId == meter.Id)); Assert.True(await db.MeterEvents.AnyAsync(e => e.MeterId == meter.Id && e.EventType == MeterEventType.MeterSwap)); var maerz = await db.Consumption.SingleAsync(c => c.MeterId == meter.Id && c.Time == new DateTimeOffset(2023, 3, 1, 0, 0, 0, TimeSpan.Zero)); Assert.Equal(12d, maerz.Amount, 3); // Revert. await service.RevertAsync(batchId); Assert.False(await db.Readings.AnyAsync(r => r.MeterId == meter.Id)); Assert.False(await db.MeterEvents.AnyAsync(e => e.MeterId == meter.Id)); Assert.False(await db.Consumption.AnyAsync(c => c.MeterId == meter.Id)); var batch = await db.ImportBatches.SingleAsync(b => b.Id == batchId); Assert.NotNull(batch.RevertedAt); // Cleanup so the shared container stays tidy for other tests. ExecuteDelete bypasses the // change tracker (which holds stale entries after the revert's set-based deletes). await db.Meters.Where(m => m.Id == meter.Id).ExecuteDeleteAsync(); } }