From 0ea43af7b63efdc95d4e85f8ec71f5c4c6ce16a7 Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Wed, 22 Jul 2026 20:11:26 +0200 Subject: [PATCH] =?UTF-8?q?feat(registratie):=20WP-34=20=E2=80=94=20phone?= =?UTF-8?q?=20field=20+=20BRP=20address=20read-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reshape the adreswijziging form into a contact-change form: the BRP address is authoritative and shown read-only (you change it at the gemeente), and the phone number becomes the editable/submittable field. New Telefoonnummer value object (parse-don't-validate); backend RejectPhoneChange re-validates as authority. POST /change-requests now carries { telefoon } (typed client regenerated). Co-Authored-By: Claude Opus 4.8 --- backend/src/BigRegister.Api/Contracts/Dtos.cs | 2 +- .../Domain/Submissions/SubmissionRules.cs | 16 +-- backend/src/BigRegister.Api/Program.cs | 2 +- backend/swagger.json | 10 +- .../tests/BigRegister.Tests/EndpointTests.cs | 8 +- .../BigRegister.Tests/IdempotencyTests.cs | 6 +- backend/tests/BigRegister.Tests/RuleTests.cs | 11 +- docs/project/backlog/README.md | 2 +- .../backlog/WP-34-adres-phone-brp-readonly.md | 45 ++++++++ .../application/submit-change-request.spec.ts | 16 +-- .../domain/change-request.machine.spec.ts | 41 +++---- .../domain/change-request.machine.ts | 40 +++---- .../value-objects/telefoonnummer.spec.ts | 20 ++++ .../domain/value-objects/telefoonnummer.ts | 25 +++++ .../infrastructure/change-request.adapter.ts | 15 +-- .../change-request-form.component.ts | 104 +++++++++++++----- .../change-request-form.stories.ts | 21 ++-- .../ui/registration-detail.page.ts | 2 +- src/app/shared/infrastructure/api-client.ts | 4 +- src/locale/messages.en.xlf | 50 +++++++-- src/locale/messages.xlf | 60 +++++++--- 21 files changed, 329 insertions(+), 171 deletions(-) create mode 100644 docs/project/backlog/WP-34-adres-phone-brp-readonly.md create mode 100644 src/app/registratie/domain/value-objects/telefoonnummer.spec.ts create mode 100644 src/app/registratie/domain/value-objects/telefoonnummer.ts diff --git a/backend/src/BigRegister.Api/Contracts/Dtos.cs b/backend/src/BigRegister.Api/Contracts/Dtos.cs index 789aa4e..8504d47 100644 --- a/backend/src/BigRegister.Api/Contracts/Dtos.cs +++ b/backend/src/BigRegister.Api/Contracts/Dtos.cs @@ -77,7 +77,7 @@ public sealed record DocumentRefDto(string CategoryId, string Channel, string? D public sealed record RegistratieRequest(string DiplomaHerkomst, IReadOnlyList? Documents = null); public sealed record IntakeRequest(int Uren); public sealed record HerregistratieRequest(int Uren, IReadOnlyList? Documents = null); -public sealed record ChangeRequestRequest(string Straat, string Postcode, string Woonplaats); +public sealed record ChangeRequestRequest(string Telefoon); public sealed record ReferentieResponse(string Referentie); diff --git a/backend/src/BigRegister.Api/Domain/Submissions/SubmissionRules.cs b/backend/src/BigRegister.Api/Domain/Submissions/SubmissionRules.cs index d7f0fd0..88089c4 100644 --- a/backend/src/BigRegister.Api/Domain/Submissions/SubmissionRules.cs +++ b/backend/src/BigRegister.Api/Domain/Submissions/SubmissionRules.cs @@ -19,15 +19,17 @@ public static class SubmissionRules public static string? RejectZeroUren(int uren) => uren == 0 ? "Aanvraag afgewezen: geen gewerkte uren geregistreerd." : null; - private static readonly Regex PostcodePattern = - new(@"^[1-9]\d{3}\s?[A-Z]{2}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex PhonePattern = + new(@"^0\d{9}$", RegexOptions.Compiled); - // RULE: a change request needs a street and a well-formed Dutch postcode. The - // server re-validates format authoritatively (the FE check is UX-only). - public static string? RejectChangeRequest(string straat, string postcode) + // RULE: a contact change needs a well-formed Dutch phone number (10 digits, leading + // 0, formatting stripped). The BRP address is authoritative and cannot be changed + // here (WP-34), so only the phone is submitted. The server re-validates format + // authoritatively (the FE check is UX-only). + public static string? RejectPhoneChange(string telefoon) { - if (string.IsNullOrWhiteSpace(straat)) return "Vul straat en huisnummer in."; - if (!PostcodePattern.IsMatch(postcode?.Trim() ?? "")) return "Voer een geldige postcode in, bijv. 1234 AB."; + var digits = (telefoon ?? "").Trim().Replace(" ", "").Replace("-", ""); + if (!PhonePattern.IsMatch(digits)) return "Voer een geldig telefoonnummer in, bijv. 0612345678."; return null; } diff --git a/backend/src/BigRegister.Api/Program.cs b/backend/src/BigRegister.Api/Program.cs index bb18f5e..807095a 100644 --- a/backend/src/BigRegister.Api/Program.cs +++ b/backend/src/BigRegister.Api/Program.cs @@ -146,7 +146,7 @@ api.MapPost("/intakes", (IntakeRequest req, HttpContext ctx) => .ProducesProblem(StatusCodes.Status422UnprocessableEntity); api.MapPost("/change-requests", (ChangeRequestRequest req, HttpContext ctx) => - Submit(ctx, "adreswijziging", SubmissionRules.RejectChangeRequest(req.Straat, req.Postcode))) + Submit(ctx, "telefoonwijziging", SubmissionRules.RejectPhoneChange(req.Telefoon))) .Produces() .ProducesProblem(StatusCodes.Status422UnprocessableEntity); diff --git a/backend/swagger.json b/backend/swagger.json index 7cdea16..6e4b488 100644 --- a/backend/swagger.json +++ b/backend/swagger.json @@ -1528,15 +1528,7 @@ "ChangeRequestRequest": { "type": "object", "properties": { - "straat": { - "type": "string", - "nullable": true - }, - "postcode": { - "type": "string", - "nullable": true - }, - "woonplaats": { + "telefoon": { "type": "string", "nullable": true } diff --git a/backend/tests/BigRegister.Tests/EndpointTests.cs b/backend/tests/BigRegister.Tests/EndpointTests.cs index 4e6b256..fa8653c 100644 --- a/backend/tests/BigRegister.Tests/EndpointTests.cs +++ b/backend/tests/BigRegister.Tests/EndpointTests.cs @@ -101,20 +101,20 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture(); Assert.StartsWith("BIG-2026-", body!.Referentie); } [Fact] - public async Task Change_request_with_bad_postcode_is_rejected() + public async Task Change_request_with_bad_phone_is_rejected() { var res = await _client.PostAsJsonAsync("/api/v1/change-requests", - new { straat = "Straat 1", postcode = "nope", woonplaats = "Den Haag" }); + new { telefoon = "nope" }); Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode); } diff --git a/backend/tests/BigRegister.Tests/IdempotencyTests.cs b/backend/tests/BigRegister.Tests/IdempotencyTests.cs index aafffe2..91ca226 100644 --- a/backend/tests/BigRegister.Tests/IdempotencyTests.cs +++ b/backend/tests/BigRegister.Tests/IdempotencyTests.cs @@ -12,7 +12,7 @@ public class IdempotencyTests(TestWebApplicationFactory factory) : IClassFixture { var req = new HttpRequestMessage(HttpMethod.Post, "/api/v1/change-requests") { - Content = JsonContent.Create(new { straat = "Lange Voorhout 9", postcode = "2514 EA", woonplaats = "Den Haag" }), + Content = JsonContent.Create(new { telefoon = "0612345678" }), }; req.Headers.Add("Idempotency-Key", key); return req; @@ -51,7 +51,7 @@ public class IdempotencyTests(TestWebApplicationFactory factory) : IClassFixture var key = Guid.NewGuid().ToString(); var badRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/change-requests") { - Content = JsonContent.Create(new { straat = "Straat 1", postcode = "nope", woonplaats = "Den Haag" }), + Content = JsonContent.Create(new { telefoon = "nope" }), }; badRequest.Headers.Add("Idempotency-Key", key); @@ -60,7 +60,7 @@ public class IdempotencyTests(TestWebApplicationFactory factory) : IClassFixture var replayRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/change-requests") { - Content = JsonContent.Create(new { straat = "Straat 1", postcode = "nope", woonplaats = "Den Haag" }), + Content = JsonContent.Create(new { telefoon = "nope" }), }; replayRequest.Headers.Add("Idempotency-Key", key); var replay = await _client.SendAsync(replayRequest); diff --git a/backend/tests/BigRegister.Tests/RuleTests.cs b/backend/tests/BigRegister.Tests/RuleTests.cs index 41ddddb..baba3f1 100644 --- a/backend/tests/BigRegister.Tests/RuleTests.cs +++ b/backend/tests/BigRegister.Tests/RuleTests.cs @@ -172,9 +172,10 @@ public class SubmissionRuleTests Assert.Null(SubmissionRules.RejectZeroUren(40)); [Theory] - [InlineData("Lange Voorhout 9", "2514 EA", null)] // valid - [InlineData("", "2514 EA", "Vul straat en huisnummer in.")] - [InlineData("Straat 1", "nope", "Voer een geldige postcode in, bijv. 1234 AB.")] - public void Change_request_is_validated(string straat, string postcode, string? expected) => - Assert.Equal(expected, SubmissionRules.RejectChangeRequest(straat, postcode)); + [InlineData("0612345678", null)] // valid mobile + [InlineData("070 123 45 67", null)] // valid landline, formatting stripped + [InlineData("nope", "Voer een geldig telefoonnummer in, bijv. 0612345678.")] + [InlineData("12345", "Voer een geldig telefoonnummer in, bijv. 0612345678.")] + public void Phone_change_is_validated(string telefoon, string? expected) => + Assert.Equal(expected, SubmissionRules.RejectPhoneChange(telefoon)); } diff --git a/docs/project/backlog/README.md b/docs/project/backlog/README.md index 9dc31c1..fe47042 100644 --- a/docs/project/backlog/README.md +++ b/docs/project/backlog/README.md @@ -78,7 +78,7 @@ for its existing violations, so every WP ends green. | [WP-31](WP-31-shared-store-helpers.md) | Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) | 7 · refinements | done | | [WP-32](WP-32-stamdata-undo.md) | Undo/redo in the stamdata editor | 7 · refinements | done | | [WP-33](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done | -| [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | todo | +| [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | done | | [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | todo | | [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | todo | diff --git a/docs/project/backlog/WP-34-adres-phone-brp-readonly.md b/docs/project/backlog/WP-34-adres-phone-brp-readonly.md new file mode 100644 index 0000000..f426113 --- /dev/null +++ b/docs/project/backlog/WP-34-adres-phone-brp-readonly.md @@ -0,0 +1,45 @@ +# WP-34 — Adres: phone field + BRP address read-only + +Status: done +Phase: 7 — refinements + +## Why + +The "Mijn gegevens" screen let the user *edit* their address (straat/postcode/woonplaats) and +submit it as an adreswijziging. But the BRP (Basisregistratie Personen) is the authority for a +person's address — you change it at the municipality, not in a register self-service portal. +This WP corrects that: the address is shown **read-only** (rendered from the BRP data the +screen already loads), and the editable/submittable contact detail becomes the **phone number** +— the thing CIBG actually holds and the professional can update here. + +## Decisions (made while building — no spec existed; flagged for review) + +- **The adreswijziging form becomes a telefoonwijziging form.** Address is read-only display; + phone is the one editable field. Same single-step machine idiom (Model/Msg/pure reduce + + value object + submit command) — only the fields changed. +- **New `Telefoonnummer` value object** (parse-don't-validate, lax Dutch format: 10 digits, + leading 0, `+31`→`0`, formatting stripped). Backend `RejectPhoneChange` re-validates as the + authority (ADR-0001); the FE check is UX-only. Replaces the old address/`RejectChangeRequest`. +- **Phone starts empty.** There is no current-phone anywhere in BRP/seed/Person, so no + current-value round-trip was added (YAGNI) — the form submits a new/updated number. The + read-only BRP address gives the screen its context. +- **Endpoint reused, contract narrowed.** `POST /change-requests` now takes `{ telefoon }` + (category `telefoonwijziging`); the typed client was regenerated (drift check clean). + +## Files + +- `registratie/domain/value-objects/telefoonnummer.ts` (+spec) — new value object. +- `registratie/domain/change-request.machine.ts` (+spec) — Draft/Valid now `{ telefoon }`. +- `registratie/infrastructure/change-request.adapter.ts` — sends `{ telefoon }`. +- `registratie/ui/change-request-form/change-request-form.component.ts` (+story) — read-only + BRP address block + editable phone field; takes `brpAdres` input. +- `registratie/ui/registration-detail.page.ts` — passes `profile()?.person?.adres`. +- Backend: `Dtos.cs`, `Program.cs`, `SubmissionRules.cs` (+ RuleTests/EndpointTests/IdempotencyTests). +- `src/locale/*` — new/changed `$localize` ids + English targets. + +## Acceptance criteria + +- [x] BRP address rendered read-only with a "change it at your municipality" note. +- [x] Phone field with format validation (client instant + server authoritative). +- [x] `npm run ci` green (lint, format, tokens, 332 FE tests, localized build, backend 122 + tests, api-client drift clean after commit). diff --git a/src/app/registratie/application/submit-change-request.spec.ts b/src/app/registratie/application/submit-change-request.spec.ts index 31e4eb4..872030b 100644 --- a/src/app/registratie/application/submit-change-request.spec.ts +++ b/src/app/registratie/application/submit-change-request.spec.ts @@ -3,16 +3,12 @@ import { describe, it, expect } from 'vitest'; import { Valid } from '@registratie/domain/change-request.machine'; import { ChangeRequestAdapter } from '@registratie/infrastructure/change-request.adapter'; import { createSubmitChangeRequest } from './submit-change-request'; -import { parsePostcode } from '@registratie/domain/value-objects/postcode'; +import { parseTelefoonnummer } from '@registratie/domain/value-objects/telefoonnummer'; -const postcode = parsePostcode('2514 EA'); -if (!postcode.ok) throw new Error('fixture postcode should parse'); +const telefoon = parseTelefoonnummer('0612345678'); +if (!telefoon.ok) throw new Error('fixture phone should parse'); -const data: Valid = { - straat: 'Lange Voorhout 9', - postcode: postcode.value, - woonplaats: 'Den Haag', -}; +const data: Valid = { telefoon: telefoon.value }; function setup(adapter: Partial) { TestBed.configureTestingModule({ @@ -38,9 +34,9 @@ describe('createSubmitChangeRequest', () => { it('surfaces a ProblemDetails detail message when the server rejects with one', async () => { const submit = setup({ - changeRequest: () => Promise.reject({ detail: 'Postcode komt niet overeen met de straat.' }), + changeRequest: () => Promise.reject({ detail: 'Telefoonnummer is ongeldig.' }), }); const r = await submit(data); - expect(r).toEqual({ ok: false, error: 'Postcode komt niet overeen met de straat.' }); + expect(r).toEqual({ ok: false, error: 'Telefoonnummer is ongeldig.' }); }); }); diff --git a/src/app/registratie/domain/change-request.machine.spec.ts b/src/app/registratie/domain/change-request.machine.spec.ts index 1333067..6aba4c1 100644 --- a/src/app/registratie/domain/change-request.machine.spec.ts +++ b/src/app/registratie/domain/change-request.machine.spec.ts @@ -1,67 +1,56 @@ import { describe, it, expect } from 'vitest'; import { ChangeRequestState, reduce, initial } from './change-request.machine'; -const editingWith = ( - draft: Partial<{ straat: string; postcode: string; woonplaats: string }>, -): ChangeRequestState => ({ +const editingWith = (telefoon: string): ChangeRequestState => ({ tag: 'Editing', - draft: { straat: '', postcode: '', woonplaats: '', ...draft }, + draft: { telefoon }, errors: {}, }); describe('change-request reduce', () => { it('SetField updates the draft while editing', () => { - const s = reduce(initial, { tag: 'SetField', key: 'straat', value: 'Lange Voorhout 9' }); + const s = reduce(initial, { tag: 'SetField', key: 'telefoon', value: '0612345678' }); expect(s.tag).toBe('Editing'); - expect((s as Extract).draft.straat).toBe( - 'Lange Voorhout 9', + expect((s as Extract).draft.telefoon).toBe( + '0612345678', ); }); it('Submit with an invalid draft stays Editing and reports field errors', () => { - const s = reduce(editingWith({ straat: '', postcode: 'nope' }), { tag: 'Submit' }); + const s = reduce(editingWith('nope'), { tag: 'Submit' }); expect(s.tag).toBe('Editing'); const errors = (s as Extract).errors; - expect(errors.straat).toBeTruthy(); - expect(errors.postcode).toBeTruthy(); + expect(errors.telefoon).toBeTruthy(); }); it('Submit with a valid draft moves to Submitting with parsed (normalised) data', () => { - const s = reduce(editingWith({ straat: 'Lange Voorhout 9', postcode: '2514ea' }), { - tag: 'Submit', - }); + const s = reduce(editingWith('06 12 34 56 78'), { tag: 'Submit' }); expect(s.tag).toBe('Submitting'); - expect((s as Extract).data.postcode).toBe('2514 EA'); + expect((s as Extract).data.telefoon).toBe( + '0612345678', + ); }); it('SubmitConfirmed maps Submitting to Submitted with the referentie', () => { - const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { - tag: 'Submit', - }); + const submitting = reduce(editingWith('0612345678'), { tag: 'Submit' }); const ok = reduce(submitting, { tag: 'SubmitConfirmed', referentie: 'BIG-2026-1' }); expect(ok).toMatchObject({ tag: 'Submitted', referentie: 'BIG-2026-1' }); }); it('SubmitFailed maps Submitting to Failed with the error', () => { - const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { - tag: 'Submit', - }); + const submitting = reduce(editingWith('0612345678'), { tag: 'Submit' }); const failed = reduce(submitting, { tag: 'SubmitFailed', error: 'boom' }); expect(failed).toMatchObject({ tag: 'Failed', error: 'boom' }); }); it('Retry re-submits a failure', () => { - const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { - tag: 'Submit', - }); + const submitting = reduce(editingWith('0612345678'), { tag: 'Submit' }); const failed = reduce(submitting, { tag: 'SubmitFailed', error: 'boom' }); expect(reduce(failed, { tag: 'Retry' }).tag).toBe('Submitting'); }); it('Reset returns to the initial editing state', () => { - const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { - tag: 'Submit', - }); + const submitting = reduce(editingWith('0612345678'), { tag: 'Submit' }); expect(reduce(submitting, { tag: 'Reset' })).toEqual(initial); }); }); diff --git a/src/app/registratie/domain/change-request.machine.ts b/src/app/registratie/domain/change-request.machine.ts index 8a954e8..ad732bb 100644 --- a/src/app/registratie/domain/change-request.machine.ts +++ b/src/app/registratie/domain/change-request.machine.ts @@ -1,24 +1,25 @@ import { Result, assertNever } from '@shared/kernel/fp'; -import { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode'; +import { + Telefoonnummer, + parseTelefoonnummer, +} from '@registratie/domain/value-objects/telefoonnummer'; -/** What the user is typing (raw, possibly invalid). */ +/** What the user is typing (raw, possibly invalid). The BRP address is NOT part of + the form — it is authoritative and shown read-only (WP-34); only the phone number + is editable here. */ export interface Draft { - straat: string; - postcode: string; - woonplaats: string; + telefoon: string; } -/** After parsing — postcode is the branded type, so downstream can't get a raw one. */ +/** After parsing — telefoon is the branded type, so downstream can't get a raw one. */ export interface Valid { - straat: string; - postcode: Postcode; - woonplaats: string; + telefoon: Telefoonnummer; } export type Errors = Partial>; /** - * The change-request (adreswijziging) form as one tagged union — the SAME idiom + * The contact-change (telefoonwijziging) form as one tagged union — the SAME idiom * as the wizards, just single-step. `draft`/`errors` exist only while Editing; * Submitting/Submitted/Failed carry the parsed `Valid`. Illegal states (submitting * an invalid draft, a success screen with errors) are unrepresentable. @@ -31,24 +32,15 @@ export type ChangeRequestState = export const initial: ChangeRequestState = { tag: 'Editing', - draft: { straat: '', postcode: '', woonplaats: '' }, + draft: { telefoon: '' }, errors: {}, }; -/** Parse via the value objects; on success hand back a Valid, else per-field errors. */ +/** Parse via the value object; on success hand back a Valid, else per-field errors. */ function validate(draft: Draft): Result { - const straat = draft.straat.trim(); - const postcode = parsePostcode(draft.postcode); - const errors: Errors = {}; - if (!straat) errors.straat = $localize`:@@validation.straat:Vul straat en huisnummer in.`; - if (!postcode.ok) errors.postcode = postcode.error; - if (straat && postcode.ok) { - return { - ok: true, - value: { straat, postcode: postcode.value, woonplaats: draft.woonplaats.trim() }, - }; - } - return { ok: false, error: errors }; + const telefoon = parseTelefoonnummer(draft.telefoon); + if (telefoon.ok) return { ok: true, value: { telefoon: telefoon.value } }; + return { ok: false, error: { telefoon: telefoon.error } }; } export type ChangeRequestMsg = diff --git a/src/app/registratie/domain/value-objects/telefoonnummer.spec.ts b/src/app/registratie/domain/value-objects/telefoonnummer.spec.ts new file mode 100644 index 0000000..2286675 --- /dev/null +++ b/src/app/registratie/domain/value-objects/telefoonnummer.spec.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest'; +import { parseTelefoonnummer } from './telefoonnummer'; + +describe('parseTelefoonnummer', () => { + it('accepts a 10-digit number starting 0 and strips formatting', () => { + const r = parseTelefoonnummer('06 12 34 56 78'); + expect(r.ok && r.value).toBe('0612345678'); + }); + + it('normalises a +31 prefix to a leading 0', () => { + const r = parseTelefoonnummer('+31 6 12345678'); + expect(r.ok && r.value).toBe('0612345678'); + }); + + it('rejects a too-short number, a non-0 start, and junk', () => { + expect(parseTelefoonnummer('12345').ok).toBe(false); + expect(parseTelefoonnummer('1612345678').ok).toBe(false); + expect(parseTelefoonnummer('nope').ok).toBe(false); + }); +}); diff --git a/src/app/registratie/domain/value-objects/telefoonnummer.ts b/src/app/registratie/domain/value-objects/telefoonnummer.ts new file mode 100644 index 0000000..147dabe --- /dev/null +++ b/src/app/registratie/domain/value-objects/telefoonnummer.ts @@ -0,0 +1,25 @@ +import { Brand, Result, ok, err } from '@shared/kernel/fp'; + +/** + * Value object: a Dutch phone number. "Parse, don't validate" — a Telefoonnummer is + * a distinct type from a raw string, mintable only via parseTelefoonnummer, so holding + * one is proof it is well-formed. Format-only check (the FE keeps format validation for + * instant feedback; the backend stays the authority — see ADR-0001). The parsed value + * is normalised to digits (spaces/dashes/parens dropped, a leading +31 → 0). + */ +export type Telefoonnummer = Brand; + +export function parseTelefoonnummer(raw: string): Result { + const digits = raw + .trim() + .replace(/[\s\-()]/g, '') + .replace(/^\+31/, '0'); + // Deliberately lax: a Dutch number is 10 digits starting 0 (mobile 06 or landline). + // Good enough for instant feedback; the server re-validates. + if (!/^0\d{9}$/.test(digits)) { + return err( + $localize`:@@validation.telefoon:Voer een geldig telefoonnummer in, bijv. 0612345678.`, + ); + } + return ok(digits as Telefoonnummer); +} diff --git a/src/app/registratie/infrastructure/change-request.adapter.ts b/src/app/registratie/infrastructure/change-request.adapter.ts index 30747dc..fe66b8b 100644 --- a/src/app/registratie/infrastructure/change-request.adapter.ts +++ b/src/app/registratie/infrastructure/change-request.adapter.ts @@ -3,21 +3,18 @@ import { ApiClient } from '@shared/infrastructure/api-client'; import { Valid } from '@registratie/domain/change-request.machine'; /** - * Infrastructure adapter for the adreswijziging POST (`/api/v1/change-requests`) — - * the single place the network client lives for change requests, so the command - * and the UI never touch `ApiClient`. Returns the server reference; the server - * re-validates and is the authority. + * Infrastructure adapter for the telefoonwijziging POST (`/api/v1/change-requests`) — + * the single place the network client lives for contact changes, so the command + * and the UI never touch `ApiClient`. The BRP address is authoritative and not + * submitted (WP-34); only the phone number is. Returns the server reference; the + * server re-validates and is the authority. */ @Injectable({ providedIn: 'root' }) export class ChangeRequestAdapter { private client = inject(ApiClient); async changeRequest(data: Valid): Promise { - const res = await this.client.changeRequests({ - straat: data.straat, - postcode: data.postcode, - woonplaats: data.woonplaats, - }); + const res = await this.client.changeRequests({ telefoon: data.telefoon }); return res.referentie ?? ''; } } diff --git a/src/app/registratie/ui/change-request-form/change-request-form.component.ts b/src/app/registratie/ui/change-request-form/change-request-form.component.ts index 4e71852..d9a3385 100644 --- a/src/app/registratie/ui/change-request-form/change-request-form.component.ts +++ b/src/app/registratie/ui/change-request-form/change-request-form.component.ts @@ -3,11 +3,9 @@ import { FormsModule } from '@angular/forms'; import { ButtonComponent } from '@shared/ui/button/button.component'; import { HeadingComponent } from '@shared/ui/heading/heading.component'; import { AlertComponent } from '@shared/ui/alert/alert.component'; -import { - AddressFieldsComponent, - AdresValue, - AdresErrors, -} from '@registratie/ui/address-fields/address-fields.component'; +import { FormFieldComponent } from '@shared/ui/form-field/form-field.component'; +import { TextInputComponent } from '@shared/ui/text-input/text-input.component'; +import { Adres } from '@registratie/domain/person'; import { createStore } from '@shared/application/store'; import { whenTag } from '@shared/kernel/fp'; import { @@ -19,19 +17,44 @@ import { import { createSubmitChangeRequest } from '@registratie/application/submit-change-request'; /** - * Organism: change-request (adreswijziging) form. Uses the SAME idiom as the - * wizards — all state in one signal driven by the pure `reduce` - * (change-request.machine.ts), submitted via a `submit-*` command returning - * `Result`. Renders the shared ``; the server re-validates. + * Organism: contact-change (telefoonwijziging) form. The BRP address is authoritative + * and shown READ-ONLY (WP-34) — you change your address at the gemeente, not here — so + * only the phone number is editable. Uses the SAME idiom as the wizards: all state in + * one signal driven by the pure `reduce` (change-request.machine.ts), submitted via a + * `submit-*` command returning `Result`. The server re-validates. */ @Component({ selector: 'app-change-request-form', - imports: [FormsModule, ButtonComponent, HeadingComponent, AlertComponent, AddressFieldsComponent], + imports: [ + FormsModule, + ButtonComponent, + HeadingComponent, + AlertComponent, + FormFieldComponent, + TextInputComponent, + ], + styles: [ + ` + .brp { + margin-block-end: var(--rhc-space-max-lg); + } + .brp dt { + font-weight: var(--rhc-text-font-weight-semi-bold); + } + .brp dd { + margin: 0 0 var(--rhc-space-max-sm) 0; + } + .brp .source { + color: var(--rhc-color-grijs-700); + font-size: var(--rhc-text-font-size-sm); + } + `, + ], template: ` @if (state().tag === 'Submitted') { - Uw adreswijziging is ontvangen (referentie {{ referentie() }}). U ontvangt binnen 5 - werkdagen bericht. + Uw wijziging is ontvangen (referentie {{ referentie() }}). U ontvangt binnen 5 werkdagen + bericht.
} @else { - Adreswijziging doorgeven + Contactgegevens wijzigen + + @if (brpAdres(); as a) { +
+
Adres (BRP)
+
{{ a.straat }}
{{ a.postcode }} {{ a.woonplaats }}
+
+ Uw adres komt uit de Basisregistratie Personen en kan hier niet worden gewijzigd. Wijzig + het bij uw gemeente. +
+
+ } +
* verplichte velden
- + + + @if (failedError()) { (initial, reduce); + /** BRP address, shown read-only. Undefined until the profile loads. */ + brpAdres = input(undefined); + /** Optional seed so Storybook / tests can mount any state directly. */ seed = input(initial); @@ -87,19 +137,17 @@ export class ChangeRequestFormComponent { protected readonly submitBezigLabel = $localize`:@@changeRequest.submitBezig:Bezig met indienen…`; private editing = computed(() => whenTag(this.state(), 'Editing')); - protected errors = computed(() => this.editing()?.errors ?? {}); + protected errors = computed(() => this.editing()?.errors ?? {}); protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? ''); protected referentie = computed(() => whenTag(this.state(), 'Submitted')?.referentie ?? ''); - /** The address shown in the fields — the live draft while editing, the parsed - data while submitting/failed (so the user sees what they sent). */ - protected adres = computed(() => { + /** The phone shown in the field — the live draft while editing, the parsed value + while submitting/failed (so the user sees what they sent). */ + protected telefoon = computed(() => { const s = this.state(); - if (s.tag === 'Editing') return s.draft; - if (s.tag === 'Submitting' || s.tag === 'Failed') { - return { straat: s.data.straat, postcode: s.data.postcode, woonplaats: s.data.woonplaats }; - } - return { straat: '', postcode: '', woonplaats: '' }; // Submitted shows the success alert, not the fields + if (s.tag === 'Editing') return s.draft.telefoon; + if (s.tag === 'Submitting' || s.tag === 'Failed') return s.data.telefoon; + return ''; }); constructor() { diff --git a/src/app/registratie/ui/change-request-form/change-request-form.stories.ts b/src/app/registratie/ui/change-request-form/change-request-form.stories.ts index e982ffb..9aee522 100644 --- a/src/app/registratie/ui/change-request-form/change-request-form.stories.ts +++ b/src/app/registratie/ui/change-request-form/change-request-form.stories.ts @@ -3,38 +3,31 @@ import { applicationConfig } from '@storybook/angular'; import { provideHttpClient } from '@angular/common/http'; import { ChangeRequestFormComponent } from './change-request-form.component'; import { provideApiClient } from '@shared/infrastructure/api-client.provider'; -import { Postcode } from '@registratie/domain/value-objects/postcode'; +import { Telefoonnummer } from '@registratie/domain/value-objects/telefoonnummer'; -const validData = { - straat: 'Lange Voorhout 9', - postcode: '2514 EA' as Postcode, - woonplaats: 'Den Haag', -}; +const brpAdres = { straat: 'Lange Voorhout 9', postcode: '2514 EA', woonplaats: 'Den Haag' }; +const validData = { telefoon: '0612345678' as Telefoonnummer }; const meta: Meta = { title: 'Domein/Registratie/Change Request Form', component: ChangeRequestFormComponent, // The form injects ApiClient (over HttpClient) for the submit command. decorators: [applicationConfig({ providers: [provideHttpClient(), provideApiClient()] })], + args: { brpAdres }, }; export default meta; type Story = StoryObj; // One render per state of the machine. export const Empty: Story = { - args: { - seed: { tag: 'Editing', draft: { straat: '', postcode: '', woonplaats: '' }, errors: {} }, - }, + args: { seed: { tag: 'Editing', draft: { telefoon: '' }, errors: {} } }, }; export const WithErrors: Story = { args: { seed: { tag: 'Editing', - draft: { straat: '', postcode: 'nope', woonplaats: '' }, - errors: { - straat: 'Vul straat en huisnummer in.', - postcode: 'Voer een geldige postcode in, bijv. 1234 AB.', - }, + draft: { telefoon: 'nope' }, + errors: { telefoon: 'Voer een geldig telefoonnummer in, bijv. 0612345678.' }, }, }, }; diff --git a/src/app/registratie/ui/registration-detail.page.ts b/src/app/registratie/ui/registration-detail.page.ts index 4ca85e0..25425f4 100644 --- a/src/app/registratie/ui/registration-detail.page.ts +++ b/src/app/registratie/ui/registration-detail.page.ts @@ -33,7 +33,7 @@ import { BigProfileStore } from '@registratie/application/big-profile.store';
- +
`, diff --git a/src/app/shared/infrastructure/api-client.ts b/src/app/shared/infrastructure/api-client.ts index 708b3e1..55e3a08 100644 --- a/src/app/shared/infrastructure/api-client.ts +++ b/src/app/shared/infrastructure/api-client.ts @@ -1721,9 +1721,7 @@ export interface CaseContextDto { } export interface ChangeRequestRequest { - straat?: string | undefined; - postcode?: string | undefined; - woonplaats?: string | undefined; + telefoon?: string | undefined; } export interface CreateApplicationRequest { diff --git a/src/locale/messages.en.xlf b/src/locale/messages.en.xlf index a9ffce5..72a9edf 100644 --- a/src/locale/messages.en.xlf +++ b/src/locale/messages.en.xlf @@ -1074,12 +1074,12 @@ 93 - - Vul straat en huisnummer in. - Enter a street and house number. + + Voer een geldig telefoonnummer in, bijv. 0612345678. + Enter a valid phone number, e.g. 0612345678. - src/app/registratie/domain/change-request.machine.ts - 43 + src/app/registratie/domain/value-objects/telefoonnummer.ts + 18 @@ -1295,8 +1295,8 @@ - Uw adreswijziging is ontvangen (referentie ). U ontvangt binnen 5 werkdagen bericht. - Your address change has been received (reference ). You will hear from us within 5 business days. + Uw wijziging is ontvangen (referentie ). U ontvangt binnen 5 werkdagen bericht. + Your change has been received (reference ). You will hear from us within 5 business days. src/app/registratie/ui/change-request-form/change-request-form.component.ts 28,30 @@ -1311,13 +1311,45 @@ - Adreswijziging doorgeven - Report address change + Contactgegevens wijzigen + Change contact details src/app/registratie/ui/change-request-form/change-request-form.component.ts 40,41 + + Adres (BRP) + Address (BRP) + + src/app/registratie/ui/change-request-form/change-request-form.component.ts + 42 + + + + Uw adres komt uit de Basisregistratie Personen en kan hier niet worden gewijzigd. Wijzig het bij uw gemeente. + Your address comes from the Personal Records Database (BRP) and cannot be changed here. Change it at your municipality. + + src/app/registratie/ui/change-request-form/change-request-form.component.ts + 43 + + + + Telefoonnummer + Phone number + + src/app/registratie/ui/change-request-form/change-request-form.component.ts + 50 + + + + 0612345678 + 0612345678 + + src/app/registratie/ui/change-request-form/change-request-form.component.ts + 50 + + Het indienen is niet gelukt: Submission failed: diff --git a/src/locale/messages.xlf b/src/locale/messages.xlf index 18944b7..52d5c3d 100644 --- a/src/locale/messages.xlf +++ b/src/locale/messages.xlf @@ -17,7 +17,7 @@ src/app/registratie/ui/change-request-form/change-request-form.component.ts - 49,51 + 84,86 src/app/shared/layout/wizard-shell/wizard-shell.component.ts @@ -1589,13 +1589,6 @@ 93 - - Vul straat en huisnummer in. - - src/app/registratie/domain/change-request.machine.ts - 43 - - Vul een straat en huisnummer in. @@ -1691,6 +1684,13 @@ 13 + + Voer een geldig telefoonnummer in, bijv. 0612345678. + + src/app/registratie/domain/value-objects/telefoonnummer.ts + 18 + + Vul een geheel aantal in (0 of meer). @@ -1783,45 +1783,73 @@ - Uw adreswijziging is ontvangen (referentie ). U ontvangt binnen 5 werkdagen bericht. + Uw wijziging is ontvangen (referentie ). U ontvangt binnen 5 werkdagen bericht. src/app/registratie/ui/change-request-form/change-request-form.component.ts - 33,35 + 56,58 Nieuwe wijziging doorgeven src/app/registratie/ui/change-request-form/change-request-form.component.ts - 41,43 + 64,66 - Adreswijziging doorgeven + Contactgegevens wijzigen src/app/registratie/ui/change-request-form/change-request-form.component.ts - 45,46 + 68,70 + + + + Adres (BRP) + + src/app/registratie/ui/change-request-form/change-request-form.component.ts + 72,73 + + + + Uw adres komt uit de Basisregistratie Personen en kan hier niet worden gewijzigd. Wijzig het bij uw gemeente. + + src/app/registratie/ui/change-request-form/change-request-form.component.ts + 75,78 + + + + Telefoonnummer + + src/app/registratie/ui/change-request-form/change-request-form.component.ts + 89,91 + + + + 0612345678 + + src/app/registratie/ui/change-request-form/change-request-form.component.ts + 101,102 Het indienen is niet gelukt: src/app/registratie/ui/change-request-form/change-request-form.component.ts - 61,62 + 108,109 Wijziging indienen src/app/registratie/ui/change-request-form/change-request-form.component.ts - 86 + 136 Bezig met indienen… src/app/registratie/ui/change-request-form/change-request-form.component.ts - 87 + 137