"""Texas — SOSDirect SOS portal automation. SOSDirect (direct.sos.state.tx.us) is the Texas Secretary of State's online filing system. It requires an account login. Name search is available without login via the Comptroller's Taxable Entity Search. Key URLs: Name search: https://mycpa.cpa.state.tx.us/coa/Index.html SOSDirect: https://direct.sos.state.tx.us Filing: SOSDirect → Corporations → File a Certificate of Formation Fees: LLC: $300 (Certificate of Formation — Domestic LLC) Corp: $300 (Certificate of Formation — Domestic Corp) Expedited: +$25 (24-hour), +$50 (same-day) Notes: - Texas uses "Certificate of Formation" (not Articles of Organization) - No publication requirement - Franchise tax applies only if revenue > $2.47M (most small carriers exempt) - SOSDirect uses ASP.NET with __VIEWSTATE like WY; human-pace typing required """ from __future__ import annotations import asyncio import re from typing import Optional from scripts.formation.base import ( StatePortal, NameSearchResult, FormationOrder, FilingResult, FilingStatus, EntityType, ) from .config import CONFIG class TXPortal(StatePortal): STATE_CODE = "TX" STATE_NAME = "Texas" PORTAL_NAME = "SOSDirect" PORTAL_URL = "https://direct.sos.state.tx.us" NWRA_ADDRESS = CONFIG["registered_agent"]["street"] NWRA_CITY = CONFIG["registered_agent"]["city"] NWRA_STATE = CONFIG["registered_agent"]["state"] NWRA_ZIP = CONFIG["registered_agent"]["zip"] # ── Name Search (Comptroller Taxable Entity Search — no login) ────── async def search_name(self, name: str) -> NameSearchResult: """Search Texas business name availability via the Comptroller Taxable Entity Search (free, no login required). URL: https://mycpa.cpa.state.tx.us/coa/Index.html This searches the Comptroller's database, not the SOS. A name can be "available" in the Comptroller DB but reserved at SOS. For definitive availability, SOSDirect's name check is better — but requires login. We check Comptroller first (free + fast), then flag for SOSDirect confirmation if the customer proceeds. """ try: page = await self.start_browser() await page.goto( "https://mycpa.cpa.state.tx.us/coa/Index.html", wait_until="networkidle", ) await self.human_delay(1.0, 2.0) # The Comptroller search page has a text input + search button # Selector: input field for entity name await page.fill( 'input[name="entityName"], input#entityName, input[type="text"]', "", ) await self.type_slowly( page, 'input[name="entityName"], input#entityName, input[type="text"]', name, ) await self.human_delay(0.5, 1.0) # Click search await page.click( 'input[type="submit"], button[type="submit"], ' '#searchButton, input[value="Search"]' ) await page.wait_for_load_state("networkidle") await self.human_delay(1.0, 2.0) # Parse results content = await page.content() await self.screenshot(page, f"tx_name_search_{name}") # Check for "no results" indicator no_results = ( "no match" in content.lower() or "no entities found" in content.lower() or "no records" in content.lower() or "0 results" in content.lower() ) if no_results: return NameSearchResult( available=True, exact_match=False, similar_names=[], state_code="TX", searched_name=name, raw_response=content[:2000], ) # Extract matching entity names from results similar: list[str] = [] # Common patterns: table rows with entity names name_pattern = re.compile( r'