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
80
scripts/workers/cdr_adapters/freeswitch.py
Normal file
80
scripts/workers/cdr_adapters/freeswitch.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
"""FreeSWITCH CDR adapter.
|
||||
|
||||
Handles the standard ``mod_cdr_csv`` output format:
|
||||
|
||||
"caller_id_name","caller_id_number","destination_number","context",
|
||||
"start_stamp","answer_stamp","end_stamp","duration","billsec",
|
||||
"hangup_cause","uuid","bleg_uuid","accountcode"
|
||||
|
||||
Billed amount is populated via ``mod_nibblebill`` when installed — the
|
||||
additional columns ``nibble_total_billed`` / ``nibble_bill_amount`` /
|
||||
``nibble_rate`` land in the same CSV. We pick them up when present.
|
||||
|
||||
``uuid`` is FreeSWITCH's unique call identifier and makes a perfect
|
||||
natural key for dedup.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import logging
|
||||
from typing import Iterator
|
||||
|
||||
from .base import BaseCDRAdapter, CDRRow, ValidationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FreeSWITCHAdapter(BaseCDRAdapter):
|
||||
FORMAT_SLUG = "freeswitch"
|
||||
|
||||
# Columns we check for per-call billed amount (mod_nibblebill output)
|
||||
_BILLED_COLUMNS = (
|
||||
"nibble_total_billed",
|
||||
"nibble_bill_amount",
|
||||
"billed_amount",
|
||||
"total_charge",
|
||||
"charge",
|
||||
)
|
||||
|
||||
def iter_rows(self, local_path: str) -> Iterator[CDRRow]:
|
||||
with open(local_path, "r", encoding="utf-8", errors="replace", newline="") as fh:
|
||||
reader = csv.DictReader(fh)
|
||||
for i, raw in enumerate(reader, start=1):
|
||||
try:
|
||||
start_raw = raw.get("start_stamp") or raw.get("answer_stamp")
|
||||
start = self.parse_ts(start_raw)
|
||||
duration_raw = raw.get("billsec") or raw.get("duration") or "0"
|
||||
duration = self.parse_duration(duration_raw)
|
||||
caller = (raw.get("caller_id_number") or "").strip()
|
||||
called = (raw.get("destination_number") or "").strip()
|
||||
uuid = (raw.get("uuid") or "").strip()
|
||||
|
||||
billed_cents = None
|
||||
for col in self._BILLED_COLUMNS:
|
||||
if raw.get(col):
|
||||
billed_cents = self.parse_cents(raw[col])
|
||||
if billed_cents is not None:
|
||||
break
|
||||
|
||||
row = CDRRow(
|
||||
start_time=start,
|
||||
caller_number=caller,
|
||||
called_number=called,
|
||||
duration_sec=duration,
|
||||
billed_amount_cents=billed_cents,
|
||||
billed_currency=("USD" if billed_cents is not None else None),
|
||||
trunk_group_id=(raw.get("context") or "").strip() or None,
|
||||
customer_account_id=(raw.get("accountcode") or "").strip() or None,
|
||||
disposition=(raw.get("hangup_cause") or "").strip().lower() or None,
|
||||
natural_key=uuid or f"{caller}|{called}|{start.isoformat()}|{duration}",
|
||||
source_file=local_path,
|
||||
source_row=i,
|
||||
raw=dict(raw),
|
||||
)
|
||||
self.validate_row(row)
|
||||
yield row
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise ValidationError("unparseable_row", str(exc)) from exc
|
||||
Loading…
Add table
Add a link
Reference in a new issue