import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { createTestContext } from '../test/helpers.ts' let ctx: Awaited> let manager: Awaited> let supervisor: Awaited> let site: { id: string } const year = new Date().getFullYear() /** An incident earlier this year (Jan 15, local time) so it lands on this year's log. */ async function incident(title = 'Hand caught in press') { const occurredAt = new Date(year, 0, 15, 10, 30).toISOString() const res = await supervisor.post('/api/incidents', { title, type: 'injury', severity: 'high', occurredAt, siteId: site.id, locationDetail: 'Line 3' }) return res.body.incident.number as number } const recordableAnswers = { workRelated: true, newCase: true, jobTitle: 'Press operator', natureOfInjury: 'Laceration', bodyPart: 'left hand', objectSubstance: 'Press die', dateOfBirth: '1990-04-02', dateOfHire: '2019-06-10', activityBefore: 'Clearing a jam', whatHappened: 'Hand slipped into the die area', sex: 'M', timeStartedWork: '07:00', } beforeAll(async () => { ctx = await createTestContext() site = await ctx.createSite('Main Plant') manager = await ctx.createUser('safety_manager') supervisor = await ctx.createUser('supervisor') }) afterAll(() => ctx.close()) describe('injured people (OSHA cases)', () => { it('keeps medical details to safety managers', async () => { const number = await incident() await manager.post(`/api/osha/incidents/${number}/cases`, { name: 'Pat Doe' }) expect((await supervisor.get(`/api/osha/incidents/${number}/cases`)).status).toBe(403) expect((await supervisor.post(`/api/osha/incidents/${number}/cases`, { name: 'X' })).status).toBe(403) const detail = (await supervisor.get(`/api/incidents/${number}`)).body.incident expect(detail).toMatchObject({ injuredCount: 1, can: { manageOsha: false } }) // The timeline says a person was added, without naming them const timeline = (await supervisor.get(`/api/incidents/${number}/timeline`)).body.timeline expect(JSON.stringify(timeline)).not.toContain('Pat Doe') }) it('works out recordability from the answers, and re-checks on every change', async () => { const number = await incident() let res = await manager.post(`/api/osha/incidents/${number}/cases`, { name: 'Sam Lee' }) expect(res.body.case).toMatchObject({ recordable: false, outcome: null, caseNumber: `INC-${String(number).padStart(4, '0')}` }) const id = res.body.case.id res = await manager.patch(`/api/osha/cases/${id}`, { ...recordableAnswers }) expect(res.body.case).toMatchObject({ recordable: false }) // first aid only so far res = await manager.patch(`/api/osha/cases/${id}`, { medicalTreatment: true }) expect(res.body.case).toMatchObject({ recordable: true, outcome: 'other' }) res = await manager.patch(`/api/osha/cases/${id}`, { daysAwayFromWork: true, daysAway: 4, restrictedOrTransfer: true, daysRestricted: 10 }) expect(res.body.case).toMatchObject({ outcome: 'days_away', daysAway: 4, daysRestricted: 10, missingFields: [] }) res = await manager.patch(`/api/osha/cases/${id}`, { workRelated: false }) expect(res.body.case.recordable).toBe(false) await manager.patch(`/api/osha/cases/${id}`, { workRelated: true }) }) it('numbers each person on an incident and enforces the 180-day cap', async () => { const number = await incident() await manager.post(`/api/osha/incidents/${number}/cases`, { name: 'First' }) const second = await manager.post(`/api/osha/incidents/${number}/cases`, { name: 'Second' }) expect(second.body.case.caseNumber).toBe(`INC-${String(number).padStart(4, '0')}-2`) const res = await manager.patch(`/api/osha/cases/${second.body.case.id}`, { daysAwayFromWork: true, daysAway: 150, restrictedOrTransfer: true, daysRestricted: 40 }) expect(res.status).toBe(400) expect(res.body.error).toMatch(/180/) }) it('ignores day counts when the outcome does not apply', async () => { const number = await incident() const created = await manager.post(`/api/osha/incidents/${number}/cases`, { name: 'Counts', daysAway: 12 }) expect(created.body.case.daysAway).toBe(0) }) it('alerts safety managers when a case must be reported to OSHA, once', async () => { const number = await incident('Finger amputation at shear') const created = await manager.post(`/api/osha/incidents/${number}/cases`, { name: 'Alex Kim', ...recordableAnswers, amputation: true, medicalTreatment: true }) expect(created.body.case.severeReport).toMatchObject({ withinHours: 24, reasons: ['amputation'] }) await ctx.notifier.flush() const alert = ctx.mailer.last(manager.user.email) expect(alert?.subject).toMatch(/^Report to OSHA within 24 hours: INC-\d{4}$/) expect(alert?.text).toContain('1-800-321-OSHA') const before = ctx.mailer.sent.length await manager.patch(`/api/osha/cases/${created.body.case.id}`, { reportedToOshaAt: new Date().toISOString(), lossOfEye: true }) await ctx.notifier.flush() expect(ctx.mailer.sent.length).toBe(before) }) }) describe('OSHA log, summary and rates', () => { let other: { id: string } beforeAll(async () => { other = await ctx.createSite('Second Plant') const occurredAt = new Date(year, 1, 3, 14, 5).toISOString() const res = await supervisor.post('/api/incidents', { title: 'Needlestick', type: 'injury', severity: 'medium', occurredAt, siteId: other.id }) const number = res.body.incident.number const created = await manager.post(`/api/osha/incidents/${number}/cases`, { name: 'Jordan Private', ...recordableAnswers, specialCase: 'needlestick', privacyCase: true, restrictedOrTransfer: true, daysRestricted: 3, caseType: 'other_illness' }) if (!created.body.case.recordable) throw new Error('setup: needlestick case should be recordable') // Not recordable: should stay off the log await manager.post(`/api/osha/incidents/${number}/cases`, { name: 'First Aid Only', ...recordableAnswers }) await manager.put(`/api/osha/sites/${other.id}/hours`, { year, months: Array.from({ length: 12 }, (_, i) => ({ month: i + 1, hours: 10_000, averageEmployees: i < 6 ? 50 : 70 })), }) }) it('builds the 300 Log with privacy cases hidden', async () => { const log = (await manager.get(`/api/osha/log?siteId=${other.id}&year=${year}`)).body expect(log.rows).toHaveLength(1) expect(log.rows[0]).toMatchObject({ employeeName: 'Privacy case', jobTitle: 'Press operator', outcome: 'restricted', daysRestricted: 3, caseType: 'other_illness' }) expect(log.rows[0].description).toBe('Laceration (left hand); Press die') }) it('totals the 300A, computes rates and runs the ITA checks', async () => { const log = (await manager.get(`/api/osha/log?siteId=${other.id}&year=${year}`)).body expect(log.totals).toMatchObject({ deaths: 0, daysAwayCases: 0, restrictedCases: 1, otherCases: 0, daysRestricted: 3 }) expect(log).toMatchObject({ totalHoursWorked: 120_000, annualAverageEmployees: 60, maxEmployees: 70 }) expect(log.rates).toEqual({ trir: 1.67, dart: 1.67, ltir: 0 }) expect(log.checks.errors).toEqual([]) expect(log.missingEstablishmentFields).toEqual(['street address', 'city', 'state', 'ZIP code', 'NAICS code', 'EIN']) const empty = (await manager.get(`/api/osha/log?siteId=${site.id}&year=${year}`)).body expect(empty.checks.errors).toContain('Enter hours worked for each month.') }) it('validates and normalises establishment details', async () => { expect((await manager.patch(`/api/sites/${other.id}`, { naicsCode: '33211' })).status).toBe(400) const res = await manager.patch(`/api/sites/${other.id}`, { streetAddress: '1 Mill Rd', city: 'Dayton', state: 'oh', zip: '45402-1234', naicsCode: '332119', ein: '12-3456789' }) expect(res.body.site).toMatchObject({ state: 'OH', zip: '454021234', ein: '123456789' }) }) it('exports the ITA 300A file in OSHA’s format', async () => { const res = await manager.get(`/api/osha/ita/summary.csv?year=${year}`) const lines = res.body.toString('utf8').trimEnd().split('\r\n') expect(lines[0].charCodeAt(0)).not.toBe(0xfeff) expect(lines[0]).toBe( 'establishment_name,ein_number,company_name,street_address,city,state,zip,naics_code,industry_description,size,establishment_type,year_filing_for,annual_average_employees,total_hours_worked,no_injuries_illnesses,total_deaths,total_dafw_cases,total_djtr_cases,total_other_cases,total_dafw_days,total_djtr_days,total_injuries,total_skin_disorders,total_respiratory_conditions,total_poisonings,total_hearing_loss,total_other_illnesses,change_reason', ) const second = lines.find((line: string) => line.startsWith('Second Plant')) expect(second).toBe(`Second Plant,123456789,Northstar Co.,1 Mill Rd,Dayton,OH,454021234,332119,,21,1,${year},60,120000,1,0,0,1,0,0,3,0,0,0,0,0,1,`) }) it('exports ITA case data without names', async () => { const res = await manager.get(`/api/osha/ita/cases.csv?year=${year}`) const csv = res.body.toString('utf8') expect(csv).not.toContain('Jordan Private') expect(csv).not.toContain('First Aid Only') const [header, ...rows] = csv.trimEnd().split('\r\n') expect(header.split(',')).toHaveLength(24) const row = rows.find((line: string) => line.startsWith('Second Plant'))! expect(row).toContain(`02-03-${year}`) // mm-dd-yyyy expect(row).toContain(',3,0,3,6,04-02-1990,06-10-2019,M,0,0,07:00,14:05,,') // outcome, days, type, dates, sex, treatment, times }) it('produces a Form 301 for a case', async () => { const log = (await manager.get(`/api/osha/log?siteId=${other.id}&year=${year}`)).body const form = (await manager.get(`/api/osha/cases/${log.rows[0].caseId}/301`)).body expect(form).toMatchObject({ establishment: { name: 'Second Plant', city: 'Dayton' }, case: { name: 'Jordan Private', privacyCase: true }, incident: { title: 'Needlestick' } }) }) })