feat(backend): add Batch + Claim ORM models + ClaimState enum
This commit is contained in:
+129
-2
@@ -9,11 +9,27 @@ re-uses the same engine.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import os
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
||||
from sqlalchemy import (
|
||||
Date,
|
||||
DateTime,
|
||||
Enum,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Numeric,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
)
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker
|
||||
from sqlalchemy.types import TypeDecorator
|
||||
|
||||
|
||||
DEFAULT_DB_PATH = Path.home() / ".local" / "share" / "cyclone" / "cyclone.db"
|
||||
@@ -63,8 +79,11 @@ def init_db() -> None:
|
||||
# Import here to avoid circular imports with models.
|
||||
from cyclone import db_migrate # noqa: WPS433 (deliberate late import)
|
||||
|
||||
Base.metadata.create_all(_engine)
|
||||
# Run migrations first so the schema is the source of truth.
|
||||
# `create_all` below is a safety net for tests that may not run
|
||||
# the migration runner; with the migration applied, it is a no-op.
|
||||
db_migrate.run(_engine)
|
||||
Base.metadata.create_all(_engine)
|
||||
|
||||
_SessionLocal = sessionmaker(
|
||||
bind=_engine,
|
||||
@@ -95,3 +114,111 @@ def SessionLocal() -> sessionmaker[sa.orm.Session]:
|
||||
if _SessionLocal is None:
|
||||
raise RuntimeError("db.init_db() has not been called")
|
||||
return _SessionLocal
|
||||
|
||||
|
||||
class ClaimState(str, enum.Enum):
|
||||
SUBMITTED = "submitted"
|
||||
RECEIVED = "received"
|
||||
PAID = "paid"
|
||||
PARTIAL = "partial"
|
||||
DENIED = "denied"
|
||||
RECONCILED = "reconciled"
|
||||
REVERSED = "reversed"
|
||||
|
||||
|
||||
class JSONText(TypeDecorator):
|
||||
"""Stores a dict as a JSON string in a TEXT column.
|
||||
|
||||
Matches the migration's `totals_json TEXT`, `validation_json TEXT`,
|
||||
`raw_result_json TEXT`, `raw_json TEXT` columns. The application
|
||||
code works with dicts; the database stores JSON strings.
|
||||
"""
|
||||
|
||||
impl = Text
|
||||
cache_ok = True
|
||||
|
||||
def process_bind_param(self, value, dialect): # type: ignore[no-untyped-def]
|
||||
if value is None:
|
||||
return None
|
||||
import json
|
||||
|
||||
return json.dumps(value)
|
||||
|
||||
def process_result_value(self, value, dialect): # type: ignore[no-untyped-def]
|
||||
if value is None:
|
||||
return None
|
||||
import json
|
||||
|
||||
return json.loads(value)
|
||||
|
||||
|
||||
class Batch(Base):
|
||||
__tablename__ = "batches"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
kind: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||
input_filename: Mapped[str] = mapped_column(String(512), nullable=False)
|
||||
parsed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
totals_json: Mapped[Optional[dict]] = mapped_column(JSONText, nullable=True)
|
||||
validation_json: Mapped[Optional[dict]] = mapped_column(JSONText, nullable=True)
|
||||
raw_result_json: Mapped[Optional[dict]] = mapped_column(JSONText, nullable=True)
|
||||
|
||||
claims: Mapped[list["Claim"]] = relationship(
|
||||
back_populates="batch", cascade="all, delete-orphan"
|
||||
)
|
||||
remittances: Mapped[list["Remittance"]] = relationship(
|
||||
back_populates="batch", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_batches_parsed_at", "parsed_at"),
|
||||
)
|
||||
|
||||
|
||||
class Claim(Base):
|
||||
__tablename__ = "claims"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
batch_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("batches.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
patient_control_number: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
service_date_from: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
|
||||
service_date_to: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
|
||||
charge_amount: Mapped[Decimal] = mapped_column(Numeric(12, 2), nullable=False, default=Decimal("0"))
|
||||
provider_npi: Mapped[Optional[str]] = mapped_column(String(16), nullable=True)
|
||||
payer_id: Mapped[Optional[str]] = mapped_column(String(32), nullable=True)
|
||||
state: Mapped[ClaimState] = mapped_column(
|
||||
Enum(ClaimState, native_enum=False), nullable=False, default=ClaimState.SUBMITTED
|
||||
)
|
||||
state_before_reversal: Mapped[Optional[ClaimState]] = mapped_column(
|
||||
Enum(ClaimState, native_enum=False), nullable=True
|
||||
)
|
||||
matched_remittance_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(64), ForeignKey("remittances.id"), nullable=True
|
||||
)
|
||||
raw_json: Mapped[Optional[dict]] = mapped_column(JSONText, nullable=True)
|
||||
|
||||
batch: Mapped["Batch"] = relationship(back_populates="claims")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("batch_id", "patient_control_number", name="uq_claims_batch_pcn"),
|
||||
Index("ix_claims_state", "state"),
|
||||
Index("ix_claims_patient_control_number", "patient_control_number"),
|
||||
Index("ix_claims_service_date_from", "service_date_from"),
|
||||
)
|
||||
|
||||
|
||||
# Minimal stub for forward relationship/FK resolution. The full Remittance
|
||||
# model (with payer_claim_control_number, status_code, total_charge, etc.)
|
||||
# is defined in Task 5 and will replace this stub. The stub declares only
|
||||
# the columns needed for `Batch.remittances` relationship resolution and
|
||||
# `Claim.matched_remittance_id` FK metadata resolution.
|
||||
class Remittance(Base):
|
||||
__tablename__ = "remittances"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
batch_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("batches.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
batch: Mapped["Batch"] = relationship(back_populates="remittances")
|
||||
|
||||
Reference in New Issue
Block a user