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>
This commit is contained in:
justin 2026-04-27 06:54:22 -05:00
commit f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions

View file

@ -0,0 +1,96 @@
"""
State adapter registry.
Maps 2-letter state codes to their adapter modules.
Each state directory contains:
- config.py Portal URLs, NW RA address, selectors, fees
- adapter.py StatePortal subclass with Playwright automation
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from scripts.formation.base import StatePortal
# State metadata for the registry
STATES = {
"AL": {"name": "Alabama", "search_method": "playwright"},
"AK": {"name": "Alaska", "search_method": "socrata"},
"AZ": {"name": "Arizona", "search_method": "playwright"},
"AR": {"name": "Arkansas", "search_method": "playwright"},
"CA": {"name": "California", "search_method": "playwright"},
"CO": {"name": "Colorado", "search_method": "socrata_api"},
"CT": {"name": "Connecticut", "search_method": "socrata"},
"DE": {"name": "Delaware", "search_method": "playwright"},
"FL": {"name": "Florida", "search_method": "sftp_bulk"},
"GA": {"name": "Georgia", "search_method": "playwright"},
"HI": {"name": "Hawaii", "search_method": "playwright"},
"ID": {"name": "Idaho", "search_method": "playwright"},
"IL": {"name": "Illinois", "search_method": "socrata"},
"IN": {"name": "Indiana", "search_method": "playwright"},
"IA": {"name": "Iowa", "search_method": "socrata"},
"KS": {"name": "Kansas", "search_method": "playwright"},
"KY": {"name": "Kentucky", "search_method": "playwright"},
"LA": {"name": "Louisiana", "search_method": "playwright"},
"ME": {"name": "Maine", "search_method": "playwright"},
"MD": {"name": "Maryland", "search_method": "playwright"},
"MA": {"name": "Massachusetts", "search_method": "playwright"},
"MI": {"name": "Michigan", "search_method": "socrata"},
"MN": {"name": "Minnesota", "search_method": "playwright"},
"MS": {"name": "Mississippi", "search_method": "playwright"},
"MO": {"name": "Missouri", "search_method": "playwright"},
"MT": {"name": "Montana", "search_method": "playwright"},
"NE": {"name": "Nebraska", "search_method": "playwright"},
"NV": {"name": "Nevada", "search_method": "playwright"},
"NH": {"name": "New Hampshire", "search_method": "playwright"},
"NJ": {"name": "New Jersey", "search_method": "playwright"},
"NM": {"name": "New Mexico", "search_method": "playwright"},
"NY": {"name": "New York", "search_method": "socrata"},
"NC": {"name": "North Carolina", "search_method": "playwright"},
"ND": {"name": "North Dakota", "search_method": "playwright"},
"OH": {"name": "Ohio", "search_method": "playwright"},
"OK": {"name": "Oklahoma", "search_method": "playwright"},
"OR": {"name": "Oregon", "search_method": "socrata"},
"PA": {"name": "Pennsylvania", "search_method": "socrata"},
"RI": {"name": "Rhode Island", "search_method": "playwright"},
"SC": {"name": "South Carolina", "search_method": "playwright"},
"SD": {"name": "South Dakota", "search_method": "playwright"},
"TN": {"name": "Tennessee", "search_method": "playwright"},
"TX": {"name": "Texas", "search_method": "playwright"},
"UT": {"name": "Utah", "search_method": "playwright"},
"VT": {"name": "Vermont", "search_method": "socrata"},
"VA": {"name": "Virginia", "search_method": "playwright"},
"WA": {"name": "Washington", "search_method": "socrata"},
"WV": {"name": "West Virginia", "search_method": "playwright"},
"WI": {"name": "Wisconsin", "search_method": "playwright"},
"WY": {"name": "Wyoming", "search_method": "playwright"},
"DC": {"name": "District of Columbia", "search_method": "playwright"},
# Canadian provinces
"BC": {"name": "British Columbia", "search_method": "playwright"},
"ON": {"name": "Ontario", "search_method": "playwright"},
}
def get_adapter(state_code: str) -> "StatePortal":
"""Dynamically import and return the adapter for a state."""
code = state_code.upper()
if code not in STATES:
raise ValueError(f"Unknown state code: {code}")
module_name = f".{code.lower()}.adapter"
import importlib
mod = importlib.import_module(module_name, package=__name__)
return mod.adapter()
def get_config(state_code: str) -> dict:
"""Return the config dict for a state."""
code = state_code.upper()
if code not in STATES:
raise ValueError(f"Unknown state code: {code}")
module_name = f".{code.lower()}.config"
import importlib
mod = importlib.import_module(module_name, package=__name__)
return mod.CONFIG

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,118 @@
"""Alaska — CBPL portal automation."""
from __future__ import annotations
from scripts.formation.base import StatePortal, NameSearchResult, FormationOrder, FilingResult, FilingStatus
from .config import CONFIG
class AKPortal(StatePortal):
STATE_CODE = "AK"
STATE_NAME = "Alaska"
PORTAL_NAME = CONFIG["portal_name"]
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 Alaska business name availability."""
try:
page = await self.start_browser()
await page.goto(CONFIG["name_search_url"])
await self.human_delay()
# Type name into search field
sel = CONFIG["selectors"]
if sel["name_search_input"]:
await self.type_slowly(sel["name_search_input"], name)
await self.safe_click(sel["name_search_submit"])
await page.wait_for_load_state("networkidle")
content = await page.content()
available = CONFIG["selectors"]["name_unavailable_indicator"] not in content
return NameSearchResult(
available=available,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=content[:2000],
)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response="Selectors not yet configured for this state",
)
except Exception as e:
self.log.error("Name search failed: %s", e)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=str(e),
)
finally:
await self.close_browser()
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""File LLC in Alaska."""
try:
page = await self.start_browser()
await page.goto(CONFIG["filing_url"])
await self.human_delay()
await self.screenshot("llc_start")
# TODO: Implement Alaska-specific LLC filing flow
# Each state's portal has different form fields, steps, and workflows.
# The selectors in config.py need to be populated by inspecting the portal.
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code=self.STATE_CODE,
entity_name=order.entity_name,
error_message="LLC filing automation not yet implemented for Alaska",
screenshot_path=await self.screenshot("llc_not_implemented"),
)
except Exception as e:
self.log.error("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 Corporation in Alaska."""
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="Corporation filing automation not yet implemented for Alaska",
)
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() -> AKPortal:
return AKPortal()

View file

@ -0,0 +1,49 @@
"""Alaska — Corporations, Business and Professional Licensing portal configuration."""
CONFIG = {
"state_code": "AK",
"state_name": "Alaska",
"sos_name": "Alaska Division of Corporations, Business and Professional Licensing",
"portal_name": "Alaska CBPL Entity Search",
"portal_url": "https://commerce.alaska.gov",
"name_search_url": "https://commerce.alaska.gov/cbp/main/search/entities",
"filing_url": "https://commerce.alaska.gov/cbp/main/search/entities",
"search_method": "playwright",
# Socrata API (not applicable)
"socrata_domain": "",
"socrata_dataset_id": "",
# NW Registered Agent address in this state
"nwra_name": "Northwest Registered Agent LLC",
"nwra_address": "3000 A St Ste 200",
"nwra_city": "Anchorage",
"nwra_state": "AK",
"nwra_zip": "99503",
# State fees (cents)
"llc_formation_fee": 25000,
"corp_formation_fee": 25000,
"expedited_fee": None,
"expedited_label": "",
# Selectors (Playwright CSS selectors for portal automation)
"selectors": {
"name_search_input": "",
"name_search_submit": "",
"name_results_table": "",
"name_available_indicator": "",
"name_unavailable_indicator": "",
# LLC filing form selectors
"llc_name_field": "",
"llc_agent_name_field": "",
"llc_agent_address_field": "",
"llc_principal_address_field": "",
"llc_organizer_name_field": "",
"llc_management_type_select": "",
"llc_purpose_field": "",
"llc_submit_button": "",
# Corp filing form selectors
"corp_name_field": "",
"corp_agent_name_field": "",
"corp_shares_field": "",
"corp_submit_button": "",
},
"notes": "",
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,118 @@
"""Alabama — SOS portal automation."""
from __future__ import annotations
from scripts.formation.base import StatePortal, NameSearchResult, FormationOrder, FilingResult, FilingStatus
from .config import CONFIG
class ALPortal(StatePortal):
STATE_CODE = "AL"
STATE_NAME = "Alabama"
PORTAL_NAME = CONFIG["portal_name"]
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 Alabama business name availability."""
try:
page = await self.start_browser()
await page.goto(CONFIG["name_search_url"])
await self.human_delay()
# Type name into search field
sel = CONFIG["selectors"]
if sel["name_search_input"]:
await self.type_slowly(sel["name_search_input"], name)
await self.safe_click(sel["name_search_submit"])
await page.wait_for_load_state("networkidle")
content = await page.content()
available = CONFIG["selectors"]["name_unavailable_indicator"] not in content
return NameSearchResult(
available=available,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=content[:2000],
)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response="Selectors not yet configured for this state",
)
except Exception as e:
self.log.error("Name search failed: %s", e)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=str(e),
)
finally:
await self.close_browser()
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""File LLC in Alabama."""
try:
page = await self.start_browser()
await page.goto(CONFIG["filing_url"])
await self.human_delay()
await self.screenshot("llc_start")
# TODO: Implement Alabama-specific LLC filing flow
# Each state's portal has different form fields, steps, and workflows.
# The selectors in config.py need to be populated by inspecting the portal.
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code=self.STATE_CODE,
entity_name=order.entity_name,
error_message="LLC filing automation not yet implemented for Alabama",
screenshot_path=await self.screenshot("llc_not_implemented"),
)
except Exception as e:
self.log.error("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 Corporation in Alabama."""
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="Corporation filing automation not yet implemented for Alabama",
)
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() -> ALPortal:
return ALPortal()

View file

@ -0,0 +1,49 @@
"""Alabama — Secretary of State portal configuration."""
CONFIG = {
"state_code": "AL",
"state_name": "Alabama",
"sos_name": "Alabama Secretary of State",
"portal_name": "Alabama Business Entity Records",
"portal_url": "https://sos.alabama.gov",
"name_search_url": "https://sos.alabama.gov/government-records/business-entity-records",
"filing_url": "https://sos.alabama.gov/government-records/business-entity-records",
"search_method": "playwright",
# Socrata API (not applicable)
"socrata_domain": "",
"socrata_dataset_id": "",
# NW Registered Agent address in this state
"nwra_name": "Northwest Registered Agent LLC",
"nwra_address": "100 Centerview Dr Ste 115",
"nwra_city": "Birmingham",
"nwra_state": "AL",
"nwra_zip": "35216",
# State fees (cents)
"llc_formation_fee": 20000,
"corp_formation_fee": 20000,
"expedited_fee": None,
"expedited_label": "",
# Selectors (Playwright CSS selectors for portal automation)
"selectors": {
"name_search_input": "",
"name_search_submit": "",
"name_results_table": "",
"name_available_indicator": "",
"name_unavailable_indicator": "",
# LLC filing form selectors
"llc_name_field": "",
"llc_agent_name_field": "",
"llc_agent_address_field": "",
"llc_principal_address_field": "",
"llc_organizer_name_field": "",
"llc_management_type_select": "",
"llc_purpose_field": "",
"llc_submit_button": "",
# Corp filing form selectors
"corp_name_field": "",
"corp_agent_name_field": "",
"corp_shares_field": "",
"corp_submit_button": "",
},
"notes": "",
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,118 @@
"""Arkansas — SOS portal automation."""
from __future__ import annotations
from scripts.formation.base import StatePortal, NameSearchResult, FormationOrder, FilingResult, FilingStatus
from .config import CONFIG
class ARPortal(StatePortal):
STATE_CODE = "AR"
STATE_NAME = "Arkansas"
PORTAL_NAME = CONFIG["portal_name"]
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 Arkansas business name availability."""
try:
page = await self.start_browser()
await page.goto(CONFIG["name_search_url"])
await self.human_delay()
# Type name into search field
sel = CONFIG["selectors"]
if sel["name_search_input"]:
await self.type_slowly(sel["name_search_input"], name)
await self.safe_click(sel["name_search_submit"])
await page.wait_for_load_state("networkidle")
content = await page.content()
available = CONFIG["selectors"]["name_unavailable_indicator"] not in content
return NameSearchResult(
available=available,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=content[:2000],
)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response="Selectors not yet configured for this state",
)
except Exception as e:
self.log.error("Name search failed: %s", e)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=str(e),
)
finally:
await self.close_browser()
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""File LLC in Arkansas."""
try:
page = await self.start_browser()
await page.goto(CONFIG["filing_url"])
await self.human_delay()
await self.screenshot("llc_start")
# TODO: Implement Arkansas-specific LLC filing flow
# Each state's portal has different form fields, steps, and workflows.
# The selectors in config.py need to be populated by inspecting the portal.
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code=self.STATE_CODE,
entity_name=order.entity_name,
error_message="LLC filing automation not yet implemented for Arkansas",
screenshot_path=await self.screenshot("llc_not_implemented"),
)
except Exception as e:
self.log.error("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 Corporation in Arkansas."""
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="Corporation filing automation not yet implemented for Arkansas",
)
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() -> ARPortal:
return ARPortal()

View file

@ -0,0 +1,49 @@
"""Arkansas — Secretary of State portal configuration."""
CONFIG = {
"state_code": "AR",
"state_name": "Arkansas",
"sos_name": "Arkansas Secretary of State",
"portal_name": "Arkansas Business Entity Search",
"portal_url": "https://sos.arkansas.gov",
"name_search_url": "https://biz.sos.arkansas.gov/search",
"filing_url": "https://biz.sos.arkansas.gov/search",
"search_method": "playwright",
# Socrata API (not applicable)
"socrata_domain": "",
"socrata_dataset_id": "",
# NW Registered Agent address in this state
"nwra_name": "Northwest Registered Agent LLC",
"nwra_address": "1321 Scott St",
"nwra_city": "Little Rock",
"nwra_state": "AR",
"nwra_zip": "72202",
# State fees (cents)
"llc_formation_fee": 4500,
"corp_formation_fee": 4500,
"expedited_fee": None,
"expedited_label": "",
# Selectors (Playwright CSS selectors for portal automation)
"selectors": {
"name_search_input": "",
"name_search_submit": "",
"name_results_table": "",
"name_available_indicator": "",
"name_unavailable_indicator": "",
# LLC filing form selectors
"llc_name_field": "",
"llc_agent_name_field": "",
"llc_agent_address_field": "",
"llc_principal_address_field": "",
"llc_organizer_name_field": "",
"llc_management_type_select": "",
"llc_purpose_field": "",
"llc_submit_button": "",
# Corp filing form selectors
"corp_name_field": "",
"corp_agent_name_field": "",
"corp_shares_field": "",
"corp_submit_button": "",
},
"notes": "",
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,119 @@
"""Arizona — ACC portal automation."""
from __future__ import annotations
from scripts.formation.base import StatePortal, NameSearchResult, FormationOrder, FilingResult, FilingStatus
from .config import CONFIG
class AZPortal(StatePortal):
STATE_CODE = "AZ"
STATE_NAME = "Arizona"
PORTAL_NAME = CONFIG["portal_name"]
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 Arizona business name availability."""
try:
page = await self.start_browser()
await page.goto(CONFIG["name_search_url"])
await self.human_delay()
# Type name into search field
sel = CONFIG["selectors"]
if sel["name_search_input"]:
await self.type_slowly(sel["name_search_input"], name)
await self.safe_click(sel["name_search_submit"])
await page.wait_for_load_state("networkidle")
content = await page.content()
available = CONFIG["selectors"]["name_unavailable_indicator"] not in content
return NameSearchResult(
available=available,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=content[:2000],
)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response="Selectors not yet configured for this state",
)
except Exception as e:
self.log.error("Name search failed: %s", e)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=str(e),
)
finally:
await self.close_browser()
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""File LLC in Arizona."""
try:
page = await self.start_browser()
await page.goto(CONFIG["filing_url"])
await self.human_delay()
await self.screenshot("llc_start")
# TODO: Implement Arizona-specific LLC filing flow
# Each state's portal has different form fields, steps, and workflows.
# The selectors in config.py need to be populated by inspecting the portal.
# NOTE: Arizona requires publication after formation.
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code=self.STATE_CODE,
entity_name=order.entity_name,
error_message="LLC filing automation not yet implemented for Arizona",
screenshot_path=await self.screenshot("llc_not_implemented"),
)
except Exception as e:
self.log.error("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 Corporation in Arizona."""
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="Corporation filing automation not yet implemented for Arizona",
)
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() -> AZPortal:
return AZPortal()

