namespace EventSubscriber.Application; /// /// Projects inbound NRC notifications into the read projection. Tolerates duplicate and /// out-of-order deliveries (CLAUDE.md §8.6): the notification log dedups, and the projection /// upsert is idempotent on the zaak id. Rebuilds the projection by replaying the log. /// public sealed class NotificationProjector(INotificationLog log, IProjectionStore store, IAclClient acl) { /// Handle one inbound notification. Reacts to a zaak being created (INGEDIEND) and a /// status being set (INGESCHREVEN); ignores everything else. Enriches the row with the zaak's /// reference via the ACL (§8.1) and records it so a rebuild needs no ZGW access (#78). public async Task HandleAsync(Notification notification, CancellationToken ct = default) { if (!notification.IsZaakCreated && !notification.IsZaakStatusSet) return; var reference = await acl.GetZaakReferenceAsync(notification.ZaakUrl, ct); var recorded = new RecordedNotification( notification.IdempotencyKey, notification.Actie, notification.ZaakId, notification.Resource, reference); // Atomic record-or-skip: a duplicate (or concurrent) delivery is recognised and dropped // before it touches the projection, so the projection stays a faithful derived artefact. if (!await log.TryRecordAsync(recorded, ct)) return; await store.UpsertAsync(ToEntry(recorded), ct); } /// Rebuild the projection from the durable notification log (PRD §8.4). public async Task RebuildAsync(CancellationToken ct = default) { await store.ClearAsync(ct); foreach (var recorded in await log.AllAsync(ct)) await store.UpsertAsync(ToEntry(recorded), ct); } /// The projection row for an accepted notification: a status-set maps to INGESCHREVEN, /// a zaak-create to INGEDIEND. bsn/naam are deferred (ADR-0008). private static RegisterEntry ToEntry(RecordedNotification recorded) => new( recorded.ZaakId, recorded.Resource == "status" ? RegistrationStatus.Ingeschreven : RegistrationStatus.Ingediend, Reference: recorded.Reference); }