refactor(backend): delete dead legacy endpoints, make domain types unions (WP-72 + WP-73)
Two work packages in one commit because both edit Program.cs and splitting
them would leave a commit that does not build.
WP-72 — deletes POST /api/v1/intakes and /herregistraties. Both were dead
from the UI (the wizard submits via /applications/{id}/submit) and strictly
less capable: they minted a bare reference and wrote no Aanvraag, made no
ZGW call, and did no document-ownership check. The shared Submit(...) helper
survives — /registrations and /change-requests still use it. WP-69 hardened
/intakes with a 400 last session; removing the surface is the stronger fix,
and WP-69's /applications/{id}/submit enforcement is untouched.
WP-73 — RegistrationStatus becomes an abstract record with three sealed
variants behind a private base ctor, so only Geregistreerd carries a
herregistratie deadline and reden is required on Geschorst/Doorgehaald
(matching the FE union, which was already right). HerregistratieRule
.IsStatusConsistent and its test are deleted: the type now guarantees what
the runtime check was for, and the test could no longer construct the
illegal state it existed to catch.
Aanvraag splits into a Concept | Submitted | Decided union with the EF row
demoted to AanvraagEntity behind a two-way mapper. Submitted carries a
non-null Referentie and SubmittedAt, and Decided.Afgewezen/MeerInfoGevraagd
require a Toelichting — so the five Referentie! null-forgiving derefs in
StatusAt are gone, not merely suppressed. IZaakSource.CreateZaak narrows to
Aanvraag.Submitted, removing the same class of deref in both zaak sources.
Draft is now cleared on submit rather than lingering: ApplicationStore's
doc-comment claimed "Concept only" but Submit never cleared it. Verified
nothing reads a submitted aanvraag's draft (draft-sync's applyResume only
resumes unsubmitted wizards), so the comment is now true instead of
aspirational.
No migration, no schema change, no wire change — RegistrationStatusDto and
the application DTOs are byte-identical, confirmed against a live swagger.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BigRegister.Domain.Applications;
|
||||
|
||||
/// <summary>
|
||||
/// The aanvraag lifecycle as a closed union (WP-73): <see cref="Concept"/> (the pre-submission
|
||||
/// wizard draft) → <see cref="Submitted"/> (awaiting a behandelaar's decision, or already
|
||||
/// auto-rejected at submission time — see <see cref="Submitted.Reden"/>) → <see cref="Decided"/>
|
||||
/// (a behandelaar's outcome recorded). Each variant carries only the fields that make sense for
|
||||
/// it; the private base constructor closes the hierarchy to the nested sealed records below, so
|
||||
/// a caller can never construct a fourth variant, a <see cref="Decided"/> with no referentie, or
|
||||
/// an Afwijzen/MeerInfoGevraagd with no toelichting — each is a compile error (a missing
|
||||
/// `required` member, CS9035), not a runtime null-check the way the old flat, mutable
|
||||
/// <c>Aanvraag</c> needed one.
|
||||
///
|
||||
/// <see cref="Api.Data.AanvraagEntity"/> is the EF-mapped persistence row this maps to/from
|
||||
/// (<c>Api.Data.AanvraagMapper</c>'s <c>ToDomain</c>/<c>ApplyTo</c>/<c>ToEntity</c>) — it stays a
|
||||
/// flat, mutable bag with no invariants of its own (SQLite needs exactly that shape); this type
|
||||
/// is what production code actually reads and writes everywhere else. The wire-facing,
|
||||
/// point-in-time <see cref="AanvraagStatus"/> a screen renders is a further, time-dependent
|
||||
/// projection (<c>StatusAt</c>, in <c>Api.Data</c>) — the auto-approval window is a function of
|
||||
/// wall-clock time, not of this stored shape, so it stays a derived read rather than a fourth
|
||||
/// member of this union.
|
||||
/// </summary>
|
||||
public abstract record Aanvraag
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Type { get; init; } // registratie | herregistratie | intake
|
||||
public required string Owner { get; init; }
|
||||
public required IReadOnlyList<string> DocumentIds { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public required DateTimeOffset UpdatedAt { get; init; }
|
||||
|
||||
/// <summary>The OpenZaak zaak's URL, set once CreateZaak (WP-50) registers one — null under
|
||||
/// the local source, or before a zaak has been registered at all.</summary>
|
||||
public string? ZaakUrl { get; init; }
|
||||
|
||||
/// <summary>WP-60: non-null means the ZGW side of this aanvraag's last write did not
|
||||
/// complete — see <c>Api.Data.ApplicationStore.SetZgwError</c>.</summary>
|
||||
public string? ZgwError { get; init; }
|
||||
|
||||
private Aanvraag() { }
|
||||
|
||||
/// <summary>Pre-submission wizard draft.</summary>
|
||||
public sealed record Concept : Aanvraag
|
||||
{
|
||||
public JsonElement? Draft { get; init; }
|
||||
public int StepIndex { get; }
|
||||
public int StepCount { get; }
|
||||
|
||||
/// <summary>0 <= <paramref name="stepIndex"/> <= <paramref name="stepCount"/> — the
|
||||
/// non-strict upper bound, not the strict "<" a wizard's own step cursor uses, because
|
||||
/// <c>ApplicationStore.CreateConcept</c>'s freshly-created row is (StepIndex: 0, StepCount:
|
||||
/// 0) before the wizard's first draft sync ever runs, and that has to stay constructible.
|
||||
/// </summary>
|
||||
public Concept(int stepIndex, int stepCount)
|
||||
{
|
||||
if (stepIndex < 0 || stepCount < 0 || stepIndex > stepCount)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(stepIndex), stepIndex, $"StepIndex must be within [0, StepCount ({stepCount})].");
|
||||
StepIndex = stepIndex;
|
||||
StepCount = stepCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Submitted, no behandelaar decision recorded yet. <see cref="Reden"/> non-null
|
||||
/// means <c>SubmissionRules</c> rejected it automatically at submission time (e.g. a manually
|
||||
/// entered diploma) — terminal in practice (<c>BeoordelingRules.CanDecide</c> refuses a
|
||||
/// besluit once the projected status is already Afgewezen) but structurally still "no besluit
|
||||
/// was ever recorded", hence it lives here rather than in <see cref="Decided"/>.</summary>
|
||||
public sealed record Submitted : Aanvraag
|
||||
{
|
||||
public required string Referentie { get; init; }
|
||||
public required DateTimeOffset SubmittedAt { get; init; }
|
||||
public required bool AutoApprovable { get; init; }
|
||||
public string? Reden { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A behandelaar's decision (WP-65b/68) — closed by besluit: only
|
||||
/// <see cref="Afgewezen"/>/<see cref="MeerInfoGevraagd"/> require a toelichting
|
||||
/// (<c>BeoordelingRules.RequiresToelichting</c>'s rule, now also a type, not just an endpoint
|
||||
/// check) — omitting it is a compile error, not merely a 400 the type happens to also let
|
||||
/// slip through at runtime.</summary>
|
||||
public abstract record Decided : Aanvraag
|
||||
{
|
||||
public required string Referentie { get; init; }
|
||||
public required DateTimeOffset SubmittedAt { get; init; }
|
||||
|
||||
private Decided() { }
|
||||
|
||||
public sealed record Goedgekeurd : Decided;
|
||||
|
||||
public sealed record Afgewezen : Decided
|
||||
{
|
||||
public required string Toelichting { get; init; }
|
||||
}
|
||||
|
||||
public sealed record MeerInfoGevraagd : Decided
|
||||
{
|
||||
public required string Toelichting { get; init; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user