from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, CheckConstraint 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 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)") inventory_item = relationship("InventoryItem", uselist=False, back_populates="physical_item") transaction_items = relationship("TransactionItem", back_populates="physical_item") @hybrid_property def products(self): """ Dynamically resolve the associated TCGPlayerProduct(s): - If the SKU is set, return all linked products. - Else, return a list containing a single product from direct link. """ # TODO IS THIS EVEN CORRECT OH GOD if self.sku and self.sku.products: return self.sku.products # This is a list of TCGPlayerProduct return [self.product_direct] if self.product_direct else [] 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_listings = relationship("MarketplaceListing", 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.product if self.physical_item else [] 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 SealedBox(PhysicalItem): __tablename__ = "sealed_boxes" id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True) case_id = Column(Integer, ForeignKey("sealed_cases.id"), nullable=True) expected_value = Column(Float) __mapper_args__ = { 'polymorphic_identity': 'sealed_box' } # Relationships case = relationship("SealedCase", back_populates="boxes", foreign_keys=[case_id]) open_event = relationship("OpenEvent", uselist=False, back_populates="sealed_box") class SealedCase(PhysicalItem): __tablename__ = "sealed_cases" id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True) expected_value = Column(Float) num_boxes = Column(Integer) __mapper_args__ = { 'polymorphic_identity': 'sealed_case' } # Relationships boxes = relationship("SealedBox", back_populates="case", foreign_keys=[SealedBox.case_id]) open_event = relationship("OpenEvent", uselist=False, back_populates="sealed_case") class OpenEvent(Base): __tablename__ = "open_events" id = Column(Integer, primary_key=True, index=True) sealed_case_id = Column(Integer, ForeignKey("sealed_cases.id"), nullable=True) sealed_box_id = Column(Integer, ForeignKey("sealed_boxes.id"), nullable=True) 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) # Relationships sealed_case = relationship("SealedCase", back_populates="open_event") sealed_box = relationship("SealedBox", back_populates="open_event") resulting_boxes = relationship("OpenBox", back_populates="open_event") resulting_cards = relationship("OpenCard", back_populates="open_event") class OpenCard(PhysicalItem): __tablename__ = "open_cards" id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True) open_event_id = Column(Integer, ForeignKey("open_events.id")) box_id = Column(Integer, ForeignKey("open_boxes.id"), nullable=True) __mapper_args__ = { 'polymorphic_identity': 'open_card' } # Relationships open_event = relationship("OpenEvent", back_populates="resulting_cards") box = relationship("OpenBox", back_populates="cards", foreign_keys=[box_id]) class OpenBox(PhysicalItem): __tablename__ = "open_boxes" id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True) open_event_id = Column(Integer, ForeignKey("open_events.id")) sealed_box_id = Column(Integer, ForeignKey("sealed_boxes.id")) __mapper_args__ = { 'polymorphic_identity': 'open_box' } # Relationships open_event = relationship("OpenEvent", back_populates="resulting_boxes") sealed_box = relationship("SealedBox", foreign_keys=[sealed_box_id]) cards = relationship("OpenCard", back_populates="box", foreign_keys=[OpenCard.box_id]) class SealedExpectedValue(Base): __tablename__ = "sealed_expected_values" 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, backref="sealed_expected_values" ) # helper for ev def assign_expected_value(target, session): products = target.product # uses hybrid property 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(SealedBox, 'before_insert') def sealed_box_before_insert(mapper, connection, target): session = Session.object_session(target) if session: assign_expected_value(target, session) @event.listens_for(SealedCase, 'before_insert') def sealed_case_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")) physical_item_id = Column(Integer, ForeignKey("physical_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") physical_item = relationship("PhysicalItem", 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) listing_date = Column(DateTime(timezone=True)) delisting_date = Column(DateTime(timezone=True), nullable=True) listed_price = Column(Float) 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_listings") marketplace = relationship("Marketplace", back_populates="listings")