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());
+ }
+}