ci / build-test (push) Successful in 2m41s
Three things a reported Heizoel page got wrong at once. Its tank is dipped a few times a year and its burner read every few months, which is exactly the shape the coverage rules had not been walked through. "No data" for data that exists. A tank books nothing until the next dipstick closes the interval, so the stretch after the last dipstick is covered by no run at all, and a bucket no run covers was reported missing. The burner, whose run reaches into the window, said "only coarser data" -- the honest answer -- so one card claimed there was nothing while the coverage panel beside it listed years of data. A bucket that no run covers, no gap overlaps and no opening balance explains now reports the meter's resolution when its preceding coverage is within one interval of its own class: it is not silent, it is read rarely. A meter that does book its own buckets and stops -- a dead hourly source, a sheet asked about a later month -- still reads missing. Auto answering twelve months with one bar. Coarse only means "longer than a month", so a dipstick taken each autumn straddles a New Year as surely as a month start: coarsening the chart to years bought nothing and cost every point. The planning resolution now caps coarse at month when a run crosses a local year edge, and a series that cannot resolve the natural size no longer coarsens the whole chart -- it is drawn at that size with its buckets marked, which the chart and table already explain. A page contradicting itself. The comparison line above the ranking was fed the leading measure's matched coverage but worded as if it spoke for the page, directly above a burner row that did compare. It now names the figure it is about. Alongside: the "largest changes" ranking no longer drops a meter whose change is not comparable. It ranks what can be ranked, then lists the rest with their values and the reason -- the tank had been vanishing from its own energy type. And every dated table now reads newest first, as lists are read; charts stay chronological left to right, and the CSV export stays ascending for spreadsheets. A-41 to A-43 in the note record the three rules.
112 lines
6.1 KiB
C#
112 lines
6.1 KiB
C#
using System.Net;
|
|
using MeterVault.App;
|
|
using MeterVault.Core.Domain;
|
|
using Microsoft.AspNetCore.Localization;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace MeterVault.Integration.Tests.Editor;
|
|
|
|
/// <summary>
|
|
/// The tariff and settings pages as the server prerenders them (D-37, D-52, D-57): a scoped tariff link lists what can
|
|
/// price that meter and nothing else, Bonus/Discount/Tax say they are not applied, and settings label raw retention as
|
|
/// not enforced, with the reason, beside the analysis data state.
|
|
/// </summary>
|
|
[Collection("Timescale")]
|
|
public sealed class AdminPagesRenderTests(TimescaleFixture fx) : IAsyncLifetime
|
|
{
|
|
private short _type;
|
|
private short _otherType;
|
|
private int _meter;
|
|
private readonly List<int> _tariffs = [];
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
await using var db = fx.CreateContext();
|
|
var type = new EnergyType { Key = $"tariffs-{Guid.NewGuid():N}", DisplayName = "Tariff water", BaseUnit = "m3", DefaultMode = MeterMode.CumulativeCounter };
|
|
var other = new EnergyType { Key = $"tariffs-{Guid.NewGuid():N}", DisplayName = "Tariff heat", BaseUnit = "kWh", DefaultMode = MeterMode.CumulativeCounter };
|
|
db.EnergyTypes.AddRange(type, other);
|
|
await db.SaveChangesAsync();
|
|
_type = type.Id;
|
|
_otherType = other.Id;
|
|
|
|
var meter = new Meter { Name = "Tap meter", EnergyTypeId = _type, Mode = MeterMode.CumulativeCounter, Unit = "m3", Meta = "{}" };
|
|
db.Meters.Add(meter);
|
|
await db.SaveChangesAsync();
|
|
_meter = meter.Id;
|
|
|
|
Tariff[] tariffs =
|
|
[
|
|
Tariff(TariffScope.Meter, _meter, TariffComponent.UnitPrice, 1.2345, "EUR/m3"),
|
|
Tariff(TariffScope.Meter, _meter, TariffComponent.UnitPrice, 1.9999, "EUR/m3", new DateOnly(2023, 1, 1)),
|
|
Tariff(TariffScope.EnergyType, _type, TariffComponent.Bonus, 2.3456, "EUR"),
|
|
Tariff(TariffScope.EnergyType, _otherType, TariffComponent.UnitPrice, 9.8765, "EUR/kWh"),
|
|
];
|
|
db.Tariffs.AddRange(tariffs);
|
|
await db.SaveChangesAsync();
|
|
_tariffs.AddRange(tariffs.Select(t => t.Id));
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
await using var db = fx.CreateContext();
|
|
await db.Tariffs.Where(t => _tariffs.Contains(t.Id)).ExecuteDeleteAsync();
|
|
await db.Meters.Where(m => m.Id == _meter).ExecuteDeleteAsync();
|
|
await db.EnergyTypes.Where(t => t.Id == _type || t.Id == _otherType).ExecuteDeleteAsync();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_scoped_tariff_link_lists_what_can_price_the_meter()
|
|
{
|
|
using var factory = new MeterVaultAppFactory(fx.ConnectionString);
|
|
using var client = factory.CreateClient();
|
|
|
|
var scoped = WebUtility.HtmlDecode(await client.GetStringAsync(new Uri($"{TariffLinks.Path}?scope=meter&id={_meter}", UriKind.Relative)));
|
|
Assert.Contains("Tariffs that can price Tap meter: its own, its energy type's and global ones.", scoped, StringComparison.Ordinal);
|
|
Assert.Contains("1.2345", scoped, StringComparison.Ordinal); // its own price
|
|
Assert.Contains("2.3456", scoped, StringComparison.Ordinal); // its type's bonus …
|
|
Assert.Contains("Not applied yet", scoped, StringComparison.Ordinal); // … which is not applied yet
|
|
Assert.DoesNotContain("9.8765", scoped, StringComparison.Ordinal); // another type's price is not listed
|
|
Assert.Contains("Bonus, discount and tax tariffs are stored but not applied to costs yet.", scoped, StringComparison.Ordinal);
|
|
|
|
// A-42: inside a component the newest validity is listed first, so the price in force is the top row.
|
|
Assert.True(
|
|
scoped.IndexOf("1.9999", StringComparison.Ordinal) < scoped.IndexOf("1.2345", StringComparison.Ordinal),
|
|
"The 2023 price must be listed above the 2020 one.");
|
|
|
|
var all = WebUtility.HtmlDecode(await client.GetStringAsync(new Uri(TariffLinks.Path, UriKind.Relative)));
|
|
Assert.Contains("9.8765", all, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("Tariffs that can price", all, StringComparison.Ordinal);
|
|
Assert.True(all.IndexOf("1.9999", StringComparison.Ordinal) < all.IndexOf("1.2345", StringComparison.Ordinal));
|
|
|
|
// The deep link of a missing price renders; its dialog opens only once the page is interactive.
|
|
var link = TariffLinks.New(TariffScope.Meter, _meter, TariffComponent.UnitPrice, new DateOnly(2027, 1, 1));
|
|
(await client.GetAsync(new Uri(link, UriKind.Relative))).EnsureSuccessStatusCode();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Settings_label_raw_retention_as_not_enforced_in_both_languages()
|
|
{
|
|
using var factory = new MeterVaultAppFactory(fx.ConnectionString);
|
|
using var client = factory.CreateClient();
|
|
|
|
var english = WebUtility.HtmlDecode(await client.GetStringAsync(new Uri("/admin/settings", UriKind.Relative)));
|
|
Assert.Contains("Not enforced", english, StringComparison.Ordinal);
|
|
Assert.Contains("Raw readings are kept indefinitely (configured: 1095 days).", english, StringComparison.Ordinal);
|
|
Assert.Contains("Analysis data", english, StringComparison.Ordinal);
|
|
Assert.Contains("Meters with current analysis data", english, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("could not be read", english, StringComparison.Ordinal);
|
|
|
|
using var german = factory.CreateClient();
|
|
german.DefaultRequestHeaders.Add(
|
|
"Cookie",
|
|
CookieRequestCultureProvider.DefaultCookieName + "="
|
|
+ Uri.EscapeDataString(CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("de"))));
|
|
var deutsch = WebUtility.HtmlDecode(await german.GetStringAsync(new Uri("/admin/settings", UriKind.Relative)));
|
|
Assert.Contains("Nicht aktiv", deutsch, StringComparison.Ordinal);
|
|
Assert.Contains("Auswertungsdaten", deutsch, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static Tariff Tariff(TariffScope scope, int id, TariffComponent component, double value, string unit, DateOnly? from = null) =>
|
|
new() { ScopeType = scope, ScopeId = id, Component = component, Value = value, Unit = unit, ValidFrom = from ?? new DateOnly(2020, 1, 1) };
|
|
}
|