From f8e2f0933e5abea5c558d1b3a019d34e09aae010 Mon Sep 17 00:00:00 2001 From: sp41-bot Date: Tue, 7 Jul 2026 19:53:04 -0600 Subject: [PATCH] fix(sp41): reset member_id on new CLP + tighten status-preservation test --- backend/src/cyclone/rebill/parse_835_svc.py | 1 + .../fixtures/835_sample_svc_with_member.txt | 2 +- backend/tests/test_rebill_parse_835_svc.py | 18 ++++++++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/backend/src/cyclone/rebill/parse_835_svc.py b/backend/src/cyclone/rebill/parse_835_svc.py index c48b98e..1422006 100644 --- a/backend/src/cyclone/rebill/parse_835_svc.py +++ b/backend/src/cyclone/rebill/parse_835_svc.py @@ -55,6 +55,7 @@ def parse_835_svc(path: str | Path) -> "Iterator[SvcRow]": elif seg_name == "CLP": current_claim_id = elems[1] if len(elems) > 1 else "" current_status = elems[2] if len(elems) > 2 else "" + current_member_id = "" elif seg_name == "NM1" and len(elems) > 1 and elems[1] == "QC": current_member_id = elems[9] if len(elems) > 9 else "" elif seg_name == "SVC" and current_claim_id: diff --git a/backend/tests/fixtures/835_sample_svc_with_member.txt b/backend/tests/fixtures/835_sample_svc_with_member.txt index 76d0472..e102072 100644 --- a/backend/tests/fixtures/835_sample_svc_with_member.txt +++ b/backend/tests/fixtures/835_sample_svc_with_member.txt @@ -1 +1 @@ -ISA*00* *00* *ZZ*CYCLONE *ZZ*GAINWELL *260101*1200*^*00501*000000001*0*P*:~GS*HC*CYCLONE*GAINWELL*20260101*1200*1*X*005010X221A1~ST*835*0001~BPR*I*100.00*C*ACH*CCP*01*021000021*DA*123456789*1512345678**01*021000021*DA*123456789*20260101~TRN*1*TRACE01*1512345678~DTM*405*20260118~N1*PR*COLORADO MEDICAL ASSISTANCE PROGRAM*XV*COMEDASSISTPROG~N3*PO BOX 1100~N4*DENVER*CO*80202~REF*2U*7912900843~LX*1~CLP*T1001*1*16.24*16.10**MC*2026029105200*11*1~NM1*QC*1*AALBUE*ERIC****MR*J813715~NM1*74*1*AALBUE*ERIC*W***C*J813715~DTM*232*20260118~DTM*233*20260124~SVC*HC:T1019:U2:SC:KX*2.32*2.30**.33~DTM*472*20260118~CAS*CO*45*.02~REF*G1*6252960154~REF*6R*T1001V001~AMT*B6*2.30~SVC*HC:T1019:U2:SC:KX*2.32*2.30**.33~DTM*472*20260119~CAS*OA*18*2.32~SVC*HC:T1019:U2:SC:KX*2.32*-2.30**.33~DTM*472*20260120~CAS*CO*45*-.02~SE*23*0001~GE*1*1~IEA*1*000000001~ \ No newline at end of file +ISA*00* *00* *ZZ*CYCLONE *ZZ*GAINWELL *260101*1200*^*00501*000000001*0*P*:~GS*HC*CYCLONE*GAINWELL*20260101*1200*1*X*005010X221A1~ST*835*0001~BPR*I*100.00*C*ACH*CCP*01*021000021*DA*123456789*1512345678**01*021000021*DA*123456789*20260101~TRN*1*TRACE01*1512345678~DTM*405*20260118~N1*PR*COLORADO MEDICAL ASSISTANCE PROGRAM*XV*COMEDASSISTPROG~N3*PO BOX 1100~N4*DENVER*CO*80202~REF*2U*7912900843~LX*1~CLP*T1001*1*16.24*16.10**MC*2026029105200*11*1~NM1*QC*1*AALBUE*ERIC****MR*J813715~NM1*74*1*AALBUE*ERIC*W***C*J813715~DTM*232*20260118~DTM*233*20260124~SVC*HC:T1019:U2:SC:KX*2.32*2.30**.33~DTM*472*20260118~CAS*CO*45*.02~REF*G1*6252960154~REF*6R*T1001V001~AMT*B6*2.30~SVC*HC:T1019:U2:SC:KX*2.32*2.30**.33~DTM*472*20260119~CAS*OA*18*2.32~SVC*HC:T1019:U2:SC:KX*2.32*-2.30**.33~DTM*472*20260120~CAS*CO*45*-.02~LX*2~CLP*T1002*4*16.24*0**MC*2026029105201*11*1~NM1*QC*1*OTHER-A*FIRST****MR*OTHER-MEMBER-A~SVC*HC:T1019*2.32*0**.33~DTM*472*20260121~CAS*CO*97*2.32~LX*3~CLP*T1003*22*16.24*-16.10**MC*2026029105202*11*1~NM1*QC*1*OTHER-B*SECOND****MR*OTHER-MEMBER-B~SVC*HC:T1019*2.32*-2.30**.33~DTM*472*20260122~SE*23*0001~GE*1*1~IEA*1*000000001~ \ No newline at end of file diff --git a/backend/tests/test_rebill_parse_835_svc.py b/backend/tests/test_rebill_parse_835_svc.py index cc6cca4..437487b 100644 --- a/backend/tests/test_rebill_parse_835_svc.py +++ b/backend/tests/test_rebill_parse_835_svc.py @@ -1,19 +1,28 @@ """835 SVC-level parser with member_id at SVC scope.""" from pathlib import Path +from collections import defaultdict from decimal import Decimal from cyclone.rebill.parse_835_svc import parse_835_svc FIX = Path(__file__).parent / "fixtures" / "835_sample_svc_with_member.txt" def test_parse_835_svc_extracts_member_id(): - """NM1*QC NM109 at the CLP scope must propagate to every SVC row.""" + """NM1*QC NM109 at the CLP scope must propagate to that CLP's SVC rows; + the multi-claim fixture confirms each CLP's NM1*QC lands on its own SVCs.""" rows = list(parse_835_svc(FIX)) assert len(rows) >= 1 + by_claim: dict[str, list[str]] = defaultdict(list) for r in rows: - assert r.member_id == "J813715" + by_claim[r.claim_id].append(r.member_id) assert r.procedure # non-empty assert r.svc_date # non-empty assert r.charge > 0 + # First CLP (T1001) -> original fixture member + assert all(mid == "J813715" for mid in by_claim["T1001"]), by_claim + # Second CLP (T1002, status 4) -> its own NM1*QC + assert all(mid == "OTHER-MEMBER-A" for mid in by_claim["T1002"]), by_claim + # Third CLP (T1003, status 22) -> its own NM1*QC + assert all(mid == "OTHER-MEMBER-B" for mid in by_claim["T1003"]), by_claim def test_parse_835_svc_extracts_cas_reasons(): """CAS segments after DTM*472 must be captured (post-DTM*472 ordering).""" @@ -22,7 +31,8 @@ def test_parse_835_svc_extracts_cas_reasons(): assert any("OA-18" in r.cas_reasons for r in rows) def test_parse_835_svc_picks_up_status_22_reversals(): - """Status 22 (reversal of previous payment) must be preserved.""" + """Status 22 (reversal of previous payment) must be preserved, along with + status 1 (primary) and status 4 (denied).""" rows = list(parse_835_svc(FIX)) statuses = {r.status for r in rows} - assert statuses & {"1", "4", "22"} # at least one of each present \ No newline at end of file + assert statuses == {"1", "4", "22"}