93 lines
3.3 KiB
TypeScript
93 lines
3.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { reconcileWorkSession } from '../../src/domain/attendance/work-session.js';
|
|
|
|
const options = {
|
|
now: '2026-09-24T20:00:00.000Z',
|
|
openShiftThresholdMinutes: 14 * 60,
|
|
};
|
|
|
|
describe('attendance work-session reconciliation', () => {
|
|
it('sums several in/out intervals without inventing time between them', () => {
|
|
const result = reconcileWorkSession([
|
|
{ id: '1', type: 'check_in', occurredAt: '2026-09-24T01:00:00.000Z' },
|
|
{ id: '2', type: 'check_out', occurredAt: '2026-09-24T05:00:00.000Z' },
|
|
{ id: '3', type: 'check_in', occurredAt: '2026-09-24T06:00:00.000Z' },
|
|
{ id: '4', type: 'check_out', occurredAt: '2026-09-24T10:30:00.000Z' },
|
|
], options);
|
|
|
|
expect(result).toMatchObject({
|
|
startedAt: '2026-09-24T01:00:00.000Z',
|
|
endedAt: '2026-09-24T10:30:00.000Z',
|
|
workedMinutes: 510,
|
|
status: 'pending_review',
|
|
anomalies: [],
|
|
});
|
|
});
|
|
|
|
it('keeps the first check-in and flags a repeated check-in', () => {
|
|
const result = reconcileWorkSession([
|
|
{ id: '1', type: 'check_in', occurredAt: '2026-09-24T01:00:00.000Z' },
|
|
{ id: '2', type: 'check_in', occurredAt: '2026-09-24T01:05:00.000Z' },
|
|
{ id: '3', type: 'check_out', occurredAt: '2026-09-24T09:00:00.000Z' },
|
|
], options);
|
|
|
|
expect(result?.workedMinutes).toBe(480);
|
|
expect(result?.anomalies).toEqual(['repeated_check_in']);
|
|
});
|
|
|
|
it('keeps a recent unfinished shift open', () => {
|
|
const result = reconcileWorkSession([
|
|
{ id: '1', type: 'check_in', occurredAt: '2026-09-24T12:00:00.000Z' },
|
|
], options);
|
|
|
|
expect(result).toMatchObject({ status: 'open', endedAt: null, workedMinutes: 0, anomalies: [] });
|
|
});
|
|
|
|
it('flags an old unfinished shift without estimating payable minutes', () => {
|
|
const result = reconcileWorkSession([
|
|
{ id: '1', type: 'check_in', occurredAt: '2026-09-24T01:00:00.000Z' },
|
|
], options);
|
|
|
|
expect(result).toMatchObject({
|
|
status: 'pending_review',
|
|
endedAt: null,
|
|
workedMinutes: 0,
|
|
anomalies: ['missing_check_out'],
|
|
});
|
|
});
|
|
|
|
it('auto-closes only after the scheduler cutoff and leaves an audit-worthy anomaly', () => {
|
|
const result = reconcileWorkSession([
|
|
{ id: '1', type: 'check_in', occurredAt: '2026-09-23T12:00:00.000Z' },
|
|
], { ...options, automaticCloseAt: '2026-09-23T19:00:00.000Z' });
|
|
|
|
expect(result).toMatchObject({
|
|
endedAt: '2026-09-23T19:00:00.000Z',
|
|
workedMinutes: 420,
|
|
status: 'pending_review',
|
|
anomalies: ['auto_closed'],
|
|
});
|
|
});
|
|
|
|
it('records a check-out without check-in as an anomaly with zero minutes', () => {
|
|
const result = reconcileWorkSession([
|
|
{ id: '1', type: 'check_out', occurredAt: '2026-09-24T09:00:00.000Z' },
|
|
], options);
|
|
|
|
expect(result).toMatchObject({
|
|
startedAt: null,
|
|
endedAt: '2026-09-24T09:00:00.000Z',
|
|
workedMinutes: 0,
|
|
status: 'pending_review',
|
|
anomalies: ['missing_check_in'],
|
|
});
|
|
});
|
|
|
|
it('rejects invalid timestamps and thresholds', () => {
|
|
expect(() => reconcileWorkSession([
|
|
{ id: '1', type: 'check_in', occurredAt: 'not-a-date' },
|
|
], options)).toThrow('event 1 must be an ISO date-time');
|
|
expect(() => reconcileWorkSession([], { ...options, openShiftThresholdMinutes: 0 }))
|
|
.toThrow('openShiftThresholdMinutes must be a positive integer');
|
|
});
|
|
});
|