from __future__ import annotations from datetime import UTC, date, datetime from decimal import Decimal from uuid import uuid4 from mousehold_domain import ( AccountType, EventObservationRelation, FinancialEventStatus, FinancialEventType, ImportConnectionStatus, ImportConnectionType, ObservationStatus, PaymentLineStatus, PaymentStatus, PlanningStatus, ProjectStatus, ReconciliationStatus, RecurringCommitmentStatus, RecurringInterval, RelationType, SettlementStatus, SourceType, VisibilityMode, ) from sqlalchemy import ( Boolean, Date, DateTime, ForeignKey, Index, Integer, Numeric, String, Text, UniqueConstraint, ) from sqlalchemy import Enum as SqlEnum from sqlalchemy.dialects.sqlite import JSON from sqlalchemy.orm import Mapped, mapped_column, relationship from .db import Base def new_id() -> str: return str(uuid4()) def utcnow() -> datetime: return datetime.now(UTC) Money = Numeric(14, 2) class Household(Base): __tablename__ = "households" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) name: Mapped[str] = mapped_column(String(200), nullable=False) default_currency: Mapped[str] = mapped_column(String(3), default="EUR", nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) users: Mapped[list[User]] = relationship( back_populates="household", cascade="all, delete-orphan" ) accounts: Mapped[list[Account]] = relationship( back_populates="household", cascade="all, delete-orphan" ) class User(Base): __tablename__ = "users" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) display_name: Mapped[str] = mapped_column(String(200), nullable=False) email: Mapped[str | None] = mapped_column(String(320), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) household: Mapped[Household] = relationship(back_populates="users") login_identities: Mapped[list[LoginIdentity]] = relationship( back_populates="user", cascade="all, delete-orphan" ) class LoginIdentity(Base): __tablename__ = "login_identities" __table_args__ = ( UniqueConstraint("provider", "provider_subject", name="uq_login_provider_subject"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False, index=True) provider: Mapped[str] = mapped_column(String(80), nullable=False) provider_subject: Mapped[str] = mapped_column(String(320), nullable=False) email: Mapped[str | None] = mapped_column(String(320), nullable=True) display_name: Mapped[str | None] = mapped_column(String(200), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) user: Mapped[User] = relationship(back_populates="login_identities") class LocalSession(Base): __tablename__ = "local_sessions" __table_args__ = ( Index("ix_local_session_token", "token_hash"), Index("ix_local_session_user", "user_id", "expires_at"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False, index=True) household_id: Mapped[str] = mapped_column(ForeignKey("households.id"), nullable=False, index=True) token_hash: Mapped[str] = mapped_column(String(64), nullable=False, unique=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) last_seen_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class Account(Base): __tablename__ = "accounts" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) owner_user_id: Mapped[str | None] = mapped_column( ForeignKey("users.id"), nullable=True, index=True ) name: Mapped[str] = mapped_column(String(200), nullable=False) account_type: Mapped[AccountType] = mapped_column(SqlEnum(AccountType), nullable=False) institution_name: Mapped[str | None] = mapped_column(String(200), nullable=True) currency: Mapped[str] = mapped_column(String(3), default="EUR", nullable=False) visibility_mode: Mapped[VisibilityMode] = mapped_column(SqlEnum(VisibilityMode), nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) household: Mapped[Household] = relationship(back_populates="accounts") class RawObservation(Base): __tablename__ = "raw_observations" __table_args__ = ( Index("ix_observation_dedupe", "household_id", "source_type", "raw_hash"), Index("ix_observation_external_refs", "provider_transaction_id", "message_id"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) source_type: Mapped[SourceType] = mapped_column(SqlEnum(SourceType), nullable=False, index=True) source_account_id: Mapped[str | None] = mapped_column( ForeignKey("accounts.id"), nullable=True, index=True ) owner_user_id: Mapped[str | None] = mapped_column( ForeignKey("users.id"), nullable=True, index=True ) observed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) received_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) raw_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True) source_reference: Mapped[str | None] = mapped_column(String(500), nullable=True) provider_transaction_id: Mapped[str | None] = mapped_column( String(200), nullable=True, index=True ) message_id: Mapped[str | None] = mapped_column(String(500), nullable=True, index=True) raw_text: Mapped[str | None] = mapped_column(Text, nullable=True) raw_file_path: Mapped[str | None] = mapped_column(String(1000), nullable=True) raw_payload_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) parsed_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) parser_name: Mapped[str | None] = mapped_column(String(120), nullable=True) parser_version: Mapped[str | None] = mapped_column(String(40), nullable=True) confidence: Mapped[Decimal] = mapped_column(Money, default=Decimal("1.00"), nullable=False) status: Mapped[ObservationStatus] = mapped_column( SqlEnum(ObservationStatus), default=ObservationStatus.NEW, nullable=False ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) event_links: Mapped[list[EventObservationLink]] = relationship( back_populates="observation", cascade="all, delete-orphan" ) class FinancialEvent(Base): __tablename__ = "financial_events" __table_args__ = ( Index("ix_events_household_status_date", "household_id", "status", "event_date"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) source_account_id: Mapped[str | None] = mapped_column( ForeignKey("accounts.id"), nullable=True, index=True ) payment_account_id: Mapped[str | None] = mapped_column( ForeignKey("accounts.id"), nullable=True, index=True ) payer_user_id: Mapped[str | None] = mapped_column( ForeignKey("users.id"), nullable=True, index=True ) event_type: Mapped[FinancialEventType] = mapped_column( SqlEnum(FinancialEventType), nullable=False ) status: Mapped[FinancialEventStatus] = mapped_column( SqlEnum(FinancialEventStatus), default=FinancialEventStatus.CANDIDATE, nullable=False ) event_date: Mapped[date] = mapped_column(Date, nullable=False) booking_date: Mapped[date | None] = mapped_column(Date, nullable=True) due_date: Mapped[date | None] = mapped_column(Date, nullable=True) amount: Mapped[Decimal] = mapped_column(Money, nullable=False) currency: Mapped[str] = mapped_column(String(3), default="EUR", nullable=False) merchant: Mapped[str | None] = mapped_column(String(300), nullable=True) counterparty: Mapped[str | None] = mapped_column(String(300), nullable=True) description: Mapped[str | None] = mapped_column(Text, nullable=True) category_id: Mapped[str | None] = mapped_column( ForeignKey("categories.id"), nullable=True, index=True ) project_id: Mapped[str | None] = mapped_column( ForeignKey("projects.id"), nullable=True, index=True ) created_by_user_id: Mapped[str | None] = mapped_column( ForeignKey("users.id"), nullable=True, index=True ) fingerprint: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False ) observation_links: Mapped[list[EventObservationLink]] = relationship( back_populates="event", cascade="all, delete-orphan" ) split_lines: Mapped[list[SplitLine]] = relationship( back_populates="event", cascade="all, delete-orphan" ) class EventObservationLink(Base): __tablename__ = "event_observation_links" __table_args__ = ( UniqueConstraint( "event_id", "observation_id", "relation", name="uq_event_observation_relation" ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) event_id: Mapped[str] = mapped_column( ForeignKey("financial_events.id"), nullable=False, index=True ) observation_id: Mapped[str] = mapped_column( ForeignKey("raw_observations.id"), nullable=False, index=True ) relation: Mapped[EventObservationRelation] = mapped_column( SqlEnum(EventObservationRelation), nullable=False ) confidence: Mapped[Decimal] = mapped_column(Money, default=Decimal("1.00"), nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) event: Mapped[FinancialEvent] = relationship(back_populates="observation_links") observation: Mapped[RawObservation] = relationship(back_populates="event_links") class EventRelation(Base): __tablename__ = "event_relations" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) from_event_id: Mapped[str] = mapped_column( ForeignKey("financial_events.id"), nullable=False, index=True ) to_event_id: Mapped[str] = mapped_column( ForeignKey("financial_events.id"), nullable=False, index=True ) relation_type: Mapped[RelationType] = mapped_column(SqlEnum(RelationType), nullable=False) confidence: Mapped[Decimal] = mapped_column(Money, default=Decimal("1.00"), nullable=False) created_by: Mapped[str] = mapped_column(String(40), default="system", nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) class ReconciliationDecision(Base): __tablename__ = "reconciliation_decisions" __table_args__ = ( UniqueConstraint("household_id", "suggestion_id", name="uq_reconciliation_decision"), Index("ix_reconciliation_decision_household", "household_id", "decision"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) suggestion_id: Mapped[str] = mapped_column(String(160), nullable=False) decision: Mapped[str] = mapped_column(String(40), nullable=False) kind: Mapped[str] = mapped_column(String(80), nullable=False) relation_type: Mapped[RelationType] = mapped_column(SqlEnum(RelationType), nullable=False) primary_event_id: Mapped[str] = mapped_column( ForeignKey("financial_events.id"), nullable=False, index=True ) confirming_event_id: Mapped[str] = mapped_column( ForeignKey("financial_events.id"), nullable=False, index=True ) confidence: Mapped[Decimal] = mapped_column(Money, default=Decimal("1.00"), nullable=False) actor_user_id: Mapped[str | None] = mapped_column(ForeignKey("users.id"), nullable=True) reason: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False ) class SplitLine(Base): __tablename__ = "split_lines" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) event_id: Mapped[str] = mapped_column( ForeignKey("financial_events.id"), nullable=False, index=True ) user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False, index=True) share_amount: Mapped[Decimal] = mapped_column(Money, nullable=False) share_percent: Mapped[Decimal | None] = mapped_column(Money, nullable=True) reason: Mapped[str] = mapped_column(String(80), default="manual", nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) event: Mapped[FinancialEvent] = relationship(back_populates="split_lines") class Category(Base): __tablename__ = "categories" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) parent_id: Mapped[str | None] = mapped_column(ForeignKey("categories.id"), nullable=True) name: Mapped[str] = mapped_column(String(160), nullable=False) path: Mapped[str] = mapped_column(String(500), nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) class Tag(Base): __tablename__ = "tags" __table_args__ = (UniqueConstraint("household_id", "name", name="uq_tag_household_name"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) name: Mapped[str] = mapped_column(String(120), nullable=False) class EventTag(Base): __tablename__ = "event_tags" __table_args__ = (UniqueConstraint("event_id", "tag_id", name="uq_event_tag"),) event_id: Mapped[str] = mapped_column(ForeignKey("financial_events.id"), primary_key=True) tag_id: Mapped[str] = mapped_column(ForeignKey("tags.id"), primary_key=True) class Project(Base): __tablename__ = "projects" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) name: Mapped[str] = mapped_column(String(220), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) start_date: Mapped[date | None] = mapped_column(Date, nullable=True) end_date: Mapped[date | None] = mapped_column(Date, nullable=True) default_split_rule_id: Mapped[str | None] = mapped_column(String(36), nullable=True) status: Mapped[ProjectStatus] = mapped_column( SqlEnum(ProjectStatus), default=ProjectStatus.PLANNING, nullable=False ) currency: Mapped[str] = mapped_column(String(3), default="EUR", nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False ) class ProjectParticipant(Base): __tablename__ = "project_participants" __table_args__ = (UniqueConstraint("project_id", "user_id", name="uq_project_participant"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) project_id: Mapped[str] = mapped_column(ForeignKey("projects.id"), nullable=False, index=True) user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False, index=True) role: Mapped[str] = mapped_column(String(80), default="participant", nullable=False) default_share_percent: Mapped[Decimal | None] = mapped_column(Money, nullable=True) class ProjectBudgetLine(Base): __tablename__ = "project_budget_lines" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) project_id: Mapped[str] = mapped_column(ForeignKey("projects.id"), nullable=False, index=True) category_id: Mapped[str | None] = mapped_column(ForeignKey("categories.id"), nullable=True) name: Mapped[str] = mapped_column(String(180), nullable=False) budgeted_amount: Mapped[Decimal] = mapped_column(Money, default=Decimal("0.00"), nullable=False) committed_amount_cached: Mapped[Decimal | None] = mapped_column(Money, nullable=True) paid_amount_cached: Mapped[Decimal | None] = mapped_column(Money, nullable=True) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) notes: Mapped[str | None] = mapped_column(Text, nullable=True) class ProjectPlannedItem(Base): __tablename__ = "project_planned_items" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) project_id: Mapped[str] = mapped_column(ForeignKey("projects.id"), nullable=False, index=True) budget_line_id: Mapped[str | None] = mapped_column( ForeignKey("project_budget_lines.id"), nullable=True, index=True ) description: Mapped[str] = mapped_column(String(300), nullable=False) estimated_amount: Mapped[Decimal] = mapped_column( Money, default=Decimal("0.00"), nullable=False ) committed_amount: Mapped[Decimal | None] = mapped_column(Money, nullable=True) expected_date: Mapped[date | None] = mapped_column(Date, nullable=True) due_date: Mapped[date | None] = mapped_column(Date, nullable=True) expected_payer_account_id: Mapped[str | None] = mapped_column( ForeignKey("accounts.id"), nullable=True ) split_rule_id: Mapped[str | None] = mapped_column(String(36), nullable=True) planning_status: Mapped[PlanningStatus] = mapped_column( SqlEnum(PlanningStatus), default=PlanningStatus.IDEA, nullable=False ) payment_status: Mapped[PaymentStatus] = mapped_column( SqlEnum(PaymentStatus), default=PaymentStatus.UNPAID, nullable=False ) reconciliation_status: Mapped[ReconciliationStatus] = mapped_column( SqlEnum(ReconciliationStatus), default=ReconciliationStatus.NO_EVIDENCE, nullable=False ) settlement_status: Mapped[SettlementStatus] = mapped_column( SqlEnum(SettlementStatus), default=SettlementStatus.UNSETTLED, nullable=False ) notes: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False ) class PaymentLine(Base): __tablename__ = "payment_lines" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) planned_item_id: Mapped[str | None] = mapped_column( ForeignKey("project_planned_items.id"), nullable=True, index=True ) event_id: Mapped[str | None] = mapped_column( ForeignKey("financial_events.id"), nullable=True, index=True ) amount: Mapped[Decimal] = mapped_column(Money, nullable=False) currency: Mapped[str] = mapped_column(String(3), default="EUR", nullable=False) date: Mapped[date] = mapped_column(Date, nullable=False) paid_by_user_id: Mapped[str | None] = mapped_column( ForeignKey("users.id"), nullable=True, index=True ) payment_account_id: Mapped[str | None] = mapped_column( ForeignKey("accounts.id"), nullable=True, index=True ) source_observation_id: Mapped[str | None] = mapped_column( ForeignKey("raw_observations.id"), nullable=True ) status: Mapped[PaymentLineStatus] = mapped_column( SqlEnum(PaymentLineStatus), default=PaymentLineStatus.EXPECTED, nullable=False ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) class RecurringCommitment(Base): __tablename__ = "recurring_commitments" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) provider: Mapped[str] = mapped_column(String(200), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) amount_expected: Mapped[Decimal] = mapped_column(Money, nullable=False) amount_min: Mapped[Decimal | None] = mapped_column(Money, nullable=True) amount_max: Mapped[Decimal | None] = mapped_column(Money, nullable=True) currency: Mapped[str] = mapped_column(String(3), default="EUR", nullable=False) interval: Mapped[RecurringInterval] = mapped_column(SqlEnum(RecurringInterval), nullable=False) next_due_date: Mapped[date] = mapped_column(Date, nullable=False) payer_account_id: Mapped[str | None] = mapped_column(ForeignKey("accounts.id"), nullable=True) split_rule_id: Mapped[str | None] = mapped_column(String(36), nullable=True) category_id: Mapped[str | None] = mapped_column(ForeignKey("categories.id"), nullable=True) project_id: Mapped[str | None] = mapped_column(ForeignKey("projects.id"), nullable=True) notice_period: Mapped[str | None] = mapped_column(String(120), nullable=True) renewal_date: Mapped[date | None] = mapped_column(Date, nullable=True) status: Mapped[RecurringCommitmentStatus] = mapped_column( SqlEnum(RecurringCommitmentStatus), default=RecurringCommitmentStatus.ACTIVE, nullable=False ) matching_rule_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False ) class PromisedPayment(Base): __tablename__ = "promised_payments" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) related_event_id: Mapped[str | None] = mapped_column( ForeignKey("financial_events.id"), nullable=True, index=True ) provider: Mapped[str] = mapped_column(String(120), nullable=False) amount: Mapped[Decimal] = mapped_column(Money, nullable=False) currency: Mapped[str] = mapped_column(String(3), default="EUR", nullable=False) created_date: Mapped[date] = mapped_column(Date, nullable=False) due_date: Mapped[date] = mapped_column(Date, nullable=False, index=True) expected_funding_account_id: Mapped[str | None] = mapped_column( ForeignKey("accounts.id"), nullable=True ) responsible_user_id: Mapped[str | None] = mapped_column(ForeignKey("users.id"), nullable=True) status: Mapped[str] = mapped_column(String(40), default="expected", nullable=False) source_observation_id: Mapped[str | None] = mapped_column( ForeignKey("raw_observations.id"), nullable=True ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False ) class ImportConnection(Base): __tablename__ = "import_connections" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) owner_user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False, index=True) connection_type: Mapped[ImportConnectionType] = mapped_column( SqlEnum(ImportConnectionType), nullable=False ) display_name: Mapped[str] = mapped_column(String(200), nullable=False) status: Mapped[ImportConnectionStatus] = mapped_column( SqlEnum(ImportConnectionStatus), default=ImportConnectionStatus.NEEDS_AUTH, nullable=False ) last_sync_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) last_error: Mapped[str | None] = mapped_column(Text, nullable=True) secret_storage_ref: Mapped[str | None] = mapped_column(String(500), nullable=True) runs_locally: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) connector_config_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False ) class ImportRun(Base): __tablename__ = "import_runs" __table_args__ = ( Index("ix_import_runs_household_started", "household_id", "started_at"), Index("ix_import_runs_connection_started", "connection_id", "started_at"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) connection_id: Mapped[str | None] = mapped_column( ForeignKey("import_connections.id"), nullable=True, index=True ) actor_user_id: Mapped[str | None] = mapped_column(ForeignKey("users.id"), nullable=True) source_type: Mapped[str | None] = mapped_column(String(80), nullable=True) status: Mapped[str] = mapped_column(String(40), default="running", nullable=False) imported_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) duplicate_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) created_event_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) skipped_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) error: Mapped[str | None] = mapped_column(Text, nullable=True) summary_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) started_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False ) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class AuditLog(Base): __tablename__ = "audit_log" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) household_id: Mapped[str] = mapped_column( ForeignKey("households.id"), nullable=False, index=True ) actor_user_id: Mapped[str | None] = mapped_column(ForeignKey("users.id"), nullable=True) action: Mapped[str] = mapped_column(String(120), nullable=False) entity_type: Mapped[str] = mapped_column(String(120), nullable=False) entity_id: Mapped[str | None] = mapped_column(String(36), nullable=True) details_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, nullable=False )