from datetime import datetime import sqlalchemy as sa from sqlalchemy.orm import Mapped, mapped_column, relationship from .database import Base class Store(Base): __tablename__ = "stores" id: Mapped[int] = mapped_column(sa.Integer, primary_key=True) name: Mapped[str] = mapped_column(sa.String(100), nullable=False) slug: Mapped[str] = mapped_column(sa.String(50), unique=True, nullable=False) country: Mapped[str] = mapped_column(sa.String(2), default="NL") website: Mapped[str | None] = mapped_column(sa.String(255)) products: Mapped[list["Product"]] = relationship(back_populates="store") scrape_runs: Mapped[list["ScrapeRun"]] = relationship(back_populates="store") class Product(Base): __tablename__ = "products" __table_args__ = ( sa.UniqueConstraint("store_id", "external_id", name="uq_products_store_external"), ) id: Mapped[int] = mapped_column(sa.Integer, primary_key=True) store_id: Mapped[int] = mapped_column(sa.Integer, sa.ForeignKey("stores.id"), nullable=False) external_id: Mapped[str] = mapped_column(sa.String(50), nullable=False) ean: Mapped[str | None] = mapped_column(sa.String(20), index=True) name: Mapped[str] = mapped_column(sa.String(255), nullable=False) brand: Mapped[str | None] = mapped_column(sa.String(100)) category: Mapped[str | None] = mapped_column(sa.String(100)) unit_size: Mapped[str | None] = mapped_column(sa.String(50)) url: Mapped[str | None] = mapped_column(sa.String(500)) created_at: Mapped[datetime] = mapped_column(sa.DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column( sa.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow ) store: Mapped["Store"] = relationship(back_populates="products") price_snapshots: Mapped[list["PriceSnapshot"]] = relationship(back_populates="product") class ScrapeRun(Base): __tablename__ = "scrape_runs" id: Mapped[int] = mapped_column(sa.Integer, primary_key=True) store_id: Mapped[int] = mapped_column(sa.Integer, sa.ForeignKey("stores.id"), nullable=False) query: Mapped[str] = mapped_column(sa.String(255), nullable=False) started_at: Mapped[datetime] = mapped_column(sa.DateTime, default=datetime.utcnow) finished_at: Mapped[datetime | None] = mapped_column(sa.DateTime) status: Mapped[str] = mapped_column(sa.String(20), default="running") products_found: Mapped[int] = mapped_column(sa.Integer, default=0) error_message: Mapped[str | None] = mapped_column(sa.String(1000)) store: Mapped["Store"] = relationship(back_populates="scrape_runs") price_snapshots: Mapped[list["PriceSnapshot"]] = relationship(back_populates="scrape_run") class PriceSnapshot(Base): __tablename__ = "price_snapshots" id: Mapped[int] = mapped_column(sa.Integer, primary_key=True) product_id: Mapped[int] = mapped_column( sa.Integer, sa.ForeignKey("products.id"), nullable=False ) scrape_run_id: Mapped[int] = mapped_column( sa.Integer, sa.ForeignKey("scrape_runs.id"), nullable=False ) price: Mapped[int] = mapped_column(sa.Integer, nullable=False) # euro cents unit_price: Mapped[int | None] = mapped_column(sa.Integer) # euro cents unit_description: Mapped[str | None] = mapped_column(sa.String(50)) currency: Mapped[str] = mapped_column(sa.String(3), default="EUR") discount_label: Mapped[str | None] = mapped_column(sa.String(100)) discount_description: Mapped[str | None] = mapped_column(sa.String(255)) was_price: Mapped[int | None] = mapped_column(sa.Integer) # euro cents, original price is_on_sale: Mapped[bool] = mapped_column(sa.Boolean, default=False) timestamp: Mapped[datetime] = mapped_column(sa.DateTime, default=datetime.utcnow, index=True) product: Mapped["Product"] = relationship(back_populates="price_snapshots") scrape_run: Mapped["ScrapeRun"] = relationship(back_populates="price_snapshots")