diff --git a/backend/src/BigRegister.Api/Contracts/Mappers.cs b/backend/src/BigRegister.Api/Contracts/Mappers.cs index 17ea939..dc26083 100644 --- a/backend/src/BigRegister.Api/Contracts/Mappers.cs +++ b/backend/src/BigRegister.Api/Contracts/Mappers.cs @@ -40,15 +40,18 @@ public static class Mappers // Goedgekeurd once past the processing window, else In behandeling; a manual case // stays In behandeling forever (awaits the unbuilt backoffice). Pure — testable // by passing different `now` values without waiting for the wall clock. + // + // Ingediend/MeerInfoGevraagd (AanvraagStatusTag, WP-63) aren't produced here yet — no + // behandelaar action exists to reach them (WP-65 adds the transition endpoint). public static AanvraagStatusDto ToStatusDto(this Aanvraag a, DateTimeOffset now) { if (!a.Submitted) return new("Concept", StepIndex: a.StepIndex, StepCount: a.StepCount); if (a.Reden is not null) - return new("Afgewezen", Referentie: a.Referentie, Reden: a.Reden); + return new(AanvraagStatusTag.Afgewezen.ToString(), Referentie: a.Referentie, Reden: a.Reden); if (a.AutoApprovable && now > a.SubmittedAt!.Value + ApplicationStore.ProcessingWindow) - return new("Goedgekeurd", Referentie: a.Referentie); - return new("InBehandeling", Referentie: a.Referentie, Manual: !a.AutoApprovable); + return new(AanvraagStatusTag.Goedgekeurd.ToString(), Referentie: a.Referentie); + return new(AanvraagStatusTag.InBehandeling.ToString(), Referentie: a.Referentie, Manual: !a.AutoApprovable); } public static ApplicationSummaryDto ToSummaryDto(this Aanvraag a, DateTimeOffset now) => new( diff --git a/backend/src/BigRegister.Api/Data/ApplicationStore.cs b/backend/src/BigRegister.Api/Data/ApplicationStore.cs index b8e291e..d778e98 100644 --- a/backend/src/BigRegister.Api/Data/ApplicationStore.cs +++ b/backend/src/BigRegister.Api/Data/ApplicationStore.cs @@ -3,6 +3,16 @@ using BigRegister.Domain.Submissions; namespace BigRegister.Api.Data; +/// +/// The post-submission aanvraag status lifecycle (ADR-0002, WP-63): Ingediend → In +/// behandeling → (Meer info gevraagd ⇄) → Goedgekeurd/Afgewezen. Concept (pre-submission, +/// the wizard draft) isn't part of this enum — see . +/// and are not reachable yet: no +/// endpoint sets them (that's WP-65's behandelaar-facing mutation) — modelled here so the +/// contract is ready when it does. +/// +public enum AanvraagStatusTag { Ingediend, InBehandeling, MeerInfoGevraagd, Goedgekeurd, Afgewezen } + /// /// An application (aanvraag) — the system of record the dashboard reads. A wizard /// creates one as a Concept on its first step, syncs its draft snapshot per step, diff --git a/backend/tests/BigRegister.Tests/ApplicationTests.cs b/backend/tests/BigRegister.Tests/ApplicationTests.cs index 65638ac..7a773b9 100644 --- a/backend/tests/BigRegister.Tests/ApplicationTests.cs +++ b/backend/tests/BigRegister.Tests/ApplicationTests.cs @@ -205,4 +205,15 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture Assert.Equal("InBehandeling", status.Tag); Assert.True(status.Manual); } + + // WP-63: the published lifecycle (ADR-0002) must name exactly these five tags, in this + // order — ToStatusDto's string literals must keep matching Enum.ToString(), and Ingediend/ + // MeerInfoGevraagd (unreachable until WP-65 adds the behandelaar transition) stay defined. + [Fact] + public void AanvraagStatusTag_covers_the_published_lifecycle() + { + Assert.Equal( + new[] { "Ingediend", "InBehandeling", "MeerInfoGevraagd", "Goedgekeurd", "Afgewezen" }, + Enum.GetNames()); + } } diff --git a/docs/project/backlog/README.md b/docs/project/backlog/README.md index b9bfc9c..4db7d6d 100644 --- a/docs/project/backlog/README.md +++ b/docs/project/backlog/README.md @@ -113,7 +113,7 @@ for its existing violations, so every WP ends green. | [WP-60](WP-60-write-divergence-resilience.md) | Write-divergence resilience (local + ZGW writes) | 10 · OpenZaak hardening | done | | [WP-61](WP-61-behandelportal-bootstrap.md) | Bootstrap the behandelportal app | 11 · Behandelportal | done | | [WP-62](WP-62-medewerker-identity-authz.md) | Backend: medewerker caller identity + authz seam | 11 · Behandelportal | done | -| [WP-63](WP-63-aanvraag-status-lifecycle.md) | Backend: aanvraag status lifecycle as a published DTO | 11 · Behandelportal | todo | +| [WP-63](WP-63-aanvraag-status-lifecycle.md) | Backend: aanvraag status lifecycle as a published DTO | 11 · Behandelportal | done | | [WP-64](WP-64-behandelportal-werkvoorraad.md) | Behandelportal: werkvoorraad (queue) screen | 11 · Behandelportal | todo | | [WP-65](WP-65-behandelportal-beoordeling.md) | Behandelportal: zaak detail + beoordeling (decision) screen | 11 · Behandelportal | todo | | [WP-66](WP-66-behandelportal-openzaak-write.md) | Wire the decision into OpenZaak | 11 · Behandelportal | todo | diff --git a/docs/project/backlog/WP-63-aanvraag-status-lifecycle.md b/docs/project/backlog/WP-63-aanvraag-status-lifecycle.md index fb8dba8..1d3bfe0 100644 --- a/docs/project/backlog/WP-63-aanvraag-status-lifecycle.md +++ b/docs/project/backlog/WP-63-aanvraag-status-lifecycle.md @@ -1,6 +1,6 @@ # WP-63 — Backend: aanvraag status lifecycle as a published DTO -Status: todo +Status: done Phase: 11 — Behandelportal ## Why @@ -48,10 +48,27 @@ read (this WP), the behandelportal needs it as the thing it advances (WP-65). ## Acceptance criteria -- [ ] Backend publishes the full status lifecycle value on the existing aanvraag DTO. -- [ ] `npm run gen:api` leaves no drift; SSP's existing "pending" display is unchanged in +- [x] Backend publishes the full status lifecycle value on the existing aanvraag DTO. +- [x] `npm run gen:api` leaves no drift; SSP's existing "pending" display is unchanged in behavior, now backed by the real status. -- [ ] `dotnet test` + `npm run ci` green. +- [x] `dotnet test` + `npm run ci` green. + +## Outcome + +Implemented as a pure type-system widening, not a behavior change: `AanvraagStatusTag` +(`Ingediend | InBehandeling | MeerInfoGevraagd | Goedgekeurd | Afgewezen`) is a new C# enum +backing `Mappers.ToStatusDto`'s existing string literals — `AanvraagStatusDto.Tag` stays a +plain string, so the OpenAPI schema (and `npm run gen:api`) don't change at all, satisfying +"zero required FE behavior change" trivially. `Ingediend`/`MeerInfoGevraagd` aren't reachable +from any code path yet (no behandelaar action exists to produce them) — that's WP-65's +transition endpoint, exactly per this WP's own Risks note. The FE `AanvraagStatus` union, +`parseAanvraagStatus`, `statusLabel`/`submittedRow`/`detailRows`, `blockActions`, and the +dashboard's sort order were all widened to the two new tags so TypeScript's exhaustiveness +checking forces every switch to handle them once WP-65 starts emitting them. +`big-profile.store.ts`'s `pendingHerregistratie` was deliberately left untouched — it's a +pure client-side optimistic UI flag unrelated to any DTO field (not what the WP's "Why" +section implied), and the decision text's "or kept as a computed convenience" explicitly +allows this. ## Verification diff --git a/src/app/registratie/domain/aanvraag-view.spec.ts b/src/app/registratie/domain/aanvraag-view.spec.ts index 8899f1c..8a7d3e9 100644 --- a/src/app/registratie/domain/aanvraag-view.spec.ts +++ b/src/app/registratie/domain/aanvraag-view.spec.ts @@ -45,6 +45,14 @@ describe('submittedRow', () => { } as Aanvraag); expect(rejected.status).toContain('Onvoldoende uren'); }); + + it('meer-info-gevraagd adds its reason, like a rejection', () => { + const row = submittedRow({ + ...base, + status: { tag: 'MeerInfoGevraagd', referentie: 'R3', reden: 'Diploma ontbreekt' }, + } as Aanvraag); + expect(row.status).toContain('Diploma ontbreekt'); + }); }); describe('detailRows', () => { diff --git a/src/app/registratie/domain/aanvraag-view.ts b/src/app/registratie/domain/aanvraag-view.ts index f8c75d5..bcf26bc 100644 --- a/src/app/registratie/domain/aanvraag-view.ts +++ b/src/app/registratie/domain/aanvraag-view.ts @@ -28,8 +28,12 @@ export function statusLabel(status: AanvraagStatus): string { switch (status.tag) { case 'Concept': return $localize`:@@aanvraag.status.concept:Concept (nog niet ingediend)`; + case 'Ingediend': + return $localize`:@@aanvraag.status.ingediend:Ingediend`; case 'InBehandeling': return $localize`:@@aanvraag.status.inBehandeling:In behandeling`; + case 'MeerInfoGevraagd': + return $localize`:@@aanvraag.status.meerInfoGevraagd:Meer informatie gevraagd`; case 'Goedgekeurd': return $localize`:@@aanvraag.status.goedgekeurd:Goedgekeurd`; case 'Afgewezen': @@ -65,7 +69,7 @@ export function submittedRow(a: Aanvraag): AanvraagRow { parts.push( $localize`:@@aanvraagBlock.manual:Uw aanvraag wordt handmatig beoordeeld in de backoffice.`, ); - if (s.tag === 'Afgewezen') parts.push(s.reden); + if (s.tag === 'Afgewezen' || s.tag === 'MeerInfoGevraagd') parts.push(s.reden); return { heading: TYPE_LABELS[a.type], subtitle: purposeLabel(a.type), @@ -94,5 +98,11 @@ export function detailRows(a: Aanvraag): { key: string; value: string }[] { value: a.status.reden, }); } + if (a.status.tag === 'MeerInfoGevraagd') { + rows.push({ + key: $localize`:@@aanvraag.detail.meerInfoReden:Gevraagde informatie`, + value: a.status.reden, + }); + } return rows; } diff --git a/src/app/registratie/domain/aanvraag.ts b/src/app/registratie/domain/aanvraag.ts index 8d38baa..7e906ae 100644 --- a/src/app/registratie/domain/aanvraag.ts +++ b/src/app/registratie/domain/aanvraag.ts @@ -10,9 +10,14 @@ */ export type AanvraagType = 'registratie' | 'herregistratie' | 'intake'; +// Ingediend/MeerInfoGevraagd (ADR-0002/WP-63) are widened into the union so the parse +// boundary + renderers are ready, but no backend path emits them yet — that's WP-65's +// behandelaar-facing transition endpoint. export type AanvraagStatus = | { tag: 'Concept'; stepIndex: number; stepCount: number } + | { tag: 'Ingediend'; referentie: string } | { tag: 'InBehandeling'; referentie: string; manual: boolean } // manual=true → "wordt handmatig beoordeeld" + | { tag: 'MeerInfoGevraagd'; referentie: string; reden: string } | { tag: 'Goedgekeurd'; referentie: string } | { tag: 'Afgewezen'; referentie: string; reden: string }; diff --git a/src/app/registratie/domain/block-actions.spec.ts b/src/app/registratie/domain/block-actions.spec.ts index f312ad8..7996ba3 100644 --- a/src/app/registratie/domain/block-actions.spec.ts +++ b/src/app/registratie/domain/block-actions.spec.ts @@ -15,6 +15,13 @@ describe('blockActions', () => { ]); }); + it('ingediend and meer-info-gevraagd behave like in-behandeling', () => { + expect(blockActions({ tag: 'Ingediend', referentie: 'BIG-1' })).toEqual(['viewDocuments']); + expect(blockActions({ tag: 'MeerInfoGevraagd', referentie: 'BIG-1', reden: 'x' })).toEqual([ + 'viewDocuments', + ]); + }); + it('resolved aanvragen have no actions', () => { expect(blockActions({ tag: 'Goedgekeurd', referentie: 'BIG-1' })).toEqual([]); expect(blockActions({ tag: 'Afgewezen', referentie: 'BIG-1', reden: 'x' })).toEqual([]); diff --git a/src/app/registratie/domain/block-actions.ts b/src/app/registratie/domain/block-actions.ts index 457a2b6..3d40891 100644 --- a/src/app/registratie/domain/block-actions.ts +++ b/src/app/registratie/domain/block-actions.ts @@ -9,7 +9,9 @@ export function blockActions(status: AanvraagStatus): BlockAction[] { switch (status.tag) { case 'Concept': return ['resume', 'cancel']; + case 'Ingediend': case 'InBehandeling': + case 'MeerInfoGevraagd': return ['viewDocuments']; case 'Goedgekeurd': case 'Afgewezen': diff --git a/src/app/registratie/infrastructure/applications.adapter.spec.ts b/src/app/registratie/infrastructure/applications.adapter.spec.ts index 4fe25fb..29b7683 100644 --- a/src/app/registratie/infrastructure/applications.adapter.spec.ts +++ b/src/app/registratie/infrastructure/applications.adapter.spec.ts @@ -21,9 +21,13 @@ describe('parseAanvraagStatus', () => { ok: true, value: { tag: 'Concept', stepIndex: 2, stepCount: 4 }, }); + expect(parseAanvraagStatus({ tag: 'Ingediend', referentie: 'BIG-1' }).ok).toBe(true); expect( parseAanvraagStatus({ tag: 'InBehandeling', referentie: 'BIG-1', manual: true }).ok, ).toBe(true); + expect( + parseAanvraagStatus({ tag: 'MeerInfoGevraagd', referentie: 'BIG-1', reden: 'diploma?' }).ok, + ).toBe(true); expect(parseAanvraagStatus({ tag: 'Goedgekeurd', referentie: 'BIG-1' }).ok).toBe(true); expect( parseAanvraagStatus({ tag: 'Afgewezen', referentie: 'BIG-1', reden: 'geen uren' }).ok, diff --git a/src/app/registratie/infrastructure/applications.adapter.ts b/src/app/registratie/infrastructure/applications.adapter.ts index 0ae4ebe..2e8d9d8 100644 --- a/src/app/registratie/infrastructure/applications.adapter.ts +++ b/src/app/registratie/infrastructure/applications.adapter.ts @@ -78,10 +78,17 @@ export function parseAanvraagStatus( if (typeof s.stepIndex !== 'number' || typeof s.stepCount !== 'number') return err('aanvraag: bad Concept status'); return ok({ tag: 'Concept', stepIndex: s.stepIndex, stepCount: s.stepCount }); + case 'Ingediend': + if (typeof s.referentie !== 'string') return err('aanvraag: bad Ingediend status'); + return ok({ tag: 'Ingediend', referentie: s.referentie }); case 'InBehandeling': if (typeof s.referentie !== 'string' || typeof s.manual !== 'boolean') return err('aanvraag: bad InBehandeling status'); return ok({ tag: 'InBehandeling', referentie: s.referentie, manual: s.manual }); + case 'MeerInfoGevraagd': + if (typeof s.referentie !== 'string' || typeof s.reden !== 'string') + return err('aanvraag: bad MeerInfoGevraagd status'); + return ok({ tag: 'MeerInfoGevraagd', referentie: s.referentie, reden: s.reden }); case 'Goedgekeurd': if (typeof s.referentie !== 'string') return err('aanvraag: bad Goedgekeurd status'); return ok({ tag: 'Goedgekeurd', referentie: s.referentie }); diff --git a/src/app/registratie/ui/dashboard.page.ts b/src/app/registratie/ui/dashboard.page.ts index 1b675ac..5b2cc82 100644 --- a/src/app/registratie/ui/dashboard.page.ts +++ b/src/app/registratie/ui/dashboard.page.ts @@ -236,7 +236,9 @@ export class DashboardPage { if (rd.tag !== 'Success') return []; const order: Record = { Concept: 0, + Ingediend: 1, InBehandeling: 1, + MeerInfoGevraagd: 1, Goedgekeurd: 2, Afgewezen: 2, }; diff --git a/src/locale/messages.en.xlf b/src/locale/messages.en.xlf index 0344a8f..9860f45 100644 --- a/src/locale/messages.en.xlf +++ b/src/locale/messages.en.xlf @@ -1006,12 +1006,28 @@ 30 + + Ingediend + Submitted + + src/app/registratie/domain/aanvraag-view.ts + 32 + + In behandeling In progress src/app/registratie/domain/aanvraag-view.ts - 32 + 34 + + + + Meer informatie gevraagd + More information requested + + src/app/registratie/domain/aanvraag-view.ts + 36 @@ -1094,6 +1110,14 @@ 87 + + Gevraagde informatie + Information requested + + src/app/registratie/domain/aanvraag-view.ts + 99 + + Reden van afwijzing Reason for rejection