using System.Text.Json;
using System.Text.Json.Serialization;
using BigRegister.Api.Contracts;
using BigRegister.Api.Data;
using BigRegister.Domain.Authorization;
using BigRegister.Stamdata;
using Microsoft.Extensions.Logging;
namespace BigRegister.Api.Zgw;
///
/// The backed by a real OpenZaak / ZGW Documenten API (DRC,
/// WP-51). An upload always lands locally first ( stays the record
/// of truth for preview/download/audit, same reasoning as 's
/// dual-write for aanvragen, WP-50) and is then ALSO registered as a DRC
/// enkelvoudiginformatieobject, whose url is persisted ()
/// so can find it later without a re-upload. Selected only when
/// Zgw:Enabled=true; the default stays .
///
/// Auth: a fresh HS256 JWT per request (), same as
/// — creating a document needs write scope on Documenten;
/// linking one to a zaak needs write scope on Zaken (the zaakinformatieobject resource).
///
public sealed class OpenZaakDocumentSource(
HttpClient http, ZgwTokenProvider tokens, ZgwOptions options, ILogger? log = null)
: IDocumentSource
{
private readonly ZgwHttpClient zgw = new(http, tokens);
// WP-59: per-document-type confidentiality (stamdata, ADR-0004) — "openbaar" if the
// category isn't in the table, so an unconfigured category never fails the upload.
private static readonly IReadOnlyDictionary ConfidentialiteitByCategory =
StamdataFile.Load("documentconfidentialiteit")
.ToDictionary(r => r.CategoryId, r => r.Vertrouwelijkheidaanduiding);
private static string ConfidentialiteitFor(string categoryId) =>
ConfidentialiteitByCategory.GetValueOrDefault(categoryId, "openbaar");
// ponytail: sync-over-async — IDocumentSource is sync to match the local store + the
// existing sync upload/submit endpoints, same reasoning as OpenZaakZaakSource.
public UploadResponse Upload(
string localId, string categoryId, string wizardId, string fileName, string contentType,
byte[] content, CallerIdentity caller) =>
UploadAsync(localId, categoryId, wizardId, fileName, contentType, content, caller)
.GetAwaiter().GetResult();
// WP-60: once DocumentStore.Add (below) has committed, the local document is the record of
// truth (per the class doc above) — a ZGW failure past that point is caught, logged, and
// leaves DrcUrl null rather than throwing. DrcUrl == null is already the meaningful "not
// registered in ZGW yet" detector LinkToZaak skips on, so no separate flag column is needed
// here the way ApplicationStore.ZgwError is for the zaak side (see openzaak-integration.md's
// "Write resilience" section for why the two write paths differ).
private async Task UploadAsync(
string localId, string categoryId, string wizardId, string fileName, string contentType,
byte[] content, CallerIdentity caller)
{
var doc = DocumentStore.Add(localId, categoryId, wizardId, fileName, contentType, content, caller.Bsn);
try
{
if (!options.InformatieobjecttypeUrls.TryGetValue(categoryId, out var informatieobjecttypeUrl))
throw new InvalidOperationException(
$"Zgw:InformatieobjecttypeUrls has no entry for category '{categoryId}'.");
var eio = await zgw.PostAsync($"{options.DrcBaseUrl}/enkelvoudiginformatieobjecten", new CreateEioRequest(
Bronorganisatie: options.Bronorganisatie,
Creatiedatum: DateOnly.FromDateTime(doc.UploadedAt.UtcDateTime),
Titel: fileName,
Auteur: options.UserRepresentation,
Taal: "nld",
Formaat: contentType,
Bestandsnaam: fileName,
Inhoud: Convert.ToBase64String(content),
Informatieobjecttype: informatieobjecttypeUrl,
Identificatie: doc.DocumentId,
Vertrouwelijkheidaanduiding: ConfidentialiteitFor(categoryId)), caller);
DocumentStore.SetDrcUrl(doc.DocumentId, eio.Url);
}
catch (Exception ex)
{
log?.LogError(ex, "zgw divergence document={DocumentId} category={CategoryId}", doc.DocumentId, categoryId);
}
return new UploadResponse(doc.DocumentId, doc.LocalId);
}
/// Local link always happens (dual-write, same reasoning as upload); additionally,
/// once a zaak exists, POST a zaakinformatieobject for every document that has a DRC url —
/// documents uploaded before Zgw:Enabled was ever true (or under a config gap) simply have
/// no DrcUrl yet and are skipped, matching "nothing extra to link" for the local case.
/// WP-60: unlike Upload, a ZGW failure here still throws — DocumentStore.Link (the local
/// half) already ran above, so the caller (Program.cs's submit endpoint) catching this and
/// recording it as a flagged divergence is what closes the gap, not a try/catch in here.
public void LinkToZaak(IReadOnlyList documentIds, string? zaakUrl, CallerIdentity caller)
{
DocumentStore.Link(documentIds);
if (zaakUrl is null) return;
LinkToZaakAsync(documentIds, zaakUrl, caller).GetAwaiter().GetResult();
}
private async Task LinkToZaakAsync(IReadOnlyList documentIds, string zaakUrl, CallerIdentity caller)
{
foreach (var documentId in documentIds)
{
var drcUrl = DocumentStore.Get(documentId)?.DrcUrl;
if (drcUrl is null) continue;
await zgw.PostAsync($"{options.ZrcBaseUrl}/zaakinformatieobjecten",
new CreateZaakInformatieobjectRequest(zaakUrl, drcUrl), caller);
}
}
private sealed record Eio([property: JsonPropertyName("url")] string Url);
private sealed record CreateEioRequest(
[property: JsonPropertyName("bronorganisatie")] string Bronorganisatie,
[property: JsonPropertyName("creatiedatum")] DateOnly Creatiedatum,
[property: JsonPropertyName("titel")] string Titel,
[property: JsonPropertyName("auteur")] string Auteur,
[property: JsonPropertyName("taal")] string Taal,
[property: JsonPropertyName("formaat")] string Formaat,
[property: JsonPropertyName("bestandsnaam")] string Bestandsnaam,
[property: JsonPropertyName("inhoud")] string Inhoud,
[property: JsonPropertyName("informatieobjecttype")] string Informatieobjecttype,
[property: JsonPropertyName("identificatie")] string Identificatie,
[property: JsonPropertyName("vertrouwelijkheidaanduiding")] string Vertrouwelijkheidaanduiding);
private sealed record CreateZaakInformatieobjectRequest(
[property: JsonPropertyName("zaak")] string Zaak,
[property: JsonPropertyName("informatieobject")] string Informatieobject);
}