diff --git a/backend/src/BigRegister.Api/Stamdata/Professions.cs b/backend/src/BigRegister.Api/Stamdata/Professions.cs
index 285f2c9..aa83209 100644
--- a/backend/src/BigRegister.Api/Stamdata/Professions.cs
+++ b/backend/src/BigRegister.Api/Stamdata/Professions.cs
@@ -19,14 +19,26 @@ public static class Professions
/// Every mapping in the data-file, typed.
public static readonly IReadOnlyList Mappings = StamdataFile.Load("professions");
- /// The mappings valid today, as a program→beroep lookup. Consumers that don't
- /// yet reason about a peildatum (e.g. DiplomaRules.ProfessionFor) use this — it
- /// preserves the pre-valid-time behaviour exactly while the file's rows are all current.
- public static readonly IReadOnlyDictionary ByProgram =
- Mappings.Where(m => StamdataFile.ActiveOn(m.GeldigVan, m.GeldigTot, DateOnly.FromDateTime(DateTime.Today)))
+ /// The mappings valid on , as a program→beroep lookup.
+ ///
+ /// Takes the peildatum as an argument rather than reading the clock. It used to be a
+ /// static readonly field filtered on DateTime.Today, which evaluated once at
+ /// type-load: a long-running process kept yesterday's answer across midnight, and a mapping
+ /// whose geldigVan fell after startup never appeared at all. It also made both
+ /// branches of permanently unreachable from here, which
+ /// is why this table's validity window was never exercised by a test.
+ public static IReadOnlyDictionary ByProgramOn(DateOnly on) =>
+ Mappings.Where(m => StamdataFile.ActiveOn(m.GeldigVan, m.GeldigTot, on))
.ToDictionary(m => m.Program, m => m.Beroep, StringComparer.OrdinalIgnoreCase);
- /// Distinct professions, in declaration order — the list a user may declare
- /// for a manual (unlisted) diploma.
+ /// The mappings valid today. Consumers that don't yet reason about a peildatum
+ /// (e.g. DiplomaRules.ProfessionFor) use this — same behaviour as before, but
+ /// evaluated per call so the date is current.
+ public static IReadOnlyDictionary ByProgram => ByProgramOn(Today());
+
+ /// Distinct professions valid today, in declaration order — the list a user may
+ /// declare for a manual (unlisted) diploma.
public static IReadOnlyList All() => ByProgram.Values.Distinct().ToList();
+
+ private static DateOnly Today() => DateOnly.FromDateTime(DateTime.Today);
}
diff --git a/backend/tests/BigRegister.Tests/Domain/ProfessionsTests.cs b/backend/tests/BigRegister.Tests/Domain/ProfessionsTests.cs
new file mode 100644
index 0000000..e0ddf82
--- /dev/null
+++ b/backend/tests/BigRegister.Tests/Domain/ProfessionsTests.cs
@@ -0,0 +1,47 @@
+using BigRegister.Stamdata;
+
+namespace BigRegister.Tests.Domain;
+
+///
+/// The profession↔program map's validity window (TE-009). `ByProgram` used to be a
+/// `static readonly` field filtered on `DateTime.Today` at type-load, so both branches of
+/// `StamdataFile.ActiveOn` were unreachable from here and nothing asserted the window at all.
+/// Now that the peildatum is a parameter, these are the two branches.
+///
+public class ProfessionsTests
+{
+ [Fact]
+ public void A_mapping_is_absent_before_its_geldigVan()
+ {
+ // Every seeded row starts 2000-01-01; nothing is valid the day before.
+ Assert.Empty(Professions.ByProgramOn(new DateOnly(1999, 12, 31)));
+ }
+
+ [Fact]
+ public void A_mapping_is_present_on_and_after_its_geldigVan()
+ {
+ Assert.Equal("Arts", Professions.ByProgramOn(new DateOnly(2000, 1, 1))["geneeskunde"]);
+ Assert.Equal("Arts", Professions.ByProgramOn(new DateOnly(2026, 8, 26))["geneeskunde"]);
+ }
+
+ [Fact]
+ public void A_closed_mapping_is_absent_from_its_geldigTot_onwards()
+ {
+ // geldigTot is exclusive (`on < tot`), so the row drops out on the boundary date itself.
+ foreach (var m in Professions.Mappings.Where(m => m.GeldigTot is DateOnly))
+ {
+ var tot = m.GeldigTot!.Value;
+ Assert.True(Professions.ByProgramOn(tot.AddDays(-1)).ContainsKey(m.Program));
+ Assert.False(Professions.ByProgramOn(tot).ContainsKey(m.Program));
+ }
+ }
+
+ [Fact]
+ public void ByProgram_is_evaluated_per_call_not_captured_at_type_load()
+ {
+ // The regression this guards: a long-running process must not keep serving the answer
+ // it computed at startup. Same date in, same answer; different date in, different answer.
+ Assert.Equal(Professions.ByProgram.Count, Professions.ByProgramOn(DateOnly.FromDateTime(DateTime.Today)).Count);
+ Assert.NotEqual(Professions.ByProgram.Count, Professions.ByProgramOn(new DateOnly(1999, 12, 31)).Count);
+ }
+}