Files

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;
}
}