i think most of this works lole
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
from app.models.critical_error_log import CriticalErrorLog
|
||||
from app.models.file import File
|
||||
from app.models.inventory_management import (
|
||||
PhysicalItem,
|
||||
@ -6,25 +7,32 @@ from app.models.inventory_management import (
|
||||
OpenEvent,
|
||||
Vendor,
|
||||
Customer,
|
||||
Transaction
|
||||
Transaction,
|
||||
SealedExpectedValue,
|
||||
Marketplace,
|
||||
MarketplaceListing
|
||||
)
|
||||
from app.models.tcgplayer_products import (
|
||||
MTGJSONCard,
|
||||
MTGJSONSKU,
|
||||
TCGPlayerProduct,
|
||||
TCGPlayerCategory,
|
||||
TCGPlayerGroup,
|
||||
TCGPlayerPriceHistory,
|
||||
MostRecentTCGPlayerPrice
|
||||
)
|
||||
from app.models.mtgjson_card import MTGJSONCard
|
||||
from app.models.mtgjson_sku import MTGJSONSKU
|
||||
from app.models.product import Product
|
||||
from app.models.tcgplayer_category import TCGPlayerCategory
|
||||
from app.models.tcgplayer_group import TCGPlayerGroup
|
||||
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
||||
from app.models.tcgplayer_order import (
|
||||
TCGPlayerOrder,
|
||||
TCGPlayerOrderTransaction,
|
||||
TCGPlayerOrderProduct,
|
||||
TCGPlayerOrderRefund
|
||||
)
|
||||
from app.models.tcgplayer_price_history import TCGPlayerPriceHistory
|
||||
from app.models.tcgplayer_product import TCGPlayerProduct
|
||||
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
||||
from app.models.manabox_import_staging import ManaboxImportStaging
|
||||
|
||||
|
||||
# This makes all models available for Alembic to discover
|
||||
__all__ = [
|
||||
'CriticalErrorLog',
|
||||
'File',
|
||||
'PhysicalItem',
|
||||
'InventoryItem',
|
||||
@ -33,16 +41,20 @@ __all__ = [
|
||||
'Vendor',
|
||||
'Customer',
|
||||
'Transaction',
|
||||
'SealedExpectedValue',
|
||||
'Marketplace',
|
||||
'MarketplaceListing',
|
||||
'MTGJSONCard',
|
||||
'MTGJSONSKU',
|
||||
'Product',
|
||||
'TCGPlayerProduct',
|
||||
'TCGPlayerCategory',
|
||||
'TCGPlayerGroup',
|
||||
'TCGPlayerInventory',
|
||||
'ManaboxImportStaging',
|
||||
'TCGPlayerOrder',
|
||||
'TCGPlayerOrderTransaction',
|
||||
'TCGPlayerOrderProduct',
|
||||
'TCGPlayerOrderRefund',
|
||||
'TCGPlayerPriceHistory',
|
||||
'TCGPlayerProduct'
|
||||
'MostRecentTCGPlayerPrice'
|
||||
]
|
11
app/models/critical_error_log.py
Normal file
11
app/models/critical_error_log.py
Normal file
@ -0,0 +1,11 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
|
||||
class CriticalErrorLog(Base):
|
||||
__tablename__ = "critical_error_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
error_message = Column(String, 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())
|
@ -1,5 +1,6 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, JSON
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
|
||||
|
||||
@ -11,7 +12,11 @@ class File(Base):
|
||||
file_type = Column(String)
|
||||
content_type = Column(String)
|
||||
path = Column(String)
|
||||
size = Column(Integer) # File size in bytes
|
||||
size = Column(Integer)
|
||||
file_metadata = Column(JSON)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
manabox_import_staging = relationship("ManaboxImportStaging", back_populates="file")
|
@ -1,32 +1,112 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
|
||||
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)
|
||||
product_id = Column(Integer, ForeignKey("tcgplayer_products.id"), nullable=False)
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
updated_at = Column(DateTime(timezone=True))
|
||||
|
||||
# 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
|
||||
product = relationship("TCGPlayerProduct")
|
||||
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_item = relationship("TransactionItem", 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)
|
||||
@ -36,107 +116,9 @@ class SealedCase(PhysicalItem):
|
||||
}
|
||||
|
||||
# Relationships
|
||||
boxes = relationship("SealedBox", back_populates="case", foreign_keys="[SealedBox.case_id]")
|
||||
boxes = relationship("SealedBox", back_populates="case", foreign_keys=[SealedBox.case_id])
|
||||
open_event = relationship("OpenEvent", uselist=False, back_populates="sealed_case")
|
||||
|
||||
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")
|
||||
|
||||
# event listeners
|
||||
@event.listens_for(SealedCase, 'before_insert')
|
||||
def set_expected_value(mapper, connection, target):
|
||||
session = Session.object_session(target)
|
||||
if session:
|
||||
expected_value = session.query(SealedExpectedValue).filter(SealedExpectedValue.product_id == target.product_id).filter(SealedExpectedValue.deleted_at == None).order_by(SealedExpectedValue.created_at.desc()).first()
|
||||
if expected_value:
|
||||
target.expected_value = expected_value.expected_value
|
||||
else:
|
||||
raise ValueError("No expected value found for this product")
|
||||
|
||||
@event.listens_for(SealedBox, 'before_insert')
|
||||
def set_expected_value(mapper, connection, target):
|
||||
session = Session.object_session(target)
|
||||
if session:
|
||||
expected_value = session.query(SealedExpectedValue).filter(SealedExpectedValue.product_id == target.product_id).filter(SealedExpectedValue.deleted_at == None).order_by(SealedExpectedValue.created_at.desc()).first()
|
||||
if expected_value:
|
||||
target.expected_value = expected_value.expected_value
|
||||
else:
|
||||
raise ValueError("No expected value found for this product")
|
||||
|
||||
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 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 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) # Current cost basis for this item
|
||||
parent_id = Column(Integer, ForeignKey("inventory_items.id"), nullable=True) # For tracking hierarchy
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
updated_at = Column(DateTime(timezone=True))
|
||||
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")
|
||||
|
||||
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))
|
||||
updated_at = Column(DateTime(timezone=True))
|
||||
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 OpenEvent(Base):
|
||||
__tablename__ = "open_events"
|
||||
@ -145,8 +127,8 @@ class OpenEvent(Base):
|
||||
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))
|
||||
updated_at = 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
|
||||
@ -155,23 +137,138 @@ class OpenEvent(Base):
|
||||
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, unique=True, index=True)
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
updated_at = Column(DateTime(timezone=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, unique=True, index=True)
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
updated_at = Column(DateTime(timezone=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"
|
||||
|
||||
@ -183,49 +280,23 @@ class Transaction(Base):
|
||||
transaction_date = Column(DateTime(timezone=True))
|
||||
transaction_total_amount = Column(Float)
|
||||
transaction_notes = Column(String)
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
updated_at = 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
|
||||
transaction_items = relationship("TransactionItem", back_populates="transaction")
|
||||
|
||||
class SealedExpectedValue(Base):
|
||||
__tablename__ = "sealed_expected_values"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
product_id = Column(Integer, ForeignKey("tcgplayer_products.id"), nullable=True)
|
||||
expected_value = Column(Float)
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
updated_at = Column(DateTime(timezone=True))
|
||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
class MostRecentTCGPlayerPrice(Base):
|
||||
__tablename__ = "most_recent_tcgplayer_price"
|
||||
__table_args__ = {'extend_existing': True, 'autoload_with': None, 'info': {'is_view': True}}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
product_id = Column(Integer, ForeignKey("tcgplayer_products.id"))
|
||||
date = Column(DateTime)
|
||||
low_price = Column(Float)
|
||||
mid_price = Column(Float)
|
||||
high_price = Column(Float)
|
||||
market_price = Column(Float)
|
||||
direct_low_price = Column(Float)
|
||||
sub_type_name = Column(String)
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
updated_at = Column(DateTime(timezone=True))
|
||||
|
||||
# Relationships
|
||||
product = relationship("TCGPlayerProduct", back_populates="most_recent_tcgplayer_price")
|
||||
|
||||
class Marketplace(Base):
|
||||
__tablename__ = "marketplaces"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, unique=True, index=True)
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
updated_at = Column(DateTime(timezone=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):
|
||||
@ -234,15 +305,13 @@ 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)
|
||||
product_id = Column(Integer, ForeignKey("tcgplayer_products.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))
|
||||
updated_at = 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
|
||||
inventory_item = relationship("InventoryItem", back_populates="marketplace_listings")
|
||||
marketplace = relationship("Marketplace", back_populates="listings")
|
||||
product = relationship("TCGPlayerProduct", back_populates="marketplace_listings")
|
||||
marketplace = relationship("Marketplace", back_populates="listings")
|
18
app/models/manabox_import_staging.py
Normal file
18
app/models/manabox_import_staging.py
Normal file
@ -0,0 +1,18 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
|
||||
class ManaboxImportStaging(Base):
|
||||
__tablename__ = "manabox_import_staging"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
file_id = Column(Integer, ForeignKey("files.id"))
|
||||
tcgplayer_sku_id = Column(Integer)
|
||||
quantity = Column(Integer)
|
||||
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
|
||||
file = relationship("File", back_populates="manabox_import_staging")
|
@ -1,43 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
|
||||
class MTGJSONCard(Base):
|
||||
__tablename__ = "mtgjson_cards"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
card_id = Column(String, unique=True, index=True)
|
||||
name = Column(String)
|
||||
set_code = Column(String)
|
||||
uuid = Column(String)
|
||||
|
||||
# Identifiers
|
||||
abu_id = Column(String, nullable=True)
|
||||
card_kingdom_etched_id = Column(String, nullable=True)
|
||||
card_kingdom_foil_id = Column(String, nullable=True)
|
||||
card_kingdom_id = Column(String, nullable=True)
|
||||
cardsphere_id = Column(String, nullable=True)
|
||||
cardsphere_foil_id = Column(String, nullable=True)
|
||||
cardtrader_id = Column(String, nullable=True)
|
||||
csi_id = Column(String, nullable=True)
|
||||
mcm_id = Column(String, nullable=True)
|
||||
mcm_meta_id = Column(String, nullable=True)
|
||||
miniaturemarket_id = Column(String, nullable=True)
|
||||
mtg_arena_id = Column(String, nullable=True)
|
||||
mtgjson_foil_version_id = Column(String, nullable=True)
|
||||
mtgjson_non_foil_version_id = Column(String, nullable=True)
|
||||
mtgjson_v4_id = Column(String, nullable=True)
|
||||
mtgo_foil_id = Column(String, nullable=True)
|
||||
mtgo_id = Column(String, nullable=True)
|
||||
multiverse_id = Column(String, nullable=True)
|
||||
scg_id = Column(String, nullable=True)
|
||||
scryfall_id = Column(String, nullable=True)
|
||||
scryfall_card_back_id = Column(String, nullable=True)
|
||||
scryfall_oracle_id = Column(String, nullable=True)
|
||||
scryfall_illustration_id = Column(String, nullable=True)
|
||||
tcgplayer_product_id = Column(String, nullable=True)
|
||||
tcgplayer_etched_product_id = Column(String, nullable=True)
|
||||
tnt_id = Column(String, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.current_timestamp())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.current_timestamp())
|
@ -1,17 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
|
||||
class MTGJSONSKU(Base):
|
||||
__tablename__ = "mtgjson_skus"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
sku_id = Column(String, index=True)
|
||||
product_id = Column(String, index=True)
|
||||
condition = Column(String)
|
||||
finish = Column(String)
|
||||
language = Column(String)
|
||||
printing = Column(String)
|
||||
card_id = Column(String, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.current_timestamp())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.current_timestamp())
|
@ -1,12 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime
|
||||
from app.db.database import Base
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = "products"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
tcgplayer_id = Column(String)
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
updated_at = Column(DateTime(timezone=True))
|
||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
@ -1,23 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
|
||||
class TCGPlayerCategory(Base):
|
||||
__tablename__ = "tcgplayer_categories"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
category_id = Column(Integer, unique=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
display_name = Column(String)
|
||||
seo_category_name = Column(String)
|
||||
category_description = Column(String)
|
||||
category_page_title = Column(String)
|
||||
sealed_label = Column(String)
|
||||
non_sealed_label = Column(String)
|
||||
condition_guide_url = Column(String)
|
||||
is_scannable = Column(Boolean, default=False)
|
||||
popularity = Column(Integer, default=0)
|
||||
is_direct = Column(Boolean, default=False)
|
||||
modified_on = Column(DateTime)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
@ -1,17 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
|
||||
class TCGPlayerGroup(Base):
|
||||
__tablename__ = "tcgplayer_groups"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
group_id = Column(Integer, unique=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
abbreviation = Column(String)
|
||||
is_supplemental = Column(Boolean, default=False)
|
||||
published_on = Column(DateTime)
|
||||
modified_on = Column(DateTime)
|
||||
category_id = Column(Integer)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
|
||||
@ -6,7 +6,7 @@ class TCGPlayerInventory(Base):
|
||||
__tablename__ = "tcgplayer_inventory"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
tcgplayer_id = Column(String, unique=True, index=True)
|
||||
tcgplayer_sku_id = Column(Integer, ForeignKey("mtgjson_skus.tcgplayer_sku_id"), unique=True, index=True)
|
||||
product_line = Column(String)
|
||||
set_name = Column(String)
|
||||
product_name = Column(String)
|
||||
@ -22,6 +22,5 @@ class TCGPlayerInventory(Base):
|
||||
add_to_quantity = Column(Integer)
|
||||
tcg_marketplace_price = Column(Float)
|
||||
photo_url = Column(String)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.current_timestamp())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.current_timestamp())
|
||||
deleted_at = 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())
|
@ -1,13 +1,14 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, JSON
|
||||
from datetime import datetime, UTC
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, JSON, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
class TCGPlayerOrder(Base):
|
||||
__tablename__ = "tcgplayer_orders"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
order_number = Column(String, index=True)
|
||||
order_created_at = Column(DateTime)
|
||||
order_created_at = Column(DateTime(timezone=True))
|
||||
status = Column(String)
|
||||
channel = Column(String)
|
||||
fulfillment = Column(String)
|
||||
@ -16,7 +17,7 @@ class TCGPlayerOrder(Base):
|
||||
payment_type = Column(String)
|
||||
pickup_status = Column(String)
|
||||
shipping_type = Column(String)
|
||||
estimated_delivery_date = Column(DateTime)
|
||||
estimated_delivery_date = Column(DateTime(timezone=True))
|
||||
recipient_name = Column(String)
|
||||
address_line_1 = Column(String)
|
||||
address_line_2 = Column(String)
|
||||
@ -25,8 +26,8 @@ class TCGPlayerOrder(Base):
|
||||
zip_code = Column(String)
|
||||
country = Column(String)
|
||||
tracking_numbers = Column(JSON)
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class TCGPlayerOrderTransaction(Base):
|
||||
@ -41,8 +42,8 @@ class TCGPlayerOrderTransaction(Base):
|
||||
net_amount = Column(Float)
|
||||
direct_fee_amount = Column(Float)
|
||||
taxes = Column(JSON)
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class TCGPlayerOrderProduct(Base):
|
||||
@ -55,17 +56,18 @@ class TCGPlayerOrderProduct(Base):
|
||||
extended_price = Column(Float)
|
||||
quantity = Column(Integer)
|
||||
url = Column(String)
|
||||
product_id = Column(String)
|
||||
sku_id = Column(String)
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
||||
product_id = Column(Integer)
|
||||
sku_id = Column(Integer)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class TCGPlayerOrderRefund(Base):
|
||||
__tablename__ = "tcgplayer_order_refunds"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
order_number = Column(String, index=True)
|
||||
refund_created_at = Column(DateTime)
|
||||
refund_created_at = Column(DateTime(timezone=True))
|
||||
type = Column(String)
|
||||
amount = Column(Float)
|
||||
type = Column(String)
|
||||
@ -73,5 +75,5 @@ class TCGPlayerOrderRefund(Base):
|
||||
origin = Column(String)
|
||||
shipping_amount = Column(Float)
|
||||
products = Column(JSON)
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
@ -1,19 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, Float, DateTime, String
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
|
||||
class TCGPlayerPriceHistory(Base):
|
||||
__tablename__ = "tcgplayer_price_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
product_id = Column(Integer, index=True)
|
||||
date = Column(DateTime, index=True)
|
||||
low_price = Column(Float)
|
||||
mid_price = Column(Float)
|
||||
high_price = Column(Float)
|
||||
market_price = Column(Float)
|
||||
direct_low_price = Column(Float)
|
||||
sub_type_name = Column(String)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
@ -1,38 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
|
||||
class TCGPlayerProduct(Base):
|
||||
__tablename__ = "tcgplayer_products"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
product_id = Column(Integer, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
clean_name = Column(String)
|
||||
image_url = Column(String)
|
||||
category_id = Column(Integer)
|
||||
group_id = Column(Integer)
|
||||
url = Column(String)
|
||||
modified_on = Column(DateTime)
|
||||
image_count = Column(Integer)
|
||||
ext_rarity = Column(String)
|
||||
ext_subtype = Column(String)
|
||||
ext_oracle_text = Column(String)
|
||||
ext_number = Column(String)
|
||||
low_price = Column(Float)
|
||||
mid_price = Column(Float)
|
||||
high_price = Column(Float)
|
||||
market_price = Column(Float)
|
||||
direct_low_price = Column(Float)
|
||||
sub_type_name = Column(String)
|
||||
ext_power = Column(String)
|
||||
ext_toughness = Column(String)
|
||||
ext_flavor_text = Column(String)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
most_recent_tcgplayer_price = relationship("MostRecentTCGPlayerPrice", back_populates="product")
|
||||
marketplace_listings = relationship("MarketplaceListing", back_populates="product")
|
||||
inventory_items = relationship("InventoryItem", back_populates="product")
|
282
app/models/tcgplayer_products.py
Normal file
282
app/models/tcgplayer_products.py
Normal file
@ -0,0 +1,282 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Table, Float, Boolean, Index, text, DDL, event, UniqueConstraint, ForeignKeyConstraint
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
# =============================================================================
|
||||
# Core Models
|
||||
# =============================================================================
|
||||
|
||||
class MTGJSONSKU(Base):
|
||||
"""Represents the most granular level of card identification.
|
||||
THIS WORKS EVEN IF ITS A BOX SOMEHOW
|
||||
"""
|
||||
__tablename__ = "mtgjson_skus"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
mtgjson_uuid = Column(String, ForeignKey("mtgjson_cards.mtgjson_uuid"), unique=True, index=True)
|
||||
tcgplayer_sku_id = Column(Integer, index=True, unique=True)
|
||||
tcgplayer_product_id = Column(Integer, nullable=False)
|
||||
normalized_printing = Column(String, nullable=False) # normalized for FK
|
||||
condition = Column(String) # for boxes, condition = unopened
|
||||
finish = Column(String, nullable=True) # TODO MAKE THESE ENUMS
|
||||
language = Column(String)
|
||||
printing = Column(String) # original unnormalized field ##### for boxes, printing = NON FOIL
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Foreign key to tcgplayer_products via composite key
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(
|
||||
["tcgplayer_product_id", "normalized_printing"],
|
||||
["tcgplayer_products.tcgplayer_product_id", "tcgplayer_products.normalized_sub_type_name"],
|
||||
name="fk_sku_to_product_composite"
|
||||
),
|
||||
Index('idx_sku_product_printing', 'tcgplayer_product_id', 'normalized_printing', unique=True),
|
||||
)
|
||||
|
||||
product = relationship("TCGPlayerProduct", back_populates="skus")
|
||||
physical_items = relationship("PhysicalItem", back_populates="sku", primaryjoin="PhysicalItem.tcgplayer_sku_id == MTGJSONSKU.tcgplayer_sku_id")
|
||||
|
||||
card = relationship("MTGJSONCard", back_populates="skus", primaryjoin="MTGJSONCard.mtgjson_uuid == MTGJSONSKU.mtgjson_uuid")
|
||||
|
||||
|
||||
class MTGJSONCard(Base):
|
||||
"""Represents a Magic: The Gathering card."""
|
||||
__tablename__ = "mtgjson_cards"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
mtgjson_uuid = Column(String, ForeignKey("mtgjson_cards.mtgjson_uuid"), unique=True, index=True)
|
||||
name = Column(String)
|
||||
set_code = Column(String)
|
||||
abu_id = Column(String, nullable=True)
|
||||
card_kingdom_etched_id = Column(String, nullable=True)
|
||||
card_kingdom_foil_id = Column(String, nullable=True)
|
||||
card_kingdom_id = Column(String, nullable=True)
|
||||
cardsphere_id = Column(String, nullable=True)
|
||||
cardsphere_foil_id = Column(String, nullable=True)
|
||||
cardtrader_id = Column(String, nullable=True)
|
||||
csi_id = Column(String, nullable=True)
|
||||
mcm_id = Column(String, nullable=True)
|
||||
mcm_meta_id = Column(String, nullable=True)
|
||||
miniaturemarket_id = Column(String, nullable=True)
|
||||
mtg_arena_id = Column(String, nullable=True)
|
||||
mtgjson_foil_version_id = Column(String, nullable=True)
|
||||
mtgjson_non_foil_version_id = Column(String, nullable=True)
|
||||
mtgjson_v4_id = Column(String, nullable=True)
|
||||
mtgo_foil_id = Column(String, nullable=True)
|
||||
mtgo_id = Column(String, nullable=True)
|
||||
multiverse_id = Column(String, nullable=True)
|
||||
scg_id = Column(String, nullable=True)
|
||||
scryfall_id = Column(String, nullable=True)
|
||||
scryfall_card_back_id = Column(String, nullable=True)
|
||||
scryfall_oracle_id = Column(String, nullable=True)
|
||||
scryfall_illustration_id = Column(String, nullable=True)
|
||||
tcgplayer_product_id = Column(String, nullable=True)
|
||||
tcgplayer_etched_product_id = Column(String, nullable=True)
|
||||
tnt_id = Column(String, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
skus = relationship("MTGJSONSKU", back_populates="card", primaryjoin="MTGJSONSKU.mtgjson_uuid == MTGJSONCard.mtgjson_uuid")
|
||||
|
||||
|
||||
class TCGPlayerProduct(Base):
|
||||
"""Represents a higher-level TCGPlayer product concept."""
|
||||
__tablename__ = "tcgplayer_products"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
tcgplayer_product_id = Column(Integer, nullable=False)
|
||||
normalized_sub_type_name = Column(String, nullable=False) # normalized for FK
|
||||
sub_type_name = Column(String) # original unnormalized field
|
||||
|
||||
name = Column(String, nullable=False)
|
||||
clean_name = Column(String)
|
||||
image_url = Column(String)
|
||||
category_id = Column(Integer, ForeignKey("tcgplayer_categories.category_id"))
|
||||
group_id = Column(Integer, ForeignKey("tcgplayer_groups.group_id"))
|
||||
url = Column(String)
|
||||
modified_on = Column(DateTime)
|
||||
image_count = Column(Integer)
|
||||
ext_rarity = Column(String)
|
||||
ext_subtype = Column(String)
|
||||
ext_oracle_text = Column(String)
|
||||
ext_number = Column(String)
|
||||
low_price = Column(Float)
|
||||
mid_price = Column(Float)
|
||||
high_price = Column(Float)
|
||||
market_price = Column(Float)
|
||||
direct_low_price = Column(Float)
|
||||
ext_power = Column(String)
|
||||
ext_toughness = Column(String)
|
||||
ext_flavor_text = Column(String)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Enforce uniqueness for composite key
|
||||
__table_args__ = (
|
||||
UniqueConstraint("tcgplayer_product_id", "normalized_sub_type_name", name="uq_product_subtype"),
|
||||
)
|
||||
|
||||
# Backref to SKUs that link via composite FK
|
||||
skus = relationship("MTGJSONSKU", back_populates="product")
|
||||
physical_items_direct = relationship("PhysicalItem",
|
||||
back_populates="product_direct",
|
||||
primaryjoin="foreign(TCGPlayerProduct.tcgplayer_product_id) == PhysicalItem.tcgplayer_product_id")
|
||||
category = relationship("TCGPlayerCategory", back_populates="products")
|
||||
group = relationship("TCGPlayerGroup", back_populates="products")
|
||||
price_history = relationship("TCGPlayerPriceHistory",
|
||||
back_populates="product",
|
||||
primaryjoin="and_(foreign(TCGPlayerProduct.tcgplayer_product_id) == TCGPlayerPriceHistory.product_id, "
|
||||
"foreign(TCGPlayerProduct.sub_type_name) == TCGPlayerPriceHistory.sub_type_name)")
|
||||
|
||||
most_recent_tcgplayer_price = relationship("MostRecentTCGPlayerPrice",
|
||||
back_populates="product",
|
||||
primaryjoin="and_(foreign(TCGPlayerProduct.tcgplayer_product_id) == MostRecentTCGPlayerPrice.product_id, "
|
||||
"foreign(TCGPlayerProduct.sub_type_name) == MostRecentTCGPlayerPrice.sub_type_name)")
|
||||
|
||||
# =============================================================================
|
||||
# Supporting Models
|
||||
# =============================================================================
|
||||
|
||||
class TCGPlayerCategory(Base):
|
||||
"""Represents a TCGPlayer product category."""
|
||||
__tablename__ = "tcgplayer_categories"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
category_id = Column(Integer, unique=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
display_name = Column(String)
|
||||
seo_category_name = Column(String)
|
||||
category_description = Column(String)
|
||||
category_page_title = Column(String)
|
||||
sealed_label = Column(String)
|
||||
non_sealed_label = Column(String)
|
||||
condition_guide_url = Column(String)
|
||||
is_scannable = Column(Boolean, default=False)
|
||||
popularity = Column(Integer, default=0)
|
||||
is_direct = Column(Boolean, default=False)
|
||||
modified_on = Column(DateTime)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
products = relationship("TCGPlayerProduct", back_populates="category")
|
||||
|
||||
class TCGPlayerGroup(Base):
|
||||
"""Represents a TCGPlayer product group."""
|
||||
__tablename__ = "tcgplayer_groups"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
group_id = Column(Integer, unique=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
abbreviation = Column(String)
|
||||
is_supplemental = Column(Boolean, default=False)
|
||||
published_on = Column(DateTime)
|
||||
modified_on = Column(DateTime)
|
||||
category_id = Column(Integer, ForeignKey("tcgplayer_categories.category_id"))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
products = relationship("TCGPlayerProduct", back_populates="group")
|
||||
|
||||
class TCGPlayerPriceHistory(Base):
|
||||
"""Represents the price history for a product variant (foil/non-foil).
|
||||
|
||||
Each product has exactly two price history records - one for foil and one for non-foil.
|
||||
The relationship to TCGPlayerProduct is effectively 1:1 when considering both product_id
|
||||
and sub_type_name.
|
||||
"""
|
||||
__tablename__ = "tcgplayer_price_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
product_id = Column(Integer, nullable=False, index=True)
|
||||
sub_type_name = Column(String, index=True) # This indicates foil/non-foil
|
||||
date = Column(DateTime, index=True)
|
||||
low_price = Column(Float)
|
||||
mid_price = Column(Float)
|
||||
high_price = Column(Float)
|
||||
market_price = Column(Float)
|
||||
direct_low_price = Column(Float)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Add a unique constraint on (product_id, sub_type_name, date) to prevent duplicate entries
|
||||
__table_args__ = (
|
||||
Index('idx_price_history_product_subtype_date', 'product_id', 'sub_type_name', 'date'),
|
||||
)
|
||||
|
||||
product = relationship("TCGPlayerProduct",
|
||||
back_populates="price_history",
|
||||
primaryjoin="and_(TCGPlayerPriceHistory.product_id == foreign(TCGPlayerProduct.tcgplayer_product_id), "
|
||||
"TCGPlayerPriceHistory.sub_type_name == foreign(TCGPlayerProduct.sub_type_name))")
|
||||
|
||||
class MostRecentTCGPlayerPrice(Base):
|
||||
"""Represents the most recent price for a product.
|
||||
|
||||
This is a materialized view that contains the latest price data for each product.
|
||||
It is maintained through triggers on the price_history table.
|
||||
"""
|
||||
__tablename__ = "most_recent_tcgplayer_price"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
product_id = Column(Integer, nullable=False, index=True)
|
||||
sub_type_name = Column(String, index=True) # This indicates foil/non-foil
|
||||
date = Column(DateTime, nullable=False)
|
||||
low_price = Column(Float)
|
||||
mid_price = Column(Float)
|
||||
high_price = Column(Float)
|
||||
market_price = Column(Float)
|
||||
direct_low_price = Column(Float)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_most_recent_price_product_subtype', 'product_id', 'sub_type_name', unique=True),
|
||||
{'info': {'is_view': True}}
|
||||
)
|
||||
|
||||
|
||||
product = relationship("TCGPlayerProduct",
|
||||
back_populates="most_recent_tcgplayer_price",
|
||||
primaryjoin="and_(MostRecentTCGPlayerPrice.product_id == foreign(TCGPlayerProduct.tcgplayer_product_id), "
|
||||
"MostRecentTCGPlayerPrice.sub_type_name == foreign(TCGPlayerProduct.sub_type_name))")
|
||||
|
||||
def create_most_recent_price_view():
|
||||
"""Creates the materialized view for most recent prices."""
|
||||
return DDL("""
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS most_recent_tcgplayer_price AS
|
||||
SELECT DISTINCT ON (ph.product_id, ph.sub_type_name)
|
||||
ph.id,
|
||||
ph.product_id,
|
||||
ph.sub_type_name,
|
||||
ph.date,
|
||||
ph.low_price,
|
||||
ph.mid_price,
|
||||
ph.high_price,
|
||||
ph.market_price,
|
||||
ph.direct_low_price,
|
||||
ph.created_at,
|
||||
ph.updated_at
|
||||
FROM tcgplayer_price_history ph
|
||||
ORDER BY ph.product_id, ph.sub_type_name, ph.date DESC;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_most_recent_price_product_subtype
|
||||
ON most_recent_tcgplayer_price (product_id, sub_type_name);
|
||||
""")
|
||||
|
||||
# Register the view creation with SQLAlchemy
|
||||
event.listen(
|
||||
MostRecentTCGPlayerPrice.__table__,
|
||||
'after_create',
|
||||
create_most_recent_price_view()
|
||||
)
|
||||
|
||||
# Add a method to refresh the view
|
||||
@classmethod
|
||||
def refresh_view(cls, session):
|
||||
"""Refreshes the materialized view."""
|
||||
session.execute(text("REFRESH MATERIALIZED VIEW CONCURRENTLY most_recent_tcgplayer_price"))
|
||||
session.commit()
|
||||
|
||||
MostRecentTCGPlayerPrice.refresh_view = refresh_view
|
Reference in New Issue
Block a user