View file

@ -0,0 +1,49 @@
"""Arizona — Arizona Corporation Commission portal configuration."""
CONFIG = {
"state_code": "AZ",
"state_name": "Arizona",
"sos_name": "Arizona Corporation Commission",
"portal_name": "ACC eCorp Entity Search",
"portal_url": "https://azcc.gov",
"name_search_url": "https://ecorp.azcc.gov/EntitySearch/Index",
"filing_url": "https://ecorp.azcc.gov/EntitySearch/Index",
"search_method": "playwright",
# Socrata API (not applicable)
"socrata_domain": "",
"socrata_dataset_id": "",
# NW Registered Agent address in this state
"nwra_name": "Northwest Registered Agent LLC",
"nwra_address": "8700 E Vista Bonita Dr Ste 268",
"nwra_city": "Scottsdale",
"nwra_state": "AZ",
"nwra_zip": "85255",
# State fees (cents)
"llc_formation_fee": 5000,
"corp_formation_fee": 6000,
"expedited_fee": None,
"expedited_label": "",
# Selectors (Playwright CSS selectors for portal automation)
"selectors": {
"name_search_input": "",
"name_search_submit": "",
"name_results_table": "",
"name_available_indicator": "",
"name_unavailable_indicator": "",
# LLC filing form selectors
"llc_name_field": "",
"llc_agent_name_field": "",
"llc_agent_address_field": "",
"llc_principal_address_field": "",
"llc_organizer_name_field": "",
"llc_management_type_select": "",
"llc_purpose_field": "",
"llc_submit_button": "",
# Corp filing form selectors
"corp_name_field": "",
"corp_agent_name_field": "",
"corp_shares_field": "",
"corp_submit_button": "",
},
"notes": "Publication required. After formation, Articles of Organization must be published in an approved newspaper within 60 days.",
}

View file

@ -0,0 +1,4 @@
from .config import CONFIG
from .adapter import BCPortal
__all__ = ["CONFIG", "BCPortal"]

View file

