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>
311 lines
11 KiB
Python
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 & 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
|