new-site/frappe_ca_registry/frappe_ca_registry/api.py
justin f8cd37ac8c 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>
2026-04-27 06:54:22 -05:00

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()