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:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
96
scripts/formation/states/__init__.py
Normal file
96
scripts/formation/states/__init__.py
Normal 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
|
||||
2
scripts/formation/states/ak/__init__.py
Normal file
2
scripts/formation/states/ak/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
118
scripts/formation/states/ak/adapter.py
Normal file
118
scripts/formation/states/ak/adapter.py
Normal 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()
|
||||
49
scripts/formation/states/ak/config.py
Normal file
49
scripts/formation/states/ak/config.py
Normal 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": "",
|
||||
}
|
||||
2
scripts/formation/states/al/__init__.py
Normal file
2
scripts/formation/states/al/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
118
scripts/formation/states/al/adapter.py
Normal file
118
scripts/formation/states/al/adapter.py
Normal 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()
|
||||
49
scripts/formation/states/al/config.py
Normal file
49
scripts/formation/states/al/config.py
Normal 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": "",
|
||||
}
|
||||
2
scripts/formation/states/ar/__init__.py
Normal file
2
scripts/formation/states/ar/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
118
scripts/formation/states/ar/adapter.py
Normal file
118
scripts/formation/states/ar/adapter.py
Normal 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()
|
||||
49
scripts/formation/states/ar/config.py
Normal file
49
scripts/formation/states/ar/config.py
Normal 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": "",
|
||||
}
|
||||
2
scripts/formation/states/az/__init__.py
Normal file
2
scripts/formation/states/az/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
119
scripts/formation/states/az/adapter.py
Normal file
119
scripts/formation/states/az/adapter.py
Normal 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()
|
||||
49
scripts/formation/states/az/config.py
Normal file
49
scripts/formation/states/az/config.py
Normal 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.",
|
||||
}
|
||||
4
scripts/formation/states/bc/__init__.py
Normal file
4
scripts/formation/states/bc/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .config import CONFIG
|
||||
from .adapter import BCPortal
|
||||
|
||||
__all__ = ["CONFIG", "BCPortal"]
|
||||
977
scripts/formation/states/bc/adapter.py
Normal file
977
scripts/formation/states/bc/adapter.py
Normal 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()
|
||||
615
scripts/formation/states/bc/config.py
Normal file
615
scripts/formation/states/bc/config.py
Normal 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
|
||||
# Mon–Sat 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."
|
||||
),
|
||||
}
|
||||
2
scripts/formation/states/ca/__init__.py
Normal file
2
scripts/formation/states/ca/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
119
scripts/formation/states/ca/adapter.py
Normal file
119
scripts/formation/states/ca/adapter.py
Normal 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()
|
||||
49
scripts/formation/states/ca/config.py
Normal file
49
scripts/formation/states/ca/config.py
Normal 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.",
|
||||
}
|
||||
2
scripts/formation/states/co/__init__.py
Normal file
2
scripts/formation/states/co/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
177
scripts/formation/states/co/adapter.py
Normal file
177
scripts/formation/states/co/adapter.py
Normal 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()
|
||||
49
scripts/formation/states/co/config.py
Normal file
49
scripts/formation/states/co/config.py
Normal 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": "",
|
||||
}
|
||||
2
scripts/formation/states/ct/__init__.py
Normal file
2
scripts/formation/states/ct/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
118
scripts/formation/states/ct/adapter.py
Normal file
118
scripts/formation/states/ct/adapter.py
Normal 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()
|
||||
49
scripts/formation/states/ct/config.py
Normal file
49
scripts/formation/states/ct/config.py
Normal 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": "",
|
||||
}
|
||||
2
scripts/formation/states/dc/__init__.py
Normal file
2
scripts/formation/states/dc/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
70
scripts/formation/states/dc/adapter.py
Normal file
70
scripts/formation/states/dc/adapter.py
Normal 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()
|
||||
29
scripts/formation/states/dc/config.py
Normal file
29
scripts/formation/states/dc/config.py
Normal 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.",
|
||||
}
|
||||
2
scripts/formation/states/de/__init__.py
Normal file
2
scripts/formation/states/de/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
119
scripts/formation/states/de/adapter.py
Normal file
119
scripts/formation/states/de/adapter.py
Normal 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()
|
||||
68
scripts/formation/states/de/config.py
Normal file
68
scripts/formation/states/de/config.py
Normal 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."
|
||||
),
|
||||
}
|
||||
2
scripts/formation/states/fl/__init__.py
Normal file
2
scripts/formation/states/fl/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
118
scripts/formation/states/fl/adapter.py
Normal file
118
scripts/formation/states/fl/adapter.py
Normal 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()
|
||||
49
scripts/formation/states/fl/config.py
Normal file
49
scripts/formation/states/fl/config.py
Normal 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": "",
|
||||
}
|
||||
2
scripts/formation/states/ga/__init__.py
Normal file
2
scripts/formation/states/ga/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
118
scripts/formation/states/ga/adapter.py
Normal file
118
scripts/formation/states/ga/adapter.py
Normal 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()
|
||||
49
scripts/formation/states/ga/config.py
Normal file
49
scripts/formation/states/ga/config.py
Normal 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": "",
|
||||
}
|
||||
2
scripts/formation/states/hi/__init__.py
Normal file
2
scripts/formation/states/hi/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/hi/adapter.py
Normal file
71
scripts/formation/states/hi/adapter.py
Normal 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()
|
||||
28
scripts/formation/states/hi/config.py
Normal file
28
scripts/formation/states/hi/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/ia/__init__.py
Normal file
2
scripts/formation/states/ia/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
80
scripts/formation/states/ia/adapter.py
Normal file
80
scripts/formation/states/ia/adapter.py
Normal 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()
|
||||
30
scripts/formation/states/ia/config.py
Normal file
30
scripts/formation/states/ia/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/id/__init__.py
Normal file
2
scripts/formation/states/id/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/id/adapter.py
Normal file
71
scripts/formation/states/id/adapter.py
Normal 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()
|
||||
28
scripts/formation/states/id/config.py
Normal file
28
scripts/formation/states/id/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/il/__init__.py
Normal file
2
scripts/formation/states/il/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
80
scripts/formation/states/il/adapter.py
Normal file
80
scripts/formation/states/il/adapter.py
Normal 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()
|
||||
30
scripts/formation/states/il/config.py
Normal file
30
scripts/formation/states/il/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/in/__init__.py
Normal file
2
scripts/formation/states/in/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/in/adapter.py
Normal file
71
scripts/formation/states/in/adapter.py
Normal 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()
|
||||
28
scripts/formation/states/in/config.py
Normal file
28
scripts/formation/states/in/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/ks/__init__.py
Normal file
2
scripts/formation/states/ks/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/ks/adapter.py
Normal file
71
scripts/formation/states/ks/adapter.py
Normal 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()
|
||||
28
scripts/formation/states/ks/config.py
Normal file
28
scripts/formation/states/ks/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/ky/__init__.py
Normal file
2
scripts/formation/states/ky/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/ky/adapter.py
Normal file
71
scripts/formation/states/ky/adapter.py
Normal 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()
|
||||
28
scripts/formation/states/ky/config.py
Normal file
28
scripts/formation/states/ky/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/la/__init__.py
Normal file
2
scripts/formation/states/la/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/la/adapter.py
Normal file
71
scripts/formation/states/la/adapter.py
Normal 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()
|
||||
28
scripts/formation/states/la/config.py
Normal file
28
scripts/formation/states/la/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/ma/__init__.py
Normal file
2
scripts/formation/states/ma/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
73
scripts/formation/states/ma/adapter.py
Normal file
73
scripts/formation/states/ma/adapter.py
Normal 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()
|
||||
28
scripts/formation/states/ma/config.py
Normal file
28
scripts/formation/states/ma/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/md/__init__.py
Normal file
2
scripts/formation/states/md/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/md/adapter.py
Normal file
71
scripts/formation/states/md/adapter.py
Normal 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()
|
||||
28
scripts/formation/states/md/config.py
Normal file
28
scripts/formation/states/md/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/me/__init__.py
Normal file
2
scripts/formation/states/me/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/me/adapter.py
Normal file
71
scripts/formation/states/me/adapter.py
Normal 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()
|
||||
28
scripts/formation/states/me/config.py
Normal file
28
scripts/formation/states/me/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/mi/__init__.py
Normal file
2
scripts/formation/states/mi/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
80
scripts/formation/states/mi/adapter.py
Normal file
80
scripts/formation/states/mi/adapter.py
Normal 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()
|
||||
30
scripts/formation/states/mi/config.py
Normal file
30
scripts/formation/states/mi/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/mn/__init__.py
Normal file
2
scripts/formation/states/mn/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/mn/adapter.py
Normal file
71
scripts/formation/states/mn/adapter.py
Normal 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()
|
||||
29
scripts/formation/states/mn/config.py
Normal file
29
scripts/formation/states/mn/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/mo/__init__.py
Normal file
2
scripts/formation/states/mo/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/mo/adapter.py
Normal file
71
scripts/formation/states/mo/adapter.py
Normal 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()
|
||||
29
scripts/formation/states/mo/config.py
Normal file
29
scripts/formation/states/mo/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/ms/__init__.py
Normal file
2
scripts/formation/states/ms/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/ms/adapter.py
Normal file
71
scripts/formation/states/ms/adapter.py
Normal 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()
|
||||
29
scripts/formation/states/ms/config.py
Normal file
29
scripts/formation/states/ms/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/mt/__init__.py
Normal file
2
scripts/formation/states/mt/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
116
scripts/formation/states/mt/adapter.py
Normal file
116
scripts/formation/states/mt/adapter.py
Normal 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()
|
||||
32
scripts/formation/states/mt/config.py
Normal file
32
scripts/formation/states/mt/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
4
scripts/formation/states/nc/__init__.py
Normal file
4
scripts/formation/states/nc/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .config import CONFIG
|
||||
from .adapter import NCPortal
|
||||
|
||||
__all__ = ["CONFIG", "NCPortal"]
|
||||
22
scripts/formation/states/nc/adapter.py
Normal file
22
scripts/formation/states/nc/adapter.py
Normal 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)
|
||||
20
scripts/formation/states/nc/config.py
Normal file
20
scripts/formation/states/nc/config.py
Normal 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.",
|
||||
}
|
||||
4
scripts/formation/states/nd/__init__.py
Normal file
4
scripts/formation/states/nd/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .config import CONFIG
|
||||
from .adapter import NDPortal
|
||||
|
||||
__all__ = ["CONFIG", "NDPortal"]
|
||||
22
scripts/formation/states/nd/adapter.py
Normal file
22
scripts/formation/states/nd/adapter.py
Normal 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)
|
||||
19
scripts/formation/states/nd/config.py
Normal file
19
scripts/formation/states/nd/config.py
Normal 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": "",
|
||||
}
|
||||
2
scripts/formation/states/ne/__init__.py
Normal file
2
scripts/formation/states/ne/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/ne/adapter.py
Normal file
71
scripts/formation/states/ne/adapter.py
Normal 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()
|
||||
28
scripts/formation/states/ne/config.py
Normal file
28
scripts/formation/states/ne/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/nh/__init__.py
Normal file
2
scripts/formation/states/nh/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/nh/adapter.py
Normal file
71
scripts/formation/states/nh/adapter.py
Normal 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()
|
||||
28
scripts/formation/states/nh/config.py
Normal file
28
scripts/formation/states/nh/config.py
Normal 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": "",
|
||||
},
|
||||
}
|
||||
2
scripts/formation/states/nj/__init__.py
Normal file
2
scripts/formation/states/nj/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .adapter import adapter
|
||||
from .config import CONFIG
|
||||
71
scripts/formation/states/nj/adapter.py
Normal file
71
scripts/formation/states/nj/adapter.py
Normal 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()
|
||||
28
scripts/formation/states/nj/config.py
Normal file
28
scripts/formation/states/nj/config.py
Normal 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
Loading…
Add table
Add a link
Reference in a new issue