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>
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`, `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.*
|