"""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()