feat(registratie): WP-34 — phone field + BRP address read-only

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 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-22 20:11:26 +02:00
co-authored by Claude Opus 4.8
parent 1ed4850858
commit 0ea43af7b6
21 changed files with 329 additions and 171 deletions
@@ -77,7 +77,7 @@ public sealed record DocumentRefDto(string CategoryId, string Channel, string? D
public sealed record RegistratieRequest(string DiplomaHerkomst, IReadOnlyList<DocumentRefDto>? Documents = null);
public sealed record IntakeRequest(int Uren);
public sealed record HerregistratieRequest(int Uren, IReadOnlyList<DocumentRefDto>? Documents = null);
public sealed record ChangeRequestRequest(string Straat, string Postcode, string Woonplaats);
public sealed record ChangeRequestRequest(string Telefoon);
public sealed record ReferentieResponse(string Referentie);
@@ -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;
}
+1 -1
View File
@@ -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<ReferentieResponse>()
.ProducesProblem(StatusCodes.Status422UnprocessableEntity);
+1 -9
View File
@@ -1528,15 +1528,7 @@
"ChangeRequestRequest": {
"type": "object",
"properties": {
"straat": {
"type": "string",
"nullable": true
},
"postcode": {
"type": "string",
"nullable": true
},
"woonplaats": {
"telefoon": {
"type": "string",
"nullable": true
}
@@ -101,20 +101,20 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
}
[Fact]
public async Task Change_request_with_valid_address_succeeds()
public async Task Change_request_with_valid_phone_succeeds()
{
var res = await _client.PostAsJsonAsync("/api/v1/change-requests",
new { straat = "Lange Voorhout 9", postcode = "2514 EA", woonplaats = "Den Haag" });
new { telefoon = "0612345678" });
res.EnsureSuccessStatusCode();
var body = await res.Content.ReadFromJsonAsync<ReferentieResponse>();
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);
}
@@ -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);
+6 -5
View File
@@ -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));
}