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

164 lines
11 KiB
Markdown

# Implementation Plan: SLA and Escalation
**Branch**: `008-sla-escalation` | **Date**: 2026-09-03 | **Spec**: [spec.md](./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)
```text
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)
```text
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.*