using MeterVault.Core.Domain; using MeterVault.Infrastructure.Costing; using MeterVault.Infrastructure.Dashboard; using MeterVault.Infrastructure.Ingestion; using MeterVault.Infrastructure.Normalization; using MeterVault.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; namespace MeterVault.App.Api; /// Request/response contracts for the REST API (SDD §9). public sealed record ReadingPush(int MeterId, DateTimeOffset Time, double Value); public sealed record EventPush(int MeterId, DateTimeOffset Time, MeterEventType Type, double? Amount, double? PrevValue, double? NewValue, string? Unit, string? Notes); public sealed record TariffPush(TariffScope ScopeType, int? ScopeId, TariffComponent Component, double Value, string Unit, DateOnly ValidFrom, DateOnly? ValidTo); public sealed record IngestResult(int Written, int Updated, int Rejected); /// Maps the versioned REST API. All endpoints require a valid API key (SDD §9). public static class ApiEndpoints { public static IEndpointRouteBuilder MapMeterVaultApi(this IEndpointRouteBuilder app) { var api = app.MapGroup("/api/v1").AddEndpointFilter().WithTags("MeterVault"); api.MapPost("/readings", async (ReadingPush[] readings, IngestionService ingestion, CancellationToken ct) => { int written = 0, updated = 0, rejected = 0; foreach (var r in readings) { switch (await ingestion.IngestByMeterAsync(r.MeterId, r.Time, r.Value, ct)) { case IngestionOutcome.Written: written++; break; case IngestionOutcome.Updated: updated++; break; case IngestionOutcome.RejectedDecrease: rejected++; break; default: break; } } return Results.Ok(new IngestResult(written, updated, rejected)); }).WithSummary("Ingest one or more readings (idempotent). Lets Home Assistant push."); api.MapGet("/meters", async (MeterVaultDbContext db, CancellationToken ct) => Results.Ok(await db.Meters.AsNoTracking() .Select(m => new { m.Id, m.Name, m.EnergyTypeId, Mode = m.Mode.ToString(), m.Unit, m.IsActive }) .ToListAsync(ct))); api.MapGet("/energy-types", async (MeterVaultDbContext db, CancellationToken ct) => Results.Ok(await db.EnergyTypes.AsNoTracking() .Select(t => new { t.Id, t.Key, t.DisplayName, t.BaseUnit, Mode = t.DefaultMode.ToString() }) .ToListAsync(ct))); api.MapGet("/consumption", async (int meter, DateTimeOffset from, DateTimeOffset to, CostService cost, CancellationToken ct) => { var buckets = await cost.GetMeterCostsAsync(meter, from, to, CostBucket.Month, ct); return Results.Ok(buckets.Select(b => new { b.Period, b.Consumption, b.Generation })); }).WithSummary("Normalized monthly consumption/generation for a meter."); api.MapGet("/cost", async (int meter, DateTimeOffset from, DateTimeOffset to, CostService cost, CancellationToken ct) => Results.Ok(await cost.GetMeterCostsAsync(meter, from, to, CostBucket.Month, ct))); api.MapGet("/dashboard/summary", async (DashboardService dashboard, CancellationToken ct) => Results.Ok(await dashboard.GetSummaryAsync(DateOnly.FromDateTime(DateTime.UtcNow), ct))); api.MapPost("/events", async (EventPush push, MeterVaultDbContext db, NormalizationService normalization, CancellationToken ct) => { db.MeterEvents.Add(new MeterEvent { MeterId = push.MeterId, Time = push.Time, EventType = push.Type, Amount = push.Amount, PrevValue = push.PrevValue, NewValue = push.NewValue, Unit = push.Unit, Notes = push.Notes, }); await db.SaveChangesAsync(ct); await normalization.RecomputeMeterAsync(push.MeterId, null, ct); await db.SaveChangesAsync(ct); return Results.Ok(); }).WithSummary("Record a delivery / swap / tank level / correction and recompute the meter."); api.MapGet("/tariffs", async (MeterVaultDbContext db, CancellationToken ct) => Results.Ok(await db.Tariffs.AsNoTracking().OrderBy(t => t.ValidFrom).ToListAsync(ct))); api.MapPost("/tariffs", async (TariffPush push, MeterVaultDbContext db, CancellationToken ct) => { var tariff = new Tariff { ScopeType = push.ScopeType, ScopeId = push.ScopeId, Component = push.Component, Value = push.Value, Unit = push.Unit, ValidFrom = push.ValidFrom, ValidTo = push.ValidTo, }; db.Tariffs.Add(tariff); await db.SaveChangesAsync(ct); return Results.Created($"/api/v1/tariffs/{tariff.Id}", new { tariff.Id }); }); api.MapGet("/sources/status", async (MeterVaultDbContext db, CancellationToken ct) => Results.Ok(await db.MeterSources.AsNoTracking() .Select(s => new { s.Id, s.MeterId, Type = s.SourceType.ToString(), s.IsEnabled, s.LastSeenAt, s.LastValue, s.LastStatus }) .ToListAsync(ct))); return app; } }