"""Global auto-filing toggle + admin-review workflow. Gates the Playwright submission step of every FCC/USAC/BDC filing handler so that — by default — no filing is submitted to the FCC without a Performance West admin reviewing the generated packet first. Design ------ * Settings live in ERPNext ``Compliance Settings`` (single DocType) under the ``auto_filing_enabled`` Check field. Default = False (safer). * A per-order override field (``custom_auto_filing_override`` on the Sales Order) lets the admin approve a specific filing one-shot after review without flipping the global toggle. * When auto-filing is OFF and there's no per-order override, the handler: 1. Produces and uploads the packet as normal. 2. Creates an ERPNext ToDo assigned to ``admin_email`` (default ``ops@performancewest.net``) with a summary + "Approve & File" CTA link. 3. Sends the admin a short HTML email with the same summary + link. 4. Leaves the order in "Awaiting Admin Review" state — the workflow picks it back up when the admin clicks Approve. The Approve-and-File link hits the Express API endpoint ``POST /api/v1/compliance-orders/:order_number/approve-and-file`` which sets ``custom_auto_filing_override = 1`` on the Sales Order and re-dispatches the handler. Handler reads the override flag and runs the Playwright submission even if the global toggle is still off. Env fallback ------------ If ERPNext is unreachable (e.g. during local development), the module reads the env var ``AUTO_FILING_ENABLED`` as a truthy override. Admin email falls back to ``ADMIN_EMAIL`` env var, then ``ops@performancewest.net``. """ from __future__ import annotations import logging import os import smtplib from dataclasses import dataclass from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from typing import Optional logger = logging.getLogger(__name__) DEFAULT_ADMIN_EMAIL = "ops@performancewest.net" # ─── Settings lookup ────────────────────────────────────────────────────── @dataclass class AutoFilingDecision: """Result of ``check_auto_filing`` — caller branches on ``may_submit``.""" may_submit: bool reason: str admin_email: str global_enabled: bool order_override: bool def _env_truthy(value: str | None) -> bool: if not value: return False return value.strip().lower() in ("1", "true", "yes", "on") def _settings_from_erpnext() -> tuple[bool, str]: """Read ``Compliance Settings`` from ERPNext — returns (enabled, admin_email).""" try: from scripts.workers.erpnext_client import ERPNextClient erp = ERPNextClient() settings = erp.get_resource("Compliance Settings", "Compliance Settings") if isinstance(settings, list): settings = settings[0] if settings else {} enabled = bool(settings.get("auto_filing_enabled", 0)) admin = ( settings.get("admin_email") or os.environ.get("ADMIN_EMAIL") or DEFAULT_ADMIN_EMAIL ) return enabled, admin except Exception as exc: logger.debug("auto_filing: ERPNext settings read failed: %s", exc) return _env_truthy(os.environ.get("AUTO_FILING_ENABLED")), ( os.environ.get("ADMIN_EMAIL") or DEFAULT_ADMIN_EMAIL ) def check_auto_filing(order_data: dict) -> AutoFilingDecision: """Resolve whether the calling handler may submit to the FCC/USAC. The handler passes the current ``order_data`` (as received from ``job_server.py``). We inspect the per-order override first, then fall back to the global setting. """ order_override = bool(order_data.get("custom_auto_filing_override", 0)) global_enabled, admin_email = _settings_from_erpnext() if order_override: return AutoFilingDecision( may_submit=True, reason="per-order admin override", admin_email=admin_email, global_enabled=global_enabled, order_override=True, ) if global_enabled: return AutoFilingDecision( may_submit=True, reason="global auto_filing_enabled", admin_email=admin_email, global_enabled=True, order_override=False, ) return AutoFilingDecision( may_submit=False, reason="auto-filing disabled; admin review required", admin_email=admin_email, global_enabled=False, order_override=False, ) # ─── Admin review hand-off ──────────────────────────────────────────────── def request_admin_review( *, order_number: str, service_slug: str, service_name: str, entity_name: str, frn: str, packet_minio_paths: list[str], admin_email: str, summary: str = "", ) -> None: """Create an ERPNext ToDo + send a short email to the admin. The email and the ToDo both include a one-click "Approve & File" URL that re-dispatches the handler for this order with the auto-filing override set. """ api_base = os.environ.get("API_URL", "http://api:3001").rstrip("/") approve_url = ( f"{api_base}/api/v1/compliance-orders/{order_number}/approve-and-file" ) todo_description = ( f"[{service_slug}] Admin review required for {order_number}\n\n" f"Carrier: {entity_name}\n" f"FRN: {frn or 'N/A'}\n" f"Service: {service_name}\n\n" f"Auto-filing is disabled. The generated packet is staged in MinIO — " f"review it, then POST to the approve-and-file URL (or click the " f"button in the admin email) to submit the filing to the FCC.\n\n" f"Files in MinIO:\n" + "\n".join(f" • {p}" for p in packet_minio_paths) + f"\n\nApprove & File:\n {approve_url}\n" ) if summary: todo_description += f"\nHandler notes:\n{summary}\n" _create_todo(todo_description, admin_email) _send_admin_email( to_email=admin_email, order_number=order_number, service_name=service_name, entity_name=entity_name, frn=frn, packet_minio_paths=packet_minio_paths, approve_url=approve_url, summary=summary, ) def _create_todo(description: str, admin_email: str) -> None: try: from scripts.workers.erpnext_client import ERPNextClient ERPNextClient().create_resource( "ToDo", { "description": description, "priority": "High", "allocated_to": admin_email, "status": "Open", }, ) except Exception as exc: logger.warning("auto_filing: could not create admin ToDo: %s", exc) _EMAIL_TMPL = """\

