feat(#78): one citizen reference across self-service and the openbaar register (#79)
All checks were successful
All checks were successful
## What & why Before this change the self-service confirmation and the openbaar register showed **different** identifiers, so a citizen could not look their registration back up (#78). Now both surface the same **reference**: - **domain → ACL (write):** the domain `registrationId` is set as the zaak's `identificatie` on `POST /zaken`. - **event-subscriber → ACL (read):** the subscriber reads the zaak's `identificatie` back through the ACL (§8.1 — only the ACL talks to ZGW) via a new `POST /zaken/reference`, and stores it on the projection row **and** the `processed_notifications` replay log. - **BFF + openbaar:** the public view exposes `id/status/reference` (never bsn/naam) and searches by id or reference; the register's "Referentie" column shows the reference. Storing the reference in the replay log keeps ADR-0008's **rebuild-is-log-only** invariant intact — `/admin/rebuild` reproduces the reference without re-reading the ACL. Decision recorded in **ADR-0012**. ## Definition of Done - [x] Linked issue: #78 - [x] Tests written first; red → green per layer - [x] Unit + acceptance green (`make unit`): domain 49, acl 27, bff 20, event-subscriber 19, acceptance 7 - [x] Frontend lint + test green (`nx run-many -t lint test`) - [x] Mutation ≥ break(90): acl 100%, event-subscriber 100%, bff 100%, domain 98.41% (pre-existing FlowableWorkflowClient baseline, untouched) - [x] e2e extended: confirmation reference == register reference - [x] openapi.json + api-client regenerated (drift guard green) - [x] ADR-0012 added; demo-script note appended - [x] `Acl__BaseUrl` wired for the subscriber in compose closes #78 Reviewed-on: #79
This commit was merged in pull request #79.
This commit is contained in:
@@ -31,7 +31,7 @@ public sealed class BffToegangSteps : IDisposable
|
||||
[Given("the projection contains a zaak \"(.*)\" for BSN \"(.*)\"")]
|
||||
public void GivenTheProjectionContains(string zaakId, string bsn)
|
||||
{
|
||||
_host.Projection.Entries.Add(new ProjectionEntry(zaakId, "INGEDIEND", bsn, null));
|
||||
_host.Projection.Entries.Add(new ProjectionEntry(zaakId, "INGEDIEND", "REG-" + zaakId, bsn, null));
|
||||
_client = _host.CreateClient();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class EenZaakOpenenSteps
|
||||
|
||||
[Given("a domain registration for BSN \"(.*)\"")]
|
||||
public void GivenADomainRegistrationForBsn(string bsn)
|
||||
=> _registration = new DomainRegistration(bsn);
|
||||
=> _registration = new DomainRegistration(bsn, "ACC-REF-1");
|
||||
|
||||
[Given("the ACL is configured with these defaults:")]
|
||||
public void GivenTheAclIsConfiguredWithTheseDefaults(DataTable defaults)
|
||||
|
||||
@@ -17,7 +17,8 @@ public sealed class RegisterProjectieBijwerkenSteps
|
||||
private readonly NotificationProjector _projector;
|
||||
private Notification? _notification;
|
||||
|
||||
public RegisterProjectieBijwerkenSteps() => _projector = new NotificationProjector(_log, _store);
|
||||
public RegisterProjectieBijwerkenSteps()
|
||||
=> _projector = new NotificationProjector(_log, _store, new InMemoryAclReferenceClient());
|
||||
|
||||
[Given("a zaak is created in OpenZaak with id \"(.*)\"")]
|
||||
public void GivenAZaakIsCreatedInOpenZaakWithId(string id)
|
||||
|
||||
@@ -26,11 +26,13 @@ public sealed class InMemoryAclClient : IAclClient
|
||||
public static readonly Uri OpenedZaakUrl = new("http://openzaak/zaken/api/v1/zaken/acc-zaak");
|
||||
|
||||
public string? OpenedForBsn { get; private set; }
|
||||
public string? OpenedWithReference { get; private set; }
|
||||
public Uri? ApprovedZaakUrl { get; private set; }
|
||||
|
||||
public Task<Uri> OpenZaakAsync(string bsn, CancellationToken ct = default)
|
||||
public Task<Uri> OpenZaakAsync(string bsn, string reference, CancellationToken ct = default)
|
||||
{
|
||||
OpenedForBsn = bsn;
|
||||
OpenedWithReference = reference;
|
||||
return Task.FromResult(OpenedZaakUrl);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,3 +38,11 @@ public sealed class InMemoryProjectionStore : IProjectionStore
|
||||
public IReadOnlyList<RegisterEntry> RowsFor(string id)
|
||||
=> [.. _byId.Values.Where(e => e.Id == id)];
|
||||
}
|
||||
|
||||
/// <summary>A fake ACL client for the projection acceptance scenario: returns a reference derived
|
||||
/// from the zaak, so the projector can enrich rows without a running ACL (#78).</summary>
|
||||
public sealed class InMemoryAclReferenceClient : IAclClient
|
||||
{
|
||||
public Task<string> GetZaakReferenceAsync(Uri zaakUrl, CancellationToken ct = default)
|
||||
=> Task.FromResult("REG-" + zaakUrl.Segments[^1].Trim('/'));
|
||||
}
|
||||
|
||||
@@ -24,4 +24,7 @@ public sealed class InMemoryZaakGateway : IZaakGateway
|
||||
Approved = (zaakUrl, zaaktypeUrl, datumStatusGezet);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<string> GetZaakIdentificatieAsync(Uri zaakUrl, CancellationToken ct = default)
|
||||
=> Task.FromResult("ACC-REF-1");
|
||||
}
|
||||
|
||||
@@ -26,15 +26,22 @@ test('DigiD login → submit → public INGEDIEND → approve → public INGESCH
|
||||
|
||||
// The openbaar register (anonymous, its own origin) shows the submitted entry once the projection
|
||||
// catches up. The projection updates asynchronously (NRC → event-subscriber), and the register loads
|
||||
// on open, so reload until a submitted (INGEDIEND) row appears.
|
||||
// on open, so reload until *this* submission's row appears. We poll on the reference cell (not a
|
||||
// generic INGEDIEND cell): the shared verify stack already holds INGEDIEND rows from earlier checks,
|
||||
// so a status-only poll would short-circuit on a stale row before our row is projected.
|
||||
await page.goto('http://openbaar/');
|
||||
await expect(page.getByRole('heading', { name: /Openbaar BIG-register/i })).toBeVisible();
|
||||
|
||||
// #78: the reference shown in the public register must be the exact one the citizen saw on the
|
||||
// submit confirmation — no mismatch between the two portals.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
await page.reload();
|
||||
return page.getByRole('cell', { name: 'INGEDIEND' }).count();
|
||||
return page.getByRole('cell', { name: reference }).count();
|
||||
}, { timeout: 30_000, intervals: [1_000, 2_000, 3_000, 5_000] })
|
||||
.toBeGreaterThan(0);
|
||||
await expect(page.getByRole('row', { name: reference }).getByRole('cell', { name: 'INGEDIEND' }))
|
||||
.toBeVisible();
|
||||
|
||||
// Approve via the temporary admin endpoint (reached directly on the compose network, as a
|
||||
// behandelaar would until the behandel-portal exists — S-12). The zaak is opened off the request
|
||||
@@ -49,11 +56,12 @@ test('DigiD login → submit → public INGEDIEND → approve → public INGESCH
|
||||
const approve = await request.post(`http://domain:8080/registrations/${reference}/approve`);
|
||||
expect(approve.status()).toBe(204);
|
||||
|
||||
// The approval flows back to the projection; the openbaar register now shows it as INGESCHREVEN.
|
||||
// The approval flows back to the projection; the openbaar register now shows *our* row (matched by
|
||||
// its reference) as INGESCHREVEN.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
await page.reload();
|
||||
return page.getByRole('cell', { name: 'INGESCHREVEN' }).count();
|
||||
return page.getByRole('row', { name: reference }).getByRole('cell', { name: 'INGESCHREVEN' }).count();
|
||||
}, { timeout: 30_000, intervals: [1_000, 2_000, 3_000, 5_000] })
|
||||
.toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user