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;
|
contentType?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Refusal {
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SubmitAccepted {
|
export interface SubmitAccepted {
|
||||||
registrationId: string;
|
registrationId: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user