Files
support_backend/specs/008-sla-escalation/data-model.md
T
saqib mirandClaude Sonnet 5 199bd4eb4e plan: design for SLA and escalation feature (008)
Phase 0 research resolves the business-calendar working-hours algorithm
(day-by-day walk via luxon, the first date/timezone dependency in this
codebase), the workingHours JSON shape, most-specific SLA-policy match
(reusing 005/006's resolution pattern), durable pause/resume (absolute
due-date shift, no in-memory state), and a repeatable-job breach-detection
design over per-run delayed jobs. Phase 1 adds data-model.md (one additive
refinement beyond doc06: SLARun.firstResponseBreachedAt), the admin/read
contract, and six quickstart scenarios including a genuine process-restart
boundary test for Constitution Principle VII.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 12:23:35 +05:30

8.0 KiB

Data Model: SLA and Escalation

Field shapes below match docs/06-database-schema.md "Domain: SLA" / "Domain: Escalation" exactly, with two additive refinements called out explicitly (both purely additive — nothing in doc 06's shape is removed or narrowed).

SLAPolicy

Field Type Notes
id String @id @default(cuid())
name String
productId String? wildcard when null — FK to Product.id
categoryId String? wildcard when null — FK to Category.id
problemTypeId String? wildcard when null — free-text reference, no ProblemType table exists in this codebase (problem taxonomy lives on Problem directly, per 004/005); stored and matched as opaque text
priority String? wildcard when null — free-text, matches Ticket.priority
firstResponseMinutes Int required — every policy must define a first-response target
investigationMinutes Int? stored per doc 06; not read by any calculation in this feature (spec.md Assumptions)
resolutionMinutes Int required
customerResponseMinutes Int? stored per doc 06; not read by any calculation in this feature (spec.md Assumptions)
businessCalendarId String? FK to BusinessCalendar.id; null means "24/7, no exclusions" (an explicit policy choice, not a missing-calendar error)
active Boolean @default(true) inactive policies are excluded from resolution
createdAt / updatedAt DateTime updatedAt used as the resolution tie-break (research.md)

Validation (Zod, at the schema layer): firstResponseMinutes > 0, resolutionMinutes > 0, investigationMinutes/customerResponseMinutes positive when present; productId/categoryId/ businessCalendarId must reference an existing row when provided (repository-level existence check, same convention as every prior feature's FK-shaped free-form input).

Resolution (findApplicablePolicy(ticket)): among active policies where each set scope field equals the ticket's corresponding value and each unset field is a wildcard, return the one with the fewest wildcards; tie-break by latest updatedAt. No match → no SLARun is created (FR-005).

SLARun

Field Type Notes
id String @id @default(cuid())
ticketId String @unique one run per ticket — no reopen-cycle support (spec.md Assumptions)
policyId String FK to SLAPolicy.id, the policy resolved at creation time
firstResponseDueAt DateTime? computed via the calendar walk from assignedAt; null when the policy has no firstResponseMinutes... (always present per policy validation, so effectively always set)
resolutionDueAt DateTime? computed the same way from resolutionMinutes
status String running | paused | warning | breached | completed — matches doc 06 exactly
pausedAt DateTime? set when status transitions to paused; cleared on resume
resumedAt DateTime? last resume timestamp, informational (audit convenience, mirrors AssignmentHistory's always-append style)
breachedAt DateTime? set once, the first time resolutionDueAt is detected passed while running
completedAt DateTime? set when the ticket reaches a resolved/closed status; a completed run is never later marked breached (FR-011)
firstResponseBreachedAt DateTime? additive refinement, not in doc 06's literal listing — records the first-response breach separately from status/breachedAt, which this feature reserves for the resolution timer; doubles as the idempotency guard for the breach-detection job (research.md)

Status transitions (enforced in the service layer, not a DB constraint — same convention as Ticket.status's 12-state machine in 003): running → paused (on ticket entering WAITING_FOR_CUSTOMER) → running (on leaving it, due dates shifted forward by the pause duration) → breached (resolution due date passed while running) → completed (ticket resolved/ closed, from any of running/paused/breached). warning is reserved by doc 06's enum for a future near-breach signal; no code path in this feature sets it (documented, not implemented — same discipline as the 8 inert EscalationRule.triggerType values).

BusinessCalendar

Field Type Notes
id String @id @default(cuid())
name String
timezone String IANA zone name (e.g. "America/New_York"), validated against Intl.supportedValuesOf('timeZone') at the schema layer
workingHours Json shape: { mon?: {start: "HH:mm", end: "HH:mm"}, tue?: ..., wed?: ..., thu?: ..., fri?: ..., sat?: ..., sun?: ... } — a missing key means zero working hours that weekday (research.md)
holidays Holiday[]

Holiday

Field Type Notes
id String @id @default(cuid())
calendarId String FK to BusinessCalendar.id
date DateTime compared by calendar date only (year/month/day in the calendar's own timezone), not by exact instant
description String?

EscalationPolicy

Field Type Notes
id String @id @default(cuid())
name String
productId String? wildcard (global) when null
active Boolean @default(true)
rules EscalationRule[]

EscalationRule

Field Type Notes
id String @id @default(cuid())
policyId String FK to EscalationPolicy.id
triggerType String one of doc 05 §6's 10 values; schema accepts all 10, only resolution_breach/first_response_breach are ever evaluated (research.md)
condition Json stored, not evaluated, by this feature (research.md)
targetNodeId String FK to HierarchyNode.id, validated to exist at creation time (FR-017's rejection rule applies identically here)
notify Json who/how to notify — stored and returned only; no delivery mechanism exists (spec.md Assumptions, platform/notifications untouched)
active Boolean @default(true)

EscalationEvent

Field Type Notes
id String @id @default(cuid())
ticketId String FK to Ticket.id
ruleId String? null for a manual escalation or a breach with no matching rule
fromNodeId String? the node the ticket was assigned to immediately before this event, if any
toNodeId String? the rule's targetNodeId (or the manually-specified node); null when no rule matched
reason String free text — for a rule firing, a generated description (e.g. "resolution SLA breached"); for manual escalation, the caller-supplied reason
triggeredBy String system | <agentId> | <adminId> — never a bare "customer" literal in this feature's own write paths (doc 06 lists it as a valid value for a future customer-initiated trigger type, not one this feature fires)
createdAt DateTime @default(now())

Relations added to existing models

  • Ticket.slaRun SLARun? (inverse of SLARun.ticketId @unique)
  • Ticket.escalationEvents EscalationEvent[]
  • Product.slaPolicies SLAPolicy[], Product.escalationPolicies EscalationPolicy[]
  • Category.slaPolicies SLAPolicy[]
  • HierarchyNode.escalationRules EscalationRule[] (inverse of targetNodeId)

Out of scope for this data model (per spec.md Assumptions)

  • No investigationDueAt/customerResponseDueAt fields — doc 06's SLARun doesn't define them, and nothing in spec.md's acceptance scenarios exercises them; investigationMinutes/ customerResponseMinutes remain stored-but-unused on SLAPolicy, same as doc 06 itself defines.
  • No FK tightening of HierarchyNode.slaPolicyId/escalationPolicyId (still free-text, per 006) — SLA policy resolution in this feature is scope-based (product/category/problemType/priority), not looked up through those two fields; they remain unvalidated free text, unchanged from 006.