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>
112 lines
5.2 KiB
C#
112 lines
5.2 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using BigRegister.Api.Contracts;
|
|
using BigRegister.Api.Data;
|
|
using BigRegister.Domain.Applications;
|
|
using BigRegister.Domain.Beoordeling;
|
|
using BigRegister.Tests.Builders;
|
|
|
|
namespace BigRegister.Tests.Acceptance;
|
|
|
|
/// <summary>
|
|
/// Behaviour-level tests for the besluit lifecycle (WP-65b/66/68), built through the
|
|
/// <see cref="Given"/> type-state builder (WP-70) rather than the full wizard/upload dance
|
|
/// <see cref="BeoordelingTests"/> uses — a fixture that's already Submitted (or already
|
|
/// Decided) is a two-line Given, not fifteen. Each test persists its own Given-built
|
|
/// <see cref="Aanvraag"/> straight into the isolated per-class SQLite file (no HTTP round trip
|
|
/// needed to create it) and exercises the real write path from there.
|
|
/// </summary>
|
|
public class BesluitLifecycleTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
|
{
|
|
// Booting the client (once, here) is what makes Db.ConnectionString point at THIS class's
|
|
// throwaway file and runs its migrations — see TestWebApplicationFactory's own docs.
|
|
private readonly HttpClient _client = factory.CreateClient();
|
|
|
|
private static void Persist(Aanvraag aanvraag)
|
|
{
|
|
using var db = Db.Create();
|
|
db.Applications.Add(aanvraag.ToEntity());
|
|
db.SaveChanges();
|
|
}
|
|
|
|
[Fact]
|
|
public void A_terminal_besluit_is_frozen()
|
|
{
|
|
// Given a case already decided Goedgekeurd — terminal, per BeoordelingRules.CanDecide.
|
|
var aanvraag = Given.Concept(type: "registratie").Submitted().Decided(Besluit.Goedkeuren).Build();
|
|
Persist(aanvraag);
|
|
|
|
// When a behandelaar tries to record a further besluit on it...
|
|
var (outcome, updated) = ApplicationStore.RecordBesluit(aanvraag.Id, Besluit.Afwijzen, "te laat", DateTimeOffset.UtcNow);
|
|
|
|
// Then the write is refused, and the original decision still stands.
|
|
Assert.Equal(ApplicationStore.RecordBesluitOutcome.Conflict, outcome);
|
|
Assert.Null(updated);
|
|
var stillGoedgekeurd = ApplicationStore.GetAny(aanvraag.Id)!.StatusAt(DateTimeOffset.UtcNow);
|
|
Assert.Equal(AanvraagStatusTag.Goedgekeurd, stillGoedgekeurd.Tag);
|
|
}
|
|
|
|
[Fact]
|
|
public void MeerInfoGevraagd_can_be_decided_again()
|
|
{
|
|
// Given a case a behandelaar sent back for more information — not terminal.
|
|
var aanvraag = Given.Concept(type: "registratie").Submitted().Decided(Besluit.MeerInfoOpvragen, "stuur een geldig diploma").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When a further besluit is recorded on it...
|
|
var (outcome, updated) = ApplicationStore.RecordBesluit(aanvraag.Id, Besluit.Goedkeuren, null, DateTimeOffset.UtcNow);
|
|
|
|
// Then, unlike a terminal decision, it succeeds and advances the status.
|
|
Assert.Equal(ApplicationStore.RecordBesluitOutcome.Ok, outcome);
|
|
Assert.Equal(AanvraagStatusTag.Goedgekeurd, updated!.StatusAt(DateTimeOffset.UtcNow).Tag);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_recorded_decision_wins_over_the_auto_approve_computation()
|
|
{
|
|
// Given an auto-approvable submission that a behandelaar decides (Afwijzen) before the
|
|
// auto-approve window would otherwise have closed it as Goedgekeurd.
|
|
var aanvraag = Given.Concept(type: "registratie").Submitted(autoApprovable: true).Build();
|
|
Persist(aanvraag);
|
|
var (outcome, _) = ApplicationStore.RecordBesluit(aanvraag.Id, Besluit.Afwijzen, "diploma niet erkend", DateTimeOffset.UtcNow);
|
|
Assert.Equal(ApplicationStore.RecordBesluitOutcome.Ok, outcome);
|
|
|
|
// When the status is read long after the auto-approve window has passed — the instant an
|
|
// undecided auto-approvable case of the same shape WOULD read Goedgekeurd (see
|
|
// ApplicationTests.AutoApprovable_flips_to_goedgekeurd_after_the_window)...
|
|
var longAfterTheWindow = aanvraag.SubmittedAt + ApplicationStore.ProcessingWindow + TimeSpan.FromDays(1);
|
|
var status = ApplicationStore.GetAny(aanvraag.Id)!.StatusAt(longAfterTheWindow);
|
|
|
|
// Then the recorded decision still wins — Afgewezen, never Goedgekeurd.
|
|
Assert.Equal(AanvraagStatusTag.Afgewezen, status.Tag);
|
|
}
|
|
|
|
private Task<HttpResponseMessage> PostBesluit(string id, object body)
|
|
{
|
|
var req = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/beoordeling/{id}/besluit") { Content = JsonContent.Create(body) };
|
|
req.Headers.Add("X-Medewerker", "medewerker-1");
|
|
return _client.SendAsync(req);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Afwijzen_requires_a_toelichting()
|
|
{
|
|
// Given an open, decidable case (no decision recorded yet).
|
|
var aanvraag = Given.Concept(type: "registratie").Submitted().Build();
|
|
Persist(aanvraag);
|
|
|
|
// When a behandelaar posts Afwijzen with no toelichting...
|
|
var missing = await PostBesluit(aanvraag.Id, new { besluit = "Afwijzen" });
|
|
|
|
// Then the request is rejected — the wire boundary enforces the same rule
|
|
// (BeoordelingRules.RequiresToelichting) the builder enforces for a built fixture.
|
|
Assert.Equal(HttpStatusCode.BadRequest, missing.StatusCode);
|
|
|
|
// And the identical request WITH a toelichting succeeds.
|
|
var withToelichting = await PostBesluit(aanvraag.Id, new { besluit = "Afwijzen", toelichting = "Diploma niet erkend" });
|
|
withToelichting.EnsureSuccessStatusCode();
|
|
var body = (await withToelichting.Content.ReadFromJsonAsync<RecordBesluitResponse>())!;
|
|
Assert.Equal("Afgewezen", body.Status.Tag);
|
|
}
|
|
}
|