feat(behandelportal): WP-63 aanvraag status lifecycle enum
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 50s
CI / frontend (push) Successful in 1m32s
CI / backend (push) Successful in 1m50s
CI / e2e (push) Successful in 3m7s
CI / storybook-a11y (push) Successful in 6m53s
CI / semgrep (push) Successful in 1m12s
CI / api-client-drift (push) Successful in 1m43s
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 50s
CI / frontend (push) Successful in 1m32s
CI / backend (push) Successful in 1m50s
CI / e2e (push) Successful in 3m7s
CI / storybook-a11y (push) Successful in 6m53s
CI / semgrep (push) Successful in 1m12s
CI / api-client-drift (push) Successful in 1m43s
Model the full ADR-0002 lifecycle (Ingediend/InBehandeling/ MeerInfoGevraagd/Goedgekeurd/Afgewezen) as a backend enum backing the existing AanvraagStatusDto.Tag string, and widen the FE union/parse boundary/switches to match. Ingediend/MeerInfoGevraagd aren't reachable yet (no behandelaar transition exists) — that's WP-65. Zero DTO shape change, so gen:api has no drift. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -3,6 +3,16 @@ using BigRegister.Domain.Submissions;
|
||||
|
||||
namespace BigRegister.Api.Data;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="Aanvraag.Submitted"/>.
|
||||
/// <see cref="Ingediend"/> and <see cref="MeerInfoGevraagd"/> 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.
|
||||
/// </summary>
|
||||
public enum AanvraagStatusTag { Ingediend, InBehandeling, MeerInfoGevraagd, Goedgekeurd, Afgewezen }
|
||||
|
||||
/// <summary>
|
||||
/// 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,
|
||||
|
||||
@@ -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<AanvraagStatusTag>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -236,7 +236,9 @@ export class DashboardPage {
|
||||
if (rd.tag !== 'Success') return [];
|
||||
const order: Record<Aanvraag['status']['tag'], number> = {
|
||||
Concept: 0,
|
||||
Ingediend: 1,
|
||||
InBehandeling: 1,
|
||||
MeerInfoGevraagd: 1,
|
||||
Goedgekeurd: 2,
|
||||
Afgewezen: 2,
|
||||
};
|
||||
|
||||
@@ -1006,12 +1006,28 @@
|
||||
<context context-type="linenumber">30</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="aanvraag.status.ingediend" datatype="html">
|
||||
<source>Ingediend</source>
|
||||
<target datatype="html">Submitted</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/registratie/domain/aanvraag-view.ts</context>
|
||||
<context context-type="linenumber">32</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="aanvraag.status.inBehandeling" datatype="html">
|
||||
<source>In behandeling</source>
|
||||
<target datatype="html">In progress</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/registratie/domain/aanvraag-view.ts</context>
|
||||
<context context-type="linenumber">32</context>
|
||||
<context context-type="linenumber">34</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="aanvraag.status.meerInfoGevraagd" datatype="html">
|
||||
<source>Meer informatie gevraagd</source>
|
||||
<target datatype="html">More information requested</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/registratie/domain/aanvraag-view.ts</context>
|
||||
<context context-type="linenumber">36</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="aanvraag.status.goedgekeurd" datatype="html">
|
||||
@@ -1094,6 +1110,14 @@
|
||||
<context context-type="linenumber">87</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="aanvraag.detail.meerInfoReden" datatype="html">
|
||||
<source>Gevraagde informatie</source>
|
||||
<target datatype="html">Information requested</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/registratie/domain/aanvraag-view.ts</context>
|
||||
<context context-type="linenumber">99</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="aanvraag.detail.reden" datatype="html">
|
||||
<source>Reden van afwijzing</source>
|
||||
<target datatype="html">Reason for rejection</target>
|
||||
|
||||
Reference in New Issue
Block a user