@ -0,0 +1,977 @@
"""
British Columbia Corporate Online / BC Registry adapter.
Automates:
1. Anytime Mailbox setup (BC registered office) via anytimemailbox.com
2. Name search & reservation via bcregistrynames.gov.bc.ca
3. Incorporation filing via corporateonline.gov.bc.ca
4. .ca domain + email + web presence provisioning (HestiaCP)
5. Canadian phone number provisioning
6. Corporate binder compilation (DOCX PDF)
7. Business banking link delivery
8. CRTC registration letter generation (Voice, Data & Wireless Reseller)
9. CCTS registration
All Playwright methods are structural stubs CSS selectors in config.py
must be populated after manual portal inspection before going live.
"""
from __future__ import annotations
import logging
import os
import re
import secrets
import asyncio
import imaplib
import email
from email.header import decode_header, make_header
from datetime import datetime
from pathlib import Path
from typing import Optional
from scripts.formation.base import (
FilingResult,
FilingStatus,
FormationOrder,
NameSearchResult,
StatePortal,
)
from .config import CONFIG
LOG = logging.getLogger("formation.bc")
# Steps with open selector verification gaps from live Corporate Online flow.
COLIN_UNVERIFIED_STEP_SELECTORS = {
6: ["inc_director_name", "inc_director_address"],
7: ["inc_share_structure"],
8: ["inc_articles"],
9: ["pay_card_number", "pay_card_exp", "pay_card_cvv", "pay_card_name", "pay_submit"],
12: ["inc_submit"],
}
# DOCX template for CRTC letter (in templates/ directory)
CRTC_TEMPLATE = os.getenv(
"CRTC_LETTER_TEMPLATE",
str(Path(__file__).resolve().parent.parent.parent.parent / "templates" / "crtc_notification_letter.docx"),
)
class BCPortal(StatePortal):
"""Adapter for BC Registry Services (Corporate Online) and Anytime Mailbox."""
STATE_CODE = "BC"
STATE_NAME = "British Columbia"
PORTAL_NAME = "Corporate Online"
PORTAL_URL = CONFIG["filing_portal"]["url"]
SUPPORTS_LLC = False # Canada has no LLC entity type
SUPPORTS_CORP = True
SUPPORTS_ONLINE_FILING = True
SUPPORTS_NAME_SEARCH = True
# No NW Registered Agent in Canada — we use Anytime Mailbox instead
NWRA_ADDRESS = ""
NWRA_CITY = ""
NWRA_STATE = ""
NWRA_ZIP = ""
CONFIG = CONFIG
def _missing_selectors(self, keys: list[str]) -> list[str]:
selectors = CONFIG["selectors"]
return [k for k in keys if not str(selectors.get(k, "")).strip()]
def _missing_colin_step_map(self) -> dict[int, list[str]]:
missing: dict[int, list[str]] = {}
for step, keys in COLIN_UNVERIFIED_STEP_SELECTORS.items():
gaps = self._missing_selectors(keys)
if gaps:
missing[step] = gaps
return missing
def _decode_header_value(self, raw: str) -> str:
if not raw:
return ""
try:
return str(make_header(decode_header(raw)))
except Exception:
return raw
def _extract_otp_candidate(self, text: str) -> str:
if not text:
return ""
match = re.search(r"(?:verification|security|one[-\s]?time|otp|code)[^\d]{0,30}(\d{6})", text, re.IGNORECASE)
if match:
return match.group(1)
fallback = re.search(r"\b(\d{6})\b", text)
return fallback.group(1) if fallback else ""
def _fetch_anytime_otp_sync(self, expected_recipient: str) -> str:
imap_host = os.getenv("ANYTIME_MAILBOX_IMAP_HOST", os.getenv("RELAY_IMAP_HOST", "mail.performancewest.net"))
imap_port = int(os.getenv("ANYTIME_MAILBOX_IMAP_PORT", os.getenv("RELAY_IMAP_PORT", "993")))
imap_ssl = os.getenv("ANYTIME_MAILBOX_IMAP_SSL", "true").lower() == "true"
imap_user = os.getenv("ANYTIME_MAILBOX_IMAP_USER", "").strip()
imap_pass = os.getenv("ANYTIME_MAILBOX_IMAP_PASS", "").strip()
imap_folder = os.getenv("ANYTIME_MAILBOX_IMAP_FOLDER", "INBOX")
sender_hint = os.getenv("ANYTIME_MAILBOX_OTP_SENDER_HINT", "anytimemailbox")
if not imap_user or not imap_pass:
self.log.warning("Anytime OTP auto-fetch disabled: IMAP credentials missing")
return ""
client: imaplib.IMAP4 | imaplib.IMAP4_SSL
client = imaplib.IMAP4_SSL(imap_host, imap_port) if imap_ssl else imaplib.IMAP4(imap_host, imap_port)
try:
client.login(imap_user, imap_pass)
client.select(imap_folder)
status, data = client.search(None, "ALL")
if status != "OK" or not data or not data[0]:
return ""
msg_ids = data[0].split()[-40:]
for msg_id in reversed(msg_ids):
fetch_status, parts = client.fetch(msg_id, "(RFC822)")
if fetch_status != "OK" or not parts:
continue
raw = parts[0][1] if isinstance(parts[0], tuple) and len(parts[0]) > 1 else b""
if not raw:
continue
msg = email.message_from_bytes(raw)
subj = self._decode_header_value(msg.get("Subject", ""))
from_addr = self._decode_header_value(msg.get("From", ""))
to_addr = self._decode_header_value(msg.get("To", ""))
envelope = f"{subj}\n{from_addr}\n{to_addr}"
if sender_hint.lower() not in envelope.lower() and "verification" not in envelope.lower():
continue
if expected_recipient and expected_recipient.lower() not in to_addr.lower() and expected_recipient.lower() not in envelope.lower():
continue
body_text = ""
if msg.is_multipart():
for part in msg.walk():
ctype = part.get_content_type()
if ctype in ("text/plain", "text/html"):
payload = part.get_payload(decode=True) or b""
try:
body_text += payload.decode(part.get_content_charset() or "utf-8", errors="ignore") + "\n"
except Exception:
continue
else:
payload = msg.get_payload(decode=True) or b""
body_text = payload.decode(msg.get_content_charset() or "utf-8", errors="ignore")
otp = self._extract_otp_candidate(f"{envelope}\n{body_text}")
if otp:
return otp
return ""
finally:
try:
client.logout()
except Exception:
pass
async def _wait_for_anytime_otp(self, expected_recipient: str) -> str:
timeout_s = int(os.getenv("ANYTIME_MAILBOX_OTP_TIMEOUT_SECONDS", "180"))
poll_s = int(os.getenv("ANYTIME_MAILBOX_OTP_POLL_SECONDS", "6"))
elapsed = 0
while elapsed <= timeout_s:
otp = await asyncio.to_thread(self._fetch_anytime_otp_sync, expected_recipient)
if otp:
return otp
await asyncio.sleep(poll_s)
elapsed += poll_s
return ""
async def _click_first(self, candidates: list[str], timeout: int = 8000) -> bool:
if not self.page:
return False
for candidate in candidates:
if not candidate:
continue
locator = self.page.locator(candidate).first
try:
if await locator.count() > 0:
await locator.wait_for(state="visible", timeout=timeout)
await locator.click()
await self.human_delay(0.3, 0.8)
return True
except Exception:
continue
return False
async def _fill_first(self, candidates: list[str], value: str, timeout: int = 8000) -> bool:
if not self.page:
return False
for candidate in candidates:
if not candidate:
continue
locator = self.page.locator(candidate).first
try:
if await locator.count() > 0:
await locator.wait_for(state="visible", timeout=timeout)
await locator.fill(value)
await self.human_delay(0.2, 0.5)
return True
except Exception:
continue
return False
# ------------------------------------------------------------------ #
# Name Search & Reservation
# ------------------------------------------------------------------ #
async def search_name(self, name: str) -> NameSearchResult:
"""Search BC Registry for name availability.
Uses bcregistrynames.gov.bc.ca Name Request portal.
Stub selectors need portal inspection.
"""
self.log.info("Searching BC Registry for name: %s", name)
selectors = CONFIG["selectors"]
try:
page = await self.start_browser()
await page.goto(CONFIG["name_request_portal"]["url"])
await self.human_delay(2.0, 4.0)
await self.screenshot("name_search_start")
# --- STUB: fill in once selectors are captured ---
# await self.type_slowly(selectors["name_search_input"], name)
# await self.safe_click(selectors["name_search_submit"])
# await page.wait_for_load_state("networkidle", timeout=15000)
# await self.human_delay(1.5, 3.0)
# await self.screenshot("name_search_result")
#
# available_el = await page.query_selector(selectors["name_result_available"])
# unavailable_el = await page.query_selector(selectors["name_result_unavailable"])
#
# available = available_el is not None and unavailable_el is None
self.log.warning("BC name search selectors not configured — returning stub result")
return NameSearchResult(
available=False,
exact_match=False,
similar_names=[],
state_code="BC",
searched_name=name,
raw_response="STUB: selectors not yet configured",
)
except Exception as exc:
self.log.error("BC name search failed: %s", exc)
await self.screenshot("name_search_error")
return NameSearchResult(
available=False,
state_code="BC",
searched_name=name,
raw_response=str(exc),
)
finally:
await self.close_browser()
async def reserve_name(self, name: str) -> dict:
"""Submit a Name Request on bcregistrynames.gov.bc.ca.
Name reservations in BC are valid for 56 days and cost C$30.
Numbered companies skip this step entirely.
Returns:
dict with keys: success, nr_number (Name Request number), message
"""
self.log.info("Reserving name in BC: %s", name)
selectors = CONFIG["selectors"]
try:
page = await self.start_browser()
await page.goto(CONFIG["name_request_portal"]["url"])
await self.human_delay(2.0, 4.0)
# --- STUB: fill in once selectors are captured ---
# Step 1: Enter name
# await self.type_slowly(selectors["name_search_input"], name)
# await self.safe_click(selectors["name_search_submit"])
# await page.wait_for_load_state("networkidle", timeout=15000)
# await self.human_delay(1.5, 3.0)
#
# Step 2: Click reserve
# await self.safe_click(selectors["name_reserve_btn"])
# await self.human_delay(1.0, 2.0)
#
# Step 3: Pay C$30 via Relay card
# ... payment selectors ...
#
# Step 4: Capture NR number from confirmation page
self.log.warning("BC name reservation selectors not configured — returning stub")
await self.screenshot("name_reserve_stub")
return {
"success": False,
"nr_number": "",
"message": "STUB: selectors not yet configured",
}
except Exception as exc:
self.log.error("BC name reservation failed: %s", exc)
await self.screenshot("name_reserve_error")
return {
"success": False,
"nr_number": "",
"message": str(exc),
}
finally:
await self.close_browser()
# ------------------------------------------------------------------ #
# Incorporation Filing
# ------------------------------------------------------------------ #
async def file_incorporation(self, order: FormationOrder) -> FilingResult:
"""File BC incorporation via Corporate Online.
Full flow:
1. Login to Corporate Online
2. Start new incorporation
3. Enter company name (or use numbered company)
4. Enter registered office address (Anytime Mailbox)
5. Enter records office (same as registered)
6. Enter director(s)
7. Enter share structure
8. Upload/confirm articles
9. Pay C$350 via Relay virtual debit card
10. Capture BC incorporation number from confirmation
Stub selectors need portal inspection.
"""
self.log.info("Filing BC incorporation for: %s", order.entity_name)
selectors = CONFIG["selectors"]
missing_steps = self._missing_colin_step_map()
if missing_steps:
detail = "; ".join(
f"step {step}: {', '.join(keys)}" for step, keys in sorted(missing_steps.items())
)
self.log.warning("BC incorporation blocked — unverified COLIN selectors: %s", detail)
return FilingResult(
success=False,
status=FilingStatus.PENDING,
state_code="BC",
entity_name=order.entity_name,
filing_number="",
confirmation_number="",
error_message=f"COLIN selector verification required ({detail})",
)
try:
page = await self.start_browser()
# --- Step 1: Login ---
await page.goto(CONFIG["filing_portal"]["login_url"])
await self.human_delay(2.0, 4.0)
await self.screenshot("inc_login_page")
# await self.type_slowly(selectors["login_username"], os.getenv("BC_REGISTRY_USERNAME", ""))
# await self.type_slowly(selectors["login_password"], os.getenv("BC_REGISTRY_PASSWORD", ""))
# await self.safe_click(selectors["login_submit"])
# await page.wait_for_load_state("networkidle", timeout=15000)
# await self.human_delay(2.0, 4.0)
# await self.screenshot("inc_logged_in")
# --- Step 2: Start new incorporation ---
# Navigate to incorporation form
# await page.goto(CONFIG["filing_portal"]["url"] + "/incorporation/new")
# await self.human_delay(1.5, 3.0)
# --- Step 3: Company name ---
# await self.type_slowly(selectors["inc_company_name"], order.entity_name)
# await self.human_delay(0.5, 1.0)
# --- Step 4: Registered office (Anytime Mailbox) ---
office = CONFIG["registered_office"]
# await self.type_slowly(selectors["inc_registered_office_street"], office["street"])
# await self.type_slowly(selectors["inc_registered_office_city"], office["city"])
# await self.type_slowly(selectors["inc_registered_office_province"], office["province"])
# await self.type_slowly(selectors["inc_registered_office_postal"], office["postal_code"])
# --- Step 5: Records office = same as registered ---
# await self.safe_click(selectors["inc_records_office_same"])
# --- Step 6: Director(s) ---
# for member in order.members:
# await self.type_slowly(selectors["inc_director_name"], member.name)
# await self.type_slowly(selectors["inc_director_address"],
# f"{member.address}, {member.city}, {member.state} {member.zip_code}")
# await self.human_delay(0.5, 1.0)
# --- Step 7: Share structure ---
# await self.type_slowly(selectors["inc_share_structure"], str(order.shares_authorized))
# --- Step 8: Articles ---
# Default articles for BC are standard — just confirm
# await self.safe_click(selectors["inc_articles"])
# --- Step 9: Payment (C$350 via Relay card) ---
# payment_selectors = {
# "card_number_field": selectors["pay_card_number"],
# "card_exp_field": selectors["pay_card_exp"],
# "card_cvv_field": selectors["pay_card_cvv"],
# "card_name_field": selectors["pay_card_name"],
# "submit_payment_btn": selectors["pay_submit"],
# }
# await self.enter_payment(order, payment_selectors)
# --- Step 10: Capture confirmation ---
# await self.screenshot("inc_confirmation")
# bc_number = await page.text_content(".confirmation-number") # placeholder selector
self.log.warning("BC incorporation selectors not configured — returning stub")
await self.screenshot("inc_stub")
return FilingResult(
success=False,
status=FilingStatus.PENDING,
state_code="BC",
entity_name=order.entity_name,
filing_number="",
confirmation_number="",
error_message="STUB: selectors not yet configured",
)
except Exception as exc:
self.log.error("BC incorporation failed: %s", exc)
await self.screenshot("inc_error")
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code="BC",
entity_name=order.entity_name,
error_message=str(exc),
)
finally:
await self.close_browser()
# ------------------------------------------------------------------ #
# file_llc / file_corporation — required by StatePortal ABC
# ------------------------------------------------------------------ #
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""LLCs do not exist under Canadian law."""
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code="BC",
entity_name=order.entity_name,
error_message="LLCs are not available in Canada. Use file_incorporation() for a BC corporation.",
)
async def file_corporation(self, order: FormationOrder) -> FilingResult:
"""File a BC corporation — delegates to file_incorporation()."""
return await self.file_incorporation(order)
# ------------------------------------------------------------------ #
# Anytime Mailbox Setup
# ------------------------------------------------------------------ #
async def scrape_available_units(self, location_url: str) -> list[str]:
"""Scrape available mailbox unit numbers from an Anytime Mailbox location page.
Navigates to the location, clicks through to the mailbox number selection step,
and extracts all available unit numbers from the dropdown.
Returns a list of available unit number strings (e.g. ["101", "205", "B438"]).
"""
self.log.info("Scraping available units from: %s", location_url)
units = []
try:
page = await self.start_browser()
await page.goto(location_url, wait_until="networkidle", timeout=30000)
await self.human_delay(2.0, 3.0)
# Click SELECT on the first/cheapest plan to enter signup flow
await self._click_first([
'button:has-text("Select")',
'a:has-text("Select")',
'button:has-text("SELECT")',
])
await self.human_delay(2.0, 3.0)
# Click yearly plan period if available
await self._click_first([
'button:has-text("Yearly")',
'label:has-text("Yearly")',
'button:has-text("Annual")',
])
await self.human_delay(1.0, 2.0)
# Click continue/select to get to mailbox number step
await self._click_first([
'button:has-text("Continue")',
'button:has-text("Select")',
'button:has-text("Next")',
])
await self.human_delay(2.0, 3.0)
# Extract unit numbers from dropdown/select element
# AMB uses a dropdown or list for available mailbox numbers
unit_options = await page.evaluate("""() => {
// Try select dropdowns
const selects = document.querySelectorAll('select');
for (const sel of selects) {
const opts = [...sel.options].filter(o => o.value && o.value !== '');
if (opts.length > 1) {
return opts.map(o => o.value || o.textContent.trim());
}
}
// Try radio buttons or clickable list items
const radios = document.querySelectorAll('input[type="radio"][name*="mailbox"], input[type="radio"][name*="unit"]');
if (radios.length > 0) {
return [...radios].map(r => r.value || r.parentElement?.textContent?.trim() || '');
}
// Try list items that look like unit numbers
const items = document.querySelectorAll('[class*="mailbox"], [class*="unit"], [data-unit]');
if (items.length > 0) {
return [...items].map(i => i.textContent?.trim() || i.getAttribute('data-unit') || '').filter(Boolean);
}
return [];
}""")
units = [str(u).strip() for u in (unit_options or []) if str(u).strip()]
self.log.info("Found %d available units at %s", len(units), location_url)
await self.screenshot("mailbox_units_available")
except Exception as e:
self.log.error("Failed to scrape units from %s: %s", location_url, e)
finally:
try:
await self.stop_browser()
except Exception:
pass
return units
async def signup_with_unit(self, order: FormationOrder, unit_number: str, location_url: str) -> dict:
"""Sign up for Anytime Mailbox with a specific pre-selected unit number.
Similar to setup_mailbox() but uses a specific location URL and unit number
that the client selected in the portal, instead of auto-picking.
Returns dict with success, unit_number, mailbox_id, account_email.
"""
self.log.info("Signing up at %s with unit %s for: %s", location_url, unit_number, order.entity_name)
try:
page = await self.start_browser()
await page.goto(location_url, wait_until="networkidle", timeout=30000)
await self.human_delay(2.0, 3.0)
# Click SELECT on the cheapest plan
await self._click_first([
'button:has-text("Select")',
'a:has-text("Select")',
])
await self.human_delay(2.0, 3.0)
# Select yearly
await self._click_first([
'button:has-text("Yearly")',
'label:has-text("Yearly")',
])
await self.human_delay(1.0, 2.0)
await self._click_first([
'button:has-text("Continue")',
'button:has-text("Select")',
])
await self.human_delay(2.0, 3.0)
# Select the specific unit number from dropdown
selects = await page.query_selector_all("select")
unit_selected = False
for sel in selects:
opts = await sel.evaluate("el => [...el.options].map(o => o.value)")
if unit_number in opts:
await sel.select_option(unit_number)
unit_selected = True
break
if not unit_selected:
# Try clicking the unit in a list/radio
await self._click_first([
f'input[value="{unit_number}"]',
f'label:has-text("{unit_number}")',
f'[data-unit="{unit_number}"]',
])
await self.human_delay(1.0, 2.0)
# Now proceed with the rest of the signup flow (same as setup_mailbox)
member_name = order.members[0].name if order.members else order.regulatory_contact_name or "Client Name"
name_parts = member_name.split(" ", 1)
first_name = name_parts[0]
last_name = name_parts[1] if len(name_parts) > 1 else "Client"
signup_email = (
os.getenv("ANYTIME_MAILBOX_SIGNUP_EMAIL", "").strip()
or f'mailbox+{order.order_id.lower()}@performancewest.net'
)
signup_phone = order.regulatory_contact_phone or os.getenv("ANYTIME_MAILBOX_SIGNUP_PHONE", "+16025550123")
signup_password = os.getenv("ANYTIME_MAILBOX_DEFAULT_PASSWORD", "").strip() or f"Pw!{secrets.token_hex(8)}"
await self._fill_first(['input[name*="first" i]'], first_name)
await self._fill_first(['input[name*="last" i]'], last_name)
await self._fill_first(['input[name*="business" i]'], order.entity_name)
await self._click_first(['button:has-text("Continue")', 'button:has-text("Next")'])
await self.human_delay(1.5, 2.5)
# Contact details
full_street = order.principal_address or order.mailing_address or "5307 Victoria Dr"
city = order.principal_city or order.mailing_city or "Vancouver"
province = order.principal_state or order.mailing_state or "BC"
postal = order.principal_zip or order.mailing_zip or "V5P 3V6"
await self._fill_first(['input[name*="address" i]'], full_street)
await self._fill_first(['input[name*="city" i]'], city)
await self._fill_first(['input[name*="state" i]', 'input[name*="province" i]'], province)
await self._fill_first(['input[name*="zip" i]', 'input[name*="postal" i]'], postal)
await self._fill_first(['input[type="email"]'], signup_email)
await self._fill_first(['input[type="tel"]'], signup_phone)
await self._fill_first(['input[type="password"]'], signup_password)
await self._click_first(['button:has-text("Continue")', 'button:has-text("Next")'])
await self.human_delay(1.5, 2.5)
# OTP verification
otp_code = os.getenv("ANYTIME_MAILBOX_OTP_CODE", "").strip()
if not otp_code:
otp_code = await self._wait_for_anytime_otp(signup_email)
if otp_code:
await self._fill_first([
'input[name*="verification" i]',
'input[name*="otp" i]',
'input[inputmode="numeric"]',
], otp_code)
await self._click_first(['button:has-text("Verify")', 'button:has-text("Continue")'])
else:
await self.screenshot("mailbox_signup_waiting_otp")
return {
"success": False,
"unit_number": unit_number,
"mailbox_id": "",
"message": "OTP required: set ANYTIME_MAILBOX_OTP_CODE and retry",
}
# Checkout
await self._click_first([
'button:has-text("Continue")',
'button:has-text("Checkout")',
'button:has-text("Submit")',
])
await page.wait_for_load_state("networkidle", timeout=30000)
await self.human_delay(1.5, 3.0)
await self.screenshot("mailbox_signup_after_checkout")
page_text = (await page.content()) or ""
id_match = re.search(r"(?:Mailbox\s*ID|ID)\s*[:#]?\s*([A-Za-z0-9\-]{4,})", page_text, re.IGNORECASE)
mailbox_id = id_match.group(1) if id_match else ""
return {
"success": True,
"unit_number": unit_number,
"mailbox_id": mailbox_id,
"message": "Mailbox signup completed",
"account_email": signup_email,
}
except Exception as e:
self.log.error("Mailbox signup failed: %s", e)
await self.screenshot("mailbox_signup_error")
return {
"success": False,
"unit_number": unit_number,
"mailbox_id": "",
"message": f"Signup failed: {e}",
}
async def setup_mailbox(self, order: FormationOrder) -> dict:
"""Register an Anytime Mailbox account at 329 Howe St, Vancouver.
Flow:
1. Navigate to anytimemailbox.com
2. Select the Vancouver - Howe St location
3. Choose the Silver plan (C$164.99/yr)
4. Complete checkout with client details
5. Capture mailbox unit number for the registered office address
Returns:
dict with keys: success, unit_number, mailbox_id, message
"""
self.log.info("Setting up Anytime Mailbox for: %s", order.entity_name)
selectors = CONFIG["selectors"]
office_id = CONFIG.get("registered_office_default", "victoria-dr")
office = CONFIG.get("registered_office_locations", {}).get(office_id, CONFIG["registered_office"])
try:
page = await self.start_browser()
provider_url = office.get("provider_url") or CONFIG["registered_office"]["provider_url"]
await page.goto(provider_url)
await self.human_delay(2.0, 4.0)
await self.screenshot("mailbox_start")
# Step 1: location lookup and select plan
await self._fill_first(
[selectors.get("amb_location_search", ""), 'input[placeholder*="city" i]', 'input[type="search"]'],
f'{office.get("city", "Vancouver")} {office.get("province", "BC")}',
)
await self._click_first(
[
selectors.get("amb_location_select", ""),
f'text={office.get("street", "")}',
'button:has-text("Select")',
]
)
# Step 2: pick plan (yearly preferred)
await self._click_first(
[
selectors.get("amb_plan_period_yearly", ""),
'button:has-text("Yearly")',
'label:has-text("Yearly")',
]
)
await self._click_first(
[
selectors.get("amb_plan_select", ""),
f'text={office.get("plan", "Basic")}',
'button:has-text("Select")',
'button:has-text("Continue")',
]
)
# Step 3: mailbox number + identity details
await self._click_first([
selectors.get("amb_mailbox_number_first", ""),
'button:has-text("Choose")',
'button:has-text("Select")',
])
member_name = order.members[0].name if order.members else order.regulatory_contact_name or "Client Name"
name_parts = member_name.split(" ", 1)
first_name = name_parts[0]
last_name = name_parts[1] if len(name_parts) > 1 else "Client"
signup_email = (
os.getenv("ANYTIME_MAILBOX_SIGNUP_EMAIL", "").strip()
or f'mailbox+{order.order_id.lower()}@performancewest.net'
)
signup_phone = order.regulatory_contact_phone or os.getenv("ANYTIME_MAILBOX_SIGNUP_PHONE", "+16025550123")
signup_password = os.getenv("ANYTIME_MAILBOX_DEFAULT_PASSWORD", "").strip() or f"Pw!{secrets.token_hex(8)}"
await self._fill_first([selectors.get("amb_first_name", ""), 'input[name*="first" i]'], first_name)
await self._fill_first([selectors.get("amb_last_name", ""), 'input[name*="last" i]'], last_name)
await self._fill_first([selectors.get("amb_business_name", ""), 'input[name*="business" i]'], order.entity_name)
await self._click_first([
selectors.get("amb_continue", ""),
'button:has-text("Continue")',
'button:has-text("Next")',
])
# Step 4: contact details + account credentials
full_street = order.principal_address or order.mailing_address or "5307 Victoria Dr"
city = order.principal_city or order.mailing_city or office.get("city", "Vancouver")
province = order.principal_state or order.mailing_state or office.get("province", "BC")
postal = order.principal_zip or order.mailing_zip or office.get("postal_code", "V5P 3V6")
await self._fill_first([selectors.get("amb_home_address", ""), 'input[name*="address" i]'], full_street)
await self._fill_first([selectors.get("amb_home_city", ""), 'input[name*="city" i]'], city)
await self._fill_first([selectors.get("amb_home_state", ""), 'input[name*="state" i]', 'input[name*="province" i]'], province)
await self._fill_first([selectors.get("amb_home_postal", ""), 'input[name*="zip" i]', 'input[name*="postal" i]'], postal)
await self._fill_first([selectors.get("amb_email", ""), 'input[type="email"]'], signup_email)
await self._fill_first([selectors.get("amb_phone", ""), 'input[type="tel"]'], signup_phone)
await self._fill_first([selectors.get("amb_password", ""), 'input[type="password"]'], signup_password)
await self._click_first([
selectors.get("amb_continue", ""),
'button:has-text("Continue")',
'button:has-text("Next")',
])
# Step 5: OTP verification (required).
otp_code = os.getenv("ANYTIME_MAILBOX_OTP_CODE", "").strip()
if not otp_code:
otp_code = await self._wait_for_anytime_otp(signup_email)
if otp_code:
await self._fill_first(
[
selectors.get("amb_otp", ""),
'input[name*="verification" i]',
'input[name*="otp" i]',
'input[inputmode="numeric"]',
],
otp_code,
)
await self._click_first([
selectors.get("amb_otp_submit", ""),
'button:has-text("Verify")',
'button:has-text("Continue")',
])
else:
await self.screenshot("mailbox_waiting_otp")
return {
"success": False,
"unit_number": "",
"mailbox_id": "",
"message": "OTP required: set ANYTIME_MAILBOX_OTP_CODE and retry",
}
# Step 6: review + checkout
await self._click_first([
selectors.get("amb_checkout_submit", ""),
'button:has-text("Continue")',
'button:has-text("Checkout")',
'button:has-text("Submit")',
])
await page.wait_for_load_state("networkidle", timeout=30000)
await self.human_delay(1.5, 3.0)
await self.screenshot("mailbox_after_checkout")
page_text = (await page.content()) or ""
unit_match = re.search(r"(?:Suite|Unit|Mailbox|#)\s*([A-Za-z0-9\-]+)", page_text, re.IGNORECASE)
id_match = re.search(r"(?:Mailbox\s*ID|ID)\s*[:#]?\s*([A-Za-z0-9\-]{4,})", page_text, re.IGNORECASE)
mailbox_unit = unit_match.group(1) if unit_match else ""
mailbox_id = id_match.group(1) if id_match else ""
return {
"success": True,
"unit_number": mailbox_unit,
"mailbox_id": mailbox_id,
"message": "Anytime Mailbox signup submitted",
"account_email": signup_email,
}
except Exception as exc:
self.log.error("Anytime Mailbox setup failed: %s", exc)
await self.screenshot("mailbox_error")
return {
"success": False,
"unit_number": "",
"mailbox_id": "",
"message": str(exc),
}
finally:
await self.close_browser()
# ------------------------------------------------------------------ #
# CRTC Notification Letter
# ------------------------------------------------------------------ #
async def generate_crtc_letter(self, order: FormationOrder) -> Optional[str]:
"""Generate a CRTC notification letter from the DOCX template.
Fills the template with:
- Corporation name, BC incorporation number, date
- Registered office address (Anytime Mailbox)
- Director name(s)
- CRTC Secretary General address
Returns:
Path to the generated PDF, or None on failure.
"""
self.log.info("Generating CRTC letter for: %s", order.entity_name)
try:
from docx import Document
import subprocess
import tempfile
template_path = Path(CRTC_TEMPLATE)
if not template_path.exists():
self.log.error("CRTC letter template not found: %s", template_path)
return None
doc = Document(str(template_path))
office = CONFIG["registered_office"]
crtc = CONFIG["crtc"]
# Build director list
directors = ", ".join(m.name for m in order.members) if order.members else "N/A"
# Variable replacements
variables = {
"{{date}}": datetime.utcnow().strftime("%B %d, %Y"),
"{{entity_name}}": order.entity_name,
"{{bc_number}}": order.state_filing_number or "PENDING",
"{{incorporation_date}}": order.filed_at or datetime.utcnow().strftime("%B %d, %Y"),
"{{registered_office}}": (
f"{office['street']}, {office['city']}, "
f"{office['province']} {office['postal_code']}"
),
"{{directors}}": directors,
"{{crtc_address}}": (
f"{crtc['secretary_general']}\n"
f"{crtc['address']}\n"
f"{crtc['city']}, {crtc['province']} {crtc['postal_code']}"
),
}
# Replace placeholders in paragraphs
for paragraph in doc.paragraphs:
for key, value in variables.items():
if key in paragraph.text:
for run in paragraph.runs:
if key in run.text:
run.text = run.text.replace(key, value)
# Replace placeholders in tables
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
for key, value in variables.items():
if key in paragraph.text:
for run in paragraph.runs:
if key in run.text:
run.text = run.text.replace(key, value)
# Save DOCX
work_dir = tempfile.mkdtemp(prefix="pw_crtc_")
docx_path = os.path.join(work_dir, f"crtc_letter_{order.order_id}.docx")
doc.save(docx_path)
self.log.info("CRTC letter DOCX saved: %s", docx_path)
# Convert to PDF via LibreOffice
result = subprocess.run(
[
"libreoffice", "--headless",
"--convert-to", "pdf",
"--outdir", work_dir,
docx_path,
],
capture_output=True,
text=True,
timeout=120,
)
if result.returncode != 0:
self.log.error("LibreOffice conversion failed: %s", result.stderr)
return None
pdf_path = os.path.join(work_dir, f"crtc_letter_{order.order_id}.pdf")
if not Path(pdf_path).exists():
self.log.error("CRTC letter PDF not generated at: %s", pdf_path)
return None
self.log.info("CRTC letter PDF generated: %s", pdf_path)
return pdf_path
except Exception as exc:
self.log.error("CRTC letter generation failed: %s", exc)
return None
# Module-level convenience instance
adapter = BCPortal()

View file

@ -0,0 +1,615 @@
"""
Configuration for British Columbia, Canada BC Business Corporations Act.
BC uses a provincial incorporation system (not federal), governed by the
BC Business Corporations Act (SBC 2002, c. 57). Entities formed here are
BC corporations LLCs do not exist under Canadian law.
Portal stack:
- Corporate Online (corporateonline.gov.bc.ca) filing & annual reports
NOTE: No login required for new incorporations the wizard is anonymous
and payment is taken by credit card at the end. Do NOT attempt username/
password auth the login page is IDIR-only (government employees).
- BC Registry Name Request (bcregistrynames.gov.bc.ca) name reservation
- Anytime Mailbox (anytimemailbox.com) virtual mailbox for registered office
- CRTC Canadian Radio-television and Telecommunications Commission
(requires notification letter for telecom carriers)
Currency: all fees in CAD (C$).
COLIN wizard steps (in order):
1. Initial Information company name / effective date
2. Incorporator Info incorporator name + address
3. Completing Party person completing the filing
4. Translated Name (skip not applicable)
5. Director Info director name(s) + address(es)
6. Office Addresses registered office + records office
7. Share Structure share classes (Common shares, no par value)
8. Notification email for receipt
9. Company Information confirm name + type
10. Confirm Company Info review everything
11. Ready to Pay credit card entry
12. Your Receipt BC incorporation number
"""
CONFIG = {
"jurisdiction": "British Columbia",
"country": "Canada",
"abbreviation": "BC",
"entity_types": ["corporation"], # No LLCs in Canada
# ------------------------------------------------------------------ #
# Portal schedule — BC Corporate Online hours + BC holidays
# MonSat 6 AM 10 PM, Sun 1 PM 10 PM Pacific
# ------------------------------------------------------------------ #
"portal_schedule": {
"timezone": "America/Vancouver",
"jurisdiction": "BC",
"closed_holidays": True,
"hours": {
"mon": [6, 22],
"tue": [6, 22],
"wed": [6, 22],
"thu": [6, 22],
"fri": [6, 22],
"sat": [6, 22],
"sun": [13, 22],
},
},
# ------------------------------------------------------------------ #
# BC Registry — Corporate Online
# No authentication required — anonymous public filing portal.
# ------------------------------------------------------------------ #
"agency": "BC Registry Services",
"agency_url": "https://www.bcregistry.gov.bc.ca",
"filing_portal": {
"name": "Corporate Online",
"url": "https://www.corporateonline.gov.bc.ca",
# Direct URL to start a new Incorporation Application (anonymous)
"icorp_start_url": "https://www.corporateonline.gov.bc.ca/corporateonline/colin/accesstransaction/menu.do?action=startFiling&filingTypeCode=ICORP&from=main",
"icorp_overview_url": "https://www.corporateonline.gov.bc.ca/corporateonline/colin/accesstransaction/menu.do?action=overview&filingTypeCode=ICORP&from=main",
# Annual report
"annual_report_url": "https://www.corporateonline.gov.bc.ca/corporateonline/colin/accesstransaction/menu.do?action=startFiling&filingTypeCode=ANNBC&from=main",
# Legacy — kept for compat; IDIR-only now (not used for automation)
"login_url": "https://www.corporateonline.gov.bc.ca/corporateonline/colin/login/login.do",
},
"name_request_portal": {
"name": "BC Registry Name Request",
"url": "https://www.bcregistrynames.gov.bc.ca",
"search_url": "https://www.bcregistrynames.gov.bc.ca/nrSearch/name-search",
},
# ------------------------------------------------------------------ #
# Registered & Records Office — Anytime Mailbox (BC locations)
# ------------------------------------------------------------------ #
"registered_office_default": "victoria-dr",
"registered_office_locations": {
"victoria-dr": {
"id": "victoria-dr",
"label": "Vancouver - Victoria Dr (Best Value)",
"street": "5307 Victoria Dr",
"suite_prefix": "Suite",
"city": "Vancouver",
"province": "BC",
"postal_code": "V5P 3V6",
"country": "Canada",
"plan": "Basic",
"plan_cost_cad": 99.00,
"plan_period": "yearly",
"default": True,
},
"howe-st": {
"id": "howe-st",
"label": "Vancouver - Howe St (Downtown)",
"street": "329 Howe St",
"suite_prefix": "Unit",
"city": "Vancouver",
"province": "BC",
"postal_code": "V6C 3N2",
"country": "Canada",
"plan": "Silver",
"plan_cost_cad": 164.99,
"plan_period": "yearly",
"default": False,
},
"broadway": {
"id": "broadway",
"label": "Vancouver - Broadway",
"street": "1275 W Broadway",
"suite_prefix": "Suite",
"city": "Vancouver",
"province": "BC",
"postal_code": "V6H 1G2",
"country": "Canada",
"plan": "Silver",
"plan_cost_cad": 149.99,
"plan_period": "yearly",
"default": False,
},
},
# Legacy field — kept for backward compatibility
"registered_office": {
"provider": "Anytime Mailbox",
"provider_url": "https://www.anytimemailbox.com",
"location": "Vancouver - Victoria Dr",
"street": "5307 Victoria Dr",
"city": "Vancouver",
"province": "BC",
"postal_code": "V5P 3V6",
"country": "Canada",
"plan": "Basic",
"plan_cost_cad": 99.00,
"plan_period": "yearly",
},
# ------------------------------------------------------------------ #
# CRTC
# ------------------------------------------------------------------ #
"crtc": {
"name": "Canadian Radio-television and Telecommunications Commission",
"short_name": "CRTC",
"secretary_general": "Secretary General, CRTC",
"address": "1 Promenade du Portage",
"city": "Gatineau",
"province": "QC",
"postal_code": "J8X 4B1",
"country": "Canada",
"website": "https://crtc.gc.ca",
"notification_required": True,
},
# ------------------------------------------------------------------ #
# BITS (Basic International Telecommunications Services)
# ------------------------------------------------------------------ #
"bits": {
"name": "BITS Registration",
"description": (
"All Canadian telecom carriers must register with the CRTC "
"under the Basic International Telecommunications Services (BITS) regime. "
"Registration is filed via letter to the CRTC Secretary General."
),
"filing_method": "letter", # submitted with the CRTC notification letter
"annual_fee_cad": 0.00, # no fee for initial BITS notification
"renewal_required": False, # initial registration is a one-time notification
},
# ------------------------------------------------------------------ #
# CCTS (Commission for Complaints for Telecom-television Services)
# ------------------------------------------------------------------ #
"ccts": {
"name": "Commission for Complaints for Telecom-television Services",
"short_name": "CCTS",
"website": "https://www.ccts-cprst.ca",
"membership_url": "https://www.ccts-cprst.ca/for-service-providers/become-a-member/",
"description": (
"All Canadian telecom service providers must participate in the CCTS, "
"the national and independent organization dedicated to resolving "
"customer complaints about telecom and TV services. "
"Membership application is submitted online."
),
"filing_method": "online_form",
"annual_fee_cad": 0.00, # no fee for small carriers in first year
"renewal_required": True,
"renewal_period": "Yearly",
},
# ------------------------------------------------------------------ #
# GCKey — Government of Canada authentication credential
# Used to access My CRTC Account for electronic filings.
# Each carrier gets its own GCKey. Signup is a 5-step Spring Web Flow
# wizard with hCaptcha invisible on the username step.
# ------------------------------------------------------------------ #
"gckey": {
"name": "GCKey",
"description": "Government of Canada authentication credential for online services",
"homepage": "https://www.gckey.gc.ca",
"auth_domain": "clegc-gckey.gc.ca",
# SAML entry: go through CRTC SmartForms → GACS → GCKey
"saml_entry_url": "https://services.crtc.gc.ca/Pro/SmartForms/?_gc_lang=eng",
"signup_path": "/j/eng/rg", # append ?ReqID=... from SAML flow
# Signup wizard — 5 steps (Spring Web Flow)
"signup_steps": {
# Step 1: Terms and Conditions
"terms": {
"execution": "e1s1",
"accept_btn": "input[name=_eventId_accept]",
"decline_btn": "input[name=_eventId_cancel]",
},
# Step 2: Create Username
"username": {
"execution": "e1s2",
"field": "input[name=uid][id=userID]",
"submit_btn": "input[name=_eventId_submit][id=button]",
"hcaptcha_sitekey": "99871bd1-7b22-417a-b6cc-7ef645e5147a",
},
# Step 3: Create Password (selectors to be verified on first live run)
"password": {
"execution": "e1s3", # inferred — may be e1s3 or later
"field": "input[type=password][name=pwd]", # inferred
"confirm_field": "input[type=password][name=confirmPwd]", # inferred
"submit_btn": "input[name=_eventId_submit]",
},
# Step 4: Recovery Questions
"security_questions": {
"execution": "e1s4", # inferred
"question_selects": "select", # multiple <select> elements
"answer_inputs": "input[type=text]", # answer fields
"submit_btn": "input[name=_eventId_submit]",
},
# Step 5: Recovery Email (to be verified)
"email": {
"execution": "e1s5", # inferred
"field": "input[type=email], input[name=email]", # inferred
"submit_btn": "input[name=_eventId_submit]",
},
},
# Login page — used after account creation to verify the credentials work
"login_selectors": {
"username": "input[name=token1][id=token1]",
"password": "input[name=token2][id=token2]",
"signin_btn": "button[id=button]",
"csrf": "input[name=_csrf]",
"hcaptcha_sitekey": "c745648c-d973-4223-99af-8d178dc17a6c",
},
# Username format: pw-{bc_number} — deterministic per carrier
"username_prefix": "pw-",
# Password rules (from GCKey help page — to be verified)
"password_rules": {
"min_length": 8,
"max_length": 16,
"require_upper": True,
"require_lower": True,
"require_digit": True,
"require_special": True,
},
},
# ------------------------------------------------------------------ #
# ATS — CRTC Annual Telecommunications Survey
# All registered carriers must file annually via My CRTC Account (GCKey).
# ------------------------------------------------------------------ #
"ats": {
"name": "CRTC Annual Telecommunications Survey",
"portal_url": "https://services.crtc.gc.ca/Pro/SmartForms/?_gc_lang=eng",
"gckey_url": "https://www.gckey.gc.ca",
"my_crtc_url": "http://crtc.gc.ca/eng/forms/form_index.htm",
# Activation code: required for first electronic submission.
# Obtained by calling CRTC at 1-877-249-2782 or included in
# registration confirmation letter (30-60 days after filing).
"activation_code_phone": "1-877-249-2782",
"forms": {
"rep_t1": {
"name": "REP-T/T1 — Annual Telecommunications Survey",
"description": "Core annual survey for all telecom service providers",
"deadline_month": 3,
"deadline_day": 1,
"threshold": "All registered carriers must file — no revenue threshold",
"required_for_new_carriers": True,
},
"rep_u": {
"name": "REP-U — Universal Broadband Fund Survey",
"description": "Survey for carriers participating in broadband fund",
"deadline_month": 3,
"deadline_day": 31,
"threshold": "Carriers with >$10M CAD annual Canadian telecom revenue",
"required_for_new_carriers": False,
},
"form_802a": {
"name": "Form 802a — Contribution Survey",
"description": "Annual contribution obligation calculation",
"deadline_month": 3,
"deadline_day": 31,
"threshold": "Carriers with >$10M CAD annual Canadian telecom revenue",
"required_for_new_carriers": False,
},
"form_802j": {
"name": "Form 802j — Contribution Eligibility Survey",
"description": "For carriers seeking subsidy eligibility under the national contribution fund",
"deadline_month": 3,
"deadline_day": 31,
"threshold": "Only carriers seeking contribution fund subsidy eligibility",
"required_for_new_carriers": False,
},
},
"related_surveys": {
"facilities": {
"name": "Annual Facilities Survey",
"description": "Network infrastructure details for carriers owning/operating telecom facilities",
"deadline_month": 3,
"deadline_day": 31,
"threshold": "Carriers owning or operating telecom network facilities",
"required_for_new_carriers": False,
},
"pricing": {
"name": "Annual Communications Pricing Survey",
"description": "Pricing data for telecom services offered",
"deadline_month": 3,
"deadline_day": 31,
"threshold": "Carriers with >$10M CAD annual Canadian telecom revenue",
"required_for_new_carriers": False,
},
},
},
# ------------------------------------------------------------------ #
# BC Corporate Tax & Filing Obligations
# Assumes calendar fiscal year-end (Dec 31). If the client chooses a
# non-standard fiscal year, these dates need adjustment.
# ------------------------------------------------------------------ #
"corporate_obligations": {
"t2_return": {
"name": "Federal T2 Corporate Income Tax Return",
"description": (
"All Canadian corporations must file a T2 return with the CRA, "
"even if there is no tax owing or no business activity. "
"Filed electronically via CRA My Business Account or certified tax software."
),
"deadline_description": "6 months after fiscal year-end",
"deadline_month": 6, # June 30 for Dec 31 year-end
"deadline_day": 30,
"required": True,
"penalty": "5% of unpaid tax + 1%/month for up to 12 months",
"cra_url": "https://www.canada.ca/en/revenue-agency/services/tax/businesses/topics/corporations/corporation-income-tax-return.html",
},
"t2_tax_payment": {
"name": "Federal/Provincial Corporate Tax Payment",
"description": (
"Corporate income tax balance owing is due earlier than the T2 return. "
"For Canadian-Controlled Private Corporations (CCPCs) with taxable income "
"under $500K, payment is due 3 months after year-end. "
"Interest accrues on late payments."
),
"deadline_description": "3 months after fiscal year-end (CCPCs under $500K)",
"deadline_month": 3, # March 31 for Dec 31 year-end
"deadline_day": 31,
"required": True,
},
"gst_hst_return": {
"name": "GST/HST Return",
"description": (
"If registered for GST/HST (required if revenue > $30K/yr, "
"recommended to register voluntarily for input tax credits). "
"Annual filers: due 3 months after fiscal year-end. "
"Telecom services are generally GST/HST taxable."
),
"deadline_description": "3 months after fiscal year-end (annual filers)",
"deadline_month": 3, # March 31 for Dec 31 year-end
"deadline_day": 31,
"threshold": "Mandatory if >$30K revenue; voluntary registration recommended",
"required_for_new_carriers": True, # should register voluntarily
},
"t4_t4a_slips": {
"name": "T4/T4A Information Slips",
"description": (
"If the corporation has employees or contractors paid >$500/yr, "
"T4 (employment) and/or T4A (contractor) slips must be filed. "
"Not applicable for most new shell telecom corporations."
),
"deadline_description": "Last day of February following the calendar year",
"deadline_month": 2,
"deadline_day": 28,
"threshold": "Only if corporation has employees or pays contractors >$500/yr",
"required_for_new_carriers": False,
},
"bc_pst": {
"name": "BC Provincial Sales Tax (PST) Return",
"description": (
"Most telecom services in BC are subject to 7% PST. "
"If registered as a PST collector, returns are due monthly, "
"quarterly, or annually depending on volume. "
"New carriers should consult with an accountant about PST obligations."
),
"threshold": "If collecting PST on taxable telecom services",
"required_for_new_carriers": False, # depends on service type
},
"worksafebc": {
"name": "WorkSafeBC Annual Return",
"description": (
"Required if the corporation has employees in BC. "
"Annual return reports payroll for workers' compensation premium calculation. "
"Not applicable for corporations with no employees."
),
"deadline_description": "March 1 following the calendar year",
"deadline_month": 3,
"deadline_day": 1,
"threshold": "Only if corporation has BC employees",
"required_for_new_carriers": False,
},
"crtc_registration_update": {
"name": "CRTC Annual Registration Update",
"description": (
"The CRTC contacts registered carriers annually to verify and update "
"registration information. Must respond to maintain active registration status. "
"The CRTC initiates this — you just need to respond."
),
"deadline_description": "Respond within 30 days of CRTC contact (typically Q1)",
"required": True,
},
},
# ------------------------------------------------------------------ #
# Fees (CAD)
# ------------------------------------------------------------------ #
"fees": {
"name_reservation": 30.00,
"incorporation": 350.00,
"annual_report": 42.00,
"mailbox_yearly": 164.99,
"currency": "CAD",
},
# ------------------------------------------------------------------ #
# Playwright selectors — BC Corporate Online (Struts / classic HTML)
#
# IMPORTANT NOTES ON THE PORTAL:
# - No login required for Incorporation Application (anonymous filing)
# - All form fields follow Struts DTO naming: fedDto.*
# - Wizard is a multi-page POST flow; Playwright must follow the
# "Next" / "Continue" buttons between pages
# - Payment is by credit card — use Relay virtual debit card
# - After payment, the BC incorporation number appears on the receipt
#
# Selector status:
# ✓ CONFIRMED from live portal HTML (fetched 2026-04-04)
# ~ INFERRED from Struts DTO naming convention + wizard step structure
# ✗ UNVERIFIED — needs manual inspection in a live session
# ------------------------------------------------------------------ #
"selectors": {
# ── Step 0: No login required ────────────────────────────────────
# Navigate directly to icorp_start_url (anonymous)
"login_username": "", # Not used — portal is anonymous
"login_password": "", # Not used
"login_submit": "", # Not used
# ── Step 1: Initial Information ──────────────────────────────────
# URL: .../menu.do?action=startFiling&filingTypeCode=ICORP&from=main
# Confirmed from live HTML fetch 2026-04-04:
# - Radio button for numbered company: value="NMBRD"
# - Name reservation number input field is present
# - Effective date selects: fedDto.effectiveDateTime.*
"inc_numbered_company_radio": "input[type='radio'][value='NMBRD']", # ✓ confirmed
"inc_nr_number": "input[name='fedDto.nameReservationNumber']", # ~ inferred
"inc_effective_immediately": "input[type='radio'][value='immediate']", # ~ inferred
"inc_next_btn": "input[type='submit'][value='Next >']", # ~ inferred
# ── Step 2: Incorporator Info ────────────────────────────────────
# Who is incorporating the company. We list Performance West Inc.
# as the incorporator (agent on behalf of client).
"inc_incorporator_first": "input[name='fedDto.incorporatorDto.firstName']", # ~ inferred
"inc_incorporator_last": "input[name='fedDto.incorporatorDto.lastName']", # ~ inferred
"inc_incorporator_org": "input[name='fedDto.incorporatorDto.orgName']", # ~ inferred (org name)
"inc_incorporator_addr1": "input[name='fedDto.incorporatorDto.address.addr1']", # ~ inferred
"inc_incorporator_city": "input[name='fedDto.incorporatorDto.address.city']", # ~ inferred
"inc_incorporator_prov": "select[name='fedDto.incorporatorDto.address.province']", # ~ inferred
"inc_incorporator_postal":"input[name='fedDto.incorporatorDto.address.postalCd']", # ~ inferred
"inc_incorporator_country":"select[name='fedDto.incorporatorDto.address.country']", # ~ inferred
# ── Step 3: Completing Party ─────────────────────────────────────
# Person completing this filing (same as incorporator for us).
"inc_completing_same_chk": "input[type='checkbox'][name*='sameasincorporator' i]", # ~ inferred
"inc_completing_first": "input[name='fedDto.completingPartyDto.firstName']", # ~ inferred
"inc_completing_last": "input[name='fedDto.completingPartyDto.lastName']", # ~ inferred
"inc_completing_phone": "input[name='fedDto.completingPartyDto.phoneNumber']", # ~ inferred
"inc_completing_email": "input[name='fedDto.completingPartyDto.email']", # ~ inferred
# ── Step 5: Director Info ────────────────────────────────────────
# Director 1 (the client is typically sole director)
# These are the UNVERIFIED selectors flagged in adapter.py.
"inc_director_name": "input[name='fedDto.directorDtos[0].fullName']", # ✗ unverified
"inc_director_first": "input[name='fedDto.directorDtos[0].firstName']", # ✗ unverified
"inc_director_last": "input[name='fedDto.directorDtos[0].lastName']", # ✗ unverified
"inc_director_addr1": "input[name='fedDto.directorDtos[0].address.addr1']", # ✗ unverified
"inc_director_city": "input[name='fedDto.directorDtos[0].address.city']", # ✗ unverified
"inc_director_prov": "select[name='fedDto.directorDtos[0].address.province']",# ✗ unverified
"inc_director_postal": "input[name='fedDto.directorDtos[0].address.postalCd']", # ✗ unverified
"inc_director_country": "select[name='fedDto.directorDtos[0].address.country']", # ✗ unverified
# Legacy combined field (some COLIN versions use a single fullName input)
"inc_director_address": "input[name='fedDto.directorDtos[0].address.addr1']", # ✗ unverified
# ── Step 6: Office Addresses ─────────────────────────────────────
# Registered office = Anytime Mailbox address + unit number
"inc_registered_office_street": "input[name='fedDto.regOfficeDto.deliveryAddress.addr1']", # ✗ unverified
"inc_registered_office_city": "input[name='fedDto.regOfficeDto.deliveryAddress.city']", # ✗ unverified
"inc_registered_office_province":"select[name='fedDto.regOfficeDto.deliveryAddress.province']", # ✗ unverified
"inc_registered_office_postal": "input[name='fedDto.regOfficeDto.deliveryAddress.postalCd']", # ✗ unverified
# Records office same as registered — typical checkbox
"inc_records_office_same": "input[type='checkbox'][name*='recordsSame' i]", # ✗ unverified
# company_name is shown as a label here; not a text input at this step
"inc_company_name": "input[name='fedDto.nameReservationNumber']", # only for NR# path
# ── Step 7: Share Structure ──────────────────────────────────────
# Standard structure: 1 class, "Common Shares", unlimited, no par value
# COLIN presents a pre-filled Table 1 Articles option (checkbox to adopt)
"inc_share_structure": "input[type='checkbox'][name*='adoptTable1' i]", # ✗ unverified
"inc_table1_adopt": "input[type='checkbox'][name*='adoptTable1' i]", # ✗ unverified
"inc_share_class_name": "input[name='fedDto.shareDtos[0].className']", # ✗ unverified
"inc_share_max": "input[name='fedDto.shareDtos[0].maxShares']", # ✗ unverified
# Articles file upload — only needed if NOT using Table 1
"inc_articles": "input[type='file'][name*='articles' i]", # ✗ unverified
# ── Step 8: Notification ─────────────────────────────────────────
"inc_notification_email": "input[name='fedDto.notificationEmail']", # ~ inferred
# ── Step 11: Ready to Pay (credit card) ──────────────────────────
# COLIN uses a standard card form at checkout
"pay_card_number": "input[name='cardNumber'], input[id*='cardNumber' i], input[autocomplete='cc-number']", # ✗ unverified
"pay_card_exp": "input[name='expiryDate'], input[id*='expiry' i], input[autocomplete='cc-exp']", # ✗ unverified
"pay_card_cvv": "input[name='cvv'], input[name='cvd'], input[id*='cvv' i], input[autocomplete='cc-csc']", # ✗ unverified
"pay_card_name": "input[name='cardholderName'], input[id*='cardHolder' i], input[autocomplete='cc-name']", # ✗ unverified
"pay_submit": "input[type='submit'][value*='Pay' i], button:has-text('Pay Now'), input[type='submit'][value*='Submit Payment' i]", # ✗ unverified
# ── Step 12: Submit / Confirmation ───────────────────────────────
"inc_submit": "input[type='submit'][value*='Submit' i], input[type='submit'][value*='Confirm' i]", # ✗ unverified
# Confirmation / receipt page — BC incorporation number
"inc_confirmation_number": ".confirmation-number, td:has-text('Incorporation Number') + td, td.dataValue", # ✗ unverified
# ── Name Request portal (bcregistrynames.gov.bc.ca) ──────────────
# Modern Vue.js SPA — selectors are data-test or aria attributes
"name_search_input": "input[id='business-name'], input[placeholder*='Enter name' i], input[data-test='business-name']", # ~ inferred
"name_search_submit": "button[data-test='search-btn'], button:has-text('Search')", # ~ inferred
"name_result_available": ".v-chip--label:has-text('Available'), .available-badge, [class*='available']", # ~ inferred
"name_result_unavailable":"[class*='not-available'], [class*='unavailable'], .v-chip:has-text('Not Available')", # ~ inferred
"name_reserve_btn": "button:has-text('Reserve'), button[data-test='reserve-btn']", # ~ inferred
# ── Anytime Mailbox ───────────────────────────────────────────────
"amb_location_search": "input[placeholder*='city' i], input[placeholder*='search' i]",
"amb_email": "input[type='email'], input[name*='email' i]",
"amb_password": "input[type='password'], input[name*='password' i]",
"amb_phone": "input[type='tel'], input[name*='phone' i]",
"amb_first_name": "input[name*='firstName' i], input[name*='first_name' i], input[placeholder*='First name' i]",
"amb_last_name": "input[name*='lastName' i], input[name*='last_name' i], input[placeholder*='Last name' i]",
"amb_business_name": "input[name*='business' i], input[placeholder*='Business name' i]",
"amb_home_address": "input[name*='address' i]:not([name*='email' i]), input[placeholder*='Street' i]",
"amb_home_city": "input[name*='city' i]",
"amb_home_state": "select[name*='state' i], select[name*='province' i]",
"amb_home_postal": "input[name*='postal' i], input[name*='zip' i]",
"amb_plan_select": "button:has-text('Select'), a:has-text('Select')",
"amb_plan_period_yearly": "input[type='radio'][value*='year' i], label:has-text('Yearly')",
"amb_location_select": "button:has-text('Choose'), button:has-text('Select this location')",
"amb_mailbox_number_first":"select option:first-child, .mailbox-select option:nth-child(2)",
"amb_continue": "button:has-text('Continue'), button:has-text('Next')",
"amb_otp": "input[name*='otp' i], input[name*='code' i], input[placeholder*='verification' i]",
"amb_otp_submit": "button:has-text('Verify'), button:has-text('Submit')",
"amb_checkout_submit": "button:has-text('Complete'), button:has-text('Subscribe'), button:has-text('Pay')",
# ── Annual Report ─────────────────────────────────────────────────
"ar_filing_year": "select[name*='year' i], input[name*='year' i]",
"ar_confirm_address": "input[type='checkbox'][name*='confirm' i]",
"ar_submit": "input[type='submit'], button[type='submit']",
},
# ------------------------------------------------------------------ #
# Selector verification status
# Tracks which step selectors have been confirmed against the live portal.
# The adapter checks this before running to prevent half-complete filings.
# ------------------------------------------------------------------ #
"COLIN_UNVERIFIED_STEP_SELECTORS": {
# Steps 5-12 need live session verification.
# Remove a step once confirmed to unblock that part of the pipeline.
5: ["inc_director_first", "inc_director_last", "inc_director_addr1"],
6: ["inc_registered_office_street", "inc_records_office_same"],
7: ["inc_share_structure", "inc_table1_adopt"],
9: ["pay_card_number", "pay_card_exp", "pay_card_cvv", "pay_submit"],
12: ["inc_submit", "inc_confirmation_number"],
},
# ------------------------------------------------------------------ #
# Notes
# ------------------------------------------------------------------ #
"notes": (
"BC Business Corporations Act (SBC 2002, c. 57) requirements:\n"
" - Must have at least one director (can be non-resident).\n"
" - Registered office AND records office must be in BC.\n"
" - We use Anytime Mailbox at client's chosen BC location as both.\n"
" - Name reservation is optional but recommended (valid 56 days).\n"
" - Numbered companies do not require a name reservation.\n"
" - Annual Report due within 2 months of anniversary date.\n"
" - CRTC notification required for telecom service providers.\n"
" - All fees in Canadian Dollars (CAD).\n"
" - Corporate Online filing portal is ANONYMOUS — no login required.\n"
" Payment by Visa/MC/Amex at the end of the wizard.\n"
" Use Relay virtual debit card (SID-0002) for filing payment."
),
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,119 @@
"""California — SOS portal automation."""
from __future__ import annotations
from scripts.formation.base import StatePortal, NameSearchResult, FormationOrder, FilingResult, FilingStatus
from .config import CONFIG
class CAPortal(StatePortal):
STATE_CODE = "CA"
STATE_NAME = "California"
PORTAL_NAME = CONFIG["portal_name"]
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 California business name availability."""
try:
page = await self.start_browser()
await page.goto(CONFIG["name_search_url"])
await self.human_delay()
# Type name into search field
sel = CONFIG["selectors"]
if sel["name_search_input"]:
await self.type_slowly(sel["name_search_input"], name)
await self.safe_click(sel["name_search_submit"])
await page.wait_for_load_state("networkidle")
content = await page.content()
available = CONFIG["selectors"]["name_unavailable_indicator"] not in content
return NameSearchResult(
available=available,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=content[:2000],
)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response="Selectors not yet configured for this state",
)
except Exception as e:
self.log.error("Name search failed: %s", e)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=str(e),
)
finally:
await self.close_browser()
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""File LLC in California."""
try:
page = await self.start_browser()
await page.goto(CONFIG["filing_url"])
await self.human_delay()
await self.screenshot("llc_start")
# TODO: Implement California-specific LLC filing flow
# Each state's portal has different form fields, steps, and workflows.
# The selectors in config.py need to be populated by inspecting the portal.
# NOTE: California imposes an annual franchise tax of $800/yr.
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code=self.STATE_CODE,
entity_name=order.entity_name,
error_message="LLC filing automation not yet implemented for California",
screenshot_path=await self.screenshot("llc_not_implemented"),
)
except Exception as e:
self.log.error("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 Corporation in California."""
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="Corporation filing automation not yet implemented for California",
)
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() -> CAPortal:
return CAPortal()

