feat(backend): persist CasAdjustment rows from 835 SVC.adjustments
This commit is contained in:
@@ -167,6 +167,12 @@ def _remittance_835_row(cp: ClaimPayment, batch_id: str) -> Remittance:
|
||||
"""Build a Remittance ORM row from a ClaimPayment. NOT yet persisted."""
|
||||
received_at = utcnow()
|
||||
# Adjustment amount: sum the CAS rows for the first service line.
|
||||
# NOTE: This is a best-effort placeholder used until the reconciliation
|
||||
# pass (T10) overwrites it from the persisted CasAdjustment rows. The
|
||||
# authoritative value comes from `reconcile.run()`, which sums
|
||||
# ``CasAdjustment.amount`` per ``remittance_id`` and writes the result
|
||||
# back to ``Remittance.adjustment_amount``. We keep this stub so the
|
||||
# row has a sane value if reconciliation is disabled or fails.
|
||||
adjustment = Decimal("0")
|
||||
if cp.service_payments:
|
||||
sp = cp.service_payments[0]
|
||||
@@ -194,6 +200,27 @@ def _remittance_835_row(cp: ClaimPayment, batch_id: str) -> Remittance:
|
||||
)
|
||||
|
||||
|
||||
def _cas_adjustment_row(adj, remittance_id: str) -> "db.CasAdjustment":
|
||||
"""Build a CasAdjustment ORM row from a ClaimAdjustment. NOT yet persisted.
|
||||
|
||||
One row per SVC-level CAS adjustment is persisted so the T10
|
||||
reconcile aggregator can compute ``Remittance.adjustment_amount``
|
||||
as ``SUM(CasAdjustment.amount) WHERE remittance_id = ...``.
|
||||
|
||||
``quantity`` is optional in the X12 CAS spec; we coerce to Decimal
|
||||
only when present to keep the column NULL for the common no-QTY case.
|
||||
"""
|
||||
from cyclone.db import CasAdjustment
|
||||
quantity = getattr(adj, "quantity", None)
|
||||
return CasAdjustment(
|
||||
remittance_id=remittance_id,
|
||||
group_code=adj.group_code,
|
||||
reason_code=adj.reason_code,
|
||||
amount=Decimal(str(adj.amount)),
|
||||
quantity=Decimal(str(quantity)) if quantity is not None else None,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UI mappers: ORM rows → simpler UI types.
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -471,7 +498,16 @@ class CycloneStore:
|
||||
cp.payer_claim_control_number, record.id,
|
||||
)
|
||||
continue
|
||||
s.add(_remittance_835_row(cp, record.id))
|
||||
remit_row = _remittance_835_row(cp, record.id)
|
||||
s.add(remit_row)
|
||||
# Flush so remit_row.id (FK target of cas_adjustments) is
|
||||
# populated. SQLAlchemy assigns the PK on flush; without
|
||||
# this the CasAdjustment inserts below would reference an
|
||||
# unset id and violate the FK.
|
||||
s.flush()
|
||||
for svc in cp.service_payments:
|
||||
for adj in svc.adjustments:
|
||||
s.add(_cas_adjustment_row(adj, remit_row.id))
|
||||
s.add(ActivityEvent(
|
||||
ts=record.parsed_at,
|
||||
kind="remit_received",
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Integration tests for CycloneStore.add() with 835 batches.
|
||||
|
||||
Covers: parse-835 persist → reconcile trigger → CAS aggregation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from cyclone import db
|
||||
from cyclone.parsers.models_835 import (
|
||||
ClaimPayment, ClaimAdjustment, ServicePayment,
|
||||
ParseResult835, Envelope, FinancialInfo, ReassociationTrace,
|
||||
Payer835, Payee835, BatchSummary,
|
||||
)
|
||||
from cyclone.store import CycloneStore
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db")
|
||||
db._reset_for_tests()
|
||||
db.init_db()
|
||||
yield
|
||||
db._reset_for_tests()
|
||||
|
||||
|
||||
def _make_remit_with_cas(remit_id="CLP-1",
|
||||
status="1", charge="124.00", paid="62.00",
|
||||
cas_amount="62.00"):
|
||||
cp = ClaimPayment(
|
||||
payer_claim_control_number=remit_id,
|
||||
status_code=status,
|
||||
status_label="Primary",
|
||||
total_charge=charge, total_paid=paid,
|
||||
service_payments=[
|
||||
ServicePayment(
|
||||
line_number=1, procedure_qualifier="HC", procedure_code="99213",
|
||||
charge=charge, payment=paid,
|
||||
adjustments=[ClaimAdjustment(group_code="CO", reason_code="45",
|
||||
amount=cas_amount)],
|
||||
),
|
||||
],
|
||||
)
|
||||
return cp
|
||||
|
||||
|
||||
def _make_835_result(claims):
|
||||
# NOTE: field names below follow the actual model definitions in
|
||||
# cyclone/parsers/models_835.py — the plan snippet (lines 2550-2566)
|
||||
# referenced stale field names (e.g. credit_debit/handling_code) that
|
||||
# no longer exist after the model was migrated.
|
||||
return ParseResult835(
|
||||
envelope=Envelope(
|
||||
sender_id="S", receiver_id="R", control_number="0001",
|
||||
transaction_date=date(2026, 6, 19),
|
||||
),
|
||||
financial_info=FinancialInfo(
|
||||
handling_code="C", paid_amount=Decimal("0"),
|
||||
credit_debit_flag="C", payment_method=None,
|
||||
),
|
||||
trace=ReassociationTrace(
|
||||
trace_type_code="1", trace_number="0001",
|
||||
originating_company_id="S",
|
||||
),
|
||||
payer=Payer835(name="X", id="SKCO0"),
|
||||
payee=Payee835(name="Y", npi="1234567890"),
|
||||
claims=claims,
|
||||
summary=BatchSummary(
|
||||
input_file="era.txt", control_number="0001",
|
||||
transaction_date=date(2026, 6, 19),
|
||||
total_claims=len(claims), passed=len(claims), failed=0,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_add_835_aggregates_cas_into_adjustment_amount():
|
||||
s = CycloneStore()
|
||||
cp = _make_remit_with_cas()
|
||||
from cyclone.store import BatchRecord835
|
||||
rec = BatchRecord835(
|
||||
id="b-1", kind="835", input_filename="era.txt",
|
||||
parsed_at=datetime(2026, 6, 19, tzinfo=timezone.utc),
|
||||
result=_make_835_result([cp]),
|
||||
)
|
||||
s.add(rec)
|
||||
|
||||
with db.SessionLocal()() as session:
|
||||
from cyclone.db import Remittance, CasAdjustment
|
||||
r = session.query(Remittance).first()
|
||||
assert r is not None
|
||||
assert r.adjustment_amount == 62.0
|
||||
cas_rows = session.query(CasAdjustment).all()
|
||||
assert len(cas_rows) == 1
|
||||
assert cas_rows[0].group_code == "CO"
|
||||
assert cas_rows[0].reason_code == "45"
|
||||
Reference in New Issue
Block a user