new-site/scripts/workers/services/flowroute.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

298 lines
9.9 KiB
Python

"""
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