feat(bff): relay refused diplomas as 422 with reason, scanner down as 503 (refs #192)

openapi.json and the generated portal client regenerated from the served spec.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
not
2026-10-02 09:53:23 +02:00
co-authored by Claude Opus 5.5
parent 77de63bf8b
commit da7813a84e
4 changed files with 56 additions and 7 deletions
@@ -59,6 +59,10 @@ export interface ProvideDocumentsRequest {
contentType?: string | null; contentType?: string | null;
} }
export interface Refusal {
reason: string;
}
export interface SubmitAccepted { export interface SubmitAccepted {
registrationId: string; registrationId: string;
status: string; status: string;
+14 -2
View File
@@ -51,6 +51,9 @@ public interface IDomainClient
/// <summary>How the domain answered a provide-documents request (S-29).</summary> /// <summary>How the domain answered a provide-documents request (S-29).</summary>
public enum ProvideDocumentsResult { Provided, NotFound, NotAPdf, Infected, ScannerUnavailable } public enum ProvideDocumentsResult { Provided, NotFound, NotAPdf, Infected, ScannerUnavailable }
/// <summary>The body of a 422 provide-documents answer: why the file was refused.</summary>
public sealed record Refusal(string Reason);
/// <summary>Port to the read projection.</summary> /// <summary>Port to the read projection.</summary>
public interface IProjectionClient public interface IProjectionClient
{ {
@@ -125,9 +128,18 @@ public sealed class DomainClient(HttpClient http) : IDomainClient
using var response = await http.PostAsJsonAsync( using var response = await http.PostAsJsonAsync(
$"registrations/{registrationId}/documents", $"registrations/{registrationId}/documents",
new { bsn, contentBase64, fileName, contentType }, ct); new { bsn, contentBase64, fileName, contentType }, ct);
// The domain 404s an unknown or not-owned registration; relay that rather than fail hard. // The domain 404s an unknown or not-owned registration, 422s a refused file with its reason and
if (response.StatusCode == System.Net.HttpStatusCode.NotFound) // 503s when its scanner is down (S-29); relay those rather than fail hard.
switch (response.StatusCode)
{
case System.Net.HttpStatusCode.NotFound:
return ProvideDocumentsResult.NotFound; return ProvideDocumentsResult.NotFound;
case System.Net.HttpStatusCode.ServiceUnavailable:
return ProvideDocumentsResult.ScannerUnavailable;
case System.Net.HttpStatusCode.UnprocessableEntity:
var refusal = await response.Content.ReadFromJsonAsync<Refusal>(ct);
return refusal?.Reason == "infected" ? ProvideDocumentsResult.Infected : ProvideDocumentsResult.NotAPdf;
}
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
return ProvideDocumentsResult.Provided; return ProvideDocumentsResult.Provided;
} }
+13 -4
View File
@@ -166,8 +166,8 @@ app.MapPost("/self-service/registrations/{id}/withdraw", async (string id, Claim
// Self-service provide-documents (S-10a): the signed-in zorgprofessional supplies the documents their // Self-service provide-documents (S-10a): the signed-in zorgprofessional supplies the documents their
// registration is waiting for ("documenten aanleveren"). The bsn comes from the DigiD token and is // registration is waiting for ("documenten aanleveren"). The bsn comes from the DigiD token and is
// forwarded to the domain, which owner-scopes the action and completes the WachtOpDocumenten task; a // forwarded to the domain, which owner-scopes the action and completes the WachtOpDocumenten task; a
// registration that is unknown or not the caller's comes back 404. The real file upload + ZGW storage // registration that is unknown or not the caller's comes back 404. A non-PDF or infected file is 422
// is S-10b — this is the trigger that unblocks the process. // with the reason; an unreachable scanner is 503 (S-29, ADR-0036).
app.MapPost("/self-service/registrations/{id}/documents", async (string id, ProvideDocumentsRequest body, ClaimsPrincipal user, IDomainClient domain, CancellationToken ct) => app.MapPost("/self-service/registrations/{id}/documents", async (string id, ProvideDocumentsRequest body, ClaimsPrincipal user, IDomainClient domain, CancellationToken ct) =>
{ {
var bsn = user.FindFirstValue("bsn"); var bsn = user.FindFirstValue("bsn");
@@ -177,13 +177,22 @@ app.MapPost("/self-service/registrations/{id}/documents", async (string id, Prov
return Results.BadRequest("A document is required."); return Results.BadRequest("A document is required.");
var provided = await domain.ProvideDocumentsAsync(id, bsn, body.ContentBase64, body.FileName, body.ContentType, ct); var provided = await domain.ProvideDocumentsAsync(id, bsn, body.ContentBase64, body.FileName, body.ContentType, ct);
return provided == ProvideDocumentsResult.Provided ? Results.NoContent() : Results.NotFound(); return provided switch
{
ProvideDocumentsResult.Provided => Results.NoContent(),
ProvideDocumentsResult.NotAPdf => Results.UnprocessableEntity(new Refusal("not-a-pdf")),
ProvideDocumentsResult.Infected => Results.UnprocessableEntity(new Refusal("infected")),
ProvideDocumentsResult.ScannerUnavailable => Results.StatusCode(StatusCodes.Status503ServiceUnavailable),
_ => Results.NotFound(),
};
}) })
.RequireAuthorization() .RequireAuthorization()
.Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound)
.Produces<Refusal>(StatusCodes.Status422UnprocessableEntity)
.Produces(StatusCodes.Status503ServiceUnavailable);
// Openbaar register: an anonymous public lookup that exposes only public-safe fields (S-09). // Openbaar register: an anonymous public lookup that exposes only public-safe fields (S-09).
app.MapGet("/openbaar/register", async (string? q, IProjectionClient projection, CancellationToken ct) => app.MapGet("/openbaar/register", async (string? q, IProjectionClient projection, CancellationToken ct) =>
+24
View File
@@ -124,6 +124,19 @@
}, },
"404": { "404": {
"description": "Not Found" "description": "Not Found"
},
"422": {
"description": "Unprocessable Entity",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Refusal"
}
}
}
},
"503": {
"description": "Service Unavailable"
} }
} }
} }
@@ -415,6 +428,17 @@
} }
} }
}, },
"Refusal": {
"required": [
"reason"
],
"type": "object",
"properties": {
"reason": {
"type": "string"
}
}
},
"SubmitAccepted": { "SubmitAccepted": {
"required": [ "required": [
"registrationId", "registrationId",