from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, CheckConstraint, Index, Boolean, Table, UniqueConstraint from sqlalchemy.orm import relationship from app.db.database import Base from sqlalchemy import event from sqlalchemy.orm import Session from sqlalchemy import func from sqlalchemy.ext.hybrid import hybrid_property from datetime import datetime from app.models.critical_error_log import CriticalErrorLog import logging logger = logging.getLogger(__name__) open_event_resulting_items = Table( "open_event_resulting_items", Base.metadata, Column("event_id", Integer, ForeignKey("open_events.id"), primary_key=True), Column("item_id", Integer, ForeignKey("physical_items.id"), primary_key=True) ) class PhysicalItem(Base): __tablename__ = "physical_items" id = Column(Integer, primary_key=True) item_type = Column(String) # at least one of these must be set to pass the constraint tcgplayer_product_id = Column(Integer, nullable=True) tcgplayer_sku_id = Column(Integer, ForeignKey("mtgjson_skus.tcgplayer_sku_id"), nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) deleted_at = Column(DateTime(timezone=True), nullable=True) __table_args__ = ( CheckConstraint( "(tcgplayer_sku_id IS NOT NULL OR tcgplayer_product_id IS NOT NULL)", name="ck_physical_items_sku_or_product_not_null" ), ) __mapper_args__ = { 'polymorphic_on': item_type, 'polymorphic_identity': 'physical_item' } # Relationships sku = relationship("MTGJSONSKU", back_populates="physical_items", primaryjoin="PhysicalItem.tcgplayer_sku_id == MTGJSONSKU.tcgplayer_sku_id") product_direct = relationship("TCGPlayerProduct", back_populates="physical_items_direct", primaryjoin="PhysicalItem.tcgplayer_product_id == foreign(TCGPlayerProduct.tcgplayer_product_id)", uselist=False) inventory_item = relationship("InventoryItem", uselist=False, back_populates="physical_item") #transaction_items = relationship("TransactionItem", back_populates="physical_item") source_open_events = relationship( "OpenEvent", back_populates="source_item", foreign_keys="[OpenEvent.source_item_id]" ) resulting_open_events = relationship( "OpenEvent", secondary=open_event_resulting_items, back_populates="resulting_items" ) @hybrid_property def is_sealed(self): return not self.source_open_events @hybrid_property def products(self): if self.sku and self.sku.product: return self.sku.product elif self.product_direct: return self.product_direct else: return None class InventoryItem(Base): __tablename__ = "inventory_items" id = Column(Integer, primary_key=True, index=True) physical_item_id = Column(Integer, ForeignKey("physical_items.id"), unique=True) cost_basis = Column(Float) parent_id = Column(Integer, ForeignKey("inventory_items.id"), nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) deleted_at = Column(DateTime(timezone=True), nullable=True) # Relationships physical_item = relationship("PhysicalItem", back_populates="inventory_item") parent = relationship("InventoryItem", remote_side=[id], back_populates="children") children = relationship("InventoryItem", back_populates="parent", overlaps="parent") marketplace_listing = relationship("MarketplaceListing", back_populates="inventory_item") transaction_items = relationship("TransactionItem", back_populates="inventory_item") @property def products(self): """ Proxy access to the associated TCGPlayerProduct(s) via the linked PhysicalItem. Returns: list[TCGPlayerProduct] or [] if no physical item or no linked products. """ return self.physical_item.products if self.physical_item else None def soft_delete(self, timestamp=None): if not timestamp: timestamp = datetime.now() self.deleted_at = timestamp for child in self.children: child.soft_delete(timestamp) class Box(PhysicalItem): __tablename__ = "boxes" id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True) expected_value = Column(Float) __mapper_args__ = { 'polymorphic_identity': 'box' } class Case(PhysicalItem): __tablename__ = "cases" id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True) expected_value = Column(Float) num_boxes = Column(Integer) __mapper_args__ = { 'polymorphic_identity': 'case' } class Card(PhysicalItem): __tablename__ = "cards" id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True) __mapper_args__ = { 'polymorphic_identity': 'card' } class OpenEvent(Base): __tablename__ = "open_events" id = Column(Integer, primary_key=True, index=True) source_item_id = Column(Integer, ForeignKey("physical_items.id")) open_date = Column(DateTime(timezone=True)) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) deleted_at = Column(DateTime(timezone=True), nullable=True) __table_args__ = ( UniqueConstraint("source_item_id", name="uq_openevent_one_per_source"), ) # Relationships source_item = relationship( "PhysicalItem", back_populates="source_open_events", foreign_keys=[source_item_id] ) resulting_items = relationship( "PhysicalItem", secondary=open_event_resulting_items, back_populates="resulting_open_events" ) class SealedExpectedValue(Base): __tablename__ = "sealed_expected_values" __table_args__ = ( Index('idx_sealed_expected_value_product_id_deleted_at', 'tcgplayer_product_id', 'deleted_at', unique=True), ) id = Column(Integer, primary_key=True, index=True) tcgplayer_product_id = Column(Integer, nullable=False) expected_value = Column(Float, nullable=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) deleted_at = Column(DateTime(timezone=True), nullable=True) # Relationships product = relationship( "TCGPlayerProduct", primaryjoin="SealedExpectedValue.tcgplayer_product_id == foreign(TCGPlayerProduct.tcgplayer_product_id)", viewonly=True) # helper for ev #def assign_expected_value(target, session): # products = target.products # if not products: # raise ValueError(f"No product found for item ID {target.id}") # if len(products) > 1: # product_names = [p.name for p in products] # critical_error = CriticalErrorLog( # error_type="multiple_products_found", # error_message=f"Multiple products found when assigning expected value for item ID {target.id} product names {product_names}" # ) # session.add(critical_error) # session.commit() # raise ValueError(f"Multiple products found when assigning expected value for item ID {target.id} product names {product_names}") # product_id = products[0].tcgplayer_product_id # reliable lookup key # expected_value_entry = session.query(SealedExpectedValue).filter( # SealedExpectedValue.tcgplayer_product_id == product_id, # SealedExpectedValue.deleted_at == None # ).order_by(SealedExpectedValue.created_at.desc()).first() # if expected_value_entry: # target.expected_value = expected_value_entry.expected_value # else: # critical_error = CriticalErrorLog( # error_type="no_expected_value_found", # error_message=f"No expected value found for product {products[0].name}" # ) # session.add(critical_error) # session.commit() # raise ValueError(f"No expected value found for product {products[0].name}") # event listeners #@event.listens_for(InventoryItem, 'before_insert') #def ev_before_insert(mapper, connection, target): # session = Session.object_session(target) # if session: # assign_expected_value(target, session) class TransactionItem(Base): __tablename__ = "transaction_items" id = Column(Integer, primary_key=True, index=True) transaction_id = Column(Integer, ForeignKey("transactions.id")) inventory_item_id = Column(Integer, ForeignKey("inventory_items.id")) unit_price = Column(Float, nullable=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) deleted_at = Column(DateTime(timezone=True), nullable=True) # Relationships transaction = relationship("Transaction", back_populates="transaction_items") inventory_item = relationship("InventoryItem", back_populates="transaction_items") class Vendor(Base): __tablename__ = "vendors" id = Column(Integer, primary_key=True, index=True) name = Column(String, index=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) deleted_at = Column(DateTime(timezone=True), nullable=True) class Customer(Base): __tablename__ = "customers" id = Column(Integer, primary_key=True, index=True) name = Column(String, index=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) deleted_at = Column(DateTime(timezone=True), nullable=True) class Transaction(Base): __tablename__ = "transactions" id = Column(Integer, primary_key=True, index=True) vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=True) customer_id = Column(Integer, ForeignKey("customers.id"), nullable=True) marketplace_id = Column(Integer, ForeignKey("marketplaces.id"), nullable=True) transaction_type = Column(String) # 'purchase' or 'sale' transaction_date = Column(DateTime(timezone=True)) transaction_total_amount = Column(Float) transaction_notes = Column(String) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) deleted_at = Column(DateTime(timezone=True), nullable=True) # Relationships transaction_items = relationship("TransactionItem", back_populates="transaction") class Marketplace(Base): __tablename__ = "marketplaces" id = Column(Integer, primary_key=True, index=True) name = Column(String, index=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) deleted_at = Column(DateTime(timezone=True), nullable=True) # Relationships listings = relationship("MarketplaceListing", back_populates="marketplace") class MarketplaceListing(Base): __tablename__ = "marketplace_listings" id = Column(Integer, primary_key=True, index=True) inventory_item_id = Column(Integer, ForeignKey("inventory_items.id"), nullable=False) marketplace_id = Column(Integer, ForeignKey("marketplaces.id"), nullable=False) recommended_price_id = Column(Integer, ForeignKey("pricing_events.id"), nullable=True) listed_price_id = Column(Integer, ForeignKey("pricing_events.id"), nullable=True) listing_date = Column(DateTime(timezone=True), nullable=True) delisting_date = Column(DateTime(timezone=True), nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) deleted_at = Column(DateTime(timezone=True), nullable=True) # Relationships inventory_item = relationship("InventoryItem", back_populates="marketplace_listing") marketplace = relationship("Marketplace", back_populates="listings") recommended_price = relationship("PricingEvent", foreign_keys=[recommended_price_id]) listed_price = relationship("PricingEvent", foreign_keys=[listed_price_id])