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:
@@ -1,7 +1,6 @@
|
||||
using System.Threading;
|
||||
using BigRegister.Api.Data;
|
||||
using BigRegister.Domain.Applications;
|
||||
using BigRegister.Domain.Beoordeling;
|
||||
|
||||
namespace BigRegister.Tests.Builders;
|
||||
|
||||
@@ -15,18 +14,16 @@ public static class TestIdentities
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type-state test-data builder for <see cref="Aanvraag"/> (WP-70). "Build test data through the
|
||||
/// same door production code uses" — a Concept can only ever become Submitted, and only a
|
||||
/// Submitted aanvraag can be Decided, so the compiler refuses a fixture built through an illegal
|
||||
/// path (e.g. deciding a still-Concept aanvraag) instead of that being a runtime assertion nobody
|
||||
/// wrote. Start at <see cref="Given.Concept"/>.
|
||||
///
|
||||
/// ponytail: <see cref="Aanvraag"/> itself stays exactly what it always was — a mutable,
|
||||
/// EF-backed bag with no invariants of its own (that's Data/ApplicationStore.cs's job in
|
||||
/// production, via its own lock + <see cref="BeoordelingRules"/> checks). This builder does not
|
||||
/// refactor it into an immutable aggregate; it's the one enforced DOOR through which TEST code
|
||||
/// builds one, so the invariants a real request path enforces don't quietly go missing from a
|
||||
/// fixture assembled by hand.
|
||||
/// Type-state test-data builder for <see cref="Aanvraag"/> (WP-70; simplified at WP-73). "Build
|
||||
/// test data through the same door production code uses" — <see cref="Aanvraag"/> itself is now
|
||||
/// the closed Concept/Submitted/Decided union WP-73 introduced, so this builder no longer needs
|
||||
/// to mirror production's guards (step-index bounds, "Afwijzen needs a toelichting") by hand —
|
||||
/// it just calls the real nested constructors/required members, which enforce them. A call that
|
||||
/// would build an illegal Aanvraag (e.g. deciding a still-Concept aanvraag, or an Afwijzen with
|
||||
/// no toelichting) is refused the same way production refuses it: a still-Concept aanvraag has
|
||||
/// no <c>.Decided(...)</c> to call in the first place, and a missing toelichting is a runtime
|
||||
/// guard identical to <c>ApplicationStore.RecordBesluit</c>'s own. Start at
|
||||
/// <see cref="Given.Concept"/>.
|
||||
/// </summary>
|
||||
public static class Given
|
||||
{
|
||||
@@ -52,60 +49,50 @@ public sealed class ConceptAanvraag
|
||||
}
|
||||
|
||||
/// The wizard's current position — step <paramref name="index"/> of <paramref name="of"/>.
|
||||
/// Guarded the same way a real cursor is (`STEPS[Math.min(cursor, STEPS.length - 1)]` on the
|
||||
/// frontend): <paramref name="of"/> must be at least 1, and <paramref name="index"/> must fall
|
||||
/// within <c>[0, of)</c> — <c>AtStep(9, 2)</c> is not a position any real wizard can reach, so
|
||||
/// the builder refuses it instead of silently building an impossible fixture.
|
||||
/// Bounds are <see cref="Aanvraag.Concept"/>'s OWN constructor's to enforce, not this
|
||||
/// builder's — an out-of-range pair fails at <see cref="Build"/>, the same
|
||||
/// <see cref="ArgumentOutOfRangeException"/> production throws, not a guard restated here.
|
||||
public ConceptAanvraag AtStep(int index, int of)
|
||||
{
|
||||
if (of < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(of), of, "Step count must be at least 1.");
|
||||
if (index < 0 || index >= of)
|
||||
throw new ArgumentOutOfRangeException(nameof(index), index, $"Step index must be within [0, {of}).");
|
||||
_stepIndex = index;
|
||||
_stepCount = of;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Submits the draft — always assigns a Referentie AND SubmittedAt together (mirrors
|
||||
/// <c>ApplicationStore.Submit</c>), so <c>Aanvraag.StatusAt</c>'s <c>Referentie!</c> is honest
|
||||
/// for every fixture built this way, never a null-ref waiting to happen.
|
||||
public SubmittedAanvraag Submitted(bool autoApprovable = false) =>
|
||||
new(_type, _owner, _stepIndex, _stepCount, autoApprovable);
|
||||
/// <c>ApplicationStore.Submit</c>), so a fixture built this way can never hit the
|
||||
/// null-forgiving derefs the pre-WP-73 flat Aanvraag needed (there's nothing to force any
|
||||
/// more: both are required, non-null members of <see cref="Aanvraag.Submitted"/>).
|
||||
public SubmittedAanvraag Submitted(bool autoApprovable = false) => new(_type, _owner, autoApprovable);
|
||||
|
||||
public Aanvraag Build() => new()
|
||||
public Aanvraag.Concept Build() => new(_stepIndex, _stepCount)
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Type = _type,
|
||||
Owner = _owner,
|
||||
StepIndex = _stepIndex,
|
||||
StepCount = _stepCount,
|
||||
DocumentIds = Array.Empty<string>(),
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>A submitted aanvraag, open for a behandelaar's decision. The only next step is
|
||||
/// <see cref="Decided"/> — there is no way back to <c>ConceptAanvraag</c>.</summary>
|
||||
/// <see cref="Decided"/> — there is no way back to <see cref="ConceptAanvraag"/>.</summary>
|
||||
public sealed class SubmittedAanvraag
|
||||
{
|
||||
private static int _referentieSeq;
|
||||
|
||||
private readonly string _type;
|
||||
private readonly string _owner;
|
||||
private readonly int _stepIndex;
|
||||
private readonly int _stepCount;
|
||||
private readonly bool _autoApprovable;
|
||||
private readonly string _referentie;
|
||||
private readonly DateTimeOffset _submittedAt;
|
||||
private string? _zaakUrl;
|
||||
|
||||
internal SubmittedAanvraag(string type, string owner, int stepIndex, int stepCount, bool autoApprovable)
|
||||
internal SubmittedAanvraag(string type, string owner, bool autoApprovable)
|
||||
{
|
||||
_type = type;
|
||||
_owner = owner;
|
||||
_stepIndex = stepIndex;
|
||||
_stepCount = stepCount;
|
||||
_autoApprovable = autoApprovable;
|
||||
// A plausible reference in SubmissionRules.NewReference's shape ("BIG-2026-" + a number) —
|
||||
// sequential (not random) so a fixture's value is reproducible across a test run.
|
||||
@@ -114,67 +101,97 @@ public sealed class SubmittedAanvraag
|
||||
}
|
||||
|
||||
/// <summary>Registers this aanvraag's already-known OpenZaak zaak URL — mirrors
|
||||
/// <see cref="Api.Data.ApplicationStore.SetZaakUrl"/>, the one production writer of this
|
||||
/// field, so a fixture that needs a pre-existing zaak doesn't reach past <c>Build()</c> to
|
||||
/// mutate the result by hand.</summary>
|
||||
/// <see cref="ApplicationStore.SetZaakUrl"/>, the one production writer of this field, so a
|
||||
/// fixture that needs a pre-existing zaak doesn't reach past <c>Build()</c> to mutate the
|
||||
/// result by hand.</summary>
|
||||
public SubmittedAanvraag WithZaakUrl(string zaakUrl)
|
||||
{
|
||||
_zaakUrl = zaakUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Records a behandelaar's decision — reusing <see cref="BeoordelingRules.RequiresToelichting"/>,
|
||||
/// the SAME rule production's besluit endpoint runs, rather than restating it here where it
|
||||
/// could quietly drift. Throws <see cref="ArgumentException"/> for an Afwijzen/MeerInfoOpvragen
|
||||
/// with a null/blank <paramref name="toelichting"/> — exactly what that endpoint rejects with
|
||||
/// a 400, just caught here at fixture-build time instead.</summary>
|
||||
public DecidedAanvraag Decided(Besluit besluit, string? toelichting = null)
|
||||
/// <summary>Records a behandelaar's decision. Unlike the pre-WP-73 builder, there is no
|
||||
/// hand-written toelichting guard mirroring <c>BeoordelingRules.RequiresToelichting</c> any
|
||||
/// more — <see cref="Aanvraag.Decided.Afgewezen"/>/<see cref="Aanvraag.Decided.MeerInfoGevraagd"/>
|
||||
/// simply have a `required string Toelichting` member; the null-coalescing throw below is the
|
||||
/// one place a null has to turn into an exception (this method's own parameter is still the
|
||||
/// nullable <c>string?</c> a wire request would carry), same failure production's own
|
||||
/// <c>ApplicationStore.RecordBesluit</c> raises for the identical input.</summary>
|
||||
public DecidedAanvraag Decided(Besluit besluit, string? toelichting = null) => new(BuildDecided(besluit, toelichting));
|
||||
|
||||
private Aanvraag.Decided BuildDecided(Besluit besluit, string? toelichting)
|
||||
{
|
||||
if (BeoordelingRules.RequiresToelichting(besluit) && string.IsNullOrWhiteSpace(toelichting))
|
||||
throw new ArgumentException($"{besluit} requires a toelichting.", nameof(toelichting));
|
||||
return new DecidedAanvraag(this, besluit, toelichting);
|
||||
var (id, createdAt) = (Guid.NewGuid().ToString(), _submittedAt);
|
||||
return besluit switch
|
||||
{
|
||||
Besluit.Goedkeuren => new Aanvraag.Decided.Goedgekeurd
|
||||
{
|
||||
Id = id,
|
||||
Type = _type,
|
||||
Owner = _owner,
|
||||
DocumentIds = Array.Empty<string>(),
|
||||
CreatedAt = createdAt,
|
||||
UpdatedAt = createdAt,
|
||||
ZaakUrl = _zaakUrl,
|
||||
Referentie = _referentie,
|
||||
SubmittedAt = _submittedAt,
|
||||
},
|
||||
Besluit.Afwijzen => new Aanvraag.Decided.Afgewezen
|
||||
{
|
||||
Id = id,
|
||||
Type = _type,
|
||||
Owner = _owner,
|
||||
DocumentIds = Array.Empty<string>(),
|
||||
CreatedAt = createdAt,
|
||||
UpdatedAt = createdAt,
|
||||
ZaakUrl = _zaakUrl,
|
||||
Referentie = _referentie,
|
||||
SubmittedAt = _submittedAt,
|
||||
Toelichting = toelichting ?? throw new ArgumentException("Afwijzen requires a toelichting.", nameof(toelichting)),
|
||||
},
|
||||
Besluit.MeerInfoOpvragen => new Aanvraag.Decided.MeerInfoGevraagd
|
||||
{
|
||||
Id = id,
|
||||
Type = _type,
|
||||
Owner = _owner,
|
||||
DocumentIds = Array.Empty<string>(),
|
||||
CreatedAt = createdAt,
|
||||
UpdatedAt = createdAt,
|
||||
ZaakUrl = _zaakUrl,
|
||||
Referentie = _referentie,
|
||||
SubmittedAt = _submittedAt,
|
||||
Toelichting = toelichting ?? throw new ArgumentException("MeerInfoOpvragen requires a toelichting.", nameof(toelichting)),
|
||||
},
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(besluit), besluit, "Unknown besluit."),
|
||||
};
|
||||
}
|
||||
|
||||
public Aanvraag Build() => new()
|
||||
public Aanvraag.Submitted Build() => new()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Type = _type,
|
||||
Owner = _owner,
|
||||
StepIndex = _stepIndex,
|
||||
StepCount = _stepCount,
|
||||
Submitted = true,
|
||||
Referentie = _referentie,
|
||||
AutoApprovable = _autoApprovable,
|
||||
SubmittedAt = _submittedAt,
|
||||
DocumentIds = Array.Empty<string>(),
|
||||
CreatedAt = _submittedAt,
|
||||
UpdatedAt = _submittedAt,
|
||||
ZaakUrl = _zaakUrl,
|
||||
Referentie = _referentie,
|
||||
SubmittedAt = _submittedAt,
|
||||
AutoApprovable = _autoApprovable,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>A submitted aanvraag with a behandelaar's decision already recorded. Terminal in the
|
||||
/// builder too — there's nothing past <see cref="Build"/>, matching Goedgekeurd/Afgewezen being
|
||||
/// terminal in the domain (<see cref="BeoordelingRules.CanDecide"/>); a fixture that needs a
|
||||
/// SECOND besluit (the MeerInfoGevraagd "still decidable" case) builds fresh from
|
||||
/// <see cref="Given.Concept"/> again, exactly as a real second request would.</summary>
|
||||
public sealed class DecidedAanvraag
|
||||
/// <summary>A submitted aanvraag with a behandelaar's decision already recorded — terminal in
|
||||
/// the builder too, matching Goedgekeurd/Afgewezen being terminal in the domain
|
||||
/// (<see cref="BigRegister.Domain.Beoordeling.BeoordelingRules.CanDecide"/>); a fixture that
|
||||
/// needs a SECOND besluit (the MeerInfoGevraagd "still decidable" case) builds fresh from
|
||||
/// <see cref="Given.Concept"/> again, exactly as a real second request would. Just a one-line
|
||||
/// wrapper around the already-fully-built <see cref="Aanvraag.Decided"/> value — WP-73 moved
|
||||
/// all the actual construction (and its invariant enforcement) into
|
||||
/// <see cref="SubmittedAanvraag.Decided"/> itself, so there's nothing left for this type to do
|
||||
/// except keep <c>.Decided(...).Build()</c> a valid two-call chain for the existing test
|
||||
/// suite.</summary>
|
||||
public sealed class DecidedAanvraag(Aanvraag.Decided value)
|
||||
{
|
||||
private readonly SubmittedAanvraag _submitted;
|
||||
private readonly Besluit _besluit;
|
||||
private readonly string? _toelichting;
|
||||
|
||||
internal DecidedAanvraag(SubmittedAanvraag submitted, Besluit besluit, string? toelichting)
|
||||
{
|
||||
_submitted = submitted;
|
||||
_besluit = besluit;
|
||||
_toelichting = toelichting;
|
||||
}
|
||||
|
||||
public Aanvraag Build()
|
||||
{
|
||||
var aanvraag = _submitted.Build();
|
||||
aanvraag.BesluitStatus = _besluit;
|
||||
aanvraag.BesluitToelichting = _toelichting;
|
||||
return aanvraag;
|
||||
}
|
||||
public Aanvraag.Decided Build() => value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user