using System.Threading; using BigRegister.Api.Data; using BigRegister.Domain.Applications; using BigRegister.Domain.Beoordeling; 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 (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 . /// /// ponytail: 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 + 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. /// 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 . 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 Aanvraag.StatusAt's Referentie! 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); public Aanvraag Build() => new() { Id = Guid.NewGuid().ToString(), Type = _type, Owner = _owner, StepIndex = _stepIndex, StepCount = _stepCount, 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 ConceptAanvraag. 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; internal SubmittedAanvraag(string type, string owner, int stepIndex, int stepCount, 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. _referentie = $"BIG-2026-{Interlocked.Increment(ref _referentieSeq)}"; _submittedAt = DateTimeOffset.UtcNow; } /// Records a behandelaar's decision — reusing , /// the SAME rule production's besluit endpoint runs, rather than restating it here where it /// could quietly drift. Throws for an Afwijzen/MeerInfoOpvragen /// with a null/blank — exactly what that endpoint rejects with /// a 400, just caught here at fixture-build time instead. public DecidedAanvraag Decided(Besluit besluit, string? toelichting = null) { if (BeoordelingRules.RequiresToelichting(besluit) && string.IsNullOrWhiteSpace(toelichting)) throw new ArgumentException($"{besluit} requires a toelichting.", nameof(toelichting)); return new DecidedAanvraag(this, besluit, toelichting); } public Aanvraag Build() => new() { Id = Guid.NewGuid().ToString(), Type = _type, Owner = _owner, StepIndex = _stepIndex, StepCount = _stepCount, Submitted = true, Referentie = _referentie, AutoApprovable = _autoApprovable, SubmittedAt = _submittedAt, CreatedAt = _submittedAt, UpdatedAt = _submittedAt, }; } /// A submitted aanvraag with a behandelaar's decision already recorded. Terminal in the /// builder too — there's nothing past , 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. public sealed class DecidedAanvraag { 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; } }