Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
307 lines
10 KiB
Python
307 lines
10 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)."""
|
|
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
|