From 06e59965cc34d5c3a2f4d4eb1a61ac00bf306de8 Mon Sep 17 00:00:00 2001 From: justin Date: Tue, 2 Jun 2026 19:28:58 -0500 Subject: [PATCH] DOT D&A: instant PDF compliance-program binder (49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- api/src/routes/compliance-orders.ts | 2 +- .../templates/dot_da_binder_generator.py | 872 ++++++++++++++++++ scripts/tests/test_dot_da_binder.py | 117 +++ scripts/workers/job_server.py | 2 + scripts/workers/services/__init__.py | 3 +- scripts/workers/services/dot_drug_alcohol.py | 178 ++++ 6 files changed, 1172 insertions(+), 2 deletions(-) create mode 100644 scripts/document_gen/templates/dot_da_binder_generator.py create mode 100644 scripts/tests/test_dot_da_binder.py create mode 100644 scripts/workers/services/dot_drug_alcohol.py diff --git a/api/src/routes/compliance-orders.ts b/api/src/routes/compliance-orders.ts index 1cc678d..55e72ec 100644 --- a/api/src/routes/compliance-orders.ts +++ b/api/src/routes/compliance-orders.ts @@ -267,7 +267,7 @@ const COMPLIANCE_SERVICES: Record< name: "DOT Drug & Alcohol Compliance Program", price_cents: 14900, erpnext_item: "DOT-DRUG-ALCOHOL", - discountable: false, // passthrough cost — D&A testing provider + discountable: true, // instant PDF binder we generate — our own deliverable, no passthrough cost }, "dot-audit-prep": { name: "New Entrant Safety Audit Preparation", diff --git a/scripts/document_gen/templates/dot_da_binder_generator.py b/scripts/document_gen/templates/dot_da_binder_generator.py new file mode 100644 index 0000000..9f4a390 --- /dev/null +++ b/scripts/document_gen/templates/dot_da_binder_generator.py @@ -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 = [ + "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 " + "pool works and your obligations.", + "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)) + if state_dfwp: + story.append(Spacer(1, 4)) + story.append(Paragraph( + f"Addendum — {state_dfwp} Drug-Free Workplace Program. " + "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 " + "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 []))) + + story.append(Paragraph("Ongoing operations (every year)", h3)) + 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).", + ] + 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()) + + # ── 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 ─────────────��───────────────── + 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 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([ + "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 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.", + "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([ + "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()) + + # ── 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 " + "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 " + "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( + "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([ + "SAMHSA National Helpline: 1-800-662-HELP (4357) — free, " + "confidential, 24/7 treatment referral and information service.", + "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( + "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([ + "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.", + ])) + + _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"{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): " + "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( + "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 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/tests/test_dot_da_binder.py b/scripts/tests/test_dot_da_binder.py new file mode 100644 index 0000000..4ad8878 --- /dev/null +++ b/scripts/tests/test_dot_da_binder.py @@ -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.") diff --git a/scripts/workers/job_server.py b/scripts/workers/job_server.py index 6b16b0b..66ed943 100644 --- a/scripts/workers/job_server.py +++ b/scripts/workers/job_server.py @@ -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") diff --git a/scripts/workers/services/__init__.py b/scripts/workers/services/__init__.py index 634693f..c90f389 100644 --- a/scripts/workers/services/__init__.py +++ b/scripts/workers/services/__init__.py @@ -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 diff --git a/scripts/workers/services/dot_drug_alcohol.py b/scripts/workers/services/dot_drug_alcohol.py new file mode 100644 index 0000000..35cde2b --- /dev/null +++ b/scripts/workers/services/dot_drug_alcohol.py @@ -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)