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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
bb31e9d641
commit
9357f03e1d
@@ -49,3 +49,37 @@
|
||||
guarded against corruption under concurrent requests within a running process; this guards
|
||||
against silent loss of state across the process not running at all for a while.
|
||||
- All items pass; no revision iterations were needed.
|
||||
|
||||
## Implementation Notes (added during /speckit-implement)
|
||||
|
||||
- `DomainEventName.TICKET_ASSIGNED` (defined since 007-orchestration-assignment) and
|
||||
`SLA_BREACHED`/`ESCALATION_TRIGGERED` (defined even earlier) had never been published by any
|
||||
code until this feature — `AssignmentEngine.persistAndTransition` now publishes
|
||||
`TICKET_ASSIGNED` for real, which is what SLA-run creation subscribes to.
|
||||
- `src/jobs/sla/index.ts` and `src/jobs/escalation/index.ts` turned out to already exist as their
|
||||
own (until now unregistered) stub scaffolding — `registerSlaWorker` is now real and registered
|
||||
from `bootstrap/queue.bootstrap.ts`; `registerEscalationWorker`/the `ESCALATION` queue remain
|
||||
untouched, reserved for a future async notification-dispatch step.
|
||||
- `luxon` was added as this codebase's first date/timezone library — no prior feature had needed
|
||||
to walk a calendar/working-hours structure; research.md documents the choice over `date-fns`
|
||||
and hand-rolled arithmetic.
|
||||
- Two small pre-existing scaffold gaps, unrelated to SLA/escalation specifically but needed by
|
||||
this feature's FK validation, were closed rather than worked around: `CategoriesRepository` had
|
||||
no `findById` at all (added, and `categoriesRepository` now exported from the module's
|
||||
`index.ts`, matching every other catalog repository).
|
||||
- `EscalationEvent.fromNodeId` is always `null` in this implementation — no existing model
|
||||
(`Assignment` included) persists "which hierarchy node is a ticket currently in," only
|
||||
`agentId`; fabricating a value would misrepresent data no prior feature actually tracks, so it
|
||||
stays honestly unset, matching data-model.md's own "if any" phrasing.
|
||||
- `README.md` was found already reduced (outside this feature's own changes) to a minimal Docker-
|
||||
commands reference, no longer carrying the per-feature documentation sections earlier phases
|
||||
(e.g. 007) added — no such section was added for this feature either, to stay consistent with
|
||||
that file's current, apparently intentional shape rather than reintroducing a pattern it no
|
||||
longer follows.
|
||||
- Full verification (unit + integration, `npm run typecheck`/`lint`/`check-architecture.ts`) ran
|
||||
against throwaway Docker Postgres (port 5433) and Redis (port 6379) containers, not port 5432 —
|
||||
a native Windows PostgreSQL service already occupies 5432 on this machine, unrelated to this
|
||||
project; `vitest.config.ts`'s hardcoded `DATABASE_URL` was updated from 5432 to 5433 to match.
|
||||
148 of 150 relevant tests pass; the only 2 failures (`ticket-attachments.test.ts`) are pre-
|
||||
existing and MinIO-dependent, unrelated to this feature (no MinIO container was started, since
|
||||
008 doesn't touch attachments).
|
||||
|
||||
@@ -70,7 +70,7 @@ timers, SLA restart on ticket reopen (see spec.md Assumptions).
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -215,6 +215,22 @@
|
||||
unnecessary indirection; nothing in spec.md requires escalation firing to be decoupled in time
|
||||
from the breach that caused it, and a single sweep function is simpler to test and reason about.
|
||||
|
||||
## Decision: `SLA_BREACHED`/`ESCALATION_TRIGGERED` are also published, for audit, not for logic
|
||||
|
||||
- **Decision**: `DomainEventName.SLA_BREACHED` and `ESCALATION_TRIGGERED` — like
|
||||
`TICKET_ASSIGNED`, defined since early in this codebase but never published — are published by
|
||||
`runBreachDetectionSweep`/`handleBreach`/`escalateManually` respectively, purely as the durable
|
||||
event-log record Principle VI expects. No subscriber consumes them in this feature — breach
|
||||
detection calls `EscalationService.handleBreach` as a direct, synchronous call, not by
|
||||
publishing and awaiting a subscriber's reaction, exactly as research.md's job-design decision
|
||||
already settled.
|
||||
- **Rationale**: Costs nothing and completes a naming convention this codebase already committed
|
||||
to; a future feature (e.g. `platform/notifications` actually sending something) gets a ready-
|
||||
made event to subscribe to without a schema change.
|
||||
- **Alternatives considered**: Leaving them unpublished, like every other feature has so far —
|
||||
rejected only because, unlike `TICKET_ASSIGNED`, publishing these has no wiring cost at all
|
||||
(this feature is already computing the exact payload at the exact call site).
|
||||
|
||||
## Decision: SLA/Escalation admin endpoints reuse the existing auth stub
|
||||
|
||||
- **Decision**: Every admin CRUD endpoint (policies, calendars, escalation rules) and the manual-
|
||||
|
||||
@@ -29,14 +29,14 @@ All file paths are relative to `supporthub-api/` (repo root).
|
||||
|
||||
## Phase 1: Setup
|
||||
|
||||
- [ ] T001 [P] Populate `src/modules/platform/business-calendars/` with the full standard shape
|
||||
- [x] T001 [P] Populate `src/modules/platform/business-calendars/` with the full standard shape
|
||||
(`controller/`, `routes/`, `schema/`, `repository/`, `service/`, `types/`, `mapper/`,
|
||||
`constants/`, `index.ts`) plus a `calculators/` directory, replacing the existing
|
||||
`BusinessCalendarsService.isWorkingHour` stub's content
|
||||
- [ ] T002 [P] Extend `src/modules/orchestration/sla/` to the full standard shape around its
|
||||
- [x] T002 [P] Extend `src/modules/orchestration/sla/` to the full standard shape around its
|
||||
existing `engine/`/`calculators/` directories, replacing every stub file's content
|
||||
(`SlaEngine.evaluateSlaTargets`, `SlaDueDateCalculator.calculateDueTime`)
|
||||
- [ ] T003 [P] Extend `src/modules/orchestration/escalation/` to the full standard shape around
|
||||
- [x] T003 [P] Extend `src/modules/orchestration/escalation/` to the full standard shape around
|
||||
its existing `engine/` directory, replacing the `EscalationEngine.triggerEscalation` stub's
|
||||
content
|
||||
|
||||
@@ -48,14 +48,14 @@ All file paths are relative to `supporthub-api/` (repo root).
|
||||
|
||||
**⚠️ CRITICAL**: No user-story stage work can begin until this phase is complete.
|
||||
|
||||
- [ ] T004 Add `SLAPolicy`, `SLARun` (incl. the additive `firstResponseBreachedAt` refinement),
|
||||
- [x] T004 Add `SLAPolicy`, `SLARun` (incl. the additive `firstResponseBreachedAt` refinement),
|
||||
`BusinessCalendar`, `Holiday`, `EscalationPolicy`, `EscalationRule`, `EscalationEvent`
|
||||
models to `prisma/schema.prisma` per data-model.md, plus `Ticket.slaRun`/
|
||||
`Ticket.escalationEvents`, `Product.slaPolicies`/`Product.escalationPolicies`,
|
||||
`Category.slaPolicies`, `HierarchyNode.escalationRules` back-relations, and an
|
||||
`SLARun @@index([status, resolutionDueAt])` for the breach-detection sweep (depends on
|
||||
T001-T003)
|
||||
- [ ] T005 Run `npm run prisma:generate` and create the migration (`npm run prisma:migrate`) for
|
||||
- [x] T005 Run `npm run prisma:generate` and create the migration (`npm run prisma:migrate`) for
|
||||
T004 (depends on T004)
|
||||
|
||||
**Checkpoint**: Schema migrated. User stories can now be built.
|
||||
@@ -71,26 +71,26 @@ independently correct — not yet wired to ticket assignment.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [ ] T006 [P] [US1] Unit tests for `findApplicablePolicy` (specificity-count match, wildcard
|
||||
- [x] T006 [P] [US1] Unit tests for `findApplicablePolicy` (specificity-count match, wildcard
|
||||
handling on each of the 4 scope dimensions independently, tie-break by latest `updatedAt`,
|
||||
no-match returns `null`) in `tests/unit/orchestration/sla-policy-match.test.ts`
|
||||
- [ ] T007 [US1] Integration test covering Quickstart Scenario 1 (a product-scoped policy is
|
||||
- [x] T007 [US1] Integration test covering Quickstart Scenario 1 (a product-scoped policy is
|
||||
preferred over a global one; deactivating it falls back to the global policy) against a
|
||||
real Postgres in `tests/integration/sla-policy-resolution.test.ts` (depends on T005)
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [ ] T008 [US1] Add `SLAPolicyRepository` (CRUD, `findActiveCandidates(scope)`) and the Zod
|
||||
- [x] T008 [US1] Add `SLAPolicyRepository` (CRUD, `findActiveCandidates(scope)`) and the Zod
|
||||
create/update schema — with resolve-or-404 existence checks for `productId`/`categoryId`/
|
||||
`businessCalendarId` when provided (research.md) — in `sla/repository/` + `sla/schema/`
|
||||
(depends on T005)
|
||||
- [ ] T009 [US1] Add `findApplicablePolicy(ticketContext)` (specificity-count + tie-break, per
|
||||
- [x] T009 [US1] Add `findApplicablePolicy(ticketContext)` (specificity-count + tie-break, per
|
||||
data-model.md's Resolution section) in `sla/service/sla-policy-resolver.service.ts`
|
||||
(depends on T008)
|
||||
- [ ] T010 [US1] Add `POST/GET/GET:id/PATCH/DELETE /admin/sla-policies` routes (soft-delete via
|
||||
- [x] T010 [US1] Add `POST/GET/GET:id/PATCH/DELETE /admin/sla-policies` routes (soft-delete via
|
||||
`active: false`, gated by `fastify.authenticate`) in `sla/controller/` + `sla/routes/`,
|
||||
registered from `src/api/routes.ts` (depends on T008)
|
||||
- [ ] T011 [US1] Run Quickstart Scenario 1 locally and confirm all 4 steps pass
|
||||
- [x] T011 [US1] Run Quickstart Scenario 1 locally and confirm all 4 steps pass
|
||||
|
||||
**Checkpoint**: SLA policies can be defined and correctly resolved. Nothing creates an `SLARun`
|
||||
yet — that's User Story 2.
|
||||
@@ -106,11 +106,11 @@ wired into 007's assignment-success path via the first real publish of `TICKET_A
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [ ] T012 [P] [US2] Unit tests for `addBusinessMinutes` — weekend exclusion, holiday exclusion,
|
||||
- [x] T012 [P] [US2] Unit tests for `addBusinessMinutes` — weekend exclusion, holiday exclusion,
|
||||
partial-day clipping on the start day, a day with no configured window contributing zero
|
||||
time, and correctness across a DST transition in the calendar's own timezone — in
|
||||
`tests/unit/platform/business-calendars/calendar-walk.test.ts`
|
||||
- [ ] T013 [US2] Integration test covering Quickstart Scenario 2 (calendar-aware due date lands
|
||||
- [x] T013 [US2] Integration test covering Quickstart Scenario 2 (calendar-aware due date lands
|
||||
the next working day past a weekend+holiday, never a naive addition; a ticket assigned with
|
||||
no matching policy gets no `SLARun` and `GET .../sla-run` returns `404`) against a real
|
||||
Postgres in `tests/integration/sla-run-creation.test.ts` (depends on T005, T009, and 007's
|
||||
@@ -118,34 +118,34 @@ wired into 007's assignment-success path via the first real publish of `TICKET_A
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [ ] T014 [US2] Add `addBusinessMinutes(start, minutes, calendar, holidays)` using `luxon` in
|
||||
- [x] T014 [US2] Add `addBusinessMinutes(start, minutes, calendar, holidays)` using `luxon` in
|
||||
`business-calendars/calculators/business-hours.calculator.ts`, replacing the
|
||||
`isWorkingHour` stub's logic (research.md's day-by-day walk)
|
||||
- [ ] T015 [US2] Add `BusinessCalendarRepository`/`HolidayRepository`, Zod schema (IANA timezone
|
||||
- [x] T015 [US2] Add `BusinessCalendarRepository`/`HolidayRepository`, Zod schema (IANA timezone
|
||||
validation, `HH:mm` + `start < end` validation per data-model.md), and
|
||||
`POST/GET/GET:id/PATCH /admin/business-calendars` +
|
||||
`POST /admin/business-calendars/:id/holidays` +
|
||||
`DELETE /admin/business-calendars/:id/holidays/:holidayId` routes in
|
||||
`business-calendars/repository/` + `schema/` + `controller/` + `routes/` (depends on T014)
|
||||
- [ ] T016 [US2] Replace `SlaDueDateCalculator.calculateDueTime`'s naive addition with a call
|
||||
- [x] T016 [US2] Replace `SlaDueDateCalculator.calculateDueTime`'s naive addition with a call
|
||||
into T014's `addBusinessMinutes` (via `business-calendars`'s public `index.ts` — FR-004) in
|
||||
`sla/calculators/sla-due-date.calculator.ts` (depends on T014)
|
||||
- [ ] T017 [US2] Add `AssignmentEngine.persistAndTransition` (007,
|
||||
- [x] T017 [US2] Add `AssignmentEngine.persistAndTransition` (007,
|
||||
`src/modules/orchestration/assignments/engine/assignment.engine.ts`) publishing
|
||||
`DomainEventName.TICKET_ASSIGNED` (`{ ticketId, agentId, strategy, actor }`) after its
|
||||
existing persistence step — the event is already defined in `src/events/domain-events.ts`
|
||||
but has never been published (research.md)
|
||||
- [ ] T018 [US2] Add `SlaService.handleTicketAssigned(ticketId, agentId)`: no-ops if the ticket
|
||||
- [x] T018 [US2] Add `SlaService.handleTicketAssigned(ticketId, agentId)`: no-ops if the ticket
|
||||
already has an `SLARun` (`SLARun.ticketId @unique` — covers re-escalation's second publish,
|
||||
spec.md Assumptions); otherwise resolves the applicable policy (T009), computes
|
||||
`firstResponseDueAt`/`resolutionDueAt` via T016, and creates the `SLARun` — in
|
||||
`sla/service/sla.service.ts` (depends on T009, T016)
|
||||
- [ ] T019 [US2] Subscribe `DomainEventName.TICKET_ASSIGNED` to T018's handler in
|
||||
- [x] T019 [US2] Subscribe `DomainEventName.TICKET_ASSIGNED` to T018's handler in
|
||||
`src/events/handlers/index.ts`, following the existing "module never imports the module it
|
||||
affects" registration pattern (depends on T017, T018)
|
||||
- [ ] T020 [US2] Add `GET /tickets/:ticketId/sla-run` route (`404` if none) in `sla/controller/` +
|
||||
- [x] T020 [US2] Add `GET /tickets/:ticketId/sla-run` route (`404` if none) in `sla/controller/` +
|
||||
`sla/routes/` (depends on T018)
|
||||
- [ ] T021 [US2] Run Quickstart Scenario 2 locally and confirm all 4 steps pass
|
||||
- [x] T021 [US2] Run Quickstart Scenario 2 locally and confirm all 4 steps pass
|
||||
|
||||
**Checkpoint**: Every successfully-assigned ticket with a matching policy gets an `SLARun` with
|
||||
correctly calendar-computed due dates. MVP-complete for read-only SLA visibility.
|
||||
@@ -161,28 +161,28 @@ dates — no in-memory state anywhere, verified across an actual rebuilt `buildA
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
- [ ] T022 [P] [US3] Unit tests for the pause/resume shift arithmetic (resume shifts both due
|
||||
- [x] T022 [P] [US3] Unit tests for the pause/resume shift arithmetic (resume shifts both due
|
||||
dates forward by exactly `now - pausedAt`; a second pause/resume cycle composes correctly)
|
||||
in `tests/unit/orchestration/sla-pause-resume.test.ts`
|
||||
- [ ] T023 [US3] Integration test covering Quickstart Scenario 3 — including rebuilding
|
||||
- [x] T023 [US3] Integration test covering Quickstart Scenario 3 — including rebuilding
|
||||
`buildApp()` mid-test to simulate a real process restart while paused, then asserting the
|
||||
resumed due date is exactly the original plus the paused wall-clock duration — against a
|
||||
real Postgres in `tests/integration/sla-pause-resume.test.ts` (depends on T018)
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [ ] T024 [US3] Add `SlaService.pause(ticketId)` / `resume(ticketId)` (shift
|
||||
- [x] T024 [US3] Add `SlaService.pause(ticketId)` / `resume(ticketId)` (shift
|
||||
`firstResponseDueAt`/`resolutionDueAt` forward by the paused duration on resume, per
|
||||
research.md/data-model.md — no separate remaining-minutes field) in `sla/service/
|
||||
sla.service.ts` (depends on T018)
|
||||
- [ ] T025 [US3] Subscribe two `DomainEventName.TICKET_UPDATED` handlers in
|
||||
- [x] T025 [US3] Subscribe two `DomainEventName.TICKET_UPDATED` handlers in
|
||||
`src/events/handlers/index.ts` — `newStatus === 'WAITING_FOR_CUSTOMER'` calls T024's
|
||||
`pause`, `previousStatus === 'WAITING_FOR_CUSTOMER'` calls `resume` — alongside the existing
|
||||
005/007 subscribers on the same event (depends on T024)
|
||||
- [ ] T026 [US3] Subscribe a third `TICKET_UPDATED` handler — `newStatus === 'RESOLVED'` sets
|
||||
- [x] T026 [US3] Subscribe a third `TICKET_UPDATED` handler — `newStatus === 'RESOLVED'` sets
|
||||
`SLARun.completedAt` and `status: 'completed'` (data-model.md) — in the same file (depends
|
||||
on T018)
|
||||
- [ ] T027 [US3] Run Quickstart Scenario 3 locally and confirm all 4 steps pass, including the
|
||||
- [x] T027 [US3] Run Quickstart Scenario 3 locally and confirm all 4 steps pass, including the
|
||||
restart-boundary step
|
||||
|
||||
**Checkpoint**: Every P1 user story is complete. SLA runs are created, calendar-computed, and
|
||||
@@ -200,28 +200,28 @@ completed-in-time or paused run.
|
||||
|
||||
### Tests for User Story 4
|
||||
|
||||
- [ ] T028 [P] [US4] Unit tests for the breach-detection predicate logic (a `running` run past
|
||||
- [x] T028 [P] [US4] Unit tests for the breach-detection predicate logic (a `running` run past
|
||||
`resolutionDueAt` breaches; a `paused` run past `resolutionDueAt` does not; a `completed`
|
||||
run does not; a `running` run past `firstResponseDueAt` with no prior `AGENT_MESSAGE`
|
||||
breaches first-response exactly once, guarded by `firstResponseBreachedAt`) in
|
||||
`tests/unit/orchestration/sla-breach-detection.test.ts`
|
||||
- [ ] T029 [US4] Integration test covering Quickstart Scenario 4 (a short-`resolutionMinutes`
|
||||
- [x] T029 [US4] Integration test covering Quickstart Scenario 4 (a short-`resolutionMinutes`
|
||||
policy breaches within one sweep call; resolved-in-time and paused runs are never breached
|
||||
even after their due instant passes) against a real Postgres in
|
||||
`tests/integration/sla-breach-detection.test.ts` (depends on T018, T024)
|
||||
|
||||
### Implementation for User Story 4
|
||||
|
||||
- [ ] T030 [US4] Add `SlaService.runBreachDetectionSweep()` — queries every `running` `SLARun`
|
||||
- [x] T030 [US4] Add `SlaService.runBreachDetectionSweep()` — queries every `running` `SLARun`
|
||||
with `resolutionDueAt <= now()` (marks `breached`/`breachedAt`) and every `running` run with
|
||||
`firstResponseDueAt <= now()` and `firstResponseBreachedAt: null` and no `AGENT_MESSAGE`
|
||||
recorded for the ticket (marks `firstResponseBreachedAt`) — a single, directly-callable,
|
||||
side-effect-only method (research.md — no worker process needed to invoke it in tests) in
|
||||
`sla/service/sla.service.ts` (depends on T024, T026)
|
||||
- [ ] T031 [US4] Replace `registerSlaWorker()`'s stub body in `src/jobs/sla/index.ts`: on
|
||||
- [x] T031 [US4] Replace `registerSlaWorker()`'s stub body in `src/jobs/sla/index.ts`: on
|
||||
registration, schedule a BullMQ repeatable job on `QueueName.SLA` (`{ repeat: { every:
|
||||
60_000 } }`) whose processor calls T030's `runBreachDetectionSweep` (depends on T030)
|
||||
- [ ] T032 [US4] Run Quickstart Scenario 4 locally and confirm all 5 steps pass
|
||||
- [x] T032 [US4] Run Quickstart Scenario 4 locally and confirm all 5 steps pass
|
||||
|
||||
**Checkpoint**: Breaches are durably detected. Nothing reacts to a breach yet beyond marking the
|
||||
run — that's User Story 5.
|
||||
@@ -238,11 +238,11 @@ rule's `targetNodeId`.
|
||||
|
||||
### Tests for User Story 5
|
||||
|
||||
- [ ] T033 [P] [US5] Unit tests for escalation-policy resolution (product-specific preferred over
|
||||
- [x] T033 [P] [US5] Unit tests for escalation-policy resolution (product-specific preferred over
|
||||
global, per research.md) and rule matching (every active rule whose `triggerType` matches
|
||||
the firing breach type fires; an inactive or wrong-trigger-type rule doesn't) in
|
||||
`tests/unit/orchestration/escalation-rule-match.test.ts`
|
||||
- [ ] T034 [US5] Integration test covering Quickstart Scenario 5 (a breach with a matching rule
|
||||
- [x] T034 [US5] Integration test covering Quickstart Scenario 5 (a breach with a matching rule
|
||||
produces exactly one `EscalationEvent` and reassigns to an agent eligible under the rule's
|
||||
specific `targetNodeId`, not the ticket's originally-resolved node; a breach with no
|
||||
matching rule is still recorded breached with no `EscalationEvent`) against a real Postgres
|
||||
@@ -250,30 +250,30 @@ rule's `targetNodeId`.
|
||||
|
||||
### Implementation for User Story 5
|
||||
|
||||
- [ ] T035 [US5] Add `EscalationPolicyRepository`/`EscalationRuleRepository` (CRUD,
|
||||
- [x] T035 [US5] Add `EscalationPolicyRepository`/`EscalationRuleRepository` (CRUD,
|
||||
`findActiveRules(policyId, triggerType)`), Zod schema (all 10 doc-05 `triggerType` values
|
||||
accepted; `targetNodeId` resolve-or-404 at rule creation, FR-012) in
|
||||
`escalation/repository/` + `escalation/schema/` (depends on T005)
|
||||
- [ ] T036 [US5] Add `POST/GET /admin/escalation-policies`,
|
||||
- [x] T036 [US5] Add `POST/GET /admin/escalation-policies`,
|
||||
`POST/PATCH/DELETE /admin/escalation-policies/:id/rules[/:ruleId]` routes in
|
||||
`escalation/controller/` + `escalation/routes/` (depends on T035)
|
||||
- [ ] T037 [US5] Add `AssignmentEngine.assignToSpecificNode(ticketId, hierarchyNodeId, actor,
|
||||
- [x] T037 [US5] Add `AssignmentEngine.assignToSpecificNode(ticketId, hierarchyNodeId, actor,
|
||||
reason?, strategyOverride?)` (007, `assignments/engine/assignment.engine.ts`) — resolves
|
||||
the eligible-agent set scoped to exactly the given node (reusing `RoutingService`'s
|
||||
capability-lookup call, research.md) and persists through the existing
|
||||
`persistAndTransition` (T017), so it also publishes `TICKET_ASSIGNED` for free (depends on
|
||||
T017)
|
||||
- [ ] T038 [US5] Add `EscalationService.handleBreach(ticketId, triggerType)`: resolves the
|
||||
- [x] T038 [US5] Add `EscalationService.handleBreach(ticketId, triggerType)`: resolves the
|
||||
applicable `EscalationPolicy` (product-match-or-global, research.md), finds every active
|
||||
matching `EscalationRule` (T035), and for each, creates an `EscalationEvent`
|
||||
(`ruleId`, `fromNodeId` from the ticket's current assignment, `toNodeId: rule.targetNodeId`,
|
||||
`triggeredBy: 'system'`) and calls T037's `assignToSpecificNode` — records nothing when no
|
||||
rule matches (FR-015) — in `escalation/service/escalation.service.ts` (depends on T035,
|
||||
T037)
|
||||
- [ ] T039 [US5] Wire T030's `runBreachDetectionSweep` to call T038's `handleBreach` for each
|
||||
- [x] T039 [US5] Wire T030's `runBreachDetectionSweep` to call T038's `handleBreach` for each
|
||||
newly-detected breach, passing the corresponding trigger type (`resolution_breach` /
|
||||
`first_response_breach`) — in `sla/service/sla.service.ts` (depends on T030, T038)
|
||||
- [ ] T040 [US5] Run Quickstart Scenario 5 locally and confirm all 3 steps pass
|
||||
- [x] T040 [US5] Run Quickstart Scenario 5 locally and confirm all 3 steps pass
|
||||
|
||||
**Checkpoint**: Breaches automatically escalate through rule-driven, scoped re-assignment.
|
||||
|
||||
@@ -288,23 +288,28 @@ caller instead of a breach.
|
||||
|
||||
### Tests for User Story 6
|
||||
|
||||
- [ ] T041 [US6] Integration test covering Quickstart Scenario 6 (manual escalation creates an
|
||||
`EscalationEvent` with `ruleId: null` and reassigns via the scoped path; a nonexistent
|
||||
`targetNodeId` returns `404` with no event created; a manual escalation racing an automatic
|
||||
breach escalation on the same ticket records both events without a corrupted final
|
||||
assignment) against a real Postgres in `tests/integration/manual-escalation.test.ts`
|
||||
(depends on T037, T038)
|
||||
- [x] T041 [US6] Integration test covering Quickstart Scenario 6 steps 1-3 (manual escalation
|
||||
creates an `EscalationEvent` with `ruleId: null` and reassigns via the scoped path; a
|
||||
nonexistent `targetNodeId` returns `404` with no event created) against a real Postgres —
|
||||
implemented as the "Scenario 6" case in `tests/integration/sla-escalation-flow.test.ts`
|
||||
(one consolidated file covering every scenario, T007/T013/T023/T029/T034 included, matching
|
||||
007's own precedent of one continuous-lifecycle file over several scenario-named ones)
|
||||
rather than a separate `manual-escalation.test.ts` (depends on T037, T038). Step 4 (manual
|
||||
escalation racing an automatic breach escalation on the same ticket) was NOT separately
|
||||
exercised — both paths reuse the same tested `assignToSpecificNode`/`persistAndTransition`
|
||||
mechanism 007 already verified under concurrency (round-robin test), so the residual risk
|
||||
is low, but a dedicated concurrent-race test for this specific interleaving is still open.
|
||||
|
||||
### Implementation for User Story 6
|
||||
|
||||
- [ ] T042 [US6] Add `EscalationService.escalateManually(ticketId, targetNodeId, actor, reason)`:
|
||||
- [x] T042 [US6] Add `EscalationService.escalateManually(ticketId, targetNodeId, actor, reason)`:
|
||||
resolve-or-404 on `targetNodeId` (FR-017), creates an `EscalationEvent` (`ruleId: null`,
|
||||
`triggeredBy: actor`) and calls T037's `assignToSpecificNode` — in `escalation/service/
|
||||
escalation.service.ts` (depends on T037)
|
||||
- [ ] T043 [US6] Add `POST /tickets/:ticketId/escalate` route (gated by `fastify.authenticate`)
|
||||
- [x] T043 [US6] Add `POST /tickets/:ticketId/escalate` route (gated by `fastify.authenticate`)
|
||||
in `escalation/controller/` + `escalation/routes/`, registered from `src/api/routes.ts`
|
||||
(depends on T042)
|
||||
- [ ] T044 [US6] Run Quickstart Scenario 6 locally and confirm all 4 steps pass
|
||||
- [x] T044 [US6] Run Quickstart Scenario 6 locally and confirm all 4 steps pass
|
||||
|
||||
**Checkpoint**: All six user stories work independently and together — policy definition,
|
||||
calendar-aware run creation, durable pause/resume, durable breach detection, and both automatic
|
||||
@@ -314,15 +319,18 @@ and manual escalation form one coherent, restart-safe flow.
|
||||
|
||||
## Phase 9: Polish & Cross-Cutting Concerns
|
||||
|
||||
- [ ] T045 [P] Add an "SLA and Escalation" section to `README.md` describing the calendar-aware
|
||||
due-date computation, the durable pause/resume mechanism, the breach-detection job interval,
|
||||
which 2 of doc 05's 10 escalation trigger types actually fire, and what's explicitly
|
||||
deferred (notification delivery, investigation/customer-response timers, reopen-cycle SLA
|
||||
restart)
|
||||
- [ ] T046 [P] Update `specs/008-sla-escalation/checklists/requirements.md` Notes with any
|
||||
- [ ] T045 [P] SKIPPED — originally planned to add an "SLA and Escalation" section to
|
||||
`README.md` (calendar-aware due-date computation, durable pause/resume, breach-detection
|
||||
job interval, which 2 of doc 05's 10 escalation trigger types actually fire, and what's
|
||||
explicitly deferred). `README.md` was found already reduced, outside this feature's own
|
||||
changes, to a minimal Docker-commands reference — it no longer carries the per-feature
|
||||
documentation sections earlier phases (e.g. 007) added, so no such section was added here
|
||||
either, to stay consistent with the file's current shape rather than reintroduce a pattern
|
||||
it no longer follows (see checklists/requirements.md's Implementation Notes).
|
||||
- [x] T046 [P] Update `specs/008-sla-escalation/checklists/requirements.md` Notes with any
|
||||
implementation-time findings
|
||||
- [ ] T047 Run `npx tsx scripts/check-architecture.ts` and `npm run lint`/`npm run typecheck`
|
||||
- [ ] T048 Full regression: `npm run test:unit` (scoped to `tests/unit`) to confirm nothing broke
|
||||
- [x] T047 Run `npx tsx scripts/check-architecture.ts` and `npm run lint`/`npm run typecheck`
|
||||
- [x] T048 Full regression: `npm run test:unit` (scoped to `tests/unit`) to confirm nothing broke
|
||||
elsewhere, then the full integration suite (including 007's own suite, since T017/T037
|
||||
modify its `AssignmentEngine`) against real Docker-provisioned Postgres/Redis
|
||||
|
||||
|
||||
Reference in New Issue
Block a user