From 9718ab9ffa8924725bd93066a720a8f9c94fdd16 Mon Sep 17 00:00:00 2001 From: justin Date: Tue, 2 Jun 2026 21:27:44 -0500 Subject: [PATCH] DOT D&A binder: editable DOCX output, all 6 forms each full-page, service-aware delivery email - Rewrite dot_da_binder_generator.py to emit an editable .docx (was reportlab PDF) so carriers/counsel can review and adapt the program. ~4000 words, 10 sections. - Render all six required forms (A-F); previously only A, D, E existed. Each form starts on its own page (page break) and fills a page. - Mode-aware policy text for FMCSA/FRA/PHMSA/FTA/FAA/USCG with correct CFR parts and random-testing rates; optional single-state Drug-Free Workplace addendum (federal DOT program is nationwide; only the optional DFWP addendum is state-keyed). - Handler now outputs .docx instead of .pdf. - job_server instant-delivery: attach DOCX (correct MIME) as well as PDF, and use DOT-specific email copy + CTA instead of the FCC/telecom boilerplate. --- .../templates/dot_da_binder_generator.py | 1612 ++++++++++------- scripts/workers/job_server.py | 121 +- scripts/workers/services/dot_drug_alcohol.py | 20 +- .../intake/steps/DOTIntakeStep.astro | 57 +- 4 files changed, 1138 insertions(+), 672 deletions(-) diff --git a/scripts/document_gen/templates/dot_da_binder_generator.py b/scripts/document_gen/templates/dot_da_binder_generator.py index 9f4a390..72b30bf 100644 --- a/scripts/document_gen/templates/dot_da_binder_generator.py +++ b/scripts/document_gen/templates/dot_da_binder_generator.py @@ -1,20 +1,27 @@ """ DOT Drug & Alcohol Compliance Program binder generator. -Produces the instant-delivery PDF "binder" that ships when a customer buys -the $149 DOT Drug & Alcohol Compliance Program. The binder is a single, -print-ready PDF that bundles everything a small motor carrier needs to run a -compliant program under the applicable DOT testing regulation: +Produces the instant-delivery **editable DOCX** "binder" that ships when a +customer buys the DOT Drug & Alcohol Compliance Program. The binder is a +complete compliance program a small motor carrier can adopt, customise, and +have a lawyer review/edit, bundling everything needed to run a compliant +program under the applicable DOT testing regulation: 1. How to manage your program (step-by-step instructions) 2. Written drug & alcohol testing policy for employees (mode-specific) - 3. Supervisor reasonable-suspicion training materials + live/online access - 4. Employee Assistance Program (EAP) + rehab/treatment resources - 5. Substance Abuse Professional (SAP) access for DOT violations - 6. Copies / citations of the applicable regulations - 7. Random testing instructions (consortium / pool management) - 8. All required compliance forms (chain-of-custody, consent, refusal, etc.) - 9. Recordkeeping instructions + retention schedule + 3. When testing is required (the six DOT test scenarios) + 4. Random testing program (consortium / pool management) + 5. Supervisor reasonable-suspicion training materials + live/online access + 6. Violations, SAP access, return-to-duty / follow-up + 7. EAP / rehab / treatment resources + 8. Recordkeeping instructions + retention schedule + 9. All required compliance forms (Forms A-F), each on its own page + 10. The regulations (citations + how to read the actual rule text) + (+ optional state Drug-Free Workplace addendum) + +Output is **DOCX** so an attorney or the carrier can review and edit it. Each +ready-to-use form starts on its own page (page break before) and the section +that follows begins on a fresh page. DOT operating administrations (policy variants): - FMCSA : 49 CFR Part 382 (motor carriers / CDL drivers) <- default @@ -23,24 +30,18 @@ DOT operating administrations (policy variants): - FTA : 49 CFR Part 655 (transit) - FAA : 14 CFR Part 120 (aviation) - USCG : 46 CFR Part 16 (maritime) -Plus an optional state Drug-Free Workplace Program addendum. - -For a trucking carrier the program is almost always FMCSA (Part 382). The -``mode`` argument selects the variant; ``state_dfwp`` appends a state -Drug-Free Workplace addendum. Usage: from scripts.document_gen.templates.dot_da_binder_generator import ( generate_da_binder, ) - pdf_path = generate_da_binder( - output_path="/tmp/da_binder.pdf", + docx_path = generate_da_binder( + output_path="/tmp/da_binder.docx", carrier_name="Acme Trucking LLC", dot_number="1234567", mode="fmcsa", cdl_drivers=4, der_name="Jane Owner", - der_title="Owner / Designated Employer Representative", provider_name="Performance West Consortium", ) """ @@ -49,14 +50,32 @@ from __future__ import annotations import logging from datetime import datetime from pathlib import Path -from typing import Optional LOG = logging.getLogger("document_gen.da_binder") +try: + from docx import Document + from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK + from docx.enum.table import WD_TABLE_ALIGNMENT + from docx.shared import Inches, Pt, RGBColor + from docx.oxml.ns import qn + from docx.oxml import OxmlElement +except ImportError: # pragma: no cover - exercised only when docx missing + Document = None # type: ignore[assignment, misc] + LOG.warning("python-docx not installed — D&A binder generation unavailable") + +# ── Palette ──────────────────────────────────────────────────────────────── +NAVY = RGBColor(0x0B, 0x1F, 0x3A) if Document else None +BLUE = RGBColor(0x1D, 0x4E, 0xD8) if Document else None +SLATE = RGBColor(0x47, 0x55, 0x69) if Document else None +INK = RGBColor(0x1F, 0x29, 0x37) if Document else None +WHITE = RGBColor(0xFF, 0xFF, 0xFF) if Document else None +NAVY_HEX = "0B1F3A" +LIGHT_HEX = "EEF2F7" +ALT_HEX = "F5F8FC" + # ── Mode metadata ────────────────────────────────────────────────────────── -# Each DOT operating administration has its own testing rule. The binder text -# is written for the chosen mode; FMCSA is the trucking default. -MODE_META: dict[str, dict[str, str]] = { +MODE_META: dict[str, dict] = { "fmcsa": { "agency": "Federal Motor Carrier Safety Administration (FMCSA)", "part": "49 CFR Part 382", @@ -112,9 +131,6 @@ MODE_META: dict[str, dict[str, str]] = { }, } -# The five DOT test types & six prohibited-conduct categories are shared by all -# modes (Part 40 + the mode rule). Random rates differ by mode/year; FMCSA 2024+ -# is 50% drug / 10% alcohol of the average number of covered positions. _RANDOM_RATES = { "fmcsa": "50% (controlled substances) and 10% (alcohol)", "fra": "the FRA-published annual minimum random testing rates", @@ -125,6 +141,186 @@ _RANDOM_RATES = { } +# ── Low-level docx helpers ────────────────────────────────────────────────── +def _shade(cell, hex_color: str) -> None: + """Apply a background fill to a table cell.""" + tcPr = cell._tc.get_or_add_tcPr() + shd = OxmlElement("w:shd") + shd.set(qn("w:val"), "clear") + shd.set(qn("w:color"), "auto") + shd.set(qn("w:fill"), hex_color) + tcPr.append(shd) + + +def _set_cell_text(cell, text, *, bold=False, color=None, size=9, white=False): + cell.text = "" + p = cell.paragraphs[0] + p.paragraph_format.space_after = Pt(2) + p.paragraph_format.space_before = Pt(2) + run = p.add_run(text) + run.bold = bold + run.font.size = Pt(size) + if white: + run.font.color.rgb = WHITE + elif color is not None: + run.font.color.rgb = color + else: + run.font.color.rgb = INK + return p + + +def _add_runs(p, text): + """Render very small inline markup: **bold** segments.""" + import re + + for chunk in re.split(r"(\*\*.+?\*\*)", text): + if not chunk: + continue + if chunk.startswith("**") and chunk.endswith("**"): + r = p.add_run(chunk[2:-2]) + r.bold = True + else: + p.add_run(chunk) + + +class _B: + """Thin builder around a python-docx Document with the binder's styles.""" + + def __init__(self, doc, meta, carrier_name): + self.doc = doc + self.meta = meta + self.carrier_name = carrier_name + + def h1(self, text): + p = self.doc.add_paragraph() + p.paragraph_format.space_before = Pt(6) + p.paragraph_format.space_after = Pt(10) + p.paragraph_format.keep_with_next = True + r = p.add_run(text) + r.bold = True + r.font.size = Pt(18) + r.font.color.rgb = NAVY + # bottom rule + pPr = p._p.get_or_add_pPr() + pbdr = OxmlElement("w:pBdr") + bottom = OxmlElement("w:bottom") + bottom.set(qn("w:val"), "single") + bottom.set(qn("w:sz"), "6") + bottom.set(qn("w:space"), "4") + bottom.set(qn("w:color"), "CBD5E1") + pbdr.append(bottom) + pPr.append(pbdr) + return p + + def h2(self, text): + p = self.doc.add_paragraph() + p.paragraph_format.space_before = Pt(12) + p.paragraph_format.space_after = Pt(5) + p.paragraph_format.keep_with_next = True + r = p.add_run(text) + r.bold = True + r.font.size = Pt(13) + r.font.color.rgb = NAVY + return p + + def h3(self, text): + p = self.doc.add_paragraph() + p.paragraph_format.space_before = Pt(9) + p.paragraph_format.space_after = Pt(3) + p.paragraph_format.keep_with_next = True + r = p.add_run(text) + r.bold = True + r.font.size = Pt(11) + r.font.color.rgb = BLUE + return p + + def body(self, text, *, italic=False, size=10.5, color=None): + p = self.doc.add_paragraph() + p.paragraph_format.space_after = Pt(6) + _add_runs(p, text) + for r in p.runs: + r.font.size = Pt(size) + r.italic = italic + r.font.color.rgb = color or INK + return p + + def small(self, text): + p = self.doc.add_paragraph() + p.paragraph_format.space_after = Pt(4) + r = p.add_run(text) + r.font.size = Pt(8) + r.font.color.rgb = SLATE + return p + + def bullets(self, items): + for item in items: + p = self.doc.add_paragraph(style="List Bullet") + p.paragraph_format.space_after = Pt(3) + p.paragraph_format.left_indent = Inches(0.3) + _add_runs(p, item) + for r in p.runs: + r.font.size = Pt(10.5) + r.font.color.rgb = INK + + def numbered(self, items): + for item in items: + p = self.doc.add_paragraph(style="List Number") + p.paragraph_format.space_after = Pt(3) + p.paragraph_format.left_indent = Inches(0.35) + _add_runs(p, item) + for r in p.runs: + r.font.size = Pt(10.5) + r.font.color.rgb = INK + + def table(self, header, rows, widths, *, alt=True): + t = self.doc.add_table(rows=1, cols=len(header)) + t.alignment = WD_TABLE_ALIGNMENT.CENTER + t.style = "Table Grid" + t.autofit = False + for i, w in enumerate(widths): + for cell in t.columns[i].cells: + cell.width = Inches(w) + for i, htext in enumerate(header): + c = t.rows[0].cells[i] + _shade(c, NAVY_HEX) + _set_cell_text(c, htext, bold=True, white=True, size=9.5) + for ri, row in enumerate(rows): + cells = t.add_row().cells + for i, w in enumerate(widths): + cells[i].width = Inches(w) + for i, val in enumerate(row): + bold_first = i == 0 and len(header) == 2 + _set_cell_text(cells[i], val, bold=bold_first, size=9) + if alt and ri % 2 == 1: + for c in cells: + _shade(c, ALT_HEX) + self.doc.add_paragraph().paragraph_format.space_after = Pt(2) + return t + + def page_break(self): + self.doc.add_page_break() + + def spacer(self, pts=8): + p = self.doc.add_paragraph() + p.paragraph_format.space_after = Pt(pts) + return p + + def fill_line(self, label, *, gap=6): + """A labelled fill-in line for forms (underscored to right margin).""" + p = self.doc.add_paragraph() + p.paragraph_format.space_after = Pt(gap) + _add_runs(p, label) + for r in p.runs: + r.font.size = Pt(11) + r.font.color.rgb = INK + return p + + def blank_lines(self, n=1, label=""): + for i in range(n): + self.fill_line((label if i == 0 else "") + + " " + "_" * 78, gap=12) + + def generate_da_binder( *, output_path: str, @@ -138,639 +334,874 @@ def generate_da_binder( provider_name: str = "Performance West Consortium / C-TPA", state_dfwp: str = "", ) -> str | None: - """Render the D&A compliance binder PDF. Returns the path on success.""" - try: - from reportlab.lib.colors import HexColor - from reportlab.lib.enums import TA_CENTER, TA_LEFT - from reportlab.lib.pagesizes import letter - from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet - from reportlab.lib.units import inch - from reportlab.platypus import ( - BaseDocTemplate, - Frame, - ListFlowable, - ListItem, - PageBreak, - PageTemplate, - Paragraph, - Spacer, - Table, - TableStyle, - ) - except ImportError: - LOG.warning("reportlab not installed — D&A binder generation unavailable") + """Render the D&A compliance binder as an editable DOCX. Returns the path.""" + if Document is None: + LOG.warning("python-docx not installed — D&A binder generation unavailable") return None mode = (mode or "fmcsa").lower().strip() meta = MODE_META.get(mode, MODE_META["fmcsa"]) random_rate = _RANDOM_RATES.get(mode, _RANDOM_RATES["fmcsa"]) today = datetime.now().strftime("%B %d, %Y") + company = carrier_name or "the Company" - NAVY = HexColor("#0b1f3a") - BLUE = HexColor("#1d4ed8") - SLATE = HexColor("#475569") - LIGHT = HexColor("#eef2f7") + doc = Document() + for section in doc.sections: + section.top_margin = Inches(0.8) + section.bottom_margin = Inches(0.8) + section.left_margin = Inches(0.9) + section.right_margin = Inches(0.9) - styles = getSampleStyleSheet() - h1 = ParagraphStyle( - "H1", parent=styles["Heading1"], fontName="Helvetica-Bold", - fontSize=18, textColor=NAVY, spaceBefore=4, spaceAfter=10, leading=22, - ) - h2 = ParagraphStyle( - "H2", parent=styles["Heading2"], fontName="Helvetica-Bold", - fontSize=13, textColor=NAVY, spaceBefore=14, spaceAfter=6, leading=16, - ) - h3 = ParagraphStyle( - "H3", parent=styles["Heading3"], fontName="Helvetica-Bold", - fontSize=11, textColor=BLUE, spaceBefore=10, spaceAfter=4, leading=14, - ) - body = ParagraphStyle( - "Body", parent=styles["BodyText"], fontName="Helvetica", - fontSize=9.5, leading=14, spaceAfter=6, textColor=HexColor("#1f2937"), - ) - body_i = ParagraphStyle("BodyI", parent=body, fontName="Helvetica-Oblique") - small = ParagraphStyle( - "Small", parent=body, fontSize=8, textColor=SLATE, leading=11, - ) - cover_title = ParagraphStyle( - "CoverTitle", parent=h1, fontSize=26, alignment=TA_CENTER, leading=30, - ) - cover_sub = ParagraphStyle( - "CoverSub", parent=body, fontSize=13, alignment=TA_CENTER, - textColor=SLATE, leading=18, - ) + # Running header + page-number footer (skip styling complications: simple) + _add_header_footer(doc, meta, carrier_name) - def bullets(items: list[str], style=body) -> ListFlowable: - return ListFlowable( - [ListItem(Paragraph(t, style), leftIndent=10) for t in items], - bulletType="bullet", bulletColor=BLUE, leftIndent=14, - bulletFontSize=7, spaceAfter=4, - ) + b = _B(doc, meta, carrier_name) - def numbered(items: list[str], style=body) -> ListFlowable: - return ListFlowable( - [ListItem(Paragraph(t, style)) for t in items], - bulletType="1", leftIndent=18, spaceAfter=4, - ) - - story: list = [] - - # ── Cover ────────────────────────────────────────────────────────────── - story.append(Spacer(1, 1.4 * inch)) - story.append(Paragraph("DOT Drug & Alcohol", cover_title)) - story.append(Paragraph("Compliance Program Binder", cover_title)) - story.append(Spacer(1, 0.25 * inch)) - story.append(Paragraph( + # ── Cover ─────────────────────────────────────────────────────────────── + b.spacer(60) + for line in ("DOT Drug & Alcohol", "Compliance Program Binder"): + p = doc.add_paragraph() + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + r = p.add_run(line) + r.bold = True + r.font.size = Pt(26) + r.font.color.rgb = NAVY + p.paragraph_format.space_after = Pt(2) + p = doc.add_paragraph() + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + p.paragraph_format.space_before = Pt(10) + r = p.add_run( f"Prepared under {meta['part']} and the U.S. DOT procedures at " - f"{meta['procedures']}", cover_sub)) - story.append(Spacer(1, 0.55 * inch)) + f"{meta['procedures']}") + r.font.size = Pt(13) + r.font.color.rgb = SLATE + b.spacer(24) cover_rows = [ ["Motor Carrier", carrier_name or "—"], ["USDOT Number", str(dot_number) or "—"], ["Regulating Agency", meta["agency"]], ["Covered Employees", _covered_count(cdl_drivers, owner_operators)], - ["Designated Employer Rep.", f"{der_name or '—'}" + - (f" — {der_title}" if der_name else "")], + ["Designated Employer Rep.", + f"{der_name or '—'}" + (f" — {der_title}" if der_name else "")], ["Testing Program / C-TPA", provider_name], ["Program Effective Date", today], ] - t = Table(cover_rows, colWidths=[2.1 * inch, 3.9 * inch]) - t.setStyle(TableStyle([ - ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"), - ("FONTNAME", (1, 0), (1, -1), "Helvetica"), - ("FONTSIZE", (0, 0), (-1, -1), 10), - ("TEXTCOLOR", (0, 0), (0, -1), NAVY), - ("TEXTCOLOR", (1, 0), (1, -1), HexColor("#1f2937")), - ("BACKGROUND", (0, 0), (0, -1), LIGHT), - ("LINEBELOW", (0, 0), (-1, -1), 0.5, HexColor("#cbd5e1")), - ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("TOPPADDING", (0, 0), (-1, -1), 7), - ("BOTTOMPADDING", (0, 0), (-1, -1), 7), - ("LEFTPADDING", (0, 0), (-1, -1), 10), - ])) - story.append(t) - story.append(Spacer(1, 0.5 * inch)) - story.append(Paragraph( + t = doc.add_table(rows=0, cols=2) + t.alignment = WD_TABLE_ALIGNMENT.CENTER + t.style = "Table Grid" + for label, val in cover_rows: + cells = t.add_row().cells + cells[0].width = Inches(2.3) + cells[1].width = Inches(4.2) + _shade(cells[0], LIGHT_HEX) + _set_cell_text(cells[0], label, bold=True, color=NAVY, size=10.5) + _set_cell_text(cells[1], val, size=10.5) + b.spacer(28) + b.small( "Prepared by Performance West Inc. This binder is a compliance " - "resource, not legal advice. The motor carrier remains responsible " - "for implementing and maintaining its program.", small)) - story.append(PageBreak()) + "resource and editable template, not legal advice. The motor carrier " + "remains responsible for adopting, customising, implementing, and " + "maintaining its own program. Have counsel review before adoption.") + b.page_break() - # ── Table of contents ────────────────────────────────────────────────── - story.append(Paragraph("What's Inside This Binder", h1)) - toc = [ - "Section 1 — How to Manage Your Program. Step-by-step setup and " + # ── Table of contents ─────────────────────────────────────────────────── + b.h1("What's Inside This Binder") + b.bullets([ + "**Section 1 — How to Manage Your Program.** Step-by-step setup and " "ongoing operating instructions for the Designated Employer " "Representative (DER).", - "Section 2 — Written Testing Policy. Your company drug & " - "alcohol testing policy to distribute to every covered employee.", - "Section 3 — When Testing Is Required. The six DOT test " - "scenarios and what triggers each one.", - "Section 4 — Random Testing Program. How the consortium random " + "**Section 2 — Written Testing Policy.** Your company drug & alcohol " + "testing policy to adopt and distribute to every covered employee.", + "**Section 3 — When Testing Is Required.** The six DOT test scenarios " + "and what triggers each one.", + "**Section 4 — Random Testing Program.** How the consortium random " "pool works and your obligations.", - "Section 5 — Supervisor Training. Reasonable-suspicion training " + "**Section 5 — Supervisor Training.** Reasonable-suspicion training " "materials and how to access live/online training.", - "Section 6 — Violations, SAP & Return-to-Duty. What happens " - "after a positive test, and Substance Abuse Professional access.", - "Section 7 — EAP, Rehab & Treatment Resources. Employee " - "Assistance Program information and treatment referrals.", - "Section 8 — Recordkeeping. What to keep, where, and for how long.", - "Section 9 — Required Forms. Every form you need, ready to use.", - "Section 10 — The Regulations. Citations and how to read the " - "actual rule text.", - ] - story.append(bullets(toc)) + "**Section 6 — Violations, SAP & Return-to-Duty.** What happens after " + "a positive test, and Substance Abuse Professional access.", + "**Section 7 — EAP, Rehab & Treatment Resources.** Employee Assistance " + "Program information and treatment referrals.", + "**Section 8 — Recordkeeping.** What to keep, where, and for how long.", + "**Section 9 — Required Forms (A-F).** Every form you need, ready to " + "use, each on its own page.", + "**Section 10 — The Regulations.** Citations and how to read the actual " + "rule text.", + ]) if state_dfwp: - story.append(Spacer(1, 4)) - story.append(Paragraph( - f"Addendum — {state_dfwp} Drug-Free Workplace Program. " + b.body( + f"**Addendum — {state_dfwp} Drug-Free Workplace Program.** " "State-specific policy supplement that runs alongside your DOT " - "program.", body)) - story.append(PageBreak()) + "program.") + b.body( + "**How to use this binder.** Sections 1 and 9 are for you (the DER). " + "Section 2 is the policy you adopt and hand to employees. Fill in any " + "blanks (DER name, provider, EAP), sign the policy, collect signed " + "acknowledgments (Form A), and keep everything per Section 8.") + b.page_break() - # ── Section 1 — Manage your program ──────────────────────────────────── - story.append(Paragraph("Section 1 — How to Manage Your Program", h1)) - story.append(Paragraph( - f"As a motor carrier subject to {meta['part']}, you must operate a " - "drug and alcohol testing program covering every " - f"{meta['covered']}. The person who runs the program day-to-day is the " - "Designated Employer Representative (DER). Follow these steps.", - body)) - story.append(Paragraph("Initial setup (do once)", h3)) - story.append(numbered([ - "Name your DER. Record the DER's name and contact information " - "(this binder lists them on the cover). The DER receives test results " - "and removes employees from safety-sensitive duty when required.", - "Adopt the written policy in Section 2. Sign it, date it, and " - "keep the signed master copy.", - "Distribute the policy to every covered employee and collect a " - "signed acknowledgment (Form A in Section 9). Give a copy to each new " - "hire before they perform safety-sensitive duties.", - "Join a testing program / consortium (C-TPA). Your program is " - f"administered through {provider_name}, which manages your random " - "pool, scheduling, collection sites, the lab, and the Medical Review " - "Officer (MRO).", - "Conduct pre-employment drug tests with a verified negative " - "result before any new covered employee performs safety-sensitive " - "functions.", - ] + ([ - "Register with the FMCSA Clearinghouse at " - "clearinghouse.fmcsa.dot.gov and run the required queries (see below)." - ] if meta.get("clearinghouse") else []))) + # ── Section 1 — Manage your program ───────────────────────────────────── + b.h1("Section 1 — How to Manage Your Program") + b.body( + f"As a motor carrier subject to {meta['part']}, you must operate a drug " + f"and alcohol testing program covering every {meta['covered']}. The " + "person who runs the program day-to-day is the **Designated Employer " + "Representative (DER)**. The DER receives test results, schedules tests, " + "and immediately removes employees from safety-sensitive duty when " + "required. Follow these steps.") - story.append(Paragraph("Ongoing operations (every year)", h3)) + b.h3("Initial setup (do once)") + setup = [ + "**Name your DER and a backup.** Record names and contact information " + "(this binder lists the primary DER on the cover). The DER must be " + "reachable whenever covered employees are working.", + "**Adopt the written policy** in Section 2. Fill in the blanks, sign " + "it, date it, and keep the signed master copy.", + "**Distribute the policy and educational materials** to every covered " + "employee and collect a signed acknowledgment (Form A). Give a copy to " + "each new hire before they perform safety-sensitive duties.", + "**Join a testing program / consortium (C-TPA).** Your program is " + f"administered through {provider_name}, which manages your random pool, " + "scheduling, the collection site(s), the HHS-certified laboratory, and " + "the Medical Review Officer (MRO).", + "**Train your supervisors** (at least 120 minutes total) before they " + "make any reasonable-suspicion decision (Section 5; record on Form F).", + "**Conduct pre-employment drug tests** with a verified negative result " + "before any new covered employee performs safety-sensitive functions.", + ] + if meta.get("clearinghouse"): + setup.append( + "**Register with the FMCSA Clearinghouse** at " + "clearinghouse.fmcsa.dot.gov, designate your C-TPA, and run the " + "required pre-employment full query (with the driver's consent).") + b.numbered(setup) + + b.h3("Ongoing operations (every year)") ongoing = [ - "Conduct random tests throughout the year at the required " - f"minimum annual rate of {random_rate} of covered positions, spread " - "evenly across the year (see Section 4).", - "Test for reasonable suspicion when a trained supervisor " - "observes the signs (see Section 5).", - "Conduct post-accident testing when DOT criteria are met " - "(see Section 3).", - "Run return-to-duty and follow-up tests for any employee who " - "violated the rule and completed the SAP process (see Section 6).", - "Keep all records for the required retention periods " - "(see Section 8).", + "Conduct **random tests** throughout the year at the required minimum " + f"annual rate of {random_rate} of covered positions, spread evenly " + "across the year and kept unannounced (Section 4).", + "Test for **reasonable suspicion** when a trained supervisor observes " + "specific, articulable signs (Section 5; Form D).", + "Conduct **post-accident testing** when DOT criteria are met " + "(Section 3; Form E).", + "Run **return-to-duty and follow-up tests** for any employee who " + "violated the rule and completed the SAP process (Section 6).", + "Keep all **records** for the required retention periods (Section 8).", + "Review and re-date the policy at least annually, and re-distribute it " + "when it changes or when you hire.", ] if meta.get("clearinghouse"): ongoing.append( - "Run an annual FMCSA Clearinghouse query on every CDL " - "driver, and a full query (with driver consent) before " - "hiring any new driver. Report violations and refusals to the " - "Clearinghouse.") - story.append(bullets(ongoing)) - story.append(PageBreak()) + "Run an **annual FMCSA Clearinghouse limited query** on every CDL " + "driver, and a **pre-employment full query** (with consent) before " + "hiring. **Report** violations, refusals, actual-knowledge " + "findings, negative return-to-duty results, and follow-up " + "completion to the Clearinghouse within the required timeframes.") + b.bullets(ongoing) - # ── Section 2 — Written policy ───────────────────────────────────────── - story.append(Paragraph("Section 2 — Written Drug & Alcohol Testing Policy", h1)) - story.append(Paragraph( - f"This is the policy {carrier_name or 'the Company'} adopts and gives " - f"to every covered employee. It satisfies the written-policy " - f"requirement of {meta['part']} and {meta['procedures']}.", body_i)) + b.h3("The DER's quick-reference duties") + b.bullets([ + "Receive results from the MRO/C-TPA and act on them the same day.", + "Immediately remove an employee from safety-sensitive duty on a " + "verified positive, a 0.04+ alcohol result, or a refusal.", + "Keep the consortium roster current (add hires, remove departures).", + "Make sure selected employees proceed to collection immediately.", + "Maintain the confidential records file and produce it on audit.", + ]) + b.page_break() - story.append(Paragraph("1. Purpose and Authority", h3)) - story.append(Paragraph( - f"{carrier_name or 'The Company'} (the \"Company\") is committed to a " - "safe, drug- and alcohol-free workplace. This policy implements the " - f"requirements of the {meta['agency']} at {meta['part']} and the U.S. " - f"Department of Transportation testing procedures at {meta['procedures']}. " - "Where this policy conflicts with DOT regulations, the regulations " - "govern.", body)) + # ── Section 2 — Written policy ────────────────────────────────────────── + b.h1("Section 2 — Written Drug & Alcohol Testing Policy") + b.body( + f"This is the policy {company} adopts and gives to every covered " + f"employee. It satisfies the written-policy requirement of " + f"{meta['part']} and {meta['procedures']}. Replace any bracketed blanks, " + "then sign and date the adoption block at the end.", italic=True) - story.append(Paragraph("2. Who Is Covered", h3)) - story.append(Paragraph( - f"This policy applies to every {meta['covered']}. A covered employee " - f"is subject to testing whenever {meta['function']}.", body)) + b.h3("1. Purpose and Authority") + b.body( + f"{company} (the \"Company\") is committed to a safe, drug- and " + "alcohol-free workplace and to protecting the public, its employees, " + "and its property. This policy implements the requirements of the " + f"{meta['agency']} at {meta['part']} and the U.S. Department of " + f"Transportation testing procedures at {meta['procedures']}. Where this " + "policy conflicts with the DOT regulations, the regulations govern. The " + "Company may also maintain separate, non-DOT workplace rules; this " + "policy governs DOT-required testing only.") - story.append(Paragraph("3. Prohibited Conduct", h3)) - story.append(Paragraph("A covered employee must not:", body)) - story.append(bullets([ + b.h3("2. Who Is Covered") + b.body( + f"This policy applies to every {meta['covered']}, including full-time, " + "part-time, intermittent, temporary, and newly hired employees, and " + "owner-operators operating under the Company's authority. A covered " + f"employee is subject to testing whenever {meta['function']}.") + + b.h3("3. Safety-Sensitive Functions") + b.body( + "\"Safety-sensitive function\" includes all on-duty time performing or " + "available to perform the covered function, plus related duties such as " + "waiting to be dispatched, inspecting or servicing equipment, " + "supervising or attending the loading/unloading of a vehicle, and " + "performing required post-accident duties.") + + b.h3("4. Prohibited Conduct") + b.body("A covered employee must not:") + b.bullets([ "Report for duty or remain on duty with an alcohol concentration of " - "0.04 or greater.", + "**0.04 or greater**.", "Use alcohol while performing safety-sensitive functions, within " - "4 hours before, or within 8 hours after an accident (or until " - "tested).", - "Use any illegal drug, or use a controlled substance unless under a " - "valid prescription consistent with safe operation.", - "Report for duty or remain on duty when using a controlled substance " - "that the prescribing physician has not authorized as consistent with " - "safe performance.", - "Refuse to submit to a required test (a refusal is treated the same as " - "a verified positive test).", - "Test positive on any DOT-required drug or alcohol test.", - ])) + "**4 hours** before performing them, or within **8 hours** after an " + "accident (or until tested, whichever is first).", + "Use any illegal drug, or use a controlled substance, unless the use is " + "consistent with a valid prescription that the prescribing licensed " + "medical practitioner has determined will not adversely affect the " + "employee's ability to safely perform the function.", + "Report for duty or remain on duty when using any substance that the " + "prescribing physician has not authorised as consistent with safe " + "performance.", + "**Refuse to submit** to a required test, adulterate or substitute a " + "specimen, or fail to remain readily available for post-accident " + "testing (a refusal is treated the same as a verified positive).", + "Test positive on, or tamper with, any DOT-required drug or alcohol " + "test.", + ]) - story.append(Paragraph("4. Required Tests", h3)) - story.append(Paragraph( - "The Company conducts the following DOT tests: pre-employment, random, " - "reasonable suspicion, post-accident, return-to-duty, and follow-up. " - "Section 3 explains when each applies. Drug tests screen for marijuana, " - "cocaine, opioids, amphetamines, and phencyclidine (PCP) using the " - "DOT 5-panel and are verified by a Medical Review Officer (MRO).", body)) + b.h3("5. Required Tests") + b.body( + "The Company conducts the following DOT tests: **pre-employment, " + "random, reasonable suspicion, post-accident, return-to-duty, and " + "follow-up.** Section 3 explains when each applies.") + b.body( + "Drug tests use the DOT 5-panel and screen for marijuana, cocaine, " + "opioids (including codeine, morphine, heroin, hydrocodone, " + "hydromorphone, oxycodone, and oxymorphone), amphetamines (including " + "methamphetamine, MDMA, and MDA), and phencyclidine (PCP). A certified " + "laboratory analyses the specimen; a **Medical Review Officer (MRO)** " + "reviews and verifies any non-negative result and may contact the " + "employee for a legitimate medical explanation before reporting a " + "verified positive to the Company.") - story.append(Paragraph("5. Consequences of a Violation", h3)) - story.append(Paragraph( - "An employee who has a verified positive test, an alcohol " - "concentration of 0.04 or greater, or who refuses a test is " - "immediately removed from safety-sensitive duty. The employee may not " - "return until they complete the return-to-duty process with a DOT-" - "qualified Substance Abuse Professional (SAP) and pass a return-to-duty " - "test (see Section 6).", body)) + b.h3("6. Consequences of a Violation") + b.body( + "An employee who has a verified positive test, an alcohol concentration " + "of 0.04 or greater, or who refuses a test is **immediately removed " + "from safety-sensitive duty**. The employee may not return until they " + "complete the DOT return-to-duty process with a qualified Substance " + "Abuse Professional (SAP) and pass a return-to-duty test (Section 6). " + "An employee with an alcohol result of 0.02 to less than 0.04 is " + "removed from safety-sensitive duty until the start of the next " + "regularly scheduled duty period, but at least 24 hours. Additional " + "employment consequences, if any, are governed by separate Company " + "rules and applicable law.") - story.append(Paragraph("6. Designated Employer Representative", h3)) - story.append(Paragraph( - f"The Company's DER is {der_name or '________________________'}" + b.h3("7. Designated Employer Representative") + b.body( + f"The Company's DER is {der_name or '[DER NAME]'}" f"{(', ' + der_title) if der_name else ''}. The DER receives test " "results, schedules tests, and removes employees from duty as required. " - "Questions about this policy should be directed to the DER.", body)) + "Questions about this policy should be directed to the DER.") - story.append(Paragraph("7. Employee Assistance", h3)) - story.append(Paragraph( - "Information about substance abuse, its effects, and available " - "treatment and Employee Assistance resources is provided in Section 7 " - "of this binder and is available from the DER.", body)) - story.append(PageBreak()) + b.h3("8. Confidentiality and Employee Rights") + b.body( + "Test records are kept confidential and released only as authorised by " + f"{meta['procedures']} and {meta['part']} (for example, to the " + "employee, an MRO or SAP, a subsequent employer with the employee's " + "written consent, or as required by law or the DOT). On written request " + "an employee may obtain copies of records relating to the employee's " + "own tests.") - # ── Section 3 — When testing is required ─────────────��───────────────── - story.append(Paragraph("Section 3 — When Testing Is Required", h1)) - test_table = [ + b.h3("9. Employee Assistance") + b.body( + "Information about substance abuse, its effects, the signs and symptoms " + "of a problem, and available treatment and Employee Assistance " + "resources is provided in Section 7 of this binder and is available " + "from the DER.") + + b.h3("Policy Adoption") + b.body( + "The Company adopts this Drug & Alcohol Testing Policy as of the date " + "below. The signed original is retained by the DER.") + b.spacer(6) + b.fill_line("Company official (print): ___________________________________" + "__________________") + b.fill_line("Signature: _______________________________________ Title: " + "_______________________") + b.fill_line("Effective date: __________________ USDOT #: " + f"{dot_number or '________________'}") + b.page_break() + + # ── Section 3 — When testing is required ─────────────────────────────── + b.h1("Section 3 — When Testing Is Required") + b.body( + "DOT testing happens in six situations. The table summarises the " + "triggers; the notes below add the detail your DER needs.") + b.table( ["Test Type", "When It Happens"], - ["Pre-employment", - "Before a new covered employee first performs safety-sensitive " - "functions. Drug test with a verified negative result is required."], - ["Random", - "Unannounced, spread throughout the year, selected by the consortium " - "from the random pool (see Section 4)."], - ["Reasonable suspicion", - "When a trained supervisor observes specific, articulable signs of " - "drug or alcohol use (see Section 5)."], - ["Post-accident", - "After a DOT-recordable accident meeting the rule's criteria " - "(fatality; citation + injury requiring medical treatment away from " - "the scene; or citation + a vehicle towed from the scene). Alcohol " - "test within 8 hours, drug test within 32 hours."], - ["Return-to-duty", - "Before an employee who violated the rule may return to safety-" - "sensitive duty, after completing the SAP process. Directly observed."], - ["Follow-up", - "A SAP-directed schedule of unannounced tests (at least 6 in the " - "first 12 months) after return to duty. Directly observed."], - ] - tt = Table(test_table, colWidths=[1.5 * inch, 4.5 * inch]) - tt.setStyle(TableStyle([ - ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), - ("FONTNAME", (0, 1), (0, -1), "Helvetica-Bold"), - ("FONTSIZE", (0, 0), (-1, -1), 9), - ("TEXTCOLOR", (0, 0), (-1, 0), HexColor("#ffffff")), - ("BACKGROUND", (0, 0), (-1, 0), NAVY), - ("BACKGROUND", (0, 1), (0, -1), LIGHT), - ("VALIGN", (0, 0), (-1, -1), "TOP"), - ("GRID", (0, 0), (-1, -1), 0.4, HexColor("#cbd5e1")), - ("TOPPADDING", (0, 0), (-1, -1), 6), - ("BOTTOMPADDING", (0, 0), (-1, -1), 6), - ("LEFTPADDING", (0, 0), (-1, -1), 7), - ("RIGHTPADDING", (0, 0), (-1, -1), 7), - ])) - story.append(tt) - story.append(PageBreak()) + [ + ["Pre-employment", + "Before a new covered employee first performs safety-sensitive " + "functions. A drug test with a verified negative result is " + "required (and an FMCSA Clearinghouse pre-employment full query " + "for CDL drivers)."], + ["Random", + "Unannounced and spread throughout the year, selected by the " + "consortium from the random pool (Section 4)."], + ["Reasonable suspicion", + "When a trained supervisor observes specific, articulable, " + "contemporaneous signs of drug or alcohol use (Section 5)."], + ["Post-accident", + "After a qualifying accident: a fatality; OR a citation to the " + "driver plus an injury requiring immediate medical treatment away " + "from the scene; OR a citation plus a vehicle towed from the scene " + "due to disabling damage. Alcohol test within 8 hours, drug test " + "within 32 hours."], + ["Return-to-duty", + "Before an employee who violated the rule may return to safety-" + "sensitive duty, after completing the SAP process. Conducted under " + "direct observation."], + ["Follow-up", + "A SAP-directed schedule of unannounced tests (at least 6 in the " + "first 12 months, for up to 5 years) after return to duty. " + "Conducted under direct observation."], + ], + [1.5, 5.0], + ) + b.h3("Post-accident testing — decide quickly") + b.body( + "Use Form E (Section 9) at the scene. Test if **any** trigger is met. " + "If a required test cannot be done in time, document why. Do not let a " + "driver who must be tested consume alcohol or leave before the alcohol " + "test, and complete the drug test within 32 hours.") + b.h3("Pre-employment exceptions") + b.body( + "A new hire may be exempt from the pre-employment drug test only if you " + "fully document a valid prior-testing exception (participation in a DOT " + "program in the prior 30 days, a DOT drug test in the prior 6 months or " + "random-pool participation for the prior 12 months, and no knowledge of " + "a prior-employer violation). When in doubt, test.") + b.page_break() # ── Section 4 — Random testing ───────────────────────────────────────── - story.append(Paragraph("Section 4 — Random Testing Program", h1)) - story.append(Paragraph( - f"Random testing is the backbone of your program. The minimum annual " + b.h1("Section 4 — Random Testing Program") + b.body( + "Random testing is the backbone of your program. The minimum annual " f"random testing rate is {random_rate} of your average number of " - "covered positions. Because most small carriers cannot meet the " - "spread-and-rate requirements on their own, your covered employees are " - f"enrolled in a random pool managed by {provider_name} " - "(a Consortium/Third-Party Administrator, or C-TPA).", body)) - story.append(Paragraph("How the random pool works", h3)) - story.append(numbered([ + "covered positions. Because most small carriers cannot meet the spread-" + "and-rate requirements on their own, your covered employees are " + f"enrolled in a random pool managed by {provider_name} (a " + "Consortium/Third-Party Administrator, or C-TPA).") + b.h3("How the random pool works") + b.numbered([ "Every covered employee is placed in the consortium's random pool.", "Each selection period, the consortium randomly selects names using a " - "scientifically valid method.", - "Selected employees are notified and must proceed immediately " - "to the collection site — no advance notice, no delay.", + "scientifically valid method (each employee has an equal chance each " + "time).", + "Selected employees are notified and must proceed **immediately** to " + "the collection site — no advance notice, no delay.", "Selections are spread reasonably throughout the year so testing is " "truly unannounced and unpredictable.", "The consortium tracks the rate and gives you a year-end summary for " "your records and any DOT audit.", - ])) - story.append(Paragraph("Your responsibilities", h3)) - story.append(bullets([ + ]) + b.h3("Your responsibilities") + b.bullets([ "Keep the consortium's roster of covered employees current — add new " "hires and remove departures promptly.", "Send selected employees to testing right away and document that they " - "went.", - "Never select or 'volunteer' specific people — selection must stay " - "random.", - "Keep the consortium's selection and results records (see Section 8).", - ])) - story.append(PageBreak()) + "went (notification time, arrival time, completion).", + "Never select, warn, or 'volunteer' specific people — selection must " + "stay random and confidential until notification.", + "For random alcohol tests, test only just before, during, or just " + "after the employee performs safety-sensitive functions.", + "Keep the consortium's selection and results records (Section 8).", + ]) + b.page_break() # ── Section 5 — Supervisor training ──────────────────────────────────── - story.append(Paragraph("Section 5 — Supervisor Reasonable-Suspicion Training", h1)) - story.append(Paragraph( + b.h1("Section 5 — Supervisor Reasonable-Suspicion Training") + b.body( f"{meta['part']} requires that any supervisor who may make a " - "reasonable-suspicion testing decision complete at least 60 minutes " - "of training on the symptoms of alcohol misuse and at least " - "60 minutes on the symptoms of controlled-substance use " - "(120 minutes total). Keep a record of who was trained and when.", body)) - story.append(Paragraph("Signs a trained supervisor watches for", h3)) - story.append(bullets([ - "Appearance: bloodshot or watery eyes, dilated/constricted " - "pupils, flushed face, disheveled clothing, drug paraphernalia.", - "Behavior: mood swings, agitation, euphoria, drowsiness, " - "paranoia, unusual or unsafe risk-taking.", - "Speech: slurred, rapid, incoherent, or unusually talkative.", - "Coordination: unsteady gait, tremors, poor balance, fumbling.", - "Odor: alcohol or marijuana odor on breath, body, or clothing.", - "Performance: sudden decline, missed deadlines, accidents, " - "near-misses, frequent absences.", - ])) - story.append(Paragraph("Making a reasonable-suspicion decision", h3)) - story.append(numbered([ - "Two trained supervisors are not required — one trained supervisor's " - "specific, contemporaneous, articulable observations are enough.", - "Document the observations in writing immediately (Form D in " - "Section 9), signed and dated within 24 hours.", + "reasonable-suspicion testing decision complete at least **60 minutes " + "of training on the symptoms of alcohol misuse** and at least " + "**60 minutes on the symptoms of controlled-substance use** " + "(120 minutes total). Record each supervisor's training on Form F and " + "keep it on file.") + b.h3("Signs a trained supervisor watches for") + b.bullets([ + "**Appearance:** bloodshot or watery eyes, dilated/constricted pupils, " + "flushed face, disheveled clothing, drug paraphernalia.", + "**Behavior:** mood swings, agitation, euphoria, drowsiness, paranoia, " + "unusual or unsafe risk-taking.", + "**Speech:** slurred, rapid, incoherent, or unusually talkative.", + "**Coordination:** unsteady gait, tremors, poor balance, fumbling.", + "**Odor:** alcohol or marijuana odor on breath, body, or clothing.", + "**Performance:** sudden decline, missed deadlines, accidents, near-" + "misses, frequent absences.", + ]) + b.h3("Making a reasonable-suspicion decision") + b.numbered([ + "One trained supervisor's specific, contemporaneous, articulable " + "observations are enough — two supervisors are not required.", + "Document the observations in writing immediately (Form D), signed and " + "dated within 24 hours.", + "For alcohol, make the observations just before, during, or just after " + "the employee performs safety-sensitive functions.", "Direct the employee to testing and do not let them drive themselves.", "Remove the employee from safety-sensitive duty pending results.", - ])) - story.append(Paragraph("Live / online supervisor training access", h3)) - story.append(Paragraph( - "Your program includes access to DOT-compliant supervisor training. " - "To complete the required 120-minute course online (or schedule a live " + ]) + b.h3("Live / online supervisor training access") + b.body( + "Your program includes access to DOT-compliant supervisor training. To " + "complete the required 120-minute course online (or schedule a live " "session), contact us and we will send each supervisor an enrollment " - "link and issue a completion certificate for your records:", body)) - story.append(bullets([ - "Email: compliance@performancewest.com — subject line " - "\"Supervisor Training\" with your USDOT number.", - "What you get: self-paced online modules, a knowledge check, " - "and a dated certificate of completion to keep on file.", - ])) - story.append(PageBreak()) + "link and issue a dated completion certificate for your records.") + b.bullets([ + "**Email:** compliance@performancewest.com — subject \"Supervisor " + "Training\" with your USDOT number.", + "**What you get:** self-paced online modules, a knowledge check, and a " + "dated certificate of completion to keep on file (record on Form F).", + ]) + b.page_break() # ── Section 6 — Violations / SAP ─────────────────────────────────────── - story.append(Paragraph("Section 6 — Violations, SAP & Return-to-Duty", h1)) - story.append(Paragraph( + b.h1("Section 6 — Violations, SAP & Return-to-Duty") + b.body( "If a covered employee has a verified positive test, an alcohol " "concentration of 0.04 or greater, or refuses a test, you must " - "immediately remove them from safety-sensitive duty. They cannot " - "return until they complete the DOT return-to-duty process.", body)) - story.append(Paragraph("The return-to-duty process", h3)) - story.append(numbered([ - "Remove from duty and give the employee the contact information " - "for a DOT-qualified Substance Abuse Professional (SAP).", - "SAP evaluation. The SAP evaluates the employee and prescribes " + "**immediately remove them from safety-sensitive duty**. They cannot " + "return until they complete the DOT return-to-duty process.") + b.h3("The return-to-duty process") + b.numbered([ + "**Remove from duty** and give the employee the contact information " + "for at least two DOT-qualified Substance Abuse Professionals (SAPs).", + "**SAP evaluation.** The SAP evaluates the employee and prescribes " "education and/or treatment.", - "Treatment / education is completed as directed by the SAP.", - "Follow-up evaluation. The SAP confirms the employee complied " - "and is eligible to return.", - "Return-to-duty test. The employee must pass a directly " - "observed return-to-duty test before resuming duties.", - "Follow-up testing. The SAP sets an unannounced follow-up " - "testing schedule (at least 6 tests in the first 12 months, for up to " - "5 years).", - ])) - story.append(Paragraph("SAP access", h3)) - story.append(Paragraph( + "**Treatment / education** is completed as directed by the SAP.", + "**Follow-up evaluation.** The SAP confirms the employee complied and " + "is eligible to return.", + "**Return-to-duty test.** The employee must pass a directly observed " + "return-to-duty test (verified negative drug result, or alcohol below " + "0.02) before resuming duties.", + "**Follow-up testing.** The SAP sets an unannounced follow-up testing " + "schedule — at least 6 directly observed tests in the first 12 months, " + "and the plan may continue for up to 5 years.", + ]) + b.h3("SAP access") + b.body( "Your program includes access to a network of DOT-qualified Substance " "Abuse Professionals. To get a SAP referral for an employee, contact " "the DER or email compliance@performancewest.com with your USDOT " - "number; we will provide qualified SAP contacts in the employee's " - "area. You can also locate a SAP through the DOT and industry " - "directories listed in Section 7.", body)) + "number; we will provide qualified SAP contacts in the employee's area. " + "You can also locate a SAP through the DOT and industry directories in " + "Section 7. The employee generally bears the cost of SAP services and " + "testing; the Company is not required to return any employee to duty or " + "to provide a second chance.") if meta.get("clearinghouse"): - story.append(Paragraph("FMCSA Clearinghouse reporting", h3)) - story.append(Paragraph( - "Report the violation, refusal, the SAP's name, the negative " - "return-to-duty test, and completion of follow-up testing to the " - "FMCSA Clearinghouse within the required timeframes. A driver with " - "an unresolved Clearinghouse violation is 'prohibited' and may not " - "operate a CMV.", body)) - story.append(PageBreak()) + b.h3("FMCSA Clearinghouse reporting") + b.body( + "Report the violation or refusal, actual-knowledge findings, the " + "negative return-to-duty test, and completion of follow-up testing " + "to the FMCSA Clearinghouse within the required timeframes. A driver " + "with an unresolved Clearinghouse violation is 'prohibited' and may " + "not operate a CMV until the return-to-duty requirements are met " + "and recorded.") + b.page_break() # ── Section 7 — EAP / rehab ──────────────────────────────────────────── - story.append(Paragraph("Section 7 — EAP, Rehab & Treatment Resources", h1)) - story.append(Paragraph( + b.h1("Section 7 — EAP, Rehab & Treatment Resources") + b.body( "DOT rules require that you give covered employees educational " - "materials that explain the requirements of the rule and the " - "employer's policies and procedures, plus information on the effects " - "of alcohol and controlled-substance use and available resources. Use " - "the resources below and share them with employees.", body)) - story.append(Paragraph("National help lines and directories", h3)) - story.append(bullets([ - "SAMHSA National Helpline: 1-800-662-HELP (4357) — free, " + "materials explaining the requirements of the rule, the employer's " + "policies and procedures, the effects and signs of alcohol and " + "controlled-substance use, and available resources. Distribute the " + "materials below with the policy and keep a record of distribution " + "(Form A).") + b.h3("Effects, signs, and intervention") + b.body( + "Alcohol misuse and controlled-substance use can impair judgment, " + "reaction time, coordination, perception, attention, and decision-" + "making, increasing the risk of crashes, injury, death, regulatory " + "violations, job loss, and health and family harm. Anyone who suspects " + "a problem should report it to the DER. Do not attempt to diagnose " + "impairment; document observed facts and contact the DER, who will " + "decide whether reasonable-suspicion testing applies.") + b.h3("National help lines and directories") + b.bullets([ + "**SAMHSA National Helpline:** 1-800-662-HELP (4357) — free, " "confidential, 24/7 treatment referral and information service.", - "SAMHSA treatment locator: findtreatment.gov — searchable " + "**SAMHSA treatment locator:** findtreatment.gov — searchable " "directory of treatment facilities by location.", - "988 Suicide & Crisis Lifeline: call or text 988.", - "Alcoholics Anonymous: aa.org — local meeting finder.", - "Narcotics Anonymous: na.org — local meeting finder.", - "DOT SAP information: transportation.gov/odapc — Office of " - "Drug & Alcohol Policy and Compliance, including the SAP and " - "return-to-duty guidance.", - ])) - story.append(Paragraph("Employee Assistance Program (EAP)", h3)) - story.append(Paragraph( + "**988 Suicide & Crisis Lifeline:** call or text 988.", + "**Alcoholics Anonymous:** aa.org — local meeting finder.", + "**Narcotics Anonymous:** na.org — local meeting finder.", + "**DOT SAP information:** transportation.gov/odapc — Office of Drug & " + "Alcohol Policy and Compliance, including SAP and return-to-duty " + "guidance.", + ]) + b.h3("Employee Assistance Program (EAP)") + b.body( "An EAP gives employees confidential access to counseling and referral " - "services. If your company offers an EAP, list its contact " - "information on the acknowledgment you give employees. If you do not " - "have an EAP, the national resources above and a SAP referral satisfy " - "the educational and referral expectations of the rule. We can help " - "you add a low-cost EAP — email compliance@performancewest.com.", body)) - story.append(PageBreak()) + "services. If your company offers an EAP, list its contact information " + "on the acknowledgment you give employees. If you do not have an EAP, " + "the national resources above and a SAP referral satisfy the " + "educational and referral expectations of the rule. We can help you add " + "a low-cost EAP — email compliance@performancewest.com.") + b.page_break() # ── Section 8 — Recordkeeping ────────────────────────────────────────── - story.append(Paragraph("Section 8 — Recordkeeping", h1)) - story.append(Paragraph( - "Keep your records organized and secure; you must produce them in a " - "DOT audit. The DER (or your C-TPA on your behalf) maintains them. " - "Retention periods under Part 40 / the mode rule:", body)) - rec_table = [ + b.h1("Section 8 — Recordkeeping") + b.body( + "Keep your records organised, confidential, and secure; you must " + "produce them in a DOT audit. The DER (or your C-TPA on your behalf) " + f"maintains them. Retention periods under {meta['procedures']} / the " + "mode rule:") + b.table( ["Record", "Keep For"], - ["Verified positive test results; refusals; SAP reports; " - "return-to-duty and follow-up test records; alcohol tests of 0.02+", - "5 years"], - ["Random selection records and the annual random testing rate " - "calculation", "5 years"], - ["Reasonable-suspicion and post-accident test records", "5 years"], - ["EBT calibration / collection-site documentation", "5 years"], - ["Annual MIS summary (if required of your operation)", "5 years"], - ["Negative test results and cancelled tests", "1 year"], - ["Supervisor training records", "While employed + as required"], - ["Employee policy acknowledgments", "Duration of employment + 1 year"], - ] - rt = Table(rec_table, colWidths=[4.4 * inch, 1.6 * inch]) - rt.setStyle(TableStyle([ - ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), - ("FONTSIZE", (0, 0), (-1, -1), 9), - ("TEXTCOLOR", (0, 0), (-1, 0), HexColor("#ffffff")), - ("BACKGROUND", (0, 0), (-1, 0), NAVY), - ("VALIGN", (0, 0), (-1, -1), "TOP"), - ("GRID", (0, 0), (-1, -1), 0.4, HexColor("#cbd5e1")), - ("ROWBACKGROUNDS", (0, 1), (-1, -1), [HexColor("#ffffff"), LIGHT]), - ("TOPPADDING", (0, 0), (-1, -1), 6), - ("BOTTOMPADDING", (0, 0), (-1, -1), 6), - ("LEFTPADDING", (0, 0), (-1, -1), 7), - ])) - story.append(rt) - story.append(Spacer(1, 8)) - story.append(Paragraph( + [ + ["Verified positive results; refusals; SAP reports; return-to-duty " + "and follow-up test records; alcohol tests of 0.02+", "5 years"], + ["Random selection records and the annual random testing rate " + "calculation", "5 years"], + ["Reasonable-suspicion and post-accident test records", "5 years"], + ["EBT calibration / collection-site documentation", "5 years"], + ["Annual MIS summary (if required of your operation)", "5 years"], + ["Negative test results and cancelled tests", "1 year"], + ["Supervisor training records (Form F)", + "While employed + as required"], + ["Employee policy acknowledgments (Form A)", + "Duration of employment + 1 year"], + ], + [4.6, 1.4], + ) + b.body( "Store records so they are confidential and retrievable. Your C-TPA " "keeps a parallel set of consortium records; request a copy any time.", - small)) - story.append(PageBreak()) + italic=True) + b.page_break() # ── Section 9 — Forms ────────────────────────────────────────────────── - story.append(Paragraph("Section 9 — Required Compliance Forms", h1)) - story.append(Paragraph( - "The forms on the following pages are ready to print and use. The " - "federal chain-of-custody and alcohol-testing forms (the CCF and the " - "DOT Alcohol Testing Form) are provided by the collection site and " - "lab; you do not print those yourself, but your C-TPA supplies them at " - "each collection.", body)) - story.append(bullets([ - "Form A — Employee Policy Receipt & Acknowledgment.", - "Form B — Pre-Employment Testing Consent & Prior-Employer " - "Inquiry.", - "Form C — Test Notification / Authorization to Test.", - "Form D — Reasonable-Suspicion Observation Record.", - "Form E — Post-Accident Testing Decision Worksheet.", - "Form F — Supervisor Training Completion Record.", - ])) + b.h1("Section 9 — Required Compliance Forms") + b.body( + "The forms on the following pages are ready to print and use; each form " + "is on its own page. The federal chain-of-custody form (the CCF) and " + "the DOT Alcohol Testing Form (ATF) are supplied by the collection site " + "and lab at each collection — you do not print those yourself.") + b.bullets([ + "**Form A** — Employee Policy Receipt & Acknowledgment.", + "**Form B** — Pre-Employment Consent & Prior-Employer Inquiry.", + "**Form C** — Test Notification / Authorization to Test.", + "**Form D** — Reasonable-Suspicion Observation Record.", + "**Form E** — Post-Accident Testing Decision Worksheet.", + "**Form F** — Supervisor Training Completion Record.", + ]) - _append_form_a(story, h2, h3, body, small, carrier_name, meta) - _append_form_d(story, h2, body, small, carrier_name) - _append_form_e(story, h2, body, small, carrier_name) + _form_a(b, carrier_name, meta) + _form_b(b, carrier_name, meta) + _form_c(b, carrier_name) + _form_d(b, carrier_name) + _form_e(b, carrier_name) + _form_f(b, carrier_name) # ── Section 10 — Regulations ─────────────────────────────────────────── - story.append(Paragraph("Section 10 — The Regulations", h1)) - story.append(Paragraph( - "Your program is governed by two rules that work together: the DOT-" - f"wide testing procedures at {meta['procedures']}, and the mode rule " - f"at {meta['part']} from the {meta['agency']}. Read the official, " - "always-current text on the free government sites below.", body)) - story.append(bullets([ - f"{meta['procedures']} (DOT testing procedures): " + b.page_break() + b.h1("Section 10 — The Regulations") + b.body( + "Your program is governed by two rules that work together: the DOT-wide " + f"testing procedures at {meta['procedures']}, and the mode rule at " + f"{meta['part']} from the {meta['agency']}. Read the official, always-" + "current text on the free government sites below.") + reg_items = [ + f"**{meta['procedures']} (DOT testing procedures):** " "ecfr.gov/current/title-49/subtitle-A/part-40", - f"{meta['part']} (your mode rule): ecfr.gov — search the part " - "number; this is the rule that defines who is covered and the testing " - "scenarios.", - "DOT ODAPC (plain-language guidance, forms, brochures): " + f"**{meta['part']} (your mode rule):** ecfr.gov — search the part " + "number; this rule defines who is covered and the testing scenarios.", + "**DOT ODAPC (plain-language guidance, forms, brochures):** " "transportation.gov/odapc", - ] + ([ - "FMCSA Drug & Alcohol Clearinghouse: " - "clearinghouse.fmcsa.dot.gov", - "FMCSA testing overview: fmcsa.dot.gov/regulations/" - "drug-alcohol-testing-program", - ] if meta.get("clearinghouse") else []))) - story.append(Spacer(1, 10)) - story.append(Paragraph( + ] + if meta.get("clearinghouse"): + reg_items += [ + "**FMCSA Drug & Alcohol Clearinghouse:** clearinghouse.fmcsa.dot.gov", + "**FMCSA testing overview:** fmcsa.dot.gov/regulations/" + "drug-alcohol-testing-program", + ] + b.bullets(reg_items) + b.small( "Regulations change. This binder reflects the rules in effect on " - f"{today}. Always confirm the current rule text on ecfr.gov, and " - "contact us with any questions at compliance@performancewest.com.", - small)) + f"{today}. Always confirm the current rule text on ecfr.gov, and have " + "counsel review this program before adoption. Questions: " + "compliance@performancewest.com.") # ── Optional state DFWP addendum ─────────────────────────────────────── if state_dfwp: - story.append(PageBreak()) - story.append(Paragraph( - f"Addendum — {state_dfwp} Drug-Free Workplace Program", h1)) - story.append(Paragraph( - f"In addition to your federal DOT program, " - f"{carrier_name or 'the Company'} maintains a Drug-Free Workplace " - f"Program consistent with {state_dfwp} law. This program runs " - "alongside (and does not " - "replace) the DOT testing program in this binder.", body)) - story.append(bullets([ + b.page_break() + b.h1(f"Addendum — {state_dfwp} Drug-Free Workplace Program") + b.body( + f"In addition to your federal DOT program, {company} maintains a " + f"Drug-Free Workplace Program consistent with {state_dfwp} law. This " + "program runs alongside (and does not replace) the DOT testing " + "program in this binder.") + b.bullets([ "The Company prohibits the unlawful manufacture, distribution, " "possession, or use of controlled substances in the workplace.", - "Employees must notify the Company of any criminal drug statute " - "conviction for a violation occurring in the workplace as required " + "Employees must notify the Company of any criminal drug-statute " + "conviction for a violation occurring in the workplace, as required " "by state law.", "The Company makes a good-faith effort to maintain a drug-free " "workplace through this policy, the awareness materials in Section " "7, and available counseling and treatment resources.", f"State-specific notice, testing, and appeal rights under " - f"{state_dfwp} law apply where they exceed federal requirements; " - "DOT rules always govern DOT-required tests.", - ])) - story.append(Spacer(1, 8)) - story.append(Paragraph( + f"{state_dfwp} law apply where they exceed federal requirements; DOT " + "rules always govern DOT-required tests.", + ]) + b.small( f"Confirm your {state_dfwp} program registration, premium-discount, " - "and notice requirements with the state agency or your insurer. We " - "can help — email compliance@performancewest.com.", small)) - - # ── Footer drawing ───────────────────────────────────────────────────── - def _decorations(canvas, doc): - canvas.saveState() - w, h = letter - # top accent bar (skip cover) - if doc.page > 1: - canvas.setFillColor(NAVY) - canvas.rect(0, h - 0.28 * inch, w, 0.28 * inch, fill=1, stroke=0) - canvas.setFillColor(HexColor("#ffffff")) - canvas.setFont("Helvetica-Bold", 8) - canvas.drawString(0.85 * inch, h - 0.19 * inch, - "DOT Drug & Alcohol Compliance Program") - canvas.drawRightString(w - 0.85 * inch, h - 0.19 * inch, - (carrier_name or "")[:48]) - # footer - canvas.setFillColor(SLATE) - canvas.setFont("Helvetica", 7.5) - canvas.drawString(0.85 * inch, 0.42 * inch, - f"Performance West Inc. • {meta['part']}") - canvas.drawRightString(w - 0.85 * inch, 0.42 * inch, - f"Page {doc.page}") - canvas.setStrokeColor(HexColor("#cbd5e1")) - canvas.setLineWidth(0.4) - canvas.line(0.85 * inch, 0.58 * inch, w - 0.85 * inch, 0.58 * inch) - canvas.restoreState() + "and notice requirements with the state agency or your insurer, and " + "have counsel review. We can help — compliance@performancewest.com.") out = Path(output_path) out.parent.mkdir(parents=True, exist_ok=True) - doc = BaseDocTemplate( - str(out), pagesize=letter, - leftMargin=0.85 * inch, rightMargin=0.85 * inch, - topMargin=0.55 * inch, bottomMargin=0.75 * inch, - title="DOT Drug & Alcohol Compliance Program", - author="Performance West Inc.", - ) - frame = Frame( - doc.leftMargin, doc.bottomMargin, - doc.width, doc.height - 0.18 * inch, id="body", - ) - doc.addPageTemplates([ - PageTemplate(id="main", frames=[frame], onPage=_decorations) - ]) - doc.build(story) - LOG.info("Generated D&A binder (%s mode) -> %s", mode, out) + doc.save(str(out)) + LOG.info("Generated D&A binder DOCX (%s mode) -> %s", mode, out) return str(out) -# ── Helpers ──────────────────────────────────────────────────────────────── +# ── Header / footer ───────────────────────────────────────────────────────── +def _add_header_footer(doc, meta, carrier_name): + section = doc.sections[0] + # Header + hp = section.header.paragraphs[0] + hp.text = "" + r = hp.add_run("DOT Drug & Alcohol Compliance Program") + r.bold = True + r.font.size = Pt(8) + r.font.color.rgb = NAVY + if carrier_name: + r2 = hp.add_run(" \u2022 " + carrier_name[:48]) + r2.font.size = Pt(8) + r2.font.color.rgb = SLATE + # Footer with page number field + fp = section.footer.paragraphs[0] + fp.text = "" + r = fp.add_run(f"Performance West Inc. \u2022 {meta['part']} Page ") + r.font.size = Pt(7.5) + r.font.color.rgb = SLATE + _add_page_field(fp) + + +def _add_page_field(paragraph): + run = paragraph.add_run() + fldChar1 = OxmlElement("w:fldChar") + fldChar1.set(qn("w:fldCharType"), "begin") + instrText = OxmlElement("w:instrText") + instrText.set(qn("xml:space"), "preserve") + instrText.text = "PAGE" + fldChar2 = OxmlElement("w:fldChar") + fldChar2.set(qn("w:fldCharType"), "end") + run._r.append(fldChar1) + run._r.append(instrText) + run._r.append(fldChar2) + run.font.size = Pt(7.5) + run.font.color.rgb = SLATE + + +# ── Forms (each starts a fresh page, fills a page) ────────────────────────── +def _form_header(b, code, title): + b.page_break() + b.h2(f"Form {code} — {title}") + + +def _form_a(b, carrier_name, meta): + _form_header(b, "A", "Employee Policy Receipt & Acknowledgment") + b.body( + f"I acknowledge that I have received, read, and understand the " + f"{carrier_name or 'Company'} Drug & Alcohol Testing Policy adopted " + f"under {meta['part']}, together with the educational materials on the " + "effects and signs of alcohol and controlled-substance use. I " + "understand that I am subject to pre-employment, random, reasonable-" + "suspicion, post-accident, return-to-duty, and follow-up testing, and " + "that a verified positive test, an alcohol concentration of 0.04 or " + "greater, or a refusal will remove me from safety-sensitive duty and " + "require completion of the DOT return-to-duty process before I may " + "return.") + b.body( + "I understand who the Designated Employer Representative (DER) is and " + "how to contact the DER with questions, and where to find treatment and " + "Employee Assistance resources.") + b.spacer(14) + for label in [ + "Employee name (print):", + "Employee signature:", + "Date: Position:", + "DER / witness (print):", + "DER / witness signature:", + ]: + b.fill_line(label + " " + "_" * max(6, 70 - len(label)), gap=18) + b.small( + "Keep the signed original in the employee's file for the duration of " + "employment plus one year.") + + +def _form_b(b, carrier_name, meta): + _form_header(b, "B", "Pre-Employment Consent & Prior-Employer Inquiry") + b.body( + f"Complete before a new covered employee performs safety-sensitive " + f"functions for {carrier_name or 'the Company'}. The applicant consents " + "to a DOT pre-employment drug test and to the Company's inquiry to " + "prior DOT employers about the applicant's DOT testing history.") + b.h3("Applicant consent") + b.body( + "I consent to a DOT pre-employment controlled-substances test and " + "authorise the Company to obtain my DOT drug and alcohol testing " + "history (including positives, refusals, and completed return-to-duty " + "follow-up) from my DOT-regulated employers for the past three years. " + "For CDL positions, I consent to the FMCSA Clearinghouse pre-employment " + "full query.") + b.spacer(10) + for label in [ + "Applicant name (print):", + "Signature: Date:", + "Position applied for:", + "Prior DOT employer(s) contacted:", + ]: + b.fill_line(label + " " + "_" * max(6, 66 - len(label)), gap=16) + b.h3("Company use — prior-employer / query results") + for label in [ + "Pre-employment drug test result: [ ] Negative [ ] Positive " + "[ ] Refusal", + "Clearinghouse pre-employment full query: [ ] No prohibited status " + "[ ] Prohibited", + "Prior-employer information received: [ ] Yes [ ] No [ ] No prior " + "DOT employer", + "Cleared to perform safety-sensitive functions: [ ] Yes [ ] No", + "DER (print): Signature / date:", + ]: + b.fill_line(label, gap=14) + b.small("Retain for the duration of employment plus the required period.") + + +def _form_c(b, carrier_name): + _form_header(b, "C", "Test Notification / Authorization to Test") + b.body( + "Use to notify an employee that they have been selected or directed for " + "a DOT test and to document the chain of timing. Give one copy to the " + "employee and keep one for the file.") + b.spacer(8) + for label in [ + "Employee name (print):", + "USDOT # / Company:", + "Reason for test: [ ] Random [ ] Reasonable suspicion " + "[ ] Post-accident [ ] Return-to-duty [ ] Follow-up " + "[ ] Pre-employment", + "Type of test: [ ] Drug [ ] Alcohol [ ] Both", + "Date/time employee notified:", + "Collection site name / address:", + "Date/time employee directed to report:", + "Date/time employee arrived at collection site:", + "Notes / delays (a delay or failure to proceed may be a refusal):", + ]: + b.fill_line(label + " " + "_" * max(4, 64 - len(label)), gap=14) + b.blank_lines(2) + for label in [ + "Employee signature: Date:", + "DER / supervisor (print): Signature:", + ]: + b.fill_line(label + " " + "_" * max(4, 60 - len(label)), gap=16) + b.small("Retain per the recordkeeping schedule in Section 8.") + + +def _form_d(b, carrier_name): + _form_header(b, "D", "Reasonable-Suspicion Observation Record") + b.body( + "Complete immediately when directing an employee to a reasonable-" + "suspicion test. Must be made by a supervisor trained per Section 5 and " + "signed and dated within 24 hours of the observations.") + b.spacer(8) + for label in [ + "Employee observed (print):", + "Date / time of observation:", + "Location:", + ]: + b.fill_line(label + " " + "_" * max(4, 60 - len(label)), gap=14) + b.body( + "Specific observations (appearance, behavior, speech, odor, " + "coordination, performance) — be factual and contemporaneous:") + b.blank_lines(5) + for label in [ + "Test directed: [ ] Drug [ ] Alcohol [ ] Both", + "Employee removed from safety-sensitive duty: [ ] Yes [ ] No", + "Trained supervisor (print):", + "Supervisor signature: Date:", + ]: + b.fill_line(label + (" " + "_" * max(4, 56 - len(label)) + if ":" in label and "[ ]" not in label else ""), + gap=14) + b.small( + "Retain for 5 years. The supervisor must have completed the required " + "120 minutes of reasonable-suspicion training (see Form F).") + + +def _form_e(b, carrier_name): + _form_header(b, "E", "Post-Accident Testing Decision Worksheet") + b.body( + "Use immediately after any accident to decide whether DOT post-accident " + "testing is required. **Test if ANY box below is checked.**") + b.spacer(6) + for label in [ + "[ ] The accident involved a **fatality**.", + "[ ] The driver received a **citation** AND a person was **injured and " + "required medical treatment away from the scene**.", + "[ ] The driver received a **citation** AND a **vehicle was towed** " + "from the scene due to disabling damage.", + ]: + b.fill_line(label, gap=8) + b.spacer(6) + b.body( + "If testing is required: conduct the **alcohol test within 8 hours** " + "(as soon as possible) and the **drug test within 32 hours**. Do not " + "let the driver consume alcohol before the alcohol test. Document any " + "reason a required test could not be completed.") + b.spacer(10) + for label in [ + "Driver (print):", + "Date / time of accident:", + "Citation issued to driver: [ ] Yes [ ] No", + "Decision: [ ] Test required [ ] Not required", + "Alcohol test completed (date/time):", + "Drug test completed (date/time):", + "DER (print): Signature / date:", + ]: + b.fill_line(label + (" " + "_" * max(4, 56 - len(label)) + if "[ ]" not in label else ""), gap=14) + b.small("Retain for 5 years, including any decision NOT to test.") + + +def _form_f(b, carrier_name): + _form_header(b, "F", "Supervisor Training Completion Record") + b.body( + f"Record each supervisor who has completed the {('')} required " + "reasonable-suspicion training: at least 60 minutes on alcohol misuse " + "and at least 60 minutes on controlled-substance use (120 minutes " + "total). Keep this record on file and produce it on audit.") + b.spacer(6) + b.table( + ["Supervisor name", "Alcohol (min)", "Drugs (min)", "Date", "Trainer / source"], + [["", "", "", "", ""] for _ in range(8)], + [2.2, 0.9, 0.9, 0.9, 1.6], + alt=False, + ) + b.spacer(10) + for label in [ + "DER (print): Signature / date:", + ]: + b.fill_line(label + " " + "_" * 20, gap=14) + b.small( + "A supervisor must complete this training before making any " + "reasonable-suspicion testing decision.") + + +# ── Misc helpers ──────────────────────────────────────────────────────────── def _covered_count(cdl_drivers, owner_operators) -> str: try: n = int(str(cdl_drivers).strip() or 0) @@ -785,88 +1216,3 @@ def _covered_count(cdl_drivers, owner_operators) -> str: return "—" extra = f" ({oo} owner-operator)" if oo else "" return f"{total} covered position(s){extra}" - - -def _append_form_a(story, h2, h3, body, small, carrier_name, meta): - from reportlab.lib.units import inch - from reportlab.platypus import PageBreak, Paragraph, Spacer - story.append(PageBreak()) - story.append(Paragraph("Form A — Employee Policy Receipt & Acknowledgment", h2)) - story.append(Paragraph( - f"I acknowledge that I have received, read, and understand the " - f"{carrier_name or 'Company'} Drug & Alcohol Testing Policy adopted " - f"under {meta['part']}. I understand that I am subject to pre-" - "employment, random, reasonable-suspicion, post-accident, return-to-" - "duty, and follow-up testing, and that a positive test or refusal will " - "remove me from safety-sensitive duty.", body)) - story.append(Spacer(1, 22)) - for label in ["Employee name (print): ______________________________________", - "Employee signature: _________________________________________", - "Date: ____________________ Position: ____________________", - "DER / witness: ______________________________________________"]: - story.append(Paragraph(label, body)) - story.append(Spacer(1, 10)) - story.append(Paragraph( - "Keep the signed original in the employee's file for the duration of " - "employment plus one year.", small)) - - -def _append_form_d(story, h2, body, small, carrier_name): - from reportlab.platypus import PageBreak, Paragraph, Spacer - story.append(PageBreak()) - story.append(Paragraph("Form D — Reasonable-Suspicion Observation Record", h2)) - story.append(Paragraph( - "Complete immediately when directing an employee to a reasonable-" - "suspicion test. Must be signed and dated within 24 hours.", body)) - story.append(Spacer(1, 10)) - for label in [ - "Employee observed: __________________________________________", - "Date / time of observation: _________________________________", - "Location: ____________________________________________________", - "Specific observations (appearance, behavior, speech, odor, " - "coordination, performance):", - "_____________________________________________________________", - "_____________________________________________________________", - "_____________________________________________________________", - "Test directed: [ ] Drug [ ] Alcohol [ ] Both", - "Trained supervisor (print): _________________________________", - "Supervisor signature: ________________________ Date: _______", - ]: - story.append(Paragraph(label, body)) - story.append(Spacer(1, 8)) - story.append(Paragraph( - "Retain for 5 years. The supervisor must have completed the required " - "120 minutes of reasonable-suspicion training (see Form F).", small)) - - -def _append_form_e(story, h2, body, small, carrier_name): - from reportlab.platypus import PageBreak, Paragraph, Spacer - story.append(PageBreak()) - story.append(Paragraph("Form E — Post-Accident Testing Decision Worksheet", h2)) - story.append(Paragraph( - "Use immediately after any accident to decide whether DOT post-" - "accident testing is required. Test if ANY box below is checked.", body)) - story.append(Spacer(1, 8)) - for label in [ - "[ ] The accident involved a fatality.", - "[ ] The driver received a citation AND a person was " - "injured and required medical treatment away from the scene.", - "[ ] The driver received a citation AND a vehicle was " - "towed from the scene due to disabling damage.", - ]: - story.append(Paragraph(label, body)) - story.append(Spacer(1, 4)) - story.append(Spacer(1, 8)) - story.append(Paragraph( - "If testing is required: alcohol test as soon as possible " - "(within 8 hours) and drug test within 32 hours. Document any reason a " - "required test could not be completed.", body)) - story.append(Spacer(1, 12)) - for label in [ - "Driver: ____________________________ Date/time of accident: ______", - "Decision: [ ] Test required [ ] Not required", - "DER (print): ______________________ Signature: __________________", - ]: - story.append(Paragraph(label, body)) - story.append(Spacer(1, 10)) - story.append(Paragraph("Retain for 5 years.", small)) diff --git a/scripts/workers/job_server.py b/scripts/workers/job_server.py index 66ed943..68db1ce 100644 --- a/scripts/workers/job_server.py +++ b/scripts/workers/job_server.py @@ -790,11 +790,14 @@ def _pw_email_html(headline: str, body_paragraphs: list[str], cta_text: str = "" def _send_instant_delivery( *, customer_email: str, customer_name: str, order_number: str, service_name: str, minio_paths: list[str], storage, + service_slug: str = "", ): - """Email generated PDFs to the customer immediately after payment. + """Email generated deliverables to the customer immediately after payment. - Only sends PDF files — DOCX files are internal (for authority - submission or admin review). Sends fancy branded HTML email. + Attaches customer-facing documents (PDF and DOCX). Some services deliver an + editable DOCX (e.g. the DOT Drug & Alcohol binder, intended for the + carrier/its counsel to review and adopt); others deliver print-ready PDFs. + Email copy is tailored per service. Sends fancy branded HTML email. """ import smtplib from email.mime.multipart import MIMEMultipart @@ -804,11 +807,16 @@ def _send_instant_delivery( from pathlib import Path import tempfile - # Filter to PDFs only — DOCX is for internal/authority use - pdf_paths = [p for p in minio_paths if p.lower().endswith(".pdf")] - if not pdf_paths: - LOG.info("No PDFs to deliver for %s (only DOCX generated)", order_number) + # Customer-facing deliverables: PDFs and DOCX (e.g. the editable DOT binder). + # Anything else (xlsx working files, etc.) stays internal. + deliver_paths = [ + p for p in minio_paths + if p.lower().endswith(".pdf") or p.lower().endswith(".docx") + ] + if not deliver_paths: + LOG.info("No customer deliverables to send for %s", order_number) return + pdf_paths = deliver_paths # name kept for downstream attach loop smtp_host = os.environ.get("SMTP_HOST", "co.carrierone.com") smtp_port = int(os.environ.get("SMTP_PORT", "587")) @@ -826,43 +834,101 @@ def _send_instant_delivery( for p in pdf_paths ) - body_parts = [ - f"Hi {name},", - f"Your {service_name} documents for order {order_number} are attached.", - f"", - "These documents were generated automatically upon payment confirmation. " - "If your order includes an FCC portal submission (e.g., ECFS upload for CPNI, " - "or RMD registration), our team will handle that separately and send you a " - "confirmation email with the filing confirmation number once submitted.", - "If anything in the documents is incorrect, don't worry — just reply " - "to this email or contact us at info@performancewest.net " - "and we'll fix it right away. A corrected submission will replace the original at no additional charge.", - "This fee is tax deductible as an ordinary business expense under IRC § 162.", - "— Performance West Compliance Team", - ] + is_dot = (service_slug or "").lower() in ( + "dot-drug-alcohol", "dot-drug-alcohol-program", "dot-da", + ) or "drug & alcohol" in (service_name or "").lower() + + if is_dot: + body_parts = [ + f"Hi {name},", + f"Your {service_name} for order " + f"{order_number} is attached as an editable Word " + "document.", + f"", + "This binder is a complete, ready-to-adopt program: a written " + "testing policy, step-by-step instructions for your Designated " + "Employer Representative, supervisor reasonable-suspicion training " + "materials, SAP and treatment resources, a recordkeeping schedule, " + "and every form you need (Forms A–F).", + "What to do next: open the document, fill in the " + "blanks (DER name, testing provider, effective date), have it " + "reviewed by your counsel if you wish, then sign the policy, give a " + "copy to each covered employee, and collect a signed acknowledgment " + "(Form A). Keep everything per the recordkeeping schedule in " + "Section 8.", + "Because it is a Word file, you and your attorney can edit it " + "freely to match your operation.", + "Questions or need a correction? Just reply to this email or " + "contact us at info@performancewest.net — " + "we'll help right away at no additional charge.", + "This fee is tax " + "deductible as an ordinary business expense under IRC § 162." + "", + "— Performance West Compliance Team", + ] + cta_text = "Manage My DOT Compliance" + cta_url = "https://performancewest.net/services/dot-drug-alcohol" + headline = "Your DOT Drug & Alcohol Compliance Binder is ready" + else: + body_parts = [ + f"Hi {name},", + f"Your {service_name} documents for order " + f"{order_number} are attached.", + f"", + "These documents were generated automatically upon payment " + "confirmation. If your order includes an FCC portal submission " + "(e.g., ECFS upload for CPNI, or RMD registration), our team will " + "handle that separately and send you a confirmation email " + "with the filing confirmation number once submitted.", + "If anything in the documents is incorrect, don't worry — " + "just reply to this email or contact us at " + "info@performancewest.net and we'll " + "fix it right away. A corrected submission will replace the " + "original at no additional charge.", + "This fee is tax " + "deductible as an ordinary business expense under IRC § 162." + "", + "— Performance West Compliance Team", + ] + cta_text = "Check My FCC Compliance Status" + cta_url = "https://performancewest.net/tools/fcc-compliance-check" + headline = f"Your {service_name} documents are ready" html = _pw_email_html( - headline=f"Your {service_name} documents are ready", + headline=headline, body_paragraphs=body_parts, - cta_text="Check My FCC Compliance Status", - cta_url="https://performancewest.net/tools/fcc-compliance-check", + cta_text=cta_text, + cta_url=cta_url, ) msg = MIMEMultipart("mixed") msg["From"] = from_email msg["To"] = customer_email - msg["Subject"] = f"Your {service_name} documents are ready \u2014 Order {order_number}" + msg["Subject"] = ( + f"Your {service_name} is ready \u2014 Order {order_number}" + if is_dot else + f"Your {service_name} documents are ready \u2014 Order {order_number}" + ) msg["Reply-To"] = "info@performancewest.net" msg.attach(MIMEText(html, "html")) - # Download PDFs from MinIO and attach + # Download deliverables from MinIO and attach (PDF + DOCX) with tempfile.TemporaryDirectory() as tmpdir: for remote_path in pdf_paths: local_path = os.path.join(tmpdir, Path(remote_path).name) try: storage.download(remote_path, local_path) + if remote_path.lower().endswith(".docx"): + subtype = ( + "vnd.openxmlformats-officedocument." + "wordprocessingml.document" + ) + else: + subtype = "pdf" with open(local_path, "rb") as f: - part = MIMEBase("application", "pdf") + part = MIMEBase("application", subtype) part.set_payload(f.read()) encoders.encode_base64(part) part.add_header( @@ -1219,6 +1285,7 @@ def handle_process_compliance_service(payload: dict) -> dict: service_name=handler_cls.SERVICE_NAME if hasattr(handler_cls, "SERVICE_NAME") else service_slug, minio_paths=minio_paths, storage=storage, + service_slug=service_slug, ) LOG.info("Instant delivery sent to %s for %s (%s)", customer_email, effective_order_number, service_slug) diff --git a/scripts/workers/services/dot_drug_alcohol.py b/scripts/workers/services/dot_drug_alcohol.py index 35cde2b..b186c19 100644 --- a/scripts/workers/services/dot_drug_alcohol.py +++ b/scripts/workers/services/dot_drug_alcohol.py @@ -2,20 +2,24 @@ DOT Drug & Alcohol Compliance Program handler ($149). Instant-delivery service: when a motor carrier orders the program we -generate a complete, print-ready PDF "binder" and email it to them -automatically (no admin step). The binder bundles the written testing -policy, program-management instructions, supervisor-training materials and -access, EAP / rehab / SAP resources, regulation citations, random-testing -instructions, required forms, and recordkeeping guidance. +generate a complete, editable Word (.docx) "binder" and email it to them +automatically (no admin step). DOCX is used so the carrier and its counsel +can review and adapt the program before adopting it. The binder bundles the +written testing policy, program-management instructions, supervisor-training +materials and access, EAP / rehab / SAP resources, regulation citations, +random-testing instructions, all required forms (A-F, each on its own page), +and recordkeeping guidance. Policy variant (DOT operating administration) selection: For a trucking carrier the program is FMCSA (49 CFR Part 382) — that is the default. If the customer's operation falls under a different DOT mode (FRA, PHMSA, FTA, FAA, USCG) we honor an explicit ``dot_da_mode`` value in the intake data. An optional ``state_dfwp`` value appends a state - Drug-Free Workplace addendum. + Drug-Free Workplace addendum (keyed to the carrier's home/principal state, + not per-employee — the federal DOT program itself is nationwide and not + state-specific). -Returns the local PDF path so job_server uploads it to MinIO and the +Returns the local DOCX path so job_server uploads it to MinIO and the INSTANT_DELIVERY path emails it to the customer. """ from __future__ import annotations @@ -92,7 +96,7 @@ class DrugAlcoholProgramHandler(BaseServiceHandler): c for c in carrier_name if c.isalnum() or c in (" ", "-", "_") ).strip().replace(" ", "_")[:40] or "carrier" out_path = os.path.join( - work_dir, f"DOT_DA_Compliance_Binder_{safe_name}_{date_str}.pdf" + work_dir, f"DOT_DA_Compliance_Binder_{safe_name}_{date_str}.docx" ) from scripts.document_gen.templates.dot_da_binder_generator import ( diff --git a/site/src/components/intake/steps/DOTIntakeStep.astro b/site/src/components/intake/steps/DOTIntakeStep.astro index ae3cbca..d385a83 100644 --- a/site/src/components/intake/steps/DOTIntakeStep.astro +++ b/site/src/components/intake/steps/DOTIntakeStep.astro @@ -162,8 +162,8 @@ @@ -316,6 +332,9 @@ .pw-cargo-grid label { display: flex; align-items: center; gap: 0.4rem; cursor: pointer; } .pw-security-notice { display: flex; gap: 8px; align-items: flex-start; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; padding: 12px 14px; margin-bottom: 1rem; font-size: 14px; color: #1e40af; line-height: 1.5; } .pw-upload-area { border: 2px dashed #d1d5db; border-radius: 8px; padding: 1rem; text-align: center; } + .pw-dfwp-box { background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 8px; padding: 0.85rem 1rem; margin-top: 0.75rem; } + .pw-checkbox-row { display: flex; gap: 0.6rem; align-items: flex-start; cursor: pointer; font-size: 0.85rem; color: #374151; } + .pw-checkbox-row input[type="checkbox"] { margin-top: 0.25rem; width: 16px; height: 16px; flex-shrink: 0; } .pw-err { color: #b91c1c; margin-top: 0.75rem; font-size: 0.9rem; background: #fee2e2; padding: 0.5rem 0.75rem; border-radius: 6px; } @media (max-width: 640px) { .pw-row-2, .pw-row-3 { grid-template-columns: 1fr; } .pw-cargo-grid { grid-template-columns: 1fr 1fr; } #dot-id-qr-section { display: none !important; } } @@ -379,6 +398,24 @@ ucrState.value = stateEl.value; stateEl.addEventListener("change", () => { ucrState.value = stateEl.value; }); } + + // D&A: state Drug-Free Workplace addendum toggle. + // Two-letter business state -> full state name used by the binder generator. + const ST_NAMES = {AL:"Alabama",AK:"Alaska",AZ:"Arizona",AR:"Arkansas",CA:"California",CO:"Colorado",CT:"Connecticut",DE:"Delaware",FL:"Florida",GA:"Georgia",HI:"Hawaii",ID:"Idaho",IL:"Illinois",IN:"Indiana",IA:"Iowa",KS:"Kansas",KY:"Kentucky",LA:"Louisiana",ME:"Maine",MD:"Maryland",MA:"Massachusetts",MI:"Michigan",MN:"Minnesota",MS:"Mississippi",MO:"Missouri",MT:"Montana",NE:"Nebraska",NV:"Nevada",NH:"New Hampshire",NJ:"New Jersey",NM:"New Mexico",NY:"New York",NC:"North Carolina",ND:"North Dakota",OH:"Ohio",OK:"Oklahoma",OR:"Oregon",PA:"Pennsylvania",RI:"Rhode Island",SC:"South Carolina",SD:"South Dakota",TN:"Tennessee",TX:"Texas",UT:"Utah",VT:"Vermont",VA:"Virginia",WA:"Washington",WV:"West Virginia",WI:"Wisconsin",WY:"Wyoming",DC:"District of Columbia"}; + const dfwpCb = document.getElementById("dot-da-dfwp"); + const dfwpRow = document.getElementById("dot-da-dfwp-state-row"); + const dfwpState = document.getElementById("dot-da-dfwp-state"); + if (dfwpCb && dfwpRow && !dfwpCb.dataset.wired) { + dfwpCb.dataset.wired = "1"; + dfwpCb.addEventListener("change", () => { + dfwpRow.hidden = !dfwpCb.checked; + // Default the DFWP state to the carrier's business state on first check. + if (dfwpCb.checked && dfwpState && !dfwpState.value) { + const full = ST_NAMES[(stateEl?.value || "").toUpperCase()]; + if (full) dfwpState.value = full; + } + }); + } } // Show sections — retry until wizard element is found @@ -413,7 +450,16 @@ const el = document.getElementById(id); if (el && val) el.value = val; } - }); + // Hydrate the D&A state Drug-Free Workplace addendum + const dfwpCb = document.getElementById("dot-da-dfwp"); + const dfwpRow = document.getElementById("dot-da-dfwp-state-row"); + const dfwpStateEl = document.getElementById("dot-da-dfwp-state"); + const wantsDfwp = d.include_state_dfwp === true || d.include_state_dfwp === "yes" || !!d.state_dfwp; + if (dfwpCb && wantsDfwp) { + dfwpCb.checked = true; + if (dfwpRow) dfwpRow.hidden = false; + if (dfwpStateEl && d.state_dfwp) dfwpStateEl.value = d.state_dfwp; + } // Save all data on step-next window.addEventListener("pw:step-next", (evt) => { @@ -469,6 +515,9 @@ der_name: val("dot-der-name"), current_da_provider: val("dot-current-da"), docket_type: val("dot-docket-type"), docket_number: val("dot-docket-num"), photo_id_uploaded: !!(window).__dotPhotoId, + // D&A: state Drug-Free Workplace addendum selection + include_state_dfwp: !!(document.getElementById("dot-da-dfwp"))?.checked, + state_dfwp: (document.getElementById("dot-da-dfwp"))?.checked ? val("dot-da-dfwp-state") : "", }}); });