Filing ready for review

Order: {order_number}
Carrier: {entity_name}
FRN: {frn}
Service: {service_name}

Auto-filing is disabled. The generated packet is staged in MinIO. Review the documents, then click below to submit the filing to the FCC.

Approve & File →

Packet files

{summary_block}

To enable auto-filing globally, set auto_filing_enabled = 1 in the ERPNext Compliance Settings doctype (or set AUTO_FILING_ENABLED=1 in the worker environment).

""" def _send_admin_email( *, to_email: str, order_number: str, service_name: str, entity_name: str, frn: str, packet_minio_paths: list[str], approve_url: str, summary: str, ) -> None: smtp_host = os.environ.get("SMTP_HOST", "co.carrierone.com") smtp_port = int(os.environ.get("SMTP_PORT", "587")) smtp_user = os.environ.get("SMTP_USER", "") smtp_pass = os.environ.get("SMTP_PASSWORD", "") smtp_from = os.environ.get("SMTP_FROM", "orders@performancewest.net") if not smtp_user or not smtp_pass: logger.warning("auto_filing: SMTP not configured — skipping admin email") return files_html = "\n ".join( f"
  • {p}
  • " for p in packet_minio_paths ) or "
  • (none)
  • " summary_block = ( f"

    Handler notes

    " f"
    {summary}
    " if summary else "" ) html = _EMAIL_TMPL.format( order_number=order_number, entity_name=entity_name, frn=frn or "N/A", service_name=service_name, approve_url=approve_url, files=files_html, summary_block=summary_block, ) msg = MIMEMultipart("alternative") msg["Subject"] = f"[Review & File] {order_number} — {service_name}" msg["From"] = smtp_from msg["To"] = to_email msg.attach(MIMEText(html, "html")) try: with smtplib.SMTP(smtp_host, smtp_port) as server: if smtp_port != 25: server.starttls() server.login(smtp_user, smtp_pass) server.sendmail(smtp_from, [to_email], msg.as_string()) logger.info("auto_filing: admin review email sent to %s", to_email) except Exception as exc: logger.warning("auto_filing: could not send admin email: %s", exc) # ─── Per-order override writer (used by the API approve-and-file endpoint) ─ def set_order_override(sales_order_name: str) -> bool: """Flip ``custom_auto_filing_override`` to 1 on the Sales Order.""" try: from scripts.workers.erpnext_client import ERPNextClient ERPNextClient().set_value( "Sales Order", sales_order_name, "custom_auto_filing_override", 1, ) return True except Exception as exc: logger.warning("auto_filing: could not set override: %s", exc) return False