new-site/scripts/workers/cdr_adapters/freeswitch.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

80 lines
3.2 KiB
Python

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