diff --git a/scripts/formation/states/nv/adapter.py b/scripts/formation/states/nv/adapter.py index db3fc9d..98917b4 100644 --- a/scripts/formation/states/nv/adapter.py +++ b/scripts/formation/states/nv/adapter.py @@ -6,7 +6,6 @@ LLC/Corp filing selectors pending live portal verification. from __future__ import annotations -import re from scripts.formation.base import ( StatePortal, @@ -29,63 +28,50 @@ class NVPortal(StatePortal): NWRA_ZIP = CONFIG["nwra_zip"] async def search_name(self, name: str) -> NameSearchResult: - """Search Nevada business name availability via the public portal.""" + """Nevada business name availability. + + Nevada's entity search (esos.nv.gov / SilverFlume) sits behind Imperva + Incapsula bot protection, which blocks automated/headless access (and + the residential proxy IPs are flagged too), and Nevada publishes no + public open-data API or bulk dataset we can query. So we cannot reliably + automate NV name availability the way we do TX (open-data API). + + Rather than scrape a blocked portal and risk a false "available"/"taken", + we attempt one lightweight portal hit and, if we get the Incapsula + challenge or anything that is not clearly a result page, we return + available=None ("could not determine") with a note so the order flow + flags it for a manual admin check on SilverFlume. available=None is NEVER + treated as "taken", so it never wrongly blocks an order. + """ try: page = await self.start_browser() - await page.goto(CONFIG["name_search_url"], wait_until="networkidle") - await self.human_delay(1.0, 2.5) - - search_sel = ( - CONFIG["selectors"].get("search_input") - or 'input[type="text"], input[name*="earch"], input[name*="ame"]' - ) - await page.fill(search_sel, "") - await self.type_slowly(page, search_sel, name) - await self.human_delay(0.5, 1.0) - - btn_sel = ( - CONFIG["selectors"].get("search_button") - or 'button[type="submit"], input[type="submit"]' - ) - await page.click(btn_sel) - await page.wait_for_load_state("networkidle") - await self.human_delay(1.0, 2.0) - + await page.goto(CONFIG["name_search_url"], wait_until="domcontentloaded", timeout=45000) + await self.human_delay(2.0, 4.0) content = await page.content() - await self.screenshot(page, f"${CODE}_name_search_{name}") + await self.screenshot(page, f"nv_name_search_{name[:20]}") - no_results = any( - phrase in content.lower() - for phrase in ["no match", "no results", "no records", "no entities", "0 results"] + blocked = ( + "_incapsula_resource" in content.lower() + or "incapsula" in content.lower() + or len(content) < 2000 # challenge stub, not a real page ) - - if no_results: + if blocked: return NameSearchResult( - available=True, exact_match=False, similar_names=[], - state_code="NV", searched_name=name, - raw_response=content[:2000], + available=None, state_code="NV", searched_name=name, + raw_response=( + "NV portal behind Incapsula bot protection - automated " + "name check unavailable. Verify manually on SilverFlume " + "(esos.nv.gov) before filing." + ), ) - similar: list[str] = [] - pattern = re.compile(r'