feat(backend): add Match + ActivityEvent ORM models
This commit is contained in:
@@ -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"),
|
||||||
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user