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>
4.6 KiB
4.6 KiB
Contract: SLA and Escalation
Every admin CRUD/manual-escalation route below is gated by fastify.authenticate (research.md —
known limitation inherited from 002/003/004/005/006/007). SLA-run creation, pause/resume, and
breach detection have no public trigger endpoint — they run automatically off the domain event
bus and the breach-detection BullMQ job (research.md), matching 007's "orchestration has no
manual trigger endpoint" precedent.
SLA Policy admin
POST /admin/sla-policies— body{ name, productId?, categoryId?, problemTypeId?, priority?, firstResponseMinutes, investigationMinutes?, resolutionMinutes, customerResponseMinutes?, businessCalendarId? }.404ifproductId/categoryId/businessCalendarIdis given but doesn't exist.GET /admin/sla-policies— list, optionally filtered byproductId.GET /admin/sla-policies/:id—404if not found.PATCH /admin/sla-policies/:id— partial update, same existence checks as create.DELETE /admin/sla-policies/:id— soft delete (active: false), never a hard delete (matches 005/006 precedent for policy-shaped config the system may still reference).
Business Calendar admin
POST /admin/business-calendars— body{ name, timezone, workingHours }.400iftimezoneisn't a valid IANA zone name, or if anyworkingHoursentry'sstart/endisn't a validHH:mmpair withstart < end.GET /admin/business-calendars/GET /admin/business-calendars/:id—404if not found.PATCH /admin/business-calendars/:id— same validation as create.POST /admin/business-calendars/:id/holidays— body{ date, description? }.DELETE /admin/business-calendars/:id/holidays/:holidayId.
Escalation Policy / Rule admin
POST /admin/escalation-policies— body{ name, productId? }.404ifproductIdgiven but doesn't exist.GET /admin/escalation-policies/GET /admin/escalation-policies/:id.POST /admin/escalation-policies/:id/rules— body{ triggerType, condition, targetNodeId, notify, active? }.triggerTypevalidated against doc 05 §6's full 10-value set (research.md — only 2 are ever evaluated, all 10 are valid config).404iftargetNodeIddoesn't reference an existingHierarchyNode(FR-012).PATCH /admin/escalation-policies/:id/rules/:ruleId— same validation as create.DELETE /admin/escalation-policies/:id/rules/:ruleId— soft delete (active: false).
SLA run reads
GET /tickets/:ticketId/sla-run— the currentSLARunfor the ticket, or404if none was ever created (e.g. the ticket was never assigned, or no policy matched at assignment time).
Manual escalation
POST /tickets/:ticketId/escalate— body{ targetNodeId, reason }.404ifticketIdortargetNodeIddoesn't exist (FR-017). Records anEscalationEventwithtriggeredByset to the calling actor and re-assigns via the same scoped-assignment path a rule-fired escalation uses (research.md).
Guarantees (callable contract)
- An
SLARunis created the moment a ticket receives its first successful assignment (007), if and only if an activeSLAPolicymatches the ticket's context — never for an unassigned ticket, never inventing a default policy when none matches (FR-005, US2). firstResponseDueAt/resolutionDueAtare always computed by walking the resolved policy's business calendar, excluding non-working hours, weekends, and holidays — never a naivecreatedAt + N hoursaddition (FR-004, SC-001).- A ticket entering
WAITING_FOR_CUSTOMERpauses its runningSLARun; leaving it resumes with the remaining time preserved exactly — the paused duration is neither double-counted nor dropped, and this holds even if the process restarts while paused (FR-007/FR-008, SC-002). - A breach is detected within one breach-detection job cycle of its due date passing, even if the process wasn't running at the exact due instant — never silently missed (FR-009, SC-003).
- A run that completes before its due date is never marked breached; a paused run is never marked breached (FR-010/FR-011).
- Every
resolution_breachorfirst_response_breachdetection evaluates every activeEscalationRulematching that trigger type under the ticket's resolvedEscalationPolicy, firing oneEscalationEvent(and one scoped re-assignment) per matching rule — a breach with no matching rule is still recorded as breached, with noEscalationEvent(FR-013/FR-014/ FR-015, SC-004). - A manual escalation to a nonexistent
targetNodeIdalways returns404and creates neither anEscalationEventnor a reassignment (FR-017, SC-005).