"""Foreign Qualification (Certificate of Authority) handler. Processes per-state COA filings for entities expanding to additional states. Supports two modes: - `foreign-qualification-single`: one target state per order. - `foreign-qualification-multi`: N target states, one order. The handler fans out one `foreign_qualification_registrations` row per state and processes them sequentially. FCC carriers ordering multi-state registration get the same flow — their entity is identified by telecom_entity_id, the handler reads the selected target states from intake_data.target_states. Filing steps per state (high-level): 1. Obtain Certificate of Good Standing from home state (if required by target state's `foreign_qual_requires_coa` flag). 2. Provision NW Registered Agent address in target state (if the order includes RA service). 3. Submit the Certificate of Authority / Application for Registration via the target state's SOS portal (Playwright adapter). 4. Capture confirmation, persist COA document. 5. Mark `foreign_qualification_registrations.status = 'approved'`. """ from __future__ import annotations import logging import os from datetime import datetime from typing import Optional import psycopg2 from .base_handler import BaseServiceHandler logger = logging.getLogger(__name__) DATABASE_URL = os.environ.get("DATABASE_URL", "") class ForeignQualificationHandler(BaseServiceHandler): SERVICE_SLUG = "foreign-qualification" SERVICE_NAME = "Foreign Qualification" REQUIRES_LLM = False async def process(self, order_data: dict) -> list[str]: order_number = order_data["name"] entity = order_data.get("entity", {}) or {} intake = order_data.get("intake_data", {}) or {} home_state = ( intake.get("home_state_code") or entity.get("state_of_formation") or entity.get("address_state") or "" ).upper() entity_type = ( intake.get("entity_type") or entity.get("entity_type") or "llc" ).lower() target_states = intake.get("target_states") or [] if isinstance(target_states, str): target_states = [s.strip().upper() for s in target_states.split(",") if s.strip()] else: target_states = [str(s).strip().upper() for s in target_states] if not target_states: self._create_admin_todo( order_number, f"{self.SERVICE_NAME}: no target states specified in intake_data. " "Admin should add target_states and re-dispatch.", ) return [] entity_name = ( intake.get("entity_legal_name") or entity.get("legal_name") or "" ) if not entity_name: self._create_admin_todo( order_number, f"{self.SERVICE_NAME}: missing entity legal name. " "Admin should set entity_legal_name in intake_data.", ) return [] # ── Fan out: one registration row per target state ────────────── conn = psycopg2.connect(DATABASE_URL) try: with conn.cursor() as cur: for state_code in target_states: # Fetch state fee from state_filing_fees. fee_col = ( "foreign_llc_fee" if entity_type in ("llc", "pllc") else "foreign_corp_fee" ) cur.execute( f"SELECT {fee_col} AS fee FROM state_filing_fees WHERE state_code = %s", (state_code,), ) fee_row = cur.fetchone() state_fee = int(fee_row[0]) if fee_row and fee_row[0] else 0 # Check for existing active registration to avoid dupes. cur.execute( """ SELECT id FROM foreign_qualification_registrations WHERE order_number = %s AND target_state_code = %s AND status NOT IN ('cancelled','rejected') LIMIT 1 """, (order_number, state_code), ) if cur.fetchone(): logger.info( "ForeignQualHandler: %s already has active registration " "for %s — skipping", order_number, state_code, ) continue # Compliance order ID (nullable). co_id = order_data.get("compliance_order_id") or order_data.get("id") cur.execute( """ INSERT INTO foreign_qualification_registrations ( compliance_order_id, order_number, telecom_entity_id, home_country, home_state_code, target_state_code, entity_legal_name, entity_type, formed_on, home_state_filing_number, ein, principal_address_json, include_ra_service, state_fee_cents, expedited, status ) VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, %s, %s, %s, 'received' ) """, ( co_id, order_number, entity.get("id"), intake.get("home_country", "US"), home_state, state_code, entity_name, entity_type, entity.get("formed_on"), entity.get("state_filing_number"), entity.get("ein"), _json_or_null(intake.get("principal_address")), intake.get("include_ra_each", True), state_fee, bool(intake.get("expedited", False)), ), ) conn.commit() logger.info( "ForeignQualHandler: created %d registration(s) for %s", len(target_states), order_number, ) except Exception as exc: logger.exception( "ForeignQualHandler: DB error creating registrations for %s: %s", order_number, exc, ) conn.rollback() self._create_admin_todo( order_number, f"{self.SERVICE_NAME}: failed to create registration rows — {exc}", ) return [] finally: conn.close() # ── Per-state filing dispatch ─────────────────────────────────── # For v1, each state filing needs admin review because we haven't # built the state-specific Playwright adapters for foreign qual yet. # The handler creates a ToDo per state so the admin can file manually # or trigger the adapter once it's written. filed_count = 0 for state_code in target_states: try: filed = await self._process_one_state( order_number=order_number, entity=entity, intake=intake, entity_name=entity_name, entity_type=entity_type, home_state=home_state, target_state=state_code, ) if filed: filed_count += 1 except Exception as exc: logger.error( "ForeignQualHandler: error filing %s in %s: %s", order_number, state_code, exc, ) self._update_registration_status( order_number, state_code, "admin_review", last_error=str(exc), ) if filed_count == 0: logger.info( "ForeignQualHandler: no states auto-filed for %s — " "all set to admin_review", order_number, ) return [] # artifacts come from per-state filings, not the parent async def _process_one_state( self, *, order_number: str, entity: dict, intake: dict, entity_name: str, entity_type: str, home_state: str, target_state: str, ) -> bool: """Attempt to file the COA in one target state. Returns True if the adapter succeeded, False if we parked in admin_review.""" from scripts.formation.jurisdictions import get_jurisdiction jc = get_jurisdiction(target_state) if not jc.has_adapter(): logger.info( "ForeignQualHandler: no adapter for %s — parking for admin", target_state, ) self._update_registration_status( order_number, target_state, "admin_review", last_error=f"No Playwright adapter for {target_state} foreign-qual", ) self._create_admin_todo( order_number, f"Foreign qualification in {jc.name} ({target_state}) needs " f"manual filing — no adapter. Entity: {entity_name} ({entity_type}) " f"from {home_state}.", ) return False # TODO: dispatch to adapter.file_foreign_qualification() once # per-state adapters implement the method. For now, all states # park at admin_review. self._update_registration_status( order_number, target_state, "admin_review", last_error="Foreign qual adapter not yet implemented", ) self._create_admin_todo( order_number, f"Foreign qualification in {jc.name} ({target_state}): entity " f"{entity_name} ({entity_type}) from {home_state}. " f"State fee: ${jc.foreign_qualification_fee_cents(entity_type) or 0 / 100:.0f}. " f"RA: {'NWRA requested' if intake.get('include_ra_each', True) else 'not requested'}. " f"File via {jc.portal_name or 'portal'} at {jc.portal_url or 'N/A'}.", ) return False def _update_registration_status( self, order_number: str, target_state: str, status: str, *, last_error: Optional[str] = None, ) -> None: try: conn = psycopg2.connect(DATABASE_URL) with conn.cursor() as cur: cur.execute( """ UPDATE foreign_qualification_registrations SET status = %s, last_error = %s, attempt_count = attempt_count + 1 WHERE order_number = %s AND target_state_code = %s """, (status, last_error, order_number, target_state), ) conn.commit() conn.close() except Exception as exc: logger.error( "ForeignQualHandler: failed to update status for %s/%s: %s", order_number, target_state, exc, ) def _create_admin_todo(self, order_number: str, description: str) -> None: try: from scripts.workers.erpnext_client import ERPNextClient ERPNextClient().create_resource( "ToDo", { "description": ( f"[{self.SERVICE_SLUG}] {order_number}\n\n{description}" ), "priority": "Medium", "role": "Accounting Advisor", }, ) except Exception as exc: logger.error("Could not create admin ToDo: %s", exc) def _json_or_null(v) -> Optional[str]: if v is None: return None import json return json.dumps(v) if isinstance(v, dict) else str(v)