feat: initial approval workflow engine with in-memory and JDBC storage

Lightweight linear approval engine (v0.1):

- ordo-api: domain model, OrdoEngine port, repository SPI with
  conditional updates (insertIfAbsent, completeIfPending) and a
  TransactionExecutor port for atomic multi-step writes
- ordo-core: DefaultOrdoEngine running register/start/approve/reject
  inside a transaction boundary; in-memory engine and repositories
- ordo-storage-jdbc: thread-bound JDBC transactions, normalized V1
  schema migration, Jackson-based ProcessContext JSON codec
- tests: unit tests plus H2 integration tests; PostgreSQL integration
  tests run against a local instance via ordo.test.pg.* properties and
  skip when unreachable

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
0264408
2026-09-08 16:56:04 +08:00
co-authored by Claude Code
commit 397a9c229b
58 changed files with 2779 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jetlumen</groupId>
<artifactId>ordo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>ordo-core</artifactId>
<dependencies>
<dependency>
<groupId>com.jetlumen</groupId>
<artifactId>ordo-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,200 @@
package com.jetlumen.ordo.core;
import com.jetlumen.ordo.api.ApprovalStep;
import com.jetlumen.ordo.api.ApprovalTask;
import com.jetlumen.ordo.api.AssigneeResolver;
import com.jetlumen.ordo.api.OrdoEngine;
import com.jetlumen.ordo.api.ProcessContext;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.ProcessInstance;
import com.jetlumen.ordo.api.ProcessStatus;
import com.jetlumen.ordo.api.TaskAction;
import com.jetlumen.ordo.api.TaskStatus;
import com.jetlumen.ordo.api.TransactionExecutor;
import com.jetlumen.ordo.api.exception.DefinitionAlreadyExistsException;
import com.jetlumen.ordo.api.exception.DefinitionNotFoundException;
import com.jetlumen.ordo.api.exception.TaskAlreadyCompletedException;
import com.jetlumen.ordo.api.exception.TaskNotFoundException;
import com.jetlumen.ordo.api.exception.UnauthorizedTaskOperationException;
import com.jetlumen.ordo.api.repository.ApprovalTaskRepository;
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
import com.jetlumen.ordo.api.repository.ProcessInstanceRepository;
import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
/**
* Repository-backed implementation of the v0.1 linear approval runtime.
* Multistep write operations run inside a {@link TransactionExecutor} and
* rely on conditional repository updates, so the engine stays correct even
* when several JVMs share the same storage.
*/
public final class DefaultOrdoEngine implements OrdoEngine {
private final Clock clock;
private final AssigneeResolver assigneeResolver;
private final TransactionExecutor transactionExecutor;
private final ProcessDefinitionRepository definitionRepository;
private final ProcessInstanceRepository instanceRepository;
private final ApprovalTaskRepository taskRepository;
public DefaultOrdoEngine(Clock clock, AssigneeResolver assigneeResolver, TransactionExecutor transactionExecutor,
ProcessDefinitionRepository definitionRepository,
ProcessInstanceRepository instanceRepository,
ApprovalTaskRepository taskRepository) {
this.clock = Objects.requireNonNull(clock, "clock must not be null");
this.assigneeResolver = Objects.requireNonNull(assigneeResolver, "assigneeResolver must not be null");
this.transactionExecutor = Objects.requireNonNull(transactionExecutor, "transactionExecutor must not be null");
this.definitionRepository = Objects.requireNonNull(definitionRepository, "definitionRepository must not be null");
this.instanceRepository = Objects.requireNonNull(instanceRepository, "instanceRepository must not be null");
this.taskRepository = Objects.requireNonNull(taskRepository, "taskRepository must not be null");
}
@Override
public synchronized void register(ProcessDefinition definition) {
Objects.requireNonNull(definition, "definition must not be null");
transactionExecutor.execute(() -> {
if (!definitionRepository.insertIfAbsent(definition)) {
throw new DefinitionAlreadyExistsException(definition.id());
}
return null;
});
}
@Override
public synchronized ProcessInstance start(String definitionId, String initiator, ProcessContext context) {
requireText(initiator, "initiator");
Objects.requireNonNull(context, "context must not be null");
return transactionExecutor.execute(() -> {
ProcessDefinition definition = requireDefinition(definitionId);
Instant now = clock.instant();
ProcessInstance instance = new ProcessInstance(nextId(), definition.id(), initiator,
ProcessStatus.RUNNING, now, null, context);
instanceRepository.insert(instance);
createTask(instance, definition.steps().getFirst(), now);
return instance;
});
}
@Override
public synchronized ApprovalTask approve(String taskId, String actor, String comment) {
return transactionExecutor.execute(() -> {
ApprovalTask task = requirePendingTaskForActor(taskId, actor);
Instant now = clock.instant();
ApprovalTask completedTask = completeTask(task, TaskStatus.APPROVED, new TaskAction(actor, comment, now));
ProcessInstance instance = requireInstance(task.instanceId());
ProcessDefinition definition = requireDefinition(instance.definitionId());
int stepIndex = indexOf(definition, task.stepId());
if (stepIndex == definition.steps().size() - 1) {
completeInstance(instance, ProcessStatus.APPROVED, now);
} else {
createTask(instance, definition.steps().get(stepIndex + 1), now);
}
return completedTask;
});
}
@Override
public synchronized ApprovalTask reject(String taskId, String actor, String comment) {
return transactionExecutor.execute(() -> {
ApprovalTask task = requirePendingTaskForActor(taskId, actor);
Instant now = clock.instant();
ApprovalTask completedTask = completeTask(task, TaskStatus.REJECTED, new TaskAction(actor, comment, now));
completeInstance(requireInstance(task.instanceId()), ProcessStatus.REJECTED, now);
return completedTask;
});
}
@Override
public synchronized Optional<ProcessInstance> findInstance(String instanceId) {
return instanceRepository.findById(instanceId);
}
@Override
public synchronized Optional<ApprovalTask> findTask(String taskId) {
return taskRepository.findById(taskId);
}
@Override
public synchronized List<ApprovalTask> findTasks(String instanceId) {
return taskRepository.findByInstanceId(instanceId);
}
@Override
public synchronized List<ApprovalTask> findPendingTasksByAssignee(String assignee) {
requireText(assignee, "assignee");
return taskRepository.findPendingByAssignee(assignee);
}
@Override
public synchronized List<ApprovalTask> findPendingTasksByInstanceId(String instanceId) {
requireText(instanceId, "instance id");
return taskRepository.findPendingByInstanceId(instanceId);
}
private void createTask(ProcessInstance instance, ApprovalStep step, Instant now) {
String assignee = assigneeResolver.resolve(step, instance.context());
requireText(assignee, "resolved assignee");
taskRepository.save(new ApprovalTask(nextId(), instance.id(), step.id(), step.name(), assignee,
TaskStatus.PENDING, now, null, null));
}
private ApprovalTask requirePendingTaskForActor(String taskId, String actor) {
requireText(actor, "actor");
ApprovalTask task = taskRepository.findById(taskId)
.orElseThrow(() -> new TaskNotFoundException(taskId));
if (task.status() != TaskStatus.PENDING) {
throw new TaskAlreadyCompletedException(taskId);
}
if (!task.assignee().equals(actor)) {
throw new UnauthorizedTaskOperationException(taskId, actor);
}
return task;
}
private ApprovalTask completeTask(ApprovalTask task, TaskStatus status, TaskAction action) {
ApprovalTask completed = new ApprovalTask(task.id(), task.instanceId(), task.stepId(), task.name(),
task.assignee(), status, task.createdAt(), action.operatedAt(), action);
if (!taskRepository.completeIfPending(completed)) {
throw new TaskAlreadyCompletedException(task.id());
}
return completed;
}
private void completeInstance(ProcessInstance instance, ProcessStatus status, Instant now) {
instanceRepository.update(new ProcessInstance(instance.id(), instance.definitionId(), instance.initiator(),
status, instance.startedAt(), now, instance.context()));
}
private ProcessDefinition requireDefinition(String definitionId) {
return definitionRepository.findById(definitionId)
.orElseThrow(() -> new DefinitionNotFoundException(definitionId));
}
private ProcessInstance requireInstance(String instanceId) {
return instanceRepository.findById(instanceId)
.orElseThrow(() -> new IllegalStateException("instance not found: " + instanceId));
}
private static int indexOf(ProcessDefinition definition, String stepId) {
for (int index = 0; index < definition.steps().size(); index++) {
if (definition.steps().get(index).id().equals(stepId)) {
return index;
}
}
throw new IllegalStateException("step not found in definition: " + stepId);
}
private static String nextId() {
return UUID.randomUUID().toString();
}
private static void requireText(String value, String name) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " must not be blank");
}
}
}
@@ -0,0 +1,84 @@
package com.jetlumen.ordo.core;
import com.jetlumen.ordo.api.ApprovalTask;
import com.jetlumen.ordo.api.AssigneeResolver;
import com.jetlumen.ordo.api.OrdoEngine;
import com.jetlumen.ordo.api.ProcessContext;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.ProcessInstance;
import com.jetlumen.ordo.core.repository.InMemoryApprovalTaskRepository;
import com.jetlumen.ordo.core.repository.InMemoryProcessDefinitionRepository;
import com.jetlumen.ordo.core.repository.InMemoryProcessInstanceRepository;
import java.time.Clock;
import java.util.List;
import java.util.Optional;
/** Development and test engine backed by the in-memory repository implementations. */
public final class InMemoryOrdoEngine implements OrdoEngine {
private final DefaultOrdoEngine delegate;
public InMemoryOrdoEngine() {
this(Clock.systemUTC(), AssigneeResolver.direct());
}
public InMemoryOrdoEngine(Clock clock) {
this(clock, AssigneeResolver.direct());
}
public InMemoryOrdoEngine(AssigneeResolver assigneeResolver) {
this(Clock.systemUTC(), assigneeResolver);
}
public InMemoryOrdoEngine(Clock clock, AssigneeResolver assigneeResolver) {
this.delegate = new DefaultOrdoEngine(clock, assigneeResolver, new NoopTransactionExecutor(),
new InMemoryProcessDefinitionRepository(),
new InMemoryProcessInstanceRepository(),
new InMemoryApprovalTaskRepository());
}
@Override
public void register(ProcessDefinition definition) {
delegate.register(definition);
}
@Override
public ProcessInstance start(String definitionId, String initiator, ProcessContext context) {
return delegate.start(definitionId, initiator, context);
}
@Override
public ApprovalTask approve(String taskId, String actor, String comment) {
return delegate.approve(taskId, actor, comment);
}
@Override
public ApprovalTask reject(String taskId, String actor, String comment) {
return delegate.reject(taskId, actor, comment);
}
@Override
public Optional<ProcessInstance> findInstance(String instanceId) {
return delegate.findInstance(instanceId);
}
@Override
public Optional<ApprovalTask> findTask(String taskId) {
return delegate.findTask(taskId);
}
@Override
public List<ApprovalTask> findTasks(String instanceId) {
return delegate.findTasks(instanceId);
}
@Override
public List<ApprovalTask> findPendingTasksByAssignee(String assignee) {
return delegate.findPendingTasksByAssignee(assignee);
}
@Override
public List<ApprovalTask> findPendingTasksByInstanceId(String instanceId) {
return delegate.findPendingTasksByInstanceId(instanceId);
}
}
@@ -0,0 +1,15 @@
package com.jetlumen.ordo.core;
import com.jetlumen.ordo.api.TransactionExecutor;
import java.util.Objects;
import java.util.function.Supplier;
/** Transaction boundary that runs the action as-is; used by the in-memory engine. */
public final class NoopTransactionExecutor implements TransactionExecutor {
@Override
public <T> T execute(Supplier<T> action) {
Objects.requireNonNull(action, "action must not be null");
return action.get();
}
}
@@ -0,0 +1,56 @@
package com.jetlumen.ordo.core.repository;
import com.jetlumen.ordo.api.ApprovalTask;
import com.jetlumen.ordo.api.TaskStatus;
import com.jetlumen.ordo.api.repository.ApprovalTaskRepository;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/** Development-only in-memory implementation of the task storage port. */
public final class InMemoryApprovalTaskRepository implements ApprovalTaskRepository {
private final Map<String, ApprovalTask> tasks = new LinkedHashMap<>();
@Override
public synchronized void save(ApprovalTask task) {
tasks.put(task.id(), task);
}
@Override
public synchronized Optional<ApprovalTask> findById(String taskId) {
return Optional.ofNullable(tasks.get(taskId));
}
@Override
public synchronized List<ApprovalTask> findByInstanceId(String instanceId) {
return tasks.values().stream().filter(task -> task.instanceId().equals(instanceId)).toList();
}
@Override
public synchronized List<ApprovalTask> findPendingByAssignee(String assignee) {
return tasks.values().stream()
.filter(task -> task.status() == TaskStatus.PENDING)
.filter(task -> task.assignee().equals(assignee))
.toList();
}
@Override
public synchronized List<ApprovalTask> findPendingByInstanceId(String instanceId) {
return tasks.values().stream()
.filter(task -> task.status() == TaskStatus.PENDING)
.filter(task -> task.instanceId().equals(instanceId))
.toList();
}
@Override
public synchronized boolean completeIfPending(ApprovalTask completedTask) {
ApprovalTask current = tasks.get(completedTask.id());
if (current == null || current.status() != TaskStatus.PENDING) {
return false;
}
tasks.put(completedTask.id(), completedTask);
return true;
}
}
@@ -0,0 +1,23 @@
package com.jetlumen.ordo.core.repository;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/** Development-only in-memory implementation of the definition storage port. */
public final class InMemoryProcessDefinitionRepository implements ProcessDefinitionRepository {
private final Map<String, ProcessDefinition> definitions = new HashMap<>();
@Override
public synchronized boolean insertIfAbsent(ProcessDefinition definition) {
return definitions.putIfAbsent(definition.id(), definition) == null;
}
@Override
public synchronized Optional<ProcessDefinition> findById(String definitionId) {
return Optional.ofNullable(definitions.get(definitionId));
}
}
@@ -0,0 +1,28 @@
package com.jetlumen.ordo.core.repository;
import com.jetlumen.ordo.api.ProcessInstance;
import com.jetlumen.ordo.api.repository.ProcessInstanceRepository;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/** Development-only in-memory implementation of the instance storage port. */
public final class InMemoryProcessInstanceRepository implements ProcessInstanceRepository {
private final Map<String, ProcessInstance> instances = new HashMap<>();
@Override
public synchronized void insert(ProcessInstance instance) {
instances.put(instance.id(), instance);
}
@Override
public synchronized void update(ProcessInstance instance) {
instances.put(instance.id(), instance);
}
@Override
public synchronized Optional<ProcessInstance> findById(String instanceId) {
return Optional.ofNullable(instances.get(instanceId));
}
}
@@ -0,0 +1,178 @@
package com.jetlumen.ordo.core;
import com.jetlumen.ordo.api.ApprovalStep;
import com.jetlumen.ordo.api.ApprovalTask;
import com.jetlumen.ordo.api.ProcessContext;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.ProcessStatus;
import com.jetlumen.ordo.api.TaskStatus;
import com.jetlumen.ordo.api.exception.DefinitionAlreadyExistsException;
import com.jetlumen.ordo.api.exception.DefinitionNotFoundException;
import com.jetlumen.ordo.api.exception.TaskAlreadyCompletedException;
import com.jetlumen.ordo.api.exception.TaskNotFoundException;
import com.jetlumen.ordo.api.exception.UnauthorizedTaskOperationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
class InMemoryOrdoEngineTest {
private InMemoryOrdoEngine engine;
@BeforeEach
void setUp() {
engine = new InMemoryOrdoEngine();
engine.register(new ProcessDefinition("leave", "Leave request", List.of(
new ApprovalStep("manager", "Manager approval", "maria"),
new ApprovalStep("hr", "HR approval", "henry")
)));
}
@Test
void completesASequentialApprovalProcess() {
var instance = engine.start("leave", "alice");
ApprovalTask managerTask = engine.findTasks(instance.id()).getFirst();
assertEquals(TaskStatus.APPROVED, engine.approve(managerTask.id(), "maria").status());
ApprovalTask hrTask = engine.findTasks(instance.id()).get(1);
assertEquals("henry", hrTask.assignee());
engine.approve(hrTask.id(), "henry");
assertEquals(ProcessStatus.APPROVED, engine.findInstance(instance.id()).orElseThrow().status());
}
@Test
void rejectionTerminatesTheProcess() {
var instance = engine.start("leave", "alice");
ApprovalTask task = engine.findTasks(instance.id()).getFirst();
ApprovalTask rejectedTask = engine.reject(task.id(), "maria", "Insufficient leave balance");
assertEquals(TaskStatus.REJECTED, rejectedTask.status());
assertEquals("maria", rejectedTask.action().actor());
assertEquals("Insufficient leave balance", rejectedTask.action().comment());
assertEquals(rejectedTask.completedAt(), rejectedTask.action().operatedAt());
assertEquals(ProcessStatus.REJECTED, engine.findInstance(instance.id()).orElseThrow().status());
assertEquals(1, engine.findTasks(instance.id()).size());
assertThrows(TaskAlreadyCompletedException.class, () -> engine.approve(task.id(), "maria"));
}
@Test
void rejectsDuplicateAndUnauthorizedOperations() {
var instance = engine.start("leave", "alice");
ApprovalTask task = engine.findTasks(instance.id()).getFirst();
assertThrows(UnauthorizedTaskOperationException.class, () -> engine.approve(task.id(), "mallory"));
engine.approve(task.id(), "maria");
assertThrows(TaskAlreadyCompletedException.class, () -> engine.approve(task.id(), "maria"));
}
@Test
void exposesSpecificExceptionsForMissingAndDuplicateResources() {
assertThrows(DefinitionNotFoundException.class, () -> engine.start("missing", "alice"));
assertThrows(TaskNotFoundException.class, () -> engine.approve("missing", "maria"));
assertThrows(DefinitionAlreadyExistsException.class, () -> engine.register(new ProcessDefinition(
"leave", "Another leave request", List.of(new ApprovalStep("lead", "Lead approval", "lee")))));
}
@Test
void rejectsBlankRuntimeArguments() {
assertThrows(IllegalArgumentException.class, () -> engine.start("leave", " "));
assertThrows(DefinitionNotFoundException.class, () -> engine.start(" ", "alice"));
var instance = engine.start("leave", "alice");
ApprovalTask task = engine.findTasks(instance.id()).getFirst();
assertThrows(IllegalArgumentException.class, () -> engine.approve(task.id(), " "));
assertThrows(IllegalArgumentException.class, () -> engine.reject(task.id(), " "));
}
@Test
void keepsSeparateInstancesIndependent() {
var aliceInstance = engine.start("leave", "alice");
var bobInstance = engine.start("leave", "bob");
ApprovalTask aliceTask = engine.findTasks(aliceInstance.id()).getFirst();
engine.approve(aliceTask.id(), "maria");
assertEquals(2, engine.findTasks(aliceInstance.id()).size());
assertEquals(1, engine.findTasks(bobInstance.id()).size());
assertEquals(ProcessStatus.RUNNING, engine.findInstance(aliceInstance.id()).orElseThrow().status());
assertEquals(ProcessStatus.RUNNING, engine.findInstance(bobInstance.id()).orElseThrow().status());
assertEquals("maria", engine.findTasks(bobInstance.id()).getFirst().assignee());
assertTrue(engine.findTasks("missing-instance").isEmpty());
}
@Test
void findsOnlyCurrentPendingTasksByAssigneeAndInstance() {
var aliceInstance = engine.start("leave", "alice");
var bobInstance = engine.start("leave", "bob");
ApprovalTask aliceManagerTask = engine.findPendingTasksByInstanceId(aliceInstance.id()).getFirst();
assertEquals(2, engine.findPendingTasksByAssignee("maria").size());
assertEquals(List.of(aliceManagerTask), engine.findPendingTasksByInstanceId(aliceInstance.id()));
assertTrue(engine.findPendingTasksByAssignee("henry").isEmpty());
engine.approve(aliceManagerTask.id(), "maria");
ApprovalTask hrTask = engine.findPendingTasksByInstanceId(aliceInstance.id()).getFirst();
assertEquals("hr", hrTask.stepId());
assertEquals("henry", hrTask.assignee());
assertEquals(1, engine.findPendingTasksByAssignee("maria").size());
assertEquals(List.of(hrTask), engine.findPendingTasksByAssignee("henry"));
engine.reject(hrTask.id(), "henry");
assertTrue(engine.findPendingTasksByInstanceId(aliceInstance.id()).isEmpty());
assertTrue(engine.findPendingTasksByAssignee("henry").isEmpty());
assertEquals(1, engine.findPendingTasksByInstanceId(bobInstance.id()).size());
}
@Test
void recordsApprovalAuditDataAndLeavesPendingTasksWithoutAnAction() {
var instance = engine.start("leave", "alice");
ApprovalTask managerTask = engine.findPendingTasksByInstanceId(instance.id()).getFirst();
assertNull(managerTask.action());
ApprovalTask approvedTask = engine.approve(managerTask.id(), "maria", " Approved for the requested dates. ");
assertEquals("maria", approvedTask.action().actor());
assertEquals("Approved for the requested dates.", approvedTask.action().comment());
assertEquals(approvedTask.completedAt(), approvedTask.action().operatedAt());
assertNotNull(approvedTask.action().operatedAt());
}
@Test
void rejectsBlankPendingTaskQueryArguments() {
assertThrows(IllegalArgumentException.class, () -> engine.findPendingTasksByAssignee(" "));
assertThrows(IllegalArgumentException.class, () -> engine.findPendingTasksByInstanceId(" "));
}
@Test
void resolvesAssigneesFromTheProcessContext() {
InMemoryOrdoEngine contextAwareEngine = new InMemoryOrdoEngine((step, context) -> context.value(step.id())
.filter(String.class::isInstance)
.map(String.class::cast)
.orElse(step.assignee()));
contextAwareEngine.register(new ProcessDefinition("leave", "Leave request", List.of(
new ApprovalStep("manager", "Manager approval", "maria"),
new ApprovalStep("hr", "HR approval", "henry")
)));
var instance = contextAwareEngine.start("leave", "alice", new ProcessContext(Map.of(
"manager", "david",
"hr", "helena"
)));
ApprovalTask managerTask = contextAwareEngine.findPendingTasksByInstanceId(instance.id()).getFirst();
assertEquals("david", managerTask.assignee());
contextAwareEngine.approve(managerTask.id(), "david");
assertEquals("helena", contextAwareEngine.findPendingTasksByInstanceId(instance.id()).getFirst().assignee());
assertEquals("david", instance.context().value("manager").orElseThrow());
}
}