View file

@ -0,0 +1,49 @@
"""California — Secretary of State portal configuration."""
CONFIG = {
"state_code": "CA",
"state_name": "California",
"sos_name": "California Secretary of State",
"portal_name": "California bizfile Online",
"portal_url": "https://sos.ca.gov",
"name_search_url": "https://businesssearch.sos.ca.gov",
"filing_url": "https://bizfileonline.sos.ca.gov",
"search_method": "playwright",
# Socrata API (not applicable)
"socrata_domain": "",
"socrata_dataset_id": "",
# NW Registered Agent address in this state
"nwra_name": "Northwest Registered Agent LLC",
"nwra_address": "1800 S Brand Blvd Ste 201",
"nwra_city": "Glendale",
"nwra_state": "CA",
"nwra_zip": "91204",
# State fees (cents)
"llc_formation_fee": 7000,
"corp_formation_fee": 10000,
"expedited_fee": None,
"expedited_label": "",
# Selectors (Playwright CSS selectors for portal automation)
"selectors": {
"name_search_input": "",
"name_search_submit": "",
"name_results_table": "",
"name_available_indicator": "",
"name_unavailable_indicator": "",
# LLC filing form selectors
"llc_name_field": "",
"llc_agent_name_field": "",
"llc_agent_address_field": "",
"llc_principal_address_field": "",
"llc_organizer_name_field": "",
"llc_management_type_select": "",
"llc_purpose_field": "",
"llc_submit_button": "",
# Corp filing form selectors
"corp_name_field": "",
"corp_agent_name_field": "",
"corp_shares_field": "",
"corp_submit_button": "",
},
"notes": "California imposes an annual franchise tax of $800/yr for LLCs and corporations.",
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,177 @@
"""Colorado — Socrata API for name search, Playwright for filing.
Colorado publishes business entity data on data.colorado.gov via the
Socrata Open Data API (SODA). Dataset ID: 4ykn-tg5h.
This allows name availability searches WITHOUT a headless browser.
Filing still requires Playwright against the SOS web portal.
"""
from __future__ import annotations
import json
import urllib.parse
import urllib.request
from scripts.formation.base import (
StatePortal, NameSearchResult, FormationOrder, FilingResult,
FilingStatus,
)
from .config import CONFIG
SOCRATA_BASE = "https://data.colorado.gov/resource/4ykn-tg5h.json"
class COPortal(StatePortal):
STATE_CODE = "CO"
STATE_NAME = "Colorado"
PORTAL_NAME = CONFIG["portal_name"]
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 Colorado business name availability via Socrata SODA API.
Uses the free REST API at data.colorado.gov no browser, no login,
no rate-limit issues for moderate usage. Returns JSON directly.
"""
try:
# SoQL query: find entities whose name contains our search term
upper_name = name.upper().replace("'", "''")
query = f"$where=upper(entityname) like '%25{urllib.parse.quote(upper_name)}%25'"
query += "&$limit=20&$order=entityformdate DESC"
url = f"{SOCRATA_BASE}?{query}"
self.log.info("CO Socrata API query: %s", url)
req = urllib.request.Request(
url,
headers={"Accept": "application/json", "User-Agent": "PerformanceWest/1.0"},
)
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read().decode("utf-8"))
# Check for exact match (case-insensitive)
exact_match = any(
r.get("entityname", "").upper().strip() == upper_name.strip()
for r in data
)
# Collect similar names
similar_names = [
r.get("entityname", "").strip()
for r in data[:10]
if r.get("entityname", "").strip()
]
available = not exact_match
self.log.info(
"CO name search: '%s'%s (exact_match=%s, similar=%d)",
name, "AVAILABLE" if available else "TAKEN", exact_match, len(similar_names),
)
return NameSearchResult(
available=available,
exact_match=exact_match,
similar_names=similar_names,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=json.dumps(data[:5]),
)
except urllib.error.URLError as e:
self.log.error("CO Socrata API request failed: %s", e)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=f"Socrata API error: {e}",
)
except Exception as e:
self.log.error("CO name search failed: %s", e)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=f"Error: {e}",
)
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""File LLC Articles of Organization in Colorado.
Colorado SOS portal: sos.state.co.us
Filing fee: $50
Online filing is immediate (no processing delay).
TODO: Implement Playwright filing flow.
"""
try:
page = await self.start_browser()
await page.goto(CONFIG["filing_url"])
await self.human_delay()
await self.screenshot("co_llc_start")
# Verify name first via Socrata API (no browser needed)
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 Colorado. "
f"Similar: {', '.join(name_result.similar_names[:5])}",
)
# TODO: Complete filing flow once portal selectors are mapped
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code=self.STATE_CODE,
entity_name=order.entity_name,
error_message="CO LLC filing: name search via API works, "
"filing form selectors pending portal walkthrough",
)
except Exception as e:
self.log.error("CO 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 Colorado ($50)."""
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="CO Corp filing 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() -> COPortal:
return COPortal()

View file

@ -0,0 +1,49 @@
"""Colorado — Secretary of State portal configuration."""
CONFIG = {
"state_code": "CO",
"state_name": "Colorado",
"sos_name": "Colorado Secretary of State",
"portal_name": "Colorado Business Database",
"portal_url": "https://sos.state.co.us",
"name_search_url": "https://sos.state.co.us",
"filing_url": "https://sos.state.co.us",
"search_method": "socrata_api",
# Socrata API
"socrata_domain": "data.colorado.gov",
"socrata_dataset_id": "4ykn-tg5h",
# NW Registered Agent address in this state
"nwra_name": "Northwest Registered Agent LLC",
"nwra_address": "7700 E Arapahoe Rd Ste 110",
"nwra_city": "Centennial",
"nwra_state": "CO",
"nwra_zip": "80112",
# State fees (cents)
"llc_formation_fee": 5000,
"corp_formation_fee": 5000,
"expedited_fee": None,
"expedited_label": "",
# Selectors (Playwright CSS selectors for portal automation)
"selectors": {
"name_search_input": "",
"name_search_submit": "",
"name_results_table": "",
"name_available_indicator": "",
"name_unavailable_indicator": "",
# LLC filing form selectors
"llc_name_field": "",
"llc_agent_name_field": "",
"llc_agent_address_field": "",
"llc_principal_address_field": "",
"llc_organizer_name_field": "",
"llc_management_type_select": "",
"llc_purpose_field": "",
"llc_submit_button": "",
# Corp filing form selectors
"corp_name_field": "",
"corp_agent_name_field": "",
"corp_shares_field": "",
"corp_submit_button": "",
},
"notes": "",
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,118 @@
"""Connecticut — SOTS portal automation."""
from __future__ import annotations
from scripts.formation.base import StatePortal, NameSearchResult, FormationOrder, FilingResult, FilingStatus
from .config import CONFIG
class CTPortal(StatePortal):
STATE_CODE = "CT"
STATE_NAME = "Connecticut"
PORTAL_NAME = CONFIG["portal_name"]
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 Connecticut business name availability."""
try:
page = await self.start_browser()
await page.goto(CONFIG["name_search_url"])
await self.human_delay()
# Type name into search field
sel = CONFIG["selectors"]
if sel["name_search_input"]:
await self.type_slowly(sel["name_search_input"], name)
await self.safe_click(sel["name_search_submit"])
await page.wait_for_load_state("networkidle")
content = await page.content()
available = CONFIG["selectors"]["name_unavailable_indicator"] not in content
return NameSearchResult(
available=available,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=content[:2000],
)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response="Selectors not yet configured for this state",
)
except Exception as e:
self.log.error("Name search failed: %s", e)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=str(e),
)
finally:
await self.close_browser()
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""File LLC in Connecticut."""
try:
page = await self.start_browser()
await page.goto(CONFIG["filing_url"])
await self.human_delay()
await self.screenshot("llc_start")
# TODO: Implement Connecticut-specific LLC filing flow
# Each state's portal has different form fields, steps, and workflows.
# The selectors in config.py need to be populated by inspecting the portal.
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code=self.STATE_CODE,
entity_name=order.entity_name,
error_message="LLC filing automation not yet implemented for Connecticut",
screenshot_path=await self.screenshot("llc_not_implemented"),
)
except Exception as e:
self.log.error("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 Corporation in Connecticut."""
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="Corporation filing automation not yet implemented for Connecticut",
)
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() -> CTPortal:
return CTPortal()

View file

@ -0,0 +1,49 @@
"""Connecticut — Secretary of the State portal configuration."""
CONFIG = {
"state_code": "CT",
"state_name": "Connecticut",
"sos_name": "Connecticut Secretary of the State",
"portal_name": "Connecticut Online Business Search",
"portal_url": "https://portal.ct.gov/sots",
"name_search_url": "https://service.ct.gov/business/s/onlinebusinesssearch",
"filing_url": "https://service.ct.gov/business/s/onlinebusinesssearch",
"search_method": "playwright",
# Socrata API (not applicable)
"socrata_domain": "",
"socrata_dataset_id": "",
# NW Registered Agent address in this state
"nwra_name": "Northwest Registered Agent LLC",
"nwra_address": "40 Old Ridgebury Rd Ste 205",
"nwra_city": "Danbury",
"nwra_state": "CT",
"nwra_zip": "06810",
# State fees (cents)
"llc_formation_fee": 12000,
"corp_formation_fee": 25000,
"expedited_fee": None,
"expedited_label": "",
# Selectors (Playwright CSS selectors for portal automation)
"selectors": {
"name_search_input": "",
"name_search_submit": "",
"name_results_table": "",
"name_available_indicator": "",
"name_unavailable_indicator": "",
# LLC filing form selectors
"llc_name_field": "",
"llc_agent_name_field": "",
"llc_agent_address_field": "",
"llc_principal_address_field": "",
"llc_organizer_name_field": "",
"llc_management_type_select": "",
"llc_purpose_field": "",
"llc_submit_button": "",
# Corp filing form selectors
"corp_name_field": "",
"corp_agent_name_field": "",
"corp_shares_field": "",
"corp_submit_button": "",
},
"notes": "",
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,70 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class DCPortal(StatePortal):
"""District of Columbia DLCP portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the DC business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the DC DLCP ($99).
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the DC DLCP ($99).
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = DCPortal()

View file

@ -0,0 +1,29 @@
CONFIG = {
"state": "DC",
"state_name": "District of Columbia",
"agency": "DLCP",
"agency_name": "Department of Licensing and Consumer Protection",
"portal_url": "https://dcra.dc.gov",
"search_url": "https://corponline.dcra.dc.gov/BizEntity.aspx/Search",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "611 Pennsylvania Ave SE Ste 443",
"city": "Washington",
"state": "DC",
"zip": "20003",
},
"fees": {
"llc": 99,
"corporation": 99,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
"notes": "$300 biennial report.",
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,119 @@
"""Delaware — Division of Corporations portal automation."""
from __future__ import annotations
from scripts.formation.base import StatePortal, NameSearchResult, FormationOrder, FilingResult, FilingStatus
from .config import CONFIG
class DEPortal(StatePortal):
STATE_CODE = "DE"
STATE_NAME = "Delaware"
PORTAL_NAME = CONFIG["portal_name"]
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 Delaware business name availability."""
try:
page = await self.start_browser()
await page.goto(CONFIG["name_search_url"])
await self.human_delay()
# Type name into search field
sel = CONFIG["selectors"]
if sel["name_search_input"]:
await self.type_slowly(sel["name_search_input"], name)
await self.safe_click(sel["name_search_submit"])
await page.wait_for_load_state("networkidle")
content = await page.content()
available = CONFIG["selectors"]["name_unavailable_indicator"] not in content
return NameSearchResult(
available=available,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=content[:2000],
)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response="Selectors not yet configured for this state",
)
except Exception as e:
self.log.error("Name search failed: %s", e)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=str(e),
)
finally:
await self.close_browser()
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""File LLC in Delaware."""
try:
page = await self.start_browser()
await page.goto(CONFIG["filing_url"])
await self.human_delay()
await self.screenshot("llc_start")
# TODO: Implement Delaware-specific LLC filing flow
# Each state's portal has different form fields, steps, and workflows.
# The selectors in config.py need to be populated by inspecting the portal.
# NOTE: Delaware imposes an annual franchise tax of $300/yr for LLCs.
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code=self.STATE_CODE,
entity_name=order.entity_name,
error_message="LLC filing automation not yet implemented for Delaware",
screenshot_path=await self.screenshot("llc_not_implemented"),
)
except Exception as e:
self.log.error("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 Corporation in Delaware."""
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="Corporation filing automation not yet implemented for Delaware",
)
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() -> DEPortal:
return DEPortal()

