from datetime import UTC, datetime from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Text from sqlalchemy.orm import relationship from .database import Base class User(Base): """User model for authentication""" __tablename__ = "users" id: int = Column(Integer, primary_key=True, index=True) # type: ignore username: str = Column(String, unique=True, nullable=False, index=True) # type: ignore password_hash: str = Column(String, nullable=False) # type: ignore is_admin: bool = Column(Boolean, default=False) # type: ignore is_active: bool = Column(Boolean, default=True) # type: ignore created_at: datetime = Column(DateTime, default=lambda: datetime.now(UTC)) # type: ignore last_login: datetime | None = Column(DateTime, nullable=True) # type: ignore class Item(Base): __tablename__ = "items" id: int = Column(Integer, primary_key=True, index=True) # type: ignore url: str = Column(String, index=True) # type: ignore name: str = Column(String) # type: ignore selector: str | None = Column(String, nullable=True) # type: ignore target_price: float | None = Column(Float, nullable=True) # type: ignore check_interval_minutes: int = Column(Integer, default=60) # type: ignore # New fields current_price: float | None = Column(Float, nullable=True) # type: ignore in_stock: bool | None = Column(Boolean, nullable=True) # type: ignore is_available: bool | None = Column(Boolean, nullable=True, default=True) # type: ignore # Product still exists category: str | None = Column(String, nullable=True) # type: ignore # User-defined category tags: str | None = Column(String, nullable=True) # type: ignore description: str | None = Column(String, nullable=True) # type: ignore # Confidence scores for latest extraction current_price_confidence: float | None = Column(Float, nullable=True) # type: ignore in_stock_confidence: float | None = Column(Float, nullable=True) # type: ignore is_active: bool = Column(Boolean, default=True) # type: ignore last_checked: datetime | None = Column(DateTime, nullable=True) # type: ignore is_refreshing: bool = Column(Boolean, default=False) # type: ignore last_error: str | None = Column(String, nullable=True) # type: ignore price_history = relationship("PriceHistory", back_populates="item", cascade="all, delete-orphan") notification_channel_id: int | None = Column(Integer, ForeignKey("notification_channels.id"), nullable=True) # type: ignore notification_channel = relationship("NotificationChannel", back_populates="items") class PriceHistory(Base): __tablename__ = "price_history" id: int = Column(Integer, primary_key=True, index=True) # type: ignore item_id: int = Column(Integer, ForeignKey("items.id")) # type: ignore price: float = Column(Float) # type: ignore timestamp: datetime = Column(DateTime, default=lambda: datetime.now(UTC)) # type: ignore screenshot_path: str | None = Column(String, nullable=True) # type: ignore # Confidence scores and AI metadata price_confidence: float | None = Column(Float, nullable=True) # type: ignore in_stock_confidence: float | None = Column(Float, nullable=True) # type: ignore ai_model: str | None = Column(String, nullable=True) # type: ignore ai_provider: str | None = Column(String, nullable=True) # type: ignore prompt_version: str | None = Column(String, nullable=True) # type: ignore repair_used: bool | None = Column(Boolean, nullable=True, default=False) # type: ignore item = relationship("Item", back_populates="price_history") class Settings(Base): __tablename__ = "settings" key = Column(String, primary_key=True, index=True) value = Column(Text) class SearchSite(Base): """Sites configurés pour la recherche de produits""" __tablename__ = "search_sites" id: int = Column(Integer, primary_key=True, index=True) # type: ignore name: str = Column(String, nullable=False) # type: ignore # "Amazon France" domain: str = Column(String, unique=True, nullable=False, index=True) # type: ignore # "amazon.fr" logo_url: str | None = Column(String, nullable=True) # type: ignore category: str | None = Column(String, nullable=True) # type: ignore # "Tech", "Déco", "Généraliste" is_active: bool = Column(Boolean, default=True) # type: ignore priority: int = Column(Integer, default=0) # type: ignore # Ordre d'affichage requires_js: bool = Column(Boolean, default=False) # type: ignore # Force Browserless si True price_selector: str | None = Column(String, nullable=True) # type: ignore # Sélecteur CSS pour le prix search_url: str | None = Column(String, nullable=True) # type: ignore # URL de recherche avec {query} placeholder product_link_selector: str | None = Column(String, nullable=True) # type: ignore # Sélecteur CSS pour les liens produits created_at: datetime = Column(DateTime, default=lambda: datetime.now(UTC)) # type: ignore updated_at: datetime = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)) # type: ignore class NotificationChannel(Base): __tablename__ = "notification_channels" id: int = Column(Integer, primary_key=True, index=True) # type: ignore name: str = Column(String, nullable=False) # type: ignore type: str = Column(String, nullable=False) # type: ignore # "email", "discord", "mattermost" configuration: str = Column(Text, nullable=False) # type: ignore # JSON or encrypted string is_active: bool = Column(Boolean, default=True) # type: ignore created_at: datetime = Column(DateTime, default=lambda: datetime.now(UTC)) # type: ignore items = relationship("Item", back_populates="notification_channel")