from datetime import UTC, datetime from enum import StrEnum from uuid import uuid4 from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.session import Base def now_utc() -> datetime: return datetime.now(UTC) class InstanceRole(StrEnum): ADMIN = "instance_admin" USER = "user" class HomeRole(StrEnum): OWNER = "home_owner" MEMBER = "home_member" READ_ONLY = "read_only" class Theme(StrEnum): LIGHT = "light" DARK = "dark" SYSTEM = "system" class Language(StrEnum): DE = "de" EN = "en" class User(Base): __tablename__ = "users" id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) email: Mapped[str] = mapped_column(String(320), unique=True, index=True) name: Mapped[str] = mapped_column(String(160)) password_hash: Mapped[str | None] = mapped_column(String(255), nullable=True) instance_role: Mapped[str] = mapped_column(String(40), default=InstanceRole.USER) language: Mapped[str] = mapped_column(String(8), default=Language.DE) theme: Mapped[str] = mapped_column(String(16), default=Theme.SYSTEM) timezone: Mapped[str] = mapped_column(String(80), default="Europe/Vienna") is_active: Mapped[bool] = mapped_column(Boolean, default=True) onboarding_completed: Mapped[bool] = mapped_column(Boolean, default=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc) memberships: Mapped[list["HomeMembership"]] = relationship(back_populates="user", cascade="all, delete") class Home(Base): __tablename__ = "homes" id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) name: Mapped[str] = mapped_column(String(160)) join_code_hash: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True) join_code_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) expiry_warning_days: Mapped[int] = mapped_column(Integer, default=5) daily_summary_enabled: Mapped[bool] = mapped_column(Boolean, default=True) daily_summary_time: Mapped[str] = mapped_column(String(5), default="08:00") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc) memberships: Mapped[list["HomeMembership"]] = relationship(back_populates="home", cascade="all, delete") products: Mapped[list["Product"]] = relationship(back_populates="home", cascade="all, delete") class HomeMembership(Base): __tablename__ = "home_memberships" __table_args__ = (UniqueConstraint("home_id", "user_id", name="uq_home_user"),) id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) home_id: Mapped[str] = mapped_column(ForeignKey("homes.id", ondelete="CASCADE")) user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE")) role: Mapped[str] = mapped_column(String(40), default=HomeRole.MEMBER) notification_preferences: Mapped[dict] = mapped_column(JSONB, default=dict) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc) home: Mapped[Home] = relationship(back_populates="memberships") user: Mapped[User] = relationship(back_populates="memberships") class Product(Base): __tablename__ = "products" id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) home_id: Mapped[str] = mapped_column(ForeignKey("homes.id", ondelete="CASCADE"), index=True) name: Mapped[str] = mapped_column(String(220)) barcode: Mapped[str | None] = mapped_column(String(80), nullable=True, index=True) brand: Mapped[str | None] = mapped_column(String(160), nullable=True) category: Mapped[str] = mapped_column(String(120), default="Other") location: Mapped[str] = mapped_column(String(120), default="Pantry") quantity: Mapped[float] = mapped_column(default=1) unit: Mapped[str] = mapped_column(String(32), default="pcs") expires_at: Mapped[datetime | None] = mapped_column(Date, nullable=True) min_quantity: Mapped[float] = mapped_column(default=0) notes: Mapped[str | None] = mapped_column(Text, nullable=True) image_url: Mapped[str | None] = mapped_column(Text, nullable=True) created_by_id: Mapped[str | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL")) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc) home: Mapped[Home] = relationship(back_populates="products") class ShoppingItem(Base): __tablename__ = "shopping_items" id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) home_id: Mapped[str] = mapped_column(ForeignKey("homes.id", ondelete="CASCADE"), index=True) name: Mapped[str] = mapped_column(String(220)) category: Mapped[str] = mapped_column(String(120), default="Other") quantity: Mapped[float] = mapped_column(default=1) unit: Mapped[str] = mapped_column(String(32), default="pcs") checked: Mapped[bool] = mapped_column(Boolean, default=False) product_id: Mapped[str | None] = mapped_column(ForeignKey("products.id", ondelete="SET NULL"), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc) class AppSetting(Base): __tablename__ = "app_settings" key: Mapped[str] = mapped_column(String(120), primary_key=True) value: Mapped[dict] = mapped_column(JSONB, default=dict) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc) class MailSetting(Base): __tablename__ = "mail_settings" id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1) smtp_host: Mapped[str | None] = mapped_column(String(220), nullable=True) smtp_port: Mapped[int] = mapped_column(Integer, default=587) smtp_user: Mapped[str | None] = mapped_column(String(220), nullable=True) smtp_password_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True) use_tls: Mapped[bool] = mapped_column(Boolean, default=False) use_starttls: Mapped[bool] = mapped_column(Boolean, default=True) sender_address: Mapped[str | None] = mapped_column(String(320), nullable=True) sender_name: Mapped[str] = mapped_column(String(160), default="NexaPantry") updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc) class InvitationToken(Base): __tablename__ = "invitation_tokens" id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE")) token_hash: Mapped[str] = mapped_column(String(128), unique=True, index=True) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) consumed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc) class PasswordResetToken(Base): __tablename__ = "password_reset_tokens" id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE")) token_hash: Mapped[str] = mapped_column(String(128), unique=True, index=True) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) consumed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc) class Notification(Base): __tablename__ = "notifications" id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) home_id: Mapped[str | None] = mapped_column(ForeignKey("homes.id", ondelete="CASCADE"), nullable=True) title: Mapped[str] = mapped_column(String(220)) body: Mapped[str] = mapped_column(Text) kind: Mapped[str] = mapped_column(String(80), default="info") read_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc) class AuditLog(Base): __tablename__ = "audit_logs" id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) actor_user_id: Mapped[str | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL")) action: Mapped[str] = mapped_column(String(160), index=True) target_type: Mapped[str | None] = mapped_column(String(80), nullable=True) target_id: Mapped[str | None] = mapped_column(String(80), nullable=True) metadata_json: Mapped[dict] = mapped_column(JSONB, default=dict) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)