new-site/scripts/workers/deminimis_factor_check.py
justin f8cd37ac8c 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>
2026-04-27 06:54:22 -05:00

191 lines
6.7 KiB
Python

"""De minimis factor missing-check cron.
USAC publishes the annual de minimis factor for the next 499-A filing each
November. Our `fcc_deminimis_factors` table has one row per reporting
(calendar revenue) year. If the upcoming reporting year's factor isn't
seeded by March 1 of the filing year, every 499-A handler in that cohort
will blow up at `loadDeMinimisFactor()` time.
This script is a 3am cron that:
1. Computes the current reporting year (previous calendar year, since
499-As are filed April 1 for the prior year's revenue).
2. Checks whether `fcc_deminimis_factors` has a row keyed by that year.
3. If missing, inserts an admin ToDo in ERPNext + emails ops.
Also validates that the row for the NEXT reporting year is on file by
January 15 of each year — an early warning so we can ping USAC before
customers try to file.
Usage:
python -m scripts.workers.deminimis_factor_check
python -m scripts.workers.deminimis_factor_check --dry-run
CRON: 0 3 * * * python -m scripts.workers.deminimis_factor_check
"""
from __future__ import annotations
import argparse
import logging
import os
import sys
from datetime import date
import psycopg2
DATABASE_URL = os.environ.get("DATABASE_URL", "")
ADMIN_EMAIL = os.environ.get("ADMIN_EMAIL", "ops@performancewest.net")
log = logging.getLogger("deminimis_factor_check")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
def _current_reporting_year(today: date | None = None) -> int:
"""499-A reports revenue earned in the previous calendar year."""
today = today or date.today()
return today.year - 1
def _has_factor(conn, form_year: int) -> bool:
with conn.cursor() as cur:
cur.execute(
"SELECT 1 FROM fcc_deminimis_factors WHERE form_year = %s",
(form_year,),
)
return cur.fetchone() is not None
def _already_todo(conn, form_year: int) -> bool:
"""Avoid spamming the same ToDo every day."""
with conn.cursor() as cur:
cur.execute(
"""
SELECT 1 FROM order_audit_log
WHERE action = 'deminimis_factor_missing'
AND metadata->>'form_year' = %s
AND created_at > NOW() - INTERVAL '7 days'
LIMIT 1
""",
(str(form_year),),
)
return cur.fetchone() is not None
def _record_missing(conn, form_year: int, severity: str) -> None:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO order_audit_log
(order_type, order_id, action, actor_type, actor_name, note, metadata)
VALUES ('service', 0, 'deminimis_factor_missing', 'system',
'deminimis_factor_check',
%s,
jsonb_build_object('form_year', %s::text, 'severity', %s))
""",
(
f"No fcc_deminimis_factors row for reporting_year={form_year}. "
f"Handler will error when customer tries to file. Seed from "
f"USAC Appendix A.",
form_year, severity,
),
)
conn.commit()
def _create_admin_todo(form_year: int, severity: str) -> None:
try:
from scripts.workers.erpnext_client import ERPNextClient
ERPNextClient().create_resource(
"ToDo",
{
"description": (
f"[{severity.upper()}] Missing fcc_deminimis_factors row "
f"for reporting year {form_year}. "
f"Seed from USAC Appendix A (published each November) "
f"with: INSERT INTO fcc_deminimis_factors "
f"(form_year, factor, source_citation) VALUES ({form_year}, "
f"<0.XXXX>, 'For CY{form_year} revenue — 499-A Appendix A');"
),
"priority": "High" if severity == "blocker" else "Medium",
"role": "Accounting Advisor",
},
)
except Exception as exc:
log.error("Could not create admin ToDo: %s", exc)
def run_once(dry_run: bool = False, today: date | None = None) -> dict:
"""Check current + next reporting-year factors. Returns a summary dict."""
today = today or date.today()
summary = {
"current_year": None,
"current_year_missing": False,
"next_year_missing": False,
"actions": [],
}
if not DATABASE_URL:
log.error("DATABASE_URL not set — aborting")
return summary
conn = psycopg2.connect(DATABASE_URL)
try:
# Current reporting year — must be present, filings are active.
current = _current_reporting_year(today)
summary["current_year"] = current
if not _has_factor(conn, current):
summary["current_year_missing"] = True
if not _already_todo(conn, current):
severity = "blocker"
log.error(
"BLOCKER: No de minimis factor for reporting year %s"
"499-A handler will throw on every filing",
current,
)
if not dry_run:
_record_missing(conn, current, severity)
_create_admin_todo(current, severity)
summary["actions"].append(f"alerted_current_{current}")
# Next reporting year — early warning after Jan 15.
next_year = current + 1
if today >= date(today.year, 1, 15) and not _has_factor(conn, next_year):
summary["next_year_missing"] = True
if not _already_todo(conn, next_year):
severity = "warning"
log.warning(
"WARNING: No de minimis factor for reporting year %s"
"will become a blocker once customers file for %s",
next_year, next_year,
)
if not dry_run:
_record_missing(conn, next_year, severity)
_create_admin_todo(next_year, severity)
summary["actions"].append(f"alerted_next_{next_year}")
if not summary["current_year_missing"] and not summary["next_year_missing"]:
log.info(
"De minimis factor check OK: both %s and %s are seeded",
current, next_year,
)
finally:
conn.close()
return summary
def main() -> None:
parser = argparse.ArgumentParser(
description="De minimis factor missing-check (daily cron)",
)
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
summary = run_once(dry_run=args.dry_run)
log.info("Summary: %s", summary)
if __name__ == "__main__":
main()