feat(backend): GET /api/reconciliation/unmatched
This commit is contained in:
@@ -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")
|
@app.get("/api/remittances")
|
||||||
def list_remittances(
|
def list_remittances(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -287,3 +287,113 @@ def test_claims_default_accept_returns_json(seeded_store):
|
|||||||
assert "total" in body
|
assert "total" in body
|
||||||
assert "returned" in body
|
assert "returned" in body
|
||||||
assert "has_more" 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user