new-site/scripts/workers/services/telecom/auto_filing.py
justin 0bdaa4c373 Fix auto_filing: check env var before ERPNext to avoid hanging on dev
When AUTO_FILING_ENABLED is explicitly set as an env var, skip the
ERPNext API call entirely. The ERPNext client hangs indefinitely
when the host is unreachable (dev workers can't reach prod ERPNext),
blocking all compliance handlers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 23:22:39 -05:00

311 lines
11 KiB
Python

"""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)."""
# Fast path: if AUTO_FILING_ENABLED env var is explicitly set, skip ERPNext
env_val = os.environ.get("AUTO_FILING_ENABLED")
if env_val is not None:
return _env_truthy(env_val), (os.environ.get("ADMIN_EMAIL") or DEFAULT_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 = """\
<html>
<body style="font-family:Arial,Helvetica,sans-serif;color:#1f2937;">
<h2 style="color:#1a2744;margin:0 0 12px;">Filing ready for review</h2>
<p><strong>Order:</strong> {order_number}<br>
<strong>Carrier:</strong> {entity_name}<br>
<strong>FRN:</strong> {frn}<br>
<strong>Service:</strong> {service_name}</p>
<p>Auto-filing is <em>disabled</em>. The generated packet is staged in MinIO.
Review the documents, then click below to submit the filing to the FCC.</p>
<p><a href="{approve_url}"
style="display:inline-block;padding:12px 22px;background:#059669;
color:#fff;border-radius:4px;font-weight:600;text-decoration:none;">
Approve &amp; File →
</a></p>
<h3 style="margin:22px 0 6px;color:#1a2744;">Packet files</h3>
<ul style="margin:0 0 12px 18px;padding:0;">
{files}
</ul>
{summary_block}
<p style="color:#64748b;font-size:12px;margin-top:24px;">
To enable auto-filing globally, set <code>auto_filing_enabled = 1</code>
in the ERPNext Compliance Settings doctype (or set
<code>AUTO_FILING_ENABLED=1</code> in the worker environment).
</p>
</body>
</html>
"""
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"<li style=\"margin:3px 0;\"><code>{p}</code></li>"
for p in packet_minio_paths
) or "<li><em>(none)</em></li>"
summary_block = (
f"<h3 style=\"margin:22px 0 6px;color:#1a2744;\">Handler notes</h3>"
f"<pre style=\"background:#f8fafc;padding:10px;border-radius:4px;"
f"white-space:pre-wrap;font-size:12px;\">{summary}</pre>"
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