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
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."
|
||||
),
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue