Files
cyclone/backend/tests/test_store.py
T

156 lines
4.7 KiB
Python

"""Tests for the SQLAlchemy-backed CycloneStore.
Public API is preserved from the in-memory version: add / get_batch /
iter_claims / iter_remittances / distinct_providers / recent_activity.
New methods: list_unmatched / manual_match / manual_unmatch.
"""
from __future__ import annotations
from datetime import date, datetime, timezone
from decimal import Decimal
import pytest
from cyclone import db
from cyclone.store import BatchRecord, CycloneStore, store
from cyclone.parsers.models import (
Address,
BatchSummary,
BillingProvider,
ClaimHeader,
ClaimOutput,
Envelope,
Payer,
ParseResult,
Subscriber,
ValidationReport,
)
@pytest.fixture(autouse=True)
def _setup(tmp_path, monkeypatch):
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db")
db._reset_for_tests()
db.init_db()
yield
db._reset_for_tests()
def _make_claim_837(claim_id: str = "CLM-1", charge: str = "124.00") -> ClaimOutput:
return ClaimOutput(
claim_id=claim_id,
control_number="0001",
transaction_date=date(2026, 6, 19),
billing_provider=BillingProvider(
name="Test", npi="1234567890",
),
# Member_id doubles as patient_control_number in the DB; the
# ``uq_claims_batch_pcn`` unique constraint requires it to be
# distinct within a batch, so derive it from the claim id.
subscriber=Subscriber(
first_name="Jane", last_name="Doe", member_id=f"M-{claim_id}",
),
payer=Payer(name="Test Payer", id="P1"),
claim=ClaimHeader(
claim_id=claim_id, total_charge=Decimal(charge),
frequency_code="1", place_of_service="11",
),
diagnoses=[],
service_lines=[],
validation=ValidationReport(passed=True, errors=[], warnings=[]),
raw_segments=[],
)
def _make_result_837() -> ParseResult:
return ParseResult(
envelope=Envelope(
sender_id="S", receiver_id="R", control_number="0001",
transaction_date=date(2026, 6, 19),
),
claims=[_make_claim_837("CLM-1"), _make_claim_837("CLM-2", "88.50")],
summary=BatchSummary(
input_file="test.txt", control_number="0001",
transaction_date=date(2026, 6, 19),
total_claims=2, passed=2, failed=0,
),
)
def _make_batch_record(kind: str = "837p", result=None, batch_id: str = "b-uuid-1"):
return BatchRecord(
id=batch_id, kind=kind, input_filename="test.txt",
parsed_at=datetime(2026, 6, 19, 12, 0, tzinfo=timezone.utc),
result=result or _make_result_837(),
)
def test_module_singleton():
assert isinstance(store, CycloneStore)
@pytest.mark.asyncio
async def test_add_837_publishes_claim_written_and_activity_recorded_events():
"""store.add() with event_bus= should publish one claim_written event
per inserted claim and one activity_recorded event per activity row."""
from cyclone.pubsub import EventBus
bus = EventBus()
claim_q = bus.subscribe(["claim_written"])
activity_q = bus.subscribe(["activity_recorded"])
s = CycloneStore()
rec = _make_batch_record()
s.add(rec, event_bus=bus)
# Two claims + two claim_submitted activity events expected.
import asyncio
claims = []
for _ in range(2):
claims.append(await asyncio.wait_for(claim_q.__anext__(), timeout=0.5))
activities = []
for _ in range(2):
activities.append(await asyncio.wait_for(activity_q.__anext__(), timeout=0.5))
# Each event has _kind and a payload with the expected shape.
for ev in claims:
assert ev["_kind"] == "claim_written"
assert "id" in ev
for ev in activities:
assert ev["_kind"] == "activity_recorded"
assert ev["kind"] == "claim_submitted"
assert "claimId" in ev
def test_add_837_persists_batch_and_claims():
s = CycloneStore()
rec = _make_batch_record()
s.add(rec)
# New session confirms persistence.
with db.SessionLocal()() as session:
from cyclone.db import Batch, Claim, ClaimState
b = session.get(Batch, "b-uuid-1")
assert b is not None
assert b.kind == "837p"
claims = session.query(Claim).all()
assert len(claims) == 2
assert all(c.state == ClaimState.SUBMITTED for c in claims)
def test_iter_claims_returns_dicts():
s = CycloneStore()
s.add(_make_batch_record())
rows = s.iter_claims()
assert len(rows) == 2
assert all("id" in r and "state" in r for r in rows)
def test_persistence_across_session():
s1 = CycloneStore()
s1.add(_make_batch_record(batch_id="b-x"))
s2 = CycloneStore() # fresh instance, same engine
loaded = s2.get_batch("b-x")
assert loaded is not None
assert loaded["kind"] == "837p"