feat(backend): fix T4 index drift, lazy import, FK policy, expand test coverage

This commit is contained in:
Tyler
2026-06-19 21:42:11 -06:00
parent 0fdb760bc1
commit 2019a651ed
2 changed files with 96 additions and 6 deletions
+19 -6
View File
@@ -11,6 +11,7 @@ same engine.
from __future__ import annotations from __future__ import annotations
import enum import enum
import json
import os import os
from datetime import date, datetime from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
@@ -28,6 +29,7 @@ from sqlalchemy import (
String, String,
Text, Text,
UniqueConstraint, UniqueConstraint,
text,
) )
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker
from sqlalchemy.types import TypeDecorator from sqlalchemy.types import TypeDecorator
@@ -141,15 +143,11 @@ class JSONText(TypeDecorator):
def process_bind_param(self, value, dialect): # type: ignore[no-untyped-def] def process_bind_param(self, value, dialect): # type: ignore[no-untyped-def]
if value is None: if value is None:
return None return None
import json
return json.dumps(value) return json.dumps(value)
def process_result_value(self, value, dialect): # type: ignore[no-untyped-def] def process_result_value(self, value, dialect): # type: ignore[no-untyped-def]
if value is None: if value is None:
return None return None
import json
return json.loads(value) return json.loads(value)
@@ -172,8 +170,13 @@ class Batch(Base):
) )
__table_args__ = ( __table_args__ = (
Index("ix_batches_parsed_at", "parsed_at"), # Migration is the source of truth: 0001_initial.sql declares this DESC.
# `text(...)` keeps the ORM in sync so fresh DBs built via `create_all`
# alone (no migration runner) get the same index direction as prod.
Index("ix_batches_parsed_at", text("parsed_at DESC")),
) )
# TODO(T6): when ActivityEvent model is added, mirror this pattern for
# `ix_activity_events_ts` on activity_events(ts DESC) per 0001_initial.sql:86.
class Claim(Base): class Claim(Base):
@@ -196,7 +199,17 @@ class Claim(Base):
Enum(ClaimState, native_enum=False), nullable=True Enum(ClaimState, native_enum=False), nullable=True
) )
matched_remittance_id: Mapped[Optional[str]] = mapped_column( matched_remittance_id: Mapped[Optional[str]] = mapped_column(
String(64), ForeignKey("remittances.id"), nullable=True # ORM policy: SET NULL on remittance delete. The claim may outlive its
# remittance during reversal/reimport flows; without this, deleting a
# remittance with matched claims raises IntegrityError.
# Note: 0001_initial.sql predates this policy (no ON DELETE clause).
# SQLite ignores FK direction by default unless PRAGMA foreign_keys=ON,
# so the inconsistency is benign there; the ORM is the source of truth
# for PostgreSQL/test environments with FK enforcement. Amendment to the
# migration is deferred to post-SQLite rollout.
String(64),
ForeignKey("remittances.id", ondelete="SET NULL"),
nullable=True,
) )
raw_json: Mapped[Optional[dict]] = mapped_column(JSONText, nullable=True) raw_json: Mapped[Optional[dict]] = mapped_column(JSONText, nullable=True)
+77
View File
@@ -78,3 +78,80 @@ def test_create_and_query_claim_with_default_state():
assert loaded.state == ClaimState.SUBMITTED assert loaded.state == ClaimState.SUBMITTED
assert loaded.charge_amount == Decimal("124.00") assert loaded.charge_amount == Decimal("124.00")
assert loaded.service_date_from == date(2026, 6, 1) assert loaded.service_date_from == date(2026, 6, 1)
def test_claim_uses_db_default_when_state_omitted():
"""The `state` column has a DB-side DEFAULT of 'submitted' (per migration
and ORM `default=`). When the caller omits `state=`, the loaded value
must be ClaimState.SUBMITTED, not None."""
with db.SessionLocal()() as s:
s.add(Batch(
id="b1", kind="837p", input_filename="x",
parsed_at=datetime(2026, 6, 19, tzinfo=timezone.utc),
))
c = Claim(
id="CLM-1",
batch_id="b1",
patient_control_number="CLM-1",
charge_amount=Decimal("0"),
)
s.add(c)
s.commit()
with db.SessionLocal()() as s:
loaded = s.get(Claim, "CLM-1")
assert loaded is not None
assert loaded.state == ClaimState.SUBMITTED
def test_state_before_reversal_round_trips():
"""`state_before_reversal` is nullable but stores a ClaimState enum.
Setting it to PAID must round-trip as ClaimState.PAID after commit+reload."""
with db.SessionLocal()() as s:
s.add(Batch(
id="b1", kind="837p", input_filename="x",
parsed_at=datetime(2026, 6, 19, tzinfo=timezone.utc),
))
c = Claim(
id="CLM-1",
batch_id="b1",
patient_control_number="CLM-1",
charge_amount=Decimal("50.00"),
state_before_reversal=ClaimState.PAID,
)
s.add(c)
s.commit()
with db.SessionLocal()() as s:
loaded = s.get(Claim, "CLM-1")
assert loaded is not None
assert loaded.state_before_reversal == ClaimState.PAID
def test_batch_cascade_delete_drops_claims():
"""Deleting a Batch must cascade-delete its claims (FK is ON DELETE CASCADE
in migration; ORM relationship uses cascade='all, delete-orphan' which makes
SQLAlchemy emit the child DELETE at flush time even without
PRAGMA foreign_keys=ON)."""
with db.SessionLocal()() as s:
s.add(Batch(
id="b1", kind="837p", input_filename="x",
parsed_at=datetime(2026, 6, 19, tzinfo=timezone.utc),
))
s.add(Claim(
id="CLM-1",
batch_id="b1",
patient_control_number="CLM-1",
charge_amount=Decimal("0"),
))
s.commit()
with db.SessionLocal()() as s:
batch = s.get(Batch, "b1")
assert batch is not None
s.delete(batch)
s.commit()
with db.SessionLocal()() as s:
assert s.get(Batch, "b1") is None
assert s.get(Claim, "CLM-1") is None