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>
104 lines
4.9 KiB
C#
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 <= <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 — 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; }
|
|
}
|
|
}
|
|
}
|