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; /// /// Behaviour-level tests for the scholing-threshold enforcement (WP-69) over /// POST /aanvragen/{id}/submit (the wizard's real path — WP-72 deleted the legacy /// POST /intakes endpoint this once also covered). Built through the type-state builder, mirroring rather /// than the full wizard/upload dance — the builder's default owner IS 's default caller, so /// no header juggling. /// public class IntakeSubmissionTests(TestWebApplicationFactory factory) : IClassFixture { 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 Submit(string id, object body) => _client.PostAsJsonAsync($"/api/v1/aanvragen/{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(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())!; Assert.Equal("Afgewezen", body.Status.Tag); } }