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
This commit is contained in:
parent
e2c7cc582b
commit
1f1113d63c
4 changed files with 645 additions and 1 deletions
254
scripts/workers/fax_sender.py
Normal file
254
scripts/workers/fax_sender.py
Normal file
|
|
@ -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}",
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue