This commit is contained in:
2025-04-29 00:00:47 -04:00
parent 56ba750aad
commit c9bba8a26e
25 changed files with 1266 additions and 152052 deletions

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, CheckConstraint
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
@@ -7,6 +7,17 @@ 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"
@@ -38,21 +49,32 @@ class PhysicalItem(Base):
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)")
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")
#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):
"""
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 []
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"
@@ -69,7 +91,8 @@ class InventoryItem(Base):
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")
marketplace_listing = relationship("MarketplaceListing", back_populates="inventory_item")
transaction_items = relationship("TransactionItem", back_populates="inventory_item")
@property
def products(self):
@@ -78,7 +101,7 @@ class InventoryItem(Base):
Returns:
list[TCGPlayerProduct] or [] if no physical item or no linked products.
"""
return self.physical_item.product if self.physical_item else []
return self.physical_item.products if self.physical_item else None
def soft_delete(self, timestamp=None):
if not timestamp:
@@ -87,92 +110,68 @@ class InventoryItem(Base):
for child in self.children:
child.soft_delete(timestamp)
class SealedBox(PhysicalItem):
__tablename__ = "sealed_boxes"
class Box(PhysicalItem):
__tablename__ = "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'
'polymorphic_identity': '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"
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': 'sealed_case'
'polymorphic_identity': 'case'
}
# Relationships
boxes = relationship("SealedBox", back_populates="case", foreign_keys=[SealedBox.case_id])
open_event = relationship("OpenEvent", uselist=False, back_populates="sealed_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)
sealed_case_id = Column(Integer, ForeignKey("sealed_cases.id"), nullable=True)
sealed_box_id = Column(Integer, ForeignKey("sealed_boxes.id"), nullable=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)
# 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'
}
__table_args__ = (
UniqueConstraint("source_item_id", name="uq_openevent_one_per_source"),
)
# 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])
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)
@@ -185,64 +184,56 @@ class SealedExpectedValue(Base):
product = relationship(
"TCGPlayerProduct",
primaryjoin="SealedExpectedValue.tcgplayer_product_id == foreign(TCGPlayerProduct.tcgplayer_product_id)",
viewonly=True,
backref="sealed_expected_values"
)
viewonly=True)
# 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}")
#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}")
# 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
# 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()
# 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}")
# 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)
#@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"))
physical_item_id = Column(Integer, ForeignKey("physical_items.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())
@@ -250,7 +241,7 @@ class TransactionItem(Base):
# Relationships
transaction = relationship("Transaction", back_populates="transaction_items")
physical_item = relationship("PhysicalItem", back_populates="transaction_items")
inventory_item = relationship("InventoryItem", back_populates="transaction_items")
class Vendor(Base):
__tablename__ = "vendors"
@@ -305,13 +296,16 @@ class MarketplaceListing(Base):
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))
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)
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")
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])