156 lines
4.7 KiB
Python
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" |