#!/usr/bin/env python3 """AutoFax - Send a fax every hour via Sinch and log results.""" import json import subprocess import sys import time from datetime import datetime, timezone from pathlib import Path import requests import config API = f"https://fax.api.sinch.com/v3/projects/{config.SINCH_PROJECT_ID}" def auth(): return (config.SINCH_KEY_ID, config.SINCH_KEY_SECRET) def find_claim_pdf() -> Path: pdfs = sorted(config.CLAIMS_DIR.glob("*.pdf")) if not pdfs: print("ERROR: No PDF found in claims/ directory.", file=sys.stderr) sys.exit(1) return pdfs[0] def send_fax(pdf_path: Path) -> dict: """Send a fax via Sinch with direct file upload. Returns result dict.""" timestamp = datetime.now(timezone.utc).isoformat() try: with open(pdf_path, "rb") as f: resp = requests.post( f"{API}/faxes", auth=auth(), files={"file": (pdf_path.name, f, "application/pdf")}, data={k: v for k, v in { "to": config.FAX_TO_NUMBER, "from": config.SINCH_FROM_NUMBER or None, }.items() if v}, timeout=60, ) resp.raise_for_status() fax_data = resp.json() return { "timestamp": timestamp, "status": fax_data.get("status", "QUEUED"), "fax_id": fax_data.get("id"), "to": config.FAX_TO_NUMBER, "from": config.SINCH_FROM_NUMBER, "file": pdf_path.name, "error": None, } except requests.HTTPError as e: error_body = "" try: error_body = e.response.json() except Exception: error_body = e.response.text[:500] return { "timestamp": timestamp, "status": "send_failed", "fax_id": None, "to": config.FAX_TO_NUMBER, "from": config.SINCH_FROM_NUMBER, "file": pdf_path.name, "error": f"{e} | {error_body}", } except Exception as e: return { "timestamp": timestamp, "status": "send_failed", "fax_id": None, "to": config.FAX_TO_NUMBER, "from": config.SINCH_FROM_NUMBER, "file": pdf_path.name, "error": str(e), } def check_fax_status(fax_id: str) -> dict | None: if not fax_id: return None try: resp = requests.get(f"{API}/faxes/{fax_id}", auth=auth(), timeout=15) resp.raise_for_status() return resp.json() except Exception: return None def load_log() -> list[dict]: if config.LOG_FILE.exists(): return json.loads(config.LOG_FILE.read_text()) return [] def save_log(entries: list[dict]): config.REPORTS_DIR.mkdir(parents=True, exist_ok=True) config.LOG_FILE.write_text(json.dumps(entries, indent=2)) def notify(subject: str, message: str, priority: str = "default"): if not config.NTFY_URL: return try: hdrs = {"Title": subject, "Priority": priority} if config.NTFY_TOKEN: hdrs["Authorization"] = config.NTFY_TOKEN requests.post(config.NTFY_URL, data=message.encode(), headers=hdrs, timeout=10) except Exception: pass def generate_report(entries: list[dict]): config.REPORTS_DIR.mkdir(parents=True, exist_ok=True) total = len(entries) success_statuses = {"QUEUED", "IN_PROGRESS", "COMPLETED"} failed = sum(1 for e in entries if e["status"] not in success_statuses) succeeded = sum(1 for e in entries if e["status"] == "COMPLETED") pending = total - failed - succeeded rows = "" for i, e in enumerate(entries, 1): status = e["status"] if status == "COMPLETED": status_class = "success" status_display = "DELIVERED" elif status in ("QUEUED", "IN_PROGRESS"): status_class = "pending" status_display = status else: status_class = "failed" status_display = "FAILED" error_col = e.get("error") or e.get("failure_reason") or "-" try: dt = datetime.fromisoformat(e["timestamp"]) ts_display = dt.strftime("%b %d, %Y %I:%M %p UTC") except Exception: ts_display = e["timestamp"] rows += f""" {i} {ts_display} {e.get('to', '-')} {status_display} {e.get('fax_id') or '-'} {error_col} """ html = f""" Fax Transmission Report

Fax Transmission Report

This report documents automated fax transmission attempts to the insurance company for claim processing.

Total attempts: {total} | Delivered: {succeeded} | Failed: {failed} | Pending: {pending}

Fax destination: {entries[0]['to'] if entries else 'N/A'}

Report generated: {datetime.now(timezone.utc).strftime('%B %d, %Y at %I:%M %p UTC')}

{rows}
# Date & Time Fax Number Status Confirmation ID Error Details
""" report_path = config.REPORTS_DIR / "fax_report.html" report_path.write_text(html) return report_path def update_previous_statuses(entries: list[dict]) -> list[dict]: for entry in entries: if entry["status"] in ("QUEUED", "IN_PROGRESS"): status_data = check_fax_status(entry.get("fax_id")) if status_data: new_status = status_data.get("status", entry["status"]) if new_status != entry["status"]: entry["status"] = new_status if new_status in ("FAILURE", "failed"): entry["failure_reason"] = status_data.get("failureReason", "Unknown") return entries REQUIRED_SUCCESSES = 3 def count_successes(entries: list[dict]) -> int: return sum(1 for e in entries if e["status"] == "COMPLETED") def check_done(entries: list[dict]): """If we have enough successful deliveries, remove cron and exit.""" successes = count_successes(entries) if successes >= REQUIRED_SUCCESSES: print(f"AutoFax complete: {successes} successful deliveries. Removing cron job.") result = subprocess.run(["crontab", "-l"], capture_output=True, text=True) lines = [l for l in result.stdout.splitlines() if "autofax" not in l] subprocess.run(["crontab", "-"], input="\n".join(lines) + "\n", text=True) notify( "AutoFax Complete", f"{successes} faxes delivered successfully to {entries[-1].get('to', 'N/A')}. Cron job removed.", ) sys.exit(0) def main(): pdf_path = find_claim_pdf() entries = load_log() entries = update_previous_statuses(entries) # Check if we already hit the target after updating statuses check_done(entries) print(f"Sending fax: {pdf_path.name} -> {config.FAX_TO_NUMBER}") result = send_fax(pdf_path) entries.append(result) # Poll until final status (up to 3 minutes) if result["fax_id"] and result["status"] in ("QUEUED", "IN_PROGRESS"): print("Waiting for delivery confirmation...", end="", flush=True) for _ in range(12): time.sleep(15) print(".", end="", flush=True) status_data = check_fax_status(result["fax_id"]) if status_data: result["status"] = status_data.get("status", result["status"]) if result["status"] == "FAILURE": result["failure_reason"] = status_data.get("failureReason", "Unknown") if result["status"] in ("COMPLETED", "FAILURE"): break print() report_path = generate_report(entries) # Notify via ntfy successes = count_successes(entries) status_labels = { "COMPLETED": "Fax Delivered", "IN_PROGRESS": "Fax Sending", "QUEUED": "Fax Queued", } subject = status_labels.get(result["status"], "Fax Failed") msg = f"To: {result['to']}\nStatus: {result['status']}\nFile: {result['file']}" msg += f"\nDelivered: {successes}/{REQUIRED_SUCCESSES}" if result.get("error"): msg += f"\nError: {result['error']}" priority = "high" if result["status"] not in ("QUEUED", "IN_PROGRESS", "COMPLETED") else "default" notify(subject, msg, priority) print(f"Status: {result['status']}") print(f"Delivered: {successes}/{REQUIRED_SUCCESSES}") print(f"Report: {report_path}") # Check if this delivery hit the target check_done(entries) if result["status"] == "send_failed": sys.exit(1) if __name__ == "__main__": main()