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