From 1f1113d63c136758c6843947bc6bbd07ffd1d8c0 Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 30 May 2026 18:32:01 -0500 Subject: [PATCH] add fax filing pipeline: VitalPBX sender, attestation cover page with digital signature, compliance checker pending filing override - filing_attestation.py: generates cover page attesting PW submitted document to recipient with date/time stamp, contact info, and digital signature - fax_sender.py: sends PDFs via VitalPBX API, polls for delivery, generates attested copy for customer records - dot-lookup.ts: if DOT has pending MCS-150 order, show green 'UPDATE SUBMITTED' instead of red 'OVERDUE' in compliance checker - requirements.txt: add pyhanko + cryptography for PDF digital signatures --- api/src/routes/dot-lookup.ts | 32 +- .../templates/filing_attestation.py | 355 ++++++++++++++++++ scripts/requirements.txt | 5 + scripts/workers/fax_sender.py | 254 +++++++++++++ 4 files changed, 645 insertions(+), 1 deletion(-) create mode 100644 scripts/document_gen/templates/filing_attestation.py create mode 100644 scripts/workers/fax_sender.py diff --git a/api/src/routes/dot-lookup.ts b/api/src/routes/dot-lookup.ts index 60fb9bf..6165b09 100644 --- a/api/src/routes/dot-lookup.ts +++ b/api/src/routes/dot-lookup.ts @@ -104,7 +104,37 @@ router.get("/api/v1/dot/lookup", async (req, res) => { const mcs150Date = mcs150Raw ? new Date(mcs150Raw).toISOString().split("T")[0] : null; const isOverdue = mcs150Date ? new Date(mcs150Date) < twoYearsAgo() : null; - if (outdated || isOverdue) { + // Check if we have a pending/recently-filed MCS-150 order for this DOT + let pendingFiling: { filed_at: string; order_number: string } | null = null; + try { + const pendingResult = await pool.query( + `SELECT order_number, updated_at, status FROM compliance_orders + WHERE intake_data->>'dot_number' = $1 + AND service_slug = 'mcs150-update' + AND status IN ('filed', 'submitted', 'processing', 'in_progress') + ORDER BY updated_at DESC LIMIT 1`, + [dotNumber], + ); + if (pendingResult.rows.length > 0) { + const row = pendingResult.rows[0] as Record; + pendingFiling = { + filed_at: new Date(row.updated_at as string).toISOString().split("T")[0], + order_number: row.order_number as string, + }; + } + } catch {} + + if (pendingFiling) { + // We filed it — show green/blue instead of red + checks.push({ + id: "mcs150", + label: "MCS-150 Biennial Update", + status: "green", + detail: `UPDATE SUBMITTED — filed by Performance West on ${pendingFiling.filed_at}. ` + + `FMCSA typically reflects updates within 5-10 business days. ` + + `If you need verification of this filing, contact us at (888) 411-0383.`, + }); + } else if (outdated || isOverdue) { checks.push({ id: "mcs150", label: "MCS-150 Biennial Update", diff --git a/scripts/document_gen/templates/filing_attestation.py b/scripts/document_gen/templates/filing_attestation.py new file mode 100644 index 0000000..a030646 --- /dev/null +++ b/scripts/document_gen/templates/filing_attestation.py @@ -0,0 +1,355 @@ +"""Filing Attestation Cover Page Generator. + +After a fax is confirmed delivered, this generates a cover page attesting +that Performance West submitted the document to the recipient, with a +date/time stamp and contact information for regulatory verification. + +The cover page is prepended to the original filed PDF and stored as the +customer's proof of filing. +""" + +from __future__ import annotations + +import logging +import os +import tempfile +from datetime import datetime, timezone +from pathlib import Path + +LOG = logging.getLogger("document_gen.filing_attestation") + +try: + from pypdf import PdfReader, PdfWriter + from pypdf.generic import NameObject +except ImportError: + PdfReader = None + + +def generate_attestation_page( + order_number: str, + dot_number: str, + entity_name: str, + document_type: str, + submitted_at: datetime, + recipient_name: str = "Federal Motor Carrier Safety Administration (FMCSA)", +) -> str: + """Generate a one-page attestation PDF. + + Args: + order_number: Our internal order number. + dot_number: USDOT number on the filing. + entity_name: Legal entity name. + document_type: e.g. "MCS-150 Biennial Update", "MCS-150B", etc. + submitted_at: UTC datetime when the filing was transmitted. + recipient_name: Who it was sent to (don't include fax/phone numbers). + + Returns: + Path to the attestation PDF. + """ + from reportlab.lib.pagesizes import letter + from reportlab.lib.units import inch + from reportlab.pdfgen import canvas + from reportlab.lib.colors import HexColor + + work_dir = tempfile.mkdtemp(prefix="pw_attest_") + filepath = os.path.join(work_dir, f"attestation_{order_number}.pdf") + + c = canvas.Canvas(filepath, pagesize=letter) + w, h = letter + + # Header bar + c.setFillColor(HexColor("#1a2744")) + c.rect(0, h - 1.2 * inch, w, 1.2 * inch, fill=True, stroke=False) + + c.setFillColor(HexColor("#ffffff")) + c.setFont("Helvetica-Bold", 20) + c.drawCentredString(w / 2, h - 0.6 * inch, "CERTIFICATE OF FILING") + c.setFont("Helvetica", 11) + c.drawCentredString(w / 2, h - 0.9 * inch, "Performance West Inc. — DOT Compliance Services") + + # Body + y = h - 1.8 * inch + c.setFillColor(HexColor("#1a2744")) + + def line(text, font="Helvetica", size=12, spacing=22): + nonlocal y + c.setFont(font, size) + c.drawString(1 * inch, y, text) + y -= spacing + + def label_value(label, value, spacing=22): + nonlocal y + c.setFont("Helvetica-Bold", 11) + c.drawString(1 * inch, y, label) + c.setFont("Helvetica", 11) + c.drawString(3.2 * inch, y, str(value)) + y -= spacing + + # Attestation text + c.setFont("Helvetica", 12) + text_block = ( + f"This is to certify that Performance West Inc., acting as the authorized " + f"compliance representative for {entity_name}, submitted the following " + f"document to {recipient_name} on the date and time indicated below." + ) + + # Word wrap + from reportlab.lib.utils import simpleSplit + lines = simpleSplit(text_block, "Helvetica", 12, w - 2 * inch) + for ln in lines: + c.drawString(1 * inch, y, ln) + y -= 18 + y -= 10 + + # Filing details + label_value("Document:", document_type) + label_value("USDOT Number:", dot_number) + label_value("Entity Name:", entity_name) + label_value("Submitted To:", recipient_name) + + # Format the timestamp + local_str = submitted_at.strftime("%B %d, %Y at %I:%M %p %Z") + label_value("Date/Time of Submission:", local_str) + label_value("Order Reference:", order_number) + + y -= 15 + + # Confirmation box + c.setStrokeColor(HexColor("#059669")) + c.setFillColor(HexColor("#f0fdf4")) + c.roundRect(0.8 * inch, y - 50, w - 1.6 * inch, 55, 6, fill=True, stroke=True) + c.setFillColor(HexColor("#166534")) + c.setFont("Helvetica-Bold", 11) + c.drawString(1.1 * inch, y - 18, "TRANSMISSION CONFIRMED") + c.setFont("Helvetica", 10) + c.drawString(1.1 * inch, y - 35, + "This document was successfully transmitted and received by the recipient.") + + y -= 80 + + # Attestation signature block + c.setFillColor(HexColor("#1a2744")) + line("", spacing=10) + c.setFont("Helvetica", 11) + c.drawString(1 * inch, y, + "This attestation is provided for regulatory verification purposes.") + y -= 16 + c.drawString(1 * inch, y, + "Please retain this document with your compliance records.") + y -= 30 + + # Signature line + c.setStrokeColor(HexColor("#374151")) + c.line(1 * inch, y, 3.5 * inch, y) + y -= 15 + c.setFont("Helvetica", 10) + c.drawString(1 * inch, y, "Performance West Inc.") + y -= 14 + c.drawString(1 * inch, y, "DOT Compliance Division") + + # Contact info footer + y = 1.2 * inch + c.setStrokeColor(HexColor("#e2e8f0")) + c.line(0.8 * inch, y + 15, w - 0.8 * inch, y + 15) + + c.setFillColor(HexColor("#64748b")) + c.setFont("Helvetica", 9) + c.drawCentredString(w / 2, y, + "For verification of this filing, please contact:") + y -= 14 + c.setFont("Helvetica-Bold", 10) + c.setFillColor(HexColor("#1a2744")) + c.drawCentredString(w / 2, y, "Performance West Inc.") + y -= 14 + c.setFont("Helvetica", 9) + c.setFillColor(HexColor("#64748b")) + c.drawCentredString(w / 2, y, + "(888) 411-0383 | compliance@performancewest.net | performancewest.net") + y -= 12 + c.drawCentredString(w / 2, y, + f"Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}") + + c.save() + LOG.info("Attestation cover page generated: %s", filepath) + return filepath + + +# ── Digital Signature ──────────────────────────────────────────────── + +# Path to the Performance West signing certificate (PKCS#12) +CERT_DIR = Path(__file__).resolve().parent.parent.parent.parent / "certs" +CERT_P12 = CERT_DIR / "performancewest-signing.p12" +CERT_PASS = os.getenv("PW_SIGNING_CERT_PASS", "performancewest2024") + + +def ensure_signing_cert() -> str: + """Create the self-signed signing certificate if it doesn't exist. + + Returns path to the .p12 file. + """ + if CERT_P12.exists(): + return str(CERT_P12) + + CERT_DIR.mkdir(parents=True, exist_ok=True) + + from cryptography import x509 + from cryptography.x509.oid import NameOID + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.hazmat.primitives.serialization import pkcs12 + + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.COUNTRY_NAME, "US"), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Texas"), + x509.NameAttribute(NameOID.LOCALITY_NAME, "Dallas"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Performance West Inc."), + x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "DOT Compliance Services"), + x509.NameAttribute(NameOID.COMMON_NAME, "Performance West Filing Attestation"), + ]) + + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.now(timezone.utc)) + .not_valid_after(datetime(2030, 12, 31, tzinfo=timezone.utc)) + .add_extension( + x509.BasicConstraints(ca=False, path_length=None), critical=True, + ) + .add_extension( + x509.KeyUsage( + digital_signature=True, content_commitment=True, + key_encipherment=False, data_encipherment=False, + key_agreement=False, key_cert_sign=False, crl_sign=False, + encipher_only=False, decipher_only=False, + ), + critical=True, + ) + .sign(key, hashes.SHA256()) + ) + + p12_data = pkcs12.serialize_key_and_certificates( + name=b"Performance West Signing", + key=key, + cert=cert, + cas=None, + encryption_algorithm=serialization.BestAvailableEncryption( + CERT_PASS.encode() + ), + ) + + CERT_P12.write_bytes(p12_data) + LOG.info("Self-signed signing certificate created: %s", CERT_P12) + return str(CERT_P12) + + +def digitally_sign_pdf(pdf_path: str) -> str: + """Apply a PAdES digital signature to a PDF. + + Uses the Performance West self-signed certificate. The signature + is visible in Adobe Reader and any PDF viewer that supports digital + signatures. + + Args: + pdf_path: Path to the PDF to sign. + + Returns: + Path to the signed PDF (overwrites input). + """ + try: + from pyhanko.sign import signers, fields as sig_fields + from pyhanko.sign.general import load_cert_from_pemder + from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter + from pyhanko import stamp + from cryptography.hazmat.primitives.serialization import pkcs12 + + cert_path = ensure_signing_cert() + + p12_data = Path(cert_path).read_bytes() + private_key, cert, chain = pkcs12.load_key_and_certificates( + p12_data, CERT_PASS.encode() + ) + + signer = signers.SimpleSigner( + signing_cert=cert, + signing_key=private_key, + cert_registry=None, + ) + + with open(pdf_path, "rb") as f: + w = IncrementalPdfFileWriter(f) + + sig_meta = signers.PdfSignatureMetadata( + field_name="PerformanceWestAttestation", + reason="Filing attestation — document submitted to FMCSA", + location="Dallas, TX", + contact_info="compliance@performancewest.net", + ) + + signed_path = pdf_path.replace(".pdf", "_signed.pdf") + with open(signed_path, "wb") as out_f: + signers.sign_pdf( + w, + sig_meta, + signer=signer, + output=out_f, + ) + + # Replace original with signed version + os.replace(signed_path, pdf_path) + LOG.info("PDF digitally signed: %s", pdf_path) + return pdf_path + + except ImportError: + LOG.warning("pyhanko not installed — skipping digital signature") + return pdf_path + except Exception as exc: + LOG.error("Digital signature failed (PDF still valid unsigned): %s", exc) + return pdf_path + + +def prepend_attestation(attestation_pdf: str, original_pdf: str, output_path: str = "") -> str: + """Prepend the attestation cover page to the original filed PDF. + + Args: + attestation_pdf: Path to the attestation PDF. + original_pdf: Path to the original MCS-150 PDF. + output_path: Where to write the combined PDF. If empty, auto-generates. + + Returns: + Path to the combined PDF. + """ + if PdfReader is None: + raise ImportError("pypdf not installed") + + writer = PdfWriter() + + # Add attestation page first + attest_reader = PdfReader(attestation_pdf) + for page in attest_reader.pages: + writer.add_page(page) + + # Add original document pages + orig_reader = PdfReader(original_pdf) + for page in orig_reader.pages: + writer.add_page(page) + + if not output_path: + work_dir = os.path.dirname(original_pdf) + base = os.path.splitext(os.path.basename(original_pdf))[0] + output_path = os.path.join(work_dir, f"{base}_with_attestation.pdf") + + with open(output_path, "wb") as f: + writer.write(f) + + LOG.info("Combined attestation + original → %s (%d pages)", + output_path, len(writer.pages)) + + # Apply digital signature to the final combined PDF + digitally_sign_pdf(output_path) + + return output_path diff --git a/scripts/requirements.txt b/scripts/requirements.txt index e318ca7..77a6401 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -29,6 +29,11 @@ dnspython>=2.6.0 # PDF form filling (MCS-150 official forms) pypdf>=4.0.0 +# PDF digital signatures (filing attestation) +pyhanko>=0.25.0 +pyhanko-certvalidator>=0.26.0 +cryptography>=43.0.0 + # HTTP clients aiohttp>=3.9.0 requests>=2.31.0 diff --git a/scripts/workers/fax_sender.py b/scripts/workers/fax_sender.py new file mode 100644 index 0000000..5799f2e --- /dev/null +++ b/scripts/workers/fax_sender.py @@ -0,0 +1,254 @@ +"""VitalPBX Fax Sender. + +Sends PDFs via VitalPBX API and polls for delivery confirmation. +After confirmed delivery, generates an attestation cover page and +prepends it to the original document for the customer's records. + +Usage: + from scripts.workers.fax_sender import send_fax, send_and_attest + + # Just send + result = await send_fax(pdf_url, recipient="12023663477") + + # Send, wait for confirmation, then generate attested copy + attested_path = await send_and_attest( + pdf_url=presigned_url, + original_pdf_path="/tmp/mcs150_filled.pdf", + recipient="12023663477", + order_number="CO-12345", + dot_number="1234567", + entity_name="ACME TRUCKING INC", + document_type="MCS-150 Biennial Update", + ) +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import os +from datetime import datetime, timezone + +import httpx + +LOG = logging.getLogger("workers.fax_sender") + +# VitalPBX configuration +PBX_HOST = os.getenv("VITALPBX_HOST", "pbx.carrierone.com") +PBX_API_KEY = os.getenv("VITALPBX_API_KEY", "cc50de7d854f344c596855435ce40821") +PBX_TENANT = os.getenv("VITALPBX_TENANT", "VitalPBX") +PBX_FAX_ID = int(os.getenv("VITALPBX_FAX_ID", "1")) + +# FMCSA fax number +FMCSA_FAX = "12023663477" + + +async def send_fax( + pdf_url: str, + recipient: str, + resolution: str = "medium", + retries: int = 2, + retry_time: int = 300, +) -> dict: + """Send a fax via VitalPBX API. + + Args: + pdf_url: Public URL to the PDF to fax. + recipient: Phone number to fax to (e.g. "12023663477"). + resolution: "standard", "medium", or "high". + retries: Number of retry attempts on failure. + retry_time: Seconds between retries. + + Returns: + dict with keys: success, log_id, device, status, error + """ + url = f"https://{PBX_HOST}/api/v2/virtual_faxes/{PBX_FAX_ID}/send" + headers = { + "app-key": PBX_API_KEY, + "tenant": PBX_TENANT, + } + form_data = { + "url": pdf_url, + "recipients": recipient, + "resolution": resolution, + "retries": str(retries), + "retry_time": str(retry_time), + } + + try: + async with httpx.AsyncClient(verify=False, timeout=30) as client: + resp = await client.post(url, headers=headers, data=form_data) + data = resp.json() + + if data.get("status") == "success": + log_ids = data.get("data", {}).get("logs", []) + log_id = log_ids[0] if log_ids else None + LOG.info("[fax] Sent to %s — log_id=%s, device=%s", + recipient, log_id, data["data"].get("device")) + return { + "success": True, + "log_id": log_id, + "device": data["data"].get("device"), + "file": data["data"].get("file"), + "status": "queued", + "error": None, + } + else: + LOG.error("[fax] API error: %s", data) + return { + "success": False, + "log_id": None, + "status": "error", + "error": data.get("message", "Unknown API error"), + } + except Exception as exc: + LOG.error("[fax] Send failed: %s", exc) + return { + "success": False, + "log_id": None, + "status": "error", + "error": str(exc), + } + + +async def poll_fax_status(log_id: str | int, timeout: int = 300, interval: int = 10) -> dict: + """Poll VitalPBX for fax delivery status. + + Args: + log_id: Fax log ID from send_fax(). + timeout: Max seconds to wait. + interval: Seconds between polls. + + Returns: + dict with keys: delivered, status, error, duration + """ + url = f"https://{PBX_HOST}/api/v2/virtual_faxes/log/{log_id}" + headers = { + "app-key": PBX_API_KEY, + "tenant": PBX_TENANT, + } + + start = datetime.now(timezone.utc) + elapsed = 0 + + while elapsed < timeout: + try: + async with httpx.AsyncClient(verify=False, timeout=15) as client: + resp = await client.get(url, headers=headers) + data = resp.json() + + fax = data.get("data", {}) + status = fax.get("status", "").lower() + error = fax.get("error") + + if status == "sent" or (status == "sent" and error == "OK"): + LOG.info("[fax] Log %s delivered in %ds", log_id, elapsed) + return { + "delivered": True, + "status": "sent", + "error": error, + "duration": elapsed, + } + elif status == "failed": + LOG.warning("[fax] Log %s failed: %s", log_id, error) + return { + "delivered": False, + "status": "failed", + "error": error, + "duration": elapsed, + } + # Still sending/queued — keep polling + except Exception as exc: + LOG.debug("[fax] Poll error (will retry): %s", exc) + + await asyncio.sleep(interval) + elapsed = int((datetime.now(timezone.utc) - start).total_seconds()) + + LOG.warning("[fax] Log %s timed out after %ds", log_id, timeout) + return { + "delivered": False, + "status": "timeout", + "error": f"Timed out after {timeout}s", + "duration": timeout, + } + + +async def send_and_attest( + pdf_url: str, + original_pdf_path: str, + recipient: str = FMCSA_FAX, + order_number: str = "", + dot_number: str = "", + entity_name: str = "", + document_type: str = "MCS-150 Biennial Update", + recipient_name: str = "Federal Motor Carrier Safety Administration (FMCSA)", +) -> dict: + """Send a fax, wait for delivery, then generate attested copy. + + Returns: + dict with keys: success, attested_pdf, fax_log_id, submitted_at, error + """ + # 1. Send the fax + send_result = await send_fax(pdf_url, recipient) + if not send_result["success"]: + return { + "success": False, + "attested_pdf": None, + "fax_log_id": None, + "submitted_at": None, + "error": send_result["error"], + } + + log_id = send_result["log_id"] + + # 2. Poll for delivery confirmation + poll_result = await poll_fax_status(log_id, timeout=300, interval=10) + + if not poll_result["delivered"]: + return { + "success": False, + "attested_pdf": None, + "fax_log_id": log_id, + "submitted_at": None, + "error": f"Fax delivery failed: {poll_result['error']}", + } + + # 3. Fax delivered — generate attestation + submitted_at = datetime.now(timezone.utc) + + try: + from scripts.document_gen.templates.filing_attestation import ( + generate_attestation_page, + prepend_attestation, + ) + + attest_pdf = generate_attestation_page( + order_number=order_number, + dot_number=dot_number, + entity_name=entity_name, + document_type=document_type, + submitted_at=submitted_at, + recipient_name=recipient_name, + ) + + combined_pdf = prepend_attestation(attest_pdf, original_pdf_path) + + LOG.info("[fax] Attested copy generated: %s", combined_pdf) + + return { + "success": True, + "attested_pdf": combined_pdf, + "fax_log_id": log_id, + "submitted_at": submitted_at.isoformat(), + "error": None, + } + except Exception as exc: + LOG.error("[fax] Attestation generation failed: %s", exc) + return { + "success": True, # Fax still succeeded + "attested_pdf": None, + "fax_log_id": log_id, + "submitted_at": submitted_at.isoformat(), + "error": f"Fax sent but attestation failed: {exc}", + }