Files
atomic-design-poc/backend/src/BigRegister.Api/Domain/Applications/Aanvraag.cs
T
ehoandClaude Sonnet 5 8560746d15 refactor: strip WP-/RB- ticket refs from backend (RD-19)
The backend half of the sweep RD-18 did for the front end. git blame
holds the provenance and stays correct when the code moves; the
comment names a closed ticket and tells the reader nothing the
sentence around it does not.

public/letter.css and LetterHtml.golden.html change together, because
the renderer inlines the CSS and the golden file snapshots the
result.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 21:48:08 +02:00

104 lines
4.9 KiB
C#

using System.Text.Json;
namespace BigRegister.Domain.Applications;
/// <summary>
/// The aanvraag lifecycle as a closed union: <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 registers one — null under
/// the local source, or before a zaak has been registered at all.</summary>
public string? ZaakUrl { get; init; }
/// <summary>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 &lt;= <paramref name="stepIndex"/> &lt;= <paramref name="stepCount"/> — the
/// non-strict upper bound, not the strict "&lt;" 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 — 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; }
}
}
}