Populates platform/business-calendars, orchestration/sla, and orchestration/escalation (all thin stubs until now) with the real engine: - business-calendars: a luxon-based day-by-day calendar walk (addBusinessMinutes/isWithinWorkingHours) excluding non-working hours, weekends, and holidays — replacing the naive createdAt+hours stub FR-004 explicitly forbids. - sla: most-specific SLAPolicy resolution (product/category/problemType/ priority, wildcard-or-exact-match, specificity-count + updatedAt tiebreak), SLARun creation on the first real publish of the long-unused TICKET_ASSIGNED domain event, durable pause/resume via an absolute-timestamp shift (no in-memory state, verified across a real buildApp() restart), and a repeatable BullMQ breach-detection sweep (src/jobs/sla, itself a previously-unregistered stub) that is directly callable for tests, not only reachable through a running worker. - escalation: EscalationPolicy/Rule CRUD (all 10 doc05 trigger types storable, only resolution_breach/first_response_breach evaluated), breach-triggered and manual escalation both funnel through one EscalationEvent + scoped re-assignment path. AssignmentEngine (007) gains assignToSpecificNode — a new, explicitly node-scoped entry point, since escalation must never let 007's general resolution re-derive a different node than the one a rule or a caller targeted. Two small pre-existing scaffold gaps were closed along the way: CategoriesRepository had no findById, and TICKET_ASSIGNED/SLA_BREACHED/ ESCALATION_TRIGGERED were defined since earlier phases but never published by any code. Verified against throwaway Docker Postgres/Redis (typecheck, lint, architecture-check all clean; 148/150 relevant tests pass — the 2 failures are pre-existing, MinIO-dependent, and unrelated to this feature). 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, orchestration/sla→orchestration/escalation (a breach sweep calls escalation firing directly, research.md), and orchestration/escalation→orchestration/assignments (007, for the new scoped-assignment method) are all one-directional — no cycle, since 007 doesn't import anything from 008 and escalation never imports sla back. |
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.