Files
support_backend/tests/unit/platform/business-calendars/calendar-walk.test.ts
T
saqib mirandClaude Sonnet 5 9357f03e1d 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>
2026-09-03 13:02:05 +05:30

98 lines
4.8 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { addBusinessMinutes, isWithinWorkingHours } from '@/modules/platform/business-calendars/calculators/business-hours.calculator';
const MON_FRI_9_TO_5 = {
timezone: 'America/New_York',
workingHours: {
mon: { start: '09:00', end: '17:00' },
tue: { start: '09:00', end: '17:00' },
wed: { start: '09:00', end: '17:00' },
thu: { start: '09:00', end: '17:00' },
fri: { start: '09:00', end: '17:00' },
},
};
// America/New_York is UTC-5 (EST) outside DST, UTC-4 (EDT) during DST — every Date.UTC(...)
// below is written as the equivalent UTC instant for the Eastern wall-clock time noted in the
// comment, since luxon's own DST handling is exactly what's under test here.
describe('addBusinessMinutes', () => {
it('stays within the same working day when enough time remains', () => {
// Mon 2026-01-05 10:00 EST (UTC-5) -> 15:00 UTC
const start = new Date(Date.UTC(2026, 0, 5, 15, 0));
const due = addBusinessMinutes(start, 120, MON_FRI_9_TO_5, []);
// +120 min -> 12:00 EST -> 17:00 UTC
expect(due.toISOString()).toBe(new Date(Date.UTC(2026, 0, 5, 17, 0)).toISOString());
});
it('rolls over a weekend, never counting Saturday/Sunday as available time', () => {
// Fri 2026-01-02 16:00 EST (1 hour left in the window) -> 21:00 UTC
const start = new Date(Date.UTC(2026, 0, 2, 21, 0));
// 1 hour left Friday + need 3 more hours -> Monday 12:00 EST
const due = addBusinessMinutes(start, 240, MON_FRI_9_TO_5, []);
expect(due.toISOString()).toBe(new Date(Date.UTC(2026, 0, 5, 17, 0)).toISOString()); // Mon 12:00 EST
});
it('excludes a holiday entirely, rolling to the next working day', () => {
// Fri 2026-01-02 16:00 EST, Monday Jan 5 is a holiday
const start = new Date(Date.UTC(2026, 0, 2, 21, 0));
const holiday = new Date(Date.UTC(2026, 0, 5, 17, 0)); // Mon 12:00 EST — safely mid-Monday
const due = addBusinessMinutes(start, 240, MON_FRI_9_TO_5, [holiday]);
// 1h Friday + Monday excluded + 3h into Tuesday -> Tue 12:00 EST
expect(due.toISOString()).toBe(new Date(Date.UTC(2026, 0, 6, 17, 0)).toISOString());
});
it('a weekday with no configured window contributes zero available time', () => {
const noWednesday = {
timezone: 'America/New_York',
workingHours: {
tue: { start: '09:00', end: '17:00' },
thu: { start: '09:00', end: '17:00' },
},
};
// Tue 2026-01-06 16:00 EST (1h left)
const start = new Date(Date.UTC(2026, 0, 6, 21, 0));
const due = addBusinessMinutes(start, 300, noWednesday, []);
// 1h Tue + (Wed skipped, zero hours) + 4h into Thu -> Thu 13:00 EST
expect(due.toISOString()).toBe(new Date(Date.UTC(2026, 0, 8, 18, 0)).toISOString());
});
it('clips to the start time on the first day, never counting time before it', () => {
// Mon 10:00 EST: 7h remain in the window (to 17:00) + 1 more hour -> Tue 10:00 EST
const start = new Date(Date.UTC(2026, 0, 5, 15, 0));
const due = addBusinessMinutes(start, 420 + 60, MON_FRI_9_TO_5, []);
expect(due.toISOString()).toBe(new Date(Date.UTC(2026, 0, 6, 15, 0)).toISOString()); // Tue 10:00 EST
});
it('returns the start time unchanged when minutes is zero or negative', () => {
const start = new Date(Date.UTC(2026, 0, 5, 15, 0));
expect(addBusinessMinutes(start, 0, MON_FRI_9_TO_5, []).toISOString()).toBe(start.toISOString());
});
it('is correct across a DST transition (US spring-forward, March 2026)', () => {
// 2026-03-08 is the US DST transition (02:00 -> 03:00). Fri 2026-03-06 16:00 EST (1h left).
const start = new Date(Date.UTC(2026, 2, 6, 21, 0));
// 1h Friday + weekend skipped + 3h into Monday 2026-03-09 (now EDT, UTC-4) -> 12:00 EDT -> 16:00 UTC
const due = addBusinessMinutes(start, 240, MON_FRI_9_TO_5, []);
expect(due.toISOString()).toBe(new Date(Date.UTC(2026, 2, 9, 16, 0)).toISOString());
});
});
describe('isWithinWorkingHours', () => {
it('is true inside a configured window and false outside it', () => {
const insideWindow = new Date(Date.UTC(2026, 0, 5, 16, 0)); // Mon 11:00 EST
const beforeWindow = new Date(Date.UTC(2026, 0, 5, 13, 0)); // Mon 08:00 EST
expect(isWithinWorkingHours(insideWindow, MON_FRI_9_TO_5, [])).toBe(true);
expect(isWithinWorkingHours(beforeWindow, MON_FRI_9_TO_5, [])).toBe(false);
});
it('is false on a weekend and on a holiday', () => {
const saturday = new Date(Date.UTC(2026, 0, 3, 16, 0));
expect(isWithinWorkingHours(saturday, MON_FRI_9_TO_5, [])).toBe(false);
const mondayNoon = new Date(Date.UTC(2026, 0, 5, 16, 0)); // Mon 11:00 EST
const holiday = new Date(Date.UTC(2026, 0, 5, 17, 0)); // Mon 12:00 EST — safely mid-Monday
expect(isWithinWorkingHours(mondayNoon, MON_FRI_9_TO_5, [holiday])).toBe(false);
});
});