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>
80 lines
3.2 KiB
Python
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
|