feat(workflow): user-task client for beoordeling — werkbak, claim, complete (refs #13)
Adds the IUserTaskClient port and implements it on the Workflow Client (the only code that talks to Flowable, §8.2): query open Beoordelen tasks (werkbak) with their registrationId, claim a task for a behandelaar, and complete it carrying the besluit into the process. Registered in DI for later wiring (S-12c). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ builder.Services.AddSingleton<IRegistrationStore, InMemoryRegistrationStore>();
|
|||||||
builder.Services.AddHttpClient<FlowableWorkflowClient>();
|
builder.Services.AddHttpClient<FlowableWorkflowClient>();
|
||||||
builder.Services.AddTransient<IWorkflowClient>(sp => sp.GetRequiredService<FlowableWorkflowClient>());
|
builder.Services.AddTransient<IWorkflowClient>(sp => sp.GetRequiredService<FlowableWorkflowClient>());
|
||||||
builder.Services.AddTransient<IExternalWorkerClient>(sp => sp.GetRequiredService<FlowableWorkflowClient>());
|
builder.Services.AddTransient<IExternalWorkerClient>(sp => sp.GetRequiredService<FlowableWorkflowClient>());
|
||||||
|
builder.Services.AddTransient<IUserTaskClient>(sp => sp.GetRequiredService<FlowableWorkflowClient>());
|
||||||
builder.Services.AddHttpClient<IAclClient, AclHttpClient>();
|
builder.Services.AddHttpClient<IAclClient, AclHttpClient>();
|
||||||
|
|
||||||
builder.Services.AddScoped<SubmitRegistration>();
|
builder.Services.AddScoped<SubmitRegistration>();
|
||||||
|
|||||||
@@ -36,6 +36,30 @@ public interface IAclClient
|
|||||||
Task ApproveZaakAsync(Uri zaakUrl, CancellationToken ct = default);
|
Task ApproveZaakAsync(Uri zaakUrl, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The port to the behandelaar's user tasks in the workflow engine (S-12). Implemented by the
|
||||||
|
/// Workflow Client — the only code that talks to Flowable (§8.2). The application lists the open
|
||||||
|
/// <c>Beoordelen</c> tasks (the werkbak), claims one for a behandelaar, and completes it with the
|
||||||
|
/// decision; it never names Flowable's REST shapes.
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserTaskClient
|
||||||
|
{
|
||||||
|
/// <summary>The werkbak: the <c>Beoordelen</c> tasks awaiting a behandelaar, each with the
|
||||||
|
/// registration it belongs to.</summary>
|
||||||
|
Task<IReadOnlyList<BeoordelingTask>> GetOpenBeoordelingenAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>Claim a beoordeling task for a behandelaar (assigns it to them).</summary>
|
||||||
|
Task ClaimAsync(string taskId, string behandelaar, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>Complete a beoordeling task, carrying the decision into the process as the
|
||||||
|
/// <c>besluit</c> variable so the workflow can continue on the chosen branch.</summary>
|
||||||
|
Task CompleteBeoordelingAsync(string taskId, BeoordelingsBesluit besluit, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A <c>Beoordelen</c> user task in the werkbak: the Flowable task id (needed to claim and
|
||||||
|
/// complete it) and the registration it carries as a process variable.</summary>
|
||||||
|
public sealed record BeoordelingTask(string TaskId, RegistrationId RegistrationId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Persistence port for the <see cref="Registration"/> aggregate. In-memory for the minimal slice
|
/// Persistence port for the <see cref="Registration"/> aggregate. In-memory for the minimal slice
|
||||||
/// (ADR-0009); an EF-backed store is a documented follow-up, and this port keeps that change additive.
|
/// (ADR-0009); an EF-backed store is a documented follow-up, and this port keeps that change additive.
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ namespace Big.Infrastructure;
|
|||||||
/// The REST contract here is the one verified against a live flowable-rest engine (ADR-0009).
|
/// The REST contract here is the one verified against a live flowable-rest engine (ADR-0009).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions options)
|
public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions options)
|
||||||
: IWorkflowClient, IExternalWorkerClient
|
: IWorkflowClient, IExternalWorkerClient, IUserTaskClient
|
||||||
{
|
{
|
||||||
private const string Topic = "OpenZaakAanmaken";
|
private const string Topic = "OpenZaakAanmaken";
|
||||||
private const string ProcessDefinitionKey = "registratie";
|
private const string ProcessDefinitionKey = "registratie";
|
||||||
|
private const string BeoordelenTaskKey = "Beoordelen";
|
||||||
private const string RegistrationIdVariable = "registrationId";
|
private const string RegistrationIdVariable = "registrationId";
|
||||||
private const string ZaakUrlVariable = "zaakUrl";
|
private const string ZaakUrlVariable = "zaakUrl";
|
||||||
|
private const string BesluitVariable = "besluit";
|
||||||
|
|
||||||
public async Task<string> StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default)
|
public async Task<string> StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -55,6 +57,34 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti
|
|||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<BeoordelingTask>> GetOpenBeoordelingenAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var request = new TaskQueryRequest(ProcessDefinitionKey, BeoordelenTaskKey, IncludeProcessVariables: true);
|
||||||
|
|
||||||
|
var page = await PostAsync<TaskQueryRequest, TaskQueryResult>(
|
||||||
|
"service/runtime/tasks/query", request, ct);
|
||||||
|
|
||||||
|
var tasks = page?.Data ?? [];
|
||||||
|
return [.. tasks.Select(t => new BeoordelingTask(t.Id, RegistrationId.Parse(t.RegistrationId())))];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClaimAsync(string taskId, string behandelaar, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var response = await SendAsync(
|
||||||
|
$"service/runtime/tasks/{taskId}", new ClaimTaskRequest("claim", behandelaar), ct);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CompleteBeoordelingAsync(string taskId, BeoordelingsBesluit besluit, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var request = new CompleteTaskRequest(
|
||||||
|
"complete",
|
||||||
|
[new Variable(BesluitVariable, "string", besluit.ToString().ToLowerInvariant())]);
|
||||||
|
|
||||||
|
using var response = await SendAsync($"service/runtime/tasks/{taskId}", request, ct);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<TResponse?> PostAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken ct)
|
private async Task<TResponse?> PostAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken ct)
|
||||||
{
|
{
|
||||||
using var response = await SendAsync(path, body, ct);
|
using var response = await SendAsync(path, body, ct);
|
||||||
@@ -89,6 +119,32 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti
|
|||||||
[property: JsonPropertyName("workerId")] string WorkerId,
|
[property: JsonPropertyName("workerId")] string WorkerId,
|
||||||
[property: JsonPropertyName("variables")] IReadOnlyList<Variable> Variables);
|
[property: JsonPropertyName("variables")] IReadOnlyList<Variable> Variables);
|
||||||
|
|
||||||
|
private sealed record TaskQueryRequest(
|
||||||
|
[property: JsonPropertyName("processDefinitionKey")] string ProcessDefinitionKey,
|
||||||
|
[property: JsonPropertyName("taskDefinitionKey")] string TaskDefinitionKey,
|
||||||
|
[property: JsonPropertyName("includeProcessVariables")] bool IncludeProcessVariables);
|
||||||
|
|
||||||
|
private sealed record ClaimTaskRequest(
|
||||||
|
[property: JsonPropertyName("action")] string Action,
|
||||||
|
[property: JsonPropertyName("assignee")] string Assignee);
|
||||||
|
|
||||||
|
private sealed record CompleteTaskRequest(
|
||||||
|
[property: JsonPropertyName("action")] string Action,
|
||||||
|
[property: JsonPropertyName("variables")] IReadOnlyList<Variable> Variables);
|
||||||
|
|
||||||
|
private sealed record TaskQueryResult(
|
||||||
|
[property: JsonPropertyName("data")] IReadOnlyList<UserTaskDto>? Data);
|
||||||
|
|
||||||
|
private sealed record UserTaskDto(
|
||||||
|
[property: JsonPropertyName("id")] string Id,
|
||||||
|
[property: JsonPropertyName("processVariables")] IReadOnlyList<Variable>? ProcessVariables)
|
||||||
|
{
|
||||||
|
/// <summary>The registration id this task carries as a process variable.</summary>
|
||||||
|
public string RegistrationId() =>
|
||||||
|
ProcessVariables?.SingleOrDefault(v => v.Name == "registrationId")?.Value
|
||||||
|
?? throw new InvalidOperationException($"Beoordelen task {Id} carries no registrationId variable.");
|
||||||
|
}
|
||||||
|
|
||||||
private sealed record Variable(
|
private sealed record Variable(
|
||||||
[property: JsonPropertyName("name")] string Name,
|
[property: JsonPropertyName("name")] string Name,
|
||||||
[property: JsonPropertyName("type")] string Type,
|
[property: JsonPropertyName("type")] string Type,
|
||||||
|
|||||||
Reference in New Issue
Block a user