feat(parsers+api+ui): expose CARC-labeled CAS adjustments (SP3 P2)
This commit is contained in:
@@ -637,3 +637,172 @@ def test_post_unmatch_removes_match(client: TestClient):
|
||||
assert body["claim"]["matchedRemittanceId"] is None
|
||||
# No Match rows existed before the call → 0 deleted.
|
||||
assert body["deletedMatches"] == 0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# /api/remittances/{remittance_id} (T7 / SP3 P2)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_get_remittance_404_when_missing(client: TestClient) -> None:
|
||||
"""A missing remittance returns 404 (not 500)."""
|
||||
resp = client.get("/api/remittances/does-not-exist")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def _seed_835_remittance(
|
||||
*,
|
||||
pcns: list[str],
|
||||
cas_rows: dict[str, list[tuple[str, str, str, str]]] | None = None,
|
||||
status_code: str = "1",
|
||||
total_charge: str = "100",
|
||||
total_paid: str = "100",
|
||||
) -> None:
|
||||
"""Helper: insert one 835 batch with one ClaimPayment per PCN.
|
||||
|
||||
``cas_rows`` maps ``pcn -> [(group, reason, amount, qty_or_None), ...]``
|
||||
so a single CAS row maps cleanly to ``CasAdjustment``.
|
||||
"""
|
||||
from datetime import date, datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
from cyclone.parsers.models_835 import (
|
||||
BatchSummary as BatchSummary835,
|
||||
ClaimAdjustment as ClaimAdjustment835,
|
||||
ClaimPayment,
|
||||
Envelope as Envelope835,
|
||||
FinancialInfo,
|
||||
Payer835,
|
||||
ParseResult835,
|
||||
Payee835,
|
||||
ReassociationTrace,
|
||||
ServicePayment,
|
||||
)
|
||||
from cyclone.store import BatchRecord835
|
||||
|
||||
claims = []
|
||||
for pcn in pcns:
|
||||
adjustments: list[ClaimAdjustment835] = []
|
||||
rows = (cas_rows or {}).get(pcn, [])
|
||||
for group, reason, amount, qty in rows:
|
||||
adjustments.append(ClaimAdjustment835(
|
||||
group_code=group, reason_code=reason,
|
||||
amount=Decimal(amount),
|
||||
quantity=Decimal(qty) if qty else None,
|
||||
))
|
||||
sp = ServicePayment(
|
||||
line_number=1,
|
||||
procedure_qualifier="HC",
|
||||
procedure_code="T1019",
|
||||
modifiers=[],
|
||||
charge=Decimal(total_charge),
|
||||
payment=Decimal(total_paid),
|
||||
units=Decimal("1"),
|
||||
unit_type="UN",
|
||||
adjustments=adjustments,
|
||||
)
|
||||
claims.append(ClaimPayment(
|
||||
payer_claim_control_number=pcn,
|
||||
status_code=status_code,
|
||||
total_charge=Decimal(total_charge),
|
||||
total_paid=Decimal(total_paid),
|
||||
service_payments=[sp] if rows else [],
|
||||
))
|
||||
|
||||
global_store.add(BatchRecord835(
|
||||
id=f"b-835-{pcns[0]}", kind="835", input_filename="era.txt",
|
||||
parsed_at=datetime(2026, 6, 19, 12, 5, tzinfo=timezone.utc),
|
||||
result=ParseResult835(
|
||||
envelope=Envelope835(
|
||||
sender_id="S", receiver_id="R", control_number="0001",
|
||||
transaction_date=date(2026, 6, 19),
|
||||
),
|
||||
financial_info=FinancialInfo(
|
||||
handling_code="C", paid_amount=Decimal(total_paid),
|
||||
credit_debit_flag="C", payment_method=None,
|
||||
),
|
||||
trace=ReassociationTrace(
|
||||
trace_type_code="1", trace_number="0001",
|
||||
originating_company_id="S",
|
||||
),
|
||||
payer=Payer835(name="Colorado Medicaid", id="SKCO0"),
|
||||
payee=Payee835(name="TOC, Inc.", npi="1881068062"),
|
||||
claims=claims,
|
||||
summary=BatchSummary835(
|
||||
input_file="era.txt", control_number="0001",
|
||||
transaction_date=date(2026, 6, 19),
|
||||
total_claims=len(claims), passed=len(claims), failed=0,
|
||||
),
|
||||
),
|
||||
))
|
||||
|
||||
|
||||
def test_get_remittance_with_empty_adjustments(client: TestClient) -> None:
|
||||
"""A remittance with no CAS rows returns ``adjustments == []``."""
|
||||
_seed_835_remittance(pcns=["PCN-EMPTY"])
|
||||
resp = client.get("/api/remittances/PCN-EMPTY")
|
||||
assert resp.status_code == 200, resp.text
|
||||
body = resp.json()
|
||||
assert body["id"] == "PCN-EMPTY"
|
||||
assert body["adjustments"] == []
|
||||
|
||||
|
||||
def test_get_remittance_with_adjustments_returns_labeled(client: TestClient) -> None:
|
||||
"""A remittance with two CAS rows surfaces both with the correct labels.
|
||||
|
||||
Seeds CO-45 (the fee-schedule code) and PR-1 (deductible) so we
|
||||
exercise both a CO group code and a PR group code, plus both a
|
||||
quantified and non-quantified row.
|
||||
"""
|
||||
_seed_835_remittance(
|
||||
pcns=["PCN-LABELED"],
|
||||
cas_rows={
|
||||
"PCN-LABELED": [
|
||||
("CO", "45", "50.00", ""), # no quantity
|
||||
("PR", "1", "10.00", "1"), # quantified
|
||||
],
|
||||
},
|
||||
)
|
||||
resp = client.get("/api/remittances/PCN-LABELED")
|
||||
assert resp.status_code == 200, resp.text
|
||||
body = resp.json()
|
||||
assert body["id"] == "PCN-LABELED"
|
||||
adjs = body["adjustments"]
|
||||
assert len(adjs) == 2
|
||||
|
||||
by_key = {(a["group"], a["reason"]): a for a in adjs}
|
||||
assert ("CO", "45") in by_key
|
||||
assert ("PR", "1") in by_key
|
||||
|
||||
co45 = by_key[("CO", "45")]
|
||||
assert co45["amount"] == 50.00
|
||||
assert co45["quantity"] is None
|
||||
# Label is non-empty and isn't the "Unknown" fallback (both are in the dict).
|
||||
assert co45["label"] and "Unknown" not in co45["label"]
|
||||
|
||||
pr1 = by_key[("PR", "1")]
|
||||
assert pr1["amount"] == 10.00
|
||||
assert pr1["quantity"] == 1
|
||||
assert pr1["label"] and "Unknown" not in pr1["label"]
|
||||
|
||||
# The CO and PR rows must have distinct labels (different codes → different meanings).
|
||||
assert co45["label"] != pr1["label"]
|
||||
|
||||
|
||||
def test_get_remittance_basic_fields_preserved(client: TestClient) -> None:
|
||||
"""claimId, payerName, paidAmount, status all surface unchanged."""
|
||||
_seed_835_remittance(
|
||||
pcns=["PCN-FIELDS"],
|
||||
cas_rows={"PCN-FIELDS": [("CO", "45", "12.34", "")]},
|
||||
total_paid="75.50",
|
||||
)
|
||||
resp = client.get("/api/remittances/PCN-FIELDS")
|
||||
assert resp.status_code == 200, resp.text
|
||||
body = resp.json()
|
||||
assert body["id"] == "PCN-FIELDS"
|
||||
assert body["payerName"] == "Colorado Medicaid"
|
||||
assert body["paidAmount"] == 75.50
|
||||
assert body["status"] == "received"
|
||||
assert body["batchId"] # populated; not pinned to a specific value
|
||||
assert body["receivedDate"] # populated
|
||||
assert body["parsedAt"] # populated
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Tests for :mod:`cyclone.parsers.cas_codes`.
|
||||
|
||||
Covers the public API surface:
|
||||
|
||||
- ``LAST_UPDATED`` module constant (parses as YYYY-MM-DD).
|
||||
- :func:`reason_label` — known codes return their label, unknown codes
|
||||
fall back to ``"Unknown ({group}-{reason})"``.
|
||||
- :func:`all_known_codes` — at least 100 entries, sorted, no duplicate
|
||||
``(group, reason)`` pairs.
|
||||
|
||||
The dict is a snapshot of CARC (Claim Adjustment Reason Codes); tests
|
||||
are deliberately decoupled from the exact labels (they change as
|
||||
payers revise codes) but pin down the API contract.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from cyclone.parsers import cas_codes
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# LAST_UPDATED
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_last_updated_constant() -> None:
|
||||
"""``LAST_UPDATED`` is a string that parses as YYYY-MM-DD."""
|
||||
assert isinstance(cas_codes.LAST_UPDATED, str)
|
||||
parsed = date.fromisoformat(cas_codes.LAST_UPDATED)
|
||||
assert parsed.isoformat() == cas_codes.LAST_UPDATED
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# reason_label — known codes
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_reason_label_known_co_45() -> None:
|
||||
"""CO-45 ("Charge exceeds fee schedule…") is the most-common CARC."""
|
||||
label = cas_codes.reason_label("CO", "45")
|
||||
assert isinstance(label, str)
|
||||
assert label.strip() # non-empty
|
||||
# We don't pin the exact text (carriers revise wording); the contract
|
||||
# is "non-empty and not the unknown-fallback".
|
||||
assert "Unknown" not in label
|
||||
|
||||
|
||||
def test_reason_label_known_pr_1() -> None:
|
||||
"""PR-1 ("Deductible amount") is the bread-and-butter PR code."""
|
||||
label = cas_codes.reason_label("PR", "1")
|
||||
assert isinstance(label, str)
|
||||
assert label.strip()
|
||||
assert "Unknown" not in label
|
||||
|
||||
|
||||
def test_reason_label_returns_specific_label_for_co_97() -> None:
|
||||
"""CO-97 is the bundled-payment / included-in-payment code; pins
|
||||
that distinct codes map to distinct labels (not all the same
|
||||
fallback string)."""
|
||||
co45 = cas_codes.reason_label("CO", "45")
|
||||
co97 = cas_codes.reason_label("CO", "97")
|
||||
# Both are non-fallback but should not be identical text.
|
||||
assert co45 != co97
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# reason_label — unknown fallback
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_reason_label_unknown_returns_fallback() -> None:
|
||||
"""Unknown (group, reason) pairs return ``"Unknown ({group}-{reason})"``."""
|
||||
assert cas_codes.reason_label("XX", "999") == "Unknown (XX-999)"
|
||||
# Even a known group with an unknown reason falls back.
|
||||
assert cas_codes.reason_label("CO", "9999") == "Unknown (CO-9999)"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# all_known_codes — shape + invariants
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_all_known_codes_count_ge_100() -> None:
|
||||
"""We need a useful snapshot — at least 100 codes."""
|
||||
entries = cas_codes.all_known_codes()
|
||||
assert len(entries) >= 100
|
||||
|
||||
|
||||
def test_all_known_codes_sorted_unique() -> None:
|
||||
"""Each entry is ``(group, reason, label)``; pairs are unique; result sorted."""
|
||||
entries = cas_codes.all_known_codes()
|
||||
# Shape: every entry is a 3-tuple of strings.
|
||||
for entry in entries:
|
||||
assert len(entry) == 3
|
||||
group, reason, label = entry
|
||||
assert isinstance(group, str) and group
|
||||
assert isinstance(reason, str) and reason
|
||||
assert isinstance(label, str) and label
|
||||
|
||||
# Unique (group, reason) pairs.
|
||||
pairs = [(g, r) for g, r, _ in entries]
|
||||
assert len(pairs) == len(set(pairs))
|
||||
|
||||
# Sorted by (group, reason) lexicographically.
|
||||
assert entries == sorted(entries)
|
||||
|
||||
|
||||
def test_all_known_codes_includes_required_minimum_set() -> None:
|
||||
"""Pin the spec's required codes so a future dict swap that drops them
|
||||
is caught here, not in production."""
|
||||
entries = cas_codes.all_known_codes()
|
||||
keys = {(g, r) for g, r, _ in entries}
|
||||
required = {
|
||||
("CO", "45"), ("CO", "97"), ("CO", "16"), ("CO", "29"),
|
||||
("CO", "50"), ("CO", "109"), ("CO", "119"), ("CO", "197"),
|
||||
("CO", "204"), ("CO", "B7"), ("CO", "251"),
|
||||
("PR", "1"), ("PR", "2"), ("PR", "3"), ("PR", "204"),
|
||||
("OA", "23"), ("OA", "97"),
|
||||
("PI", "16"), ("PI", "21"), ("PI", "22"), ("PI", "25"),
|
||||
("CR", "1"), ("CR", "2"), ("CR", "3"), ("CR", "4"),
|
||||
}
|
||||
missing = required - keys
|
||||
assert not missing, f"missing required CARC codes: {sorted(missing)}"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# reason_label — spot-check that every dict entry round-trips correctly
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@pytest.mark.parametrize("entry", list(cas_codes._CARC_LABELS.items()))
|
||||
def test_reason_label_matches_dict_for_every_entry(entry) -> None:
|
||||
"""Every entry in the dict returns its own label via ``reason_label``."""
|
||||
(group, reason), expected = entry
|
||||
assert cas_codes.reason_label(group, reason) == expected
|
||||
Reference in New Issue
Block a user