"""New Carrier Onboarding bundle. One order, five sub-handlers executed in order: CORES/FRN → Form 499 Initial → RMD → CPNI → CALEA SSI. Each sub-handler honors the global auto-filing toggle independently; bundle halts only on hard-gating failures (missing FRN blocks 499 Initial and downstream RMD / CPNI). """ from __future__ import annotations import copy import logging from .base_handler import BaseServiceHandler from .cores_frn_registration import CORESFRNRegistrationHandler from .dc_agent import DCAgentHandler from .form_499_initial import Form499InitialHandler from .rmd_filing import RMDFilingHandler from .cpni_certification import CPNIFilingHandler from .calea_ssi import CALEASSIHandler logger = logging.getLogger(__name__) class NewCarrierBundleHandler(BaseServiceHandler): SERVICE_SLUG = "new-carrier-bundle" SERVICE_NAME = "New Carrier Onboarding Bundle" REQUIRES_LLM = False SUB_HANDLERS = ( ("cores-frn-registration", CORESFRNRegistrationHandler, True), # hard gate: blocks rest w/o FRN ("dc-agent", DCAgentHandler, False), # soft fail: 499 can default to NWRA ("fcc-499-initial", Form499InitialHandler, True), # hard gate: 499-A needs Filer ID ("rmd-filing", RMDFilingHandler, False), # soft fail ("cpni-certification", CPNIFilingHandler, False), ("calea-ssi", CALEASSIHandler, False), ) async def process(self, order_data: dict) -> list[str]: generated: list[str] = [] order_number = order_data.get("name", "") entity = dict(order_data.get("entity", {})) for slug, cls, is_hard_gate in self.SUB_HANDLERS: logger.info("NewCarrierBundle: dispatching %s for %s", slug, order_number) # Deep copy so each sub-handler gets its own intake_data dict — # mutations in one sub-handler must not leak to downstream siblings. sub_order = copy.deepcopy(order_data) sub_order["entity"] = dict(entity) sub_order["service_slug"] = slug sub = cls() try: files = await sub.process(sub_order) if files: generated.extend(files) except Exception as exc: logger.exception( "NewCarrierBundle: %s raised for %s: %s", cls.__name__, order_number, exc, ) self._record_sub_failure(order_number, cls.__name__, str(exc)) if is_hard_gate: logger.warning( "NewCarrierBundle: halting remaining sub-handlers " "(hard gate %s failed)", slug, ) return generated # Refresh entity snapshot — CORES writes FRN, 499 Initial writes # filer_id_499; downstream handlers need the updated values. entity = self._reload_entity(entity.get("id")) or entity # Hard-gate sanity: after CORES, we should have an FRN. if slug == "cores-frn-registration" and not entity.get("frn"): self._record_sub_failure( order_number, "CORES", "No FRN assigned — remaining bundle steps require it.", ) if is_hard_gate: return generated if slug == "fcc-499-initial" and not entity.get("filer_id_499"): self._record_sub_failure( order_number, "Form499Initial", "No Filer ID assigned — RMD and CPNI need it on the cert letters.", ) return generated # ------------------------------------------------------------------ def _reload_entity(self, entity_id) -> dict | None: if not entity_id: return None try: import os import psycopg2 import psycopg2.extras conn = psycopg2.connect(os.environ.get("DATABASE_URL", "")) try: with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute( "SELECT * FROM telecom_entities WHERE id=%s", (entity_id,), ) row = cur.fetchone() return dict(row) if row else None finally: conn.close() except Exception as exc: logger.warning("NewCarrierBundle: could not reload entity %s: %s", entity_id, exc) return None def _record_sub_failure(self, order_number: str, sub: str, detail: str) -> None: try: from scripts.workers.erpnext_client import ERPNextClient ERPNextClient().create_resource( "ToDo", { "description": ( f"[{self.SERVICE_SLUG}] {order_number}\n\n" f"Sub-handler {sub} failed or produced incomplete state: " f"{detail}. Other sub-handlers either continued " f"(soft-fail) or halted (hard gate). Review and " f"re-dispatch the failing slug once the underlying " f"issue is fixed." ), "priority": "High", "role": "Accounting Advisor", }, ) except Exception as exc: logger.error("Could not create bundle sub-failure ToDo: %s", exc)