using System.Threading; using BigRegister.Api.Data; using BigRegister.Domain.Applications; namespace BigRegister.Tests.Builders; /// Fixture identities test builders share across the suite. public static class TestIdentities { /// The default owner for a builder-made — matches /// (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; } /// /// Type-state test-data builder for . "Build /// test data through the same door production code uses" — itself is now /// the closed Concept/Submitted/Decided union, 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 .Decided(...) to call in the first place, and a missing toelichting is a runtime /// guard identical to ApplicationStore.RecordBesluit's own. Start at /// . /// public static class Given { /// A fresh, unsubmitted wizard draft — step 0 of 0 until /// says otherwise, exactly what ApplicationStore.CreateConcept hands back. public static ConceptAanvraag Concept(string type = "registratie", string owner = TestIdentities.DemoBsn) => new(type, owner); } /// A not-yet-submitted aanvraag. The only next step is — there /// is deliberately no Decided here, since only a submitted aanvraag can be decided. 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 of . /// Bounds are 's OWN constructor's to enforce, not this /// builder's — an out-of-range pair fails at , the same /// 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 /// ApplicationStore.Submit), so a fixture built this way can never hit the /// null-forgiving derefs the earlier flat Aanvraag needed (there's nothing to force any /// more: both are required, non-null members of ). 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(), CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow, }; } /// A submitted aanvraag, open for a behandelaar's decision. The only next step is /// — there is no way back to . 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; } /// Registers this aanvraag's already-known OpenZaak zaak URL — mirrors /// , the one production writer of this field, so a /// fixture that needs a pre-existing zaak doesn't reach past Build() to mutate the /// result by hand. public SubmittedAanvraag WithZaakUrl(string zaakUrl) { _zaakUrl = zaakUrl; return this; } /// Records a behandelaar's decision. Unlike the earlier builder, there is no /// hand-written toelichting guard mirroring BeoordelingRules.RequiresToelichting any /// more — / /// 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 string? a wire request would carry), same failure production's own /// ApplicationStore.RecordBesluit raises for the identical input. 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(), 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(), 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(), 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(), CreatedAt = _submittedAt, UpdatedAt = _submittedAt, ZaakUrl = _zaakUrl, Referentie = _referentie, SubmittedAt = _submittedAt, AutoApprovable = _autoApprovable, }; } /// A submitted aanvraag with a behandelaar's decision already recorded — terminal in /// the builder too, matching Goedgekeurd/Afgewezen being terminal in the domain /// (); a fixture that /// needs a SECOND besluit (the MeerInfoGevraagd "still decidable" case) builds fresh from /// again, exactly as a real second request would. Just a one-line /// wrapper around the already-fully-built value — construction moved /// all the actual construction (and its invariant enforcement) into /// itself, so there's nothing left for this type to do /// except keep .Decided(...).Build() a valid two-call chain for the existing test /// suite. public sealed class DecidedAanvraag(Aanvraag.Decided value) { public Aanvraag.Decided Build() => value; }