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