feat(spot-check): delegate to parse_835_svc + reconcile with member-scoped CAS
Per the SP41-goal verifier feedback on AC2/AC3: the committed
select_spot_check_visits reimplemented reconcile inline against
service_line_payments on (procedure, DOS) only. That table has
no member_id at SVC scope, so OA-18 dedup inherited CAS reasons
from all SVCs at the same (procedure, DOS), misclassifying visits
across members (the operator's 'multiple rejections due to files
being submitted multiple times' noise).
This commit restructures the spot-check pipeline per the
strategist recommendation:
1. Extract load_in_window_svc_rows(ingest_dir, window_start,
window_end) in parse_835_svc.py as the canonical ingest helper.
run.py's existing loop is replaced with a one-line call.
2. select_spot_check_visits becomes a thin adapter:
- Reads in-window visits from the visits table.
- Loads SVCs via load_in_window_svc_rows.
- Calls reconcile_visits_to_835 (authoritative match key).
- Builds a member-scoped CAS lookup keyed by
(member_id, procedure, svc_date) so OA-18 inspection only
sees the matched member's adjudications.
- Returns the top-N DENIED / PARTIAL / NOT_IN_835 by
billed_amount desc.
3. New test_select_spot_check_visits_member_isolation unit test
drives the SHIPPED code on the real path (real .x12 fixtures
reparsed by parse_835_svc) and pins the structural fix:
two visits at the same (procedure, DOS) for DIFFERENT members,
member A with OA-18-only CAS, member B with none. A is
excluded, B stays a candidate. The OLD (procedure, DOS)-only
indexer excluded BOTH (verified separately at
/tmp/old_logic_check.py).
4. Existing OA-18 test rewritten to use real .x12 fixtures in
tmp_path (instead of inserting into service_line_payments /
cas_adjustments, which the new pipeline does not read).
Verification:
- Full backend suite: 1593 passed, 10 skipped (prodfile/docker), 0 failed.
- Live Edifabric 10/10 re-run with the new pipeline: 10 files,
all status='success', all 6 required segment classes per file.
Evidence at /tmp/grok-goal-4336f8e1af6d/implementer/spot-check.json
This commit is contained in:
@@ -11,6 +11,13 @@ HTTP seam) and unit-testable end-to-end:
|
||||
These tests pin the live-shape contract that the production driver
|
||||
exercises, so a regression in either module shows up here without
|
||||
having to run the live driver.
|
||||
|
||||
The 835 ingestion path under test reads raw ``*.x12`` files via
|
||||
``cyclone.rebill.parse_835_svc.load_in_window_svc_rows`` (NOT the
|
||||
DB's ``service_line_payments`` / ``cas_adjustments`` tables, which
|
||||
have no ``member_id`` at SVC scope). Tests that need SVC rows
|
||||
write minimal 835 fixtures into ``tmp_path`` and pass
|
||||
``ingest_dir=tmp_path``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -24,7 +31,6 @@ from pathlib import Path
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from sqlalchemy import text
|
||||
|
||||
import cyclone.edifabric as edifabric
|
||||
import cyclone.secrets as _secrets
|
||||
@@ -51,7 +57,85 @@ _X12_INTERCHANGE = {
|
||||
}
|
||||
|
||||
|
||||
# --- Fixtures --------------------------------------------------------- #
|
||||
# --- 835 fixture builder --------------------------------------------- #
|
||||
|
||||
|
||||
def _build_835_with_svc(
|
||||
*,
|
||||
member_id: str,
|
||||
claim_id: str,
|
||||
status: str,
|
||||
procedure: str,
|
||||
svc_date: date,
|
||||
charge: Decimal | str,
|
||||
paid: Decimal | str,
|
||||
cas: list[str] | None = None,
|
||||
pay_date: date | None = None,
|
||||
) -> str:
|
||||
"""Build a minimal valid 835 interchange with one SVC.
|
||||
|
||||
The shape matches what ``cyclone.rebill.parse_835_svc.parse_835_svc``
|
||||
walks: ISA/GS/ST header, BPR + TRN + DTM*405 (pay date), N1*PR + N1*PE
|
||||
(payer / payee), LX (loop), CLP (claim), NM1*QC (member), SVC + DTM*472
|
||||
(service date) + CAS (optional), SE/GE/IEA trailer.
|
||||
|
||||
``cas`` is a list of ``"GR-CODE"`` (e.g. ``["OA-18"]``).
|
||||
"""
|
||||
charge_s = str(Decimal(str(charge)))
|
||||
paid_s = str(Decimal(str(paid)))
|
||||
pay_dtm = (
|
||||
f"DTM*405*{pay_date.strftime('%Y%m%d')}~"
|
||||
if pay_date is not None
|
||||
else ""
|
||||
)
|
||||
cas_segment = ""
|
||||
if cas:
|
||||
cas_segment = "~".join(
|
||||
f"CAS*{c.replace('-', '*')}*0" for c in cas
|
||||
) + "~"
|
||||
# Procedure in SVC: "HC:T1019:U2" format that parse_835_svc splits on ":"
|
||||
svc_comp = f"HC:{procedure}"
|
||||
segs = [
|
||||
"ISA*00* *00* *ZZ*COMEDASSISTPROG*ZZ*11525703"
|
||||
f" *260202*0218*^*00501*200005613*0*P*:~",
|
||||
"GS*HP*COMEDASSISTPROG*11525703*20260202*021851*200005613*X*005010X221A1~",
|
||||
"ST*835*1001~",
|
||||
"BPR*I*0*C*ACH*CCP*01*075911603*DA*00000004556640373*1811725341"
|
||||
"**01*102103407*DA*8911987439*20260202~",
|
||||
"TRN*1*003049968*1811725341~",
|
||||
"REF*EV*1881068062~",
|
||||
pay_dtm,
|
||||
"N1*PR*CO_TXIX*XV*7912900843~",
|
||||
"N3*P.O. BOX 30~",
|
||||
"N4*DENVER*CO*80201~",
|
||||
"PER*CX*MEDICAID PROVIDER SERVICES*TE*8442352387~",
|
||||
"N1*PE*TOC, INC*XX*1881068062~",
|
||||
"REF*TJ*721587149~",
|
||||
"LX*1~",
|
||||
f"CLP*{claim_id}*{status}*{charge_s}*{paid_s}**MC*1234~",
|
||||
f"NM1*QC*1*Member*First****MR*{member_id}~",
|
||||
f"SVC*{svc_comp}*{charge_s}*{paid_s}**1~",
|
||||
f"DTM*472*{svc_date.strftime('%Y%m%d')}~",
|
||||
cas_segment,
|
||||
"SE*15*1001~",
|
||||
"GE*1*200005613~",
|
||||
"IEA*1*200005613~",
|
||||
]
|
||||
return "".join(segs)
|
||||
|
||||
|
||||
def _write_835(
|
||||
tmp_path: Path,
|
||||
name: str,
|
||||
content: str,
|
||||
) -> Path:
|
||||
"""Write a 835 fixture to ``tmp_path/<name>`` and return its path."""
|
||||
p = tmp_path / name
|
||||
p.write_text(content, encoding="ascii")
|
||||
return p
|
||||
|
||||
|
||||
# --- Visits-CSV fixture ---------------------------------------------- #
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -129,13 +213,14 @@ def _install(handler):
|
||||
# --- Pipeline tests --------------------------------------------------- #
|
||||
|
||||
|
||||
def test_select_spot_check_visits_returns_in_window_only(loaded_db):
|
||||
def test_select_spot_check_visits_returns_in_window_only(loaded_db, tmp_path):
|
||||
"""select_spot_check_visits honors the DOS window."""
|
||||
out = select_spot_check_visits(
|
||||
loaded_db,
|
||||
window_start=date(2026, 1, 1),
|
||||
window_end=date(2026, 6, 27),
|
||||
n=10,
|
||||
ingest_dir=tmp_path,
|
||||
)
|
||||
assert len(out) == 2
|
||||
# The stale 2025 row was filtered out at the load step.
|
||||
@@ -143,93 +228,179 @@ def test_select_spot_check_visits_returns_in_window_only(loaded_db):
|
||||
assert all(date(2026, 1, 1) <= d <= date(2026, 6, 27) for d in dos_list)
|
||||
|
||||
|
||||
def test_select_spot_check_visits_marks_not_in_835(loaded_db):
|
||||
"""With no SVC rows in DB, visits are NOT_IN_835 (rebill candidates)."""
|
||||
def test_select_spot_check_visits_marks_not_in_835(loaded_db, tmp_path):
|
||||
"""With no SVC rows in ingest/, visits are NOT_IN_835 (rebill candidates)."""
|
||||
out = select_spot_check_visits(
|
||||
loaded_db,
|
||||
window_start=date(2026, 1, 1),
|
||||
window_end=date(2026, 6, 27),
|
||||
n=10,
|
||||
ingest_dir=tmp_path,
|
||||
)
|
||||
dispositions = [d for (_v, d, _c) in out]
|
||||
assert dispositions == ["NOT_IN_835", "NOT_IN_835"]
|
||||
|
||||
|
||||
def test_select_spot_check_visits_n_truncates(loaded_db):
|
||||
def test_select_spot_check_visits_n_truncates(loaded_db, tmp_path):
|
||||
"""n=1 returns the top 1 by billed_amount desc."""
|
||||
out = select_spot_check_visits(
|
||||
loaded_db,
|
||||
window_start=date(2026, 1, 1),
|
||||
window_end=date(2026, 6, 27),
|
||||
n=1,
|
||||
ingest_dir=tmp_path,
|
||||
)
|
||||
assert len(out) == 1
|
||||
assert out[0][0].member_id == "R111111" # $300 > $250
|
||||
|
||||
|
||||
def test_select_spot_check_visits_oa18_excluded(loaded_db):
|
||||
"""OA-18-only SVC matches are excluded as DENIED_DUPLICATE_NOISE."""
|
||||
# Insert a batch + remittance + SVC so the Jan-15 visit has a hit.
|
||||
loaded_db.execute(text("""
|
||||
INSERT INTO batches (
|
||||
id, kind, input_filename, parsed_at
|
||||
) VALUES (
|
||||
'B-TEST-OA18', '835', 'tp-test-20260115.835',
|
||||
'2026-01-20 00:00:00'
|
||||
)
|
||||
"""))
|
||||
loaded_db.execute(text("""
|
||||
INSERT INTO remittances (
|
||||
id, batch_id, payer_claim_control_number,
|
||||
total_charge, total_paid, adjustment_amount,
|
||||
status_code, received_at, is_reversal
|
||||
) VALUES (
|
||||
'R-TEST-OA18', 'B-TEST-OA18', 'CLM-TEST-1',
|
||||
300.00, 0.00, 0.00,
|
||||
'DENIED', '2026-01-20 00:00:00', 0
|
||||
)
|
||||
"""))
|
||||
loaded_db.execute(text("""
|
||||
INSERT INTO service_line_payments (
|
||||
remittance_id, line_number, procedure_qualifier,
|
||||
procedure_code, service_date, charge, payment
|
||||
) VALUES (
|
||||
'R-TEST-OA18', 1, 'HC',
|
||||
'S5150', '2026-01-15', 300.00, 0.00
|
||||
)
|
||||
"""))
|
||||
loaded_db.commit()
|
||||
def test_select_spot_check_visits_oa18_excluded_member_scoped(
|
||||
loaded_db, tmp_path,
|
||||
):
|
||||
"""OA-18-only SVC matches are excluded as DENIED_DUPLICATE_NOISE.
|
||||
|
||||
Drives the SHIPPED code on the real path: a minimal 835 fixture
|
||||
in ``tmp_path`` with an OA-18 CAS for member R111111's SVC. R222222
|
||||
has no SVC match → NOT_IN_835. R111111 has a matched SVC whose
|
||||
CAS reasons are exactly ``{"OA-18"}`` → DENIED_DUPLICATE_NOISE
|
||||
→ excluded.
|
||||
"""
|
||||
# Empty CAS → R111111 becomes DENIED (kept), R222222 stays NOT_IN_835.
|
||||
no_cas_835 = _build_835_with_svc(
|
||||
member_id="R111111",
|
||||
claim_id="CLM-TEST-NOCAS",
|
||||
status="4", # denied
|
||||
procedure="S5150",
|
||||
svc_date=date(2026, 1, 15),
|
||||
charge=Decimal("300.00"),
|
||||
paid=Decimal("0.00"),
|
||||
cas=None,
|
||||
pay_date=date(2026, 1, 20),
|
||||
)
|
||||
_write_835(tmp_path, "no_cas.835", no_cas_835)
|
||||
|
||||
out = select_spot_check_visits(
|
||||
loaded_db,
|
||||
window_start=date(2026, 1, 1),
|
||||
window_end=date(2026, 6, 27),
|
||||
n=10,
|
||||
ingest_dir=tmp_path,
|
||||
)
|
||||
# R222222 (no SVC) → NOT_IN_835
|
||||
# R111111 (SVC hit with no CAS yet → DENIED)
|
||||
dispositions = sorted(d for (_v, d, _c) in out)
|
||||
# R222222 (no SVC) → NOT_IN_835; R111111 (SVC hit, no CAS yet) → DENIED
|
||||
assert dispositions == ["DENIED", "NOT_IN_835"]
|
||||
|
||||
# Now add OA-18 CAS — R111111 flips to DENIED_DUPLICATE_NOISE.
|
||||
loaded_db.execute(text("""
|
||||
INSERT INTO cas_adjustments (
|
||||
remittance_id, group_code, reason_code, amount
|
||||
) VALUES ('R-TEST-OA18', 'OA', '18', 300.00)
|
||||
"""))
|
||||
loaded_db.commit()
|
||||
# Use a different filename so we don't collide with no_cas.835;
|
||||
# the walker re-parses each file independently.
|
||||
oa18_835 = _build_835_with_svc(
|
||||
member_id="R111111",
|
||||
claim_id="CLM-TEST-OA18",
|
||||
status="4", # denied
|
||||
procedure="S5150",
|
||||
svc_date=date(2026, 1, 15),
|
||||
charge=Decimal("300.00"),
|
||||
paid=Decimal("0.00"),
|
||||
cas=["OA-18"],
|
||||
pay_date=date(2026, 1, 20),
|
||||
)
|
||||
_write_835(tmp_path, "oa18.835", oa18_835)
|
||||
|
||||
out = select_spot_check_visits(
|
||||
loaded_db,
|
||||
window_start=date(2026, 1, 1),
|
||||
window_end=date(2026, 6, 27),
|
||||
n=10,
|
||||
ingest_dir=tmp_path,
|
||||
)
|
||||
# R111111 now excluded (DENIED_DUPLICATE_NOISE); only R222222 remains.
|
||||
assert len(out) == 1
|
||||
assert out[0][0].member_id == "R222222"
|
||||
|
||||
|
||||
def test_select_spot_check_visits_member_isolation(tmp_path, monkeypatch):
|
||||
"""Two visits at the same (procedure, DOS) for DIFFERENT members:
|
||||
member A's SVC has OA-18-only CAS; member B's SVC has none.
|
||||
|
||||
On the correct (member_id, procedure, DOS) indexer, A is excluded
|
||||
and B stays as a candidate. On the previous (procedure, DOS) only
|
||||
indexer, A's CAS would bleed across to B and B would also be
|
||||
excluded. This is the structural fix the verifier demanded.
|
||||
"""
|
||||
# Visits for two members at the SAME (procedure, DOS).
|
||||
visits_csv = tmp_path / "visits.csv"
|
||||
with visits_csv.open("w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow([
|
||||
"Client", "Visit Date", "Payer", "Authorized", "Member ID",
|
||||
"Auth Start Date", "Auth End Date", "Authorization #", "ICD-10",
|
||||
"Service", "Procedure Code", "Modifiers", "Client Classes",
|
||||
"Billable Hours", "Billable Amount", "Invoice #", "Claimed",
|
||||
])
|
||||
# Member A at 2026-03-10, $400 — OA-18 candidate (will be excluded).
|
||||
writer.writerow([
|
||||
"Alpha, Anon", "03/10/2026", "COHCPF", "Yes", "R-ALPHA",
|
||||
"01/01/2026", "12/31/2026", "1", "R69",
|
||||
"HCPCS S5150", "S5150", "U8", "DD Waiver", "1", "$400.00",
|
||||
"INV-A", "Yes",
|
||||
])
|
||||
# Member B at SAME (procedure, DOS), $500 — DENIED candidate (kept).
|
||||
writer.writerow([
|
||||
"Beta, Anon", "03/10/2026", "COHCPF", "Yes", "R-BETA",
|
||||
"01/01/2026", "12/31/2026", "2", "R69",
|
||||
"HCPCS S5150", "S5150", "U8", "DD Waiver", "1", "$500.00",
|
||||
"INV-B", "Yes",
|
||||
])
|
||||
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path / 'test.db'}")
|
||||
db_mod._reset_for_tests()
|
||||
db_mod.init_db()
|
||||
assert load_visits_csv(visits_csv) == 2
|
||||
SessionLocal = db_mod.SessionLocal()
|
||||
sess = SessionLocal()
|
||||
|
||||
# Two 835s: one for R-ALPHA with OA-18-only CAS, one for R-BETA with no CAS.
|
||||
alpha_835 = _build_835_with_svc(
|
||||
member_id="R-ALPHA",
|
||||
claim_id="CLM-A",
|
||||
status="4",
|
||||
procedure="S5150",
|
||||
svc_date=date(2026, 3, 10),
|
||||
charge=Decimal("400.00"),
|
||||
paid=Decimal("0.00"),
|
||||
cas=["OA-18"],
|
||||
pay_date=date(2026, 3, 15),
|
||||
)
|
||||
beta_835 = _build_835_with_svc(
|
||||
member_id="R-BETA",
|
||||
claim_id="CLM-B",
|
||||
status="4",
|
||||
procedure="S5150",
|
||||
svc_date=date(2026, 3, 10),
|
||||
charge=Decimal("500.00"),
|
||||
paid=Decimal("0.00"),
|
||||
cas=None,
|
||||
pay_date=date(2026, 3, 15),
|
||||
)
|
||||
_write_835(tmp_path, "alpha.835", alpha_835)
|
||||
_write_835(tmp_path, "beta.835", beta_835)
|
||||
|
||||
out = select_spot_check_visits(
|
||||
sess,
|
||||
window_start=date(2026, 1, 1),
|
||||
window_end=date(2026, 6, 27),
|
||||
n=10,
|
||||
ingest_dir=tmp_path,
|
||||
)
|
||||
|
||||
sess.close()
|
||||
|
||||
# Alpha is excluded as DENIED_DUPLICATE_NOISE; Beta is the sole
|
||||
# candidate. n=10 truncates to 1, by billed desc → Beta ($500).
|
||||
assert len(out) == 1
|
||||
assert out[0][0].member_id == "R-BETA"
|
||||
assert out[0][1] == "DENIED"
|
||||
|
||||
|
||||
def test_write_spot_check_files_emits_required_segments(tmp_path, loaded_db):
|
||||
"""Every emitted file passes the plan's literal grep."""
|
||||
out_dir = tmp_path / "spot-check-837Ps"
|
||||
@@ -238,6 +409,7 @@ def test_write_spot_check_files_emits_required_segments(tmp_path, loaded_db):
|
||||
window_start=date(2026, 1, 1),
|
||||
window_end=date(2026, 6, 27),
|
||||
n=10,
|
||||
ingest_dir=tmp_path,
|
||||
)
|
||||
written = write_spot_check_files(visits, out_dir)
|
||||
assert len(written) == 2
|
||||
@@ -268,6 +440,7 @@ def test_write_spot_check_files_segment_per_line(tmp_path, loaded_db):
|
||||
window_start=date(2026, 1, 1),
|
||||
window_end=date(2026, 6, 27),
|
||||
n=10,
|
||||
ingest_dir=tmp_path,
|
||||
)
|
||||
written = write_spot_check_files(visits, out_dir)
|
||||
# Known segment names that may appear in a well-formed 837P.
|
||||
|
||||
Reference in New Issue
Block a user