"""State corporate registry lines — verbatim rows from a state's own open-data registry where one exists; official SOS search link otherwise. Never inferred.""" import re from datetime import datetime, timezone from urllib.parse import quote, urlencode import httpx from fastapi import APIRouter, Query from deps import db from publicrecord import _cached from lawsuits import _slug router = APIRouter(prefix="/api/public-record/state-registry") UA = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/125 Safari/537.36 AlphaCovenantRecord/1.0"} NOTE = ("Rows are copied verbatim from the state's published business registry. Matched on legal name prefix — a name match is not proof it is the same company. " "Registry status describes the state filing, not operating authority or safety. Most states publish no open dataset; use the official search link.") # Open-data registries (Socrata). field map: name, status, type, jurisdiction, formed, id OPEN_DATA = { "CO": {"label": "Colorado Secretary of State — Business Entities", "host": "data.colorado.gov", "dataset": "4ykn-tg5h", "f": {"name": "entityname", "status": "entitystatus", "type": "entitytype", "jurisdiction": "jurisdictonofformation", "formed": "entityformdate", "id": "entityid", "city": "principalcity", "state": "principalstate"}, "coverage": "all entities on file (active and inactive)"}, "NY": {"label": "New York Department of State — Active Corporations", "host": "data.ny.gov", "dataset": "n9v6-gdp6", "f": {"name": "current_entity_name", "status": None, "type": "entity_type", "jurisdiction": "jurisdiction", "formed": "initial_dos_filing_date", "id": "dos_id", "city": "county", "state": None}, "coverage": "ACTIVE entities only — an entity that is not listed may be inactive, dissolved, or never registered in NY", "implied_status": "Listed in Active Corporations dataset"}, "CT": {"label": "Connecticut Secretary of the State — Business Registry", "host": "data.ct.gov", "dataset": "n7gp-d28j", "f": {"name": "name", "status": "status", "type": "business_type", "jurisdiction": "formation_place", "formed": "date_registration", "id": "accountnumber", "city": "billingcity", "state": "billingstate"}, "coverage": "all entities on file (active and inactive)"}, } # Official state business-entity search pages (manual lookup; no API). SOS_SEARCH = { "AL": "https://arc-sos.state.al.us/CGI/CORPNAME.MBR/INPUT", "AK": "https://www.commerce.alaska.gov/cbp/main/search/entities", "AZ": "https://arizonabusinesscenter.azcc.gov/EntitySearch/Index", "AR": "https://sos-corp-search.ark.org/corps", "CA": "https://bizfileonline.sos.ca.gov/search/business", "CO": "https://www.sos.state.co.us/biz/BusinessEntityCriteriaExt.do", "CT": "https://service.ct.gov/business/s/onlinebusinesssearch", "DE": "https://icis.corp.delaware.gov/ecorp/entitysearch/namesearch.aspx", "DC": "https://boss.dc.gov/", "FL": "https://search.sunbiz.org/Inquiry/CorporationSearch/ByName", "GA": "https://ecorp.sos.ga.gov/BusinessSearch", "HI": "https://hbe.ehawaii.gov/documents/search.html", "ID": "https://sosbiz.idaho.gov/search/business", "IL": "https://apps.ilsos.gov/businessentitysearch/", "IN": "https://bsd.sos.in.gov/publicbusinesssearch", "IA": "https://sos.iowa.gov/search/business/search.aspx", "KS": "https://www.sos.ks.gov/eforms/BusinessEntity/Search.aspx", "KY": "https://sosbes.sos.ky.gov/BusSearchNProfile/search.aspx", "LA": "https://coraweb.sos.la.gov/commercialsearch/commercialsearch.aspx", "ME": "https://apps1.web.maine.gov/nei-sos-icrs/ICRS?MainPage=x", "MD": "https://egov.maryland.gov/BusinessExpress/EntitySearch", "MA": "https://corp.sec.state.ma.us/corpweb/CorpSearch/CorpSearch.aspx", "MI": "https://cofs.lara.state.mi.us/CorpWeb/CorpSearch/CorpSearch.aspx", "MN": "https://mblsportal.sos.mn.gov/Business/Search", "MS": "https://corp.sos.ms.gov/corp/portal/c/page/corpBusinessIdSearch/portal.aspx", "MO": "https://bsd.sos.mo.gov/BusinessEntity/BESearch.aspx", "MT": "https://biz.sosmt.gov/search/business", "NE": "https://www.nebraska.gov/sos/corp/corpsearch.cgi", "NV": "https://esos.nv.gov/EntitySearch/OnlineEntitySearch", "NH": "https://quickstart.sos.nh.gov/online/BusinessInquire", "NJ": "https://www.njportal.com/DOR/BusinessNameSearch/", "NM": "https://enterprise.sos.nm.gov/search", "NY": "https://apps.dos.ny.gov/publicInquiry/", "NC": "https://www.sosnc.gov/online_services/search/by_title/_Business_Registration", "ND": "https://firststop.sos.nd.gov/search/business", "OH": "https://businesssearch.ohiosos.gov/", "OK": "https://www.sos.ok.gov/corp/corpInquiryFind.aspx", "OR": "https://sos.oregon.gov/business/pages/find.aspx", "PA": "https://file.dos.pa.gov/search/business", "RI": "https://business.sos.ri.gov/CorpWeb/CorpSearch/CorpSearch.aspx", "SC": "https://businessfilings.sc.gov/BusinessFiling/Entity/Search", "SD": "https://sosenterprise.sd.gov/BusinessServices/Business/FilingSearch.aspx", "TN": "https://tncab.tnsos.gov/business-entity-search", "TX": "https://comptroller.texas.gov/taxes/franchise/account-status/search", "UT": "https://businessregistration.utah.gov/", "VT": "https://bizfilings.vermont.gov/online/BusinessInquire", "VA": "https://cis.scc.virginia.gov/EntitySearch/Index", "WA": "https://ccfs.sos.wa.gov/#/BusinessSearch", "WV": "https://apps.sos.wv.gov/business/corporations/", "WI": "https://apps.dfi.wi.gov/apps/corpsearch/search.aspx", "WY": "https://wyobiz.wyo.gov/Business/FilingSearch.aspx", } _SUFFIX = re.compile(r"[\s,.]*\b(INCORPORATED|INC|CORPORATION|CORP|COMPANY|CO|LLC|L\.L\.C|LTD|LIMITED|LP|L\.P|LLP)\b[.\s]*$", re.I) def base_name(name: str) -> str: n = re.sub(r"\s+", " ", name.strip().upper()) for _ in range(2): n = _SUFFIX.sub("", n).strip(" ,.") return n.replace("'", "''") def _date(v): return (v or "")[:10] or None def query_url(st: str, name: str) -> str: cfg, f = OPEN_DATA[st], OPEN_DATA[st]["f"] params = {"$where": f"upper({f['name']}) like '{base_name(name)}%'", "$limit": 8, "$order": f["name"]} return f"https://{cfg['host']}/resource/{cfg['dataset']}.json?{urlencode(params, quote_via=quote)}" async def query_state(st: str, name: str) -> dict: cfg, f = OPEN_DATA[st], OPEN_DATA[st]["f"] base = base_name(name) url = query_url(st, name) fetched_at = datetime.now(timezone.utc).isoformat() out = {"state": st, "label": cfg["label"], "coverage": cfg["coverage"], "dataset_url": f"https://{cfg['host']}/d/{cfg['dataset']}", "source_url": url, "manual_url": SOS_SEARCH[st], "fetched_at": fetched_at, "matched_prefix": base} try: async with httpx.AsyncClient(timeout=25, headers=UA) as client: r = await client.get(url) if r.status_code != 200: return {**out, "available": False, "reason": f"{cfg['host']} returned HTTP {r.status_code}"} rows = r.json() except Exception as e: return {**out, "available": False, "reason": f"{cfg['host']} unreachable ({type(e).__name__})"} recs = [{"entity_name": row.get(f["name"]), "status": row.get(f["status"]) if f["status"] else cfg.get("implied_status"), "entity_type": row.get(f["type"]), "jurisdiction": row.get(f["jurisdiction"]), "formed": _date(row.get(f["formed"])), "entity_id": row.get(f["id"]), "principal_location": ", ".join(x for x in [row.get(f["city"]) if f["city"] else None, row.get(f["state"]) if f["state"] else None] if x) or None} for row in rows] await db.registry_snapshots.insert_one({"state": st, "query": name, "prefix": base, "fetched_at": fetched_at, "source_url": url, "raw": rows}) return {**out, "available": True, "count": len(recs), "records": recs} async def lookup(name: str, home_state: str | None) -> dict: states = list(OPEN_DATA) results = [await query_state(st, name) for st in states] home = (home_state or "").upper()[:2] return {"name": name, "home_state": home or None, "home_state_open_data": home in OPEN_DATA, "home_manual_url": SOS_SEARCH.get(home), "registries": results, "open_data_states": states, "note": NOTE, "fetched_at": datetime.now(timezone.utc).isoformat()} @router.get("") async def state_registry(q: str = Query(..., min_length=2, max_length=120), state: str = Query("", max_length=2)): name = q.strip() return await _cached(f"stateregistry:{_slug(name)}:{state.upper()}", 24, lambda: lookup(name, state)) async def summary_line(name: str) -> str: """One comparable line for monitoring: 'CO · ENTITY · Status | NY · …'. 'check not active' if any registry was unreachable (skips the diff that day).""" d = await lookup(name, None) if any(not r.get("available") for r in d["registries"]): return "check not active" parts = [f"{r['state']} · {x['entity_name']} · {x['status'] or '—'}" for r in d["registries"] for x in r.get("records", [])] return " | ".join(parts) if parts else "no name-prefix match (CO, NY, CT)"