feat(zgw): OpenZaak Documenten (DRC) upload + zaak link (WP-51)
Extends the OpenZaak seam with IDocumentSource, sibling of IZaakSource (WP-49/50): an upload always lands locally first (DocumentStore stays the record of truth for preview/download/audit) and, when Zgw:Enabled=true, is also registered as a DRC enkelvoudiginformatie- object; once a zaak exists (IZaakSource.CreateZaak now also returns its ZaakUrl), submit links each document to it via zaakinformatie- object. FE upload/list DTOs are unchanged. - ZgwOptions gains DrcBaseUrl + a category->informatieobjecttype URL map (the document analogue of ZaaktypeUrls). - LocalDocumentSource is the same DocumentStore.Add/Link calls the endpoints used to make inline — zero behaviour change offline. - OpenZaakDocumentSource POSTs the eio then the zaak link, persisting the DRC url (DocumentStore.SetDrcUrl) so linking doesn't re-upload. - Factored the GET/POST-with-bearer-JWT plumbing shared with OpenZaakZaakSource into ZgwHttpClient; shared the stub handler between the two source test classes as ZgwStubHandler. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,13 @@ public sealed class Aanvraag
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
public DateTimeOffset? SubmittedAt { get; set; }
|
||||
|
||||
/// <summary>The OpenZaak zaak's URL, set once CreateZaak (WP-50) registers one — null under
|
||||
/// the local source. Persisted so later steps (WP-51's document→zaak link) can find it
|
||||
/// without a network round-trip; IZaakSource.CreateZaak itself doesn't write here (the
|
||||
/// endpoint does, via <see cref="ApplicationStore.SetZaakUrl"/>) to keep the seam's write
|
||||
/// surface at "return data", not "reach into another store".</summary>
|
||||
public string? ZaakUrl { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -172,4 +179,18 @@ public static class ApplicationStore
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Persist the zaak URL CreateZaak (WP-50) registered for this aanvraag. No-op if
|
||||
/// the aanvraag is gone (shouldn't happen — this runs right after Submit found it).</summary>
|
||||
public static void SetZaakUrl(string id, string zaakUrl)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
using var db = Db.Create();
|
||||
var a = db.Applications.Find(id);
|
||||
if (a is null) return;
|
||||
a.ZaakUrl = zaakUrl;
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ public sealed record StoredDocument(
|
||||
string FileName, long SizeBytes, string ContentType, byte[] Content, string Owner, DateTimeOffset UploadedAt)
|
||||
{
|
||||
public bool Linked { get; set; }
|
||||
|
||||
/// <summary>The OpenZaak DRC enkelvoudiginformatieobject's URL, set once Upload (WP-51)
|
||||
/// registers one — null under the local source. Persisted so the later zaak-link step can
|
||||
/// find it without re-uploading; not part of the positional constructor, same reasoning as
|
||||
/// <see cref="Linked"/> (every existing `new StoredDocument(...)` call site keeps working).</summary>
|
||||
public string? DrcUrl { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Id is EF Core's auto-increment key — not part of the positional
|
||||
@@ -69,6 +75,19 @@ public static class DocumentStore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Persist the DRC url an OpenZaak upload (WP-51) registered for a document.</summary>
|
||||
public static void SetDrcUrl(string documentId, string drcUrl)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
using var db = Db.Create();
|
||||
var d = db.Documents.Find(documentId);
|
||||
if (d is null) return;
|
||||
d.DrcUrl = drcUrl;
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark digital documents as linked to a finalised submission (blocks user delete).
|
||||
public static void Link(IEnumerable<string> documentIds)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using BigRegister.Api.Contracts;
|
||||
|
||||
namespace BigRegister.Api.Data;
|
||||
|
||||
/// <summary>
|
||||
/// The documents seam (WP-51), sibling of <see cref="IZaakSource"/>: uploads always land
|
||||
/// locally first (<see cref="DocumentStore"/> stays the record of truth for preview/download/
|
||||
/// audit regardless of config, exactly like <c>ApplicationStore.Submit</c> for aanvragen,
|
||||
/// WP-50) — this interface is only the OpenZaak integration side-effect, selected the same way
|
||||
/// (<c>Zgw:Enabled</c>). Default binding is <see cref="LocalDocumentSource"/> (offline);
|
||||
/// <c>OpenZaakDocumentSource</c> also registers each upload as a DRC
|
||||
/// enkelvoudiginformatieobject and links it to a zaak once one exists.
|
||||
/// </summary>
|
||||
public interface IDocumentSource
|
||||
{
|
||||
/// <summary>Store an uploaded file (already validated by <c>DocumentRules</c>) and return the
|
||||
/// existing <see cref="UploadResponse"/> DTO unchanged, whichever source is active.</summary>
|
||||
UploadResponse Upload(
|
||||
string localId, string categoryId, string wizardId, string fileName, string contentType,
|
||||
byte[] content, string owner);
|
||||
|
||||
/// <summary>Finalise a set of already-uploaded documents against a just-submitted aanvraag
|
||||
/// (WP-50/51): local behaviour is exactly today's <c>DocumentStore.Link</c>; the OpenZaak
|
||||
/// source additionally links each document (that has a DRC url) to the zaak, once
|
||||
/// <paramref name="zaakUrl"/> is known (null under the local <see cref="IZaakSource"/>, in
|
||||
/// which case there is nothing extra to link).</summary>
|
||||
void LinkToZaak(IReadOnlyList<string> documentIds, string? zaakUrl);
|
||||
}
|
||||
@@ -22,11 +22,13 @@ public interface IZaakSource
|
||||
/// <summary>
|
||||
/// Register a just-submitted <paramref name="aanvraag"/> as a zaak (WP-50). The aanvraag is
|
||||
/// already persisted locally (<c>ApplicationStore.Submit</c> already ran) — this is the
|
||||
/// integration side-effect, and its return value is what the submit endpoint hands back to
|
||||
/// the FE (ADR-0001: route the create through the existing submit response DTO, don't add a
|
||||
/// second one). The local source is a pure passthrough of the already-computed local
|
||||
/// reference/status; the OpenZaak source creates a Zaak (+ status + rol) and maps the result
|
||||
/// back into the same shape.
|
||||
/// integration side-effect, and (Referentie, Status) is what the submit endpoint hands back
|
||||
/// to the FE (ADR-0001: route the create through the existing submit response DTO, don't add
|
||||
/// a second one). The local source is a pure passthrough of the already-computed local
|
||||
/// reference/status (ZaakUrl null — nothing to persist); the OpenZaak source creates a Zaak
|
||||
/// (+ status + rol) and maps the result back into the same shape, returning the zaak's URL
|
||||
/// so the endpoint can persist it (<see cref="ApplicationStore.SetZaakUrl"/>, WP-51 needs it
|
||||
/// to later link documents to this zaak).
|
||||
/// </summary>
|
||||
(string Referentie, AanvraagStatusDto Status) CreateZaak(Aanvraag aanvraag, DateTimeOffset now);
|
||||
(string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(Aanvraag aanvraag, DateTimeOffset now);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using BigRegister.Api.Contracts;
|
||||
|
||||
namespace BigRegister.Api.Data;
|
||||
|
||||
/// <summary>
|
||||
/// The default <see cref="IDocumentSource"/> — uploads go only to the local SQLite
|
||||
/// <see cref="DocumentStore"/>, exactly as before this seam existed (WP-51). Zero behaviour
|
||||
/// change: this is the same <c>DocumentStore.Add</c>/<c>DocumentStore.Link</c> the upload/
|
||||
/// submit endpoints used to call inline.
|
||||
/// </summary>
|
||||
public sealed class LocalDocumentSource : IDocumentSource
|
||||
{
|
||||
public UploadResponse Upload(
|
||||
string localId, string categoryId, string wizardId, string fileName, string contentType,
|
||||
byte[] content, string owner)
|
||||
{
|
||||
var doc = DocumentStore.Add(localId, categoryId, wizardId, fileName, contentType, content, owner);
|
||||
return new UploadResponse(doc.DocumentId, doc.LocalId);
|
||||
}
|
||||
|
||||
public void LinkToZaak(IReadOnlyList<string> documentIds, string? zaakUrl) =>
|
||||
DocumentStore.Link(documentIds);
|
||||
}
|
||||
@@ -15,6 +15,6 @@ public sealed class LocalZaakSource : IZaakSource
|
||||
|
||||
/// <summary>No external zaak to create — the aanvraag's local submit already IS the record
|
||||
/// of truth, exactly as before this seam existed (WP-50). Zero behaviour change.</summary>
|
||||
public (string Referentie, AanvraagStatusDto Status) CreateZaak(Aanvraag aanvraag, DateTimeOffset now) =>
|
||||
(aanvraag.Referentie!, aanvraag.ToStatusDto(now));
|
||||
public (string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(Aanvraag aanvraag, DateTimeOffset now) =>
|
||||
(aanvraag.Referentie!, aanvraag.ToStatusDto(now), null);
|
||||
}
|
||||
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using BigRegister.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BigRegister.Api.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260729071227_ZaakAndDrcUrls")]
|
||||
partial class ZaakAndDrcUrls
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.9");
|
||||
|
||||
modelBuilder.Entity("BigRegister.Api.Data.Aanvraag", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("AutoApprovable")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DocumentIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Draft")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Reden")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Referentie")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("StepCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("StepIndex")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Submitted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("SubmittedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ZaakUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Applications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BigRegister.Api.Data.AuditEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Actor")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("At")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CategoryId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DocumentId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AuditEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BigRegister.Api.Data.AuthzAuditEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("At")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CorrelationId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Decision")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Resource")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AuthzAudit");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BigRegister.Api.Data.BriefEntity", b =>
|
||||
{
|
||||
b.Property<string>("BriefId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ArchivedHtml")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Beroep")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DrafterId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Placeholders")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Sections")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SentOrgTemplateVersion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubOrgId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TemplateId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BriefId");
|
||||
|
||||
b.HasIndex("Owner")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Briefs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BigRegister.Api.Data.FeatureFlagEntity", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("FeatureFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BigRegister.Api.Data.OrgTemplateEntity", b =>
|
||||
{
|
||||
b.Property<string>("SubOrgId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Draft")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("History")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PublishedVersion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("SubOrgId");
|
||||
|
||||
b.ToTable("OrgTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BigRegister.Api.Data.StoredDocument", b =>
|
||||
{
|
||||
b.Property<string>("DocumentId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CategoryId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DrcUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Linked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LocalId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("SizeBytes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("UploadedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("WizardId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("DocumentId");
|
||||
|
||||
b.ToTable("Documents");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BigRegister.Api.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ZaakAndDrcUrls : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DrcUrl",
|
||||
table: "Documents",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ZaakUrl",
|
||||
table: "Applications",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DrcUrl",
|
||||
table: "Documents");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ZaakUrl",
|
||||
table: "Applications");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,9 @@ namespace BigRegister.Api.Data.Migrations
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ZaakUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Applications");
|
||||
@@ -235,6 +238,9 @@ namespace BigRegister.Api.Data.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DrcUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
Reference in New Issue
Block a user