Files
atomic-design-poc/backend/src/BigRegister.Api/Domain/Submissions/SubmissionRules.cs
T
ehoandClaude Opus 5 0298ecc506 fix(uploads): delete the dead POST /registrations (RB-06)
POST /registrations passed its Documents list straight to Submit, which calls
DocumentStore.Link on every digital documentId in it — and linking a document
blocks its owner from ever deleting it (DeleteOwned returns 409 Linked). That
path had no ForeignIds ownership check, so any authenticated citizen could
post another citizen's document id and permanently block them from deleting
their own diploma scan. POST /applications/{id}/submit, the endpoint actually
in use, has had that guard since it was written.

Deleted rather than guarded: the endpoint is dead. No frontend caller, and
the whole registratie flow goes through /applications/{id}/submit.
RegistratieRequest went with it, and so did SubmissionRules.RejectRegistratie
— reachable only from here, and contradicted by the live path, which treats a
handmatig diploma as "does not auto-approve" rather than a 422 rejection. Its
own message said as much while being returned as a rejection. That last part
is a judgement call beyond the ticket's wording; reverting the two
SubmissionRules hunks restores it in isolation.

Coverage moved rather than vanished: the problem+json shape assertion is now
on /change-requests (the other endpoint on the same Submit helper), and the
linked-delete 409 test goes through the real submit path.

swagger.json, the generated client and the behaviour spec regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 11:04:03 +02:00

41 lines
1.9 KiB
C#

using System.Text.RegularExpressions;
namespace BigRegister.Domain.Submissions;
/// <summary>
/// SERVER-OWNED submit rules (ported from the frontend submit-*.ts commands, where
/// they were hardcoded). Each method returns a rejection reason, or null when the
/// submission is accepted. The reference is generated server-side on acceptance.
/// </summary>
public static class SubmissionRules
{
// RULE: an application reporting zero worked hours is rejected.
public static string? RejectZeroUren(int uren) =>
uren == 0 ? "Aanvraag afgewezen: geen gewerkte uren geregistreerd." : null;
private static readonly Regex PhonePattern =
new(@"^0\d{9}$", RegexOptions.Compiled);
private static readonly Regex StrippedChars =
new(@"[\s\-()]", RegexOptions.Compiled);
private static readonly Regex LeadingCountryCode =
new(@"^\+31", RegexOptions.Compiled);
// 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) — and must strip the SAME formatting the
// FE's parseTelefoonnummer does (whitespace/dashes/parens, a leading +31 → 0; WP-75),
// or the two sides disagree on what's a valid number.
public static string? RejectPhoneChange(string telefoon)
{
var stripped = StrippedChars.Replace((telefoon ?? "").Trim(), "");
var digits = LeadingCountryCode.Replace(stripped, "0");
if (!PhonePattern.IsMatch(digits)) return "Voer een geldig telefoonnummer in, bijv. 0612345678.";
return null;
}
public static string NewReference() =>
// ponytail: random reference is fine for a demo; a real system reserves it transactionally.
"BIG-2026-" + Random.Shared.Next(100_000, 1_000_000);
}