import { allowedNextStatuses, canReadTicket, canTransition, isSlaOverdue, resolveDeadline, slaStatus, validateAttachment } from '../src/services/quality-rules'; describe('ticket status rules', () => { it('allows pending ticket to enter processing', () => { expect(canTransition('pending', 'processing')).toBe(true); }); it('allows pending ticket to be cancelled', () => { expect(canTransition('pending', 'cancelled')).toBe(true); }); it('rejects pending ticket resolved directly', () => { expect(canTransition('pending', 'resolved')).toBe(false); }); it('allows processing ticket to submit customer confirmation', () => { expect(canTransition('processing', 'confirming')).toBe(true); }); it('allows customer to return confirming ticket to processing', () => { expect(canTransition('confirming', 'processing')).toBe(true); }); it('allows customer to resolve confirming ticket', () => { expect(canTransition('confirming', 'resolved')).toBe(true); }); it('rejects closed ticket reopening through status machine', () => { expect(canTransition('closed', 'processing')).toBe(false); }); it('returns immutable next status list', () => { const next = allowedNextStatuses('pending'); next.push('closed'); expect(allowedNextStatuses('pending')).toEqual(['processing', 'cancelled']); }); }); describe('permission rules', () => { const acl = { creatorId: 'customer-1', ownerId: 'engineer-1', companyUserIds: ['customer-1', 'customer-2'] }; it('allows admin to read every ticket', () => { expect(canReadTicket('admin-1', 'admin', acl)).toBe(true); }); it('allows support to read every ticket', () => { expect(canReadTicket('support-1', 'support', acl)).toBe(true); }); it('allows ticket owner engineer to read assigned ticket', () => { expect(canReadTicket('engineer-1', 'engineer', acl)).toBe(true); }); it('rejects unrelated engineer', () => { expect(canReadTicket('engineer-2', 'engineer', acl)).toBe(false); }); it('allows creator customer to read own ticket', () => { expect(canReadTicket('customer-1', 'customer', acl)).toBe(true); }); it('allows same company customer to read company ticket', () => { expect(canReadTicket('customer-2', 'customer', acl)).toBe(true); }); it('rejects unrelated customer', () => { expect(canReadTicket('customer-3', 'customer', acl)).toBe(false); }); }); describe('SLA rules', () => { const createdAt = new Date('2026-06-01T00:00:00.000Z'); it('calculates urgent deadline within 4 hours', () => { expect(resolveDeadline({ priority: 'urgent', createdAt }).toISOString()).toBe('2026-06-01T04:00:00.000Z'); }); it('calculates high priority deadline within 24 hours', () => { expect(resolveDeadline({ priority: 'high', createdAt }).toISOString()).toBe('2026-06-02T00:00:00.000Z'); }); it('marks unresolved ticket overdue after deadline', () => { expect(isSlaOverdue({ priority: 'urgent', createdAt, now: new Date('2026-06-01T04:01:00.000Z') })).toBe(true); }); it('keeps ticket normal before warning window', () => { expect(slaStatus({ priority: 'high', createdAt, now: new Date('2026-06-01T12:00:00.000Z') })).toBe('normal'); }); it('marks ticket warning in last 20 percent time window', () => { expect(slaStatus({ priority: 'high', createdAt, now: new Date('2026-06-01T20:00:00.000Z') })).toBe('warning'); }); it('marks ticket overdue when resolved after deadline', () => { expect(isSlaOverdue({ priority: 'urgent', createdAt, now: new Date('2026-06-01T03:00:00.000Z'), resolvedAt: new Date('2026-06-01T04:30:00.000Z') })).toBe(true); }); }); describe('attachment validation rules', () => { it('accepts png attachment under 10MB', () => { expect(validateAttachment({ name: 'error.png', sizeMb: 2 }).valid).toBe(true); }); it('accepts log attachment under 10MB', () => { expect(validateAttachment({ name: 'console.log', sizeMb: 0.5 }).valid).toBe(true); }); it('rejects unsupported executable file', () => { expect(validateAttachment({ name: 'tool.exe', sizeMb: 1 })).toEqual({ valid: false, reason: 'UNSUPPORTED_TYPE' }); }); it('rejects empty file', () => { expect(validateAttachment({ name: 'empty.pdf', sizeMb: 0 })).toEqual({ valid: false, reason: 'EMPTY_FILE' }); }); it('rejects file larger than 10MB', () => { expect(validateAttachment({ name: 'large.pdf', sizeMb: 10.1 })).toEqual({ valid: false, reason: 'FILE_TOO_LARGE' }); }); });