From f49cad190081bf31c1402260393e7ebe3c8bc640 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Thu, 23 Jul 2026 11:58:42 +0200 Subject: [PATCH] feat(domain): Quartz cron job fires the herregistratie sweep; expose deadline on read (refs #18) HerregistratieReminderJob (Quartz IJob) fires HerregistratieReminderSweep on a daily cron wired in Big.Api (overridable via Quartz__Cron), and logs how many reminders went out. Quartz.NET is used for this time-triggered fleet sweep, distinct from the queue-draining pumps (ADR-0022). GET /registrations/{id} now returns herregistratieVoor + herregistratieReminderVerstuurd. The scheduling shell is excluded from mutation, mirroring the pumps. refs #18 --- services/domain/Big.Api/Big.Api.csproj | 4 +++ services/domain/Big.Api/Program.cs | 25 ++++++++++++++++-- .../Big.Infrastructure.csproj | 1 + .../HerregistratieReminderJob.cs | 26 +++++++++++++++++++ services/domain/stryker-config.json | 3 ++- .../Steps/EenRegistratieBeoordelenSteps.cs | 2 +- .../acceptance/Support/InMemoryDomainPorts.cs | 5 ++++ 7 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 services/domain/Big.Infrastructure/HerregistratieReminderJob.cs diff --git a/services/domain/Big.Api/Big.Api.csproj b/services/domain/Big.Api/Big.Api.csproj index ae03b53..673337b 100644 --- a/services/domain/Big.Api/Big.Api.csproj +++ b/services/domain/Big.Api/Big.Api.csproj @@ -5,6 +5,10 @@ + + + + net10.0 enable diff --git a/services/domain/Big.Api/Program.cs b/services/domain/Big.Api/Program.cs index 03b8a01..2a2827e 100644 --- a/services/domain/Big.Api/Program.cs +++ b/services/domain/Big.Api/Program.cs @@ -1,6 +1,7 @@ using Big.Application; using Big.Domain; using Big.Infrastructure; +using Quartz; var builder = WebApplication.CreateBuilder(args); @@ -40,6 +41,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // The hosted external-task job worker polls Flowable and drives OpenZaakAanmaken to completion. builder.Services.AddHostedService(); @@ -50,6 +52,19 @@ builder.Services.AddHostedService(); // parks and expires each lapsed registration to VERLOPEN (S-10a, ADR-0017). builder.Services.AddHostedService(); +// The herregistratie reminder sweep runs on a daily cron via Quartz.NET (S-17, ADR-0022) — a +// time-triggered fleet sweep, deliberately a different mechanism from the queue-draining pumps above. +// The cron is overridable with Quartz__Cron; it defaults to 03:00 daily. +builder.Services.AddQuartz(q => +{ + var jobKey = new JobKey("herregistratie-reminder"); + q.AddJob(jobKey); + q.AddTrigger(t => t + .ForJob(jobKey) + .WithCronSchedule(builder.Configuration["Quartz:Cron"] ?? "0 0 3 * * ?")); +}); +builder.Services.AddQuartzHostedService(o => o.WaitForJobsToComplete = true); + var app = builder.Build(); app.MapGet("/health", () => "Healthy"); @@ -169,7 +184,8 @@ app.MapGet("/registrations/{id}", async (string id, IRegistrationStore store, Ca return registration is null ? Results.NotFound() : Results.Ok(new RegistrationResponse( - registration.Id.ToString(), registration.Status.ToString(), registration.ZaakUrl?.ToString())); + registration.Id.ToString(), registration.Status.ToString(), registration.ZaakUrl?.ToString(), + registration.HerregistratieVoor?.ToString("O"), registration.HerregistratieReminderVerstuurd)); }); await app.RunAsync(); @@ -182,6 +198,11 @@ public sealed record WithdrawRequest(string Bsn); public sealed record ProvideDocumentsRequest(string Bsn, string ContentBase64, string? FileName = null, string? ContentType = null); -public sealed record RegistrationResponse(string RegistrationId, string Status, string? ZaakUrl); +public sealed record RegistrationResponse( + string RegistrationId, + string Status, + string? ZaakUrl, + string? HerregistratieVoor = null, + bool HerregistratieReminderVerstuurd = false); public partial class Program; diff --git a/services/domain/Big.Infrastructure/Big.Infrastructure.csproj b/services/domain/Big.Infrastructure/Big.Infrastructure.csproj index 6a23067..379c04e 100644 --- a/services/domain/Big.Infrastructure/Big.Infrastructure.csproj +++ b/services/domain/Big.Infrastructure/Big.Infrastructure.csproj @@ -19,6 +19,7 @@ + diff --git a/services/domain/Big.Infrastructure/HerregistratieReminderJob.cs b/services/domain/Big.Infrastructure/HerregistratieReminderJob.cs new file mode 100644 index 0000000..55933b6 --- /dev/null +++ b/services/domain/Big.Infrastructure/HerregistratieReminderJob.cs @@ -0,0 +1,26 @@ +using Big.Application; +using Microsoft.Extensions.Logging; +using Quartz; + +namespace Big.Infrastructure; + +/// +/// The Quartz job that fires the herregistratie reminder sweep on a cron schedule (S-17, ADR-0022). +/// A deliberately thin shell — it resolves the pure (Quartz's +/// MS-DI job factory gives each fire its own scope) and logs how many reminders went out; all the +/// sweep logic is unit-tested in the application layer. Quartz drives this — rather than a +/// BackgroundService poll loop like the pumps — because it is a time-triggered fleet sweep, not a +/// queue to drain (the distinction recorded in ADR-0022). +/// stops a slow sweep overlapping the next fire against the shared store. +/// +[DisallowConcurrentExecution] +public sealed class HerregistratieReminderJob( + HerregistratieReminderSweep sweep, ILogger logger) : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + var reminded = await sweep.SweepAsync(context.CancellationToken); + logger.LogInformation( + "Herregistratie-sweep voltooid: {Count} herinnering(en) verstuurd.", reminded.Count); + } +} diff --git a/services/domain/stryker-config.json b/services/domain/stryker-config.json index ac22608..3b51fae 100644 --- a/services/domain/stryker-config.json +++ b/services/domain/stryker-config.json @@ -6,7 +6,8 @@ "mutate": [ "!**/OpenZaakJobPump.cs", "!**/BeoordelingEscalatiePump.cs", - "!**/RegistratieVerlopenPump.cs" + "!**/RegistratieVerlopenPump.cs", + "!**/HerregistratieReminderJob.cs" ], "thresholds": { "high": 95, diff --git a/tests/acceptance/Steps/EenRegistratieBeoordelenSteps.cs b/tests/acceptance/Steps/EenRegistratieBeoordelenSteps.cs index 39bfa3b..4c4dd6d 100644 --- a/tests/acceptance/Steps/EenRegistratieBeoordelenSteps.cs +++ b/tests/acceptance/Steps/EenRegistratieBeoordelenSteps.cs @@ -40,7 +40,7 @@ public sealed class EenRegistratieBeoordelenSteps [When("the behandelaar decides \"(.*)\"")] public async Task WhenTheBehandelaarDecides(string besluit) - => await new BeoordeelRegistratie(_store, _acl, _tasks).HandleAsync( + => await new BeoordeelRegistratie(_store, _acl, _tasks, TimeProvider.System).HandleAsync( new BeoordeelRegistratieCommand(_id, Enum.Parse(besluit, ignoreCase: true))); [Then("the registration has status \"(.*)\"")] diff --git a/tests/acceptance/Support/InMemoryDomainPorts.cs b/tests/acceptance/Support/InMemoryDomainPorts.cs index 72165ed..5475964 100644 --- a/tests/acceptance/Support/InMemoryDomainPorts.cs +++ b/tests/acceptance/Support/InMemoryDomainPorts.cs @@ -221,4 +221,9 @@ public sealed class InMemoryRegistrationStore : IRegistrationStore public Task FindOpenByBsnAsync(string bsn, CancellationToken ct = default) => Task.FromResult(_byId.Values.FirstOrDefault(r => r.Bsn == bsn && r.Status is RegistrationStatus.Ingediend or RegistrationStatus.InBehandeling)); + + public Task> FindDueForHerregistratieReminderAsync( + DateTimeOffset asOf, CancellationToken ct = default) + => Task.FromResult>( + _byId.Values.Where(r => r.HerregistratieReminderDue(asOf)).ToList()); }