feat(sp31): add _score_fallback_candidates DB helper with ±30-day candidate pool
This commit is contained in:
+137
-1
@@ -14,6 +14,9 @@ test_api_parse_persists.py) working unchanged.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -89,4 +92,137 @@ def _reset_rate_limit_buckets(app) -> None:
|
||||
cur = getattr(cur, "app", None)
|
||||
# No RateLimitMiddleware in the stack — nothing to reset. Should
|
||||
# not happen in this codebase (security.py registers it at boot)
|
||||
# but we don't want a missing reset to crash unrelated tests.
|
||||
# but we don't want a missing reset to crash unrelated tests.
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SP31: shared DB-session + Claim/Remit factory fixtures.
|
||||
#
|
||||
# `db_session` yields a fresh session per test against the per-test DB set
|
||||
# up by the autouse `_auto_init_db` fixture above. The session rolls back at
|
||||
# teardown so a test that mutates rows doesn't leak into siblings.
|
||||
#
|
||||
# `make_claim` / `make_remit` build ORM rows with a small ergonomic surface:
|
||||
# the planned parameter names follow the content-keys helper (PCN, charge,
|
||||
# rendering NPI), but the ORM attribute names differ (`charge_amount` on
|
||||
# Claim, `total_charge` on Remittance, `provider_npi` on Claim — and no
|
||||
# `payer_id` / `rendering_provider_npi` column on Remittance at all). The
|
||||
# factories map the planned names onto the real ORM attributes and stash
|
||||
# `rendering_provider_npi` as a transient attribute so
|
||||
# ``reconcile._content_keys_match`` can still read it via ``getattr``.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
"""Yield a fresh SQLAlchemy session, rolling back at teardown."""
|
||||
from cyclone import db as _db
|
||||
session = _db.SessionLocal()()
|
||||
try:
|
||||
yield session
|
||||
session.rollback()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def _ensure_batch(session, batch_id: str = "test-batch") -> None:
|
||||
"""Create the Batch row a Claim/Remittance FKs to (idempotent)."""
|
||||
from cyclone.db import Batch
|
||||
if session.get(Batch, batch_id) is None:
|
||||
session.add(Batch(
|
||||
id=batch_id, kind="837p", input_filename="test.txt",
|
||||
parsed_at=datetime.now(timezone.utc),
|
||||
))
|
||||
session.flush()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_claim(db_session):
|
||||
"""Factory: build & flush a Claim with the planned content-keys params.
|
||||
|
||||
Planned params (match the SP31 spec / content-keys test surface):
|
||||
patient_control_number, total_charge, rendering_provider_npi,
|
||||
service_date_from, matched_remittance_id=None, state=None,
|
||||
claim_id=None
|
||||
|
||||
`state=None` defaults to ``ClaimState.SUBMITTED`` so existing tests
|
||||
that omit it keep behaving. `rendering_provider_npi` is stored as
|
||||
a transient attribute (no ORM column) for ``_content_keys_match``
|
||||
to read via ``getattr``.
|
||||
"""
|
||||
def _make(
|
||||
patient_control_number: str,
|
||||
total_charge,
|
||||
rendering_provider_npi: str,
|
||||
service_date_from,
|
||||
matched_remittance_id=None,
|
||||
state=None,
|
||||
claim_id: str | None = None,
|
||||
):
|
||||
from cyclone.db import Claim, ClaimState as _CS
|
||||
|
||||
_ensure_batch(db_session)
|
||||
cid = claim_id or f"clm-{patient_control_number}-{service_date_from.isoformat()}"
|
||||
c = Claim(
|
||||
id=cid,
|
||||
batch_id="test-batch",
|
||||
patient_control_number=patient_control_number,
|
||||
service_date_from=service_date_from,
|
||||
charge_amount=total_charge,
|
||||
state=state if state is not None else _CS.SUBMITTED,
|
||||
matched_remittance_id=matched_remittance_id,
|
||||
)
|
||||
# Transient attribute — read by reconcile._content_keys_match
|
||||
# via getattr; the Claim ORM has no rendering_provider_npi column.
|
||||
c.rendering_provider_npi = rendering_provider_npi
|
||||
db_session.add(c)
|
||||
db_session.flush()
|
||||
return c
|
||||
|
||||
return _make
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_remit(db_session):
|
||||
"""Factory: build & flush a Remittance with the planned content-keys params.
|
||||
|
||||
Planned params:
|
||||
payer_claim_control_number, total_charge_amount, rendering_provider_npi,
|
||||
service_date, remit_id=None
|
||||
|
||||
`total_charge_amount` is mapped to ``Remittance.total_charge`` (real ORM
|
||||
field). `rendering_provider_npi` is stored as a transient attribute for
|
||||
``_content_keys_match`` to read via ``getattr`` (Remittance has no NPI
|
||||
column; the parser reads it from raw_json in production).
|
||||
"""
|
||||
def _make(
|
||||
payer_claim_control_number: str,
|
||||
total_charge_amount,
|
||||
rendering_provider_npi: str,
|
||||
service_date,
|
||||
remit_id: str | None = None,
|
||||
):
|
||||
from cyclone.db import Remittance
|
||||
|
||||
_ensure_batch(db_session)
|
||||
rid = remit_id or f"remit-{payer_claim_control_number}-{service_date.isoformat()}"
|
||||
r = Remittance(
|
||||
id=rid,
|
||||
batch_id="test-batch",
|
||||
payer_claim_control_number=payer_claim_control_number,
|
||||
status_code="1",
|
||||
total_charge=total_charge_amount,
|
||||
total_paid=Decimal("0"),
|
||||
received_at=datetime.now(timezone.utc),
|
||||
service_date=service_date,
|
||||
is_reversal=False,
|
||||
)
|
||||
# Transient attribute — read by reconcile._content_keys_match via
|
||||
# getattr. The Remittance ORM has no rendering_provider_npi column;
|
||||
# the 835 parser reads it from raw_json in production.
|
||||
r.rendering_provider_npi = rendering_provider_npi
|
||||
db_session.add(r)
|
||||
db_session.flush()
|
||||
return r
|
||||
|
||||
return _make
|
||||
Reference in New Issue
Block a user