feat(backend): add Match + ActivityEvent ORM models

This commit is contained in:
Tyler
2026-06-19 21:53:52 -06:00
parent 846944448a
commit 05a49be964
2 changed files with 110 additions and 3 deletions
+43 -2
View File
@@ -177,8 +177,6 @@ class Batch(Base):
# alone (no migration runner) get the same index direction as prod. # alone (no migration runner) get the same index direction as prod.
Index("ix_batches_parsed_at", text("parsed_at DESC")), 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):
@@ -283,3 +281,46 @@ class CasAdjustment(Base):
__table_args__ = ( __table_args__ = (
Index("ix_cas_adjustments_remittance_id", "remittance_id"), Index("ix_cas_adjustments_remittance_id", "remittance_id"),
) )
class Match(Base):
__tablename__ = "matches"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
claim_id: Mapped[str] = mapped_column(
String(64), ForeignKey("claims.id", ondelete="CASCADE"),
nullable=False, unique=True,
)
remittance_id: Mapped[str] = mapped_column(
String(64), ForeignKey("remittances.id", ondelete="CASCADE"), nullable=False
)
strategy: Mapped[str] = mapped_column(String(16), nullable=False)
matched_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
prior_claim_state: Mapped[Optional[ClaimState]] = mapped_column(
Enum(ClaimState, native_enum=False), nullable=True
)
is_reversal: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
__table_args__ = (
Index("ix_matches_remittance_id", "remittance_id"),
Index("ix_matches_matched_at", "matched_at"),
)
class ActivityEvent(Base):
__tablename__ = "activity_events"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
kind: Mapped[str] = mapped_column(String(32), nullable=False)
batch_id: Mapped[Optional[str]] = mapped_column(String(32), nullable=True)
claim_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
remittance_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
payload_json: Mapped[Optional[dict]] = mapped_column(JSONText, nullable=True)
__table_args__ = (
# Migration declares this as DESC; mirror the qualifier so create_all
# on a fresh DB (without migrations) produces identical DDL.
Index("ix_activity_events_ts", text("ts DESC")),
Index("ix_activity_events_kind", "kind"),
)
+67 -1
View File
@@ -10,7 +10,7 @@ import pytest
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from cyclone import db from cyclone import db
from cyclone.db import Base, Batch, CasAdjustment, Claim, ClaimState, Remittance from cyclone.db import ActivityEvent, Base, Batch, CasAdjustment, Claim, ClaimState, Match, Remittance
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@@ -293,3 +293,69 @@ def test_remittance_cascade_delete_drops_cas_adjustments():
select(CasAdjustment).where(CasAdjustment.remittance_id == "CLP-1") select(CasAdjustment).where(CasAdjustment.remittance_id == "CLP-1")
).all() ).all()
assert len(cas) == 0 assert len(cas) == 0
def test_create_match_and_activity_event():
with db.SessionLocal()() as s:
s.add(Batch(
id="b1", kind="835", 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("124.00"), state=ClaimState.SUBMITTED,
))
s.add(Remittance(
id="CLP-1", batch_id="b1", payer_claim_control_number="CLM-1",
status_code="1", total_charge=Decimal("124.00"), total_paid=Decimal("124.00"),
received_at=datetime(2026, 6, 19, tzinfo=timezone.utc),
))
m = Match(
claim_id="CLM-1", remittance_id="CLP-1",
strategy="auto", is_reversal=False,
matched_at=datetime(2026, 6, 19, 13, 0, tzinfo=timezone.utc),
)
s.add(m)
s.flush()
s.add(ActivityEvent(
ts=datetime(2026, 6, 19, 13, 0, tzinfo=timezone.utc),
kind="reconcile", batch_id="b1", claim_id="CLM-1",
payload_json={"matched": 1},
))
s.commit()
with db.SessionLocal()() as s:
loaded = s.get(Match, 1) # autoincrement id
assert loaded is not None
assert loaded.strategy == "auto"
assert loaded.is_reversal is False
events = s.query(ActivityEvent).all()
assert len(events) == 1
assert events[0].kind == "reconcile"
assert events[0].payload_json == {"matched": 1}
def test_match_unique_per_claim():
"""A Claim can only have one current Match row."""
import sqlalchemy.exc
with db.SessionLocal()() as s:
s.add(Batch(
id="b1", kind="835", 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"), state=ClaimState.SUBMITTED))
s.add(Remittance(id="CLP-1", batch_id="b1", payer_claim_control_number="x",
status_code="1", received_at=datetime.now(timezone.utc)))
s.add(Remittance(id="CLP-2", batch_id="b1", payer_claim_control_number="y",
status_code="1", received_at=datetime.now(timezone.utc)))
s.add(Match(claim_id="CLM-1", remittance_id="CLP-1", strategy="auto",
matched_at=datetime.now(timezone.utc)))
s.commit()
s.add(Match(claim_id="CLM-1", remittance_id="CLP-2", strategy="manual",
matched_at=datetime.now(timezone.utc)))
with pytest.raises(sqlalchemy.exc.IntegrityError):
s.commit()
s.rollback()