311 lines
12 KiB
Python
311 lines
12 KiB
Python
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]) |