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>
124 lines
4.5 KiB
C#
124 lines
4.5 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using BigRegister.Api.Contracts;
|
|
using BigRegister.Api.Data;
|
|
using BigRegister.Domain.Applications;
|
|
using BigRegister.Tests.Builders;
|
|
|
|
namespace BigRegister.Tests.Acceptance;
|
|
|
|
/// <summary>
|
|
/// Behaviour-level tests for the scholing-threshold enforcement (WP-69) over
|
|
/// <c>POST /applications/{id}/submit</c> (the wizard's real path — WP-72 deleted the legacy
|
|
/// <c>POST /intakes</c> endpoint this once also covered). Built through the <see
|
|
/// cref="Given"/> type-state builder, mirroring <see cref="BesluitLifecycleTests"/> rather
|
|
/// than the full wizard/upload dance — the builder's default owner IS <see
|
|
/// cref="BigRegister.Api.Domain.Authorization.StubIdentityProvider"/>'s default caller, so
|
|
/// no header juggling.
|
|
/// </summary>
|
|
public class IntakeSubmissionTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
|
{
|
|
private readonly HttpClient _client = factory.CreateClient();
|
|
|
|
private static void Persist(Aanvraag aanvraag)
|
|
{
|
|
using var db = Db.Create();
|
|
db.Applications.Add(aanvraag.ToEntity());
|
|
db.SaveChanges();
|
|
}
|
|
|
|
private Task<HttpResponseMessage> Submit(string id, object body) =>
|
|
_client.PostAsJsonAsync($"/api/v1/applications/{id}/submit", body);
|
|
|
|
[Fact]
|
|
public async Task Below_threshold_without_an_answer_is_rejected_and_stays_a_concept()
|
|
{
|
|
// Given an intake-typed Concept aanvraag (not yet submitted).
|
|
var aanvraag = Given.Concept(type: "intake").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When it is submitted with uren below the threshold and no scholing answer at all...
|
|
var res = await Submit(aanvraag.Id, new { uren = 500 });
|
|
|
|
// Then the request is rejected as a contract violation (400, not a merit rejection)...
|
|
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
|
|
|
|
// ...and the aanvraag is left a retryable Concept, never marked Submitted.
|
|
Assert.IsType<Aanvraag.Concept>(ApplicationStore.GetAny(aanvraag.Id));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Below_threshold_with_an_answer_succeeds()
|
|
{
|
|
// Given an intake-typed Concept.
|
|
var aanvraag = Given.Concept(type: "intake").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When submitted below the threshold with "niet gevolgd" — a complete, legal answer...
|
|
var res = await Submit(aanvraag.Id, new { uren = 500, aanvullendeScholing = false });
|
|
|
|
// Then the submission succeeds.
|
|
res.EnsureSuccessStatusCode();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Above_threshold_needs_no_answer()
|
|
{
|
|
// Given an intake-typed Concept.
|
|
var aanvraag = Given.Concept(type: "intake").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When submitted with uren at/above the threshold and no scholing answer...
|
|
var res = await Submit(aanvraag.Id, new { uren = 1000 });
|
|
|
|
// Then it succeeds — the question is moot above the threshold.
|
|
res.EnsureSuccessStatusCode();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Punten_without_gevolgd_is_rejected()
|
|
{
|
|
// Given an intake-typed Concept.
|
|
var aanvraag = Given.Concept(type: "intake").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When submitted above the threshold with punten but no "gevolgd" answer — the stale
|
|
// shape §6 fixes on the frontend, still reachable as a crafted POST...
|
|
var res = await Submit(aanvraag.Id, new { uren = 1500, scholingPunten = 150 });
|
|
|
|
// Then it is rejected.
|
|
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Herregistratie_is_unaffected_by_the_intake_only_gate()
|
|
{
|
|
// Given a herregistratie-typed Concept (no scholing question in that wizard).
|
|
var aanvraag = Given.Concept(type: "herregistratie").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When submitted below the intake threshold with no scholing answer at all...
|
|
var res = await Submit(aanvraag.Id, new { uren = 500 });
|
|
|
|
// Then it still succeeds — the gate is intake-only.
|
|
res.EnsureSuccessStatusCode();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Zero_uren_is_still_afgewezen_not_a_400()
|
|
{
|
|
// Given an intake-typed Concept.
|
|
var aanvraag = Given.Concept(type: "intake").Build();
|
|
Persist(aanvraag);
|
|
|
|
// When submitted with zero uren and no scholing answer — completeness would also
|
|
// reject this, but the merit rejection (RejectZeroUren) must win (the ordering guard)...
|
|
var res = await Submit(aanvraag.Id, new { uren = 0 });
|
|
|
|
// Then the submission is accepted and resolves to Afgewezen — not a 400.
|
|
res.EnsureSuccessStatusCode();
|
|
var body = (await res.Content.ReadFromJsonAsync<SubmitApplicationResponse>())!;
|
|
Assert.Equal("Afgewezen", body.Status.Tag);
|
|
}
|
|
}
|