import { describe, expect, it } from 'vitest' import { assessRecordability, capDays, FIRST_AID, incidenceRate, itaSizeCode, severeInjuryReport, summaryChecks, type RecordabilityInput, type SummaryTotals } from './osha.ts' const base: RecordabilityInput = { employmentType: 'employee', workRelated: true, newCase: true, died: false, daysAwayFromWork: false, restrictedOrTransfer: false, medicalTreatment: false, lossOfConsciousness: false, significantDiagnosis: false, specialCase: 'none', } const assess = (overrides: Partial) => assessRecordability({ ...base, ...overrides }) describe('assessRecordability', () => { it('first aid only is not recordable', () => { expect(assess({})).toMatchObject({ status: 'not_recordable', outcome: null }) }) it('waits for the work-related and new-case answers', () => { expect(assess({ workRelated: null, medicalTreatment: true }).status).toBe('incomplete') expect(assess({ newCase: null, medicalTreatment: true }).status).toBe('incomplete') }) it('excludes non-work-related cases and recurrences', () => { expect(assess({ workRelated: false, died: true }).status).toBe('not_recordable') expect(assess({ newCase: false, medicalTreatment: true }).reasons[0]).toMatch(/1904\.6/) }) it('keeps contractors and visitors off your log', () => { expect(assess({ employmentType: 'contractor', daysAwayFromWork: true }).reasons[0]).toMatch(/1904\.31/) expect(assess({ employmentType: 'visitor', died: true }).status).toBe('not_recordable') expect(assess({ employmentType: 'temporary', medicalTreatment: true }).status).toBe('recordable') }) it('classifies by the most serious outcome', () => { expect(assess({ medicalTreatment: true }).outcome).toBe('other') expect(assess({ medicalTreatment: true, restrictedOrTransfer: true }).outcome).toBe('restricted') expect(assess({ restrictedOrTransfer: true, daysAwayFromWork: true }).outcome).toBe('days_away') expect(assess({ daysAwayFromWork: true, died: true }).outcome).toBe('death') }) it('records each general criterion on its own', () => { for (const key of ['lossOfConsciousness', 'significantDiagnosis', 'medicalTreatment'] as const) { expect(assess({ [key]: true })).toMatchObject({ status: 'recordable', outcome: 'other' }) } }) it('records special cases even without general criteria', () => { for (const specialCase of ['needlestick', 'medical_removal', 'hearing_loss', 'tuberculosis'] as const) { expect(assess({ specialCase }).status).toBe('recordable') } expect(assess({ specialCase: 'needlestick' }).reasons[0]).toMatch(/1904\.8/) }) it('lists every reason that applies', () => { expect(assess({ medicalTreatment: true, lossOfConsciousness: true }).reasons).toHaveLength(2) }) }) describe('severeInjuryReport', () => { it('requires fatalities within 8 hours and other severe injuries within 24', () => { expect(severeInjuryReport({ died: true, inpatientHospitalization: true, amputation: false, lossOfEye: false })).toEqual({ withinHours: 8, reasons: ['work-related fatality'] }) expect(severeInjuryReport({ died: false, inpatientHospitalization: false, amputation: true, lossOfEye: true })).toEqual({ withinHours: 24, reasons: ['amputation', 'loss of an eye'] }) expect(severeInjuryReport({ died: false, inpatientHospitalization: false, amputation: false, lossOfEye: false })).toBeNull() }) }) describe('capDays', () => { it('stops counting at 180 days in total', () => { expect(capDays(10, 5)).toEqual({ daysAway: 10, daysRestricted: 5 }) expect(capDays(150, 60)).toEqual({ daysAway: 150, daysRestricted: 30 }) expect(capDays(200, 10)).toEqual({ daysAway: 180, daysRestricted: 0 }) }) }) describe('rates and codes', () => { it('computes incidence rates per 200,000 hours', () => { expect(incidenceRate(3, 200_000)).toBe(3) expect(incidenceRate(2, 150_000)).toBe(2.67) expect(incidenceRate(1, 0)).toBeNull() }) it('maps headcount to ITA size codes', () => { expect([19, 20, 99, 100, 249, 250].map(itaSizeCode)).toEqual([1, 21, 21, 22, 22, 3]) }) it('has the complete 14-item first-aid list', () => { expect(FIRST_AID).toHaveLength(14) }) }) describe('summaryChecks (OSHA ITA validations)', () => { const totals = (overrides: Partial = {}): SummaryTotals => ({ deaths: 0, daysAwayCases: 1, restrictedCases: 1, otherCases: 1, daysAway: 5, daysRestricted: 3, byType: { injury: 3, skin_disorder: 0, respiratory: 0, poisoning: 0, hearing_loss: 0, other_illness: 0 }, ...overrides, }) it('passes a consistent summary', () => { expect(summaryChecks(totals(), 50, 100_000)).toEqual({ errors: [], warnings: [] }) }) it('catches the cross-field rules OSHA enforces', () => { expect(summaryChecks(totals({ byType: { ...totals().byType, injury: 2 } }), 50, 100_000).errors[0]).toMatch(/add up to 2/) expect(summaryChecks(totals({ daysAway: 0 }), 50, 100_000).errors).toContain('There are days-away cases (H) but no days away (K).') expect(summaryChecks(totals({ restrictedCases: 0, daysAwayCases: 0, otherCases: 3, daysAway: 0 }), 50, 100_000).errors).toContain( 'There are restricted days (L) but no days-away or restriction cases (H or I).', ) expect(summaryChecks(totals(), 0, 0).errors).toHaveLength(2) expect(summaryChecks(totals(), 10, 100_000).errors[0]).toMatch(/more than the hours in a year/) expect(summaryChecks(totals(), 50, 20_000).warnings[0]).toMatch(/Fewer than 500/) }) })