Files
atomic-design-poc/backend/tests/BigRegister.Tests/Builders/AanvraagBuilder.cs
T
ehoandClaude Sonnet 5 6bc00a917c 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>
2026-08-19 16:31:38 +02:00

198 lines
8.3 KiB
C#

using System.Threading;
using BigRegister.Api.Data;
using BigRegister.Domain.Applications;
namespace BigRegister.Tests.Builders;
/// <summary>Fixture identities test builders share across the suite.</summary>
public static class TestIdentities
{
/// The default owner for a builder-made <see cref="Aanvraag"/> — matches
/// <see cref="DocumentStore.DemoOwner"/> (the demo's only seeded user) so a fixture that
/// doesn't care about identity gets a realistic, elfproef-valid BSN for free.
public const string DemoBsn = DocumentStore.DemoOwner;
}
/// <summary>
/// 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
{
/// A fresh, unsubmitted wizard draft — step 0 of 0 until <see cref="ConceptAanvraag.AtStep"/>
/// says otherwise, exactly what <c>ApplicationStore.CreateConcept</c> hands back.
public static ConceptAanvraag Concept(string type = "registratie", string owner = TestIdentities.DemoBsn) =>
new(type, owner);
}
/// <summary>A not-yet-submitted aanvraag. The only next step is <see cref="Submitted"/> — there
/// is deliberately no <c>Decided</c> here, since only a submitted aanvraag can be decided.</summary>
public sealed class ConceptAanvraag
{
private readonly string _type;
private readonly string _owner;
private int _stepIndex;
private int _stepCount;
internal ConceptAanvraag(string type, string owner)
{
_type = type;
_owner = owner;
}
/// The wizard's current position — step <paramref name="index"/> of <paramref name="of"/>.
/// 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)
{
_stepIndex = index;
_stepCount = of;
return this;
}
/// Submits the draft — always assigns a Referentie AND SubmittedAt together (mirrors
/// <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.Concept Build() => new(_stepIndex, _stepCount)
{
Id = Guid.NewGuid().ToString(),
Type = _type,
Owner = _owner,
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 <see cref="ConceptAanvraag"/>.</summary>
public sealed class SubmittedAanvraag
{
private static int _referentieSeq;
private readonly string _type;
private readonly string _owner;
private readonly bool _autoApprovable;
private readonly string _referentie;
private readonly DateTimeOffset _submittedAt;
private string? _zaakUrl;
internal SubmittedAanvraag(string type, string owner, bool autoApprovable)
{
_type = type;
_owner = owner;
_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.
_referentie = $"BIG-2026-{Interlocked.Increment(ref _referentieSeq)}";
_submittedAt = DateTimeOffset.UtcNow;
}
/// <summary>Registers this aanvraag's already-known OpenZaak zaak URL — mirrors
/// <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. 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)
{
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.Submitted Build() => new()
{
Id = Guid.NewGuid().ToString(),
Type = _type,
Owner = _owner,
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, 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)
{
public Aanvraag.Decided Build() => value;
}