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>
179 lines
6.9 KiB
Python
179 lines
6.9 KiB
Python
"""CABS BOS (Billing Output Specification) adapter.
|
|
|
|
Parses the fixed-width record-oriented CABS BOS format used by ILECs and
|
|
interexchange carriers to bill switched- and special-access revenue to
|
|
interconnecting carriers. The CABS BOS (Telcordia / Bellcore BR-902-900-
|
|
000) carries many record types; this adapter is a **best-effort**
|
|
interpreter of the most common ones.
|
|
|
|
Supported record types
|
|
----------------------
|
|
``01`` Header — billing company + bill period (context only)
|
|
``10`` Switched-Access Revenue detail
|
|
positions 5..14 OCN of interconnecting carrier (10 chars)
|
|
positions 15..20 Billing period YYMMDD (period-end)
|
|
positions 50..59 Minutes of use (right-justified, 10 digits)
|
|
positions 60..69 Revenue, implied 2 decimals (10 digits)
|
|
``20`` Special-Access Revenue detail
|
|
positions 5..14 OCN
|
|
positions 15..20 Billing period YYMMDD
|
|
positions 40..59 Circuit identifier (free-form)
|
|
positions 60..69 Revenue, implied 2 decimals
|
|
``90`` Trailer — record counts (context only)
|
|
|
|
Deferred / not parsed
|
|
---------------------
|
|
* USOC / rate-element detail lines (record 15, 25)
|
|
* Adjustments (record 40) and taxes (record 50)
|
|
* End-Office-vs-Tandem jurisdictional apportionment on record 10 —
|
|
handled downstream by ``icc_499a_line_mapping.jurisdiction_split``
|
|
* Multi-line circuit detail continuation records (11, 21)
|
|
|
|
Real CABS BOS files have many carrier-specific variants; downstream jobs
|
|
that need rate-element granularity should fall back to the raw PDF/EDI
|
|
or request an ASR-formatted companion file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Iterator
|
|
|
|
from .common import BaseICCAdapter, IccRevenueLine, ValidationError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CABSBOSAdapter(BaseICCAdapter):
|
|
SOURCE_FORMAT = "cabs_bos"
|
|
|
|
# 1-based column slices drawn from the Telcordia BR-902-900-000 layout.
|
|
# Subclass tuples are (start, end_inclusive) in human positions; we
|
|
# convert to 0-based half-open slices via ``_slice()``.
|
|
_RECTYPE = (1, 2)
|
|
_OCN = (5, 14)
|
|
_BILL_PERIOD = (15, 20)
|
|
_MOU_R10 = (50, 59)
|
|
_REVENUE_R10 = (60, 69)
|
|
_CIRCUIT_R20 = (40, 59)
|
|
_REVENUE_R20 = (60, 69)
|
|
|
|
@staticmethod
|
|
def _slice(line: str, span: tuple[int, int]) -> str:
|
|
start, end = span
|
|
return line[start - 1:end]
|
|
|
|
@staticmethod
|
|
def _period_to_quarter(period_yymmdd: str) -> int | None:
|
|
"""Convert YYMMDD billing-period-end into a 1..4 quarter index."""
|
|
if not period_yymmdd or len(period_yymmdd) < 4:
|
|
return None
|
|
try:
|
|
month = int(period_yymmdd[2:4])
|
|
except ValueError:
|
|
return None
|
|
if 1 <= month <= 3:
|
|
return 1
|
|
if 4 <= month <= 6:
|
|
return 2
|
|
if 7 <= month <= 9:
|
|
return 3
|
|
if 10 <= month <= 12:
|
|
return 4
|
|
return None
|
|
|
|
@classmethod
|
|
def _implied_cents(cls, raw: str) -> int:
|
|
"""CABS BOS revenue fields are zero-padded 10-digit integers with
|
|
an implied 2-decimal scale, so '0000123456' → $1,234.56 → 123456 cents.
|
|
"""
|
|
cleaned = raw.strip()
|
|
if not cleaned:
|
|
return 0
|
|
# Handle explicit sign in leading position (some carriers use it)
|
|
negative = False
|
|
if cleaned[0] in "+-":
|
|
negative = cleaned[0] == "-"
|
|
cleaned = cleaned[1:]
|
|
if not cleaned.isdigit():
|
|
raise ValidationError("bad_revenue", f"CABS revenue non-numeric: {raw!r}")
|
|
cents = int(cleaned)
|
|
return -cents if negative else cents
|
|
|
|
def iter_rows(self, local_path: str) -> Iterator[IccRevenueLine]:
|
|
with open(local_path, "r", encoding="latin-1", errors="replace") as fh:
|
|
for lineno, line in enumerate(fh, start=1):
|
|
# Strip CRLF but keep trailing spaces that the fixed-width
|
|
# layout relies on.
|
|
stripped = line.rstrip("\r\n")
|
|
if len(stripped) < 4:
|
|
continue
|
|
rectype = self._slice(stripped, self._RECTYPE).strip()
|
|
try:
|
|
if rectype == "10":
|
|
yield self._parse_record_10(stripped, lineno)
|
|
elif rectype == "20":
|
|
yield self._parse_record_20(stripped, lineno)
|
|
# 01 / 90 / others → context-only, skipped
|
|
except ValidationError as ve:
|
|
logger.warning(
|
|
"CABS BOS row %d rejected (%s): %s",
|
|
lineno, ve.reason_code, ve.detail,
|
|
)
|
|
raise
|
|
|
|
def _parse_record_10(self, line: str, lineno: int) -> IccRevenueLine:
|
|
ocn = self._slice(line, self._OCN).strip() or None
|
|
period = self._slice(line, self._BILL_PERIOD).strip()
|
|
mou_raw = self._slice(line, self._MOU_R10).strip()
|
|
rev_raw = self._slice(line, self._REVENUE_R10)
|
|
try:
|
|
mou = self.parse_int(mou_raw) if mou_raw else None
|
|
except ValidationError:
|
|
mou = None
|
|
return IccRevenueLine(
|
|
profile_id=self.profile_id,
|
|
reporting_year=self.reporting_year,
|
|
reporting_quarter=self._period_to_quarter(period),
|
|
icc_category="term_switched_access",
|
|
counterparty_legal_name=ocn or "UNKNOWN",
|
|
counterparty_ocn=ocn,
|
|
counterparty_country="US",
|
|
revenue_cents=self._implied_cents(rev_raw),
|
|
minutes_of_use=mou,
|
|
source_line_no=lineno,
|
|
raw_row={
|
|
"rectype": "10",
|
|
"ocn": ocn,
|
|
"period": period,
|
|
"mou_raw": mou_raw,
|
|
"revenue_raw": rev_raw,
|
|
"line": line,
|
|
},
|
|
)
|
|
|
|
def _parse_record_20(self, line: str, lineno: int) -> IccRevenueLine:
|
|
ocn = self._slice(line, self._OCN).strip() or None
|
|
period = self._slice(line, self._BILL_PERIOD).strip()
|
|
circuit = self._slice(line, self._CIRCUIT_R20).strip()
|
|
rev_raw = self._slice(line, self._REVENUE_R20)
|
|
return IccRevenueLine(
|
|
profile_id=self.profile_id,
|
|
reporting_year=self.reporting_year,
|
|
reporting_quarter=self._period_to_quarter(period),
|
|
icc_category="special_access",
|
|
counterparty_legal_name=ocn or "UNKNOWN",
|
|
counterparty_ocn=ocn,
|
|
counterparty_country="US",
|
|
revenue_cents=self._implied_cents(rev_raw),
|
|
minutes_of_use=None, # special-access is not MOU-billed
|
|
source_line_no=lineno,
|
|
raw_row={
|
|
"rectype": "20",
|
|
"ocn": ocn,
|
|
"period": period,
|
|
"circuit": circuit,
|
|
"revenue_raw": rev_raw,
|
|
"line": line,
|
|
},
|
|
)
|