228 lines
8.1 KiB
Python
228 lines
8.1 KiB
Python
"""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
|