test(domain): herregistratie reminder sweep flags + returns due inscriptions (refs #18)
HerregistratieReminderSweep reminds every due inscription, persists the flag so a re-fire is a no-op, and returns the reminded ids. Stubbed to an empty result so the new tests fail; green implements the sweep. Adds a FixedClock TimeProvider fake so the sweep is deterministic without a testing package. refs #18
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
using Big.Domain;
|
||||||
|
|
||||||
|
namespace Big.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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: <see cref="Registration.MarkHerregistratieReminderVerstuurd"/> 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.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HerregistratieReminderSweep(IRegistrationStore store, TimeProvider clock)
|
||||||
|
{
|
||||||
|
public Task<IReadOnlyList<RegistrationId>> SweepAsync(CancellationToken ct = default)
|
||||||
|
=> Task.FromResult<IReadOnlyList<RegistrationId>>([]); // RED stub
|
||||||
|
}
|
||||||
@@ -90,6 +90,13 @@ internal sealed class FakeUserTaskClient(IReadOnlyList<BeoordelingTask> open) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>A <see cref="TimeProvider"/> pinned to a fixed instant, so time-based use cases (the
|
||||||
|
/// herregistratie sweep, S-17) are deterministic without the TimeProvider.Testing package.</summary>
|
||||||
|
internal sealed class FixedClock(DateTimeOffset now) : TimeProvider
|
||||||
|
{
|
||||||
|
public override DateTimeOffset GetUtcNow() => now;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>A fake ACL client that records the bsn it was asked to open a zaak for and returns a
|
/// <summary>A fake ACL client that records the bsn it was asked to open a zaak for and returns a
|
||||||
/// fixed zaak URL.</summary>
|
/// fixed zaak URL.</summary>
|
||||||
internal sealed class FakeAclClient(Uri? zaakUrl = null) : IAclClient
|
internal sealed class FakeAclClient(Uri? zaakUrl = null) : IAclClient
|
||||||
|
|||||||
@@ -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<RegistrationId> { 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user