Initial commit — Performance West telecom compliance platform
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>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
307
scripts/workers/services/telecom/auto_filing.py
Normal file
307
scripts/workers/services/telecom/auto_filing.py
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue