feat(backend): GET /api/reconciliation/unmatched

This commit is contained in:
Tyler
2026-06-19 23:34:25 -06:00
parent 0d172d0682
commit a4c59587c6
2 changed files with 122 additions and 0 deletions
+12
View File
@@ -499,6 +499,18 @@ def list_claims(
}
@app.get("/api/reconciliation/unmatched")
def get_reconciliation_unmatched() -> dict:
"""Return unmatched Claims (left) and unmatched Remittances (right).
Powers the reconciliation review surface: every Claim with no
paired Remittance appears on the left, every Remittance with no
paired Claim appears on the right. The two lists are always present
(empty list, never absent) so the UI can index unconditionally.
"""
return store.list_unmatched(kind="both")
@app.get("/api/remittances")
def list_remittances(
request: Request,
+110
View File
@@ -287,3 +287,113 @@ def test_claims_default_accept_returns_json(seeded_store):
assert "total" in body
assert "returned" in body
assert "has_more" in body
# --------------------------------------------------------------------------- #
# /api/reconciliation/unmatched (T14)
# --------------------------------------------------------------------------- #
def test_get_reconciliation_unmatched_empty(client: TestClient):
"""With no parsed batches, both sides of the unmatched list are empty."""
resp = client.get("/api/reconciliation/unmatched")
assert resp.status_code == 200
assert resp.json() == {"claims": [], "remittances": []}
def test_get_reconciliation_unmatched_returns_orphans(client: TestClient):
"""An unmatched Claim (left) and an unmatched Remittance (right) both surface.
The 837 subscriber's ``member_id="M1"`` intentionally differs from the
835 remit's ``payer_claim_control_number="PCN-OTHER"`` so the auto-matcher
in ``reconcile.run`` does not pair them on ingest — we want them to land
in the unmatched list so the endpoint has something to return.
"""
from datetime import date, datetime, timezone
from decimal import Decimal
from cyclone.parsers.models import (
BatchSummary, BillingProvider, ClaimHeader, ClaimOutput,
Envelope, Payer, ParseResult, Subscriber, ValidationReport,
)
from cyclone.parsers.models_835 import (
BatchSummary as BatchSummary835,
ClaimPayment, Envelope as Envelope835,
FinancialInfo, Payer835, ParseResult835, Payee835,
ReassociationTrace,
)
from cyclone.store import BatchRecord837, BatchRecord835
# Add a single 837 Claim. member_id="M1" so PCN-based auto-match won't pair it.
co = ClaimOutput(
claim_id="CLM-1",
control_number="0001",
transaction_date=date(2026, 6, 19),
billing_provider=BillingProvider(name="Test", npi="1234567890"),
subscriber=Subscriber(
first_name="Jane", last_name="Doe", member_id="M1",
),
payer=Payer(name="Test Payer", id="P1"),
claim=ClaimHeader(
claim_id="CLM-1", total_charge=Decimal("100"),
frequency_code="1", place_of_service="11",
),
diagnoses=[],
service_lines=[],
validation=ValidationReport(passed=True, errors=[], warnings=[]),
raw_segments=[],
)
global_store.add(BatchRecord837(
id="b-837", kind="837p", input_filename="c.txt",
parsed_at=datetime(2026, 6, 19, 12, 0, tzinfo=timezone.utc),
result=ParseResult(
claims=[co],
summary=BatchSummary(
input_file="c.txt", control_number="0001",
transaction_date=date(2026, 6, 19),
total_claims=1, passed=1, failed=0,
),
),
))
# Add a single 835 Remittance with PCN="PCN-OTHER" (won't auto-match "M1").
cp = ClaimPayment(
payer_claim_control_number="PCN-OTHER",
status_code="1",
total_charge=Decimal("50"), total_paid=Decimal("50"),
service_payments=[],
)
global_store.add(BatchRecord835(
id="b-835", kind="835", input_filename="e.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("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=[cp],
summary=BatchSummary835(
input_file="e.txt", control_number="0001",
transaction_date=date(2026, 6, 19),
total_claims=1, passed=1, failed=0,
),
),
))
resp = client.get("/api/reconciliation/unmatched")
assert resp.status_code == 200
body = resp.json()
assert len(body["claims"]) == 1
assert body["claims"][0]["id"] == "CLM-1"
assert len(body["remittances"]) == 1
assert body["remittances"][0]["payerClaimControlNumber"] == "PCN-OTHER"