- 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
254 lines
7.8 KiB
Python
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}",
|
|
}
|