fix(formation/NV): name search returns unknown (admin-verify), not a fake result
Nevada's entity search (esos.nv.gov/SilverFlume) is behind Imperva Incapsula bot
protection that blocks headless + the residential proxy IPs, and NV has no public
open-data API/bulk dataset. The old adapter scraped the blocked challenge page and
returned available=False for everything (would tell customers a free name is taken)
and also had a NameError bug (f"${CODE}..."). Now NV detects the Incapsula
challenge and returns available=None ('could not determine') with a note to verify
manually on SilverFlume -- never a false 'taken', so it never wrongly blocks an
order. TX remains fully automated via the open-data API.
This commit is contained in:
parent
f94ad1682b
commit
20c11e6180
1 changed files with 32 additions and 46 deletions
|
|
@ -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'<td[^>]*>([^<]*?' + re.escape(name[:8]) + r'[^<]*?)</td>', re.IGNORECASE)
|
||||
for m in pattern.finditer(content):
|
||||
found = m.group(1).strip()
|
||||
if found and 3 < len(found) < 200:
|
||||
similar.append(found)
|
||||
|
||||
exact = any(
|
||||
s.upper().replace(",", "").strip() == name.upper().replace(",", "").strip()
|
||||
for s in similar
|
||||
)
|
||||
|
||||
return NameSearchResult(
|
||||
available=not exact, exact_match=exact,
|
||||
similar_names=similar[:10], state_code="NV",
|
||||
searched_name=name, raw_response=content[:2000],
|
||||
available=None, state_code="NV", searched_name=name,
|
||||
raw_response="NV name check inconclusive - verify manually on SilverFlume.",
|
||||
)
|
||||
except Exception as exc:
|
||||
return NameSearchResult(
|
||||
available=False, state_code="NV", searched_name=name,
|
||||
available=None, state_code="NV", searched_name=name,
|
||||
raw_response=f"Error: {exc}",
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue