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>
11 KiB
Implementation Plan: SLA and Escalation
Branch: 008-sla-escalation | Date: 2026-09-03 | Spec: spec.md
Input: Feature specification from specs/008-sla-escalation/spec.md
Summary
Populate the existing platform/business-calendars, orchestration/sla, and
orchestration/escalation stub directories (today: isWorkingHour hardcoded true, a
SlaDueDateCalculator doing naive createdAt + hours addition, an EscalationEngine that
always returns { escalated: false }) with the real engine: business-calendars walks a
BusinessCalendar's workingHours/Holiday records via luxon to compute calendar-aware
durations; sla resolves the most-specific matching SLAPolicy on a ticket's first successful
007 assignment, computes firstResponseDueAt/resolutionDueAt through the calendar walk,
durably pauses/resumes on WAITING_FOR_CUSTOMER transitions (a TICKET_UPDATED domain-event
subscriber), and detects breaches via a repeatable BullMQ job; escalation resolves the
applicable EscalationPolicy, fires an EscalationEvent per matching active EscalationRule on
a breach (or on a manual request), and re-assigns through a new, specifically-scoped entry point
added to 007's AssignmentEngine.
Technical Context
Language/Version: TypeScript 5.4 / Node.js 20+.
Primary Dependencies: Prisma (new models), Zod, BullMQ (already a dependency — new
repeatable job, same queue infrastructure as 003's attachment scan and 005's AI session queues),
luxon (new — the first date/timezone library in this codebase; research.md).
Storage: PostgreSQL via Prisma (new SLAPolicy, SLARun, BusinessCalendar, Holiday,
EscalationPolicy, EscalationRule, EscalationEvent models). No new infrastructure — reuses
src/infrastructure/queue for the breach-detection job, same as every prior BullMQ consumer.
Testing: Vitest — unit tests for the calendar-walk algorithm (weekend/holiday exclusion,
partial-day clipping, timezone correctness), the most-specific SLA-policy match, and pause/resume
arithmetic; integration tests for the full assignment→SLA-run→pause/resume→breach→escalation
flow against real Postgres/Redis, including one test that rebuilds buildApp() mid-test to
verify pause/resume state survives a genuine process-restart boundary (Constitution Principle
VII, quickstart Scenario 3) — the first feature in this codebase whose correctness depends on
that guarantee specifically, not just within-process concurrency safety.
Target Platform: Same Fastify modular monolith. Populates existing module directories:
src/modules/platform/business-calendars/, src/modules/orchestration/{sla,escalation}/. Adds
one new BullMQ worker registration alongside the existing ones in src/infrastructure/queue.
Project Type: Backend service — single project.
Performance Goals: The breach-detection job must complete a full scan-and-mark pass in
well under its own tick interval even as SLARun rows accumulate — indexed on
(status, resolutionDueAt) so the query stays a targeted range scan, not a table scan. Not
otherwise performance-sensitive.
Constraints: MUST NOT compute due dates naively (FR-004); MUST create an SLARun only on a
successful assignment with a matching policy (FR-005); MUST survive a process restart for
pause/resume and breach detection (FR-007/FR-008/FR-009, Constitution Principle VII); MUST never
mark a completed-in-time or paused run breached (FR-010/FR-011); MUST re-assign scoped to the
rule's exact targetNodeId, not a fresh unscoped resolution (FR-014).
Scale/Scope: Three populated modules, one new dependency, three admin CRUD surfaces (SLA
policies, business calendars, escalation policies/rules), one manual-escalation endpoint, one
new BullMQ repeatable job, one new method on 007's AssignmentEngine. Explicitly excludes:
notification delivery, 8 of doc 05's 10 escalation trigger types, investigation/customer-response
timers, SLA restart on ticket reopen (see spec.md Assumptions).
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
| Principle / Section | Check | Result |
|---|---|---|
| I. SaaS Is the Sole Identity & Access Authority | SLA/escalation reference Ticket/HierarchyNode/Agent — all SupportHub's own domain. No SaaS identity touched. |
PASS |
| II. Configuration Over Hardcoding | Every SLA target, calendar, and escalation rule is admin-configured data, not a hardcoded constant — replacing the literal hardcoded-true/naive-arithmetic stubs is the point of this feature. |
PASS |
| III. Layered Architecture With Enforced Module Boundaries | Three modules follow the standard shape; orchestration/sla→platform/business-calendars and orchestration/escalation→orchestration/sla (for breach signals) and →orchestration/assignments (007, for the new scoped-assignment method) are all one-directional — no cycle, since 007 doesn't import anything from 008. |
PASS |
| IV. AI Recommends, Deterministic Policy Decides | No AI involvement in this feature at all — every decision (policy match, breach, escalation) is deterministic. | PASS — N/A |
| V. Evidence-Based Verification | Not applicable — no resolution/verification concept in this feature. | PASS — N/A |
| VI. Durable Audit & History | EscalationEvent is the durable, append-only record doc 06 defines for every escalation, automatic or manual — mirrors AssignmentHistory's established shape. |
PASS |
| VII. Concurrency-Safe, Durable Job Handling | This principle's "state must survive a process restart" clause is directly load-bearing here for the first time as the primary correctness requirement (not just a concurrent-request race) — pause/resume and breach detection are both pure-DB-state-plus-polling-job, no in-memory timer anywhere (research.md, quickstart Scenario 3). | PASS |
| VIII. Problem and Ticket Are Separate, Related Entities | SLA/escalation reference Ticket, not Problem — doesn't touch the distinction. |
PASS — N/A |
| Technology & Platform Constraints | Prisma + Zod + existing BullMQ infrastructure, plus the one new luxon dependency (justified in research.md — no timezone-correct alternative already exists in this codebase). |
PASS |
No violations requiring Complexity Tracking justification.
Post-Design Constitution Re-check
All gates above remain PASS after Phase 1 design. Worth calling out against Principle VII
explicitly: pause/resume shifts a single absolute DateTime column and breach detection is a
plain polling query — by design there is no code path in this feature that could even appear
to depend on in-memory state surviving a restart, which is what makes the restart-boundary
integration test (quickstart Scenario 3) a meaningful verification rather than a formality.
Project Structure
Documentation (this feature)
specs/008-sla-escalation/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
├── contracts/ # Phase 1 output
└── tasks.md # Phase 2 output (/speckit-tasks — not created here)
Source Code (repository root)
supporthub-api/
├── package.json # MODIFIED — add luxon, @types/luxon
├── prisma/
│ └── schema.prisma # MODIFIED — add SLAPolicy, SLARun,
│ BusinessCalendar, Holiday, EscalationPolicy,
│ EscalationRule, EscalationEvent
├── src/
│ ├── events/
│ │ └── handlers/index.ts # MODIFIED — first real publish of the
│ │ existing-but-unused TICKET_ASSIGNED event
│ │ (from 007's persistAndTransition), plus two
│ │ new TICKET_UPDATED subscribers (pause/
│ │ resume, completion) — research.md
│ ├── jobs/
│ │ └── sla/index.ts # REPLACED stub — schedules the repeatable
│ │ breach-detection job (research.md); jobs/
│ │ escalation/ stays untouched (reserved for a
│ │ future notification-dispatch step)
│ └── modules/
│ ├── platform/
│ │ └── business-calendars/ # REPLACED stub — full standard shape +
│ │ ├── controller/ routes/ schema/ calculators/ for the day-walk algorithm
│ │ │ repository/ service/ types/
│ │ │ mapper/ constants/ index.ts
│ │ └── calculators/
│ └── orchestration/
│ ├── assignments/ # 007, MODIFIED — persistAndTransition
│ │ └── engine/assignment.engine.ts publishes TICKET_ASSIGNED; new
│ │ assignToSpecificNode() method for
│ │ escalation's scoped re-assignment
│ ├── sla/ # REPLACED stub — full standard shape, keeps
│ │ ├── controller/ routes/ schema/ its existing calculators/ dir (due-date
│ │ │ repository/ service/ types/ calculator replaced, not removed) and adds
│ │ │ mapper/ constants/ index.ts engine/ for breach evaluation
│ │ ├── engine/ (policy resolution + breach detection)
│ │ └── calculators/ (due-date calculator, replaced)
│ └── escalation/ # REPLACED stub — full standard shape, keeps
│ ├── controller/ routes/ schema/ engine/ for rule matching + firing
│ │ repository/ service/ types/
│ │ mapper/ constants/ index.ts
│ └── engine/
└── tests/
├── unit/
│ ├── platform/business-calendars/ # calendar-walk algorithm
│ └── orchestration/{sla,escalation}/ # policy match, breach logic, rule match
└── integration/ # full flow incl. restart-boundary test
Structure Decision: Single project. business-calendars gets a full standard shape (not
internal-only) since it needs its own CRUD surface for calendars/holidays, unlike 007's
internal-only routing module. sla and escalation each keep the engine/ extension doc 07
§8 reserves for modules with real decision logic, matching 005/007 precedent.
Complexity Tracking
No constitution violations — table intentionally omitted.