DOT D&A: instant PDF compliance-program binder (49)
Turn the DOT Drug & Alcohol Compliance Program into an automated
instant-delivery deliverable: when a carrier orders, we generate a
complete, print-ready PDF binder and email it (no admin step).
The binder (dot_da_binder_generator.py) bundles everything a small
carrier needs under 49 CFR Part 382 + Part 40:
- How to manage the program (DER setup + annual operations)
- Written drug & alcohol testing policy for employees
- The six DOT test scenarios + triggers
- Random testing / consortium (C-TPA) instructions
- Supervisor reasonable-suspicion training + live/online access
- Violations, SAP access, return-to-duty / follow-up
- EAP / rehab / treatment resources (SAMHSA, 988, locator, ODAPC)
- Recordkeeping retention schedule
- Ready-to-use forms (acknowledgment, reasonable-suspicion,
post-accident decision worksheet)
- Regulation citations
- Optional state Drug-Free Workplace addendum
Policy-variant selection: FMCSA (Part 382) is the trucking default;
honors an explicit dot_da_mode override for FRA/PHMSA/FTA/FAA/USCG.
New DrugAlcoholProgramHandler returns the binder PDF; slug added to
INSTANT_DELIVERY_SLUGS so job_server emails it automatically. Slug
rerouted from MCS150UpdateHandler (was admin-assisted enrollment) and
re-priced as a discountable own-deliverable (no passthrough cost).
Tests: scripts/tests/test_dot_da_binder.py (FMCSA sections, PHMSA+state
addendum, all-modes render) — passing.
This commit is contained in:
parent
058d7cfbfe
commit
06e59965cc
6 changed files with 1172 additions and 2 deletions
872
scripts/document_gen/templates/dot_da_binder_generator.py
Normal file
872
scripts/document_gen/templates/dot_da_binder_generator.py
Normal file
|
|
@ -0,0 +1,872 @@
|
|||
"""
|
||||
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:
|
||||
|
||||
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
|
||||
|
||||
DOT operating administrations (policy variants):
|
||||
- FMCSA : 49 CFR Part 382 (motor carriers / CDL drivers) <- default
|
||||
- FRA : 49 CFR Part 219 (railroad / MOW)
|
||||
- PHMSA : 49 CFR Part 199 (pipeline)
|
||||
- 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",
|
||||
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",
|
||||
)
|
||||
"""
|
||||
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")
|
||||
|
||||
# ── 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]] = {
|
||||
"fmcsa": {
|
||||
"agency": "Federal Motor Carrier Safety Administration (FMCSA)",
|
||||
"part": "49 CFR Part 382",
|
||||
"procedures": "49 CFR Part 40",
|
||||
"covered": "safety-sensitive employees who operate a commercial motor "
|
||||
"vehicle (CMV) requiring a commercial driver's license (CDL)",
|
||||
"function": "operating a commercial motor vehicle requiring a CDL",
|
||||
"clearinghouse": True,
|
||||
},
|
||||
"fra": {
|
||||
"agency": "Federal Railroad Administration (FRA)",
|
||||
"part": "49 CFR Part 219",
|
||||
"procedures": "49 CFR Part 40",
|
||||
"covered": "employees who perform covered service (train and engine, "
|
||||
"signal, dispatching, and maintenance-of-way functions)",
|
||||
"function": "performing FRA-covered service",
|
||||
"clearinghouse": False,
|
||||
},
|
||||
"phmsa": {
|
||||
"agency": "Pipeline and Hazardous Materials Safety Administration (PHMSA)",
|
||||
"part": "49 CFR Part 199",
|
||||
"procedures": "49 CFR Part 40",
|
||||
"covered": "employees who perform operation, maintenance, or emergency-"
|
||||
"response functions on a pipeline or LNG facility",
|
||||
"function": "performing PHMSA-covered pipeline functions",
|
||||
"clearinghouse": False,
|
||||
},
|
||||
"fta": {
|
||||
"agency": "Federal Transit Administration (FTA)",
|
||||
"part": "49 CFR Part 655",
|
||||
"procedures": "49 CFR Part 40",
|
||||
"covered": "employees who perform safety-sensitive functions for a "
|
||||
"recipient of FTA funding",
|
||||
"function": "performing FTA safety-sensitive functions",
|
||||
"clearinghouse": False,
|
||||
},
|
||||
"faa": {
|
||||
"agency": "Federal Aviation Administration (FAA)",
|
||||
"part": "14 CFR Part 120",
|
||||
"procedures": "49 CFR Part 40",
|
||||
"covered": "employees who perform safety-sensitive aviation functions",
|
||||
"function": "performing FAA safety-sensitive functions",
|
||||
"clearinghouse": False,
|
||||
},
|
||||
"uscg": {
|
||||
"agency": "United States Coast Guard (USCG)",
|
||||
"part": "46 CFR Part 16",
|
||||
"procedures": "49 CFR Part 40",
|
||||
"covered": "crewmembers who occupy or fill a position that affects the "
|
||||
"safe operation of a vessel",
|
||||
"function": "occupying a safety-sensitive crew position",
|
||||
"clearinghouse": False,
|
||||
},
|
||||
}
|
||||
|
||||
# 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",
|
||||
"phmsa": "50% (controlled substances) of covered employees",
|
||||
"fta": "50% (controlled substances) and 10% (alcohol)",
|
||||
"faa": "the FAA-published annual minimum random testing rates",
|
||||
"uscg": "the USCG annual minimum random testing rate (currently 25%)",
|
||||
}
|
||||
|
||||
|
||||
def generate_da_binder(
|
||||
*,
|
||||
output_path: str,
|
||||
carrier_name: str,
|
||||
dot_number: str = "",
|
||||
mode: str = "fmcsa",
|
||||
cdl_drivers: int | str = "",
|
||||
owner_operators: int | str = "",
|
||||
der_name: str = "",
|
||||
der_title: str = "Designated Employer Representative (DER)",
|
||||
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")
|
||||
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")
|
||||
|
||||
NAVY = HexColor("#0b1f3a")
|
||||
BLUE = HexColor("#1d4ed8")
|
||||
SLATE = HexColor("#475569")
|
||||
LIGHT = HexColor("#eef2f7")
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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(
|
||||
f"Prepared under {meta['part']} and the U.S. DOT procedures at "
|
||||
f"{meta['procedures']}", cover_sub))
|
||||
story.append(Spacer(1, 0.55 * inch))
|
||||
|
||||
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 "")],
|
||||
["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(
|
||||
"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())
|
||||
|
||||
# ── Table of contents ──────────────────────────────────────────────────
|
||||
story.append(Paragraph("What's Inside This Binder", h1))
|
||||
toc = [
|
||||
"<b>Section 1 — How to Manage Your Program.</b> Step-by-step setup and "
|
||||
"ongoing operating instructions for the Designated Employer "
|
||||
"Representative (DER).",
|
||||
"<b>Section 2 — Written Testing Policy.</b> Your company drug & "
|
||||
"alcohol testing policy to distribute to every covered employee.",
|
||||
"<b>Section 3 — When Testing Is Required.</b> The six DOT test "
|
||||
"scenarios and what triggers each one.",
|
||||
"<b>Section 4 — Random Testing Program.</b> How the consortium random "
|
||||
"pool works and your obligations.",
|
||||
"<b>Section 5 — Supervisor Training.</b> Reasonable-suspicion training "
|
||||
"materials and how to access live/online training.",
|
||||
"<b>Section 6 — Violations, SAP & Return-to-Duty.</b> What happens "
|
||||
"after a positive test, and Substance Abuse Professional access.",
|
||||
"<b>Section 7 — EAP, Rehab & Treatment Resources.</b> Employee "
|
||||
"Assistance Program information and treatment referrals.",
|
||||
"<b>Section 8 — Recordkeeping.</b> What to keep, where, and for how long.",
|
||||
"<b>Section 9 — Required Forms.</b> Every form you need, ready to use.",
|
||||
"<b>Section 10 — The Regulations.</b> Citations and how to read the "
|
||||
"actual rule text.",
|
||||
]
|
||||
story.append(bullets(toc))
|
||||
if state_dfwp:
|
||||
story.append(Spacer(1, 4))
|
||||
story.append(Paragraph(
|
||||
f"<b>Addendum — {state_dfwp} Drug-Free Workplace Program.</b> "
|
||||
"State-specific policy supplement that runs alongside your DOT "
|
||||
"program.", body))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ── 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 "
|
||||
"<b>Designated Employer Representative (DER)</b>. Follow these steps.",
|
||||
body))
|
||||
story.append(Paragraph("Initial setup (do once)", h3))
|
||||
story.append(numbered([
|
||||
"<b>Name your DER.</b> 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.",
|
||||
"<b>Adopt the written policy</b> in Section 2. Sign it, date it, and "
|
||||
"keep the signed master copy.",
|
||||
"<b>Distribute the policy</b> 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.",
|
||||
"<b>Join a testing program / consortium (C-TPA).</b> Your program is "
|
||||
f"administered through {provider_name}, which manages your random "
|
||||
"pool, scheduling, collection sites, the lab, and the Medical Review "
|
||||
"Officer (MRO).",
|
||||
"<b>Conduct pre-employment drug tests</b> with a verified negative "
|
||||
"result before any new covered employee performs safety-sensitive "
|
||||
"functions.",
|
||||
] + ([
|
||||
"<b>Register with the FMCSA Clearinghouse</b> at "
|
||||
"clearinghouse.fmcsa.dot.gov and run the required queries (see below)."
|
||||
] if meta.get("clearinghouse") else [])))
|
||||
|
||||
story.append(Paragraph("Ongoing operations (every year)", h3))
|
||||
ongoing = [
|
||||
"Conduct <b>random tests</b> 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 <b>reasonable suspicion</b> when a trained supervisor "
|
||||
"observes the signs (see Section 5).",
|
||||
"Conduct <b>post-accident testing</b> when DOT criteria are met "
|
||||
"(see Section 3).",
|
||||
"Run <b>return-to-duty and follow-up tests</b> for any employee who "
|
||||
"violated the rule and completed the SAP process (see Section 6).",
|
||||
"Keep all <b>records</b> for the required retention periods "
|
||||
"(see Section 8).",
|
||||
]
|
||||
if meta.get("clearinghouse"):
|
||||
ongoing.append(
|
||||
"Run an <b>annual FMCSA Clearinghouse query</b> on every CDL "
|
||||
"driver, and a <b>full query</b> (with driver consent) before "
|
||||
"hiring any new driver. Report violations and refusals to the "
|
||||
"Clearinghouse.")
|
||||
story.append(bullets(ongoing))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ── 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))
|
||||
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
story.append(Paragraph("3. Prohibited Conduct", h3))
|
||||
story.append(Paragraph("A covered employee must not:", body))
|
||||
story.append(bullets([
|
||||
"Report for duty or remain on duty with an alcohol concentration of "
|
||||
"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.",
|
||||
]))
|
||||
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
story.append(Paragraph("6. Designated Employer Representative", h3))
|
||||
story.append(Paragraph(
|
||||
f"The Company's DER is {der_name or '________________________'}"
|
||||
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))
|
||||
|
||||
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())
|
||||
|
||||
# ── Section 3 — When testing is required ─────────────<E29480><E29480>─────────────────
|
||||
story.append(Paragraph("Section 3 — When Testing Is Required", h1))
|
||||
test_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())
|
||||
|
||||
# ── 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 "
|
||||
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([
|
||||
"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 <b>immediately</b> "
|
||||
"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([
|
||||
"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())
|
||||
|
||||
# ── Section 5 — Supervisor training ────────────────────────────────────
|
||||
story.append(Paragraph("Section 5 — Supervisor Reasonable-Suspicion Training", h1))
|
||||
story.append(Paragraph(
|
||||
f"{meta['part']} requires that any supervisor who may make a "
|
||||
"reasonable-suspicion testing decision complete at least <b>60 minutes "
|
||||
"of training on the symptoms of alcohol misuse</b> and at least "
|
||||
"<b>60 minutes on the symptoms of controlled-substance use</b> "
|
||||
"(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([
|
||||
"<b>Appearance:</b> bloodshot or watery eyes, dilated/constricted "
|
||||
"pupils, flushed face, disheveled clothing, drug paraphernalia.",
|
||||
"<b>Behavior:</b> mood swings, agitation, euphoria, drowsiness, "
|
||||
"paranoia, unusual or unsafe risk-taking.",
|
||||
"<b>Speech:</b> slurred, rapid, incoherent, or unusually talkative.",
|
||||
"<b>Coordination:</b> unsteady gait, tremors, poor balance, fumbling.",
|
||||
"<b>Odor:</b> alcohol or marijuana odor on breath, body, or clothing.",
|
||||
"<b>Performance:</b> 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.",
|
||||
"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 "
|
||||
"session), contact us and we will send each supervisor an enrollment "
|
||||
"link and issue a completion certificate for your records:", body))
|
||||
story.append(bullets([
|
||||
"<b>Email:</b> compliance@performancewest.com — subject line "
|
||||
"\"Supervisor Training\" with your USDOT number.",
|
||||
"<b>What you get:</b> self-paced online modules, a knowledge check, "
|
||||
"and a dated certificate of completion to keep on file.",
|
||||
]))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ── Section 6 — Violations / SAP ───────────────────────────────────────
|
||||
story.append(Paragraph("Section 6 — Violations, SAP & Return-to-Duty", h1))
|
||||
story.append(Paragraph(
|
||||
"If a covered employee has a verified positive test, an alcohol "
|
||||
"concentration of 0.04 or greater, or refuses a test, you must "
|
||||
"<b>immediately remove them from safety-sensitive duty</b>. 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([
|
||||
"<b>Remove from duty</b> and give the employee the contact information "
|
||||
"for a DOT-qualified Substance Abuse Professional (SAP).",
|
||||
"<b>SAP evaluation.</b> The SAP evaluates the employee and prescribes "
|
||||
"education and/or treatment.",
|
||||
"<b>Treatment / education</b> is completed as directed by the SAP.",
|
||||
"<b>Follow-up evaluation.</b> The SAP confirms the employee complied "
|
||||
"and is eligible to return.",
|
||||
"<b>Return-to-duty test.</b> The employee must pass a directly "
|
||||
"observed return-to-duty test before resuming duties.",
|
||||
"<b>Follow-up testing.</b> 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(
|
||||
"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))
|
||||
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())
|
||||
|
||||
# ── Section 7 — EAP / rehab ────────────────────────────────────────────
|
||||
story.append(Paragraph("Section 7 — EAP, Rehab & Treatment Resources", h1))
|
||||
story.append(Paragraph(
|
||||
"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([
|
||||
"<b>SAMHSA National Helpline:</b> 1-800-662-HELP (4357) — free, "
|
||||
"confidential, 24/7 treatment referral and information service.",
|
||||
"<b>SAMHSA treatment locator:</b> findtreatment.gov — searchable "
|
||||
"directory of treatment facilities by location.",
|
||||
"<b>988 Suicide & Crisis Lifeline:</b> call or text 988.",
|
||||
"<b>Alcoholics Anonymous:</b> aa.org — local meeting finder.",
|
||||
"<b>Narcotics Anonymous:</b> na.org — local meeting finder.",
|
||||
"<b>DOT SAP information:</b> 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(
|
||||
"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())
|
||||
|
||||
# ── 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 = [
|
||||
["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(
|
||||
"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())
|
||||
|
||||
# ── 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([
|
||||
"<b>Form A</b> — Employee Policy Receipt & Acknowledgment.",
|
||||
"<b>Form B</b> — Pre-Employment Testing Consent & Prior-Employer "
|
||||
"Inquiry.",
|
||||
"<b>Form C</b> — Test Notification / Authorization to Test.",
|
||||
"<b>Form D</b> — Reasonable-Suspicion Observation Record.",
|
||||
"<b>Form E</b> — Post-Accident Testing Decision Worksheet.",
|
||||
"<b>Form F</b> — 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)
|
||||
|
||||
# ── 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"<b>{meta['procedures']} (DOT testing procedures):</b> "
|
||||
"ecfr.gov/current/title-49/subtitle-A/part-40",
|
||||
f"<b>{meta['part']} (your mode rule):</b> ecfr.gov — search the part "
|
||||
"number; this is the rule that defines who is covered and the testing "
|
||||
"scenarios.",
|
||||
"<b>DOT ODAPC (plain-language guidance, forms, brochures):</b> "
|
||||
"transportation.gov/odapc",
|
||||
] + ([
|
||||
"<b>FMCSA Drug & Alcohol Clearinghouse:</b> "
|
||||
"clearinghouse.fmcsa.dot.gov",
|
||||
"<b>FMCSA testing overview:</b> fmcsa.dot.gov/regulations/"
|
||||
"drug-alcohol-testing-program",
|
||||
] if meta.get("clearinghouse") else [])))
|
||||
story.append(Spacer(1, 10))
|
||||
story.append(Paragraph(
|
||||
"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))
|
||||
|
||||
# ── 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([
|
||||
"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 "
|
||||
"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"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()
|
||||
|
||||
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)
|
||||
return str(out)
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||
def _covered_count(cdl_drivers, owner_operators) -> str:
|
||||
try:
|
||||
n = int(str(cdl_drivers).strip() or 0)
|
||||
except (TypeError, ValueError):
|
||||
n = 0
|
||||
try:
|
||||
oo = int(str(owner_operators).strip() or 0)
|
||||
except (TypeError, ValueError):
|
||||
oo = 0
|
||||
total = n + oo
|
||||
if total <= 0:
|
||||
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 <b>fatality</b>.",
|
||||
"[ ] The driver received a <b>citation</b> AND a person was "
|
||||
"<b>injured and required medical treatment away from the scene</b>.",
|
||||
"[ ] The driver received a <b>citation</b> AND a <b>vehicle was "
|
||||
"towed</b> 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))
|
||||
117
scripts/tests/test_dot_da_binder.py
Normal file
117
scripts/tests/test_dot_da_binder.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
"""Verify the DOT Drug & Alcohol compliance binder generates correctly.
|
||||
|
||||
Renders the binder for the default FMCSA (trucking) variant plus a non-default
|
||||
DOT mode with a state Drug-Free Workplace addendum, and asserts the PDFs are
|
||||
multi-page and contain the key deliverable sections the program promises:
|
||||
written policy, program-management instructions, supervisor training, EAP/SAP
|
||||
resources, random-testing instructions, recordkeeping, forms, and regulations.
|
||||
|
||||
Requires: reportlab, pypdf, and (for text extraction) pdfplumber.
|
||||
Run from the repo root with a venv that has those installed.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
_GEN = os.path.abspath(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__), "..", "document_gen", "templates",
|
||||
"dot_da_binder_generator.py",
|
||||
)
|
||||
)
|
||||
_spec = importlib.util.spec_from_file_location("dot_da_binder_generator", _GEN)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod) # type: ignore[union-attr]
|
||||
generate_da_binder = _mod.generate_da_binder
|
||||
MODE_META = _mod.MODE_META
|
||||
|
||||
|
||||
def _extract_text(path: str) -> str:
|
||||
import pypdf
|
||||
|
||||
reader = pypdf.PdfReader(path)
|
||||
return "\n".join(page.extract_text() or "" for page in reader.pages)
|
||||
|
||||
|
||||
def test_fmcsa_binder_has_all_sections():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
out = os.path.join(d, "binder.pdf")
|
||||
path = generate_da_binder(
|
||||
output_path=out,
|
||||
carrier_name="Acme Trucking LLC",
|
||||
dot_number="3456789",
|
||||
mode="fmcsa",
|
||||
cdl_drivers=4,
|
||||
owner_operators=1,
|
||||
der_name="Jane Owner",
|
||||
provider_name="Performance West Consortium",
|
||||
)
|
||||
assert path and os.path.exists(path)
|
||||
|
||||
import pypdf
|
||||
|
||||
assert len(pypdf.PdfReader(path).pages) >= 10, "binder should be substantial"
|
||||
|
||||
text = _extract_text(path)
|
||||
# FMCSA-specific facts
|
||||
assert "49 CFR Part 382" in text
|
||||
assert "Federal Motor Carrier Safety Administration" in text
|
||||
assert "Clearinghouse" in text, "FMCSA binder must mention the Clearinghouse"
|
||||
# The promised deliverables (one assertion each)
|
||||
for needle in [
|
||||
"How to Manage Your Program", # program-management instructions
|
||||
"Written Drug & Alcohol Testing Policy", # written policy
|
||||
"Random Testing Program", # random testing instructions
|
||||
"Supervisor", # supervisor training materials
|
||||
"Substance Abuse Professional", # SAP access
|
||||
"SAMHSA", # EAP / rehab / treatment resources
|
||||
"Recordkeeping", # recordkeeping instructions
|
||||
"Required Compliance Forms", # required forms
|
||||
"Acknowledgment", # an actual form
|
||||
"The Regulations", # copies/citations of regulations
|
||||
]:
|
||||
assert needle in text, f"missing deliverable section: {needle!r}"
|
||||
|
||||
|
||||
def test_non_fmcsa_mode_and_state_addendum():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
out = os.path.join(d, "binder_phmsa.pdf")
|
||||
path = generate_da_binder(
|
||||
output_path=out,
|
||||
carrier_name="Pipeline Co",
|
||||
dot_number="111",
|
||||
mode="phmsa",
|
||||
cdl_drivers=3,
|
||||
state_dfwp="Georgia",
|
||||
)
|
||||
assert path and os.path.exists(path)
|
||||
text = _extract_text(path)
|
||||
# PHMSA variant swaps in the right rule + agency, no Clearinghouse
|
||||
assert "49 CFR Part 199" in text
|
||||
assert "Pipeline and Hazardous Materials Safety Administration" in text
|
||||
assert "clearinghouse.fmcsa.dot.gov" not in text.lower()
|
||||
# State addendum present
|
||||
assert "Georgia Drug-Free Workplace Program" in text
|
||||
|
||||
|
||||
def test_all_modes_render():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
for mode in MODE_META:
|
||||
out = os.path.join(d, f"{mode}.pdf")
|
||||
path = generate_da_binder(
|
||||
output_path=out,
|
||||
carrier_name="Test Co",
|
||||
dot_number="1",
|
||||
mode=mode,
|
||||
cdl_drivers=2,
|
||||
)
|
||||
assert path and os.path.exists(path), f"mode {mode} failed to render"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_fmcsa_binder_has_all_sections()
|
||||
test_non_fmcsa_mode_and_state_addendum()
|
||||
test_all_modes_render()
|
||||
print("All DOT D&A binder tests passed.")
|
||||
|
|
@ -1204,6 +1204,8 @@ def handle_process_compliance_service(payload: dict) -> dict:
|
|||
"policy-development", "ccpa-audit", "privacy-policy",
|
||||
"data-mapping", "breach-response", "consent-audit",
|
||||
"dnc-compliance", "campaign-review",
|
||||
# DOT / FMCSA instant-delivery binders
|
||||
"dot-drug-alcohol", # generates the D&A compliance program binder
|
||||
}
|
||||
if service_slug in INSTANT_DELIVERY_SLUGS and minio_paths:
|
||||
customer_email = order.get("customer_email")
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ from .state_puc_filing import StatePucFilingHandler
|
|||
from .fcc_carrier_registration import FCCCarrierRegistrationHandler
|
||||
# DOT / FMCSA Motor Carrier Services
|
||||
from .mcs150_update import MCS150UpdateHandler
|
||||
from .dot_drug_alcohol import DrugAlcoholProgramHandler
|
||||
from .boc3_filing import BOC3FilingHandler
|
||||
# State-level trucking compliance (IRP, IFTA, weight taxes, MCP, etc.)
|
||||
from .state_trucking import StateTruckingHandler
|
||||
|
|
@ -109,7 +110,7 @@ SERVICE_HANDLERS: dict[str, type] = {
|
|||
"ucr-registration": MCS150UpdateHandler, # admin-assisted, same pattern
|
||||
"dot-registration": MCS150UpdateHandler, # admin-assisted
|
||||
"mc-authority": MCS150UpdateHandler, # admin-assisted
|
||||
"dot-drug-alcohol": MCS150UpdateHandler, # admin-assisted (partner enrollment)
|
||||
"dot-drug-alcohol": DrugAlcoholProgramHandler, # instant PDF binder ($149)
|
||||
"dot-audit-prep": MCS150UpdateHandler, # admin-assisted (document prep)
|
||||
"dot-full-compliance": MCS150UpdateHandler, # fans out to individual services
|
||||
"usdot-reactivation": MCS150UpdateHandler, # same FMCSA submission flow
|
||||
|
|
|
|||
178
scripts/workers/services/dot_drug_alcohol.py
Normal file
178
scripts/workers/services/dot_drug_alcohol.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
"""
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Returns the local PDF path so job_server uploads it to MinIO and the
|
||||
INSTANT_DELIVERY path emails it to the customer.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
from .base_handler import BaseServiceHandler
|
||||
|
||||
LOG = logging.getLogger("workers.services.dot_drug_alcohol")
|
||||
|
||||
# DOT operating administrations we can build a binder for. FMCSA is the
|
||||
# trucking default; everything else requires an explicit intake override.
|
||||
_VALID_MODES = {"fmcsa", "fra", "phmsa", "fta", "faa", "uscg"}
|
||||
|
||||
|
||||
class DrugAlcoholProgramHandler(BaseServiceHandler):
|
||||
"""Generate and instant-deliver the DOT D&A Compliance Program binder."""
|
||||
|
||||
SERVICE_SLUG = "dot-drug-alcohol"
|
||||
SERVICE_NAME = "DOT Drug & Alcohol Compliance Program"
|
||||
REQUIRES_LLM = False
|
||||
|
||||
async def process(self, order_data: dict) -> list[str]:
|
||||
order_number = order_data.get("order_number") or order_data.get("name", "")
|
||||
LOG.info("[%s] Building DOT D&A compliance binder", order_number)
|
||||
|
||||
intake = order_data.get("intake_data") or {}
|
||||
if isinstance(intake, str):
|
||||
try:
|
||||
intake = json.loads(intake)
|
||||
except (TypeError, ValueError):
|
||||
intake = {}
|
||||
|
||||
carrier_name = (
|
||||
intake.get("legal_name")
|
||||
or intake.get("entity_name")
|
||||
or order_data.get("customer_name", "")
|
||||
).strip()
|
||||
dot_number = str(intake.get("dot_number", "")).strip()
|
||||
cdl_drivers = intake.get("cdl_drivers", "")
|
||||
owner_operators = intake.get("owner_operators", "")
|
||||
der_name = (intake.get("der_name") or "").strip()
|
||||
current_provider = (intake.get("current_da_provider") or "").strip()
|
||||
|
||||
# ── Policy variant (DOT mode) selection ──────────────────────────
|
||||
mode = self._resolve_mode(intake)
|
||||
|
||||
# Optional state Drug-Free Workplace addendum. Accept either an
|
||||
# explicit flag/state value or derive from the carrier's base state
|
||||
# if the intake marks it as a DFWP state.
|
||||
state_dfwp = self._resolve_state_dfwp(intake)
|
||||
|
||||
# The C-TPA / consortium that administers the program. If the
|
||||
# customer already has a provider, name it; otherwise default to our
|
||||
# managed consortium.
|
||||
provider_name = current_provider or "Performance West Consortium / C-TPA"
|
||||
|
||||
# ── Guard: need at minimum the carrier name to personalize ───────
|
||||
if not carrier_name:
|
||||
LOG.warning(
|
||||
"[%s] No carrier name in intake — pausing for intake", order_number
|
||||
)
|
||||
self._request_intake(order_data)
|
||||
return []
|
||||
|
||||
# ── Generate the binder ──────────────────────────────────────────
|
||||
work_dir = self._safe_work_dir()
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
safe_name = "".join(
|
||||
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"
|
||||
)
|
||||
|
||||
from scripts.document_gen.templates.dot_da_binder_generator import (
|
||||
generate_da_binder,
|
||||
)
|
||||
|
||||
result = generate_da_binder(
|
||||
output_path=out_path,
|
||||
carrier_name=carrier_name,
|
||||
dot_number=dot_number,
|
||||
mode=mode,
|
||||
cdl_drivers=cdl_drivers,
|
||||
owner_operators=owner_operators,
|
||||
der_name=der_name,
|
||||
der_title="Designated Employer Representative (DER)",
|
||||
provider_name=provider_name,
|
||||
state_dfwp=state_dfwp,
|
||||
)
|
||||
|
||||
if not result:
|
||||
LOG.error("[%s] D&A binder generation returned no file", order_number)
|
||||
return []
|
||||
|
||||
LOG.info(
|
||||
"[%s] D&A binder ready (mode=%s, state_dfwp=%s) -> %s",
|
||||
order_number, mode, state_dfwp or "none", result,
|
||||
)
|
||||
return [result]
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Variant resolution
|
||||
# ------------------------------------------------------------------ #
|
||||
def _resolve_mode(self, intake: dict) -> str:
|
||||
"""Pick the DOT operating-administration variant.
|
||||
|
||||
Trucking carriers are FMCSA (49 CFR Part 382) by default. Honor an
|
||||
explicit ``dot_da_mode`` override for the rare non-FMCSA operation.
|
||||
"""
|
||||
raw = (intake.get("dot_da_mode") or intake.get("dot_mode") or "").lower().strip()
|
||||
if raw in _VALID_MODES:
|
||||
return raw
|
||||
# A few human-friendly aliases customers might supply.
|
||||
aliases = {
|
||||
"trucking": "fmcsa", "motor carrier": "fmcsa", "cdl": "fmcsa",
|
||||
"rail": "fra", "railroad": "fra",
|
||||
"pipeline": "phmsa", "transit": "fta", "bus": "fta",
|
||||
"aviation": "faa", "air": "faa",
|
||||
"maritime": "uscg", "marine": "uscg", "vessel": "uscg",
|
||||
}
|
||||
return aliases.get(raw, "fmcsa")
|
||||
|
||||
def _resolve_state_dfwp(self, intake: dict) -> str:
|
||||
"""Return a state name if a Drug-Free Workplace addendum is wanted."""
|
||||
explicit = (intake.get("state_dfwp") or "").strip()
|
||||
if explicit:
|
||||
return explicit
|
||||
# If the customer opted into a DFWP add-on, use their base/operating
|
||||
# state. Otherwise omit the addendum (DOT program stands alone).
|
||||
if str(intake.get("include_state_dfwp", "")).lower() in ("1", "true", "yes"):
|
||||
return (
|
||||
intake.get("base_state")
|
||||
or intake.get("address_state")
|
||||
or ""
|
||||
).strip()
|
||||
return ""
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------ #
|
||||
def _safe_work_dir(self) -> str:
|
||||
try:
|
||||
return self._make_work_dir()
|
||||
except Exception:
|
||||
return tempfile.mkdtemp(prefix="da_binder_")
|
||||
|
||||
def _request_intake(self, order_data: dict) -> None:
|
||||
"""Best-effort: nudge for intake when carrier name is missing."""
|
||||
try:
|
||||
fn = getattr(self, "_request_entity_intake", None)
|
||||
if callable(fn):
|
||||
fn(order_data)
|
||||
except Exception as exc:
|
||||
LOG.warning("Could not request intake: %s", exc)
|
||||
Loading…
Add table
Add a link
Reference in a new issue