View file

@ -0,0 +1,68 @@
"""Delaware — Division of Corporations portal configuration."""
CONFIG = {
"state_code": "DE",
"state_name": "Delaware",
"sos_name": "Delaware Division of Corporations",
"portal_name": "Delaware ICIS Entity Search",
"portal_url": "https://corp.delaware.gov",
"name_search_url": "https://icis.corp.delaware.gov/ecorp/entitysearch/namesearch.aspx",
"filing_url": "https://icis.corp.delaware.gov/ecorp/entitysearch",
"search_method": "playwright",
# Socrata API (not applicable)
"socrata_domain": "",
"socrata_dataset_id": "",
# NW Registered Agent address in this state
"nwra_name": "Northwest Registered Agent LLC",
"nwra_address": "8 The Green Ste A",
"nwra_city": "Dover",
"nwra_state": "DE",
"nwra_zip": "19901",
# State fees (cents)
"llc_formation_fee": 11000,
"corp_formation_fee": 8900,
"expedited_fee": 50000,
"expedited_label": "24-hour",
# VERIFIED selectors from live portal HTML (2026-03-19)
"selectors": {
# Name search (namesearch.aspx) — ASP.NET WebForms with __VIEWSTATE
"name_search_input": "#ctl00_ContentPlaceHolder1_frmEntityName",
"file_number_input": "#ctl00_ContentPlaceHolder1_frmFileNumber",
"name_search_submit": "#ctl00_ContentPlaceHolder1_btnSubmit",
"error_label": "#ctl00_ContentPlaceHolder1_lblError",
"error_message": "#ctl00_ContentPlaceHolder1_lblErrorMessage",
"name_results_table": "#tblResults",
"name_available_indicator": "", # No results = name available
"name_unavailable_indicator": "", # Results present = name taken
# CAPTCHA
"captcha_panel": "#ctl00_ContentPlaceHolder1_pnlCaptcha",
"captcha_image_base": "/Ecorp/CaptchaHandler.ashx?type=image&key=",
# Honeypot field (hidden)
"honeypot_field": "input[name='email_confirm']",
# LLC filing form selectors — NOT YET VERIFIED (requires active filing session)
"llc_name_field": "",
"llc_agent_name_field": "",
"llc_agent_address_field": "",
"llc_principal_address_field": "",
"llc_organizer_name_field": "",
"llc_management_type_select": "",
"llc_purpose_field": "",
"llc_submit_button": "",
# Corp filing form selectors
"corp_name_field": "",
"corp_agent_name_field": "",
"corp_shares_field": "",
"corp_submit_button": "",
},
"notes": (
"Delaware imposes an annual franchise tax of $300/yr for LLCs. "
"CRITICAL: Name search has CAPTCHA on every request (image-based, in pnlCaptcha div). "
"Anti-scraping warning on portal: 'The Division of Corporations strictly prohibits mining data. "
"Use of automated tools in any form may result in the suspension of your access.' "
"Need 2captcha or anticaptcha integration for automated name search. "
"Portal uses ASP.NET WebForms with __VIEWSTATE — must maintain session cookies. "
"Hidden honeypot field 'email_confirm' must be left empty. "
"JavaScript cookie 'js_token' set via btoa(Date.now()) required. "
"$5,000 for 1-hour rush, $1,000 for same-day, $500 for 24-hour."
),
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,118 @@
"""Florida — Sunbiz portal automation."""
from __future__ import annotations
from scripts.formation.base import StatePortal, NameSearchResult, FormationOrder, FilingResult, FilingStatus
from .config import CONFIG
class FLPortal(StatePortal):
STATE_CODE = "FL"
STATE_NAME = "Florida"
PORTAL_NAME = CONFIG["portal_name"]
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 Florida business name availability."""
try:
page = await self.start_browser()
await page.goto(CONFIG["name_search_url"])
await self.human_delay()
# Type name into search field
sel = CONFIG["selectors"]
if sel["name_search_input"]:
await self.type_slowly(sel["name_search_input"], name)
await self.safe_click(sel["name_search_submit"])
await page.wait_for_load_state("networkidle")
content = await page.content()
available = CONFIG["selectors"]["name_unavailable_indicator"] not in content
return NameSearchResult(
available=available,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=content[:2000],
)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response="Selectors not yet configured for this state",
)
except Exception as e:
self.log.error("Name search failed: %s", e)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=str(e),
)
finally:
await self.close_browser()
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""File LLC in Florida."""
try:
page = await self.start_browser()
await page.goto(CONFIG["filing_url"])
await self.human_delay()
await self.screenshot("llc_start")
# TODO: Implement Florida-specific LLC filing flow
# Each state's portal has different form fields, steps, and workflows.
# The selectors in config.py need to be populated by inspecting the portal.
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code=self.STATE_CODE,
entity_name=order.entity_name,
error_message="LLC filing automation not yet implemented for Florida",
screenshot_path=await self.screenshot("llc_not_implemented"),
)
except Exception as e:
self.log.error("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 Corporation in Florida."""
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="Corporation filing automation not yet implemented for Florida",
)
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() -> FLPortal:
return FLPortal()

