"""SP32 Task 6: end-to-end smoke tests for the ``backfill-rendering-npi`` CLI. The CLI re-parses on-disk 837p + 835 files and backfills the new typed ``rendering_provider_npi`` columns on existing Claim / Remittance rows that were ingested before the T4 writers took effect. Idempotent: rows that already have a non-NULL rendering NPI are left untouched. Test strategy mirrors ``test_cli_backup.py``: in-process Click CliRunner invocation against a fresh per-test DB (autouse ``_auto_init_db`` fixture), pre-seeded with Claim/Remittance rows whose ``rendering_provider_npi`` is NULL, then assert the CLI populated them. """ from __future__ import annotations from datetime import datetime, timezone from decimal import Decimal import pytest from click.testing import CliRunner from cyclone import db from cyclone.cli import main BACKEND_DIR = "backend" _REPO_ROOT = "." # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture def seed_837p_claim_without_renderer(): """Insert a Claim row whose ``rendering_provider_npi`` is NULL. The id (``t991102984o1c1d``) matches the first claim in ``co_medicaid_837p_with_renderer.txt`` — the fixture that does contain a NM1*82 rendering provider (NPI ``1234567893``) attached to that claim. Backfill should populate the column from the re-parsed file. """ from cyclone.db import Batch, Claim, ClaimState with db.SessionLocal()() as s: s.add(Batch( id="seed-837p-batch", kind="837p", input_filename="seed.x12", parsed_at=datetime.now(timezone.utc), )) s.add(Claim( id="t991102984o1c1d", batch_id="seed-837p-batch", patient_control_number="t991102984o1c1d", charge_amount=Decimal("85.40"), state=ClaimState.SUBMITTED, rendering_provider_npi=None, # ← the column under test provider_npi="1881068062", )) s.commit() @pytest.fixture def seed_835_remit_without_service_provider(tmp_path): """Insert a Remittance whose ``rendering_provider_npi`` is NULL. Writes a minimal 835 fixture to ``tmp_path`` containing an NM1*1P service provider (NPI ``2222222222``) attached to the only CLP segment (claim control number ``CLM001``). Returns the path to the written file. """ from cyclone.db import Batch, Remittance fixture = tmp_path / "minimal_835_with_service_provider.txt" fixture.write_text( "ISA*00* *00* *ZZ*RECEIVERID *ZZ*SENDERID " "*250101*1200*^*00501*000000001*0*P*:~" "GS*HP*RECEIVER*SENDER*20250101*1200*1*X*005010X221A1~" "ST*835*0001~" "BPR*I*85.40*C*NON*****01*021000021*DA*123456*1511111" "**01*031302955*DA*9876543~" "TRN*1*1511111*1511111~" "DTM*405*20250101~" "N1*PR*COLORADO MEDICAID*XV*CO MEDICAID~" "N3*PO BOX 1100*~" "N4*DENVER*CO*80203~" "REF*2U*12345~" "N1*PE*ACME CLINIC*XX*1111111111~" "LX*1~" "CLP*CLM001*1*85.40*85.40*0.00*MC*CLM001*11*1~" "CAS*PR*1*0.00~" "NM1*1P*2*RENDERING PROVIDER*****XX*2222222222~" "SVC*HC:99213*85.40*85.40**1~" "DTM*472*20250101~" "SE*18*0001~" "GE*1*1~" "IEA*1*000000001~" ) with db.SessionLocal()() as s: s.add(Batch( id="seed-835-batch", kind="835", input_filename=fixture.name, parsed_at=datetime.now(timezone.utc), )) s.add(Remittance( id="CLM001", batch_id="seed-835-batch", payer_claim_control_number="CLM001", claim_id=None, status_code="1", status_label="Primary payer forward", total_charge=Decimal("85.40"), total_paid=Decimal("85.40"), patient_responsibility=Decimal("0"), adjustment_amount=Decimal("0"), received_at=datetime.now(timezone.utc), is_reversal=False, rendering_provider_npi=None, # ← the column under test )) s.commit() return fixture # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _run(args: list[str]) -> object: """Invoke the CLI in-process via CliRunner. CliRunner auto-captures stdout/stderr; ``catch_exceptions=False`` so the test fails loudly on any unexpected raise instead of a swallowed traceback. """ return CliRunner().invoke(main, args, catch_exceptions=False) # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- def test_backfill_populates_837p_rendering_npi(seed_837p_claim_without_renderer): """Re-parse the 837p fixture → Claim.rendering_provider_npi populated.""" fixture_path = "tests/fixtures/co_medicaid_837p_with_renderer.txt" # Sanity: column starts NULL. with db.SessionLocal()() as s: from cyclone.db import Claim row = s.get(Claim, "t991102984o1c1d") assert row is not None assert row.rendering_provider_npi is None result = _run(["backfill-rendering-npi", "--file", fixture_path, "--type", "837p"]) assert result.exit_code == 0, result.output with db.SessionLocal()() as s: from cyclone.db import Claim row = s.get(Claim, "t991102984o1c1d") assert row is not None # First claim in the fixture carries NM1*82 NPI ``1234567893``. assert row.rendering_provider_npi == "1234567893" # CLI prints the summary line with the populated counts. assert "claims_updated=" in result.output assert "remits_updated=" in result.output def test_backfill_populates_835_rendering_npi(seed_835_remit_without_service_provider): """Re-parse an 835 with NM1*1P → Remittance.rendering_provider_npi populated.""" fixture_path = str(seed_835_remit_without_service_provider) # Sanity: column starts NULL. with db.SessionLocal()() as s: from cyclone.db import Remittance row = s.get(Remittance, "CLM001") assert row is not None assert row.rendering_provider_npi is None result = _run(["backfill-rendering-npi", "--file", fixture_path, "--type", "835"]) assert result.exit_code == 0, result.output with db.SessionLocal()() as s: from cyclone.db import Remittance row = s.get(Remittance, "CLM001") assert row is not None # Inline fixture's NM1*1P NM109 is ``2222222222``. assert row.rendering_provider_npi == "2222222222" assert "remits_updated=" in result.output def test_backfill_is_idempotent(seed_837p_claim_without_renderer): """Second run is a no-op — already-populated columns stay.""" fixture_path = "tests/fixtures/co_medicaid_837p_with_renderer.txt" # First run: populates the column. first = _run(["backfill-rendering-npi", "--file", fixture_path, "--type", "837p"]) assert first.exit_code == 0, first.output # Second run: still exits 0; column is unchanged; nothing breaks. second = _run(["backfill-rendering-npi", "--file", fixture_path, "--type", "837p"]) assert second.exit_code == 0, second.output with db.SessionLocal()() as s: from cyclone.db import Claim row = s.get(Claim, "t991102984o1c1d") assert row.rendering_provider_npi == "1234567893" def test_backfill_help_lists_subcommand(): """Regression guard: the subcommand is wired into ``main``.""" result = _run(["--help"]) assert result.exit_code == 0 assert "backfill-rendering-npi" in result.output def test_backfill_with_no_files_is_noop(): """Running with no --file and no --input-dir is a clean no-op (exit 0).""" result = _run(["backfill-rendering-npi"]) assert result.exit_code == 0, result.output # Summary line still printed (zero counts). assert "claims_updated=0" in result.output assert "remits_updated=0" in result.output