Files
open-safetylog-ehs/server/routes/osha.test.ts
T
jayjennings 61c876edec
CI / Lint, typecheck, test, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s
Add OSHA Form 301 and Osha page components
- Implemented Form301 component for displaying OSHA Form 301 equivalent, including employee and case information.
- Created Osha component to manage OSHA recordkeeping, including tabs for 300 Log, 300A Summary, Hours & rates, and Submit to OSHA.
- Added functionality for printing and downloading CSV files for OSHA submissions.
- Integrated loading and error handling states for data fetching.
2026-10-06 13:09:57 -07:00

177 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { createTestContext } from '../test/helpers.ts'
let ctx: Awaited<ReturnType<typeof createTestContext>>
let manager: Awaited<ReturnType<typeof ctx.createUser>>
let supervisor: Awaited<ReturnType<typeof ctx.createUser>>
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' } })
})
})