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>
230 lines
9.1 KiB
Python
230 lines
9.1 KiB
Python
"""
|
|
Whitelisted API for Canadian Registry Services.
|
|
|
|
Endpoints:
|
|
file_incorporation(filing_name) — execute a CA Filing Request
|
|
process_pending_filings() — scheduler: process all pending filings
|
|
"""
|
|
import frappe
|
|
import json
|
|
import logging
|
|
|
|
LOG = logging.getLogger("frappe_ca_registry.api")
|
|
|
|
|
|
@frappe.whitelist()
|
|
def file_incorporation(filing_name: str) -> dict:
|
|
"""
|
|
Execute a BC incorporation filing from a CA Filing Request record.
|
|
|
|
Loads the filing request, decrypts the payment card from Sensitive ID,
|
|
dispatches to the correct province adapter, and writes results back.
|
|
|
|
Args:
|
|
filing_name: Name of the CA Filing Request DocType record
|
|
|
|
Returns:
|
|
{"success": bool, "incorporation_number": str, "error": str}
|
|
"""
|
|
filing = frappe.get_doc("CA Filing Request", filing_name)
|
|
|
|
if filing.status not in ("Pending", "Failed"):
|
|
return {"success": False, "error": f"Filing is {filing.status}, not Pending/Failed"}
|
|
|
|
# Load the province adapter
|
|
province = filing.province or "BC"
|
|
from frappe_ca_registry.provinces import get_adapter
|
|
adapter_cls = get_adapter(province)
|
|
if not adapter_cls:
|
|
filing.db_set("status", "Failed")
|
|
filing.db_set("error_message", f"No adapter for province: {province}")
|
|
return {"success": False, "error": f"No adapter for province: {province}"}
|
|
|
|
# Decrypt the filing card from ERPNext Sensitive ID
|
|
card_name = filing.payment_card or "relay-filing-card"
|
|
try:
|
|
card_data = frappe.call(
|
|
"performancewest_erpnext.api.get_filing_card",
|
|
card_name=card_name,
|
|
)
|
|
except Exception as e:
|
|
filing.db_set("status", "Failed")
|
|
filing.db_set("error_message", f"Could not load payment card: {e}")
|
|
return {"success": False, "error": f"Card load failed: {e}"}
|
|
|
|
# Build filing data dict from the DocType fields
|
|
filing_data = {
|
|
"company_type": filing.company_type,
|
|
"name_reservation_number": filing.name_reservation_number or "",
|
|
"trade_name": filing.trade_name or "",
|
|
"effective_date_type": filing.effective_date_type or "Immediately",
|
|
# Director (customer) — used for COLIN Step 6
|
|
"director_first_name": filing.director_first_name,
|
|
"director_middle_name": filing.director_middle_name or "",
|
|
"director_last_name": filing.director_last_name,
|
|
"director_address": filing.director_address or "",
|
|
"director_address2": filing.director_address2 or "",
|
|
"director_city": filing.director_city or "",
|
|
"director_province": filing.director_province or "",
|
|
"director_postal": filing.director_postal or "",
|
|
"director_country": filing.director_country or "US",
|
|
# Director mailing address (if different)
|
|
"director_mailing_different": filing.director_mailing_different or False,
|
|
"director_mailing_street": filing.director_mailing_street or "",
|
|
"director_mailing_city": filing.director_mailing_city or "",
|
|
"director_mailing_province": filing.director_mailing_province or "",
|
|
"director_mailing_postal": filing.director_mailing_postal or "",
|
|
"director_mailing_country": filing.director_mailing_country or "",
|
|
# Additional directors (JSON)
|
|
"additional_directors": json.loads(filing.additional_directors_json or "[]"),
|
|
# Registered office
|
|
"office_address": filing.office_address or "329 Howe St",
|
|
"office_city": filing.office_city or "Vancouver",
|
|
"office_postal": filing.office_postal or "V6C 3N2",
|
|
# Shares
|
|
"share_class": filing.share_class or "Common",
|
|
"shares_authorized": filing.shares_authorized or 100,
|
|
}
|
|
|
|
# Execute the filing
|
|
filing.db_set("status", "In Progress")
|
|
frappe.db.commit()
|
|
|
|
adapter = adapter_cls()
|
|
try:
|
|
result = adapter.file_incorporation(filing_data, card_data)
|
|
except Exception as exc:
|
|
# Catch-all: if the adapter throws an unhandled exception,
|
|
# still set status to Failed so on_update triggers the alert chain.
|
|
from frappe_ca_registry.provinces.bc.adapter import FilingResult
|
|
result = FilingResult(success=False, error=f"Unhandled exception: {exc}")
|
|
frappe.log_error(
|
|
title=f"CA Filing {filing.name} crashed",
|
|
message=frappe.get_traceback(),
|
|
)
|
|
|
|
# Upload screenshots to MinIO and attach privately to the filing record
|
|
order_num = filing.external_order_id or filing.name
|
|
if result.screenshots:
|
|
try:
|
|
minio_paths = adapter.upload_screenshots_to_minio(
|
|
result.screenshots, order_num, filing.name
|
|
)
|
|
result.screenshots = minio_paths # Replace local paths with MinIO paths
|
|
except Exception as e:
|
|
LOG.warning("Screenshot upload failed: %s", e)
|
|
|
|
# Write results back to the DocType
|
|
if result.success:
|
|
filing.db_set("status", "Completed")
|
|
filing.db_set("incorporation_number", result.incorporation_number)
|
|
filing.db_set("company_name_final", result.company_name)
|
|
filing.db_set("filing_confirmation_number", result.confirmation_number)
|
|
filing.db_set("government_fee_paid", result.government_fee)
|
|
filing.db_set("filed_at", result.filed_at)
|
|
filing.db_set("screenshots", json.dumps(result.screenshots))
|
|
|
|
# Update the linked Sales Order if present
|
|
if filing.sales_order:
|
|
try:
|
|
so = frappe.get_doc("Sales Order", filing.sales_order)
|
|
so.db_set("custom_incorporation_number", result.incorporation_number)
|
|
so.db_set("custom_company_name_final", result.company_name)
|
|
frappe.db.commit()
|
|
except Exception as e:
|
|
LOG.warning("Could not update Sales Order %s: %s", filing.sales_order, e)
|
|
|
|
# Update the PG orders table
|
|
if filing.external_order_id:
|
|
try:
|
|
import psycopg2
|
|
import os
|
|
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"UPDATE canada_crtc_orders SET incorporation_number=%s, company_name_final=%s WHERE order_number=%s",
|
|
(result.incorporation_number, result.company_name, filing.external_order_id),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
except Exception as e:
|
|
LOG.warning("Could not update PG order %s: %s", filing.external_order_id, e)
|
|
else:
|
|
filing.db_set("status", "Failed")
|
|
filing.db_set("error_message", result.error)
|
|
filing.db_set("screenshots", json.dumps(result.screenshots))
|
|
|
|
frappe.db.commit()
|
|
|
|
return {
|
|
"success": result.success,
|
|
"incorporation_number": result.incorporation_number,
|
|
"company_name": result.company_name,
|
|
"error": result.error,
|
|
}
|
|
|
|
|
|
@frappe.whitelist()
|
|
def execute_filing(filing_name: str):
|
|
"""Background job wrapper for file_incorporation."""
|
|
return file_incorporation(filing_name)
|
|
|
|
|
|
@frappe.whitelist()
|
|
def get_screenshot(path: str):
|
|
"""
|
|
Serve a filing screenshot from MinIO. Restricted to System Manager.
|
|
|
|
The screenshots are stored as private ERPNext File attachments on the
|
|
CA Filing Request DocType. This endpoint streams the actual PNG from
|
|
MinIO when a System Manager clicks the attachment.
|
|
"""
|
|
if "System Manager" not in frappe.get_roles():
|
|
frappe.throw("Only System Manager can view filing screenshots", frappe.PermissionError)
|
|
|
|
import os
|
|
from minio import Minio
|
|
|
|
minio = Minio(
|
|
f"{os.environ.get('MINIO_ENDPOINT', 'minio')}:{os.environ.get('MINIO_PORT', '9000')}",
|
|
access_key=os.environ.get("MINIO_ACCESS_KEY", ""),
|
|
secret_key=os.environ.get("MINIO_SECRET_KEY", ""),
|
|
secure=os.environ.get("MINIO_SECURE", "false").lower() == "true",
|
|
)
|
|
|
|
try:
|
|
response = minio.get_object("performancewest", path)
|
|
frappe.local.response.filename = path.split("/")[-1]
|
|
frappe.local.response.filecontent = response.read()
|
|
frappe.local.response.type = "download"
|
|
frappe.local.response.display_content_as = "inline"
|
|
except Exception as e:
|
|
frappe.throw(f"Screenshot not found: {e}")
|
|
|
|
|
|
def process_pending_filings():
|
|
"""
|
|
Scheduler: process all pending CA Filing Requests.
|
|
Called every 5 minutes by the scheduler hook.
|
|
"""
|
|
pending = frappe.get_all(
|
|
"CA Filing Request",
|
|
filters={"status": "Pending", "docstatus": 1},
|
|
fields=["name"],
|
|
order_by="creation ASC",
|
|
limit=5,
|
|
)
|
|
|
|
for filing in pending:
|
|
try:
|
|
file_incorporation(filing.name)
|
|
except Exception as e:
|
|
LOG.error("Failed to process filing %s: %s", filing.name, e)
|
|
frappe.log_error(
|
|
title=f"CA Filing scheduler failed: {filing.name}",
|
|
message=frappe.get_traceback(),
|
|
)
|
|
doc = frappe.get_doc("CA Filing Request", filing.name)
|
|
doc.db_set("error_message", str(e)[:500])
|
|
doc.db_set("status", "Failed") # Triggers on_update → _alert_failure
|
|
frappe.db.commit()
|