## What & why
S-17: a BIG inscription is valid for a fixed term; before it lapses the zorgprofessional must herregistreren. This adds a **daily herregistratie reminder sweep**.
- **Domain:** `Approve(ingeschrevenOp)` now stamps the inscription moment; `HerregistratieVoor` derives the deadline (inscription + 5-year validity); `HerregistratieReminderDue(asOf)` is the single rule (inside the 90-day window, inscribed, not yet reminded); `MarkHerregistratieReminderVerstuurd()` is idempotent.
- **Store:** `FindDueForHerregistratieReminderAsync(asOf)` — the sweep's candidate set, filtered on the aggregate's own rule (no duplicated policy).
- **Application:** `HerregistratieReminderSweep` — pure over the store + an injected `TimeProvider`; flags + persists each due inscription, returns the reminded ids.
- **Infra/API:** `HerregistratieReminderJob` (Quartz `IJob`) fires the sweep on a daily cron (03:00, overridable via `Quartz__Cron`) and logs the count. `GET /registrations/{id}` surfaces `herregistratieVoor` + `herregistratieReminderVerstuurd`.
**Decisions (both raised with you before coding):** use Quartz.NET as the PRD names it — a genuine cron concern, distinct from the queue-draining pumps, which stay as-is (**ADR-0022**, proposal #120); and the reminder's observable effect is a flag on the aggregate + a log line (no outbound notification infra in v1). No coupling rule (§8) is touched — Quartz is internal to the Domain Service.
Closes #18
Closes #120
## Definition of Done
- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation (red→green per layer: domain rule, store query, sweep).
- [x] Implementation makes the test pass; refactor commit for the 90-day knob.
- [x] Conventional Commits referencing the issue (`refs #18`).
- [ ] CI green — awaiting Gitea Actions.
- [ ] `docker compose up` reaches green health checks within 3 minutes — API boots locally with Quartz initialised; verified in CI compose smoke.
- [x] Docs updated — ADR-0022, demo-script, BACKLOG.
- [x] ADR added — `docs/architecture/adr-0022-quartz-scheduler.md`.
- [x] Demo note in `docs/demo-script.md`.
## Notes for reviewers
- **Ripple:** `Approve()` gained the inscription moment, so the two approving handlers (`ApproveRegistration`, `BeoordeelRegistratie`) now take an injected `TimeProvider`; existing tests pass a fixed clock. All three `IRegistrationStore` implementers (prod, unit fake, acceptance) got the new query.
- **Calibration knobs:** validity (5y) and reminder lead time (90d) are domain constants marked with `ponytail:` comments; promotion path to beheer config (S-15) noted in the ADR.
- **Mutation:** the Quartz job shell is excluded from Stryker, mirroring the pumps; all rule/sweep/query logic is covered.
- Local: 152 domain unit tests green; API boots with the Quartz scheduler and `/health` green.
Reviewed-on: #121
This commit was merged in pull request #121.
This commit is contained in:
@@ -92,11 +92,12 @@ public sealed class Registration
|
||||
|
||||
/// <summary>
|
||||
/// Approve the registration — the behandelaar's decision to enter it in the register. Advances a
|
||||
/// submitted or in-behandeling registration to <see cref="RegistrationStatus.Ingeschreven"/>.
|
||||
/// Requires an opened zaak (the approval sets that zaak's status via the ACL); a registration that
|
||||
/// has already been decided cannot be approved again.
|
||||
/// submitted or in-behandeling registration to <see cref="RegistrationStatus.Ingeschreven"/> and
|
||||
/// records <paramref name="ingeschrevenOp"/> as the moment of inscription, which starts the
|
||||
/// herregistratie clock (S-17). Requires an opened zaak (the approval sets that zaak's status via
|
||||
/// the ACL); a registration that has already been decided cannot be approved again.
|
||||
/// </summary>
|
||||
public void Approve()
|
||||
public void Approve(DateTimeOffset ingeschrevenOp)
|
||||
{
|
||||
if (ZaakUrl is null)
|
||||
throw new InvalidOperationException(
|
||||
@@ -104,6 +105,54 @@ public sealed class Registration
|
||||
|
||||
RequireOpenForDecision(nameof(Approve));
|
||||
Status = RegistrationStatus.Ingeschreven;
|
||||
IngeschrevenOp = ingeschrevenOp;
|
||||
}
|
||||
|
||||
// --- Herregistratie (S-17) — RED stubs, implemented in the green commit ---------------------
|
||||
|
||||
/// <summary>How long a BIG inscription stays valid before herregistratie is required.</summary>
|
||||
// ponytail: fixed 5-year term — a calibration knob, not a config surface. If a demo needs it
|
||||
// per-catalogus, promote it to policy passed in from the beheer config (S-15).
|
||||
public static readonly TimeSpan HerregistratieGeldigheid = TimeSpan.FromDays(365 * 5);
|
||||
|
||||
/// <summary>How long before the deadline the herregistratie reminder is sent (S-17: 90 days).</summary>
|
||||
// ponytail: fixed 90-day lead time — calibration knob; same promotion path as HerregistratieGeldigheid.
|
||||
public static readonly TimeSpan Herinneringstermijn = TimeSpan.FromDays(90);
|
||||
|
||||
/// <summary>When the registration was entered in the register, once approved; the start of its
|
||||
/// herregistratie clock. Null until it is <see cref="RegistrationStatus.Ingeschreven"/>.</summary>
|
||||
public DateTimeOffset? IngeschrevenOp { get; private set; }
|
||||
|
||||
/// <summary>The date by which herregistratie must happen: inscription + validity. Null until
|
||||
/// inscribed.</summary>
|
||||
public DateTimeOffset? HerregistratieVoor =>
|
||||
IngeschrevenOp is DateTimeOffset ingeschrevenOp ? ingeschrevenOp + HerregistratieGeldigheid : null;
|
||||
|
||||
/// <summary>Whether the herregistratie reminder has been sent for this inscription (S-17).</summary>
|
||||
public bool HerregistratieReminderVerstuurd { get; private set; }
|
||||
|
||||
/// <summary>Whether, as of <paramref name="asOf"/>, this registration is due a herregistratie
|
||||
/// reminder: it is inscribed, the reminder window before its deadline has opened, and it has not
|
||||
/// already been reminded. Once inside the window it stays due until reminded (an overdue inscription
|
||||
/// is still due). This is the single rule the store query and the sweep both build on.</summary>
|
||||
public bool HerregistratieReminderDue(DateTimeOffset asOf) =>
|
||||
Status == RegistrationStatus.Ingeschreven
|
||||
&& !HerregistratieReminderVerstuurd
|
||||
&& IngeschrevenOp is DateTimeOffset ingeschrevenOp
|
||||
&& asOf >= ingeschrevenOp + HerregistratieGeldigheid - Herinneringstermijn;
|
||||
|
||||
/// <summary>Record that the herregistratie reminder has been sent. Idempotent — a re-sweep is a
|
||||
/// no-op (§8.6); only an inscribed registration can be reminded.</summary>
|
||||
public void MarkHerregistratieReminderVerstuurd()
|
||||
{
|
||||
if (HerregistratieReminderVerstuurd)
|
||||
return;
|
||||
|
||||
if (Status != RegistrationStatus.Ingeschreven)
|
||||
throw new InvalidOperationException(
|
||||
$"Registration {Id} is {Status}; only an INGESCHREVEN registration can be sent a herregistratie reminder.");
|
||||
|
||||
HerregistratieReminderVerstuurd = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user