From fa7e9c5cfb3582bd636b788ae887a10c93c8f487 Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Tue, 21 Jul 2026 07:41:04 +0200 Subject: [PATCH] =?UTF-8?q?feat(stamdata):=20profession=E2=86=94diploma=20?= =?UTF-8?q?map=20as=20validated=20config-as-code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the profession↔diploma table out of DiplomaRules into a dedicated Stamdata.Professions module (business-editable data, separated from the rules that consume it) and add StamdataValidationTests as the build-time gate: every seeded diploma program must resolve to a real profession, no blank entries. A bad edit now fails the build instead of silently rendering "Onbekend". Rules and behaviour unchanged. Co-Authored-By: Claude Opus 4.8 --- .../Domain/Diplomas/DiplomaRules.cs | 28 +++++-------- .../BigRegister.Api/Stamdata/Professions.cs | 32 +++++++++++++++ .../StamdataValidationTests.cs | 41 +++++++++++++++++++ 3 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 backend/src/BigRegister.Api/Stamdata/Professions.cs create mode 100644 backend/tests/BigRegister.Tests/StamdataValidationTests.cs diff --git a/backend/src/BigRegister.Api/Domain/Diplomas/DiplomaRules.cs b/backend/src/BigRegister.Api/Domain/Diplomas/DiplomaRules.cs index 445e2fd..cc2a2bc 100644 --- a/backend/src/BigRegister.Api/Domain/Diplomas/DiplomaRules.cs +++ b/backend/src/BigRegister.Api/Domain/Diplomas/DiplomaRules.cs @@ -1,29 +1,23 @@ +using BigRegister.Stamdata; + namespace BigRegister.Domain.Diplomas; /// -/// SERVER-OWNED business rules for diplomas. This is the single place a policy -/// changes: which profession a study program maps to, and which policy questions -/// (geldigheidsvragen) apply to a diploma. The frontend renders these; it never -/// derives them. +/// SERVER-OWNED business rules for diplomas: which profession a study program maps to, +/// and which policy questions (geldigheidsvragen) apply to a diploma. The frontend +/// renders these; it never derives them. +/// +/// The profession↔program *data* is business-editable stamdata in +/// (config-as-code, ADR-0004); the *rules* below consume it. /// public static class DiplomaRules { - // RULE: study program → BIG profession. - private static readonly Dictionary ProfessionByProgram = new(StringComparer.OrdinalIgnoreCase) - { - ["geneeskunde"] = "Arts", - ["verpleegkunde"] = "Verpleegkundige", - ["fysiotherapie"] = "Fysiotherapeut", - ["farmacie"] = "Apotheker", - ["tandheelkunde"] = "Tandarts", - }; - + // RULE: study program → BIG profession (data lives in Stamdata.Professions). public static string ProfessionFor(Diploma d) => - ProfessionByProgram.TryGetValue(d.Opleiding, out var beroep) ? beroep : "Onbekend"; + Professions.ByProgram.TryGetValue(d.Opleiding, out var beroep) ? beroep : "Onbekend"; /// Professions a user may declare for a manual (unlisted) diploma. - public static IReadOnlyList ManualProfessions() => - ProfessionByProgram.Values.Distinct().ToList(); + public static IReadOnlyList ManualProfessions() => Professions.All(); // --- Policy questions (geldigheidsvragen) --- diff --git a/backend/src/BigRegister.Api/Stamdata/Professions.cs b/backend/src/BigRegister.Api/Stamdata/Professions.cs new file mode 100644 index 0000000..1bbd686 --- /dev/null +++ b/backend/src/BigRegister.Api/Stamdata/Professions.cs @@ -0,0 +1,32 @@ +namespace BigRegister.Stamdata; + +/// +/// BUSINESS-EDITABLE STAMDATA (config-as-code). Which BIG profession (beroep) each +/// study program (opleiding) maps to. This is the one table the business tunes when +/// a program starts or stops leading to a registered profession. +/// +/// Change it by editing this file and opening a PR — NOT via a production database. +/// The C# compiler catches shape/type mistakes; StamdataValidationTests catches +/// the referential integrity it can't (e.g. a seeded diploma whose program has no +/// profession here). So a bad edit fails the build, never prod. See ADR-0004 +/// (docs/reference/architecture/0004-stamdata-as-code.md). +/// +/// This is DATA, not logic: the rules that consume it (which questions a diploma needs, +/// how a manual diploma is treated) stay in DiplomaRules. +/// +public static class Professions +{ + public static readonly IReadOnlyDictionary ByProgram = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["geneeskunde"] = "Arts", + ["verpleegkunde"] = "Verpleegkundige", + ["fysiotherapie"] = "Fysiotherapeut", + ["farmacie"] = "Apotheker", + ["tandheelkunde"] = "Tandarts", + }; + + /// Distinct professions, in declaration order — the list a user may declare + /// for a manual (unlisted) diploma. + public static IReadOnlyList All() => ByProgram.Values.Distinct().ToList(); +} diff --git a/backend/tests/BigRegister.Tests/StamdataValidationTests.cs b/backend/tests/BigRegister.Tests/StamdataValidationTests.cs new file mode 100644 index 0000000..aa927f7 --- /dev/null +++ b/backend/tests/BigRegister.Tests/StamdataValidationTests.cs @@ -0,0 +1,41 @@ +using BigRegister.Api.Data; +using BigRegister.Domain.Diplomas; +using BigRegister.Stamdata; + +namespace BigRegister.Tests; + +/// +/// The compile-time gate for business-editable stamdata (ADR-0004). The C# compiler +/// already catches shape/type mistakes; these tests catch the referential integrity it +/// can't, so a bad config edit fails the build instead of reaching production. +/// +public class StamdataValidationTests +{ + [Fact] + public void Every_seeded_diploma_program_maps_to_a_known_profession() + { + // The dangling-reference guard: a seed program with no entry in Professions would + // silently render "Onbekend" to the user. Fail the build instead. + foreach (var d in SeedData.Diplomas) + Assert.True(DiplomaRules.ProfessionFor(d) != "Onbekend", + $"Diploma program '{d.Opleiding}' has no profession in Stamdata.Professions."); + } + + [Fact] + public void Profession_map_has_no_blank_programs_or_professions() + { + Assert.All(Professions.ByProgram, kv => + { + Assert.False(string.IsNullOrWhiteSpace(kv.Key), "A profession-map program key is blank."); + Assert.False(string.IsNullOrWhiteSpace(kv.Value), $"Program '{kv.Key}' maps to a blank profession."); + }); + } + + [Fact] + public void Manual_professions_are_non_empty_and_distinct() + { + var professions = DiplomaRules.ManualProfessions(); + Assert.NotEmpty(professions); + Assert.Equal(professions.Count, professions.Distinct().Count()); + } +}