docs: ADR-0022 + demo note + backlog sync for the herregistratie sweep (refs #18)
CI / lint (pull_request) Successful in 4m27s
CI / build (pull_request) Successful in 1m11s
CI / unit (pull_request) Successful in 1m17s
CI / frontend (pull_request) Successful in 2m53s
CI / mutation (pull_request) Successful in 5m59s
CI / verify-stack (pull_request) Successful in 8m52s
CI / lint (pull_request) Successful in 4m27s
CI / build (pull_request) Successful in 1m11s
CI / unit (pull_request) Successful in 1m17s
CI / frontend (pull_request) Successful in 2m53s
CI / mutation (pull_request) Successful in 5m59s
CI / verify-stack (pull_request) Successful in 8m52s
ADR-0022 records using Quartz.NET for time-triggered fleet sweeps (pumps stay as queue-drainers); demo-script and BACKLOG describe S-17's outcome. refs #18
This commit is contained in:
+2
-2
@@ -257,9 +257,9 @@ Split (issue #11 closed) into two independently-demoable slices per §13 — the
|
|||||||
|
|
||||||
**Outcome:** Traces span portal → BFF → Domain → ACL → OpenZaak and portal → BFF → Domain → Flowable. Grafana dashboards pre-built for golden signals.
|
**Outcome:** Traces span portal → BFF → Domain → ACL → OpenZaak and portal → BFF → Domain → Flowable. Grafana dashboards pre-built for golden signals.
|
||||||
|
|
||||||
### S-17 · Quartz.NET scheduler — herregistratie reminder sweep
|
### S-17 · Quartz.NET scheduler — herregistratie reminder sweep ✅
|
||||||
|
|
||||||
**Outcome:** Nightly job that finds entries within 90 days of expiry and emits a domain event. (No outbound notification in v1 — logged.)
|
**Outcome:** Daily Quartz.NET cron job finds inscriptions within 90 days of their herregistratie deadline and reminds each (flag on the aggregate + log). No outbound notification and no domain event in v1 — the reminder is the persisted flag, surfaced on the read model (ADR-0022, #120). Quartz fires time-triggered sweeps; the existing pumps stay as queue-drainers.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# ADR-0022: Quartz.NET for time-triggered fleet sweeps
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-07-23
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Slice:** S-17 (#18) · **Proposal issue:** #120
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A BIG inscription is valid for a fixed term; before it lapses the zorgprofessional
|
||||||
|
must herregistreren. S-17 adds a **herregistratie reminder sweep**: once a day,
|
||||||
|
scan the register for inscriptions whose deadline is within the reminder window and
|
||||||
|
remind each one.
|
||||||
|
|
||||||
|
The Domain Service already runs periodic background work — `OpenZaakJobPump`,
|
||||||
|
`BeoordelingEscalatiePump`, `RegistratieVerlopenPump`. Those are **continuous job
|
||||||
|
pollers**: they drain Flowable's external-task/job queues at-least-once, picking up
|
||||||
|
work as soon as it is parked, on a short poll interval. The reminder sweep is a
|
||||||
|
different shape of work: **time-triggered**, once a day, over our own store — there
|
||||||
|
is no queue to drain and no "as soon as possible" requirement.
|
||||||
|
|
||||||
|
The PRD already names the scheduler component: "Scheduler (Quartz.NET): fleet-wide
|
||||||
|
sweeps (expiry, reminders)" (§39, §94). Adding Quartz.NET is nonetheless a new
|
||||||
|
dependency, so this decision is recorded before the code lands (CLAUDE.md §14).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Use Quartz.NET for time-triggered fleet sweeps, starting with the herregistratie
|
||||||
|
reminder sweep. Leave the existing pumps as `BackgroundService` job pollers.**
|
||||||
|
|
||||||
|
- `HerregistratieReminderJob` (a Quartz `IJob`) is fired by a cron trigger — daily
|
||||||
|
at 03:00 by default, overridable with `Quartz__Cron`. It is a thin shell: it
|
||||||
|
resolves the pure `HerregistratieReminderSweep` (application layer) and logs how
|
||||||
|
many reminders went out.
|
||||||
|
- The sweep's rule lives in the domain: `Registration.HerregistratieReminderDue(asOf)`,
|
||||||
|
which the store query and the sweep both build on. The sweep marks each reminded
|
||||||
|
inscription (`HerregistratieReminderVerstuurd`), so a re-fire reminds no one twice
|
||||||
|
(§8.6).
|
||||||
|
|
||||||
|
Two options were rejected:
|
||||||
|
|
||||||
|
1. **A `BackgroundService` with a 24h `Task.Delay`.** No new dependency, but it
|
||||||
|
drifts to process-start time, has no cron/misfire semantics, and contradicts the
|
||||||
|
PRD's named component. A daily "run at 03:00" is exactly what cron scheduling is
|
||||||
|
for.
|
||||||
|
2. **Migrating the three pumps onto Quartz too, for one mechanism.** Rejected: the
|
||||||
|
pumps are not schedulers. Forcing a "run at time T" tool onto "drain this queue
|
||||||
|
continuously" work is churn and a boundary change for negative benefit. The
|
||||||
|
teachable distinction is worth keeping: **pumps drain queues; Quartz fires
|
||||||
|
sweeps.**
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive**
|
||||||
|
|
||||||
|
- Cron scheduling with restart-stable timing and misfire handling, for free.
|
||||||
|
- The reminder rule is one domain method, reused by the store query and the sweep;
|
||||||
|
the scheduler owns none of the policy.
|
||||||
|
- The reference app now demonstrates the intended Scheduler component.
|
||||||
|
|
||||||
|
**Negative / costs**
|
||||||
|
|
||||||
|
- One new dependency (`Quartz`, `Quartz.Extensions.Hosting`) in the Domain Service.
|
||||||
|
- Two periodic-work mechanisms coexist (pumps + Quartz). Deliberate — they model
|
||||||
|
two genuinely different concerns, documented here.
|
||||||
|
|
||||||
|
**Follow-up**
|
||||||
|
|
||||||
|
- The validity term (5 years) and reminder lead time (16 weeks) are domain
|
||||||
|
calibration knobs; promote them to beheer config (S-15) if a demo needs them
|
||||||
|
per-catalogus.
|
||||||
|
- The Quartz job stores its schedule in RAM (`RAMJobStore`); a persistent/clustered
|
||||||
|
store is a later concern if the Domain Service is scaled out.
|
||||||
|
|
||||||
|
## Coupling rules touched (CLAUDE.md §8)
|
||||||
|
|
||||||
|
None. Quartz is internal to the Domain Service and drives an application use case
|
||||||
|
over the store port. No ZGW or Flowable coupling is added; the sweep talks to no
|
||||||
|
peer module.
|
||||||
@@ -5,6 +5,35 @@ copy-pasteable walkthrough against a local `make up` stack.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## S-17 — herregistratie reminder sweep on a Quartz cron (#18, ADR-0022)
|
||||||
|
|
||||||
|
**Outcome:** an inscription (INGESCHREVEN) now carries the moment it was entered in the register, from
|
||||||
|
which its herregistratie deadline is derived (inscription + 5-year validity). A **Quartz.NET** cron job
|
||||||
|
in the Domain Service sweeps once a day (03:00, overridable via `Quartz__Cron`): every inscription
|
||||||
|
inside the 90-day window before its deadline is flagged `HerregistratieReminderVerstuurd` and logged.
|
||||||
|
The sweep is idempotent — a re-fire reminds no one twice — and is a deliberately different mechanism
|
||||||
|
from the queue-draining pumps (Quartz fires time-triggered sweeps; pumps drain Flowable queues,
|
||||||
|
ADR-0022). There is no outbound notification in v1: the reminder is the flag on the aggregate plus a
|
||||||
|
log line.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. The domain unit tests prove the rule and the sweep end to end (rule → store query → sweep):
|
||||||
|
cd services/domain && dotnet test Big.Tests/Big.Tests.csproj \
|
||||||
|
--filter "FullyQualifiedName~Herregistratie|FullyQualifiedName~ReminderSweep"
|
||||||
|
# → the reminder is due once the 90-day window opens, not before; a reminded inscription is skipped
|
||||||
|
# on the next sweep; the sweep flags + persists every due inscription and returns their ids.
|
||||||
|
|
||||||
|
# 2. The read model surfaces the deadline once a registration is approved — the field the sweep acts on:
|
||||||
|
curl -s localhost:8000/registrations/<id> | jq '{status, herregistratieVoor, herregistratieReminderVerstuurd}'
|
||||||
|
# → after approval: herregistratieVoor is inscription + 5 years; the flag flips true once swept.
|
||||||
|
```
|
||||||
|
|
||||||
|
**The path:** `Registration.Approve(now)` stamps `IngeschrevenOp` → daily Quartz `HerregistratieReminderJob`
|
||||||
|
→ `HerregistratieReminderSweep` → `IRegistrationStore.FindDueForHerregistratieReminderAsync` (filtered by
|
||||||
|
the aggregate's own `HerregistratieReminderDue` rule) → `MarkHerregistratieReminderVerstuurd` + log.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## S-B04 — `make local` completes the whole flow with no manual seeding (#110, ADR-0020)
|
## S-B04 — `make local` completes the whole flow with no manual seeding (#110, ADR-0020)
|
||||||
|
|
||||||
**Outcome:** the host-browser stack (`make local`) now self-seeds at bring-up — it publishes the BIG
|
**Outcome:** the host-browser stack (`make local`) now self-seeds at bring-up — it publishes the BIG
|
||||||
|
|||||||
Reference in New Issue
Block a user