diff --git a/services/domain/Big.Application/HerregistratieReminderSweep.cs b/services/domain/Big.Application/HerregistratieReminderSweep.cs new file mode 100644 index 0000000..ddc3d82 --- /dev/null +++ b/services/domain/Big.Application/HerregistratieReminderSweep.cs @@ -0,0 +1,18 @@ +using Big.Domain; + +namespace Big.Application; + +/// +/// The herregistratie reminder sweep (S-17): find the inscriptions whose herregistratie deadline is +/// within the reminder window and have not yet been reminded, mark each reminded, and persist it. Pure +/// application logic over ports — it knows nothing of Quartz; the scheduled job that fires it on a cron +/// lives in Infrastructure (mirroring how the pumps' processors are pure and the pump is the shell). +/// Idempotent: drops an inscription from +/// the next sweep's candidate set, so a re-fire reminds no one twice. Returns the reminded ids so the +/// caller can observe the sweep's effect — the reminder itself is the flag persisted on the aggregate. +/// +public sealed class HerregistratieReminderSweep(IRegistrationStore store, TimeProvider clock) +{ + public Task> SweepAsync(CancellationToken ct = default) + => Task.FromResult>([]); // RED stub +} diff --git a/services/domain/Big.Tests/Fakes.cs b/services/domain/Big.Tests/Fakes.cs index 623c331..f740b96 100644 --- a/services/domain/Big.Tests/Fakes.cs +++ b/services/domain/Big.Tests/Fakes.cs @@ -90,6 +90,13 @@ internal sealed class FakeUserTaskClient(IReadOnlyList open) : } } +/// A pinned to a fixed instant, so time-based use cases (the +/// herregistratie sweep, S-17) are deterministic without the TimeProvider.Testing package. +internal sealed class FixedClock(DateTimeOffset now) : TimeProvider +{ + public override DateTimeOffset GetUtcNow() => now; +} + /// A fake ACL client that records the bsn it was asked to open a zaak for and returns a /// fixed zaak URL. internal sealed class FakeAclClient(Uri? zaakUrl = null) : IAclClient diff --git a/services/domain/Big.Tests/HerregistratieReminderSweepTests.cs b/services/domain/Big.Tests/HerregistratieReminderSweepTests.cs new file mode 100644 index 0000000..e2d3857 --- /dev/null +++ b/services/domain/Big.Tests/HerregistratieReminderSweepTests.cs @@ -0,0 +1,67 @@ +using Big.Application; +using Big.Domain; + +namespace Big.Tests; + +// S-17 (#18): the sweep behind the Quartz job. It reminds every inscription whose herregistratie +// reminder is due, marks each so a re-fire is a no-op (§8.6), and returns the reminded ids. Pure over +// the store + an injected clock — no Quartz here. +public class HerregistratieReminderSweepTests +{ + private static readonly DateTimeOffset Now = new(2026, 7, 23, 0, 0, 0, TimeSpan.Zero); + + private static Registration Inscribed(string bsn, DateTimeOffset ingeschrevenOp) + { + var registration = Registration.Submit(bsn); + registration.AttachZaak(FakeAclClient.DefaultZaakUrl); + registration.Approve(ingeschrevenOp); + return registration; + } + + // Inscribed exactly (geldigheid - herinneringstermijn) before Now: the reminder window is open. + private static Registration Due(string bsn) + => Inscribed(bsn, Now - Registration.HerregistratieGeldigheid + Registration.Herinneringstermijn); + + [Fact] + public async Task Reminds_and_persists_every_due_inscription_and_returns_their_ids() + { + var store = new FakeRegistrationStore(); + var a = Due("123456782"); + var b = Due("111111110"); + var freshlyInscribed = Inscribed("222222222", Now); // not yet in the window + store.Seed(a); + store.Seed(b); + store.Seed(freshlyInscribed); + + var reminded = await new HerregistratieReminderSweep(store, new FixedClock(Now)).SweepAsync(); + + Assert.Equal(new HashSet { a.Id, b.Id }, reminded.ToHashSet()); + Assert.True((await store.GetAsync(a.Id))!.HerregistratieReminderVerstuurd); + Assert.True((await store.GetAsync(b.Id))!.HerregistratieReminderVerstuurd); + Assert.False((await store.GetAsync(freshlyInscribed.Id))!.HerregistratieReminderVerstuurd); + Assert.Equal(2, store.SaveCount); + } + + [Fact] + public async Task A_second_sweep_reminds_no_one_again() + { + var store = new FakeRegistrationStore(); + store.Seed(Due("123456782")); + var sweep = new HerregistratieReminderSweep(store, new FixedClock(Now)); + + await sweep.SweepAsync(); + var second = await sweep.SweepAsync(); + + Assert.Empty(second); + Assert.Equal(1, store.SaveCount); // only the first sweep persisted anything + } + + [Fact] + public async Task Reminds_no_one_when_nothing_is_due() + { + var store = new FakeRegistrationStore(); + store.Seed(Inscribed("123456782", Now)); // freshly inscribed — deadline is 5 years off + + Assert.Empty(await new HerregistratieReminderSweep(store, new FixedClock(Now)).SweepAsync()); + } +}