# at top of persist_snapshot.py: from datetime import datetime from typing import Optional, List from sqlalchemy.orm import Mapped, mapped_column, relationship from typing import List, Optional from sqlalchemy import ( String, Text, Integer, ForeignKey, DateTime, Boolean, Numeric, UniqueConstraint, Index, func ) from db.base import Base # you already import Base in app.py class Product(Base): __tablename__ = "products" id: Mapped[int] = mapped_column(primary_key=True) slug: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False) title: Mapped[Optional[str]] = mapped_column(String(512)) image: Mapped[Optional[str]] = mapped_column(Text) description_short: Mapped[Optional[str]] = mapped_column(Text) description_html: Mapped[Optional[str]] = mapped_column(Text) suma_href: Mapped[Optional[str]] = mapped_column(Text) brand: Mapped[Optional[str]] = mapped_column(String(255)) rrp: Mapped[Optional[float]] = mapped_column(Numeric(12, 2)) rrp_currency: Mapped[Optional[str]] = mapped_column(String(16)) rrp_raw: Mapped[Optional[str]] = mapped_column(String(128)) price_per_unit: Mapped[Optional[float]] = mapped_column(Numeric(12, 4)) price_per_unit_currency: Mapped[Optional[str]] = mapped_column(String(16)) price_per_unit_raw: Mapped[Optional[str]] = mapped_column(String(128)) special_price: Mapped[Optional[float]] = mapped_column(Numeric(12, 2)) special_price_currency: Mapped[Optional[str]] = mapped_column(String(16)) special_price_raw: Mapped[Optional[str]] = mapped_column(String(128)) regular_price: Mapped[Optional[float]] = mapped_column(Numeric(12, 2)) regular_price_currency: Mapped[Optional[str]] = mapped_column(String(16)) regular_price_raw: Mapped[Optional[str]] = mapped_column(String(128)) oe_list_price: Mapped[Optional[float]] = mapped_column(Numeric(12, 2)) case_size_count: Mapped[Optional[int]] = mapped_column(Integer) case_size_item_qty: Mapped[Optional[float]] = mapped_column(Numeric(12, 3)) case_size_item_unit: Mapped[Optional[str]] = mapped_column(String(32)) case_size_raw: Mapped[Optional[str]] = mapped_column(String(128)) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) images: Mapped[List["ProductImage"]] = relationship( back_populates="product", cascade="all, delete-orphan", passive_deletes=True, ) sections: Mapped[List["ProductSection"]] = relationship( back_populates="product", cascade="all, delete-orphan", passive_deletes=True, ) labels: Mapped[List["ProductLabel"]] = relationship( cascade="all, delete-orphan", passive_deletes=True, ) stickers: Mapped[List["ProductSticker"]] = relationship( cascade="all, delete-orphan", passive_deletes=True, ) ean: Mapped[Optional[str]] = mapped_column(String(64)) sku: Mapped[Optional[str]] = mapped_column(String(128)) unit_size: Mapped[Optional[str]] = mapped_column(String(128)) pack_size: Mapped[Optional[str]] = mapped_column(String(128)) attributes = relationship( "ProductAttribute", back_populates="product", lazy="selectin", cascade="all, delete-orphan", ) nutrition = relationship( "ProductNutrition", back_populates="product", lazy="selectin", cascade="all, delete-orphan", ) allergens = relationship( "ProductAllergen", back_populates="product", lazy="selectin", cascade="all, delete-orphan", ) likes = relationship( "ProductLike", back_populates="product", cascade="all, delete-orphan", ) cart_items: Mapped[List["CartItem"]] = relationship( "CartItem", back_populates="product", cascade="all, delete-orphan", ) # NEW: all order items that reference this product order_items: Mapped[List["OrderItem"]] = relationship( "OrderItem", back_populates="product", cascade="all, delete-orphan", ) from sqlalchemy import Column class ProductLike(Base): __tablename__ = "product_likes" id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) product_slug: Mapped[str] = mapped_column(ForeignKey("products.slug", ondelete="CASCADE")) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) product: Mapped["Product"] = relationship("Product", back_populates="likes", foreign_keys=[product_slug]) user = relationship("User", back_populates="liked_products") # optional, if you want reverse access class ProductImage(Base): __tablename__ = "product_images" id: Mapped[int] = mapped_column(primary_key=True) product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False) url: Mapped[str] = mapped_column(Text, nullable=False) position: Mapped[int] = mapped_column(Integer, nullable=False, default=0) kind: Mapped[str] = mapped_column(String(16), nullable=False, default="gallery") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) product: Mapped["Product"] = relationship(back_populates="images") __table_args__ = ( UniqueConstraint("product_id", "url", "kind", name="uq_product_images_product_url_kind"), Index("ix_product_images_position", "position"), ) class ProductSection(Base): __tablename__ = "product_sections" id: Mapped[int] = mapped_column(primary_key=True) product_id: Mapped[int] = mapped_column( ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False, ) title: Mapped[str] = mapped_column(String(255), nullable=False) html: Mapped[str] = mapped_column(Text, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) # ⬇️ ADD THIS LINE: product: Mapped["Product"] = relationship(back_populates="sections") __table_args__ = ( UniqueConstraint("product_id", "title", name="uq_product_sections_product_title"), ) # --- Nav & listings --- class NavTop(Base): __tablename__ = "nav_tops" id: Mapped[int] = mapped_column(primary_key=True) label: Mapped[str] = mapped_column(String(255), nullable=False) slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) listings: Mapped[List["Listing"]] = relationship(back_populates="top", cascade="all, delete-orphan") __table_args__ = (UniqueConstraint("label", "slug", name="uq_nav_tops_label_slug"),) class NavSub(Base): __tablename__ = "nav_subs" id: Mapped[int] = mapped_column(primary_key=True) top_id: Mapped[int] = mapped_column(ForeignKey("nav_tops.id", ondelete="CASCADE"), index=True, nullable=False) label: Mapped[Optional[str]] = mapped_column(String(255)) slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True) href: Mapped[Optional[str]] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) listings: Mapped[List["Listing"]] = relationship(back_populates="sub", cascade="all, delete-orphan") __table_args__ = (UniqueConstraint("top_id", "slug", name="uq_nav_subs_top_slug"),) class Listing(Base): __tablename__ = "listings" id: Mapped[int] = mapped_column(primary_key=True) # Old slug-based fields (optional: remove) # top_slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True) # sub_slug: Mapped[Optional[str]] = mapped_column(String(255), index=True) top_id: Mapped[int] = mapped_column(ForeignKey("nav_tops.id", ondelete="CASCADE"), index=True, nullable=False) sub_id: Mapped[Optional[int]] = mapped_column(ForeignKey("nav_subs.id", ondelete="CASCADE"), index=True) total_pages: Mapped[Optional[int]] = mapped_column(Integer) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) top: Mapped["NavTop"] = relationship(back_populates="listings") sub: Mapped[Optional["NavSub"]] = relationship(back_populates="listings") __table_args__ = ( UniqueConstraint("top_id", "sub_id", name="uq_listings_top_sub"), ) class ListingItem(Base): __tablename__ = "listing_items" id: Mapped[int] = mapped_column(primary_key=True) listing_id: Mapped[int] = mapped_column(ForeignKey("listings.id", ondelete="CASCADE"), index=True, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True) __table_args__ = (UniqueConstraint("listing_id", "slug", name="uq_listing_items_listing_slug"),) # --- Reports / redirects / logs --- class LinkError(Base): __tablename__ = "link_errors" id: Mapped[int] = mapped_column(primary_key=True) product_slug: Mapped[Optional[str]] = mapped_column(String(255), index=True) href: Mapped[Optional[str]] = mapped_column(Text) text: Mapped[Optional[str]] = mapped_column(Text) top: Mapped[Optional[str]] = mapped_column(String(255)) sub: Mapped[Optional[str]] = mapped_column(String(255)) target_slug: Mapped[Optional[str]] = mapped_column(String(255)) type: Mapped[Optional[str]] = mapped_column(String(255)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) class LinkExternal(Base): __tablename__ = "link_externals" id: Mapped[int] = mapped_column(primary_key=True) product_slug: Mapped[Optional[str]] = mapped_column(String(255), index=True) href: Mapped[Optional[str]] = mapped_column(Text) text: Mapped[Optional[str]] = mapped_column(Text) host: Mapped[Optional[str]] = mapped_column(String(255)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) class SubcategoryRedirect(Base): __tablename__ = "subcategory_redirects" id: Mapped[int] = mapped_column(primary_key=True) old_path: Mapped[str] = mapped_column(String(512), nullable=False, index=True) new_path: Mapped[str] = mapped_column(String(512), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) class ProductLog(Base): __tablename__ = "product_logs" id: Mapped[int] = mapped_column(primary_key=True) slug: Mapped[Optional[str]] = mapped_column(String(255), index=True) href_tried: Mapped[Optional[str]] = mapped_column(Text) ok: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false") error_type: Mapped[Optional[str]] = mapped_column(String(255)) error_message: Mapped[Optional[str]] = mapped_column(Text) http_status: Mapped[Optional[int]] = mapped_column(Integer) final_url: Mapped[Optional[str]] = mapped_column(Text) transport_error: Mapped[Optional[bool]] = mapped_column(Boolean) title: Mapped[Optional[str]] = mapped_column(String(512)) has_description_html: Mapped[Optional[bool]] = mapped_column(Boolean) has_description_short: Mapped[Optional[bool]] = mapped_column(Boolean) sections_count: Mapped[Optional[int]] = mapped_column(Integer) images_count: Mapped[Optional[int]] = mapped_column(Integer) embedded_images_count: Mapped[Optional[int]] = mapped_column(Integer) all_images_count: Mapped[Optional[int]] = mapped_column(Integer) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) # ...existing models... class ProductLabel(Base): __tablename__ = "product_labels" id: Mapped[int] = mapped_column(primary_key=True) product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) product: Mapped["Product"] = relationship(back_populates="labels") __table_args__ = (UniqueConstraint("product_id", "name", name="uq_product_labels_product_name"),) class ProductSticker(Base): __tablename__ = "product_stickers" id: Mapped[int] = mapped_column(primary_key=True) product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) product: Mapped["Product"] = relationship(back_populates="stickers") __table_args__ = (UniqueConstraint("product_id", "name", name="uq_product_stickers_product_name"),) class ProductAttribute(Base): __tablename__ = "product_attributes" id: Mapped[int] = mapped_column(primary_key=True) product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False) key: Mapped[str] = mapped_column(String(255), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) value: Mapped[Optional[str]] = mapped_column(Text) product = relationship("Product", back_populates="attributes") __table_args__ = (UniqueConstraint("product_id", "key", name="uq_product_attributes_product_key"),) class ProductNutrition(Base): __tablename__ = "product_nutrition" id: Mapped[int] = mapped_column(primary_key=True) product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False) key: Mapped[str] = mapped_column(String(255), nullable=False) value: Mapped[Optional[str]] = mapped_column(String(255)) unit: Mapped[Optional[str]] = mapped_column(String(64)) product = relationship("Product", back_populates="nutrition") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) __table_args__ = (UniqueConstraint("product_id", "key", name="uq_product_nutrition_product_key"),) class ProductAllergen(Base): __tablename__ = "product_allergens" id: Mapped[int] = mapped_column(primary_key=True) product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) contains: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) product: Mapped["Product"] = relationship(back_populates="allergens") __table_args__ = (UniqueConstraint("product_id", "name", name="uq_product_allergens_product_name"),) class CartItem(Base): __tablename__ = "cart_items" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) # Either a logged-in user OR an anonymous session user_id: Mapped[int | None] = mapped_column( ForeignKey("users.id", ondelete="CASCADE"), nullable=True, ) session_id: Mapped[str | None] = mapped_column( String(128), nullable=True, ) # IMPORTANT: link to product *id*, not slug product_id: Mapped[int] = mapped_column( ForeignKey("products.id", ondelete="CASCADE"), nullable=False, ) quantity: Mapped[int] = mapped_column( Integer, nullable=False, default=1, server_default="1", ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) deleted_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, ) # Relationships product: Mapped["Product"] = relationship( "Product", back_populates="cart_items", ) user: Mapped["User | None"] = relationship("User", back_populates="cart_items") __table_args__ = ( Index("ix_cart_items_user_product", "user_id", "product_id"), Index("ix_cart_items_session_product", "session_id", "product_id"), )