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:
@@ -59,6 +59,10 @@ export interface ProvideDocumentsRequest {
|
||||
contentType?: string | null;
|
||||
}
|
||||
|
||||
export interface Refusal {
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface SubmitAccepted {
|
||||
registrationId: string;
|
||||
status: string;
|
||||
|
||||
@@ -51,6 +51,9 @@ public interface IDomainClient
|
||||
/// <summary>How the domain answered a provide-documents request (S-29).</summary>
|
||||
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>
|
||||
public interface IProjectionClient
|
||||
{
|
||||
@@ -125,9 +128,18 @@ public sealed class DomainClient(HttpClient http) : IDomainClient
|
||||
using var response = await http.PostAsJsonAsync(
|
||||
$"registrations/{registrationId}/documents",
|
||||
new { bsn, contentBase64, fileName, contentType }, ct);
|
||||
// The domain 404s an unknown or not-owned registration; relay that rather than fail hard.
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
return ProvideDocumentsResult.NotFound;
|
||||
// The domain 404s an unknown or not-owned registration, 422s a refused file with its reason and
|
||||
// 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;
|
||||
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();
|
||||
return ProvideDocumentsResult.Provided;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// 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
|
||||
// registration that is unknown or not the caller's comes back 404. The real file upload + ZGW storage
|
||||
// is S-10b — this is the trigger that unblocks the process.
|
||||
// registration that is unknown or not the caller's comes back 404. A non-PDF or infected file is 422
|
||||
// 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) =>
|
||||
{
|
||||
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.");
|
||||
|
||||
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()
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.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).
|
||||
app.MapGet("/openbaar/register", async (string? q, IProjectionClient projection, CancellationToken ct) =>
|
||||
|
||||
@@ -124,6 +124,19 @@
|
||||
},
|
||||
"404": {
|
||||
"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": {
|
||||
"required": [
|
||||
"registrationId",
|
||||
|
||||
Reference in New Issue
Block a user