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