Files
WealthySmart/backend/app/models/models.py
Carlos Escalante efe6d88286
All checks were successful
Deploy to VPS / deploy (push) Successful in 50s
Add EUR currency support for international transactions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 20:29:37 -06:00

436 lines
10 KiB
Python

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"
EUR = "EUR"
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