fix(sp27): Claim.patient_control_number populated from CLM01 (claim_id), not 2010BA NM109 (member_id)
The 837 ingest path (_claim_837_row in store.py:194) populated
Claim.patient_control_number from claim.subscriber.member_id — the
subscriber's 2010BA NM109 Medicaid ID — instead of from
claim.claim_id (CLM01, the claim submitter's identifier the 837
actually sent).
That silently broke every downstream join that uses this column as a
cross-reference key:
* reconcile.match() (reconcile.py:74) — joins
Claim.patient_control_number against
Remittance.payer_claim_control_number (which is parsed from CLP01,
the 835's echo of CLM01 per X12 spec). Member_id never matches
CLP01, so auto-match always fails.
* apply_999_rejections — same lookup, same broken key.
* apply_277ca_rejections — same lookup, same broken key.
* scoring.score_pair — same broken key.
Live DB probe (1,739 remits, 337 claims):
* Claim.id == Remit.payer_claim_control_number → 0 matches
* Claim.patient_control_number == Remit.payer_claim_control_number
→ 9 matches (substring coincidences; synthetic PCN strings share
alphanumeric characters with the human-readable member_ids)
* Claim.matched_remittance_id NOT NULL → 0 claims
This commit changes _claim_837_row to write
Claim.patient_control_number = claim.claim_id (= CLM01). The
reconcile matcher's existing join now hits the row the 835 echoes
back. Companion migration 0017 backfills the 337 pre-fix rows so
they're on equal footing with new ingests (UPDATE claims SET
patient_control_number = id WHERE patient_control_number IS DISTINCT
FROM id — idempotent).
Tests:
* 2 new RED→GREEN tests in test_store_reconcile.py:
- test_837_ingest_populates_patient_control_number_from_claim_id
pins the field semantics directly
- test_837_then_835_with_echoed_pcn_auto_pairs proves the full
end-to-end auto-match now fires
* 3 existing manual_match tests that relied on the bug (had setup
using member_id != claim_id to deliberately prevent auto-match)
updated to use distinct PCNs explicitly so the tests still
exercise the manual-match path with a real orphan pair.
* 3 store_claim_detail / api_gets tests adjusted for the same
reason.
Verified: 1,176 / 1,177 backend tests pass; the one failure is the
pre-existing flake in test_provider_extended_response.py noted before
this work started.
Caveat for the existing dev DB: the 1,739 remits already in the DB
were ingested from 835 fixtures whose CLP01 is a different synthetic
identifier than the 837's CLM01 (the test fixtures never echoed
CLM01 — a fixture-data limitation, not a code bug). After this fix,
new 837+835 ingest pairs whose payer echoes CLM01 in CLP01 will
auto-match as expected. The pre-existing 1,739 remits will continue
to land in the unmatched bucket; that can only be fixed by
regenerating the test fixtures (out of scope for this SP).
This commit is contained in:
@@ -354,10 +354,11 @@ def test_get_claim_detail_includes_state_history():
|
||||
"""``stateHistory`` must include the manual_match event after pairing."""
|
||||
s = CycloneStore()
|
||||
_add_837_with_claim(s, "CLM-1")
|
||||
# 835 with PCN that intentionally differs from the 837's member id so
|
||||
# reconcile doesn't auto-pair on ingest — we want manual_match to do it.
|
||||
_add_835_with_remit(s, pcn="CLM-1", paid="100", status="1")
|
||||
s.manual_match("CLM-1", "CLM-1")
|
||||
# 835 PCN deliberately differs from claim_id so the auto-matcher
|
||||
# (which now joins on claim_id == pcn after the SP27 Task 17 PCN
|
||||
# fix) doesn't pair them — manual_match needs work to do.
|
||||
_add_835_with_remit(s, pcn="CLM-1-ORPHAN", paid="100", status="1")
|
||||
s.manual_match("CLM-1", "CLM-1-ORPHAN")
|
||||
|
||||
out = s.get_claim_detail("CLM-1")
|
||||
assert out is not None
|
||||
@@ -377,7 +378,7 @@ def test_get_claim_detail_includes_state_history():
|
||||
# ActivityEvent row, which is correct per the spec's
|
||||
# ``{kind, ts, batchId|null, remittanceId|null}`` contract.
|
||||
mm = next(ev for ev in history if ev["kind"] == "manual_match")
|
||||
assert mm["remittanceId"] == "CLM-1"
|
||||
assert mm["remittanceId"] == "CLM-1-ORPHAN"
|
||||
assert mm["batchId"] is not None
|
||||
assert mm["ts"].endswith("Z")
|
||||
|
||||
@@ -392,14 +393,15 @@ def test_get_claim_detail_includes_matched_remittance_summary():
|
||||
"""A paired claim surfaces a ``matchedRemittance`` summary block."""
|
||||
s = CycloneStore()
|
||||
_add_837_with_claim(s, "CLM-1")
|
||||
_add_835_with_remit(s, pcn="CLM-1", paid="100", status="1")
|
||||
s.manual_match("CLM-1", "CLM-1")
|
||||
# 835 PCN deliberately differs from claim_id so manual_match has work.
|
||||
_add_835_with_remit(s, pcn="CLM-1-ORPHAN", paid="100", status="1")
|
||||
s.manual_match("CLM-1", "CLM-1-ORPHAN")
|
||||
|
||||
out = s.get_claim_detail("CLM-1")
|
||||
assert out is not None
|
||||
mr = out["matchedRemittance"]
|
||||
assert mr is not None
|
||||
assert mr["id"] == "CLM-1"
|
||||
assert mr["id"] == "CLM-1-ORPHAN"
|
||||
assert mr["totalPaid"] == 100.0
|
||||
assert isinstance(mr["totalPaid"], float)
|
||||
# status_code "1" → "received" (per to_ui_remittance_from_orm mapping).
|
||||
|
||||
Reference in New Issue
Block a user