new-site/scripts/workers/fax_sender.py
justin 1f1113d63c 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
2026-05-30 18:32:01 -05:00

254 lines
7.8 KiB
Python

"""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}",
}