48 tasks across 9 phases (6 user stories + setup/foundational/polish), sequenced US1 (policy definition) -> US2 (calendar-aware run creation) -> US3 (durable pause/resume, P1-complete MVP) -> US4 (breach detection) -> US5 (breach-triggered escalation) -> US6 (manual escalation), each dependent on the last since every story builds on the previous one's mechanism rather than being independently orderable. Also folds in two design refinements found while cross-checking the existing scaffold against research.md's plan: TICKET_ASSIGNED (defined in domain-events.ts since 007, never published) becomes the real wiring point for SLA-run creation, and src/jobs/sla//src/jobs/escalation/ turn out to already exist as their own stub scaffolding, reused rather than duplicated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
21 KiB
description
| description |
|---|
| Task list for 008-sla-escalation |
Tasks: SLA and Escalation
Input: Design documents from specs/008-sla-escalation/
Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/sla-escalation-contract.md, quickstart.md
Tests: Included as first-class tasks. This feature has real, extractable pure logic (the
calendar-walk algorithm, most-specific policy match, breach/no-breach/paused-no-breach logic)
plus — for the first time since the constitution's Principle VII was written — a genuine
process-restart-survival requirement that needs a dedicated test rebuilding buildApp()
mid-test, not just a within-process concurrency test.
Organization: Tasks are grouped by user story (US1 = P1 policy definition, US2 = P1 run creation with calendar-aware due dates, US3 = P1 durable pause/resume, US4 = P2 breach detection, US5 = P2 breach-triggered escalation, US6 = P3 manual escalation).
Format: [ID] [P?] [Story] Description
All file paths are relative to supporthub-api/ (repo root).
Phase 1: Setup
- T001 [P] Populate
src/modules/platform/business-calendars/with the full standard shape (controller/,routes/,schema/,repository/,service/,types/,mapper/,constants/,index.ts) plus acalculators/directory, replacing the existingBusinessCalendarsService.isWorkingHourstub's content - T002 [P] Extend
src/modules/orchestration/sla/to the full standard shape around its existingengine//calculators/directories, replacing every stub file's content (SlaEngine.evaluateSlaTargets,SlaDueDateCalculator.calculateDueTime) - T003 [P] Extend
src/modules/orchestration/escalation/to the full standard shape around its existingengine/directory, replacing theEscalationEngine.triggerEscalationstub's content
Phase 2: Foundational (Blocking Prerequisites)
Purpose: Schema for every entity, shared by every user story.
⚠️ CRITICAL: No user-story stage work can begin until this phase is complete.
- T004 Add
SLAPolicy,SLARun(incl. the additivefirstResponseBreachedAtrefinement),BusinessCalendar,Holiday,EscalationPolicy,EscalationRule,EscalationEventmodels toprisma/schema.prismaper data-model.md, plusTicket.slaRun/Ticket.escalationEvents,Product.slaPolicies/Product.escalationPolicies,Category.slaPolicies,HierarchyNode.escalationRulesback-relations, and anSLARun @@index([status, resolutionDueAt])for the breach-detection sweep (depends on T001-T003) - T005 Run
npm run prisma:generateand create the migration (npm run prisma:migrate) for T004 (depends on T004)
Checkpoint: Schema migrated. User stories can now be built.
Phase 3: User Story 1 - Admin defines SLA policies as configuration (Priority: P1) 🎯 MVP (part 1)
Goal: SLAPolicy CRUD and the most-specific-match resolution function exist and are
independently correct — not yet wired to ticket assignment.
Independent Test: Quickstart Scenario 1.
Tests for User Story 1
- T006 [P] [US1] Unit tests for
findApplicablePolicy(specificity-count match, wildcard handling on each of the 4 scope dimensions independently, tie-break by latestupdatedAt, no-match returnsnull) intests/unit/orchestration/sla-policy-match.test.ts - T007 [US1] Integration test covering Quickstart Scenario 1 (a product-scoped policy is
preferred over a global one; deactivating it falls back to the global policy) against a
real Postgres in
tests/integration/sla-policy-resolution.test.ts(depends on T005)
Implementation for User Story 1
- T008 [US1] Add
SLAPolicyRepository(CRUD,findActiveCandidates(scope)) and the Zod create/update schema — with resolve-or-404 existence checks forproductId/categoryId/businessCalendarIdwhen provided (research.md) — insla/repository/+sla/schema/(depends on T005) - T009 [US1] Add
findApplicablePolicy(ticketContext)(specificity-count + tie-break, per data-model.md's Resolution section) insla/service/sla-policy-resolver.service.ts(depends on T008) - T010 [US1] Add
POST/GET/GET:id/PATCH/DELETE /admin/sla-policiesroutes (soft-delete viaactive: false, gated byfastify.authenticate) insla/controller/+sla/routes/, registered fromsrc/api/routes.ts(depends on T008) - T011 [US1] Run Quickstart Scenario 1 locally and confirm all 4 steps pass
Checkpoint: SLA policies can be defined and correctly resolved. Nothing creates an SLARun
yet — that's User Story 2.
Phase 4: User Story 2 - SLA run starts automatically with calendar-aware due dates (Priority: P1) 🎯 MVP (part 2)
Goal: BusinessCalendar/Holiday CRUD, the calendar-walk algorithm, and SLARun creation
wired into 007's assignment-success path via the first real publish of TICKET_ASSIGNED.
Independent Test: Quickstart Scenario 2.
Tests for User Story 2
- T012 [P] [US2] Unit tests for
addBusinessMinutes— weekend exclusion, holiday exclusion, partial-day clipping on the start day, a day with no configured window contributing zero time, and correctness across a DST transition in the calendar's own timezone — intests/unit/platform/business-calendars/calendar-walk.test.ts - T013 [US2] Integration test covering Quickstart Scenario 2 (calendar-aware due date lands
the next working day past a weekend+holiday, never a naive addition; a ticket assigned with
no matching policy gets no
SLARunandGET .../sla-runreturns404) against a real Postgres intests/integration/sla-run-creation.test.ts(depends on T005, T009, and 007's existing assignment flow)
Implementation for User Story 2
- T014 [US2] Add
addBusinessMinutes(start, minutes, calendar, holidays)usingluxoninbusiness-calendars/calculators/business-hours.calculator.ts, replacing theisWorkingHourstub's logic (research.md's day-by-day walk) - T015 [US2] Add
BusinessCalendarRepository/HolidayRepository, Zod schema (IANA timezone validation,HH:mm+start < endvalidation per data-model.md), andPOST/GET/GET:id/PATCH /admin/business-calendars+POST /admin/business-calendars/:id/holidays+DELETE /admin/business-calendars/:id/holidays/:holidayIdroutes inbusiness-calendars/repository/+schema/+controller/+routes/(depends on T014) - T016 [US2] Replace
SlaDueDateCalculator.calculateDueTime's naive addition with a call into T014'saddBusinessMinutes(viabusiness-calendars's publicindex.ts— FR-004) insla/calculators/sla-due-date.calculator.ts(depends on T014) - T017 [US2] Add
AssignmentEngine.persistAndTransition(007,src/modules/orchestration/assignments/engine/assignment.engine.ts) publishingDomainEventName.TICKET_ASSIGNED({ ticketId, agentId, strategy, actor }) after its existing persistence step — the event is already defined insrc/events/domain-events.tsbut has never been published (research.md) - T018 [US2] Add
SlaService.handleTicketAssigned(ticketId, agentId): no-ops if the ticket already has anSLARun(SLARun.ticketId @unique— covers re-escalation's second publish, spec.md Assumptions); otherwise resolves the applicable policy (T009), computesfirstResponseDueAt/resolutionDueAtvia T016, and creates theSLARun— insla/service/sla.service.ts(depends on T009, T016) - T019 [US2] Subscribe
DomainEventName.TICKET_ASSIGNEDto T018's handler insrc/events/handlers/index.ts, following the existing "module never imports the module it affects" registration pattern (depends on T017, T018) - T020 [US2] Add
GET /tickets/:ticketId/sla-runroute (404if none) insla/controller/+sla/routes/(depends on T018) - T021 [US2] Run Quickstart Scenario 2 locally and confirm all 4 steps pass
Checkpoint: Every successfully-assigned ticket with a matching policy gets an SLARun with
correctly calendar-computed due dates. MVP-complete for read-only SLA visibility.
Phase 5: User Story 3 - SLA pause/resume is durable across a process restart (Priority: P1)
Goal: WAITING_FOR_CUSTOMER transitions pause/resume the run by shifting its absolute due
dates — no in-memory state anywhere, verified across an actual rebuilt buildApp().
Independent Test: Quickstart Scenario 3.
Tests for User Story 3
- T022 [P] [US3] Unit tests for the pause/resume shift arithmetic (resume shifts both due
dates forward by exactly
now - pausedAt; a second pause/resume cycle composes correctly) intests/unit/orchestration/sla-pause-resume.test.ts - T023 [US3] Integration test covering Quickstart Scenario 3 — including rebuilding
buildApp()mid-test to simulate a real process restart while paused, then asserting the resumed due date is exactly the original plus the paused wall-clock duration — against a real Postgres intests/integration/sla-pause-resume.test.ts(depends on T018)
Implementation for User Story 3
- T024 [US3] Add
SlaService.pause(ticketId)/resume(ticketId)(shiftfirstResponseDueAt/resolutionDueAtforward by the paused duration on resume, per research.md/data-model.md — no separate remaining-minutes field) insla/service/ sla.service.ts(depends on T018) - T025 [US3] Subscribe two
DomainEventName.TICKET_UPDATEDhandlers insrc/events/handlers/index.ts—newStatus === 'WAITING_FOR_CUSTOMER'calls T024'spause,previousStatus === 'WAITING_FOR_CUSTOMER'callsresume— alongside the existing 005/007 subscribers on the same event (depends on T024) - T026 [US3] Subscribe a third
TICKET_UPDATEDhandler —newStatus === 'RESOLVED'setsSLARun.completedAtandstatus: 'completed'(data-model.md) — in the same file (depends on T018) - T027 [US3] Run Quickstart Scenario 3 locally and confirm all 4 steps pass, including the restart-boundary step
Checkpoint: Every P1 user story is complete. SLA runs are created, calendar-computed, and durably pause/resume-correct. This is the feature's MVP.
Phase 6: User Story 4 - Breaches are detected even if no one is watching in real time (Priority: P2)
Goal: A repeatable BullMQ job durably detects both resolution and first-response breaches, never missing one because the process wasn't running at the due instant, never flagging a completed-in-time or paused run.
Independent Test: Quickstart Scenario 4.
Tests for User Story 4
- T028 [P] [US4] Unit tests for the breach-detection predicate logic (a
runningrun pastresolutionDueAtbreaches; apausedrun pastresolutionDueAtdoes not; acompletedrun does not; arunningrun pastfirstResponseDueAtwith no priorAGENT_MESSAGEbreaches first-response exactly once, guarded byfirstResponseBreachedAt) intests/unit/orchestration/sla-breach-detection.test.ts - T029 [US4] Integration test covering Quickstart Scenario 4 (a short-
resolutionMinutespolicy breaches within one sweep call; resolved-in-time and paused runs are never breached even after their due instant passes) against a real Postgres intests/integration/sla-breach-detection.test.ts(depends on T018, T024)
Implementation for User Story 4
- T030 [US4] Add
SlaService.runBreachDetectionSweep()— queries everyrunningSLARunwithresolutionDueAt <= now()(marksbreached/breachedAt) and everyrunningrun withfirstResponseDueAt <= now()andfirstResponseBreachedAt: nulland noAGENT_MESSAGErecorded for the ticket (marksfirstResponseBreachedAt) — a single, directly-callable, side-effect-only method (research.md — no worker process needed to invoke it in tests) insla/service/sla.service.ts(depends on T024, T026) - T031 [US4] Replace
registerSlaWorker()'s stub body insrc/jobs/sla/index.ts: on registration, schedule a BullMQ repeatable job onQueueName.SLA({ repeat: { every: 60_000 } }) whose processor calls T030'srunBreachDetectionSweep(depends on T030) - T032 [US4] Run Quickstart Scenario 4 locally and confirm all 5 steps pass
Checkpoint: Breaches are durably detected. Nothing reacts to a breach yet beyond marking the run — that's User Story 5.
Phase 7: User Story 5 - A breach automatically triggers rule-driven escalation (Priority: P2)
Goal: EscalationPolicy/EscalationRule CRUD, breach-triggered EscalationEvent firing, and
a new scoped-assignment entry point on 007's AssignmentEngine that re-assigns to exactly the
rule's targetNodeId.
Independent Test: Quickstart Scenario 5.
Tests for User Story 5
- T033 [P] [US5] Unit tests for escalation-policy resolution (product-specific preferred over
global, per research.md) and rule matching (every active rule whose
triggerTypematches the firing breach type fires; an inactive or wrong-trigger-type rule doesn't) intests/unit/orchestration/escalation-rule-match.test.ts - T034 [US5] Integration test covering Quickstart Scenario 5 (a breach with a matching rule
produces exactly one
EscalationEventand reassigns to an agent eligible under the rule's specifictargetNodeId, not the ticket's originally-resolved node; a breach with no matching rule is still recorded breached with noEscalationEvent) against a real Postgres intests/integration/sla-escalation-firing.test.ts(depends on T030)
Implementation for User Story 5
- T035 [US5] Add
EscalationPolicyRepository/EscalationRuleRepository(CRUD,findActiveRules(policyId, triggerType)), Zod schema (all 10 doc-05triggerTypevalues accepted;targetNodeIdresolve-or-404 at rule creation, FR-012) inescalation/repository/+escalation/schema/(depends on T005) - T036 [US5] Add
POST/GET /admin/escalation-policies,POST/PATCH/DELETE /admin/escalation-policies/:id/rules[/:ruleId]routes inescalation/controller/+escalation/routes/(depends on T035) - T037 [US5] Add
AssignmentEngine.assignToSpecificNode(ticketId, hierarchyNodeId, actor, reason?, strategyOverride?)(007,assignments/engine/assignment.engine.ts) — resolves the eligible-agent set scoped to exactly the given node (reusingRoutingService's capability-lookup call, research.md) and persists through the existingpersistAndTransition(T017), so it also publishesTICKET_ASSIGNEDfor free (depends on T017) - T038 [US5] Add
EscalationService.handleBreach(ticketId, triggerType): resolves the applicableEscalationPolicy(product-match-or-global, research.md), finds every active matchingEscalationRule(T035), and for each, creates anEscalationEvent(ruleId,fromNodeIdfrom the ticket's current assignment,toNodeId: rule.targetNodeId,triggeredBy: 'system') and calls T037'sassignToSpecificNode— records nothing when no rule matches (FR-015) — inescalation/service/escalation.service.ts(depends on T035, T037) - T039 [US5] Wire T030's
runBreachDetectionSweepto call T038'shandleBreachfor each newly-detected breach, passing the corresponding trigger type (resolution_breach/first_response_breach) — insla/service/sla.service.ts(depends on T030, T038) - T040 [US5] Run Quickstart Scenario 5 locally and confirm all 3 steps pass
Checkpoint: Breaches automatically escalate through rule-driven, scoped re-assignment.
Phase 8: User Story 6 - A human can manually escalate a ticket to a specific node (Priority: P3)
Goal: The same EscalationEvent + scoped-reassignment mechanism, triggered explicitly by a
caller instead of a breach.
Independent Test: Quickstart Scenario 6.
Tests for User Story 6
- T041 [US6] Integration test covering Quickstart Scenario 6 (manual escalation creates an
EscalationEventwithruleId: nulland reassigns via the scoped path; a nonexistenttargetNodeIdreturns404with no event created; a manual escalation racing an automatic breach escalation on the same ticket records both events without a corrupted final assignment) against a real Postgres intests/integration/manual-escalation.test.ts(depends on T037, T038)
Implementation for User Story 6
- T042 [US6] Add
EscalationService.escalateManually(ticketId, targetNodeId, actor, reason): resolve-or-404 ontargetNodeId(FR-017), creates anEscalationEvent(ruleId: null,triggeredBy: actor) and calls T037'sassignToSpecificNode— inescalation/service/ escalation.service.ts(depends on T037) - T043 [US6] Add
POST /tickets/:ticketId/escalateroute (gated byfastify.authenticate) inescalation/controller/+escalation/routes/, registered fromsrc/api/routes.ts(depends on T042) - T044 [US6] Run Quickstart Scenario 6 locally and confirm all 4 steps pass
Checkpoint: All six user stories work independently and together — policy definition, calendar-aware run creation, durable pause/resume, durable breach detection, and both automatic and manual escalation form one coherent, restart-safe flow.
Phase 9: Polish & Cross-Cutting Concerns
- T045 [P] Add an "SLA and Escalation" section to
README.mddescribing the calendar-aware due-date computation, the durable pause/resume mechanism, the breach-detection job interval, which 2 of doc 05's 10 escalation trigger types actually fire, and what's explicitly deferred (notification delivery, investigation/customer-response timers, reopen-cycle SLA restart) - T046 [P] Update
specs/008-sla-escalation/checklists/requirements.mdNotes with any implementation-time findings - T047 Run
npx tsx scripts/check-architecture.tsandnpm run lint/npm run typecheck - T048 Full regression:
npm run test:unit(scoped totests/unit) to confirm nothing broke elsewhere, then the full integration suite (including 007's own suite, since T017/T037 modify itsAssignmentEngine) against real Docker-provisioned Postgres/Redis
Dependencies & Execution Order
Phase Dependencies
- Setup (Phase 1): No dependencies
- Foundational (Phase 2): Depends on Setup — BLOCKS all user stories
- User Story 1 (Phase 3): Depends on Foundational — no dependency on US2-US6
- User Story 2 (Phase 4): Depends on US1 (the policy it resolves against) — genuinely not independent, same class of dependency 007's US2 had on US1
- User Story 3 (Phase 5): Depends on US2 (the run it pauses/resumes)
- User Story 4 (Phase 6): Depends on US3 (a run that can be paused must be excluded from breach detection correctly, so the pause mechanism must exist first)
- User Story 5 (Phase 7): Depends on US4 (the breach it reacts to) and on 007's
AssignmentEngine(T037's new method) - User Story 6 (Phase 8): Depends on US5 (T037/T038's scoped-reassignment mechanism, reused directly rather than duplicated)
- Polish (Phase 9): Depends on all six user stories
Parallel Opportunities
- T001/T002/T003 (independent scaffolding)
- T006 (unit tests) alongside T008-T009 (the implementations they test)
- T012 (unit tests) alongside T014 (the implementation it tests)
- T022 alongside T024; T028 alongside T030; T033 alongside T035/T038
- T045/T046 in Polish
Sequencing Note
T017 (publishing TICKET_ASSIGNED from 007's AssignmentEngine) and T037 (the new
assignToSpecificNode method on the same class) both modify a file 007 already owns and has its
own passing test suite for — run 007's full integration suite (part of T048) after each, not only
at the very end, to catch a regression close to its cause.
Implementation Strategy
MVP First (User Stories 1-3 Only)
- Setup + Foundational (T001-T005)
- User Story 1 (T006-T011) — policies exist and resolve correctly
- User Story 2 (T012-T021) — runs are created with real calendar-aware due dates
- User Story 3 (T022-T027) — pause/resume is durable, including across a restart
- STOP and VALIDATE: Quickstart Scenarios 1-3 pass — every assigned ticket has a correctly
computed, durably pausable
SLARun. Nothing reacts to a breach yet — that value lands with User Story 4/5.
Incremental Delivery
- Setup + Foundational → schema migrated
- Add User Story 1 → SLA policies are configurable and resolve correctly
- Add User Story 2 → runs are created automatically with calendar-aware due dates
- Add User Story 3 → pause/resume is durable (P1-complete, MVP)
- Add User Story 4 → breaches are durably detected
- Add User Story 5 → breaches automatically escalate and reassign
- Add User Story 6 → manual escalation exists, reusing the same mechanism
- Polish → docs and full regression