S-09b: Approval flow — temp admin endpoint + status transition to projection #77
@@ -23,6 +23,7 @@ builder.Services.AddTransient<IExternalWorkerClient>(sp => sp.GetRequiredService
|
||||
builder.Services.AddHttpClient<IAclClient, AclHttpClient>();
|
||||
|
||||
builder.Services.AddScoped<SubmitRegistration>();
|
||||
builder.Services.AddScoped<ApproveRegistration>();
|
||||
builder.Services.AddScoped<OpenZaakWorker>();
|
||||
builder.Services.AddScoped<OpenZaakJobProcessor>();
|
||||
|
||||
@@ -42,6 +43,18 @@ app.MapPost("/registrations", async (SubmitRegistrationRequest body, SubmitRegis
|
||||
return Results.Accepted($"/registrations/{id}", new RegistrationResponse(id.ToString(), RegistrationStatus.Ingediend.ToString(), null));
|
||||
});
|
||||
|
||||
// Temporary admin endpoint (S-09b): approve a registration — the behandelaar's decision, until the
|
||||
// behandel-portal exists (S-12). The zaak's final status is set via the ACL, which flows back over
|
||||
// NRC to the projection, making the entry publicly visible as INGESCHREVEN. Idempotent.
|
||||
app.MapPost("/registrations/{id}/approve", async (string id, ApproveRegistration approve, CancellationToken ct) =>
|
||||
{
|
||||
if (!Guid.TryParse(id, out var guid))
|
||||
return Results.NotFound();
|
||||
|
||||
await approve.HandleAsync(new ApproveRegistrationCommand(new RegistrationId(guid)), ct);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
// Read a registration. Its zaak URL appears once the worker has opened the zaak (eventually).
|
||||
app.MapGet("/registrations/{id}", async (string id, IRegistrationStore store, CancellationToken ct) =>
|
||||
{
|
||||
|
||||
36
services/domain/Big.Application/ApproveRegistration.cs
Normal file
36
services/domain/Big.Application/ApproveRegistration.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Big.Domain;
|
||||
|
||||
namespace Big.Application;
|
||||
|
||||
/// <summary>A behandelaar's decision to approve a registration, in domain language.</summary>
|
||||
public sealed record ApproveRegistrationCommand(RegistrationId RegistrationId);
|
||||
|
||||
/// <summary>
|
||||
/// The approve use case (S-09b): set the registration's zaak to its final status via the ACL (§8.1),
|
||||
/// then advance the aggregate to INGESCHREVEN. Idempotent — a redelivered or repeated approval of an
|
||||
/// already-approved registration is a no-op, so the ACL is not asked to set the status twice. The
|
||||
/// zaak status is the projection's source of truth (it flows back over NRC); the aggregate transition
|
||||
/// keeps the domain's own view consistent.
|
||||
/// </summary>
|
||||
public sealed class ApproveRegistration(IRegistrationStore store, IAclClient acl)
|
||||
{
|
||||
public async Task HandleAsync(ApproveRegistrationCommand command, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
|
||||
var registration = await store.GetAsync(command.RegistrationId, ct)
|
||||
?? throw new InvalidOperationException($"No registration {command.RegistrationId} to approve.");
|
||||
|
||||
// A repeated approval is a no-op: don't set the zaak status a second time.
|
||||
if (registration.Status == RegistrationStatus.Ingeschreven)
|
||||
return;
|
||||
|
||||
if (registration.ZaakUrl is null)
|
||||
throw new InvalidOperationException(
|
||||
$"Registration {command.RegistrationId} has no zaak yet; it cannot be approved.");
|
||||
|
||||
await acl.ApproveZaakAsync(registration.ZaakUrl, ct);
|
||||
registration.Approve();
|
||||
await store.SaveAsync(registration, ct);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,12 @@ public interface IWorkflowClient
|
||||
public interface IAclClient
|
||||
{
|
||||
Task<Uri> OpenZaakAsync(string bsn, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Record the approval on the given zaak. The ACL translates this to the ZGW concept — setting
|
||||
/// the zaak's final status — which OpenZaak notifies over NRC; the domain never names statustypen.
|
||||
/// </summary>
|
||||
Task ApproveZaakAsync(Uri zaakUrl, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -22,7 +22,18 @@ public sealed class AclHttpClient(HttpClient http, AclOptions options) : IAclCli
|
||||
return new Uri(opened.ZaakUrl);
|
||||
}
|
||||
|
||||
public async Task ApproveZaakAsync(Uri zaakUrl, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(zaakUrl);
|
||||
|
||||
using var response = await http.PostAsJsonAsync(
|
||||
new Uri(options.BaseUrl, "statussen"), new SetStatusRequest(zaakUrl.ToString()), ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
private sealed record OpenZaakRequest([property: JsonPropertyName("bsn")] string Bsn);
|
||||
|
||||
private sealed record OpenZaakResponse([property: JsonPropertyName("zaakUrl")] string ZaakUrl);
|
||||
|
||||
private sealed record SetStatusRequest([property: JsonPropertyName("zaakUrl")] string ZaakUrl);
|
||||
}
|
||||
|
||||
@@ -41,4 +41,27 @@ public class AclHttpClientTests
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => client.OpenZaakAsync("123456782"));
|
||||
Assert.Contains("empty", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Approves_a_zaak_by_posting_its_url_to_statussen()
|
||||
{
|
||||
var capture = new RequestCapture();
|
||||
var client = Client(capture.Responds(HttpStatusCode.NoContent));
|
||||
|
||||
await client.ApproveZaakAsync(new Uri("http://openzaak/zaken/api/v1/zaken/abc"));
|
||||
|
||||
Assert.Equal(HttpMethod.Post, capture.Seen!.Method);
|
||||
Assert.Equal("http://acl/statussen", capture.Seen.RequestUri!.ToString());
|
||||
Assert.Contains("\"zaakUrl\":\"http://openzaak/zaken/api/v1/zaken/abc\"", capture.Body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Approve_throws_when_the_acl_rejects_the_request()
|
||||
{
|
||||
var capture = new RequestCapture();
|
||||
var client = Client(capture.Responds(HttpStatusCode.BadGateway));
|
||||
|
||||
await Assert.ThrowsAsync<HttpRequestException>(
|
||||
() => client.ApproveZaakAsync(new Uri("http://openzaak/zaken/api/v1/zaken/abc")));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user