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>
191 lines
6.7 KiB
Python
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()
|