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>
205 lines
7.6 KiB
Python
205 lines
7.6 KiB
Python
"""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'<td[^>]*>([^<]*?' + re.escape(name[:10]) + r'[^<]*?)</td>',
|
|
re.IGNORECASE,
|
|
)
|
|
for m in name_pattern.finditer(content):
|
|
found = m.group(1).strip()
|
|
if found and len(found) > 3:
|
|
similar.append(found)
|
|
|
|
# Exact match = one of the results matches our name closely
|
|
exact = any(
|
|
s.upper().replace(",", "").replace(".", "").strip()
|
|
== name.upper().replace(",", "").replace(".", "").strip()
|
|
for s in similar
|
|
)
|
|
|
|
return NameSearchResult(
|
|
available=not exact,
|
|
exact_match=exact,
|
|
similar_names=similar[:10],
|
|
state_code="TX",
|
|
searched_name=name,
|
|
raw_response=content[:2000],
|
|
)
|
|
|
|
except Exception as exc:
|
|
return NameSearchResult(
|
|
available=False,
|
|
state_code="TX",
|
|
searched_name=name,
|
|
raw_response=f"Error: {exc}",
|
|
)
|
|
|
|
# ── LLC Filing (SOSDirect — requires login) ─────────────────────────
|
|
|
|
async def file_llc(self, order: FormationOrder) -> FilingResult:
|
|
"""File a Certificate of Formation for a Texas LLC via SOSDirect.
|
|
|
|
SOSDirect flow:
|
|
1. Login with SOS account credentials
|
|
2. Navigate: Corporations → File a Document → Certificate of Formation
|
|
3. Select entity type: Domestic Limited Liability Company (Form 205)
|
|
4. Fill form fields (name, RA, members, management type, purpose)
|
|
5. Pay $300 ($325 for 24hr expedited, $350 for same-day)
|
|
6. Capture confirmation + filing number
|
|
|
|
Selectors need verification against live portal. The form is a
|
|
multi-step ASP.NET WebForms wizard.
|
|
"""
|
|
# TODO: Verify selectors against live SOSDirect portal session
|
|
# For now, return a stub that creates an admin todo for manual filing
|
|
return FilingResult(
|
|
success=False,
|
|
status=FilingStatus.PENDING,
|
|
state_code="TX",
|
|
entity_name=order.entity_name,
|
|
error_message=(
|
|
"TX SOSDirect adapter selectors pending verification. "
|
|
"Admin: file manually at https://direct.sos.state.tx.us "
|
|
f"— LLC Certificate of Formation (Form 205), ${CONFIG['fees']['llc']}."
|
|
),
|
|
)
|
|
|
|
# ── Corporation Filing ───────────────────────────────────────────────
|
|
|
|
async def file_corporation(self, order: FormationOrder) -> FilingResult:
|
|
"""File a Certificate of Formation for a Texas corporation.
|
|
|
|
Same SOSDirect flow as LLC but selects:
|
|
Domestic For-Profit Corporation (Form 201) or
|
|
Domestic Nonprofit Corporation (Form 202).
|
|
"""
|
|
return FilingResult(
|
|
success=False,
|
|
status=FilingStatus.PENDING,
|
|
state_code="TX",
|
|
entity_name=order.entity_name,
|
|
error_message=(
|
|
"TX SOSDirect adapter selectors pending verification. "
|
|
"Admin: file manually at https://direct.sos.state.tx.us "
|
|
f"— Corp Certificate of Formation, ${CONFIG['fees']['corporation']}."
|
|
),
|
|
)
|
|
|
|
|
|
def adapter() -> TXPortal:
|
|
return TXPortal()
|