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
298
scripts/workers/services/flowroute.py
Normal file
298
scripts/workers/services/flowroute.py
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
"""
|
||||
Flowroute Canadian DID provisioning service.
|
||||
|
||||
Production client for searching and purchasing BC DIDs via the Flowroute API.
|
||||
Called by the CRTC pipeline after domain provisioning.
|
||||
|
||||
Environment variables:
|
||||
FLOWROUTE_ACCESS_KEY 743f4c49
|
||||
FLOWROUTE_SECRET_KEY 0b10c6d5...
|
||||
|
||||
Usage:
|
||||
from scripts.workers.services.flowroute import provision_bc_did
|
||||
result = provision_bc_did(order_number="CA-2026-XXXXX")
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
LOG = logging.getLogger("workers.flowroute")
|
||||
|
||||
API_BASE = "https://api.flowroute.com"
|
||||
|
||||
# BC area codes in preference order
|
||||
BC_AREA_CODES = ["1604", "1778", "1236", "1250"]
|
||||
|
||||
_access_key = os.environ.get("FLOWROUTE_ACCESS_KEY", "")
|
||||
_secret_key = os.environ.get("FLOWROUTE_SECRET_KEY", "")
|
||||
|
||||
|
||||
def _session() -> requests.Session:
|
||||
s = requests.Session()
|
||||
s.auth = (_access_key, _secret_key)
|
||||
s.headers["Accept"] = "application/json"
|
||||
return s
|
||||
|
||||
|
||||
def ping() -> bool:
|
||||
"""Verify API credentials."""
|
||||
try:
|
||||
s = _session()
|
||||
r = s.get(f"{API_BASE}/v2/numbers", params={"limit": 1}, timeout=10)
|
||||
ok = r.status_code == 200
|
||||
LOG.info("Flowroute ping: %s", "OK" if ok else f"FAIL {r.status_code}")
|
||||
return ok
|
||||
except Exception as e:
|
||||
LOG.error("Flowroute ping failed: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def search_available_dids(
|
||||
starts_with: str = "",
|
||||
limit: int = 5,
|
||||
number_type: str = "standard",
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Search for available Canadian DIDs across all BC area codes.
|
||||
|
||||
Tries 604 → 778 → 236 → 250 and stops at the first code with results.
|
||||
|
||||
Returns: List of {"did": "16045551234", "rate_center": "...", "monthly_cost": "..."}
|
||||
"""
|
||||
s = _session()
|
||||
prefixes = [starts_with] if starts_with else BC_AREA_CODES
|
||||
|
||||
for prefix in prefixes:
|
||||
try:
|
||||
r = s.get(
|
||||
f"{API_BASE}/v2/numbers/available",
|
||||
params={"starts_with": prefix, "limit": limit, "number_type": number_type},
|
||||
timeout=15,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
LOG.warning("Flowroute search %s: HTTP %d", prefix, r.status_code)
|
||||
continue
|
||||
|
||||
results = []
|
||||
for item in r.json().get("data", []):
|
||||
attrs = item.get("attributes", {})
|
||||
results.append({
|
||||
"did": item.get("id", ""),
|
||||
"rate_center": attrs.get("rate_center", ""),
|
||||
"state": attrs.get("state", ""),
|
||||
"monthly_cost": attrs.get("monthly_cost", ""),
|
||||
})
|
||||
|
||||
if results:
|
||||
LOG.info("Flowroute search '%s': %d results", prefix, len(results))
|
||||
return results
|
||||
|
||||
LOG.info("Flowroute search '%s': 0 results", prefix)
|
||||
|
||||
except Exception as e:
|
||||
LOG.error("Flowroute search error for %s: %s", prefix, e)
|
||||
|
||||
LOG.warning("No BC DIDs available in any area code")
|
||||
return []
|
||||
|
||||
|
||||
def purchase_did(did: str) -> dict:
|
||||
"""
|
||||
Purchase a DID from Flowroute.
|
||||
|
||||
Args:
|
||||
did: Phone number to purchase (e.g. "16045551234")
|
||||
|
||||
Returns:
|
||||
{"success": bool, "did": str, "monthly_cost": str, "error": str}
|
||||
"""
|
||||
s = _session()
|
||||
try:
|
||||
r = s.post(f"{API_BASE}/v2/numbers/{did}", timeout=15)
|
||||
if r.status_code in (200, 201):
|
||||
LOG.info("Flowroute: purchased DID %s", did)
|
||||
return {"success": True, "did": did}
|
||||
else:
|
||||
msg = r.text[:200]
|
||||
LOG.error("Flowroute purchase failed for %s: %d %s", did, r.status_code, msg)
|
||||
return {"success": False, "did": did, "error": msg}
|
||||
except Exception as e:
|
||||
LOG.error("Flowroute purchase error for %s: %s", did, e)
|
||||
return {"success": False, "did": did, "error": str(e)}
|
||||
|
||||
|
||||
def create_route(
|
||||
value: str,
|
||||
route_type: str = "host",
|
||||
alias: str = "",
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Create a SIP route on Flowroute.
|
||||
|
||||
Args:
|
||||
value: SIP URI or IP address (e.g. "sip:trunk@pbx.example.com" or "203.0.113.50")
|
||||
route_type: "host" (SIP) — Flowroute only supports host routes
|
||||
alias: Friendly name for the route
|
||||
|
||||
Returns: Route ID string, or None on failure
|
||||
"""
|
||||
s = _session()
|
||||
try:
|
||||
r = s.post(f"{API_BASE}/v2/routes", json={
|
||||
"data": {
|
||||
"type": "route",
|
||||
"attributes": {
|
||||
"route_type": route_type,
|
||||
"value": value,
|
||||
"alias": alias,
|
||||
},
|
||||
},
|
||||
}, timeout=15)
|
||||
if r.status_code in (200, 201):
|
||||
route_id = r.json()["data"]["id"]
|
||||
LOG.info("Flowroute: created route %s → %s (id=%s)", alias, value, route_id)
|
||||
return route_id
|
||||
else:
|
||||
LOG.error("Flowroute create route failed: %d %s", r.status_code, r.text[:200])
|
||||
return None
|
||||
except Exception as e:
|
||||
LOG.error("Flowroute create route error: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def set_primary_route(did: str, route_id: str) -> bool:
|
||||
"""Assign a route as the primary route for a DID."""
|
||||
s = _session()
|
||||
# Strip leading + if present
|
||||
clean_did = did.lstrip("+")
|
||||
try:
|
||||
r = s.patch(
|
||||
f"{API_BASE}/v2/numbers/{clean_did}/relationships/primary_route",
|
||||
json={"data": {"type": "route", "id": route_id}},
|
||||
timeout=15,
|
||||
)
|
||||
if r.status_code == 204:
|
||||
LOG.info("Flowroute: set primary route for %s → route %s", did, route_id)
|
||||
return True
|
||||
else:
|
||||
LOG.error("Flowroute set route failed for %s: %d %s", did, r.status_code, r.text[:200])
|
||||
return False
|
||||
except Exception as e:
|
||||
LOG.error("Flowroute set route error for %s: %s", did, e)
|
||||
return False
|
||||
|
||||
|
||||
def configure_routing(
|
||||
did: str,
|
||||
routing_type: str = "later",
|
||||
forward_number: str = "",
|
||||
sip_uri: str = "",
|
||||
sip_ip: str = "",
|
||||
order_number: str = "",
|
||||
) -> dict:
|
||||
"""
|
||||
Configure call routing for a purchased DID.
|
||||
|
||||
Flowroute uses SIP "host" routes for all routing. For call forwarding
|
||||
to a PSTN number, the forward number is set as the SIP route value
|
||||
(Flowroute handles the SIP→PSTN gateway internally).
|
||||
|
||||
Args:
|
||||
did: The DID to configure (e.g. "+16045551234")
|
||||
routing_type: "forward", "sip", or "later"
|
||||
forward_number: Phone number to forward to (for routing_type="forward")
|
||||
sip_uri: SIP URI (for routing_type="sip")
|
||||
sip_ip: IP address (for routing_type="sip", fallback)
|
||||
order_number: For logging context
|
||||
|
||||
Returns:
|
||||
{"success": bool, "route_id": str, "error": str}
|
||||
"""
|
||||
if routing_type == "later":
|
||||
LOG.info("[%s] DID %s routing set to 'later' — no routing configured", order_number, did)
|
||||
return {"success": True, "route_id": "", "note": "Customer will configure routing later"}
|
||||
|
||||
if routing_type == "forward" and forward_number:
|
||||
# Create a host route with the forward number
|
||||
# Flowroute routes PSTN calls when value is a phone number
|
||||
clean_fwd = forward_number.lstrip("+").replace("-", "").replace(" ", "")
|
||||
route_id = create_route(
|
||||
value=clean_fwd,
|
||||
alias=f"fwd-{order_number}-{clean_fwd[-4:]}",
|
||||
)
|
||||
if route_id:
|
||||
if set_primary_route(did, route_id):
|
||||
LOG.info("[%s] DID %s forwarding to %s", order_number, did, forward_number)
|
||||
return {"success": True, "route_id": route_id}
|
||||
return {"success": False, "route_id": route_id, "error": "Route created but could not assign to DID"}
|
||||
return {"success": False, "route_id": "", "error": "Could not create forward route"}
|
||||
|
||||
if routing_type == "sip":
|
||||
# Use SIP URI if provided, fall back to IP
|
||||
value = sip_uri or sip_ip
|
||||
if not value:
|
||||
return {"success": False, "route_id": "", "error": "No SIP URI or IP provided"}
|
||||
|
||||
route_id = create_route(
|
||||
value=value,
|
||||
alias=f"sip-{order_number}",
|
||||
)
|
||||
if route_id:
|
||||
if set_primary_route(did, route_id):
|
||||
LOG.info("[%s] DID %s routed to SIP %s", order_number, did, value)
|
||||
return {"success": True, "route_id": route_id}
|
||||
return {"success": False, "route_id": route_id, "error": "Route created but could not assign to DID"}
|
||||
return {"success": False, "route_id": "", "error": "Could not create SIP route"}
|
||||
|
||||
return {"success": False, "route_id": "", "error": f"Unknown routing type: {routing_type}"}
|
||||
|
||||
|
||||
def release_did(did: str) -> bool:
|
||||
"""Release a purchased DID (for test cleanup)."""
|
||||
s = _session()
|
||||
try:
|
||||
r = s.delete(f"{API_BASE}/v2/numbers/{did}", timeout=15)
|
||||
return r.status_code in (200, 204)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def provision_bc_did(order_number: str = "") -> dict:
|
||||
"""
|
||||
Full BC DID provisioning workflow:
|
||||
1. Search for available BC DIDs (tries 604 → 778 → 236 → 250)
|
||||
2. Purchase the first available one
|
||||
3. Return the provisioned DID
|
||||
|
||||
Args:
|
||||
order_number: For logging context
|
||||
|
||||
Returns:
|
||||
{"success": bool, "did": str, "rate_center": str, "monthly_cost": str, "error": str}
|
||||
"""
|
||||
LOG.info("[%s] Provisioning BC DID...", order_number)
|
||||
|
||||
# Step 1: Search
|
||||
available = search_available_dids(limit=3)
|
||||
if not available:
|
||||
return {"success": False, "did": "", "error": "No BC DIDs available in any area code"}
|
||||
|
||||
# Step 2: Purchase the first available
|
||||
target = available[0]
|
||||
result = purchase_did(target["did"])
|
||||
|
||||
if result["success"]:
|
||||
# Format as +1XXXXXXXXXX
|
||||
did = target["did"]
|
||||
if not did.startswith("+"):
|
||||
did = f"+{did}"
|
||||
result["did"] = did
|
||||
result["rate_center"] = target.get("rate_center", "")
|
||||
result["monthly_cost"] = target.get("monthly_cost", "")
|
||||
LOG.info("[%s] Provisioned DID: %s (%s)", order_number, did, target.get("rate_center"))
|
||||
|
||||
return result
|
||||
Loading…
Add table
Add a link
Reference in a new issue