import enum from datetime import date, datetime from typing import Optional from sqlalchemy import JSON, Column, UniqueConstraint from sqlmodel import Field, Relationship, SQLModel class RecurringItemType(str, enum.Enum): INCOME = "INCOME" EXPENSE = "EXPENSE" SAVINGS = "SAVINGS" class RecurringFrequency(str, enum.Enum): WEEKLY = "WEEKLY" MONTHLY = "MONTHLY" QUARTERLY = "QUARTERLY" BIANNUAL = "BIANNUAL" YEARLY = "YEARLY" class TransactionType(str, enum.Enum): COMPRA = "COMPRA" DEVOLUCION = "DEVOLUCION" DEPOSITO = "DEPOSITO" class TransactionSource(str, enum.Enum): CREDIT_CARD = "CREDIT_CARD" CASH = "CASH" TRANSFER = "TRANSFER" class Currency(str, enum.Enum): CRC = "CRC" USD = "USD" BTC = "BTC" XMR = "XMR" class Bank(str, enum.Enum): BAC = "BAC" BCR = "BCR" DAVIVIENDA = "DAVIVIENDA" FCL = "FCL" ROP = "ROP" VOL = "VOL" MEMP = "MEMP" MPAT = "MPAT" MORTGAGE = "MORTGAGE" class AccountType(str, enum.Enum): BANK = "BANK" PENSION = "PENSION" CRYPTO = "CRYPTO" SAVINGS = "SAVINGS" LIABILITY = "LIABILITY" # --- Category --- class CategoryBase(SQLModel): name: str = Field(index=True, unique=True) icon: str = "tag" auto_match_patterns: Optional[str] = Field( default=None, description="Comma-separated merchant substrings for auto-matching", ) class Category(CategoryBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) transactions: list["Transaction"] = Relationship(back_populates="category") class CategoryCreate(CategoryBase): pass class CategoryRead(CategoryBase): id: int class CategoryUpdate(SQLModel): name: Optional[str] = None icon: Optional[str] = None auto_match_patterns: Optional[str] = None # --- Account --- class AccountBase(SQLModel): bank: Bank currency: Currency label: str balance: float = 0.0 account_type: AccountType = AccountType.BANK next_payment: Optional[float] = None class Account(AccountBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) updated_at: datetime = Field(default_factory=datetime.utcnow) class AccountCreate(AccountBase): pass class AccountRead(AccountBase): id: int updated_at: datetime class AccountUpdate(SQLModel): balance: Optional[float] = None label: Optional[str] = None next_payment: Optional[float] = None # --- Transaction --- class TransactionBase(SQLModel): amount: float currency: Currency = Currency.CRC merchant: str city: Optional[str] = None date: datetime card_type: Optional[str] = None card_last4: Optional[str] = None authorization_code: Optional[str] = None reference: Optional[str] = Field(default=None, index=True) transaction_type: TransactionType = TransactionType.COMPRA source: TransactionSource = TransactionSource.CREDIT_CARD bank: Bank = Bank.BAC notes: Optional[str] = None category_id: Optional[int] = Field(default=None, foreign_key="category.id") deferred_to_next_cycle: bool = Field(default=False) class Transaction(TransactionBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) created_at: datetime = Field(default_factory=datetime.utcnow) category: Optional[Category] = Relationship(back_populates="transactions") class TransactionCreate(TransactionBase): pass class TransactionRead(TransactionBase): id: int created_at: datetime category: Optional[CategoryRead] = None class TransactionUpdate(SQLModel): amount: Optional[float] = None currency: Optional[Currency] = None merchant: Optional[str] = None city: Optional[str] = None date: Optional[datetime] = None transaction_type: Optional[TransactionType] = None source: Optional[TransactionSource] = None notes: Optional[str] = None category_id: Optional[int] = None deferred_to_next_cycle: Optional[bool] = None # --- Exchange Rate --- class ExchangeRate(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) date: datetime buy_rate: float sell_rate: float fetched_at: datetime = Field(default_factory=datetime.utcnow) class ExchangeRateRead(SQLModel): buy_rate: float sell_rate: float date: datetime fetched_at: datetime # --- API Token --- class APIToken(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str token_hash: str = Field(index=True) created_at: datetime = Field(default_factory=datetime.utcnow) expires_at: Optional[datetime] = None is_active: bool = True class APITokenCreate(SQLModel): name: str expires_days: Optional[int] = None class APITokenRead(SQLModel): id: int name: str created_at: datetime expires_at: Optional[datetime] is_active: bool # --- User Settings --- class UserSettings(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) key: str = Field(index=True, unique=True, default="default") data: dict = Field( default_factory=dict, sa_column=Column(JSON, nullable=False, server_default="{}"), ) updated_at: datetime = Field(default_factory=datetime.utcnow) class UserSettingsRead(SQLModel): key: str data: dict updated_at: datetime class UserSettingsUpdate(SQLModel): data: dict # --- Recurring Item --- class RecurringItemBase(SQLModel): name: str amount: float currency: Currency = Currency.CRC item_type: RecurringItemType frequency: RecurringFrequency = RecurringFrequency.MONTHLY day_of_month: Optional[int] = None month_of_year: Optional[int] = None override_amounts: Optional[dict] = Field( default=None, sa_column=Column(JSON, nullable=True), ) category_id: Optional[int] = Field(default=None, foreign_key="category.id") is_active: bool = True notes: Optional[str] = None class RecurringItem(RecurringItemBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) created_at: datetime = Field(default_factory=datetime.utcnow) category: Optional[Category] = Relationship() class RecurringItemCreate(RecurringItemBase): pass class RecurringItemRead(RecurringItemBase): id: int created_at: datetime category: Optional[CategoryRead] = None class RecurringItemUpdate(SQLModel): name: Optional[str] = None amount: Optional[float] = None currency: Optional[Currency] = None item_type: Optional[RecurringItemType] = None frequency: Optional[RecurringFrequency] = None day_of_month: Optional[int] = None month_of_year: Optional[int] = None override_amounts: Optional[dict] = None category_id: Optional[int] = None is_active: Optional[bool] = None notes: Optional[str] = None # --- Push Subscription --- class PushSubscription(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) endpoint: str = Field(unique=True) p256dh: str auth: str created_at: datetime = Field(default_factory=datetime.utcnow) class PushSubscriptionCreate(SQLModel): endpoint: str keys: dict # {"p256dh": "...", "auth": "..."} # --- Pension Snapshot --- class PensionSnapshotBase(SQLModel): fund: Bank contract_number: str period_start: date period_end: date saldo_anterior: float aportes: float rendimientos: float retiros: float traslados: float comision: float correccion: float bonificacion: float saldo_final: float source_filename: str class PensionSnapshot(PensionSnapshotBase, table=True): __table_args__ = ( UniqueConstraint("fund", "period_start", "period_end"), ) id: Optional[int] = Field(default=None, primary_key=True) created_at: datetime = Field(default_factory=datetime.utcnow) class PensionSnapshotRead(PensionSnapshotBase): id: int created_at: datetime # --- Balance Override --- class BalanceOverride(SQLModel, table=True): __table_args__ = (UniqueConstraint("year", "month"),) id: Optional[int] = Field(default=None, primary_key=True) year: int month: int override_balance: float created_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=datetime.utcnow) class BalanceOverrideCreate(SQLModel): override_balance: float class BalanceOverrideRead(SQLModel): id: int year: int month: int override_balance: float updated_at: datetime # --- Municipal Receipt --- class MunicipalReceiptBase(SQLModel): receipt_date: date due_date: date period: str # "YYYY-MM" account: str finca: str holder_name: str holder_cedula: str holder_address: str subtotal: float interests: float iva: float total: float raw_charges: list[dict] = Field( default_factory=list, sa_column=Column(JSON, nullable=False, server_default="[]"), ) source_filename: str class MunicipalReceipt(MunicipalReceiptBase, table=True): __table_args__ = (UniqueConstraint("account", "period"),) id: Optional[int] = Field(default=None, primary_key=True) created_at: datetime = Field(default_factory=datetime.utcnow) water_readings: list["WaterMeterReading"] = Relationship( back_populates="receipt", ) class MunicipalReceiptCreate(MunicipalReceiptBase): pass class MunicipalReceiptRead(MunicipalReceiptBase): id: int created_at: datetime # --- Water Meter Reading --- class WaterMeterReadingBase(SQLModel): meter_id: str period: str # "YYYY-MM" reading_previous: float = 0 reading_current: float = 0 consumption_m3: float agua_potable: float = 0 serv_ambientales: float = 0 alcant_sanitario: float = 0 iva: float = 0 is_historical: bool = False receipt_id: Optional[int] = Field(default=None, foreign_key="municipalreceipt.id") class WaterMeterReading(WaterMeterReadingBase, table=True): __table_args__ = (UniqueConstraint("meter_id", "period", "is_historical"),) id: Optional[int] = Field(default=None, primary_key=True) created_at: datetime = Field(default_factory=datetime.utcnow) receipt: Optional[MunicipalReceipt] = Relationship( back_populates="water_readings", ) class WaterMeterReadingRead(WaterMeterReadingBase): id: int created_at: datetime