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>
164 lines
11 KiB
Markdown
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` 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)
|
|
|
|
```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.*
|