View file

@ -0,0 +1,49 @@
"""Florida — Division of Corporations (Sunbiz) portal configuration."""
CONFIG = {
"state_code": "FL",
"state_name": "Florida",
"sos_name": "Florida Division of Corporations",
"portal_name": "Sunbiz",
"portal_url": "https://sunbiz.org",
"name_search_url": "https://search.sunbiz.org/Inquiry/CorporationSearch/ByName",
"filing_url": "https://sunbiz.org",
"search_method": "sftp_bulk",
# Socrata API (not applicable)
"socrata_domain": "",
"socrata_dataset_id": "",
# NW Registered Agent address in this state
"nwra_name": "Northwest Registered Agent LLC",
"nwra_address": "8 S. Tennessee Ave Ste 104",
"nwra_city": "Lakeland",
"nwra_state": "FL",
"nwra_zip": "33801",
# State fees (cents)
"llc_formation_fee": 12500,
"corp_formation_fee": 7000,
"expedited_fee": None,
"expedited_label": "",
# Selectors (Playwright CSS selectors for portal automation)
"selectors": {
"name_search_input": "",
"name_search_submit": "",
"name_results_table": "",
"name_available_indicator": "",
"name_unavailable_indicator": "",
# LLC filing form selectors
"llc_name_field": "",
"llc_agent_name_field": "",
"llc_agent_address_field": "",
"llc_principal_address_field": "",
"llc_organizer_name_field": "",
"llc_management_type_select": "",
"llc_purpose_field": "",
"llc_submit_button": "",
# Corp filing form selectors
"corp_name_field": "",
"corp_agent_name_field": "",
"corp_shares_field": "",
"corp_submit_button": "",
},
"notes": "",
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,118 @@
"""Georgia — SOS portal automation."""
from __future__ import annotations
from scripts.formation.base import StatePortal, NameSearchResult, FormationOrder, FilingResult, FilingStatus
from .config import CONFIG
class GAPortal(StatePortal):
STATE_CODE = "GA"
STATE_NAME = "Georgia"
PORTAL_NAME = CONFIG["portal_name"]
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 Georgia business name availability."""
try:
page = await self.start_browser()
await page.goto(CONFIG["name_search_url"])
await self.human_delay()
# Type name into search field
sel = CONFIG["selectors"]
if sel["name_search_input"]:
await self.type_slowly(sel["name_search_input"], name)
await self.safe_click(sel["name_search_submit"])
await page.wait_for_load_state("networkidle")
content = await page.content()
available = CONFIG["selectors"]["name_unavailable_indicator"] not in content
return NameSearchResult(
available=available,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=content[:2000],
)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response="Selectors not yet configured for this state",
)
except Exception as e:
self.log.error("Name search failed: %s", e)
return NameSearchResult(
available=False,
state_code=self.STATE_CODE,
searched_name=name,
raw_response=str(e),
)
finally:
await self.close_browser()
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""File LLC in Georgia."""
try:
page = await self.start_browser()
await page.goto(CONFIG["filing_url"])
await self.human_delay()
await self.screenshot("llc_start")
# TODO: Implement Georgia-specific LLC filing flow
# Each state's portal has different form fields, steps, and workflows.
# The selectors in config.py need to be populated by inspecting the portal.
return FilingResult(
success=False,
status=FilingStatus.ERROR,
state_code=self.STATE_CODE,
entity_name=order.entity_name,
error_message="LLC filing automation not yet implemented for Georgia",
screenshot_path=await self.screenshot("llc_not_implemented"),
)
except Exception as e:
self.log.error("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 Corporation in Georgia."""
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="Corporation filing automation not yet implemented for Georgia",
)
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() -> GAPortal:
return GAPortal()

View file

@ -0,0 +1,49 @@
"""Georgia — Secretary of State portal configuration."""
CONFIG = {
"state_code": "GA",
"state_name": "Georgia",
"sos_name": "Georgia Secretary of State",
"portal_name": "Georgia eCorp Business Search",
"portal_url": "https://sos.ga.gov",
"name_search_url": "https://ecorp.sos.ga.gov/BusinessSearch",
"filing_url": "https://ecorp.sos.ga.gov/BusinessSearch",
"search_method": "playwright",
# Socrata API (not applicable)
"socrata_domain": "",
"socrata_dataset_id": "",
# NW Registered Agent address in this state
"nwra_name": "Northwest Registered Agent LLC",
"nwra_address": "2985 Gordy Pkwy Ste 100",
"nwra_city": "Marietta",
"nwra_state": "GA",
"nwra_zip": "30066",
# State fees (cents)
"llc_formation_fee": 11000,
"corp_formation_fee": 11000,
"expedited_fee": None,
"expedited_label": "",
# Selectors (Playwright CSS selectors for portal automation)
"selectors": {
"name_search_input": "",
"name_search_submit": "",
"name_results_table": "",
"name_available_indicator": "",
"name_unavailable_indicator": "",
# LLC filing form selectors
"llc_name_field": "",
"llc_agent_name_field": "",
"llc_agent_address_field": "",
"llc_principal_address_field": "",
"llc_organizer_name_field": "",
"llc_management_type_select": "",
"llc_purpose_field": "",
"llc_submit_button": "",
# Corp filing form selectors
"corp_name_field": "",
"corp_agent_name_field": "",
"corp_shares_field": "",
"corp_submit_button": "",
},
"notes": "",
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class HIPortal(StatePortal):
"""Hawaii Business Registration Division portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Hawaii business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Hawaii BREG.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Hawaii BREG.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = HIPortal()

View file

@ -0,0 +1,28 @@
CONFIG = {
"state": "HI",
"state_name": "Hawaii",
"agency": "BREG",
"agency_name": "Business Registration Division",
"portal_url": "https://cca.hawaii.gov/breg",
"search_url": "https://hbe.ehawaii.gov/documents/search.html",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "1003 Bishop St Ste 1400",
"city": "Honolulu",
"state": "HI",
"zip": "96813",
},
"fees": {
"llc": 50,
"corporation": 50,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,80 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class IAPortal(StatePortal):
"""Iowa Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Iowa business name database.
Uses Socrata open data API (data.iowa.gov) when available,
falls back to the SOS web portal.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
search_method = CONFIG.get("search_method", "web")
if search_method == "socrata":
# TODO: implement Socrata API search against data.iowa.gov
return {"available": False, "results": [], "status": "not yet implemented"}
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Iowa SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Iowa SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = IAPortal()

View file

@ -0,0 +1,30 @@
CONFIG = {
"state": "IA",
"state_name": "Iowa",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://sos.iowa.gov",
"search_url": "https://sos.iowa.gov/search/business/search.aspx",
"search_method": "socrata",
"socrata_domain": "data.iowa.gov",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "1550 2nd Ave SE Ste 200",
"city": "Cedar Rapids",
"state": "IA",
"zip": "52401",
},
"fees": {
"llc": 50,
"corporation": 50,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class IDPortal(StatePortal):
"""Idaho Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Idaho business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Idaho SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Idaho SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = IDPortal()

View file

@ -0,0 +1,28 @@
CONFIG = {
"state": "ID",
"state_name": "Idaho",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://sos.idaho.gov",
"search_url": "https://sosbiz.idaho.gov/search/business",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "5680 E Franklin Rd Ste 250",
"city": "Nampa",
"state": "ID",
"zip": "83687",
},
"fees": {
"llc": 100,
"corporation": 100,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,80 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class ILPortal(StatePortal):
"""Illinois Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Illinois business name database.
Uses Socrata open data API (data.illinois.gov) when available,
falls back to the SOS web portal.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
search_method = CONFIG.get("search_method", "web")
if search_method == "socrata":
# TODO: implement Socrata API search against data.illinois.gov
return {"available": False, "results": [], "status": "not yet implemented"}
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Illinois SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Illinois SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = ILPortal()

View file

@ -0,0 +1,30 @@
CONFIG = {
"state": "IL",
"state_name": "Illinois",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://ilsos.gov",
"search_url": "https://apps.ilsos.gov/corporatellc/CorporateLlcController",
"search_method": "socrata",
"socrata_domain": "data.illinois.gov",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "33 N Dearborn St Ste 1210",
"city": "Chicago",
"state": "IL",
"zip": "60602",
},
"fees": {
"llc": 150,
"corporation": 150,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class INPortal(StatePortal):
"""Indiana Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Indiana business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Indiana SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Indiana SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = INPortal()

View file

@ -0,0 +1,28 @@
CONFIG = {
"state": "IN",
"state_name": "Indiana",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://in.gov/sos",
"search_url": "https://bsd.sos.in.gov/publicbusinesssearch",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "8595 E Washington St Ste 200",
"city": "Indianapolis",
"state": "IN",
"zip": "46219",
},
"fees": {
"llc": 95,
"corporation": 95,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class KSPortal(StatePortal):
"""Kansas Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Kansas business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Kansas SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Kansas SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = KSPortal()

View file

@ -0,0 +1,28 @@
CONFIG = {
"state": "KS",
"state_name": "Kansas",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://sos.ks.gov",
"search_url": "https://www.kansas.gov/bess/flow/main",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "7021 W 79th St",
"city": "Overland Park",
"state": "KS",
"zip": "66204",
},
"fees": {
"llc": 160,
"corporation": 90,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class KYPortal(StatePortal):
"""Kentucky Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Kentucky business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Kentucky SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Kentucky SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = KYPortal()

View file

@ -0,0 +1,28 @@
CONFIG = {
"state": "KY",
"state_name": "Kentucky",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://sos.ky.gov",
"search_url": "https://app.sos.ky.gov/ftsearch",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "4965 US Hwy 42 Ste 1000",
"city": "Louisville",
"state": "KY",
"zip": "40222",
},
"fees": {
"llc": 40,
"corporation": 40,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class LAPortal(StatePortal):
"""Louisiana Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Louisiana business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Louisiana SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Louisiana SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = LAPortal()

View file

@ -0,0 +1,28 @@
CONFIG = {
"state": "LA",
"state_name": "Louisiana",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://sos.la.gov",
"search_url": "https://coraweb.sos.la.gov/commercialsearch/CommercialSearchAnon.aspx",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "1340 Poydras St Ste 1770",
"city": "New Orleans",
"state": "LA",
"zip": "70112",
},
"fees": {
"llc": 100,
"corporation": 75,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,73 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class MAPortal(StatePortal):
"""Massachusetts Secretary of the Commonwealth portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Massachusetts business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Massachusetts SOC.
Note: Massachusetts has the highest LLC filing fee at $500.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Massachusetts SOC.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = MAPortal()

View file

@ -0,0 +1,28 @@
CONFIG = {
"state": "MA",
"state_name": "Massachusetts",
"agency": "SOC",
"agency_name": "Secretary of the Commonwealth",
"portal_url": "https://sec.state.ma.us",
"search_url": "https://corp.sec.state.ma.us/corpweb/CorpSearch/CorpSearch.aspx",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "11 Beacon St Ste 1400",
"city": "Boston",
"state": "MA",
"zip": "02108",
},
"fees": {
"llc": 500,
"corporation": 275,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class MDPortal(StatePortal):
"""Maryland SDAT portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Maryland business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Maryland SDAT.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Maryland SDAT.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = MDPortal()

View file

@ -0,0 +1,28 @@
CONFIG = {
"state": "MD",
"state_name": "Maryland",
"agency": "SDAT",
"agency_name": "State Department of Assessments and Taxation",
"portal_url": "https://dat.maryland.gov",
"search_url": "https://egov.maryland.gov/BusinessExpress/EntitySearch",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "7 Saint Paul St Ste 820",
"city": "Baltimore",
"state": "MD",
"zip": "21202",
},
"fees": {
"llc": 100,
"corporation": 170,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class MEPortal(StatePortal):
"""Maine Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Maine business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Maine SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Maine SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = MEPortal()

View file

@ -0,0 +1,28 @@
CONFIG = {
"state": "ME",
"state_name": "Maine",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://maine.gov/sos",
"search_url": "https://icrs.informe.org/nei-sos-icrs/ICRS",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "20 Danforth St Ste 203",
"city": "Portland",
"state": "ME",
"zip": "04101",
},
"fees": {
"llc": 175,
"corporation": 145,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,80 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class MIPortal(StatePortal):
"""Michigan LARA portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Michigan business name database.
Uses Socrata open data API (data.michigan.gov) when available,
falls back to the LARA web portal.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
search_method = CONFIG.get("search_method", "web")
if search_method == "socrata":
# TODO: implement Socrata API search against data.michigan.gov
return {"available": False, "results": [], "status": "not yet implemented"}
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with Michigan LARA.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with Michigan LARA.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = MIPortal()

View file

@ -0,0 +1,30 @@
CONFIG = {
"state": "MI",
"state_name": "Michigan",
"agency": "LARA",
"agency_name": "Licensing and Regulatory Affairs",
"portal_url": "https://michigan.gov/lara",
"search_url": "https://cofs.lara.state.mi.us/SearchApi/Search/Search",
"search_method": "socrata",
"socrata_domain": "data.michigan.gov",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "2710 Woodward Ave Ste 200",
"city": "Detroit",
"state": "MI",
"zip": "48201",
},
"fees": {
"llc": 50,
"corporation": 60,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class MNPortal(StatePortal):
"""Minnesota Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Minnesota business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Minnesota SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Minnesota SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = MNPortal()

View file

@ -0,0 +1,29 @@
CONFIG = {
"state": "MN",
"state_name": "Minnesota",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://sos.state.mn.us",
"search_url": "https://mblsportal.sos.state.mn.us/Business/Search",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "1000 Washington Ave S Ste 200",
"city": "Minneapolis",
"state": "MN",
"zip": "55415",
},
"fees": {
"llc": 155,
"corporation": 155,
},
"notes": "No RA required. No annual fee.",
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class MOPortal(StatePortal):
"""Missouri Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Missouri business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Missouri SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Missouri SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = MOPortal()

View file

@ -0,0 +1,29 @@
CONFIG = {
"state": "MO",
"state_name": "Missouri",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://sos.mo.gov",
"search_url": "https://bsd.sos.mo.gov/BusinessEntity/BESearch.aspx",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "600 Washington Ave Ste 200",
"city": "St. Louis",
"state": "MO",
"zip": "63101",
},
"fees": {
"llc": 50,
"corporation": 58,
},
"notes": "No annual fees.",
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class MSPortal(StatePortal):
"""Mississippi Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Mississippi business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Mississippi SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Mississippi SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = MSPortal()

View file

@ -0,0 +1,29 @@
CONFIG = {
"state": "MS",
"state_name": "Mississippi",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://sos.ms.gov",
"search_url": "https://corp.sos.ms.gov/corp/portal/c/page/corpBusinessIdSearch/portal.aspx",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "5760 I-55 N Ste 200",
"city": "Jackson",
"state": "MS",
"zip": "39211",
},
"fees": {
"llc": 50,
"corporation": 50,
},
"notes": "No annual report.",
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,116 @@
"""Montana — SOS SOS portal automation.
Name search implemented via the public business entity search.
LLC/Corp filing selectors pending live portal verification.
"""
from __future__ import annotations
import re
from scripts.formation.base import (
StatePortal,
NameSearchResult,
FormationOrder,
FilingResult,
FilingStatus,
)
from .config import CONFIG
class MTPortal(StatePortal):
STATE_CODE = "MT"
STATE_NAME = "Montana"
PORTAL_NAME = "SOS"
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 Montana business name availability via the public portal."""
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)
content = await page.content()
await self.screenshot(page, f"${CODE}_name_search_{name}")
no_results = any(
phrase in content.lower()
for phrase in ["no match", "no results", "no records", "no entities", "0 results"]
)
if no_results:
return NameSearchResult(
available=True, exact_match=False, similar_names=[],
state_code="MT", searched_name=name,
raw_response=content[:2000],
)
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="MT",
searched_name=name, raw_response=content[:2000],
)
except Exception as exc:
return NameSearchResult(
available=False, state_code="MT", searched_name=name,
raw_response=f"Error: {exc}",
)
async def file_llc(self, order: FormationOrder) -> FilingResult:
"""File an LLC in Montana. Selectors pending live portal verification."""
return FilingResult(
success=False, status=FilingStatus.PENDING,
state_code="MT", entity_name=order.entity_name,
error_message=(
"MT filing adapter selectors pending verification. "
f"Admin: file manually at {CONFIG['portal_url']} — LLC formation."
),
)
async def file_corporation(self, order: FormationOrder) -> FilingResult:
"""File a corporation in Montana. Selectors pending live portal verification."""
return FilingResult(
success=False, status=FilingStatus.PENDING,
state_code="MT", entity_name=order.entity_name,
error_message=(
"MT filing adapter selectors pending verification. "
f"Admin: file manually at {CONFIG['portal_url']} — Corp formation."
),
)
def adapter() -> MTPortal:
return MTPortal()

View file

@ -0,0 +1,32 @@
"""Montana Secretary of State — portal configuration."""
CONFIG = {
"state": "Montana",
"abbreviation": "MT",
"agency": "Secretary of State",
"portal_name": "SOS",
"portal_url": "https://sosmt.gov",
"name_search_url": "https://biz.sosmt.gov/search",
"portal_login_required": False,
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "2710 N Montana Ave Ste 201",
"city": "Helena",
"state": "MT",
"zip": "59601",
},
"nwra_address": "2710 N Montana Ave Ste 201",
"nwra_city": "Helena",
"nwra_state": "MT",
"nwra_zip": "59601",
"fees": {
"llc": 35,
"corporation": 70,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"no_results": "",
},
}

View file

@ -0,0 +1,4 @@
from .config import CONFIG
from .adapter import NCPortal
__all__ = ["CONFIG", "NCPortal"]

View file

@ -0,0 +1,22 @@
from scripts.formation.base import StatePortal
from .config import CONFIG
class NCPortal(StatePortal):
"""Adapter for the North Carolina Secretary of State business portal."""
CONFIG = CONFIG
def search_name(self, name: str) -> dict:
"""Search for a business name via the NC SOS corporate search."""
return self._web_search(name)
def file_llc(self, payload: dict) -> dict:
"""File Articles of Organization for a North Carolina LLC ($125)."""
payload.setdefault("fee", CONFIG["fees"]["llc"])
return self._submit_filing("llc", payload)
def file_corporation(self, payload: dict) -> dict:
"""File Articles of Incorporation in North Carolina ($125)."""
payload.setdefault("fee", CONFIG["fees"]["corporation"])
return self._submit_filing("corporation", payload)

View file

@ -0,0 +1,20 @@
CONFIG = {
"state": "North Carolina",
"abbreviation": "NC",
"agency": "Secretary of State",
"agency_url": "https://sosnc.gov",
"search_url": "https://sosnc.gov/search/index/corp",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "8520 Cliff Cameron Dr Ste 106",
"city": "Charlotte",
"state": "NC",
"zip": "28269",
},
"fees": {
"llc": 125,
"corporation": 125,
"annual_report": 200,
},
"notes": "Annual report fee is $200.",
}

View file

@ -0,0 +1,4 @@
from .config import CONFIG
from .adapter import NDPortal
__all__ = ["CONFIG", "NDPortal"]

View file

@ -0,0 +1,22 @@
from scripts.formation.base import StatePortal
from .config import CONFIG
class NDPortal(StatePortal):
"""Adapter for the North Dakota Secretary of State business portal."""
CONFIG = CONFIG
def search_name(self, name: str) -> dict:
"""Search for a business name via the ND FirstStop portal."""
return self._web_search(name)
def file_llc(self, payload: dict) -> dict:
"""File Articles of Organization for a North Dakota LLC ($135)."""
payload.setdefault("fee", CONFIG["fees"]["llc"])
return self._submit_filing("llc", payload)
def file_corporation(self, payload: dict) -> dict:
"""File Articles of Incorporation in North Dakota ($100)."""
payload.setdefault("fee", CONFIG["fees"]["corporation"])
return self._submit_filing("corporation", payload)

View file

@ -0,0 +1,19 @@
CONFIG = {
"state": "North Dakota",
"abbreviation": "ND",
"agency": "Secretary of State",
"agency_url": "https://sos.nd.gov",
"search_url": "https://firststop.sos.nd.gov/search/business",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "1110 College Dr Ste 201",
"city": "Bismarck",
"state": "ND",
"zip": "58501",
},
"fees": {
"llc": 135,
"corporation": 100,
},
"notes": "",
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class NEPortal(StatePortal):
"""Nebraska Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the Nebraska business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the Nebraska SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the Nebraska SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = NEPortal()

View file

@ -0,0 +1,28 @@
CONFIG = {
"state": "NE",
"state_name": "Nebraska",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://sos.nebraska.gov",
"search_url": "https://sos.nebraska.gov/business/corp-search",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "4610 S 74th St Ste 100",
"city": "Lincoln",
"state": "NE",
"zip": "68516",
},
"fees": {
"llc": 100,
"corporation": 100,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class NHPortal(StatePortal):
"""New Hampshire Secretary of State portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the New Hampshire business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with the New Hampshire SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with the New Hampshire SOS.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = NHPortal()

View file

@ -0,0 +1,28 @@
CONFIG = {
"state": "NH",
"state_name": "New Hampshire",
"agency": "SOS",
"agency_name": "Secretary of State",
"portal_url": "https://sos.nh.gov",
"search_url": "https://quickstart.sos.nh.gov/online/BusinessInquire",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "7 Perimeter Rd Ste 201",
"city": "Manchester",
"state": "NH",
"zip": "03103",
},
"fees": {
"llc": 100,
"corporation": 100,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

View file

@ -0,0 +1,2 @@
from .adapter import adapter
from .config import CONFIG

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from playwright.async_api import Page
from scripts.formation.base import StatePortal
from .config import CONFIG
class NJPortal(StatePortal):
"""New Jersey DORES portal adapter."""
config = CONFIG
async def search_name(self, page: Page, name: str) -> dict:
"""Search the New Jersey business name database.
Args:
page: Playwright page instance.
name: Business name to search for.
Returns:
dict with 'available' (bool) and 'results' (list).
"""
await page.goto(CONFIG["search_url"])
# TODO: populate selectors during portal inspection
search_input = CONFIG["selectors"]["search_input"]
search_button = CONFIG["selectors"]["search_button"]
results_table = CONFIG["selectors"]["results_table"]
if search_input:
await page.fill(search_input, name)
if search_button:
await page.click(search_button)
if results_table:
await page.wait_for_selector(results_table)
return {"available": False, "results": [], "status": "not yet implemented"}
async def file_llc(self, page: Page, payload: dict) -> dict:
"""File an LLC formation with New Jersey DORES.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, members.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
async def file_corporation(self, page: Page, payload: dict) -> dict:
"""File a Corporation formation with New Jersey DORES.
Args:
page: Playwright page instance.
payload: Formation data including name, agent, directors.
Returns:
dict with filing confirmation or error details.
"""
await page.goto(CONFIG["portal_url"])
# TODO: implement actual filing flow during portal inspection
return {"filed": False, "status": "not yet implemented"}
adapter = NJPortal()

View file

@ -0,0 +1,28 @@
CONFIG = {
"state": "NJ",
"state_name": "New Jersey",
"agency": "DORES",
"agency_name": "Division of Revenue and Enterprise Services",
"portal_url": "https://njportal.com",
"search_url": "https://njportal.com/dor/businessrecords",
"registered_agent": {
"name": "Northwest Registered Agent",
"street": "60 Park Pl Ste 200",
"city": "Newark",
"state": "NJ",
"zip": "07102",
},
"fees": {
"llc": 125,
"corporation": 125,
},
"selectors": {
"search_input": "",
"search_button": "",
"results_table": "",
"name_field": "",
"agent_name_field": "",
"agent_address_field": "",
"submit_button": "",
},
}

Some files were not shown because too many files have changed in this diff Show more