From 1d09efc3d4e299998f3de14fd32df225f0c9f17d Mon Sep 17 00:00:00 2001
From: Matt Gros <3311227+mpge@users.noreply.github.com>
Date: Fri, 24 Apr 2026 03:23:15 -0400
Subject: [PATCH] feat(workflow): add WorkflowExecutorService for action
dispatch
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Ports the NestJS workflow-executor.service.ts to Spring. The existing
WorkflowEngine only evaluates conditions; this service parses the JSON
action array on Workflow.actions and dispatches each entry against the
relevant repository.
Action catalog: change_priority, change_status, assign_agent,
set_department, add_tag, remove_tag, add_note, insert_canned_reply.
{{field}} placeholders in canned replies are interpolated via
WorkflowEngine.interpolateVariables.
One failing action does not halt the others — mirrors the NestJS
reference. Unknown action types warn-log and skip.
Follow-up PR will add the WorkflowRunner (loads matching workflows,
evaluates, writes WorkflowLog) and the event listener that bridges
TicketEvent into processEvent.
---
.../services/WorkflowExecutorService.java | 262 +++++++++++++++
.../services/WorkflowExecutorServiceTest.java | 298 ++++++++++++++++++
2 files changed, 560 insertions(+)
create mode 100644 src/main/java/dev/escalated/services/WorkflowExecutorService.java
create mode 100644 src/test/java/dev/escalated/services/WorkflowExecutorServiceTest.java
diff --git a/src/main/java/dev/escalated/services/WorkflowExecutorService.java b/src/main/java/dev/escalated/services/WorkflowExecutorService.java
new file mode 100644
index 0000000..d0af6e7
--- /dev/null
+++ b/src/main/java/dev/escalated/services/WorkflowExecutorService.java
@@ -0,0 +1,262 @@
+package dev.escalated.services;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.escalated.models.AgentProfile;
+import dev.escalated.models.Department;
+import dev.escalated.models.Reply;
+import dev.escalated.models.Tag;
+import dev.escalated.models.Ticket;
+import dev.escalated.models.TicketPriority;
+import dev.escalated.models.TicketStatus;
+import dev.escalated.repositories.AgentProfileRepository;
+import dev.escalated.repositories.DepartmentRepository;
+import dev.escalated.repositories.ReplyRepository;
+import dev.escalated.repositories.TagRepository;
+import dev.escalated.repositories.TicketRepository;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+/**
+ * Performs the side-effects dictated by a matched {@code Workflow}.
+ *
+ *
Distinct from {@link WorkflowEngine}, which only evaluates
+ * conditions. This service parses the JSON action array stored on
+ * {@code Workflow.actions} and dispatches each entry against the
+ * relevant repository.
+ *
+ *
Action catalog: {@code change_priority}, {@code change_status},
+ * {@code assign_agent}, {@code set_department}, {@code add_tag},
+ * {@code remove_tag}, {@code add_note}, {@code insert_canned_reply}.
+ * Mirrors the NestJS reference impl in
+ * {@code escalated-nestjs/src/services/workflow-executor.service.ts}.
+ *
+ *
Unknown or malformed actions are logged at {@code warn} and
+ * skipped — one bad action never halts execution of the other
+ * actions on the same workflow.
+ */
+@Service
+public class WorkflowExecutorService {
+
+ private static final Logger log = LoggerFactory.getLogger(WorkflowExecutorService.class);
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private final TicketRepository ticketRepository;
+ private final TagRepository tagRepository;
+ private final AgentProfileRepository agentRepository;
+ private final DepartmentRepository departmentRepository;
+ private final ReplyRepository replyRepository;
+
+ public WorkflowExecutorService(
+ TicketRepository ticketRepository,
+ TagRepository tagRepository,
+ AgentProfileRepository agentRepository,
+ DepartmentRepository departmentRepository,
+ ReplyRepository replyRepository) {
+ this.ticketRepository = ticketRepository;
+ this.tagRepository = tagRepository;
+ this.agentRepository = agentRepository;
+ this.departmentRepository = departmentRepository;
+ this.replyRepository = replyRepository;
+ }
+
+ /**
+ * Execute every action in {@code actionsJson} against {@code ticket}.
+ * Returns the list of parsed action maps so callers (e.g. the
+ * runner) can serialize them into a {@code WorkflowLog} audit row.
+ *
+ * @param actionsJson the JSON string stored on {@code Workflow.actions}
+ * @return parsed actions (never null; empty on malformed input)
+ */
+ public List