diff --git a/libs/api-client/src/lib/generated/bff-api.ts b/libs/api-client/src/lib/generated/bff-api.ts
index e806978..a51924e 100644
--- a/libs/api-client/src/lib/generated/bff-api.ts
+++ b/libs/api-client/src/lib/generated/bff-api.ts
@@ -59,6 +59,10 @@ export interface ProvideDocumentsRequest {
contentType?: string | null;
}
+export interface Refusal {
+ reason: string;
+}
+
export interface SubmitAccepted {
registrationId: string;
status: string;
diff --git a/services/bff/Bff.Api/DownstreamClients.cs b/services/bff/Bff.Api/DownstreamClients.cs
index 8ca730d..e89ac15 100644
--- a/services/bff/Bff.Api/DownstreamClients.cs
+++ b/services/bff/Bff.Api/DownstreamClients.cs
@@ -51,6 +51,9 @@ public interface IDomainClient
/// How the domain answered a provide-documents request (S-29).
public enum ProvideDocumentsResult { Provided, NotFound, NotAPdf, Infected, ScannerUnavailable }
+/// The body of a 422 provide-documents answer: why the file was refused.
+public sealed record Refusal(string Reason);
+
/// Port to the read projection.
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(ct);
+ return refusal?.Reason == "infected" ? ProvideDocumentsResult.Infected : ProvideDocumentsResult.NotAPdf;
+ }
response.EnsureSuccessStatusCode();
return ProvideDocumentsResult.Provided;
}
diff --git a/services/bff/Bff.Api/Program.cs b/services/bff/Bff.Api/Program.cs
index 5a9579b..26240d6 100644
--- a/services/bff/Bff.Api/Program.cs
+++ b/services/bff/Bff.Api/Program.cs
@@ -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(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) =>
diff --git a/services/bff/openapi.json b/services/bff/openapi.json
index a9a985c..cf7984d 100644
--- a/services/bff/openapi.json
+++ b/services/bff/openapi.json
@@ -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",