Files
atomic-design-poc/backend/src/BigRegister.Api/Contracts/Dtos.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

250 lines
12 KiB
C#

namespace BigRegister.Api.Contracts;
// Wire contracts (the FE⇄BE seam). Field names + shapes mirror the frontend's
// contracts/*.dto.ts 1:1; the NSwag-generated TS client is produced from these.
public sealed record AdresDto(string Straat, string Postcode, string Woonplaats);
public sealed record RegistrationStatusDto(
string Tag,
string? HerregistratieDatum = null,
string? GeschorstTot = null,
string? Reden = null,
string? DoorgehaaldOp = null);
public sealed record RegistrationDto(
string BigNummer,
string Naam,
string Beroep,
string Registratiedatum,
string Geboortedatum,
RegistrationStatusDto Status);
public sealed record PersonDto(string Naam, string Geboortedatum, AdresDto Adres);
public sealed record HerregistratieDecisionsDto(bool EligibleForHerregistratie, string? HerregistratieReason);
public sealed record DashboardViewDto(RegistrationDto Registration, PersonDto Person, HerregistratieDecisionsDto Decisions);
public sealed record AantekeningDto(string Type, string Omschrijving, string Datum);
public sealed record BrpAddressDto(bool Gevonden, AdresDto? Adres);
public sealed record PolicyQuestionDto(string Id, string Vraag, string Type);
public sealed record DuoDiplomaDto(
string Id,
string Naam,
string Instelling,
int Jaar,
string Beroep,
IReadOnlyList<PolicyQuestionDto> PolicyQuestions);
public sealed record ManualDiplomaPolicyDto(IReadOnlyList<string> Beroepen, IReadOnlyList<PolicyQuestionDto> PolicyQuestions);
public sealed record DuoLookupDto(IReadOnlyList<DuoDiplomaDto> Diplomas, ManualDiplomaPolicyDto Handmatig);
public sealed record IntakePolicyDto(int ScholingThreshold);
// --- Stamdata maintenance (ADR-0004): generic, schema-driven so ONE contract serves
// every business-editable table. Columns are reflected from the table's typed record;
// Rows are the raw JSON objects (opaque here — the editor renders them by column type).
public sealed record StamdataColumnDto(string Name, string Type, bool IsKey, IReadOnlyList<string>? Options);
public sealed record StamdataTableSummaryDto(string Id, string Label, IReadOnlyList<StamdataColumnDto> Columns, bool Temporal);
public sealed record StamdataTableDto(
string Id, string Label, IReadOnlyList<StamdataColumnDto> Columns, bool Temporal,
IReadOnlyList<System.Text.Json.JsonElement> Rows);
// --- Document upload contracts ---
public sealed record DocumentCategoryDto(
string CategoryId, string Label, string Description, bool Required,
IReadOnlyList<string> AcceptedTypes, int MaxSizeMb, bool Multiple, bool AllowPostDelivery);
public sealed record UploadCategoriesDto(IReadOnlyList<DocumentCategoryDto> Categories);
public sealed record UploadResponse(string DocumentId, string LocalId);
public sealed record UploadStatusItemDto(string LocalId, string Status, string? DocumentId);
public sealed record UploadStatusDto(IReadOnlyList<UploadStatusItemDto> Results);
// Per-category delivery intent on submit: digital categories carry their DocumentId,
// post-delivery categories carry Channel="post".
public sealed record DocumentRefDto(string CategoryId, string Channel, string? DocumentId = null);
// Submit requests carry only the fields the server re-validates (UX-only fields
// stay on the client). ponytail: a real submit would carry the full application.
public sealed record ChangeRequestRequest(string Telefoon);
// Authz/PII-reveal audit row (WP-41) — data-minimised, no PII (see AuthzAuditEntry).
public sealed record AuthzAuditDto(
string At, string Action, string Resource, string Decision, string Role, string CorrelationId);
// Feature flags (WP-47): the resolved flag set + the admin toggle body.
public sealed record FeatureFlagDto(string Key, string Description, bool Enabled);
public sealed record SetFeatureFlagRequest(bool Enabled);
public sealed record ReferentieResponse(string Referentie);
// --- Applications (aanvragen): the system of record for the dashboard. ---
// Status is a discriminated union by Tag (Concept | InBehandeling | Goedgekeurd |
// Afgewezen); only the fields relevant to a tag are populated. Mirrors the FE union.
public sealed record AanvraagStatusDto(
string Tag,
int? StepIndex = null,
int? StepCount = null,
string? Referentie = null,
bool? Manual = null,
string? Reden = null);
public sealed record ApplicationSummaryDto(
string Id, string Type, AanvraagStatusDto Status,
IReadOnlyList<string> DocumentIds,
string CreatedAt, string UpdatedAt, string? SubmittedAt,
string? Owner = null); // populated for the admin cross-owner list (WP-36); the user's own list ignores it
public sealed record ApplicationDetailDto(
string Id, string Type, AanvraagStatusDto Status,
System.Text.Json.JsonElement? Draft,
IReadOnlyList<string> DocumentIds,
string CreatedAt, string UpdatedAt, string? SubmittedAt);
public sealed record CreateApplicationRequest(string Type);
public sealed record DraftSyncRequest(
System.Text.Json.JsonElement Draft, int StepIndex, int StepCount,
IReadOnlyList<string>? DocumentIds = null);
// Submit carries only the fields the server re-validates per wizard type.
// AanvullendeScholing/ScholingPunten (WP-69) — intake-typed aanvragen only (gated by
// IntakePolicy.RejectIncompleteScholing's caller), null for the others.
public sealed record SubmitApplicationRequest(
string? DiplomaHerkomst = null, int? Uren = null,
IReadOnlyList<DocumentRefDto>? Documents = null,
bool? AanvullendeScholing = null, int? ScholingPunten = null);
public sealed record SubmitApplicationResponse(string Referentie, AanvraagStatusDto Status);
// --- Beoordeling (WP-65): the behandelportal's case-detail screen. ---
public sealed record BeoordelingDocumentDto(string DocumentId, string CategoryId, string FileName);
/// Decision flag (ADR-0001): the FE renders "may I decide", it never recomputes the
/// lifecycle. One flag today because all three decision actions share one rule
/// (BeoordelingRules.CanDecide); split into per-action flags if that ever diverges.
public sealed record BeoordelingDecisionsDto(bool CanBesluiten);
public sealed record BeoordelingViewDto(
ApplicationSummaryDto Aanvraag,
IReadOnlyList<BeoordelingDocumentDto> Documenten,
BeoordelingDecisionsDto Decisions);
/// Recording a decision (WP-65b). `Besluit` is the enum member name as a string — same
/// wire convention as `AanvraagStatusDto.Tag` (this backend never ships a raw C# enum,
/// it round-trips names via Enum.Parse/.ToString() at the Contracts boundary, no
/// JsonStringEnumConverter configured). The endpoint 400s an unknown name. Toelichting
/// is required for Afwijzen/MeerInfoOpvragen, validated server-side.
public sealed record RecordBesluitRequest(string Besluit, string? Toelichting = null);
public sealed record RecordBesluitResponse(AanvraagStatusDto Status);
// --- Brief (letter composition) contracts ---
// Rich text is a serialisable node tree; the node union is flattened with a `Type`
// discriminator + nullable fields, the same wire convention as AanvraagStatusDto.
// The backend treats the content as opaque (stores/returns it) — the FE owns the
// rich-text semantics and re-validates at its parse* boundary.
public sealed record RichTextNodeDto(string Type, string? Text = null, IReadOnlyList<string>? Marks = null, string? Key = null);
// List is null for a plain paragraph, "bullet" or "number" for a list item.
public sealed record ParagraphDto(IReadOnlyList<RichTextNodeDto> Nodes, string? List = null);
public sealed record RichTextBlockDto(IReadOnlyList<ParagraphDto> Paragraphs);
public sealed record PlaceholderDefDto(string Key, string Label, bool AutoResolvable, bool? Fillable = null, bool? Deprecated = null);
// LetterBlock union (passage | freeText), flattened: passage-only fields are nullable.
public sealed record LetterBlockDto(
string Type, string BlockId, RichTextBlockDto Content,
string? SourcePassageId = null, int? SourceVersion = null, bool? Edited = null);
// Locked sections (aanhef, slot) are predefined template text the drafter cannot edit.
public sealed record LetterSectionDto(string SectionKey, string Title, bool Required, IReadOnlyList<LetterBlockDto> Blocks, bool Locked = false);
// BriefStatus union, flattened by Tag (draft | submitted | approved | rejected | sent).
public sealed record BriefStatusDto(
string Tag,
string? SubmittedBy = null, string? SubmittedAt = null,
string? ApprovedBy = null, string? ApprovedAt = null,
string? RejectedBy = null, string? RejectedAt = null, string? Comments = null,
string? SentAt = null);
// Besluit/Reason tag the passage for guided drafting (WP-brief-v3): the behandelaar
// picks the besluit + reden and the FE filters this library to the matching passages.
// null besluit = relevant to any besluit; null reason = not reason-specific.
public sealed record LibraryPassageDto(
string PassageId, string Scope, string SectionKey, string Label,
RichTextBlockDto Content, int Version, string? Beroep = null, bool IsDefault = false,
string? Besluit = null, string? Reason = null);
public sealed record BriefDto(
string BriefId, string Beroep, string TemplateId,
IReadOnlyList<PlaceholderDefDto> Placeholders,
IReadOnlyList<LetterSectionDto> Sections,
BriefStatusDto Status, string DrafterId);
// Decision flags for the CURRENT acting principal + this brief's live status
// (PRD-0002 phase P1) — the FE renders these, it never recomputes them.
// CanRevealBigNummer (PRD-0002 §5c): whether the acting principal may unmask the
// BIG-nummer the case screen ships masked. Status-independent, unlike the action gates.
public sealed record BriefDecisionsDto(bool CanEdit, bool CanApprove, bool CanReject, bool CanSend, bool CanRevealBigNummer);
// The brief's screen DTO also carries the org template it renders with (WP-23):
// the sub-org's current PUBLISHED version — or, once sent, the version pinned at
// send time (sent letters are immutable; a republish never re-renders them).
// The case this letter is about — the zorgverlener + aanvraag the behandelaar is
// handling. Server-joined onto the brief's screen DTO so brief/ stays a shared-only
// leaf context (no cross-context import of registratie).
public sealed record CaseContextDto(string ZorgverlenerNaam, string BigNummer, string Beroep, string AanvraagReferentie);
public sealed record BriefViewDto(
BriefDto Brief, IReadOnlyList<LibraryPassageDto> AvailablePassages, BriefDecisionsDto Decisions,
OrgTemplateDto OrgTemplate, CaseContextDto CaseContext);
public sealed record SaveBriefRequest(IReadOnlyList<LetterSectionDto> Sections);
public sealed record RejectBriefRequest(string Comments);
// The unmasked BIG-nummer, returned only from the audited + step-up-gated reveal
// endpoint (PRD-0002 §5c). Never logged.
public sealed record RevealBigNummerResponse(string BigNummer);
// PRD-0002 §6: coarse, role-derived capabilities for nav/menu-level checks.
public sealed record MeDto(IReadOnlyList<string> Capabilities);
// --- Organization templates (WP-23, Brief v2 PRD §3) ---
// The second template axis: appearance/identity per sub-organization (letterhead,
// footer, signature, margins). Orthogonal to the case-type template (sections +
// placeholders); the two only meet at render time.
public sealed record MarginsDto(int TopMm, int RightMm, int BottomMm, int LeftMm);
// Version: 0 = a draft (work in progress), n>0 = the published snapshot it is.
public sealed record OrgTemplateDto(
string SubOrgId, string OrgName, string ReturnAddress, string? LogoDocumentId,
string FooterContact, string FooterLegal,
string SignatureName, string SignatureRole, string SignatureClosing,
MarginsDto Margins, int Version = 0);
public sealed record OrgTemplateVersionDto(int Version, string PublishedAt, OrgTemplateDto Template);
// Screen DTO for the admin editor: the editable draft, what's live, the append-only
// history, and the publish-impact count ("dit raakt N nog niet verzonden brieven").
public sealed record OrgTemplateAdminViewDto(
OrgTemplateDto Draft, int PublishedVersion,
IReadOnlyList<OrgTemplateVersionDto> History, int UnsentBriefs);
public sealed record SubOrgSummaryDto(string SubOrgId, string OrgName, int PublishedVersion);
public sealed record SaveOrgTemplateRequest(OrgTemplateDto Draft);
public sealed record PublishOrgTemplateResponse(int Version, int AffectedUnsentBriefs);