feat(batch-diff): side-by-side claim diff between two batches
This commit is contained in:
@@ -0,0 +1,611 @@
|
||||
"""Tests for ``cyclone.batch_diff`` and the ``GET /api/batch-diff`` endpoint.
|
||||
|
||||
Two layers:
|
||||
|
||||
1. Pure unit tests over :func:`cyclone.batch_diff.diff_batches` — no DB,
|
||||
no HTTP. Construct ``BatchRecord`` values directly with the parser's
|
||||
Pydantic models so the tests stay fast and deterministic.
|
||||
|
||||
2. Integration tests over the FastAPI endpoint — round-trip through
|
||||
``TestClient`` with real 837P / 835 fixtures. The ``conftest.py``
|
||||
fixture already gives every test a fresh SQLite database, so these
|
||||
run independently without manual cleanup.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from cyclone.api import app
|
||||
from cyclone.batch_diff import (
|
||||
BatchClaimSummary,
|
||||
diff_batches,
|
||||
diff_batches_to_wire,
|
||||
)
|
||||
from cyclone.parsers.models import (
|
||||
Address,
|
||||
BatchSummary,
|
||||
BillingProvider,
|
||||
ClaimHeader,
|
||||
ClaimOutput,
|
||||
Envelope,
|
||||
Payer,
|
||||
ParseResult,
|
||||
Procedure,
|
||||
ServiceLine,
|
||||
Subscriber,
|
||||
ValidationReport,
|
||||
)
|
||||
from cyclone.parsers.models_835 import (
|
||||
ClaimPayment,
|
||||
ParseResult835,
|
||||
Payer835,
|
||||
Payee835,
|
||||
ReassociationTrace,
|
||||
FinancialInfo,
|
||||
ServicePayment,
|
||||
)
|
||||
from cyclone.store import BatchRecord, store as global_store
|
||||
|
||||
# 837P fixture shipped with the test suite — two claims, useful as a
|
||||
# baseline for the integration tests below.
|
||||
FIXTURE_837 = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(tmp_path, monkeypatch):
|
||||
"""Per-test isolated DB.
|
||||
|
||||
Same pattern used by ``test_store.py``. The unit tests below don't
|
||||
touch the DB, but it's autouse'd so a test that flips to integration
|
||||
mode doesn't accidentally inherit stale state.
|
||||
"""
|
||||
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db")
|
||||
from cyclone import db
|
||||
db._reset_for_tests()
|
||||
db.init_db()
|
||||
yield
|
||||
db._reset_for_tests()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client() -> TestClient:
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _validation(passed: bool = True) -> ValidationReport:
|
||||
return ValidationReport(passed=passed, errors=[], warnings=[])
|
||||
|
||||
|
||||
def _make_claim_837(
|
||||
*,
|
||||
claim_id: str = "CLM-1",
|
||||
charge: str = "124.00",
|
||||
patient_first: str = "Jane",
|
||||
patient_last: str = "Doe",
|
||||
frequency_code: str = "1",
|
||||
service_date: date | None = None,
|
||||
cpt_code: str = "99213",
|
||||
validation: ValidationReport | None = None,
|
||||
) -> ClaimOutput:
|
||||
service_lines = []
|
||||
if cpt_code or service_date is not None:
|
||||
service_lines.append(
|
||||
ServiceLine(
|
||||
line_number=1,
|
||||
procedure=Procedure(qualifier="HC", code=cpt_code, modifiers=[]),
|
||||
charge=Decimal(charge),
|
||||
units=Decimal("1"),
|
||||
service_date=service_date,
|
||||
)
|
||||
)
|
||||
return ClaimOutput(
|
||||
claim_id=claim_id,
|
||||
control_number="0001",
|
||||
transaction_date=date(2026, 6, 19),
|
||||
billing_provider=BillingProvider(name="Test Clinic", npi="1234567890"),
|
||||
subscriber=Subscriber(
|
||||
first_name=patient_first,
|
||||
last_name=patient_last,
|
||||
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=frequency_code,
|
||||
place_of_service="11",
|
||||
),
|
||||
diagnoses=[],
|
||||
service_lines=service_lines,
|
||||
validation=validation if validation is not None else _validation(),
|
||||
raw_segments=[],
|
||||
)
|
||||
|
||||
|
||||
def _make_record_837(claims: list[ClaimOutput], *, batch_id: str = "b-a") -> BatchRecord:
|
||||
return BatchRecord(
|
||||
id=batch_id,
|
||||
kind="837p",
|
||||
input_filename=f"{batch_id}.837",
|
||||
parsed_at=datetime(2026, 6, 19, 12, 0, tzinfo=timezone.utc),
|
||||
result=ParseResult(
|
||||
envelope=Envelope(
|
||||
sender_id="S", receiver_id="R", control_number="0001",
|
||||
transaction_date=date(2026, 6, 19),
|
||||
),
|
||||
claims=claims,
|
||||
summary=BatchSummary(
|
||||
input_file=f"{batch_id}.837", control_number="0001",
|
||||
transaction_date=date(2026, 6, 19),
|
||||
total_claims=len(claims),
|
||||
passed=len(claims), failed=0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _make_remit_835(
|
||||
*,
|
||||
pccn: str = "R-1",
|
||||
charge: str = "100.00",
|
||||
status_code: str = "1",
|
||||
service_date: date | None = None,
|
||||
procedure_code: str = "99213",
|
||||
) -> ClaimPayment:
|
||||
service_payments = []
|
||||
if procedure_code or service_date is not None:
|
||||
service_payments.append(
|
||||
ServicePayment(
|
||||
line_number=1,
|
||||
procedure_qualifier="HC",
|
||||
procedure_code=procedure_code,
|
||||
modifiers=[],
|
||||
charge=Decimal(charge),
|
||||
payment=Decimal("0"),
|
||||
service_date=service_date,
|
||||
adjustments=[],
|
||||
)
|
||||
)
|
||||
return ClaimPayment(
|
||||
payer_claim_control_number=pccn,
|
||||
status_code=status_code,
|
||||
status_label=None,
|
||||
total_charge=Decimal(charge),
|
||||
total_paid=Decimal("0"),
|
||||
service_payments=service_payments,
|
||||
raw_segments=[],
|
||||
)
|
||||
|
||||
|
||||
def _make_record_835(
|
||||
remits: list[ClaimPayment], *, batch_id: str = "b-b",
|
||||
) -> BatchRecord:
|
||||
return BatchRecord(
|
||||
id=batch_id,
|
||||
kind="835",
|
||||
input_filename=f"{batch_id}.835",
|
||||
parsed_at=datetime(2026, 6, 19, 13, 0, tzinfo=timezone.utc),
|
||||
result=ParseResult835(
|
||||
envelope=Envelope(
|
||||
sender_id="P", receiver_id="R", control_number="0002",
|
||||
transaction_date=date(2026, 6, 19),
|
||||
),
|
||||
financial_info=FinancialInfo(
|
||||
handling_code="C", paid_amount=Decimal("0"),
|
||||
credit_debit_flag="C",
|
||||
),
|
||||
trace=ReassociationTrace(
|
||||
trace_type_code="1", trace_number="1",
|
||||
originating_company_id="P1",
|
||||
),
|
||||
payer=Payer835(name="Payer"),
|
||||
payee=Payee835(name="Clinic", npi="1234567890"),
|
||||
claims=remits,
|
||||
summary=BatchSummary(
|
||||
input_file=f"{batch_id}.835", control_number="0002",
|
||||
transaction_date=date(2026, 6, 19),
|
||||
total_claims=len(remits),
|
||||
passed=len(remits), failed=0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure unit tests — diff_batches
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_empty_batches_have_no_deltas():
|
||||
a = _make_record_837([], batch_id="b-a")
|
||||
b = _make_record_837([], batch_id="b-b")
|
||||
result = diff_batches(a, b)
|
||||
assert result.added == []
|
||||
assert result.removed == []
|
||||
assert result.changed == []
|
||||
summary = result.summary
|
||||
assert summary == {
|
||||
"addedCount": 0, "removedCount": 0,
|
||||
"changedCount": 0, "unchangedCount": 0,
|
||||
}
|
||||
|
||||
|
||||
def test_identical_batches_have_no_deltas():
|
||||
claims = [
|
||||
_make_claim_837(claim_id="CLM-1", charge="124.00", service_date=date(2026, 6, 1)),
|
||||
_make_claim_837(claim_id="CLM-2", charge="88.50", service_date=date(2026, 6, 2)),
|
||||
]
|
||||
a = _make_record_837(claims, batch_id="b-a")
|
||||
b = _make_record_837(claims, batch_id="b-b")
|
||||
result = diff_batches(a, b)
|
||||
assert result.added == []
|
||||
assert result.removed == []
|
||||
assert result.changed == []
|
||||
assert result.summary["unchangedCount"] == 2
|
||||
|
||||
|
||||
def test_claim_added_in_b():
|
||||
a = _make_record_837([_make_claim_837(claim_id="CLM-1")], batch_id="b-a")
|
||||
b = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1"), _make_claim_837(claim_id="CLM-2")],
|
||||
batch_id="b-b",
|
||||
)
|
||||
result = diff_batches(a, b)
|
||||
assert [s.claim_id for s in result.added] == ["CLM-2"]
|
||||
assert result.removed == []
|
||||
assert result.changed == []
|
||||
assert result.summary["addedCount"] == 1
|
||||
assert result.summary["unchangedCount"] == 1
|
||||
|
||||
|
||||
def test_claim_removed_from_a():
|
||||
a = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1"), _make_claim_837(claim_id="CLM-2")],
|
||||
batch_id="b-a",
|
||||
)
|
||||
b = _make_record_837([_make_claim_837(claim_id="CLM-1")], batch_id="b-b")
|
||||
result = diff_batches(a, b)
|
||||
assert result.added == []
|
||||
assert [s.claim_id for s in result.removed] == ["CLM-2"]
|
||||
assert result.summary["removedCount"] == 1
|
||||
|
||||
|
||||
def test_claim_charge_change_is_a_diff():
|
||||
a = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1", charge="100.00")], batch_id="b-a",
|
||||
)
|
||||
b = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1", charge="125.00")], batch_id="b-b",
|
||||
)
|
||||
result = diff_batches(a, b)
|
||||
assert result.added == [] and result.removed == []
|
||||
assert len(result.changed) == 1
|
||||
fields = [d.field for d in result.changed[0].diff]
|
||||
assert "total_charge" in fields
|
||||
charge_diff = next(d for d in result.changed[0].diff if d.field == "total_charge")
|
||||
assert charge_diff.a == 100.00
|
||||
assert charge_diff.b == 125.00
|
||||
|
||||
|
||||
def test_sub_cent_charge_jitter_is_not_a_diff():
|
||||
"""Two charges that differ by less than a cent register as equal.
|
||||
|
||||
Decimal → float can introduce sub-cent noise when the source had
|
||||
more than 2 decimal places. Tolerance is 1e-2 to match the
|
||||
production ``_eq_charge`` helper.
|
||||
"""
|
||||
a = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1", charge="100.00")], batch_id="b-a",
|
||||
)
|
||||
# 100.004 rounds to 100.00 in the float projection but is a fresh
|
||||
# value when summed — surface this as a delta only if the diff
|
||||
# exceeds the 1e-2 tolerance.
|
||||
b = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1", charge="100.005")], batch_id="b-b",
|
||||
)
|
||||
result = diff_batches(a, b)
|
||||
assert result.changed == [], (
|
||||
"sub-cent float noise should not register as a delta"
|
||||
)
|
||||
|
||||
|
||||
def test_status_change_is_a_diff():
|
||||
"""Validation-derived status flows through to the diff.
|
||||
|
||||
A claim that flipped from passing to failing should surface a
|
||||
``status`` field diff between the two sides.
|
||||
"""
|
||||
passing = _make_claim_837(
|
||||
claim_id="CLM-1", validation=_validation(passed=True),
|
||||
)
|
||||
failing = _make_claim_837(
|
||||
claim_id="CLM-1", validation=ValidationReport(
|
||||
passed=False,
|
||||
errors=[],
|
||||
warnings=[],
|
||||
),
|
||||
)
|
||||
a = _make_record_837([passing], batch_id="b-a")
|
||||
b = _make_record_837([failing], batch_id="b-b")
|
||||
result = diff_batches(a, b)
|
||||
assert len(result.changed) == 1
|
||||
fields = [d.field for d in result.changed[0].diff]
|
||||
assert "status" in fields
|
||||
|
||||
|
||||
def test_service_date_change_is_a_diff():
|
||||
a = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1", service_date=date(2026, 6, 1))],
|
||||
batch_id="b-a",
|
||||
)
|
||||
b = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1", service_date=date(2026, 6, 2))],
|
||||
batch_id="b-b",
|
||||
)
|
||||
result = diff_batches(a, b)
|
||||
assert len(result.changed) == 1
|
||||
fields = [d.field for d in result.changed[0].diff]
|
||||
assert "service_date" in fields
|
||||
|
||||
|
||||
def test_cpt_change_is_a_diff():
|
||||
a = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1", cpt_code="99213")], batch_id="b-a",
|
||||
)
|
||||
b = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1", cpt_code="99214")], batch_id="b-b",
|
||||
)
|
||||
result = diff_batches(a, b)
|
||||
assert len(result.changed) == 1
|
||||
fields = [d.field for d in result.changed[0].diff]
|
||||
assert "cpt_codes" in fields
|
||||
|
||||
|
||||
def test_cpt_reorder_is_not_a_diff():
|
||||
"""CPT codes compared as sorted, deduplicated sets.
|
||||
|
||||
Two claims with the same codes in different SV1 order should
|
||||
not register as a diff — that would generate noise on every
|
||||
service-line reordering.
|
||||
"""
|
||||
a = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1", cpt_code="99213")], batch_id="b-a",
|
||||
)
|
||||
b = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1", cpt_code="99213")], batch_id="b-b",
|
||||
)
|
||||
result = diff_batches(a, b)
|
||||
assert result.changed == []
|
||||
|
||||
|
||||
def test_mixed_added_removed_changed():
|
||||
a = _make_record_837(
|
||||
[
|
||||
_make_claim_837(claim_id="CLM-1", charge="100.00"),
|
||||
_make_claim_837(claim_id="CLM-2", charge="200.00"),
|
||||
_make_claim_837(claim_id="CLM-3", charge="300.00"),
|
||||
],
|
||||
batch_id="b-a",
|
||||
)
|
||||
b = _make_record_837(
|
||||
[
|
||||
_make_claim_837(claim_id="CLM-1", charge="150.00"), # changed
|
||||
_make_claim_837(claim_id="CLM-3", charge="300.00"), # unchanged
|
||||
_make_claim_837(claim_id="CLM-4", charge="400.00"), # added
|
||||
],
|
||||
batch_id="b-b",
|
||||
)
|
||||
result = diff_batches(a, b)
|
||||
assert [s.claim_id for s in result.added] == ["CLM-4"]
|
||||
assert [s.claim_id for s in result.removed] == ["CLM-2"]
|
||||
assert [c.a.claim_id for c in result.changed] == ["CLM-1"]
|
||||
summary = result.summary
|
||||
assert summary["addedCount"] == 1
|
||||
assert summary["removedCount"] == 1
|
||||
assert summary["changedCount"] == 1
|
||||
assert summary["unchangedCount"] == 1
|
||||
|
||||
|
||||
def test_wire_dict_strips_internal_claim_ids():
|
||||
a = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1")], batch_id="b-a",
|
||||
)
|
||||
b = _make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1"), _make_claim_837(claim_id="CLM-2")],
|
||||
batch_id="b-b",
|
||||
)
|
||||
wire = diff_batches_to_wire(a, b)
|
||||
assert "claim_ids" not in wire["a"]
|
||||
assert "claim_ids" not in wire["b"]
|
||||
# Public shape — every key documented in the spec.
|
||||
assert set(wire.keys()) == {"a", "b", "added", "removed", "changed", "summary"}
|
||||
assert set(wire["a"].keys()) == {
|
||||
"id", "kind", "parsedAt", "inputFilename", "claimCount",
|
||||
}
|
||||
assert wire["a"]["claimCount"] == 1
|
||||
assert wire["b"]["claimCount"] == 2
|
||||
|
||||
|
||||
def test_mixed_kind_diff_matches_by_claim_id():
|
||||
"""837P and 835 can be diffed; the matcher uses ``claim_id``.
|
||||
|
||||
The common case for a single kind pair is covered by the other
|
||||
tests. Here we exercise the cross-kind path so an operator can
|
||||
diff submitted claims against received remittances.
|
||||
"""
|
||||
a = _make_record_837(
|
||||
[
|
||||
_make_claim_837(claim_id="CLM-1", charge="100.00"),
|
||||
_make_claim_837(claim_id="CLM-2", charge="200.00"),
|
||||
],
|
||||
batch_id="b-a",
|
||||
)
|
||||
b = _make_record_835(
|
||||
[
|
||||
_make_remit_835(pccn="CLM-1", charge="100.00", status_code="1"),
|
||||
_make_remit_835(pccn="CLM-3", charge="300.00", status_code="1"),
|
||||
],
|
||||
batch_id="b-b",
|
||||
)
|
||||
result = diff_batches(a, b)
|
||||
# CLM-1 in both, no field change → unchanged
|
||||
# CLM-2 in A only → removed
|
||||
# CLM-3 in B only → added
|
||||
assert [s.claim_id for s in result.added] == ["CLM-3"]
|
||||
assert [s.claim_id for s in result.removed] == ["CLM-2"]
|
||||
assert result.changed == []
|
||||
assert result.summary["unchangedCount"] == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integration tests — /api/batch-diff
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _ingest_837(client: TestClient, text: str, filename: str = "x.837") -> str:
|
||||
resp = client.post(
|
||||
"/api/parse-837",
|
||||
files={"file": (filename, text, "text/plain")},
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
return resp.json()["claims"][0]["claim_id"] # used for sanity
|
||||
|
||||
|
||||
def test_endpoint_404_when_a_missing(client: TestClient):
|
||||
"""Missing ``a`` → 404, never 500.
|
||||
|
||||
Mirrors the existing ``GET /api/batches/{id}`` 404 contract.
|
||||
"""
|
||||
resp = client.get("/api/batch-diff?a=ghost&b=also-ghost")
|
||||
assert resp.status_code == 404
|
||||
body = resp.json()
|
||||
assert body["detail"]["error"] == "Not found"
|
||||
assert "ghost" in body["detail"]["detail"]
|
||||
|
||||
|
||||
def test_endpoint_400_when_params_missing(client: TestClient):
|
||||
"""Missing query params → 400 with a helpful detail message."""
|
||||
resp = client.get("/api/batch-diff?a=only")
|
||||
assert resp.status_code == 400
|
||||
body = resp.json()
|
||||
assert body["detail"]["error"] == "Missing param"
|
||||
assert "b" in body["detail"]["detail"].lower()
|
||||
|
||||
|
||||
def test_endpoint_returns_diff_between_two_ingested_batches(client: TestClient):
|
||||
"""End-to-end: POST two 837 files, GET the diff, assert the shape.
|
||||
|
||||
The fixture file ships with 2 claims; we ingest it twice. The
|
||||
diff should be empty (the two batches are identical) and the
|
||||
unchangedCount should equal 2.
|
||||
"""
|
||||
text = FIXTURE_837.read_text()
|
||||
r1 = client.post(
|
||||
"/api/parse-837",
|
||||
files={"file": ("first.837", text, "text/plain")},
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
assert r1.status_code == 200, r1.text
|
||||
a_id = r1.json()["claims"][0]["claim_id"] # sanity: parse worked
|
||||
|
||||
r2 = client.post(
|
||||
"/api/parse-837",
|
||||
files={"file": ("second.837", text, "text/plain")},
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
assert r2.status_code == 200, r2.text
|
||||
|
||||
# Resolve the actual batch ids via /api/batches.
|
||||
batches = client.get("/api/batches").json()["items"]
|
||||
assert len(batches) == 2
|
||||
a_batch_id = batches[0]["id"]
|
||||
b_batch_id = batches[1]["id"]
|
||||
|
||||
resp = client.get(
|
||||
f"/api/batch-diff?a={a_batch_id}&b={b_batch_id}",
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
body = resp.json()
|
||||
assert body["a"]["id"] == a_batch_id
|
||||
assert body["b"]["id"] == b_batch_id
|
||||
assert body["a"]["kind"] == "837p"
|
||||
assert body["b"]["kind"] == "837p"
|
||||
assert body["added"] == []
|
||||
assert body["removed"] == []
|
||||
assert body["changed"] == []
|
||||
# Spec summary keys are all present and numeric.
|
||||
summary = body["summary"]
|
||||
assert summary["addedCount"] == 0
|
||||
assert summary["removedCount"] == 0
|
||||
assert summary["changedCount"] == 0
|
||||
assert summary["unchangedCount"] == 2
|
||||
|
||||
# Reference the parsed claim id once so the fixture's first claim
|
||||
# id is exercised end-to-end (the lint otherwise complains about
|
||||
# the unused variable).
|
||||
assert a_id
|
||||
|
||||
|
||||
def test_endpoint_includes_added_removed_summary_keys(client: TestClient):
|
||||
"""Diff summary keys are documented and always present.
|
||||
|
||||
Even when zero, the JSON contract should expose every key so the
|
||||
UI can render the four cards unconditionally.
|
||||
"""
|
||||
text = FIXTURE_837.read_text()
|
||||
r1 = client.post(
|
||||
"/api/parse-837",
|
||||
files={"file": ("first.837", text, "text/plain")},
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
r2 = client.post(
|
||||
"/api/parse-837",
|
||||
files={"file": ("second.837", text, "text/plain")},
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
assert r1.status_code == r2.status_code == 200
|
||||
batches = client.get("/api/batches").json()["items"]
|
||||
a_batch_id, b_batch_id = batches[0]["id"], batches[1]["id"]
|
||||
body = client.get(
|
||||
f"/api/batch-diff?a={a_batch_id}&b={b_batch_id}",
|
||||
).json()
|
||||
assert set(body["summary"].keys()) == {
|
||||
"addedCount", "removedCount", "changedCount", "unchangedCount",
|
||||
}
|
||||
|
||||
|
||||
def test_store_helper_load_two_for_diff_raises_lookup():
|
||||
"""``CycloneStore.load_two_for_diff`` raises ``LookupError`` on
|
||||
missing ids so the API layer can map to 404 cleanly.
|
||||
"""
|
||||
from cyclone.store import CycloneStore
|
||||
s = CycloneStore()
|
||||
s.add(_make_record_837(
|
||||
[_make_claim_837(claim_id="CLM-1")],
|
||||
batch_id="present",
|
||||
))
|
||||
with pytest.raises(LookupError):
|
||||
s.load_two_for_diff("present", "ghost")
|
||||
with pytest.raises(LookupError):
|
||||
s.load_two_for_diff("ghost", "present")
|
||||
# Happy path
|
||||
a, b = s.load_two_for_diff("present", "present")
|
||||
assert a.id == "present"
|
||||
assert b.id == "present"
|
||||
|
||||
|
||||
# Silence the unused-import linter for items used purely as type hints
|
||||
# or fixture anchors.
|
||||
void_global_store = global_store
|
||||
Reference in New Issue
Block a user