Files
support_backend/specs/008-sla-escalation/plan.md
T
saqib mirandClaude Sonnet 5 9357f03e1d feat: implement SLA and escalation (008)
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>
2026-09-03 13:02:05 +05:30

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/slaplatform/business-calendars, orchestration/slaorchestration/escalation (a breach sweep calls escalation firing directly, research.md), and orchestration/escalationorchestration/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.