i think most of this works lole

This commit is contained in:
2025-04-24 23:34:13 -04:00
parent 210a033695
commit 56ba750aad
50 changed files with 154001 additions and 2606 deletions

View File

@ -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'
]

View 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())

View File

@ -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")

View 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")

View 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")

View File

@ -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())

View File

@ -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())

View File

@ -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)

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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")

View 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