"""Colorado — Socrata API for name search, Playwright for filing. Colorado publishes business entity data on data.colorado.gov via the Socrata Open Data API (SODA). Dataset ID: 4ykn-tg5h. This allows name availability searches WITHOUT a headless browser. Filing still requires Playwright against the SOS web portal. """ from __future__ import annotations import json import urllib.parse import urllib.request from scripts.formation.base import ( StatePortal, NameSearchResult, FormationOrder, FilingResult, FilingStatus, ) from .config import CONFIG SOCRATA_BASE = "https://data.colorado.gov/resource/4ykn-tg5h.json" class COPortal(StatePortal): STATE_CODE = "CO" STATE_NAME = "Colorado" PORTAL_NAME = CONFIG["portal_name"] PORTAL_URL = CONFIG["portal_url"] NWRA_ADDRESS = CONFIG["nwra_address"] NWRA_CITY = CONFIG["nwra_city"] NWRA_STATE = CONFIG["nwra_state"] NWRA_ZIP = CONFIG["nwra_zip"] async def search_name(self, name: str) -> NameSearchResult: """Search Colorado business name availability via Socrata SODA API. Uses the free REST API at data.colorado.gov — no browser, no login, no rate-limit issues for moderate usage. Returns JSON directly. """ try: # SoQL query: find entities whose name contains our search term upper_name = name.upper().replace("'", "''") query = f"$where=upper(entityname) like '%25{urllib.parse.quote(upper_name)}%25'" query += "&$limit=20&$order=entityformdate DESC" url = f"{SOCRATA_BASE}?{query}" self.log.info("CO Socrata API query: %s", url) req = urllib.request.Request( url, headers={"Accept": "application/json", "User-Agent": "PerformanceWest/1.0"}, ) with urllib.request.urlopen(req, timeout=15) as resp: data = json.loads(resp.read().decode("utf-8")) # Check for exact match (case-insensitive) exact_match = any( r.get("entityname", "").upper().strip() == upper_name.strip() for r in data ) # Collect similar names similar_names = [ r.get("entityname", "").strip() for r in data[:10] if r.get("entityname", "").strip() ] available = not exact_match self.log.info( "CO name search: '%s' — %s (exact_match=%s, similar=%d)", name, "AVAILABLE" if available else "TAKEN", exact_match, len(similar_names), ) return NameSearchResult( available=available, exact_match=exact_match, similar_names=similar_names, state_code=self.STATE_CODE, searched_name=name, raw_response=json.dumps(data[:5]), ) except urllib.error.URLError as e: self.log.error("CO Socrata API request failed: %s", e) return NameSearchResult( available=False, state_code=self.STATE_CODE, searched_name=name, raw_response=f"Socrata API error: {e}", ) except Exception as e: self.log.error("CO name search failed: %s", e) return NameSearchResult( available=False, state_code=self.STATE_CODE, searched_name=name, raw_response=f"Error: {e}", ) async def file_llc(self, order: FormationOrder) -> FilingResult: """File LLC Articles of Organization in Colorado. Colorado SOS portal: sos.state.co.us Filing fee: $50 Online filing is immediate (no processing delay). TODO: Implement Playwright filing flow. """ try: page = await self.start_browser() await page.goto(CONFIG["filing_url"]) await self.human_delay() await self.screenshot("co_llc_start") # Verify name first via Socrata API (no browser needed) name_result = await self.search_name(order.entity_name) if not name_result.available: return FilingResult( success=False, status=FilingStatus.NAME_UNAVAILABLE, state_code=self.STATE_CODE, entity_name=order.entity_name, error_message=f"Name '{order.entity_name}' is not available in Colorado. " f"Similar: {', '.join(name_result.similar_names[:5])}", ) # TODO: Complete filing flow once portal selectors are mapped return FilingResult( success=False, status=FilingStatus.ERROR, state_code=self.STATE_CODE, entity_name=order.entity_name, error_message="CO LLC filing: name search via API works, " "filing form selectors pending portal walkthrough", ) except Exception as e: self.log.error("CO LLC filing failed: %s", e) return FilingResult( success=False, status=FilingStatus.ERROR, state_code=self.STATE_CODE, entity_name=order.entity_name, error_message=str(e), ) finally: await self.close_browser() async def file_corporation(self, order: FormationOrder) -> FilingResult: """File Articles of Incorporation in Colorado ($50).""" try: page = await self.start_browser() await page.goto(CONFIG["filing_url"]) await self.human_delay() return FilingResult( success=False, status=FilingStatus.ERROR, state_code=self.STATE_CODE, entity_name=order.entity_name, error_message="CO Corp filing pending — LLC flow first", ) except Exception as e: return FilingResult( success=False, status=FilingStatus.ERROR, state_code=self.STATE_CODE, entity_name=order.entity_name, error_message=str(e), ) finally: await self.close_browser() def adapter() -> COPortal: return COPortal()