Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
76 lines
3.9 KiB
C#
76 lines
3.9 KiB
C#
using Big.Domain;
|
|
|
|
namespace Big.Application;
|
|
|
|
/// <summary>A zorgprofessional's upload of the diploma their registration is waiting for ("documenten
|
|
/// aanleveren"). <paramref name="Bsn"/> is the authenticated caller (from the DigiD token, forwarded by
|
|
/// the BFF): only the registration's own bsn may provide its documents. <paramref name="Content"/> is
|
|
/// the raw file, with its <paramref name="FileName"/> and <paramref name="ContentType"/>.</summary>
|
|
public sealed record ProvideDocumentsCommand(
|
|
RegistrationId RegistrationId, string Bsn, byte[] Content, string FileName, string ContentType);
|
|
|
|
/// <summary>The outcome of a provide-documents request.</summary>
|
|
public enum ProvideDocumentsOutcome
|
|
{
|
|
/// <summary>The documents were accepted; the process's document wait was completed (if any).</summary>
|
|
Accepted,
|
|
|
|
/// <summary>No registration with that id belongs to the caller — unknown, or owned by someone else
|
|
/// (the two are deliberately indistinguishable, so the endpoint reveals neither).</summary>
|
|
NotFound,
|
|
|
|
/// <summary>The file does not start with the PDF signature (<c>%PDF-</c>); nothing was stored.</summary>
|
|
NotAPdf,
|
|
|
|
/// <summary>The malware scan found something; nothing was stored and the wait stays open.</summary>
|
|
Infected,
|
|
|
|
/// <summary>The scanner could not be reached — refused rather than storing an unscanned file.</summary>
|
|
ScannerUnavailable,
|
|
}
|
|
|
|
/// <summary>
|
|
/// The provide-documents use case (S-10a/S-10b): a zorgprofessional uploads the diploma their
|
|
/// registration is parked waiting for. Only a PDF that scans clean is accepted (S-29, ADR-0036). The document is stored in ZGW via the ACL (§8.1), then the
|
|
/// WachtOpDocumenten task is completed so the registratie process leaves the 30-day wait and continues
|
|
/// to beoordeling (ADR-0017). Owner-scoped by bsn. Both steps are best-effort about missing preconditions
|
|
/// (mirroring <see cref="WithdrawRegistration"/>): storage needs an opened zaak, and completion needs a
|
|
/// running process — a request that arrives before either still stands, storing/completing what it can.
|
|
/// </summary>
|
|
public sealed class ProvideDocuments(IRegistrationStore store, IWorkflowClient workflow, IAclClient acl, IDocumentScanner scanner)
|
|
{
|
|
public async Task<ProvideDocumentsOutcome> HandleAsync(ProvideDocumentsCommand command, CancellationToken ct = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
var registration = await store.GetAsync(command.RegistrationId, ct);
|
|
|
|
// Unknown, or not the caller's registration: report NotFound either way (don't reveal which).
|
|
if (registration is null || registration.Bsn != command.Bsn)
|
|
return ProvideDocumentsOutcome.NotFound;
|
|
|
|
// Only a clean PDF goes any further (S-29, ADR-0036): checked after ownership, so a stranger
|
|
// learns nothing about the file, and before anything is stored or the wait is completed. Scan
|
|
// before the PDF check, so malware is reported as malware whatever it claims to be.
|
|
switch (await scanner.ScanAsync(command.Content, ct))
|
|
{
|
|
case ScanVerdict.Infected: return ProvideDocumentsOutcome.Infected;
|
|
case ScanVerdict.Unavailable: return ProvideDocumentsOutcome.ScannerUnavailable;
|
|
}
|
|
|
|
if (!command.Content.AsSpan().StartsWith("%PDF-"u8))
|
|
return ProvideDocumentsOutcome.NotAPdf;
|
|
|
|
// Store the diploma against the zaak (once it is opened) — the ACL is the only ZGW caller (§8.1).
|
|
if (registration.ZaakUrl is not null)
|
|
await acl.StoreDiplomaAsync(
|
|
registration.ZaakUrl, command.Content, command.FileName, command.ContentType, ct);
|
|
|
|
// Complete the document wait (if a process is running) so beoordeling can proceed.
|
|
if (registration.ProcessInstanceId is not null)
|
|
await workflow.CompleteDocumentWaitAsync(registration.ProcessInstanceId, ct);
|
|
|
|
return ProvideDocumentsOutcome.Accepted;
|
|
}
|
|
}
|