All checks were successful
## What & why S-09b (#75, split from #10) — the **approval flow** that completes the walking skeleton. A behandelaar can now approve a submitted registration; the entry flips from `INGEDIEND` to `INGESCHREVEN` in the public register. Flow: `POST /registrations/{id}/approve` (domain) → ACL sets the zaak eindstatus (ZGW `/statussen`) → OpenZaak → NRC → event-subscriber → projection → openbaar. ## Changes (bottom-up, each red→green TDD) - **Domain** — `RegistrationStatus.Ingeschreven` + `Registration.Approve()` (guards: opened zaak, only from INGEDIEND); `ApproveRegistration` use case (idempotent) + temp `POST /registrations/{id}/approve` endpoint; `IAclClient.ApproveZaakAsync`. - **ACL** — resolves the zaaktype's **eindstatus** from the catalogus (`isEindstatus` / highest volgnummer) and POSTs a ZGW status; exposed as `POST /statussen`. Unit + real-OpenZaak integration test. - **Event-subscriber** — binds NRC `hoofdObject`, projects a `status`/`create` as `INGESCHREVEN` keyed on the zaak (updates the existing row), **without reading OpenZaak** (§8.1). Retains the ZGW `resource` in the log (new column + EF migration) so a rebuild reproduces the status. - **e2e** — extended: submit → public INGEDIEND → approve → public INGESCHREVEN. - **Docs** — ADR-0011 (the two non-obvious decisions + the walking-skeleton assumption) + demo note. ## Key decisions (see ADR-0011) - **ACL discovers the eindstatus** (chosen over injecting a statustype URL): no new config/seed plumbing, domain stays ZGW-ignorant. - **Any post-creation status-set ⇒ INGESCHREVEN**: in the walking skeleton the only status ever set after creation is the approval, and the subscriber may not read ZGW — documented to tighten when more transitions arrive (S-12+). ## Verification - All .NET unit suites green locally (domain 47, acl 11, event-subscriber 14, bff 16, acceptance 7); Release build + `dotnet format` clean. - No new compose config (the eindstatus-discovery approach avoided it). - The real-OpenZaak integration test (ACL status-set) and the full submit→approve→visible e2e run in CI `verify-stack` (live NRC→projection + selectielijst egress, not reproducible locally). closes #75 Reviewed-on: #77
72 lines
2.6 KiB
C#
72 lines
2.6 KiB
C#
using Big.Application;
|
|
using Big.Domain;
|
|
|
|
namespace Big.Tests;
|
|
|
|
/// <summary>An in-memory <see cref="IRegistrationStore"/> for the application-layer tests. Upserts
|
|
/// keyed on the registration id, mirroring the production store (kept distinct from the production
|
|
/// <c>InMemoryRegistrationStore</c> so tests can seed and inspect save counts).</summary>
|
|
internal sealed class FakeRegistrationStore : IRegistrationStore
|
|
{
|
|
private readonly Dictionary<RegistrationId, Registration> _byId = [];
|
|
|
|
public int SaveCount { get; private set; }
|
|
|
|
public Task SaveAsync(Registration registration, CancellationToken ct = default)
|
|
{
|
|
SaveCount++;
|
|
_byId[registration.Id] = registration;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<Registration?> GetAsync(RegistrationId id, CancellationToken ct = default)
|
|
=> Task.FromResult(_byId.GetValueOrDefault(id));
|
|
|
|
public void Seed(Registration registration) => _byId[registration.Id] = registration;
|
|
}
|
|
|
|
/// <summary>A fake Workflow Client that records the registration it was asked to start a process for
|
|
/// and returns a fixed process-instance id. An optional callback runs at start time, letting a test
|
|
/// assert ordering (e.g. that the registration was persisted before the process started).</summary>
|
|
internal sealed class FakeWorkflowClient(string processInstanceId = "proc-1", Action<RegistrationId>? onStart = null)
|
|
: IWorkflowClient
|
|
{
|
|
public RegistrationId? StartedFor { get; private set; }
|
|
|
|
public Task<string> StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default)
|
|
{
|
|
onStart?.Invoke(registrationId);
|
|
StartedFor = registrationId;
|
|
return Task.FromResult(processInstanceId);
|
|
}
|
|
}
|
|
|
|
/// <summary>A fake ACL client that records the bsn it was asked to open a zaak for and returns a
|
|
/// fixed zaak URL.</summary>
|
|
internal sealed class FakeAclClient(Uri? zaakUrl = null) : IAclClient
|
|
{
|
|
public static readonly Uri DefaultZaakUrl = new("http://openzaak/zaken/api/v1/zaken/abc");
|
|
|
|
private readonly Uri _zaakUrl = zaakUrl ?? DefaultZaakUrl;
|
|
|
|
public string? OpenedForBsn { get; private set; }
|
|
public int CallCount { get; private set; }
|
|
|
|
public Uri? ApprovedZaakUrl { get; private set; }
|
|
public int ApproveCallCount { get; private set; }
|
|
|
|
public Task<Uri> OpenZaakAsync(string bsn, CancellationToken ct = default)
|
|
{
|
|
CallCount++;
|
|
OpenedForBsn = bsn;
|
|
return Task.FromResult(_zaakUrl);
|
|
}
|
|
|
|
public Task ApproveZaakAsync(Uri zaakUrl, CancellationToken ct = default)
|
|
{
|
|
ApproveCallCount++;
|
|
ApprovedZaakUrl = zaakUrl;
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|