new-site/scripts/formation/states/wy/adapter.py
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

237 lines
8.9 KiB
Python

"""Wyoming — WyoBiz SOS portal automation.
WyoBiz (wyobiz.wyo.gov) uses ASP.NET WebForms with __VIEWSTATE.
Name search confirmed working with verified CSS selectors.
LLC/Corp filing requires an active session — selectors TBD during portal walkthrough.
"""
from __future__ import annotations
import asyncio
import re
from scripts.formation.base import (
StatePortal, NameSearchResult, FormationOrder, FilingResult,
FilingStatus, EntityType,
)
from .config import CONFIG
class WYPortal(StatePortal):
STATE_CODE = "WY"
STATE_NAME = "Wyoming"
PORTAL_NAME = "WyoBiz"
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 Wyoming business name availability via WyoBiz.
Uses verified selectors from the FilingSearch.aspx page.
WyoBiz is ASP.NET WebForms — uses postback for search.
"""
try:
page = await self.start_browser()
await page.goto(CONFIG["name_search_url"], wait_until="networkidle")
await self.human_delay(1.5, 3.0)
sel = CONFIG["selectors"]
# Check for CAPTCHA first
if await self.detect_captcha():
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response="CAPTCHA detected — manual intervention required",
)
# Click "Contains" radio for broader search
await page.click(sel["name_search_contains"])
await self.human_delay(0.3, 0.6)
# Type the name slowly
await self.type_slowly(sel["name_search_input"], name)
await self.human_delay(0.5, 1.0)
# Click search — this triggers ASP.NET postback
await page.click(sel["name_search_submit"])
# Wait for postback to complete (loading indicator disappears)
try:
await page.wait_for_selector(
sel["loading_indicator"],
state="visible",
timeout=3000,
)
except Exception:
pass # Loading indicator may flash too fast
await page.wait_for_load_state("networkidle", timeout=15000)
await self.human_delay(1.0, 2.0)
# Screenshot for audit trail
screenshot = await self.screenshot("name_search_results")
# Get the results area content
results_el = await page.query_selector(sel["name_results_area"])
results_html = await results_el.inner_html() if results_el else ""
results_text = await results_el.inner_text() if results_el else ""
# Parse results
# WyoBiz shows results in a table. If no results, it shows a message.
similar_names: list[str] = []
exact_match = False
if "No records found" in results_text or not results_text.strip():
# No matching names — name is likely available
return NameSearchResult(
available=True,
exact_match=False,
similar_names=[],
state_code=self.STATE_CODE,
searched_name=name,
raw_response=results_text[:2000],
)
# Extract business names from results
# WyoBiz renders results as links in the results area
name_links = await page.query_selector_all(f"{sel['name_results_area']} a")
for link in name_links[:20]:
link_text = (await link.inner_text()).strip()
if link_text:
similar_names.append(link_text)
if link_text.upper() == name.upper():
exact_match = True
# If there are exact matches, name is taken
# If only similar names, name might still be available (WY checks distinguishability)
available = not exact_match
return NameSearchResult(
available=available,
exact_match=exact_match,
similar_names=similar_names[:10],
state_code=self.STATE_CODE,
searched_name=name,
raw_response=results_text[:2000],
)
except Exception as e:
self.log.error("WY name search failed: %s", e)
screenshot = await self.screenshot("name_search_error")
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=f"Error: {e}",
)
finally:
await self.close_browser()
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""File LLC Articles of Organization in Wyoming.
Wyoming LLC formation requires:
- Entity name (must be distinguishable from existing names)
- Registered agent name and address (NW RA)
- Organizer name and address (person filing)
- Principal office address
- Mailing address (optional)
- Filing fee: $100
TODO: Implement actual filing flow. Requires:
1. Navigate to https://wyobiz.wyo.gov/Business/Default.aspx
2. Click "File a New Business Entity"
3. Select "Limited Liability Company"
4. Fill in formation form fields
5. Submit payment ($100)
6. Capture confirmation number and filing ID
"""
try:
page = await self.start_browser()
await page.goto(CONFIG["filing_url"])
await self.human_delay()
await self.screenshot("wy_llc_start")
# Verify name is available first
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 Wyoming. "
f"Similar names: {', '.join(name_result.similar_names[:5])}",
)
# TODO: Complete the filing flow once portal selectors are mapped
# The WyoBiz filing wizard has multiple steps:
# Step 1: Select entity type (LLC)
# Step 2: Enter entity name
# Step 3: Enter registered agent info
# Step 4: Enter organizer info
# Step 5: Enter principal office address
# Step 6: Payment ($100)
# Step 7: Confirmation
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code=self.STATE_CODE,
entity_name=order.entity_name,
error_message="WY LLC filing automation: name search works, "
"filing form selectors pending portal walkthrough",
screenshot_path=await self.screenshot("wy_llc_pending"),
)
except Exception as e:
self.log.error("WY 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 Wyoming.
Similar flow to LLC but with additional fields:
- Authorized shares (number and par value)
- Directors (names and addresses)
- Incorporator (name and address)
TODO: Implement once LLC flow is working.
"""
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="WY Corp filing automation 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() -> WYPortal:
return WYPortal()