Compare commits
10 Commits
6178fdd15d
...
main
Author | SHA1 | Date | |
---|---|---|---|
dca11b0ede | |||
f2c2b69d63 | |||
5c85411c69 | |||
11aa4cda16 | |||
c9bba8a26e | |||
d75e20ff2c | |||
56ba750aad | |||
210a033695 | |||
d8ae45c025 | |||
34eac3d954 |
@ -67,7 +67,8 @@ def run_migrations_online() -> None:
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
|
36
alembic/versions/0f534237fc90_i_literally_hate_sql.py
Normal file
36
alembic/versions/0f534237fc90_i_literally_hate_sql.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""i literally hate sql
|
||||
|
||||
Revision ID: 0f534237fc90
|
||||
Revises: cf61f006db46
|
||||
Create Date: 2025-04-25 16:59:07.177958
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '0f534237fc90'
|
||||
down_revision: Union[str, None] = 'cf61f006db46'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint('uq_sku_mtgjson_uuid', 'mtgjson_skus', type_='unique')
|
||||
op.drop_index('ix_mtgjson_skus_mtgjson_uuid', table_name='mtgjson_skus')
|
||||
op.create_index(op.f('ix_mtgjson_skus_mtgjson_uuid'), 'mtgjson_skus', ['mtgjson_uuid'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_mtgjson_skus_mtgjson_uuid'), table_name='mtgjson_skus')
|
||||
op.create_index('ix_mtgjson_skus_mtgjson_uuid', 'mtgjson_skus', ['mtgjson_uuid'], unique=True)
|
||||
op.create_unique_constraint('uq_sku_mtgjson_uuid', 'mtgjson_skus', ['mtgjson_uuid'])
|
||||
# ### end Alembic commands ###
|
@ -1,42 +0,0 @@
|
||||
"""tcg product update again
|
||||
|
||||
Revision ID: 1746d35187a2
|
||||
Revises: 9775314e337b
|
||||
Create Date: 2025-04-17 22:02:35.492726
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '1746d35187a2'
|
||||
down_revision: Union[str, None] = '9775314e337b'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('tcgplayer_products', sa.Column('ext_subtype', sa.String(), nullable=True))
|
||||
op.add_column('tcgplayer_products', sa.Column('ext_oracle_text', sa.String(), nullable=True))
|
||||
op.add_column('tcgplayer_products', sa.Column('ext_flavor_text', sa.String(), nullable=True))
|
||||
op.drop_column('tcgplayer_products', 'ext_mana_cost')
|
||||
op.drop_column('tcgplayer_products', 'ext_loyalty')
|
||||
op.drop_column('tcgplayer_products', 'ext_mana_value')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('tcgplayer_products', sa.Column('ext_mana_value', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||
op.add_column('tcgplayer_products', sa.Column('ext_loyalty', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||
op.add_column('tcgplayer_products', sa.Column('ext_mana_cost', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||
op.drop_column('tcgplayer_products', 'ext_flavor_text')
|
||||
op.drop_column('tcgplayer_products', 'ext_oracle_text')
|
||||
op.drop_column('tcgplayer_products', 'ext_subtype')
|
||||
# ### end Alembic commands ###
|
40
alembic/versions/236605bcac6e_asdf.py
Normal file
40
alembic/versions/236605bcac6e_asdf.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""asdf
|
||||
|
||||
Revision ID: 236605bcac6e
|
||||
Revises: d13600612a8f
|
||||
Create Date: 2025-04-28 21:44:28.030202
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '236605bcac6e'
|
||||
down_revision: Union[str, None] = 'd13600612a8f'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('marketplace_listings', sa.Column('recommended_price_id', sa.Integer(), nullable=True))
|
||||
op.add_column('marketplace_listings', sa.Column('listed_price_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(None, 'marketplace_listings', 'pricing_events', ['recommended_price_id'], ['id'])
|
||||
op.create_foreign_key(None, 'marketplace_listings', 'pricing_events', ['listed_price_id'], ['id'])
|
||||
op.drop_column('marketplace_listings', 'listed_price')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('marketplace_listings', sa.Column('listed_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
|
||||
op.drop_constraint(None, 'marketplace_listings', type_='foreignkey')
|
||||
op.drop_constraint(None, 'marketplace_listings', type_='foreignkey')
|
||||
op.drop_column('marketplace_listings', 'listed_price_id')
|
||||
op.drop_column('marketplace_listings', 'recommended_price_id')
|
||||
# ### end Alembic commands ###
|
@ -1,32 +0,0 @@
|
||||
"""tcg prices again
|
||||
|
||||
Revision ID: 2fcce9c8883a
|
||||
Revises: b45c43900b56
|
||||
Create Date: 2025-04-17 22:48:53.378544
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '2fcce9c8883a'
|
||||
down_revision: Union[str, None] = 'b45c43900b56'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
@ -1,369 +0,0 @@
|
||||
"""i hate alembic so goddamn much
|
||||
|
||||
Revision ID: 479003fbead7
|
||||
Revises:
|
||||
Create Date: 2025-04-17 12:08:13.714276
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '479003fbead7'
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('cards',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('rarity', sa.String(), nullable=True),
|
||||
sa.Column('set_name', sa.String(), nullable=True),
|
||||
sa.Column('price', sa.Float(), nullable=True),
|
||||
sa.Column('quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('tcgplayer_sku', sa.String(), nullable=True),
|
||||
sa.Column('product_line', sa.String(), nullable=True),
|
||||
sa.Column('product_name', sa.String(), nullable=True),
|
||||
sa.Column('title', sa.String(), nullable=True),
|
||||
sa.Column('number', sa.String(), nullable=True),
|
||||
sa.Column('condition', sa.String(), nullable=True),
|
||||
sa.Column('tcg_market_price', sa.Float(), nullable=True),
|
||||
sa.Column('tcg_direct_low', sa.Float(), nullable=True),
|
||||
sa.Column('tcg_low_price_with_shipping', sa.Float(), nullable=True),
|
||||
sa.Column('tcg_low_price', sa.Float(), nullable=True),
|
||||
sa.Column('total_quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('add_to_quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('tcg_marketplace_price', sa.Float(), nullable=True),
|
||||
sa.Column('photo_url', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_cards_id'), 'cards', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_cards_name'), 'cards', ['name'], unique=False)
|
||||
op.create_index(op.f('ix_cards_set_name'), 'cards', ['set_name'], unique=False)
|
||||
op.create_index(op.f('ix_cards_tcgplayer_sku'), 'cards', ['tcgplayer_sku'], unique=True)
|
||||
op.create_table('files',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('file_type', sa.String(), nullable=True),
|
||||
sa.Column('content_type', sa.String(), nullable=True),
|
||||
sa.Column('path', sa.String(), nullable=True),
|
||||
sa.Column('size', sa.Integer(), nullable=True),
|
||||
sa.Column('file_metadata', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_files_id'), 'files', ['id'], unique=False)
|
||||
op.create_table('games',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('description', sa.String(), nullable=True),
|
||||
sa.Column('image_url', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_games_id'), 'games', ['id'], unique=False)
|
||||
op.create_table('inventory',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('tcgplayer_id', sa.String(), nullable=True),
|
||||
sa.Column('product_line', sa.String(), nullable=True),
|
||||
sa.Column('set_name', sa.String(), nullable=True),
|
||||
sa.Column('product_name', sa.String(), nullable=True),
|
||||
sa.Column('title', sa.String(), nullable=True),
|
||||
sa.Column('number', sa.String(), nullable=True),
|
||||
sa.Column('rarity', sa.String(), nullable=True),
|
||||
sa.Column('condition', sa.String(), nullable=True),
|
||||
sa.Column('tcg_market_price', sa.Float(), nullable=True),
|
||||
sa.Column('tcg_direct_low', sa.Float(), nullable=True),
|
||||
sa.Column('tcg_low_price_with_shipping', sa.Float(), nullable=True),
|
||||
sa.Column('tcg_low_price', sa.Float(), nullable=True),
|
||||
sa.Column('total_quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('add_to_quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('tcg_marketplace_price', sa.Float(), nullable=True),
|
||||
sa.Column('photo_url', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_inventory_id'), 'inventory', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_inventory_tcgplayer_id'), 'inventory', ['tcgplayer_id'], unique=True)
|
||||
op.create_table('mtgjson_cards',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('card_id', sa.String(), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('set_code', sa.String(), nullable=True),
|
||||
sa.Column('uuid', sa.String(), nullable=True),
|
||||
sa.Column('abu_id', sa.String(), nullable=True),
|
||||
sa.Column('card_kingdom_etched_id', sa.String(), nullable=True),
|
||||
sa.Column('card_kingdom_foil_id', sa.String(), nullable=True),
|
||||
sa.Column('card_kingdom_id', sa.String(), nullable=True),
|
||||
sa.Column('cardsphere_id', sa.String(), nullable=True),
|
||||
sa.Column('cardsphere_foil_id', sa.String(), nullable=True),
|
||||
sa.Column('cardtrader_id', sa.String(), nullable=True),
|
||||
sa.Column('csi_id', sa.String(), nullable=True),
|
||||
sa.Column('mcm_id', sa.String(), nullable=True),
|
||||
sa.Column('mcm_meta_id', sa.String(), nullable=True),
|
||||
sa.Column('miniaturemarket_id', sa.String(), nullable=True),
|
||||
sa.Column('mtg_arena_id', sa.String(), nullable=True),
|
||||
sa.Column('mtgjson_foil_version_id', sa.String(), nullable=True),
|
||||
sa.Column('mtgjson_non_foil_version_id', sa.String(), nullable=True),
|
||||
sa.Column('mtgjson_v4_id', sa.String(), nullable=True),
|
||||
sa.Column('mtgo_foil_id', sa.String(), nullable=True),
|
||||
sa.Column('mtgo_id', sa.String(), nullable=True),
|
||||
sa.Column('multiverse_id', sa.String(), nullable=True),
|
||||
sa.Column('scg_id', sa.String(), nullable=True),
|
||||
sa.Column('scryfall_id', sa.String(), nullable=True),
|
||||
sa.Column('scryfall_card_back_id', sa.String(), nullable=True),
|
||||
sa.Column('scryfall_oracle_id', sa.String(), nullable=True),
|
||||
sa.Column('scryfall_illustration_id', sa.String(), nullable=True),
|
||||
sa.Column('tcgplayer_product_id', sa.String(), nullable=True),
|
||||
sa.Column('tcgplayer_etched_product_id', sa.String(), nullable=True),
|
||||
sa.Column('tnt_id', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_mtgjson_cards_card_id'), 'mtgjson_cards', ['card_id'], unique=True)
|
||||
op.create_index(op.f('ix_mtgjson_cards_id'), 'mtgjson_cards', ['id'], unique=False)
|
||||
op.create_table('mtgjson_skus',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('sku_id', sa.String(), nullable=True),
|
||||
sa.Column('product_id', sa.String(), nullable=True),
|
||||
sa.Column('condition', sa.String(), nullable=True),
|
||||
sa.Column('finish', sa.String(), nullable=True),
|
||||
sa.Column('language', sa.String(), nullable=True),
|
||||
sa.Column('printing', sa.String(), nullable=True),
|
||||
sa.Column('card_id', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_mtgjson_skus_id'), 'mtgjson_skus', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_mtgjson_skus_product_id'), 'mtgjson_skus', ['product_id'], unique=False)
|
||||
op.create_index(op.f('ix_mtgjson_skus_sku_id'), 'mtgjson_skus', ['sku_id'], unique=False)
|
||||
op.create_table('tcgplayer_categories',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('category_id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('display_name', sa.String(), nullable=True),
|
||||
sa.Column('seo_category_name', sa.String(), nullable=True),
|
||||
sa.Column('category_description', sa.String(), nullable=True),
|
||||
sa.Column('category_page_title', sa.String(), nullable=True),
|
||||
sa.Column('sealed_label', sa.String(), nullable=True),
|
||||
sa.Column('non_sealed_label', sa.String(), nullable=True),
|
||||
sa.Column('condition_guide_url', sa.String(), nullable=True),
|
||||
sa.Column('is_scannable', sa.Boolean(), nullable=True),
|
||||
sa.Column('popularity', sa.Integer(), nullable=True),
|
||||
sa.Column('is_direct', sa.Boolean(), nullable=True),
|
||||
sa.Column('modified_on', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_categories_category_id'), 'tcgplayer_categories', ['category_id'], unique=True)
|
||||
op.create_index(op.f('ix_tcgplayer_categories_id'), 'tcgplayer_categories', ['id'], unique=False)
|
||||
op.create_table('tcgplayer_groups',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('group_id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('abbreviation', sa.String(), nullable=True),
|
||||
sa.Column('is_supplemental', sa.Boolean(), nullable=True),
|
||||
sa.Column('published_on', sa.DateTime(), nullable=True),
|
||||
sa.Column('modified_on', sa.DateTime(), nullable=True),
|
||||
sa.Column('category_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_groups_group_id'), 'tcgplayer_groups', ['group_id'], unique=True)
|
||||
op.create_index(op.f('ix_tcgplayer_groups_id'), 'tcgplayer_groups', ['id'], unique=False)
|
||||
op.create_table('tcgplayer_order_products',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_number', sa.String(), nullable=True),
|
||||
sa.Column('product_name', sa.String(), nullable=True),
|
||||
sa.Column('unit_price', sa.Float(), nullable=True),
|
||||
sa.Column('extended_price', sa.Float(), nullable=True),
|
||||
sa.Column('quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('url', sa.String(), nullable=True),
|
||||
sa.Column('product_id', sa.String(), nullable=True),
|
||||
sa.Column('sku_id', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_order_products_id'), 'tcgplayer_order_products', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_order_products_order_number'), 'tcgplayer_order_products', ['order_number'], unique=False)
|
||||
op.create_table('tcgplayer_order_refunds',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_number', sa.String(), nullable=True),
|
||||
sa.Column('refund_created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('type', sa.String(), nullable=True),
|
||||
sa.Column('amount', sa.Float(), nullable=True),
|
||||
sa.Column('description', sa.String(), nullable=True),
|
||||
sa.Column('origin', sa.String(), nullable=True),
|
||||
sa.Column('shipping_amount', sa.Float(), nullable=True),
|
||||
sa.Column('products', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_order_refunds_id'), 'tcgplayer_order_refunds', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_order_refunds_order_number'), 'tcgplayer_order_refunds', ['order_number'], unique=False)
|
||||
op.create_table('tcgplayer_order_transactions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_number', sa.String(), nullable=True),
|
||||
sa.Column('product_amount', sa.Float(), nullable=True),
|
||||
sa.Column('shipping_amount', sa.Float(), nullable=True),
|
||||
sa.Column('gross_amount', sa.Float(), nullable=True),
|
||||
sa.Column('fee_amount', sa.Float(), nullable=True),
|
||||
sa.Column('net_amount', sa.Float(), nullable=True),
|
||||
sa.Column('direct_fee_amount', sa.Float(), nullable=True),
|
||||
sa.Column('taxes', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_order_transactions_id'), 'tcgplayer_order_transactions', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_order_transactions_order_number'), 'tcgplayer_order_transactions', ['order_number'], unique=False)
|
||||
op.create_table('tcgplayer_orders',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_number', sa.String(), nullable=True),
|
||||
sa.Column('order_created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('status', sa.String(), nullable=True),
|
||||
sa.Column('channel', sa.String(), nullable=True),
|
||||
sa.Column('fulfillment', sa.String(), nullable=True),
|
||||
sa.Column('seller_name', sa.String(), nullable=True),
|
||||
sa.Column('buyer_name', sa.String(), nullable=True),
|
||||
sa.Column('payment_type', sa.String(), nullable=True),
|
||||
sa.Column('pickup_status', sa.String(), nullable=True),
|
||||
sa.Column('shipping_type', sa.String(), nullable=True),
|
||||
sa.Column('estimated_delivery_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('recipient_name', sa.String(), nullable=True),
|
||||
sa.Column('address_line_1', sa.String(), nullable=True),
|
||||
sa.Column('address_line_2', sa.String(), nullable=True),
|
||||
sa.Column('city', sa.String(), nullable=True),
|
||||
sa.Column('state', sa.String(), nullable=True),
|
||||
sa.Column('zip_code', sa.String(), nullable=True),
|
||||
sa.Column('country', sa.String(), nullable=True),
|
||||
sa.Column('tracking_numbers', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_orders_id'), 'tcgplayer_orders', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_orders_order_number'), 'tcgplayer_orders', ['order_number'], unique=False)
|
||||
op.create_table('boxes',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=True),
|
||||
sa.Column('type', sa.String(), nullable=True),
|
||||
sa.Column('set_code', sa.String(), nullable=True),
|
||||
sa.Column('sku', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('game_id', sa.Integer(), nullable=True),
|
||||
sa.Column('expected_number_of_cards', sa.Integer(), nullable=True),
|
||||
sa.Column('description', sa.String(), nullable=True),
|
||||
sa.Column('image_url', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['game_id'], ['games.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_boxes_id'), 'boxes', ['id'], unique=False)
|
||||
op.create_table('tcgplayer_products',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('clean_name', sa.String(), nullable=True),
|
||||
sa.Column('image_url', sa.String(), nullable=True),
|
||||
sa.Column('category_id', sa.Integer(), nullable=True),
|
||||
sa.Column('group_id', sa.Integer(), nullable=True),
|
||||
sa.Column('url', sa.String(), nullable=True),
|
||||
sa.Column('modified_on', sa.DateTime(), nullable=True),
|
||||
sa.Column('image_count', sa.Integer(), nullable=True),
|
||||
sa.Column('ext_rarity', sa.String(), nullable=True),
|
||||
sa.Column('ext_number', sa.String(), nullable=True),
|
||||
sa.Column('low_price', sa.Float(), nullable=True),
|
||||
sa.Column('mid_price', sa.Float(), nullable=True),
|
||||
sa.Column('high_price', sa.Float(), nullable=True),
|
||||
sa.Column('market_price', sa.Float(), nullable=True),
|
||||
sa.Column('direct_low_price', sa.Float(), nullable=True),
|
||||
sa.Column('sub_type_name', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['group_id'], ['tcgplayer_groups.group_id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_products_id'), 'tcgplayer_products', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_products_product_id'), 'tcgplayer_products', ['product_id'], unique=False)
|
||||
op.create_table('open_boxes',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('box_id', sa.Integer(), nullable=True),
|
||||
sa.Column('number_of_cards', sa.Integer(), nullable=True),
|
||||
sa.Column('date_opened', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['box_id'], ['boxes.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_open_boxes_id'), 'open_boxes', ['id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_open_boxes_id'), table_name='open_boxes')
|
||||
op.drop_table('open_boxes')
|
||||
op.drop_index(op.f('ix_tcgplayer_products_product_id'), table_name='tcgplayer_products')
|
||||
op.drop_index(op.f('ix_tcgplayer_products_id'), table_name='tcgplayer_products')
|
||||
op.drop_table('tcgplayer_products')
|
||||
op.drop_index(op.f('ix_boxes_id'), table_name='boxes')
|
||||
op.drop_table('boxes')
|
||||
op.drop_index(op.f('ix_tcgplayer_orders_order_number'), table_name='tcgplayer_orders')
|
||||
op.drop_index(op.f('ix_tcgplayer_orders_id'), table_name='tcgplayer_orders')
|
||||
op.drop_table('tcgplayer_orders')
|
||||
op.drop_index(op.f('ix_tcgplayer_order_transactions_order_number'), table_name='tcgplayer_order_transactions')
|
||||
op.drop_index(op.f('ix_tcgplayer_order_transactions_id'), table_name='tcgplayer_order_transactions')
|
||||
op.drop_table('tcgplayer_order_transactions')
|
||||
op.drop_index(op.f('ix_tcgplayer_order_refunds_order_number'), table_name='tcgplayer_order_refunds')
|
||||
op.drop_index(op.f('ix_tcgplayer_order_refunds_id'), table_name='tcgplayer_order_refunds')
|
||||
op.drop_table('tcgplayer_order_refunds')
|
||||
op.drop_index(op.f('ix_tcgplayer_order_products_order_number'), table_name='tcgplayer_order_products')
|
||||
op.drop_index(op.f('ix_tcgplayer_order_products_id'), table_name='tcgplayer_order_products')
|
||||
op.drop_table('tcgplayer_order_products')
|
||||
op.drop_index(op.f('ix_tcgplayer_groups_id'), table_name='tcgplayer_groups')
|
||||
op.drop_index(op.f('ix_tcgplayer_groups_group_id'), table_name='tcgplayer_groups')
|
||||
op.drop_table('tcgplayer_groups')
|
||||
op.drop_index(op.f('ix_tcgplayer_categories_id'), table_name='tcgplayer_categories')
|
||||
op.drop_index(op.f('ix_tcgplayer_categories_category_id'), table_name='tcgplayer_categories')
|
||||
op.drop_table('tcgplayer_categories')
|
||||
op.drop_index(op.f('ix_mtgjson_skus_sku_id'), table_name='mtgjson_skus')
|
||||
op.drop_index(op.f('ix_mtgjson_skus_product_id'), table_name='mtgjson_skus')
|
||||
op.drop_index(op.f('ix_mtgjson_skus_id'), table_name='mtgjson_skus')
|
||||
op.drop_table('mtgjson_skus')
|
||||
op.drop_index(op.f('ix_mtgjson_cards_id'), table_name='mtgjson_cards')
|
||||
op.drop_index(op.f('ix_mtgjson_cards_card_id'), table_name='mtgjson_cards')
|
||||
op.drop_table('mtgjson_cards')
|
||||
op.drop_index(op.f('ix_inventory_tcgplayer_id'), table_name='inventory')
|
||||
op.drop_index(op.f('ix_inventory_id'), table_name='inventory')
|
||||
op.drop_table('inventory')
|
||||
op.drop_index(op.f('ix_games_id'), table_name='games')
|
||||
op.drop_table('games')
|
||||
op.drop_index(op.f('ix_files_id'), table_name='files')
|
||||
op.drop_table('files')
|
||||
op.drop_index(op.f('ix_cards_tcgplayer_sku'), table_name='cards')
|
||||
op.drop_index(op.f('ix_cards_set_name'), table_name='cards')
|
||||
op.drop_index(op.f('ix_cards_name'), table_name='cards')
|
||||
op.drop_index(op.f('ix_cards_id'), table_name='cards')
|
||||
op.drop_table('cards')
|
||||
# ### end Alembic commands ###
|
@ -1,51 +0,0 @@
|
||||
"""tcg prices again 2
|
||||
|
||||
Revision ID: 493b2cb724d0
|
||||
Revises: 2fcce9c8883a
|
||||
Create Date: 2025-04-17 23:05:11.919652
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '493b2cb724d0'
|
||||
down_revision: Union[str, None] = '2fcce9c8883a'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('ix_tcgplayer_prices_date', table_name='tcgplayer_prices')
|
||||
op.drop_index('ix_tcgplayer_prices_id', table_name='tcgplayer_prices')
|
||||
op.drop_index('ix_tcgplayer_prices_product_id', table_name='tcgplayer_prices')
|
||||
op.drop_table('tcgplayer_prices')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('tcgplayer_prices',
|
||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||
sa.Column('product_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
|
||||
sa.Column('low_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('mid_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('high_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('market_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('direct_low_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('sub_type_name', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True),
|
||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
||||
sa.PrimaryKeyConstraint('id', name='tcgplayer_prices_pkey')
|
||||
)
|
||||
op.create_index('ix_tcgplayer_prices_product_id', 'tcgplayer_prices', ['product_id'], unique=False)
|
||||
op.create_index('ix_tcgplayer_prices_id', 'tcgplayer_prices', ['id'], unique=False)
|
||||
op.create_index('ix_tcgplayer_prices_date', 'tcgplayer_prices', ['date'], unique=False)
|
||||
# ### end Alembic commands ###
|
@ -1,53 +0,0 @@
|
||||
"""fuck foreign keys for real dog
|
||||
|
||||
Revision ID: 54cd251d13a3
|
||||
Revises: e34bfa37db00
|
||||
Create Date: 2025-04-17 23:10:59.010644
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '54cd251d13a3'
|
||||
down_revision: Union[str, None] = 'e34bfa37db00'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('ix_tcgplayer_price_history_date', table_name='tcgplayer_price_history')
|
||||
op.drop_index('ix_tcgplayer_price_history_id', table_name='tcgplayer_price_history')
|
||||
op.drop_index('ix_tcgplayer_price_history_product_id', table_name='tcgplayer_price_history')
|
||||
op.drop_table('tcgplayer_price_history')
|
||||
op.drop_constraint('tcgplayer_products_group_id_fkey', 'tcgplayer_products', type_='foreignkey')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_foreign_key('tcgplayer_products_group_id_fkey', 'tcgplayer_products', 'tcgplayer_groups', ['group_id'], ['group_id'])
|
||||
op.create_table('tcgplayer_price_history',
|
||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||
sa.Column('product_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
|
||||
sa.Column('low_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('mid_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('high_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('market_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('direct_low_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('sub_type_name', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True),
|
||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
||||
sa.PrimaryKeyConstraint('id', name='tcgplayer_price_history_pkey')
|
||||
)
|
||||
op.create_index('ix_tcgplayer_price_history_product_id', 'tcgplayer_price_history', ['product_id'], unique=False)
|
||||
op.create_index('ix_tcgplayer_price_history_id', 'tcgplayer_price_history', ['id'], unique=False)
|
||||
op.create_index('ix_tcgplayer_price_history_date', 'tcgplayer_price_history', ['date'], unique=False)
|
||||
# ### end Alembic commands ###
|
@ -1,8 +1,8 @@
|
||||
"""tcg prices
|
||||
"""b
|
||||
|
||||
Revision ID: b45c43900b56
|
||||
Revises: 1746d35187a2
|
||||
Create Date: 2025-04-17 22:47:44.405906
|
||||
Revision ID: 62eee00bae8e
|
||||
Revises: 0f534237fc90
|
||||
Create Date: 2025-04-28 11:01:28.564264
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
@ -12,8 +12,8 @@ import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b45c43900b56'
|
||||
down_revision: Union[str, None] = '1746d35187a2'
|
||||
revision: str = '62eee00bae8e'
|
||||
down_revision: Union[str, None] = '0f534237fc90'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
@ -21,12 +21,12 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
op.create_index('idx_product_subtype', 'tcgplayer_products', ['tcgplayer_product_id', 'normalized_sub_type_name'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
op.drop_index('idx_product_subtype', table_name='tcgplayer_products')
|
||||
# ### end Alembic commands ###
|
@ -1,32 +0,0 @@
|
||||
"""fuck foreign keys for real dog
|
||||
|
||||
Revision ID: 7f309a891094
|
||||
Revises: 54cd251d13a3
|
||||
Create Date: 2025-04-17 23:11:55.027126
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '7f309a891094'
|
||||
down_revision: Union[str, None] = '54cd251d13a3'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
@ -1,40 +0,0 @@
|
||||
"""tcg product update
|
||||
|
||||
Revision ID: 9775314e337b
|
||||
Revises: 479003fbead7
|
||||
Create Date: 2025-04-17 21:58:17.637210
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '9775314e337b'
|
||||
down_revision: Union[str, None] = '479003fbead7'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('tcgplayer_products', sa.Column('ext_power', sa.String(), nullable=True))
|
||||
op.add_column('tcgplayer_products', sa.Column('ext_toughness', sa.String(), nullable=True))
|
||||
op.add_column('tcgplayer_products', sa.Column('ext_loyalty', sa.String(), nullable=True))
|
||||
op.add_column('tcgplayer_products', sa.Column('ext_mana_cost', sa.String(), nullable=True))
|
||||
op.add_column('tcgplayer_products', sa.Column('ext_mana_value', sa.String(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('tcgplayer_products', 'ext_mana_value')
|
||||
op.drop_column('tcgplayer_products', 'ext_mana_cost')
|
||||
op.drop_column('tcgplayer_products', 'ext_loyalty')
|
||||
op.drop_column('tcgplayer_products', 'ext_toughness')
|
||||
op.drop_column('tcgplayer_products', 'ext_power')
|
||||
# ### end Alembic commands ###
|
@ -1,51 +0,0 @@
|
||||
"""recreate tcgplayer price history
|
||||
|
||||
Revision ID: 9fb73424598c
|
||||
Revises: 7f309a891094
|
||||
Create Date: 2025-04-17 23:13:55.027126
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '9fb73424598c'
|
||||
down_revision: Union[str, None] = '7f309a891094'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('tcgplayer_price_history',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=True),
|
||||
sa.Column('date', sa.DateTime(), nullable=True),
|
||||
sa.Column('low_price', sa.Float(), nullable=True),
|
||||
sa.Column('mid_price', sa.Float(), nullable=True),
|
||||
sa.Column('high_price', sa.Float(), nullable=True),
|
||||
sa.Column('market_price', sa.Float(), nullable=True),
|
||||
sa.Column('direct_low_price', sa.Float(), nullable=True),
|
||||
sa.Column('sub_type_name', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_price_history_id'), 'tcgplayer_price_history', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_price_history_product_id'), 'tcgplayer_price_history', ['product_id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_price_history_date'), 'tcgplayer_price_history', ['date'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_tcgplayer_price_history_date'), table_name='tcgplayer_price_history')
|
||||
op.drop_index(op.f('ix_tcgplayer_price_history_product_id'), table_name='tcgplayer_price_history')
|
||||
op.drop_index(op.f('ix_tcgplayer_price_history_id'), table_name='tcgplayer_price_history')
|
||||
op.drop_table('tcgplayer_price_history')
|
||||
# ### end Alembic commands ###
|
@ -1,28 +0,0 @@
|
||||
"""changing db bigly
|
||||
|
||||
Revision ID: cc7dd65bcdd9
|
||||
Revises: 9fb73424598c
|
||||
Create Date: 2025-04-19 13:36:41.784661
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'cc7dd65bcdd9'
|
||||
down_revision: Union[str, None] = '9fb73424598c'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
pass
|
@ -0,0 +1,553 @@
|
||||
"""alembic is actually behaving so this message will be nice :)
|
||||
|
||||
Revision ID: cf61f006db46
|
||||
Revises:
|
||||
Create Date: 2025-04-25 14:34:28.206737
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'cf61f006db46'
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('critical_error_logs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('error_message', sa.String(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_critical_error_logs_id'), 'critical_error_logs', ['id'], unique=False)
|
||||
op.create_table('customers',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_customers_id'), 'customers', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_customers_name'), 'customers', ['name'], unique=False)
|
||||
op.create_table('files',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('file_type', sa.String(), nullable=True),
|
||||
sa.Column('content_type', sa.String(), nullable=True),
|
||||
sa.Column('path', sa.String(), nullable=True),
|
||||
sa.Column('size', sa.Integer(), nullable=True),
|
||||
sa.Column('file_metadata', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_files_id'), 'files', ['id'], unique=False)
|
||||
op.create_table('marketplaces',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_marketplaces_id'), 'marketplaces', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_marketplaces_name'), 'marketplaces', ['name'], unique=False)
|
||||
op.create_table('most_recent_tcgplayer_price',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('sub_type_name', sa.String(), nullable=True),
|
||||
sa.Column('date', sa.DateTime(), nullable=False),
|
||||
sa.Column('low_price', sa.Float(), nullable=True),
|
||||
sa.Column('mid_price', sa.Float(), nullable=True),
|
||||
sa.Column('high_price', sa.Float(), nullable=True),
|
||||
sa.Column('market_price', sa.Float(), nullable=True),
|
||||
sa.Column('direct_low_price', sa.Float(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_most_recent_price_product_subtype', 'most_recent_tcgplayer_price', ['product_id', 'sub_type_name'], unique=True)
|
||||
op.create_index(op.f('ix_most_recent_tcgplayer_price_product_id'), 'most_recent_tcgplayer_price', ['product_id'], unique=False)
|
||||
op.create_index(op.f('ix_most_recent_tcgplayer_price_sub_type_name'), 'most_recent_tcgplayer_price', ['sub_type_name'], unique=False)
|
||||
op.create_table('mtgjson_cards',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('mtgjson_uuid', sa.String(), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('set_code', sa.String(), nullable=True),
|
||||
sa.Column('abu_id', sa.String(), nullable=True),
|
||||
sa.Column('card_kingdom_etched_id', sa.String(), nullable=True),
|
||||
sa.Column('card_kingdom_foil_id', sa.String(), nullable=True),
|
||||
sa.Column('card_kingdom_id', sa.String(), nullable=True),
|
||||
sa.Column('cardsphere_id', sa.String(), nullable=True),
|
||||
sa.Column('cardsphere_foil_id', sa.String(), nullable=True),
|
||||
sa.Column('cardtrader_id', sa.String(), nullable=True),
|
||||
sa.Column('csi_id', sa.String(), nullable=True),
|
||||
sa.Column('mcm_id', sa.String(), nullable=True),
|
||||
sa.Column('mcm_meta_id', sa.String(), nullable=True),
|
||||
sa.Column('miniaturemarket_id', sa.String(), nullable=True),
|
||||
sa.Column('mtg_arena_id', sa.String(), nullable=True),
|
||||
sa.Column('mtgjson_foil_version_id', sa.String(), nullable=True),
|
||||
sa.Column('mtgjson_non_foil_version_id', sa.String(), nullable=True),
|
||||
sa.Column('mtgjson_v4_id', sa.String(), nullable=True),
|
||||
sa.Column('mtgo_foil_id', sa.String(), nullable=True),
|
||||
sa.Column('mtgo_id', sa.String(), nullable=True),
|
||||
sa.Column('multiverse_id', sa.String(), nullable=True),
|
||||
sa.Column('scg_id', sa.String(), nullable=True),
|
||||
sa.Column('scryfall_id', sa.String(), nullable=True),
|
||||
sa.Column('scryfall_card_back_id', sa.String(), nullable=True),
|
||||
sa.Column('scryfall_oracle_id', sa.String(), nullable=True),
|
||||
sa.Column('scryfall_illustration_id', sa.String(), nullable=True),
|
||||
sa.Column('tcgplayer_product_id', sa.Integer(), nullable=True),
|
||||
sa.Column('tcgplayer_etched_product_id', sa.Integer(), nullable=True),
|
||||
sa.Column('tnt_id', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['mtgjson_uuid'], ['mtgjson_cards.mtgjson_uuid'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('mtgjson_uuid', name='uq_card_mtgjson_uuid')
|
||||
)
|
||||
op.create_index(op.f('ix_mtgjson_cards_id'), 'mtgjson_cards', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_mtgjson_cards_mtgjson_uuid'), 'mtgjson_cards', ['mtgjson_uuid'], unique=True)
|
||||
op.create_table('sealed_expected_values',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('tcgplayer_product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('expected_value', sa.Float(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_sealed_expected_value_product_id_deleted_at', 'sealed_expected_values', ['tcgplayer_product_id', 'deleted_at'], unique=True)
|
||||
op.create_index(op.f('ix_sealed_expected_values_id'), 'sealed_expected_values', ['id'], unique=False)
|
||||
op.create_table('tcgplayer_categories',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('category_id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('display_name', sa.String(), nullable=True),
|
||||
sa.Column('seo_category_name', sa.String(), nullable=True),
|
||||
sa.Column('category_description', sa.String(), nullable=True),
|
||||
sa.Column('category_page_title', sa.String(), nullable=True),
|
||||
sa.Column('sealed_label', sa.String(), nullable=True),
|
||||
sa.Column('non_sealed_label', sa.String(), nullable=True),
|
||||
sa.Column('condition_guide_url', sa.String(), nullable=True),
|
||||
sa.Column('is_scannable', sa.Boolean(), nullable=True),
|
||||
sa.Column('popularity', sa.Integer(), nullable=True),
|
||||
sa.Column('is_direct', sa.Boolean(), nullable=True),
|
||||
sa.Column('modified_on', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_categories_category_id'), 'tcgplayer_categories', ['category_id'], unique=True)
|
||||
op.create_index(op.f('ix_tcgplayer_categories_id'), 'tcgplayer_categories', ['id'], unique=False)
|
||||
op.create_table('tcgplayer_order_products',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_number', sa.String(), nullable=True),
|
||||
sa.Column('product_name', sa.String(), nullable=True),
|
||||
sa.Column('unit_price', sa.Float(), nullable=True),
|
||||
sa.Column('extended_price', sa.Float(), nullable=True),
|
||||
sa.Column('quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('url', sa.String(), nullable=True),
|
||||
sa.Column('product_id', sa.Integer(), nullable=True),
|
||||
sa.Column('sku_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_order_products_id'), 'tcgplayer_order_products', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_order_products_order_number'), 'tcgplayer_order_products', ['order_number'], unique=False)
|
||||
op.create_table('tcgplayer_order_refunds',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_number', sa.String(), nullable=True),
|
||||
sa.Column('refund_created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('type', sa.String(), nullable=True),
|
||||
sa.Column('amount', sa.Float(), nullable=True),
|
||||
sa.Column('description', sa.String(), nullable=True),
|
||||
sa.Column('origin', sa.String(), nullable=True),
|
||||
sa.Column('shipping_amount', sa.Float(), nullable=True),
|
||||
sa.Column('products', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_order_refunds_id'), 'tcgplayer_order_refunds', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_order_refunds_order_number'), 'tcgplayer_order_refunds', ['order_number'], unique=False)
|
||||
op.create_table('tcgplayer_order_transactions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_number', sa.String(), nullable=True),
|
||||
sa.Column('product_amount', sa.Float(), nullable=True),
|
||||
sa.Column('shipping_amount', sa.Float(), nullable=True),
|
||||
sa.Column('gross_amount', sa.Float(), nullable=True),
|
||||
sa.Column('fee_amount', sa.Float(), nullable=True),
|
||||
sa.Column('net_amount', sa.Float(), nullable=True),
|
||||
sa.Column('direct_fee_amount', sa.Float(), nullable=True),
|
||||
sa.Column('taxes', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_order_transactions_id'), 'tcgplayer_order_transactions', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_order_transactions_order_number'), 'tcgplayer_order_transactions', ['order_number'], unique=False)
|
||||
op.create_table('tcgplayer_orders',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_number', sa.String(), nullable=True),
|
||||
sa.Column('order_created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('status', sa.String(), nullable=True),
|
||||
sa.Column('channel', sa.String(), nullable=True),
|
||||
sa.Column('fulfillment', sa.String(), nullable=True),
|
||||
sa.Column('seller_name', sa.String(), nullable=True),
|
||||
sa.Column('buyer_name', sa.String(), nullable=True),
|
||||
sa.Column('payment_type', sa.String(), nullable=True),
|
||||
sa.Column('pickup_status', sa.String(), nullable=True),
|
||||
sa.Column('shipping_type', sa.String(), nullable=True),
|
||||
sa.Column('estimated_delivery_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('recipient_name', sa.String(), nullable=True),
|
||||
sa.Column('address_line_1', sa.String(), nullable=True),
|
||||
sa.Column('address_line_2', sa.String(), nullable=True),
|
||||
sa.Column('city', sa.String(), nullable=True),
|
||||
sa.Column('state', sa.String(), nullable=True),
|
||||
sa.Column('zip_code', sa.String(), nullable=True),
|
||||
sa.Column('country', sa.String(), nullable=True),
|
||||
sa.Column('tracking_numbers', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_orders_id'), 'tcgplayer_orders', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_orders_order_number'), 'tcgplayer_orders', ['order_number'], unique=False)
|
||||
op.create_table('tcgplayer_price_history',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('sub_type_name', sa.String(), nullable=True),
|
||||
sa.Column('date', sa.DateTime(), nullable=True),
|
||||
sa.Column('low_price', sa.Float(), nullable=True),
|
||||
sa.Column('mid_price', sa.Float(), nullable=True),
|
||||
sa.Column('high_price', sa.Float(), nullable=True),
|
||||
sa.Column('market_price', sa.Float(), nullable=True),
|
||||
sa.Column('direct_low_price', sa.Float(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_price_history_product_subtype_date', 'tcgplayer_price_history', ['product_id', 'sub_type_name', 'date'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_price_history_date'), 'tcgplayer_price_history', ['date'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_price_history_id'), 'tcgplayer_price_history', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_price_history_product_id'), 'tcgplayer_price_history', ['product_id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_price_history_sub_type_name'), 'tcgplayer_price_history', ['sub_type_name'], unique=False)
|
||||
op.create_table('vendors',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_vendors_id'), 'vendors', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_vendors_name'), 'vendors', ['name'], unique=False)
|
||||
op.create_table('manabox_import_staging',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('file_id', sa.Integer(), nullable=True),
|
||||
sa.Column('tcgplayer_product_id', sa.Integer(), nullable=True),
|
||||
sa.Column('tcgplayer_sku_id', sa.Integer(), nullable=True),
|
||||
sa.Column('quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['file_id'], ['files.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('tcgplayer_groups',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('group_id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('abbreviation', sa.String(), nullable=True),
|
||||
sa.Column('is_supplemental', sa.Boolean(), nullable=True),
|
||||
sa.Column('published_on', sa.DateTime(), nullable=True),
|
||||
sa.Column('modified_on', sa.DateTime(), nullable=True),
|
||||
sa.Column('category_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['category_id'], ['tcgplayer_categories.category_id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_groups_group_id'), 'tcgplayer_groups', ['group_id'], unique=True)
|
||||
op.create_index(op.f('ix_tcgplayer_groups_id'), 'tcgplayer_groups', ['id'], unique=False)
|
||||
op.create_table('transactions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=True),
|
||||
sa.Column('customer_id', sa.Integer(), nullable=True),
|
||||
sa.Column('marketplace_id', sa.Integer(), nullable=True),
|
||||
sa.Column('transaction_type', sa.String(), nullable=True),
|
||||
sa.Column('transaction_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('transaction_total_amount', sa.Float(), nullable=True),
|
||||
sa.Column('transaction_notes', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ),
|
||||
sa.ForeignKeyConstraint(['marketplace_id'], ['marketplaces.id'], ),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_transactions_id'), 'transactions', ['id'], unique=False)
|
||||
op.create_table('tcgplayer_products',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('tcgplayer_product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('normalized_sub_type_name', sa.String(), nullable=False),
|
||||
sa.Column('sub_type_name', sa.String(), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('clean_name', sa.String(), nullable=True),
|
||||
sa.Column('image_url', sa.String(), nullable=True),
|
||||
sa.Column('category_id', sa.Integer(), nullable=True),
|
||||
sa.Column('group_id', sa.Integer(), nullable=True),
|
||||
sa.Column('url', sa.String(), nullable=True),
|
||||
sa.Column('modified_on', sa.DateTime(), nullable=True),
|
||||
sa.Column('image_count', sa.Integer(), nullable=True),
|
||||
sa.Column('ext_rarity', sa.String(), nullable=True),
|
||||
sa.Column('ext_subtype', sa.String(), nullable=True),
|
||||
sa.Column('ext_oracle_text', sa.String(), nullable=True),
|
||||
sa.Column('ext_number', sa.String(), nullable=True),
|
||||
sa.Column('low_price', sa.Float(), nullable=True),
|
||||
sa.Column('mid_price', sa.Float(), nullable=True),
|
||||
sa.Column('high_price', sa.Float(), nullable=True),
|
||||
sa.Column('market_price', sa.Float(), nullable=True),
|
||||
sa.Column('direct_low_price', sa.Float(), nullable=True),
|
||||
sa.Column('ext_power', sa.String(), nullable=True),
|
||||
sa.Column('ext_toughness', sa.String(), nullable=True),
|
||||
sa.Column('ext_flavor_text', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['category_id'], ['tcgplayer_categories.category_id'], ),
|
||||
sa.ForeignKeyConstraint(['group_id'], ['tcgplayer_groups.group_id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('tcgplayer_product_id', 'normalized_sub_type_name', name='uq_product_subtype')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_products_id'), 'tcgplayer_products', ['id'], unique=False)
|
||||
op.create_table('mtgjson_skus',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('mtgjson_uuid', sa.String(), nullable=True),
|
||||
sa.Column('tcgplayer_sku_id', sa.Integer(), nullable=True),
|
||||
sa.Column('tcgplayer_product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('normalized_printing', sa.String(), nullable=False),
|
||||
sa.Column('condition', sa.String(), nullable=True),
|
||||
sa.Column('finish', sa.String(), nullable=True),
|
||||
sa.Column('language', sa.String(), nullable=True),
|
||||
sa.Column('printing', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['mtgjson_uuid'], ['mtgjson_cards.mtgjson_uuid'], ),
|
||||
sa.ForeignKeyConstraint(['tcgplayer_product_id', 'normalized_printing'], ['tcgplayer_products.tcgplayer_product_id', 'tcgplayer_products.normalized_sub_type_name'], name='fk_sku_to_product_composite'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('mtgjson_uuid', name='uq_sku_mtgjson_uuid')
|
||||
)
|
||||
op.create_index('idx_sku_product_printing', 'mtgjson_skus', ['tcgplayer_product_id', 'normalized_printing'], unique=False)
|
||||
op.create_index(op.f('ix_mtgjson_skus_id'), 'mtgjson_skus', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_mtgjson_skus_mtgjson_uuid'), 'mtgjson_skus', ['mtgjson_uuid'], unique=True)
|
||||
op.create_index(op.f('ix_mtgjson_skus_tcgplayer_sku_id'), 'mtgjson_skus', ['tcgplayer_sku_id'], unique=True)
|
||||
op.create_table('physical_items',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('item_type', sa.String(), nullable=True),
|
||||
sa.Column('tcgplayer_product_id', sa.Integer(), nullable=True),
|
||||
sa.Column('tcgplayer_sku_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.CheckConstraint('(tcgplayer_sku_id IS NOT NULL OR tcgplayer_product_id IS NOT NULL)', name='ck_physical_items_sku_or_product_not_null'),
|
||||
sa.ForeignKeyConstraint(['tcgplayer_sku_id'], ['mtgjson_skus.tcgplayer_sku_id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('tcgplayer_inventory',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('tcgplayer_sku_id', sa.Integer(), nullable=True),
|
||||
sa.Column('product_line', sa.String(), nullable=True),
|
||||
sa.Column('set_name', sa.String(), nullable=True),
|
||||
sa.Column('product_name', sa.String(), nullable=True),
|
||||
sa.Column('title', sa.String(), nullable=True),
|
||||
sa.Column('number', sa.String(), nullable=True),
|
||||
sa.Column('rarity', sa.String(), nullable=True),
|
||||
sa.Column('condition', sa.String(), nullable=True),
|
||||
sa.Column('tcg_market_price', sa.Float(), nullable=True),
|
||||
sa.Column('tcg_direct_low', sa.Float(), nullable=True),
|
||||
sa.Column('tcg_low_price_with_shipping', sa.Float(), nullable=True),
|
||||
sa.Column('tcg_low_price', sa.Float(), nullable=True),
|
||||
sa.Column('total_quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('add_to_quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('tcg_marketplace_price', sa.Float(), nullable=True),
|
||||
sa.Column('photo_url', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['tcgplayer_sku_id'], ['mtgjson_skus.tcgplayer_sku_id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_inventory_id'), 'tcgplayer_inventory', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_inventory_tcgplayer_sku_id'), 'tcgplayer_inventory', ['tcgplayer_sku_id'], unique=True)
|
||||
op.create_table('boxes',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('expected_value', sa.Float(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['id'], ['physical_items.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('cards',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['id'], ['physical_items.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('cases',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('expected_value', sa.Float(), nullable=True),
|
||||
sa.Column('num_boxes', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['id'], ['physical_items.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('inventory_items',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('physical_item_id', sa.Integer(), nullable=True),
|
||||
sa.Column('cost_basis', sa.Float(), nullable=True),
|
||||
sa.Column('parent_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['parent_id'], ['inventory_items.id'], ),
|
||||
sa.ForeignKeyConstraint(['physical_item_id'], ['physical_items.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('physical_item_id')
|
||||
)
|
||||
op.create_index(op.f('ix_inventory_items_id'), 'inventory_items', ['id'], unique=False)
|
||||
op.create_table('open_events',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('source_item_id', sa.Integer(), nullable=True),
|
||||
sa.Column('open_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['source_item_id'], ['physical_items.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('source_item_id', name='uq_openevent_one_per_source')
|
||||
)
|
||||
op.create_index(op.f('ix_open_events_id'), 'open_events', ['id'], unique=False)
|
||||
op.create_table('marketplace_listings',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('inventory_item_id', sa.Integer(), nullable=False),
|
||||
sa.Column('marketplace_id', sa.Integer(), nullable=False),
|
||||
sa.Column('listing_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('delisting_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('listed_price', sa.Float(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['inventory_item_id'], ['inventory_items.id'], ),
|
||||
sa.ForeignKeyConstraint(['marketplace_id'], ['marketplaces.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_marketplace_listings_id'), 'marketplace_listings', ['id'], unique=False)
|
||||
op.create_table('open_event_resulting_items',
|
||||
sa.Column('event_id', sa.Integer(), nullable=False),
|
||||
sa.Column('item_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['event_id'], ['open_events.id'], ),
|
||||
sa.ForeignKeyConstraint(['item_id'], ['physical_items.id'], ),
|
||||
sa.PrimaryKeyConstraint('event_id', 'item_id')
|
||||
)
|
||||
op.create_table('transaction_items',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('transaction_id', sa.Integer(), nullable=True),
|
||||
sa.Column('inventory_item_id', sa.Integer(), nullable=True),
|
||||
sa.Column('unit_price', sa.Float(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['inventory_item_id'], ['inventory_items.id'], ),
|
||||
sa.ForeignKeyConstraint(['transaction_id'], ['transactions.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_transaction_items_id'), 'transaction_items', ['id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_transaction_items_id'), table_name='transaction_items')
|
||||
op.drop_table('transaction_items')
|
||||
op.drop_table('open_event_resulting_items')
|
||||
op.drop_index(op.f('ix_marketplace_listings_id'), table_name='marketplace_listings')
|
||||
op.drop_table('marketplace_listings')
|
||||
op.drop_index(op.f('ix_open_events_id'), table_name='open_events')
|
||||
op.drop_table('open_events')
|
||||
op.drop_index(op.f('ix_inventory_items_id'), table_name='inventory_items')
|
||||
op.drop_table('inventory_items')
|
||||
op.drop_table('cases')
|
||||
op.drop_table('cards')
|
||||
op.drop_table('boxes')
|
||||
op.drop_index(op.f('ix_tcgplayer_inventory_tcgplayer_sku_id'), table_name='tcgplayer_inventory')
|
||||
op.drop_index(op.f('ix_tcgplayer_inventory_id'), table_name='tcgplayer_inventory')
|
||||
op.drop_table('tcgplayer_inventory')
|
||||
op.drop_table('physical_items')
|
||||
op.drop_index(op.f('ix_mtgjson_skus_tcgplayer_sku_id'), table_name='mtgjson_skus')
|
||||
op.drop_index(op.f('ix_mtgjson_skus_mtgjson_uuid'), table_name='mtgjson_skus')
|
||||
op.drop_index(op.f('ix_mtgjson_skus_id'), table_name='mtgjson_skus')
|
||||
op.drop_index('idx_sku_product_printing', table_name='mtgjson_skus')
|
||||
op.drop_table('mtgjson_skus')
|
||||
op.drop_index(op.f('ix_tcgplayer_products_id'), table_name='tcgplayer_products')
|
||||
op.drop_table('tcgplayer_products')
|
||||
op.drop_index(op.f('ix_transactions_id'), table_name='transactions')
|
||||
op.drop_table('transactions')
|
||||
op.drop_index(op.f('ix_tcgplayer_groups_id'), table_name='tcgplayer_groups')
|
||||
op.drop_index(op.f('ix_tcgplayer_groups_group_id'), table_name='tcgplayer_groups')
|
||||
op.drop_table('tcgplayer_groups')
|
||||
op.drop_table('manabox_import_staging')
|
||||
op.drop_index(op.f('ix_vendors_name'), table_name='vendors')
|
||||
op.drop_index(op.f('ix_vendors_id'), table_name='vendors')
|
||||
op.drop_table('vendors')
|
||||
op.drop_index(op.f('ix_tcgplayer_price_history_sub_type_name'), table_name='tcgplayer_price_history')
|
||||
op.drop_index(op.f('ix_tcgplayer_price_history_product_id'), table_name='tcgplayer_price_history')
|
||||
op.drop_index(op.f('ix_tcgplayer_price_history_id'), table_name='tcgplayer_price_history')
|
||||
op.drop_index(op.f('ix_tcgplayer_price_history_date'), table_name='tcgplayer_price_history')
|
||||
op.drop_index('idx_price_history_product_subtype_date', table_name='tcgplayer_price_history')
|
||||
op.drop_table('tcgplayer_price_history')
|
||||
op.drop_index(op.f('ix_tcgplayer_orders_order_number'), table_name='tcgplayer_orders')
|
||||
op.drop_index(op.f('ix_tcgplayer_orders_id'), table_name='tcgplayer_orders')
|
||||
op.drop_table('tcgplayer_orders')
|
||||
op.drop_index(op.f('ix_tcgplayer_order_transactions_order_number'), table_name='tcgplayer_order_transactions')
|
||||
op.drop_index(op.f('ix_tcgplayer_order_transactions_id'), table_name='tcgplayer_order_transactions')
|
||||
op.drop_table('tcgplayer_order_transactions')
|
||||
op.drop_index(op.f('ix_tcgplayer_order_refunds_order_number'), table_name='tcgplayer_order_refunds')
|
||||
op.drop_index(op.f('ix_tcgplayer_order_refunds_id'), table_name='tcgplayer_order_refunds')
|
||||
op.drop_table('tcgplayer_order_refunds')
|
||||
op.drop_index(op.f('ix_tcgplayer_order_products_order_number'), table_name='tcgplayer_order_products')
|
||||
op.drop_index(op.f('ix_tcgplayer_order_products_id'), table_name='tcgplayer_order_products')
|
||||
op.drop_table('tcgplayer_order_products')
|
||||
op.drop_index(op.f('ix_tcgplayer_categories_id'), table_name='tcgplayer_categories')
|
||||
op.drop_index(op.f('ix_tcgplayer_categories_category_id'), table_name='tcgplayer_categories')
|
||||
op.drop_table('tcgplayer_categories')
|
||||
op.drop_index(op.f('ix_sealed_expected_values_id'), table_name='sealed_expected_values')
|
||||
op.drop_index('idx_sealed_expected_value_product_id_deleted_at', table_name='sealed_expected_values')
|
||||
op.drop_table('sealed_expected_values')
|
||||
op.drop_index(op.f('ix_mtgjson_cards_mtgjson_uuid'), table_name='mtgjson_cards')
|
||||
op.drop_index(op.f('ix_mtgjson_cards_id'), table_name='mtgjson_cards')
|
||||
op.drop_table('mtgjson_cards')
|
||||
op.drop_index(op.f('ix_most_recent_tcgplayer_price_sub_type_name'), table_name='most_recent_tcgplayer_price')
|
||||
op.drop_index(op.f('ix_most_recent_tcgplayer_price_product_id'), table_name='most_recent_tcgplayer_price')
|
||||
op.drop_index('idx_most_recent_price_product_subtype', table_name='most_recent_tcgplayer_price')
|
||||
op.drop_table('most_recent_tcgplayer_price')
|
||||
op.drop_index(op.f('ix_marketplaces_name'), table_name='marketplaces')
|
||||
op.drop_index(op.f('ix_marketplaces_id'), table_name='marketplaces')
|
||||
op.drop_table('marketplaces')
|
||||
op.drop_index(op.f('ix_files_id'), table_name='files')
|
||||
op.drop_table('files')
|
||||
op.drop_index(op.f('ix_customers_name'), table_name='customers')
|
||||
op.drop_index(op.f('ix_customers_id'), table_name='customers')
|
||||
op.drop_table('customers')
|
||||
op.drop_index(op.f('ix_critical_error_logs_id'), table_name='critical_error_logs')
|
||||
op.drop_table('critical_error_logs')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,34 @@
|
||||
"""there is literally no point to ever using foreign keys
|
||||
|
||||
Revision ID: d13600612a8f
|
||||
Revises: 62eee00bae8e
|
||||
Create Date: 2025-04-28 11:37:11.023788
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'd13600612a8f'
|
||||
down_revision: Union[str, None] = '62eee00bae8e'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint('tcgplayer_inventory_tcgplayer_sku_id_fkey', 'tcgplayer_inventory', type_='foreignkey')
|
||||
op.drop_constraint('unmanaged_tcgplayer_inventory_tcgplayer_sku_id_fkey', 'unmanaged_tcgplayer_inventory', type_='foreignkey')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_foreign_key('unmanaged_tcgplayer_inventory_tcgplayer_sku_id_fkey', 'unmanaged_tcgplayer_inventory', 'mtgjson_skus', ['tcgplayer_sku_id'], ['tcgplayer_sku_id'])
|
||||
op.create_foreign_key('tcgplayer_inventory_tcgplayer_sku_id_fkey', 'tcgplayer_inventory', 'mtgjson_skus', ['tcgplayer_sku_id'], ['tcgplayer_sku_id'])
|
||||
# ### end Alembic commands ###
|
@ -1,339 +0,0 @@
|
||||
"""changing db bigly
|
||||
|
||||
Revision ID: d4d3f43ce86a
|
||||
Revises: cc7dd65bcdd9
|
||||
Create Date: 2025-04-19 13:46:27.330261
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'd4d3f43ce86a'
|
||||
down_revision: Union[str, None] = 'cc7dd65bcdd9'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('customers',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_customers_id'), 'customers', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_customers_name'), 'customers', ['name'], unique=True)
|
||||
op.create_table('products',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('tcgplayer_id', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('tcgplayer_inventory',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('tcgplayer_id', sa.String(), nullable=True),
|
||||
sa.Column('product_line', sa.String(), nullable=True),
|
||||
sa.Column('set_name', sa.String(), nullable=True),
|
||||
sa.Column('product_name', sa.String(), nullable=True),
|
||||
sa.Column('title', sa.String(), nullable=True),
|
||||
sa.Column('number', sa.String(), nullable=True),
|
||||
sa.Column('rarity', sa.String(), nullable=True),
|
||||
sa.Column('condition', sa.String(), nullable=True),
|
||||
sa.Column('tcg_market_price', sa.Float(), nullable=True),
|
||||
sa.Column('tcg_direct_low', sa.Float(), nullable=True),
|
||||
sa.Column('tcg_low_price_with_shipping', sa.Float(), nullable=True),
|
||||
sa.Column('tcg_low_price', sa.Float(), nullable=True),
|
||||
sa.Column('total_quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('add_to_quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('tcg_marketplace_price', sa.Float(), nullable=True),
|
||||
sa.Column('photo_url', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tcgplayer_inventory_id'), 'tcgplayer_inventory', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tcgplayer_inventory_tcgplayer_id'), 'tcgplayer_inventory', ['tcgplayer_id'], unique=True)
|
||||
op.create_table('vendors',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_vendors_id'), 'vendors', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_vendors_name'), 'vendors', ['name'], unique=True)
|
||||
op.create_table('physical_items',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('item_type', sa.String(), nullable=True),
|
||||
sa.Column('product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('transactions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=True),
|
||||
sa.Column('customer_id', sa.Integer(), nullable=True),
|
||||
sa.Column('transaction_type', sa.String(), nullable=True),
|
||||
sa.Column('transaction_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('transaction_total_amount', sa.Float(), nullable=True),
|
||||
sa.Column('transaction_notes', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_transactions_id'), 'transactions', ['id'], unique=False)
|
||||
op.create_table('inventory_items',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('physical_item_id', sa.Integer(), nullable=True),
|
||||
sa.Column('cost_basis', sa.Float(), nullable=True),
|
||||
sa.Column('parent_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['parent_id'], ['inventory_items.id'], ),
|
||||
sa.ForeignKeyConstraint(['physical_item_id'], ['physical_items.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('physical_item_id')
|
||||
)
|
||||
op.create_index(op.f('ix_inventory_items_id'), 'inventory_items', ['id'], unique=False)
|
||||
op.create_table('sealed_cases',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['id'], ['physical_items.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('transaction_items',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('transaction_id', sa.Integer(), nullable=True),
|
||||
sa.Column('physical_item_id', sa.Integer(), nullable=True),
|
||||
sa.Column('unit_price', sa.Float(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['physical_item_id'], ['physical_items.id'], ),
|
||||
sa.ForeignKeyConstraint(['transaction_id'], ['transactions.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_transaction_items_id'), 'transaction_items', ['id'], unique=False)
|
||||
op.create_table('sealed_boxes',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('case_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['case_id'], ['sealed_cases.id'], ),
|
||||
sa.ForeignKeyConstraint(['id'], ['physical_items.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('open_events',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('sealed_case_id', sa.Integer(), nullable=True),
|
||||
sa.Column('sealed_box_id', sa.Integer(), nullable=True),
|
||||
sa.Column('open_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['sealed_box_id'], ['sealed_boxes.id'], ),
|
||||
sa.ForeignKeyConstraint(['sealed_case_id'], ['sealed_cases.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_open_events_id'), 'open_events', ['id'], unique=False)
|
||||
op.create_table('open_cards',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('open_event_id', sa.Integer(), nullable=True),
|
||||
sa.Column('box_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['box_id'], ['open_boxes.id'], ),
|
||||
sa.ForeignKeyConstraint(['id'], ['physical_items.id'], ),
|
||||
sa.ForeignKeyConstraint(['open_event_id'], ['open_events.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('cost_basis',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('transaction_item_id', sa.Integer(), nullable=True),
|
||||
sa.Column('sealed_case_id', sa.Integer(), nullable=True),
|
||||
sa.Column('sealed_box_id', sa.Integer(), nullable=True),
|
||||
sa.Column('open_box_id', sa.Integer(), nullable=True),
|
||||
sa.Column('open_card_id', sa.Integer(), nullable=True),
|
||||
sa.Column('quantity', sa.Integer(), nullable=False),
|
||||
sa.Column('unit_cost', sa.Float(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['open_box_id'], ['open_boxes.id'], ),
|
||||
sa.ForeignKeyConstraint(['open_card_id'], ['open_cards.id'], ),
|
||||
sa.ForeignKeyConstraint(['sealed_box_id'], ['sealed_boxes.id'], ),
|
||||
sa.ForeignKeyConstraint(['sealed_case_id'], ['sealed_cases.id'], ),
|
||||
sa.ForeignKeyConstraint(['transaction_item_id'], ['transaction_items.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_cost_basis_id'), 'cost_basis', ['id'], unique=False)
|
||||
|
||||
# Drop tables in correct dependency order
|
||||
# First drop foreign key constraints
|
||||
op.execute('DROP TABLE IF EXISTS open_cards CASCADE')
|
||||
op.execute('DROP TABLE IF EXISTS cost_basis CASCADE')
|
||||
op.execute('DROP TABLE IF EXISTS open_boxes CASCADE')
|
||||
op.execute('DROP TABLE IF EXISTS boxes CASCADE')
|
||||
op.execute('DROP TABLE IF EXISTS games CASCADE')
|
||||
|
||||
op.drop_index('ix_inventory_id', table_name='inventory')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# Create tables in correct dependency order
|
||||
op.create_table('games',
|
||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||
sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('image_url', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True),
|
||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
||||
sa.PrimaryKeyConstraint('id', name='games_pkey')
|
||||
)
|
||||
op.create_index('ix_games_id', 'games', ['id'], unique=False)
|
||||
|
||||
op.create_table('boxes',
|
||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||
sa.Column('product_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('type', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('set_code', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('sku', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('game_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('expected_number_of_cards', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('image_url', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True),
|
||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
||||
sa.ForeignKeyConstraint(['game_id'], ['games.id'], name='boxes_game_id_fkey'),
|
||||
sa.PrimaryKeyConstraint('id', name='boxes_pkey')
|
||||
)
|
||||
op.create_index('ix_boxes_id', 'boxes', ['id'], unique=False)
|
||||
|
||||
op.create_table('open_boxes',
|
||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||
sa.Column('box_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('date_opened', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
||||
sa.Column('number_of_cards', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True),
|
||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
||||
sa.ForeignKeyConstraint(['box_id'], ['boxes.id'], name='open_boxes_box_id_fkey'),
|
||||
sa.PrimaryKeyConstraint('id', name='open_boxes_pkey')
|
||||
)
|
||||
op.create_index('ix_open_boxes_id', 'open_boxes', ['id'], unique=False)
|
||||
|
||||
op.create_table('open_cards',
|
||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||
sa.Column('box_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True),
|
||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
||||
sa.ForeignKeyConstraint(['box_id'], ['open_boxes.id'], name='open_cards_box_id_fkey'),
|
||||
sa.PrimaryKeyConstraint('id', name='open_cards_pkey')
|
||||
)
|
||||
op.create_index('ix_open_cards_id', 'open_cards', ['id'], unique=False)
|
||||
|
||||
op.create_table('cost_basis',
|
||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||
sa.Column('open_box_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True),
|
||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
||||
sa.ForeignKeyConstraint(['open_box_id'], ['open_boxes.id'], name='cost_basis_open_box_id_fkey'),
|
||||
sa.PrimaryKeyConstraint('id', name='cost_basis_pkey')
|
||||
)
|
||||
op.create_index('ix_cost_basis_id', 'cost_basis', ['id'], unique=False)
|
||||
|
||||
op.create_table('cards',
|
||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||
sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('rarity', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('set_name', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('quantity', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('tcgplayer_sku', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('product_line', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('product_name', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('number', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('condition', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('tcg_market_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('tcg_direct_low', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('tcg_low_price_with_shipping', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('tcg_low_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('total_quantity', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('add_to_quantity', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('tcg_marketplace_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('photo_url', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True),
|
||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
||||
sa.PrimaryKeyConstraint('id', name='cards_pkey')
|
||||
)
|
||||
op.create_index('ix_cards_tcgplayer_sku', 'cards', ['tcgplayer_sku'], unique=True)
|
||||
op.create_index('ix_cards_set_name', 'cards', ['set_name'], unique=False)
|
||||
op.create_index('ix_cards_name', 'cards', ['name'], unique=False)
|
||||
op.create_index('ix_cards_id', 'cards', ['id'], unique=False)
|
||||
op.create_table('inventory',
|
||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||
sa.Column('tcgplayer_id', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('product_line', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('set_name', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('product_name', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('number', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('rarity', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('condition', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('tcg_market_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('tcg_direct_low', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('tcg_low_price_with_shipping', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('tcg_low_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('total_quantity', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('add_to_quantity', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('tcg_marketplace_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
||||
sa.Column('photo_url', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True),
|
||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
||||
sa.PrimaryKeyConstraint('id', name='inventory_pkey')
|
||||
)
|
||||
op.create_index('ix_inventory_tcgplayer_id', 'inventory', ['tcgplayer_id'], unique=True)
|
||||
op.create_index('ix_inventory_id', 'inventory', ['id'], unique=False)
|
||||
op.drop_index(op.f('ix_cost_basis_id'), table_name='cost_basis')
|
||||
op.drop_table('cost_basis')
|
||||
op.drop_table('open_cards')
|
||||
op.drop_index(op.f('ix_open_events_id'), table_name='open_events')
|
||||
op.drop_table('open_events')
|
||||
op.drop_table('sealed_boxes')
|
||||
op.drop_index(op.f('ix_transaction_items_id'), table_name='transaction_items')
|
||||
op.drop_table('transaction_items')
|
||||
op.drop_table('sealed_cases')
|
||||
op.drop_index(op.f('ix_inventory_items_id'), table_name='inventory_items')
|
||||
op.drop_table('inventory_items')
|
||||
op.drop_index(op.f('ix_transactions_id'), table_name='transactions')
|
||||
op.drop_table('transactions')
|
||||
op.drop_table('physical_items')
|
||||
op.drop_index(op.f('ix_vendors_name'), table_name='vendors')
|
||||
op.drop_index(op.f('ix_vendors_id'), table_name='vendors')
|
||||
op.drop_table('vendors')
|
||||
op.drop_index(op.f('ix_tcgplayer_inventory_tcgplayer_id'), table_name='tcgplayer_inventory')
|
||||
op.drop_index(op.f('ix_tcgplayer_inventory_id'), table_name='tcgplayer_inventory')
|
||||
op.drop_table('tcgplayer_inventory')
|
||||
op.drop_table('products')
|
||||
op.drop_index(op.f('ix_customers_name'), table_name='customers')
|
||||
op.drop_index(op.f('ix_customers_id'), table_name='customers')
|
||||
op.drop_table('customers')
|
||||
# ### end Alembic commands ###
|
@ -1,32 +0,0 @@
|
||||
"""tcg prices again 3
|
||||
|
||||
Revision ID: e34bfa37db00
|
||||
Revises: 493b2cb724d0
|
||||
Create Date: 2025-04-17 23:05:40.805511
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'e34bfa37db00'
|
||||
down_revision: Union[str, None] = '493b2cb724d0'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
28
app.log
28
app.log
@ -1,28 +0,0 @@
|
||||
2025-04-19 13:56:40,410 - INFO - app.main - Application starting up...
|
||||
2025-04-19 13:56:40,492 - INFO - app.main - Database initialized successfully
|
||||
2025-04-19 13:56:40,492 - INFO - app.services.service_manager - Service OrderManagementService registered
|
||||
2025-04-19 13:56:40,492 - INFO - app.services.service_manager - Service TCGPlayerInventoryService registered
|
||||
2025-04-19 13:56:40,492 - INFO - app.services.service_manager - Service LabelPrinterService registered
|
||||
2025-04-19 13:56:40,492 - INFO - app.services.service_manager - Service RegularPrinterService registered
|
||||
2025-04-19 13:56:40,495 - INFO - app.services.service_manager - Service AddressLabelService registered
|
||||
2025-04-19 13:56:40,497 - INFO - app.services.service_manager - Service PullSheetService registered
|
||||
2025-04-19 13:56:40,497 - INFO - app.services.service_manager - Service SetLabelService registered
|
||||
2025-04-19 13:56:40,497 - INFO - app.services.service_manager - Service DataInitializationService registered
|
||||
2025-04-19 13:56:40,498 - INFO - app.services.service_manager - Service SchedulerService registered
|
||||
2025-04-19 13:56:40,498 - INFO - app.services.service_manager - Service FileService registered
|
||||
2025-04-19 13:56:40,498 - INFO - app.services.service_manager - Service TCGCSVService registered
|
||||
2025-04-19 13:56:40,498 - INFO - app.services.service_manager - Service MTGJSONService registered
|
||||
2025-04-19 13:56:40,499 - INFO - app.services.service_manager - All services initialized successfully
|
||||
2025-04-19 13:56:40,499 - INFO - app.services.data_initialization - Starting data initialization process
|
||||
2025-04-19 13:56:40,499 - INFO - app.services.data_initialization - Data initialization completed
|
||||
2025-04-19 13:56:40,499 - INFO - app.main - Data initialization results: {}
|
||||
2025-04-19 13:56:40,499 - INFO - apscheduler.scheduler - Adding job tentatively -- it will be properly scheduled when the scheduler starts
|
||||
2025-04-19 13:56:40,499 - INFO - app.services.scheduler.base_scheduler - Scheduled task update_open_orders_hourly to run every 3600 seconds
|
||||
2025-04-19 13:56:40,499 - INFO - apscheduler.scheduler - Adding job tentatively -- it will be properly scheduled when the scheduler starts
|
||||
2025-04-19 13:56:40,499 - INFO - app.services.scheduler.base_scheduler - Scheduled task update_all_orders_daily to run every 86400 seconds
|
||||
2025-04-19 13:56:40,499 - INFO - apscheduler.scheduler - Added job "SchedulerService.start_scheduled_tasks.<locals>.<lambda>" to job store "default"
|
||||
2025-04-19 13:56:40,500 - INFO - apscheduler.scheduler - Added job "SchedulerService.start_scheduled_tasks.<locals>.<lambda>" to job store "default"
|
||||
2025-04-19 13:56:40,500 - INFO - apscheduler.scheduler - Scheduler started
|
||||
2025-04-19 13:56:40,500 - INFO - app.services.scheduler.base_scheduler - Scheduler started
|
||||
2025-04-19 13:56:40,500 - INFO - app.services.scheduler.scheduler_service - All scheduled tasks started
|
||||
2025-04-19 13:56:40,500 - INFO - app.main - Scheduler started successfully
|
87
app/contexts/inventory_item.py
Normal file
87
app/contexts/inventory_item.py
Normal file
@ -0,0 +1,87 @@
|
||||
from app.models.inventory_management import InventoryItem
|
||||
from app.contexts.inventory_product import InventoryProductContext
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
from app.models.tcgplayer_products import TCGPlayerProduct
|
||||
|
||||
class InventoryItemContext:
|
||||
def __init__(self, item: InventoryItem, db: Session):
|
||||
self.item = item
|
||||
self.physical_item = item.physical_item
|
||||
self.marketplace_listing = item.marketplace_listing
|
||||
self.parent = item.parent
|
||||
self.children = item.children
|
||||
self.product = item.products
|
||||
self.db = db
|
||||
|
||||
@property
|
||||
def cost_basis(self) -> float:
|
||||
return self.item.cost_basis
|
||||
|
||||
@property
|
||||
def product_id(self) -> int:
|
||||
return self.physical_item.product_id
|
||||
|
||||
@property
|
||||
def product_name(self) -> str:
|
||||
return self.product.name if self.product else None
|
||||
|
||||
@property
|
||||
def item_type(self) -> str:
|
||||
return self.physical_item.item_type
|
||||
|
||||
@property
|
||||
def market_price(self) -> float:
|
||||
if not self.product or not self.product.most_recent_tcgplayer_price:
|
||||
return 0.0
|
||||
return self.product.most_recent_tcgplayer_price.market_price
|
||||
|
||||
@property
|
||||
def tcg_low_price(self) -> float:
|
||||
if not self.product or not self.product.most_recent_tcgplayer_price:
|
||||
return 0.0
|
||||
return self.product.most_recent_tcgplayer_price.low_price
|
||||
|
||||
@property
|
||||
def listed_price(self) -> float:
|
||||
if not self.marketplace_listing:
|
||||
return 0.0
|
||||
return self.marketplace_listing[0].listed_price if self.marketplace_listing else 0.0
|
||||
|
||||
def top_level_parent(self) -> "InventoryItemContext":
|
||||
if self.parent:
|
||||
return InventoryItemContext(self.parent, self.db)
|
||||
return self
|
||||
|
||||
def box_expected_value(self) -> float:
|
||||
top_level_parent = self.top_level_parent()
|
||||
if 'case' in top_level_parent.item_type:
|
||||
return top_level_parent.physical_item.open_event.sealed_case.expected_value
|
||||
elif 'box' in top_level_parent.item_type:
|
||||
return top_level_parent.physical_item.open_event.sealed_box.expected_value
|
||||
else:
|
||||
raise ValueError("Unknown top level parent item type")
|
||||
|
||||
def box_acquisition_cost(self) -> float:
|
||||
if self.physical_item.transaction_items:
|
||||
return self.physical_item.transaction_items[0].unit_price
|
||||
elif self.parent:
|
||||
return InventoryItemContext(self.parent, self.db).box_acquisition_cost()
|
||||
else:
|
||||
raise ValueError("Cannot find transaction unit price for this item")
|
||||
|
||||
def age_on_marketplace(self) -> int:
|
||||
if not self.marketplace_listing:
|
||||
return 0
|
||||
return (datetime.now() - self.marketplace_listing[0].listing_date).days
|
||||
|
||||
|
||||
class InventoryItemContextFactory:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get_context(self, item: InventoryItem) -> InventoryItemContext:
|
||||
return InventoryItemContext(item, self.db)
|
||||
|
||||
def get_context_for_product(self, product: TCGPlayerProduct) -> InventoryProductContext:
|
||||
return InventoryProductContext(product, self.db)
|
27
app/contexts/inventory_product.py
Normal file
27
app/contexts/inventory_product.py
Normal file
@ -0,0 +1,27 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.tcgplayer_products import TCGPlayerProduct
|
||||
class InventoryProductContext:
|
||||
def __init__(self, product: TCGPlayerProduct, db: Session):
|
||||
self.product = product
|
||||
self.prices = product.most_recent_tcgplayer_price
|
||||
self.db = db
|
||||
|
||||
@property
|
||||
def market_price(self) -> float:
|
||||
return self.product.most_recent_tcgplayer_price.market_price
|
||||
|
||||
@property
|
||||
def low_price(self) -> float:
|
||||
return self.product.most_recent_tcgplayer_price.low_price
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.product.name
|
||||
|
||||
@property
|
||||
def image_url(self) -> str:
|
||||
return self.product.image_url
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
return self.product.id
|
94
app/data/test_data/dragon.csv
Normal file
94
app/data/test_data/dragon.csv
Normal file
@ -0,0 +1,94 @@
|
||||
Name,Set code,Set name,Collector number,Foil,Rarity,Quantity,ManaBox ID,Scryfall ID,Purchase price,Misprint,Altered,Condition,Language,Purchase price currency
|
||||
Undergrowth Leopard,TDM,Tarkir: Dragonstorm,165,foil,common,1,104307,67ab8f9a-b17c-452f-b4ef-a3f91909e3de,0.08,false,false,near_mint,en,USD
|
||||
Gurmag Nightwatch,TDM,Tarkir: Dragonstorm,190,foil,common,1,104369,de731430-6bbf-4782-953e-b69c46353959,0.03,false,false,near_mint,en,USD
|
||||
Mystic Monastery,TDM,Tarkir: Dragonstorm,262,foil,uncommon,1,104945,c7b8a01c-c400-47c7-8270-78902efe850e,0.21,false,false,near_mint,en,USD
|
||||
Stormshriek Feral // Flush Out,TDM,Tarkir: Dragonstorm,124,foil,common,1,104447,0ec92c44-7cf0-48a5-a3ca-bc633496d887,0.1,false,false,near_mint,en,USD
|
||||
Reigning Victor,TDM,Tarkir: Dragonstorm,216,foil,common,1,104334,a394112a-032b-4047-887a-6522cf7b83d5,0.02,false,false,near_mint,en,USD
|
||||
Dragonbroods' Relic,TDM,Tarkir: Dragonstorm,140,foil,uncommon,1,104569,3d634087-77ba-4543-aa7a-8a3774d69cd7,0.13,false,false,near_mint,en,USD
|
||||
Sagu Wildling // Roost Seek,TDM,Tarkir: Dragonstorm,306,foil,common,1,104903,b72ee8f9-5e79-4f77-ae7e-e4c274f78187,0.13,false,false,near_mint,en,USD
|
||||
Sibsig Appraiser,TDM,Tarkir: Dragonstorm,56,foil,common,1,105135,670c5b96-bac6-449b-a2bd-cb43750d3911,0.05,false,false,near_mint,en,USD
|
||||
Sage of the Fang,TDM,Tarkir: Dragonstorm,155,foil,uncommon,1,105123,1ebf4a9d-d90c-4017-9f00-fca89899f301,0.09,false,false,near_mint,en,USD
|
||||
Snowmelt Stag,TDM,Tarkir: Dragonstorm,57,foil,common,1,104869,a6b3b131-704a-4586-84f8-db465cd4a277,0.04,false,false,near_mint,en,USD
|
||||
Tranquil Cove,TDM,Tarkir: Dragonstorm,270,foil,common,1,104249,1c4efa6c-4f29-41cd-a728-bf0e479ace05,0.07,false,false,near_mint,en,USD
|
||||
Rally the Monastery,TDM,Tarkir: Dragonstorm,19,foil,uncommon,1,104136,b56e0037-8143-4c13-83e1-0c3f44e685ea,0.22,false,false,near_mint,en,USD
|
||||
Dragon's Prey,TDM,Tarkir: Dragonstorm,79,foil,common,1,104754,7a6004ff-4180-4332-8b51-960f8c7521d9,0.03,false,false,near_mint,en,USD
|
||||
Ambling Stormshell,TDM,Tarkir: Dragonstorm,37,foil,rare,1,104942,c74d4a57-0f66-4965-9ed7-f88a08aa1d15,0.45,false,false,near_mint,en,USD
|
||||
Mountain,TDM,Tarkir: Dragonstorm,275,foil,common,1,104397,fe0865ba-47c0-40bc-b0c6-e1ea5ae08a98,1.83,false,false,near_mint,en,USD
|
||||
Mardu Devotee,TDM,Tarkir: Dragonstorm,16,foil,common,1,104366,da45e9b0-a4f6-413b-9e62-666c511eb5b0,0.09,false,false,near_mint,en,USD
|
||||
Swiftwater Cliffs,TDM,Tarkir: Dragonstorm,268,foil,common,1,104361,ca53fb19-b8ca-485b-af1a-5117ae54bfe3,0.11,false,false,near_mint,en,USD
|
||||
Adorned Crocodile,TDM,Tarkir: Dragonstorm,69,foil,common,1,105159,bb13a34b-6ac8-47cb-9e91-47106a585fc1,0.05,false,false,near_mint,en,USD
|
||||
Dusyut Earthcarver,TDM,Tarkir: Dragonstorm,141,foil,common,1,104352,b98ecc96-f557-479a-8685-2b5487d5b407,0.02,false,false,near_mint,en,USD
|
||||
Knockout Maneuver,TDM,Tarkir: Dragonstorm,147,foil,uncommon,1,105149,9d218831-2a41-46a3-8e9d-93462cae5cab,0.07,false,false,near_mint,en,USD
|
||||
Roiling Dragonstorm,TDM,Tarkir: Dragonstorm,55,foil,uncommon,1,104280,455f4c96-684b-4b14-bd21-6799da2e1fa7,0.22,false,false,near_mint,en,USD
|
||||
Dragonclaw Strike,TDM,Tarkir: Dragonstorm,180,foil,uncommon,1,105161,bc7692ef-7091-4365-85a8-1edbd374f279,0.12,false,false,near_mint,en,USD
|
||||
Seize Opportunity,TDM,Tarkir: Dragonstorm,119,foil,common,1,104391,f7818d28-b9a5-4341-9adc-666070b8878d,0.03,false,false,near_mint,en,USD
|
||||
Shock Brigade,TDM,Tarkir: Dragonstorm,120,foil,common,1,104700,66940466-8e9d-4a85-bfb0-e92189b7a121,0.11,false,false,near_mint,en,USD
|
||||
Hardened Tactician,TDM,Tarkir: Dragonstorm,191,foil,uncommon,1,104780,86b225cb-5c45-4da1-a64e-b04091e483e8,0.43,false,false,near_mint,en,USD
|
||||
Stormplain Detainment,TDM,Tarkir: Dragonstorm,28,foil,common,1,104135,39f3aab5-7b54-4b55-8114-c6f9f79c255d,0.04,false,false,near_mint,en,USD
|
||||
Formation Breaker,TDM,Tarkir: Dragonstorm,143,foil,uncommon,1,105136,67ab8e8f-3ef6-4339-8c66-68c5aca4867a,0.08,false,false,near_mint,en,USD
|
||||
Duty Beyond Death,TDM,Tarkir: Dragonstorm,10,foil,uncommon,1,104265,2e92640d-768b-4357-905f-bea017d351cc,1.11,false,false,near_mint,en,USD
|
||||
Piercing Exhale,TDM,Tarkir: Dragonstorm,151,foil,common,1,104891,b2a0deb9-5bc3-42d5-9e1e-5f463d176aef,0.04,false,false,near_mint,en,USD
|
||||
Trade Route Envoy,TDM,Tarkir: Dragonstorm,163,foil,common,1,105174,f0c89d95-d697-4cfa-9dfa-52d7adb96176,0.05,false,false,near_mint,en,USD
|
||||
Thornwood Falls,TDM,Tarkir: Dragonstorm,269,foil,common,1,104376,ebb502c2-5fd0-46a9-b77d-010f4a942056,0.07,false,false,near_mint,en,USD
|
||||
Kin-Tree Nurturer,TDM,Tarkir: Dragonstorm,83,foil,common,1,105124,2177ef64-28bf-4acf-b1f1-c1408f03c411,0.03,false,false,near_mint,en,USD
|
||||
Rebellious Strike,TDM,Tarkir: Dragonstorm,20,foil,common,1,104949,c9bafe19-3bd6-4da0-b3e5-e0b89262504c,0.06,false,false,near_mint,en,USD
|
||||
Scoured Barrens,TDM,Tarkir: Dragonstorm,267,foil,common,1,104346,b4b47b80-69ed-44b0-afa0-ca90206dc16d,0.06,false,false,near_mint,en,USD
|
||||
Avenger of the Fallen,TDM,Tarkir: Dragonstorm,73,foil,rare,1,104984,d5397366-151f-46b0-b9b2-fa4d5bd892d8,0.68,false,false,near_mint,en,USD
|
||||
Dismal Backwater,TDM,Tarkir: Dragonstorm,254,foil,common,1,104238,082b52c9-c46e-44d3-b723-546ba528e07b,0.07,false,false,near_mint,en,USD
|
||||
Rediscover the Way,TDM,Tarkir: Dragonstorm,215,normal,rare,1,104313,79d6decf-afd5-4e96-b87e-fd7ab7e3c068,0.19,false,false,near_mint,en,USD
|
||||
Auroral Procession,TDM,Tarkir: Dragonstorm,169,normal,uncommon,1,104701,672f94ad-65d6-4c7d-925d-165ef264626f,0.22,false,false,near_mint,en,USD
|
||||
Runescale Stormbrood // Chilling Screech,TDM,Tarkir: Dragonstorm,316,normal,uncommon,1,104733,72e8f916-5a01-4918-bcb5-7fd69fe32785,0.31,false,false,near_mint,en,USD
|
||||
Stadium Headliner,TDM,Tarkir: Dragonstorm,122,normal,rare,1,104552,37d4ab2a-a06a-4768-b5e1-e1def957d7f4,0.44,false,false,near_mint,en,USD
|
||||
Nature's Rhythm,TDM,Tarkir: Dragonstorm,150,normal,rare,1,104460,1397d904-c51d-451e-8505-7f3118acc1f6,3.08,false,false,near_mint,en,USD
|
||||
Karakyk Guardian,TDM,Tarkir: Dragonstorm,198,normal,uncommon,1,104859,a4c77b08-c3f6-4458-8636-f226f9843b6d,0.08,false,false,near_mint,en,USD
|
||||
"Anafenza, Unyielding Lineage",TDM,Tarkir: Dragonstorm,2,normal,rare,1,104258,29957f49-9a6b-42f6-b2fb-b48f653ab725,0.22,false,false,near_mint,en,USD
|
||||
"Narset, Jeskai Waymaster",TDM,Tarkir: Dragonstorm,209,normal,rare,1,103995,6b77cbc1-dbc8-44d9-aa29-15cbb19afecd,0.22,false,false,near_mint,en,USD
|
||||
Stillness in Motion,TDM,Tarkir: Dragonstorm,59,normal,rare,1,104864,a6289251-17e4-4987-96b9-2fb1a8f90e2a,0.17,false,false,near_mint,en,USD
|
||||
Thunder of Unity,TDM,Tarkir: Dragonstorm,231,normal,rare,1,104671,5c953b36-f5e4-4258-91cb-f07e799321f7,0.14,false,false,near_mint,en,USD
|
||||
The Sibsig Ceremony,TDM,Tarkir: Dragonstorm,340,normal,rare,1,104719,6daa156c-478f-47dd-9284-b95e82ccfd68,0.67,false,false,near_mint,en,USD
|
||||
Tersa Lightshatter,TDM,Tarkir: Dragonstorm,127,normal,rare,1,104825,99e96b34-b1c4-4647-a38e-2cf1aedaaace,2.31,false,false,near_mint,en,USD
|
||||
Forest,TDM,Tarkir: Dragonstorm,286,normal,common,1,104324,8e3e83d2-96ba-4d5c-a1ed-6c08a90b339c,0.07,false,false,near_mint,en,USD
|
||||
Windcrag Siege,TDM,Tarkir: Dragonstorm,235,normal,rare,1,104534,31a8329b-23a1-4c49-a579-a5da8d01435a,1.68,false,false,near_mint,en,USD
|
||||
Mountain,TDM,Tarkir: Dragonstorm,284,normal,common,1,104274,3df7c206-97b6-49d7-ba01-7a35fd8c61d9,0.05,false,false,near_mint,en,USD
|
||||
Inevitable Defeat,TDM,Tarkir: Dragonstorm,194,normal,rare,1,103997,9d677980-b608-407e-9f17-790a81263f15,0.28,false,false,near_mint,en,USD
|
||||
Dalkovan Encampment,TDM,Tarkir: Dragonstorm,394,normal,rare,1,104670,5af006f6-135e-4ea0-8ce4-7824934e87da,0.72,false,false,near_mint,en,USD
|
||||
Host of the Hereafter,TDM,Tarkir: Dragonstorm,193,normal,uncommon,1,104448,0f182957-8133-45a7-80a3-1944bead4d43,0.14,false,false,near_mint,en,USD
|
||||
"Sarkhan, Dragon Ascendant",TDM,Tarkir: Dragonstorm,118,normal,rare,1,104003,c2200646-7b7c-489d-bbae-16b03e1d7fb2,0.32,false,false,near_mint,en,USD
|
||||
Stormscale Scion,TDM,Tarkir: Dragonstorm,123,normal,mythic,1,103987,0ac43386-bd32-425c-8776-cec00b064cbc,6.78,false,false,near_mint,en,USD
|
||||
Dragon Sniper,TDM,Tarkir: Dragonstorm,139,normal,uncommon,1,105120,074b1e00-45bb-4436-8f5e-058512b2d08a,0.25,false,false,near_mint,en,USD
|
||||
Island,TDM,Tarkir: Dragonstorm,273,normal,common,1,104276,4208e66c-8c98-4c48-ab07-8523c0b26ca4,1.02,false,false,near_mint,en,USD
|
||||
Yathan Roadwatcher,TDM,Tarkir: Dragonstorm,236,normal,rare,1,104800,8e77339b-dd82-481c-9ee2-4156ca69ad35,0.14,false,false,near_mint,en,USD
|
||||
Nomad Outpost,TDM,Tarkir: Dragonstorm,263,normal,uncommon,1,104868,a68fbeaa-941f-4d53-becd-f93ed22b9a54,0.12,false,false,near_mint,en,USD
|
||||
Sage of the Skies,TDM,Tarkir: Dragonstorm,22,normal,rare,1,104710,6ade6918-6d1d-448d-ab56-93996051e9a9,0.21,false,false,near_mint,en,USD
|
||||
Kheru Goldkeeper,TDM,Tarkir: Dragonstorm,199,normal,uncommon,1,104798,8d11183a-57f5-4ddb-8a6e-15fff704b114,0.18,false,false,near_mint,en,USD
|
||||
All-Out Assault,TDM,Tarkir: Dragonstorm,167,normal,mythic,1,104348,b74876d8-f6a6-4b47-b960-b01a331bab01,4.11,false,false,near_mint,en,USD
|
||||
Winternight Stories,TDM,Tarkir: Dragonstorm,67,normal,rare,1,104693,64d9367c-f50c-4568-aa63-6760c44ecaeb,0.44,false,false,near_mint,en,USD
|
||||
New Way Forward,TDM,Tarkir: Dragonstorm,211,normal,rare,1,104996,d9d48f9e-79f0-478c-9db0-ff7ac4a8f401,0.17,false,false,near_mint,en,USD
|
||||
Strategic Betrayal,TDM,Tarkir: Dragonstorm,94,normal,uncommon,1,105145,95617742-548d-464a-bb89-a858ffa9018f,0.18,false,false,near_mint,en,USD
|
||||
Opulent Palace,TDM,Tarkir: Dragonstorm,264,normal,uncommon,1,104491,21cb3b3b-0738-4c2e-a3fc-927fd6b9d3fb,0.14,false,false,near_mint,en,USD
|
||||
United Battlefront,TDM,Tarkir: Dragonstorm,32,normal,rare,1,104370,dff398be-4ba4-4976-9acc-be99d2e07a61,0.51,false,false,near_mint,en,USD
|
||||
Temur Battlecrier,TDM,Tarkir: Dragonstorm,228,normal,rare,1,104309,72184791-0767-4108-920c-763e92dae2d4,0.68,false,false,near_mint,en,USD
|
||||
"Kotis, the Fangkeeper",TDM,Tarkir: Dragonstorm,362,normal,rare,1,104388,f70098f2-e5a8-4056-b5b3-1229fc290c51,0.48,false,false,near_mint,en,USD
|
||||
Forest,TDM,Tarkir: Dragonstorm,285,normal,common,1,104317,8100bceb-ffba-487a-bb45-4fe2a156a8dc,0.06,false,false,near_mint,en,USD
|
||||
Dragonfire Blade,TDM,Tarkir: Dragonstorm,240,normal,rare,1,104427,031afea3-fbfb-4663-a8cc-9b7eb7b16020,0.64,false,false,near_mint,en,USD
|
||||
Great Arashin City,TDM,Tarkir: Dragonstorm,257,normal,rare,1,105033,ecba23b6-9f3a-431e-bc22-f1fb04d27b68,0.33,false,false,near_mint,en,USD
|
||||
Smile at Death,TDM,Tarkir: Dragonstorm,24,normal,mythic,1,104000,ae2da18f-0d7d-446c-b463-8bf170ed95da,3.51,false,false,near_mint,en,USD
|
||||
Maelstrom of the Spirit Dragon,TDM,Tarkir: Dragonstorm,260,normal,rare,1,104359,c4e90bfb-d9a5-48a9-9ff9-b0f50a813eee,1.31,false,false,near_mint,en,USD
|
||||
Eshki Dragonclaw,TDM,Tarkir: Dragonstorm,182,normal,rare,1,104445,0d369c44-78ee-4f3c-bf2b-cddba7fe26d4,0.19,false,false,near_mint,en,USD
|
||||
Skirmish Rhino,TDM,Tarkir: Dragonstorm,224,normal,uncommon,1,103992,4a2e9ba1-c254-41e3-9845-4e81f9fec38d,0.15,false,false,near_mint,en,USD
|
||||
"Teval, Arbiter of Virtue",TDM,Tarkir: Dragonstorm,373,normal,mythic,1,104332,a19c38bc-946c-438a-ac8b-f59ff0b4c613,7.06,false,false,near_mint,en,USD
|
||||
"Ureni, the Song Unending",TDM,Tarkir: Dragonstorm,233,normal,mythic,1,104253,227802c0-4ff6-43a8-a850-ed0f546dc5ac,3.79,false,false,near_mint,en,USD
|
||||
Hardened Tactician,TDM,Tarkir: Dragonstorm,191,normal,uncommon,1,104780,86b225cb-5c45-4da1-a64e-b04091e483e8,1.0,false,false,near_mint,en,USD
|
||||
Sandsteppe Citadel,TDM,Tarkir: Dragonstorm,266,normal,uncommon,1,104603,47f47e7f-39ba-4807-8e32-7262a61dfbba,0.13,false,false,near_mint,en,USD
|
||||
"Kotis, the Fangkeeper",TDM,Tarkir: Dragonstorm,202,normal,rare,1,104364,d3736f17-f80b-4b2c-b919-2c963bc14682,0.28,false,false,near_mint,en,USD
|
||||
Magmatic Hellkite,TDM,Tarkir: Dragonstorm,111,normal,rare,1,104895,b3b3aec8-d931-4c7f-86b5-1e7dfb717b59,0.56,false,false,near_mint,en,USD
|
||||
Dalkovan Encampment,TDM,Tarkir: Dragonstorm,253,normal,rare,1,104822,98ad5f0c-8775-4e89-8e92-84a6ade93e35,0.38,false,false,near_mint,en,USD
|
||||
Ambling Stormshell,TDM,Tarkir: Dragonstorm,37,normal,rare,1,104942,c74d4a57-0f66-4965-9ed7-f88a08aa1d15,0.18,false,false,near_mint,en,USD
|
||||
Hollowmurk Siege,TDM,Tarkir: Dragonstorm,192,normal,rare,1,104668,5ac0e136-8877-4bfc-a831-2bf7b7b5ad1e,0.53,false,false,near_mint,en,USD
|
||||
Avenger of the Fallen,TDM,Tarkir: Dragonstorm,73,normal,rare,1,104984,d5397366-151f-46b0-b9b2-fa4d5bd892d8,0.34,false,false,near_mint,en,USD
|
||||
Mystic Monastery,TDM,Tarkir: Dragonstorm,262,normal,uncommon,1,104945,c7b8a01c-c400-47c7-8270-78902efe850e,0.11,false,false,near_mint,en,USD
|
||||
Songcrafter Mage,TDM,Tarkir: Dragonstorm,225,normal,rare,1,104813,9523bc07-49e5-409c-ae6b-b28e305eef36,0.35,false,false,near_mint,en,USD
|
||||
Misty Rainforest,SPG,Special Guests,111,normal,mythic,1,104321,894105c4-d3ce-4d38-855b-24aa47b112c1,32.31,false,false,near_mint,en,USD
|
||||
Tempest Hawk,TDM,Tarkir: Dragonstorm,31,normal,common,3,104587,422f9453-ab12-4e3c-8c51-be87391395a1,0.92,false,false,near_mint,en,USD
|
||||
Duty Beyond Death,TDM,Tarkir: Dragonstorm,10,normal,uncommon,2,104265,2e92640d-768b-4357-905f-bea017d351cc,0.33,false,false,near_mint,en,USD
|
||||
Heritage Reclamation,TDM,Tarkir: Dragonstorm,145,normal,common,2,104636,4f8fee37-a050-4329-8b10-46d150e7a95e,0.2,false,false,near_mint,en,USD
|
|
504
app/data/test_data/manabox_test_file.csv
Normal file
504
app/data/test_data/manabox_test_file.csv
Normal file
@ -0,0 +1,504 @@
|
||||
Name,Set code,Set name,Collector number,Foil,Rarity,Quantity,ManaBox ID,Scryfall ID,Purchase price,Misprint,Altered,Condition,Language,Purchase price currency
|
||||
"Tinybones, Bauble Burglar",FDN,Foundations,72,normal,rare,1,101414,ff3d85bc-ef2d-4251-baf4-a14bd0cee61e,0.66,false,false,near_mint,en,USD
|
||||
Scrawling Crawler,FDN,Foundations,132,normal,rare,1,100912,a1176dcf-40ee-4342-aa74-791b8352e99a,4.81,false,false,near_mint,en,USD
|
||||
"Giada, Font of Hope",FDN,Foundations,141,normal,rare,1,100804,8ae6fc26-cfad-4da8-98d9-49c27c24d293,1.33,false,false,near_mint,en,USD
|
||||
Blasphemous Edict,FDN,Foundations,57,normal,rare,1,100168,11040ecd-3153-4029-b42b-1441bc51ec34,6.9,false,false,near_mint,en,USD
|
||||
"Drakuseth, Maw of Flames",FDN,Foundations,193,normal,rare,1,100092,029b1edb-e1de-4f1c-81df-8d17f4920318,0.33,false,false,near_mint,en,USD
|
||||
"Koma, World-Eater",FDN,Foundations,347,normal,rare,1,100792,8889e1ca-eec1-408b-b11e-98cc0a357a97,4.69,false,false,near_mint,en,USD
|
||||
"Ghalta, Primal Hunger",FDN,Foundations,222,normal,rare,1,100635,6a9c39e4-a8cf-42dd-8d0e-45634b335546,0.54,false,false,near_mint,en,USD
|
||||
Sire of Seven Deaths,FDN,Foundations,1,normal,mythic,1,100812,8d8432a7-1c8a-4cfb-947c-ecf9791063eb,18.63,false,false,near_mint,en,USD
|
||||
Hero's Downfall,FDN,Foundations,319,normal,uncommon,1,101639,10cedc6d-075a-4f9b-a858-e2c29809ee33,0.39,false,false,near_mint,en,USD
|
||||
"Etali, Primal Storm",FDN,Foundations,194,normal,rare,1,101037,b6af9894-95b5-4c8e-902f-a9ba70f02e4a,0.32,false,false,near_mint,en,USD
|
||||
High Fae Trickster,FDN,Foundations,307,normal,rare,1,100918,a21180a4-208f-4c13-a704-58403ddaf12f,3.39,false,false,near_mint,en,USD
|
||||
Mocking Sprite,FDN,Foundations,159,foil,common,1,101624,f6792f63-b651-497d-8aa5-cddf4cedeca8,0.09,false,false,near_mint,en,USD
|
||||
Bake into a Pie,FDN,Foundations,169,foil,common,1,101494,2ab0e660-86a3-4b92-82fa-77dcb5db947d,0.06,false,false,near_mint,en,USD
|
||||
Boltwave,FDN,Foundations,79,foil,uncommon,1,100810,8d1ec351-5e70-4eb2-b590-6bff94ef8178,4.27,false,false,near_mint,en,USD
|
||||
Jungle Hollow,FDN,Foundations,263,foil,common,1,101224,dc758e14-d370-45e4-bbc5-938fb4d21127,0.08,false,false,near_mint,en,USD
|
||||
Ambush Wolf,FDN,Foundations,98,foil,common,1,101492,2903832c-318e-42ab-bf58-c682ec2f7afd,0.03,false,false,near_mint,en,USD
|
||||
An Offer You Can't Refuse,FDN,Foundations,160,foil,uncommon,1,100948,a829747f-cf9b-4d81-ba66-9f0630ed4565,1.51,false,false,near_mint,en,USD
|
||||
Sower of Chaos,FDN,Foundations,95,foil,common,1,101556,7ff50606-491c-4946-8d03-719b01cfad77,0.02,false,false,near_mint,en,USD
|
||||
Guarded Heir,FDN,Foundations,14,foil,uncommon,1,100505,525ba5c7-3ce5-4e52-b8b5-96c9040a6738,0.06,false,false,near_mint,en,USD
|
||||
Wind-Scarred Crag,FDN,Foundations,271,foil,common,1,100684,759e99df-11a8-4aee-b6bc-344e84e10d94,0.08,false,false,near_mint,en,USD
|
||||
Think Twice,FDN,Foundations,165,foil,common,1,101202,d88faaa1-eb41-40f7-991c-5c06e1138f3d,0.03,false,false,near_mint,en,USD
|
||||
Grow from the Ashes,FDN,Foundations,225,foil,common,1,101502,42525f8a-aee7-4811-8f05-471b559c2c4a,0.07,false,false,near_mint,en,USD
|
||||
Spitfire Lagac,FDN,Foundations,208,foil,common,1,101496,30f600cd-b696-4f49-9cbc-5a33aa43d04c,0.05,false,false,near_mint,en,USD
|
||||
Abyssal Harvester,FDN,Foundations,54,foil,rare,1,101342,f2e0f538-5825-47e9-883c-3ec6fd5b25ea,3.18,false,false,near_mint,en,USD
|
||||
Sanguine Syphoner,FDN,Foundations,68,foil,common,1,101582,b1daf5bb-c8e9-4e79-a532-ca92a9a885cd,0.19,false,false,near_mint,en,USD
|
||||
Goldvein Pick,FDN,Foundations,253,foil,common,1,101572,a241317d-2277-467e-a8f9-aa71c944e244,0.06,false,false,near_mint,en,USD
|
||||
Goblin Negotiation,FDN,Foundations,88,foil,uncommon,1,101335,f2016585-e26c-4d13-b09f-af6383c192f7,0.14,false,false,near_mint,en,USD
|
||||
Banishing Light,FDN,Foundations,138,foil,common,1,101613,e38dc3b3-1629-491b-8afd-0e7a9a857713,0.05,false,false,near_mint,en,USD
|
||||
Dauntless Veteran,FDN,Foundations,8,foil,uncommon,1,100704,7a136f26-ac66-407f-b389-357222d2c4a2,0.06,false,false,near_mint,en,USD
|
||||
Run Away Together,FDN,Foundations,162,foil,common,1,101614,e598eb7b-10dc-49e6-ac60-2fefa987173e,0.02,false,false,near_mint,en,USD
|
||||
"Tatyova, Benthic Druid",FDN,Foundations,247,foil,uncommon,1,101301,eabc978a-0666-472d-bdc6-d4b29d29eca4,0.14,false,false,near_mint,en,USD
|
||||
"Balmor, Battlemage Captain",FDN,Foundations,237,foil,uncommon,1,100142,0b45ab13-9bb6-48af-8b37-d97b25801ac8,0.13,false,false,near_mint,en,USD
|
||||
Involuntary Employment,FDN,Foundations,203,foil,common,1,101622,f3ad3d62-2f24-4562-b3fa-809213dbc4a4,0.03,false,false,near_mint,en,USD
|
||||
"Dwynen, Gilt-Leaf Daen",FDN,Foundations,217,foil,uncommon,1,100086,01c00d7b-7fac-4f8c-a1ea-de2cf4d06627,0.23,false,false,near_mint,en,USD
|
||||
Swiftfoot Boots,FDN,Foundations,258,foil,uncommon,1,100414,41040541-b129-4cf4-9411-09b1d9d32c19,2.03,false,false,near_mint,en,USD
|
||||
Soul-Shackled Zombie,FDN,Foundations,70,foil,common,1,101609,deea5690-6eb2-4353-b917-cbbf840e4e71,0.05,false,false,near_mint,en,USD
|
||||
Fake Your Own Death,FDN,Foundations,174,foil,common,1,101539,693635a6-df50-44c5-9598-0c79b45d4df4,0.09,false,false,near_mint,en,USD
|
||||
Gnarlid Colony,FDN,Foundations,224,foil,common,1,101508,47565d10-96bf-4fb0-820f-f20a44a76b6f,0.05,false,false,near_mint,en,USD
|
||||
Apothecary Stomper,FDN,Foundations,99,foil,common,1,101537,680b7b0c-0e1b-46ce-9917-9fc6e05aa148,0.02,false,false,near_mint,en,USD
|
||||
Rugged Highlands,FDN,Foundations,265,foil,common,1,101400,fd6eaf8e-8881-4d7b-bafc-75e4ca5cbef6,0.05,false,false,near_mint,en,USD
|
||||
Firebrand Archer,FDN,Foundations,196,foil,common,1,101630,fe0312f1-4c98-4b7f-8a34-0059ea80edef,0.13,false,false,near_mint,en,USD
|
||||
Scoured Barrens,FDN,Foundations,266,foil,common,1,100277,2632a4b2-9ca6-4b67-9a99-14f52ad3dc41,0.12,false,false,near_mint,en,USD
|
||||
Courageous Goblin,FDN,Foundations,82,foil,common,1,101566,8db6819c-666a-409d-85a5-b9ac34d8dd2f,0.02,false,false,near_mint,en,USD
|
||||
Jungle Hollow,FDN,Foundations,263,normal,common,1,101224,dc758e14-d370-45e4-bbc5-938fb4d21127,0.07,false,false,near_mint,en,USD
|
||||
Wind-Scarred Crag,FDN,Foundations,271,normal,common,1,100684,759e99df-11a8-4aee-b6bc-344e84e10d94,0.04,false,false,near_mint,en,USD
|
||||
Dismal Backwater,FDN,Foundations,261,normal,common,1,101220,dbb0df36-8467-4a41-8e1c-6c3584d4fd10,0.06,false,false,near_mint,en,USD
|
||||
Bloodfell Caves,FDN,Foundations,259,normal,common,1,100806,8b90dc92-cb66-41d9-89f9-2b6e3cfc8082,0.05,false,false,near_mint,en,USD
|
||||
Rugged Highlands,FDN,Foundations,265,normal,common,1,101400,fd6eaf8e-8881-4d7b-bafc-75e4ca5cbef6,0.05,false,false,near_mint,en,USD
|
||||
Scavenging Ooze,FDN,Foundations,232,normal,rare,1,100808,8c504c23-1e9a-411b-9cfe-4180d0c744f6,0.15,false,false,near_mint,en,USD
|
||||
"Kiora, the Rising Tide",FDN,Foundations,45,normal,rare,1,100762,83f20a32-9f5d-4a68-8995-549e57554da2,1.57,false,false,near_mint,en,USD
|
||||
Curator of Destinies,FDN,Foundations,34,normal,rare,1,100908,9ff79da7-c3f7-4541-87a0-503544c699b5,0.12,false,false,near_mint,en,USD
|
||||
"Loot, Exuberant Explorer",FDN,Foundations,106,normal,rare,1,100131,09980ce6-425b-4e03-94d0-0f02043cb361,4.8,false,false,near_mint,en,USD
|
||||
Micromancer,FDN,Foundations,158,normal,uncommon,1,101274,e6af54ea-b57a-4e50-8e46-1747cca14430,0.07,false,false,near_mint,en,USD
|
||||
"Ruby, Daring Tracker",FDN,Foundations,245,normal,uncommon,1,101405,fe3e7dd2-b66d-4218-9fde-f84bec26b7bf,0.05,false,false,near_mint,en,USD
|
||||
Mild-Mannered Librarian,FDN,Foundations,228,normal,uncommon,1,100515,5389663a-fe25-41b9-8c92-1f4d7721ffc2,0.03,false,false,near_mint,en,USD
|
||||
Guarded Heir,FDN,Foundations,14,normal,uncommon,1,100505,525ba5c7-3ce5-4e52-b8b5-96c9040a6738,0.05,false,false,near_mint,en,USD
|
||||
Garruk's Uprising,FDN,Foundations,220,normal,uncommon,1,100447,4805c303-e73b-443b-a09f-49d2c2c88bb5,0.25,false,false,near_mint,en,USD
|
||||
Vampire Nighthawk,FDN,Foundations,186,normal,uncommon,1,101474,0a1934ab-3171-4fc6-8033-ad998899ba73,0.12,false,false,near_mint,en,USD
|
||||
Soulstone Sanctuary,FDN,Foundations,133,normal,rare,1,100596,642553a7-6d0f-483d-a873-3a703786db42,1.9,false,false,near_mint,en,USD
|
||||
"Balmor, Battlemage Captain",FDN,Foundations,237,normal,uncommon,1,100142,0b45ab13-9bb6-48af-8b37-d97b25801ac8,0.07,false,false,near_mint,en,USD
|
||||
Adventuring Gear,FDN,Foundations,249,normal,uncommon,1,100358,361f9b99-5b5d-40da-b4b9-5ad90f6280ee,0.06,false,false,near_mint,en,USD
|
||||
Grappling Kraken,FDN,Foundations,39,normal,uncommon,1,101165,d1f5cab3-3fc0-448d-8252-cd55abf5b596,0.12,false,false,near_mint,en,USD
|
||||
Quakestrider Ceratops,FDN,Foundations,110,normal,uncommon,1,100120,067f72c2-ead6-4879-bc9d-696c9f87c0b2,0.11,false,false,near_mint,en,USD
|
||||
Genesis Wave,FDN,Foundations,221,normal,rare,1,101177,d46f7ddb-f986-4f1f-b096-ae1a02d0bdc8,0.29,false,false,near_mint,en,USD
|
||||
"Lathril, Blade of the Elves",FDN,Foundations,242,normal,rare,1,100811,8d4e5480-a287-4a25-b855-a26dae555b1c,0.25,false,false,near_mint,en,USD
|
||||
Elvish Archdruid,FDN,Foundations,219,normal,rare,1,100341,341da856-7414-403b-b2e3-4bebd58a5aa4,0.4,false,false,near_mint,en,USD
|
||||
Imprisoned in the Moon,FDN,Foundations,156,normal,uncommon,1,101313,ee28e147-6622-4399-a314-c14a5c912dd0,0.18,false,false,near_mint,en,USD
|
||||
Inspiring Call,FDN,Foundations,226,normal,uncommon,1,100400,3e241642-5172-4437-b694-f6aa159d5cd9,0.15,false,false,near_mint,en,USD
|
||||
Essence Scatter,FDN,Foundations,153,normal,uncommon,1,101226,dd05c850-f91e-4ffb-b4cc-8418d49dad90,0.04,false,false,near_mint,en,USD
|
||||
Exemplar of Light,FDN,Foundations,11,normal,rare,1,100832,920c8fc5-fdd2-446a-a676-5c363f96928f,2.82,false,false,near_mint,en,USD
|
||||
Meteor Golem,FDN,Foundations,256,normal,uncommon,1,101167,d291ea1e-36bc-46b3-b3ae-084fa0ba69eb,0.05,false,false,near_mint,en,USD
|
||||
Swiftfoot Boots,FDN,Foundations,258,normal,uncommon,1,100414,41040541-b129-4cf4-9411-09b1d9d32c19,1.19,false,false,near_mint,en,USD
|
||||
Brazen Scourge,FDN,Foundations,191,normal,uncommon,1,101616,eb84b86c-3276-4fc1-a09d-47de388cb729,0.02,false,false,near_mint,en,USD
|
||||
Sylvan Scavenging,FDN,Foundations,113,normal,rare,1,101100,c35b683c-d3b2-46a1-876a-81b34e8ba2fc,0.25,false,false,near_mint,en,USD
|
||||
Claws Out,FDN,Foundations,6,normal,uncommon,1,100429,4396049c-b976-4b7f-8ecd-564e24ebd631,0.1,false,false,near_mint,en,USD
|
||||
Snakeskin Veil,FDN,Foundations,233,normal,uncommon,1,100645,6cc4c21d-9bdc-4490-9203-17f51db0ddd1,0.08,false,false,near_mint,en,USD
|
||||
Skyship Buccaneer,FDN,Foundations,50,normal,uncommon,1,100587,62958fc3-55dc-4b97-a070-490d6ed27820,0.02,false,false,near_mint,en,USD
|
||||
Arcane Epiphany,FDN,Foundations,29,normal,uncommon,1,100116,06431793-5dfe-4cbf-990b-4bcc960d1f31,0.03,false,false,near_mint,en,USD
|
||||
Brass's Bounty,FDN,Foundations,190,normal,rare,1,100610,65fe7127-b0ec-400f-97f1-6e17ab8e319d,0.14,false,false,near_mint,en,USD
|
||||
Fiendish Panda,FDN,Foundations,120,normal,uncommon,1,100483,4e434d74-cad0-45f5-bc8d-f34aa5e1d879,0.09,false,false,near_mint,en,USD
|
||||
Frenzied Goblin,FDN,Foundations,199,normal,uncommon,1,101602,d5592573-2889-40b1-b1d5-c2802482549a,0.03,false,false,near_mint,en,USD
|
||||
Lunar Insight,FDN,Foundations,46,normal,rare,1,100958,a9a159f6-fecf-4bdd-b2f8-a9665a5cc32d,0.25,false,false,near_mint,en,USD
|
||||
Twinblade Blessing,FDN,Foundations,26,normal,uncommon,1,101310,ecf01cbe-9fcb-4f35-bc6b-2280620b06ff,0.1,false,false,near_mint,en,USD
|
||||
"Tatyova, Benthic Druid",FDN,Foundations,247,normal,uncommon,1,101301,eabc978a-0666-472d-bdc6-d4b29d29eca4,0.06,false,false,near_mint,en,USD
|
||||
Dragon Trainer,FDN,Foundations,84,normal,uncommon,1,100830,91bd75a1-cb54-4e38-9ce1-e8f32a73c6eb,0.04,false,false,near_mint,en,USD
|
||||
Raise the Past,FDN,Foundations,22,normal,rare,1,100641,6c6be129-56da-4fe7-a6bd-6a1d402c09e1,2.27,false,false,near_mint,en,USD
|
||||
Divine Resilience,FDN,Foundations,10,normal,uncommon,1,101347,f3a08245-a535-4d24-b8c0-78759bb9c4b0,0.11,false,false,near_mint,en,USD
|
||||
Bulk Up,FDN,Foundations,80,normal,uncommon,1,100857,977dcc50-da10-4281-b522-9240c1204f5d,0.2,false,false,near_mint,en,USD
|
||||
Diregraf Ghoul,FDN,Foundations,171,normal,uncommon,1,100439,4682012c-d7e0-4257-b538-3de497507464,0.03,false,false,near_mint,en,USD
|
||||
Drake Hatcher,FDN,Foundations,35,normal,rare,1,101071,bcaf4196-6bf3-47fa-b5c7-0e77f45cf820,0.12,false,false,near_mint,en,USD
|
||||
Youthful Valkyrie,FDN,Foundations,149,normal,uncommon,1,100894,9d795f79-c3a5-4ea1-a5cf-1ce73d6837b6,0.14,false,false,near_mint,en,USD
|
||||
Seeker's Folly,FDN,Foundations,69,normal,uncommon,1,101067,bc359da6-8b7f-45ec-b530-ce159fc35953,0.06,false,false,near_mint,en,USD
|
||||
Heroic Reinforcements,FDN,Foundations,241,normal,uncommon,1,100631,6a05e8d5-c2ad-489a-888d-22622886b620,0.04,false,false,near_mint,en,USD
|
||||
Inspiration from Beyond,FDN,Foundations,43,normal,uncommon,1,101033,b636fe95-664f-4fb1-aab9-28856edeccd6,0.04,false,false,near_mint,en,USD
|
||||
"Dwynen, Gilt-Leaf Daen",FDN,Foundations,217,normal,uncommon,1,100086,01c00d7b-7fac-4f8c-a1ea-de2cf4d06627,0.14,false,false,near_mint,en,USD
|
||||
Twinflame Tyrant,FDN,Foundations,97,normal,mythic,1,100228,1eb34f51-0bd2-43c3-af95-2ce8dabcc7bb,17.77,false,false,near_mint,en,USD
|
||||
Sun-Blessed Healer,FDN,Foundations,25,normal,uncommon,1,100332,323d029e-9a88-4188-b3a4-38ef32cffc9f,0.09,false,false,near_mint,en,USD
|
||||
Seismic Rupture,FDN,Foundations,205,normal,uncommon,1,100268,2519a51a-26a0-4884-9ba8-9db135c9ee49,0.02,false,false,near_mint,en,USD
|
||||
Slumbering Cerberus,FDN,Foundations,94,normal,uncommon,1,100892,9d06faa8-201d-45db-b398-ad56f7b01848,0.03,false,false,near_mint,en,USD
|
||||
Tragic Banshee,FDN,Foundations,73,normal,uncommon,1,100324,30df3e33-2f17-4067-99f1-5db6b0f41fd4,0.03,false,false,near_mint,en,USD
|
||||
Stromkirk Bloodthief,FDN,Foundations,185,normal,uncommon,1,97176,485d6a5a-2054-47d5-91b8-71ce308ed4dc,0.04,false,false,near_mint,en,USD
|
||||
Blanchwood Armor,FDN,Foundations,213,normal,uncommon,1,100237,1fd7ec1a-dafa-42ca-bc25-f6848fb03f60,0.07,false,false,near_mint,en,USD
|
||||
Spectral Sailor,FDN,Foundations,164,normal,uncommon,1,100100,03a49535-c5f3-4a6f-b333-7ac7bffdc9ae,0.06,false,false,near_mint,en,USD
|
||||
Extravagant Replication,FDN,Foundations,154,normal,rare,1,100634,6a41dfae-bc7e-4105-8f7e-fd0109197ad8,0.43,false,false,near_mint,en,USD
|
||||
Electroduplicate,FDN,Foundations,85,normal,rare,1,100976,abb06b1c-5d4e-49b9-9c4a-e60ab656a257,0.3,false,false,near_mint,en,USD
|
||||
Angel of Finality,FDN,Foundations,136,normal,uncommon,1,101057,baaabd52-3aa9-4e2f-9369-d4db8b405ba8,0.07,false,false,near_mint,en,USD
|
||||
Battlesong Berserker,FDN,Foundations,78,normal,uncommon,1,100917,a1f8b199-5d62-485f-b1c3-b30aa550595b,0.03,false,false,near_mint,en,USD
|
||||
Swiftblade Vindicator,FDN,Foundations,246,normal,rare,1,101372,f94618ec-000c-4371-b925-05ff82bfe221,0.12,false,false,near_mint,en,USD
|
||||
Dauntless Veteran,FDN,Foundations,8,normal,uncommon,1,100704,7a136f26-ac66-407f-b389-357222d2c4a2,0.05,false,false,near_mint,en,USD
|
||||
Hero's Downfall,FDN,Foundations,175,normal,uncommon,1,97185,ad2c01d9-8f54-46c0-9dc9-d4d4764ce1c9,0.1,false,false,near_mint,en,USD
|
||||
Resolute Reinforcements,FDN,Foundations,145,normal,uncommon,1,100841,940f3989-77cc-49a9-92e0-095a75d80f0f,0.09,false,false,near_mint,en,USD
|
||||
Zombify,FDN,Foundations,187,normal,uncommon,1,101225,dc798e6f-13c4-457c-b052-b7b65bc83cfe,0.09,false,false,near_mint,en,USD
|
||||
Fiery Annihilation,FDN,Foundations,86,normal,uncommon,1,100523,54fe00aa-d284-48f9-b5a2-1bd4c5fa8e58,0.07,false,false,near_mint,en,USD
|
||||
Clinquant Skymage,FDN,Foundations,33,normal,uncommon,1,100357,36012810-0e83-4640-8ba7-7262229f1b84,0.05,false,false,near_mint,en,USD
|
||||
Consuming Aberration,FDN,Foundations,238,normal,rare,1,101066,bc2b28fd-66b0-457c-80ea-7caed2cc7926,0.16,false,false,near_mint,en,USD
|
||||
Fishing Pole,FDN,Foundations,128,normal,uncommon,1,101128,c95ab836-3277-4223-9aaa-ef2c77256b65,0.07,false,false,near_mint,en,USD
|
||||
Felling Blow,FDN,Foundations,105,normal,uncommon,1,100854,96948ae3-b15d-4d6d-aa73-9f52084cd903,0.05,false,false,near_mint,en,USD
|
||||
Abrade,FDN,Foundations,188,normal,uncommon,1,100522,548947dc-a5ca-43b5-9531-bcef20fa4ae5,0.09,false,false,near_mint,en,USD
|
||||
Spinner of Souls,FDN,Foundations,112,normal,rare,1,101358,f50a8dec-b079-4192-9098-6cdc1026c693,0.66,false,false,near_mint,en,USD
|
||||
Vampire Gourmand,FDN,Foundations,74,normal,uncommon,1,100827,917514c0-9cd5-4b97-85b9-c4f753560ad4,0.09,false,false,near_mint,en,USD
|
||||
Needletooth Pack,FDN,Foundations,108,normal,uncommon,1,100868,993c1679-e02b-44f2-b34e-12fd6b5142e9,0.05,false,false,near_mint,en,USD
|
||||
Burnished Hart,FDN,Foundations,250,normal,uncommon,1,100609,65ebbff0-fbe6-4310-a33f-e00bb2534979,0.06,false,false,near_mint,en,USD
|
||||
Arbiter of Woe,FDN,Foundations,55,normal,uncommon,1,101008,b2496c4a-df03-4583-bd76-f98ed5cb61ee,0.06,false,false,near_mint,en,USD
|
||||
Good-Fortune Unicorn,FDN,Foundations,240,normal,uncommon,1,101300,eabbe163-2b15-42e3-89ce-7363e6250d3a,0.1,false,false,near_mint,en,USD
|
||||
Reassembling Skeleton,FDN,Foundations,182,normal,uncommon,1,100291,28e84b1b-1c05-4e1b-93b8-9cc2ca73509d,0.08,false,false,near_mint,en,USD
|
||||
Reclamation Sage,FDN,Foundations,231,normal,uncommon,1,100197,1918ea65-ab7f-4d40-97fd-a656c892a2a1,0.14,false,false,near_mint,en,USD
|
||||
Leyline Axe,FDN,Foundations,129,normal,rare,1,101052,b9c03336-a321-4c06-94d1-809f328fabd8,3.17,false,false,near_mint,en,USD
|
||||
An Offer You Can't Refuse,FDN,Foundations,160,normal,uncommon,1,100948,a829747f-cf9b-4d81-ba66-9f0630ed4565,0.99,false,false,near_mint,en,USD
|
||||
Goblin Negotiation,FDN,Foundations,88,normal,uncommon,1,101335,f2016585-e26c-4d13-b09f-af6383c192f7,0.09,false,false,near_mint,en,USD
|
||||
Empyrean Eagle,FDN,Foundations,239,normal,uncommon,1,100533,577e99a7-4a55-4314-8f08-2ae0c33b85c7,0.08,false,false,near_mint,en,USD
|
||||
Solemn Simulacrum,FDN,Foundations,257,normal,rare,1,100514,5383f45e-3da2-40fb-beee-801448bbb60f,0.3,false,false,near_mint,en,USD
|
||||
Crystal Barricade,FDN,Foundations,7,normal,rare,1,100822,905d3e02-ea06-45e7-9adb-c8e7583323a2,1.24,false,false,near_mint,en,USD
|
||||
Hidetsugu's Second Rite,FDN,Foundations,202,normal,uncommon,1,100577,609421da-8d89-4365-b18b-778832d91482,0.04,false,false,near_mint,en,USD
|
||||
Affectionate Indrik,FDN,Foundations,211,normal,uncommon,1,100310,2da8347d-06a4-46e0-a55e-cc2da4660263,0.02,false,false,near_mint,en,USD
|
||||
Infernal Vessel,FDN,Foundations,63,normal,uncommon,1,101560,877b6330-2d0b-4f2f-a848-f10b06fb4ef5,0.06,false,false,near_mint,en,USD
|
||||
"Zimone, Paradox Sculptor",FDN,Foundations,126,normal,mythic,1,100241,20ccbfdd-ddae-440c-9bc0-38b15a56fdd1,2.13,false,false,near_mint,en,USD
|
||||
High-Society Hunter,FDN,Foundations,61,normal,rare,1,100501,51da4a4b-ea12-4169-a7cf-eb4427f13e84,0.64,false,false,near_mint,en,USD
|
||||
Heraldic Banner,FDN,Foundations,254,normal,uncommon,1,100678,743ea709-dbb3-4db8-a2ce-544f47eb6339,0.24,false,false,near_mint,en,USD
|
||||
Wardens of the Cycle,FDN,Foundations,125,normal,uncommon,1,100761,83ea9b2c-5723-4eff-88ac-6669975939e3,0.07,false,false,near_mint,en,USD
|
||||
Preposterous Proportions,FDN,Foundations,109,normal,rare,1,100983,acb65189-60e4-42e0-9fb1-da6b716b91d7,0.94,false,false,near_mint,en,USD
|
||||
Savannah Lions,FDN,Foundations,146,normal,uncommon,1,97184,9c9ac1bc-cdf3-4fa6-8319-a7ea164e9e47,0.04,false,false,near_mint,en,USD
|
||||
Secluded Courtyard,FDN,Foundations,267,normal,uncommon,1,101161,d13373d2-139b-48c7-a8c9-828cefc4f150,0.12,false,false,near_mint,en,USD
|
||||
Ajani's Pridemate,FDN,Foundations,135,normal,uncommon,1,100255,222c1a68-e34c-4103-b1be-17d4ceaef6ce,0.06,false,false,near_mint,en,USD
|
||||
"Arahbo, the First Fang",FDN,Foundations,2,normal,rare,1,100503,524a5d93-26ed-436d-a437-dc9460acce98,1.0,false,false,near_mint,en,USD
|
||||
Authority of the Consuls,FDN,Foundations,137,normal,rare,1,100425,42ce2d7f-5924-47c0-b5ed-dacf9f9617a0,5.3,false,false,near_mint,en,USD
|
||||
Nine-Lives Familiar,FDN,Foundations,321,normal,rare,1,100060,6cc1623f-370d-42b5-88a2-039f31e9be0b,2.67,false,false,near_mint,en,USD
|
||||
Ajani's Pridemate,FDN,Foundations,293,foil,uncommon,1,101180,d4cfb9bc-4273-4e5f-a7ac-2006a8345a4e,0.38,false,false,near_mint,en,USD
|
||||
Helpful Hunter,FDN,Foundations,16,foil,common,1,97172,1b9a0e91-80b5-428f-8f08-931d0631be14,1.61,false,false,near_mint,en,USD
|
||||
Felidar Savior,FDN,Foundations,12,foil,common,1,97191,cd092b14-d72f-4de0-8f19-1338661b9e3b,0.05,false,false,near_mint,en,USD
|
||||
Thrill of Possibility,FDN,Foundations,210,normal,common,3,101561,882b348c-076b-41d8-b505-063480636669,0.03,false,false,near_mint,en,USD
|
||||
Lightshell Duo,FDN,Foundations,157,normal,common,7,101063,bb75315c-ea8f-4eb0-899e-c73ef75fc396,0.04,false,false,near_mint,en,USD
|
||||
Mischievous Pup,FDN,Foundations,144,normal,uncommon,2,100670,7214d984-6400-44d7-bde6-57d96b606e78,0.04,false,false,near_mint,en,USD
|
||||
Swiftwater Cliffs,FDN,Foundations,268,normal,common,3,101389,fb88667d-7088-4889-960f-317486ebe856,0.03,false,false,near_mint,en,USD
|
||||
Hare Apparent,FDN,Foundations,15,normal,common,3,100907,9fc6f0e9-eb5f-4bc0-b3d7-756644b66d12,3.62,false,false,near_mint,en,USD
|
||||
Dazzling Angel,FDN,Foundations,9,normal,common,3,101468,027dc444-e544-4693-8653-3dcdda530162,0.1,false,false,near_mint,en,USD
|
||||
Bigfin Bouncer,FDN,Foundations,31,normal,common,3,100882,9b1d5b76-b07e-45c6-800d-4cfce085164f,0.02,false,false,near_mint,en,USD
|
||||
Ambush Wolf,FDN,Foundations,98,normal,common,4,101492,2903832c-318e-42ab-bf58-c682ec2f7afd,0.05,false,false,near_mint,en,USD
|
||||
Healer's Hawk,FDN,Foundations,142,normal,common,3,101595,cc8e4563-04bb-46b5-835e-64ba11c0e972,0.09,false,false,near_mint,en,USD
|
||||
Rune-Sealed Wall,FDN,Foundations,49,normal,uncommon,2,101212,da0f147b-95ed-4f32-9b46-6a633ae31976,0.15,false,false,near_mint,en,USD
|
||||
Pilfer,FDN,Foundations,181,normal,common,4,101564,8c7c88b5-6d09-453b-b9c1-7dcbba8f1080,0.03,false,false,near_mint,en,USD
|
||||
Stab,FDN,Foundations,71,normal,common,3,101538,6859a5ba-1c1c-4631-bba8-f9900b827178,0.04,false,false,near_mint,en,USD
|
||||
Heartfire Immolator,FDN,Foundations,201,normal,uncommon,2,100390,3ca38f4d-01f5-4a02-9000-01261a440dbf,0.03,false,false,near_mint,en,USD
|
||||
Marauding Blight-Priest,FDN,Foundations,178,normal,common,3,101528,5f70dafc-c638-4ec0-ab5b-62998f752720,0.12,false,false,near_mint,en,USD
|
||||
Broken Wings,FDN,Foundations,214,normal,common,3,100584,61f9cbeb-cc9c-4562-be65-8a77053faefe,0.02,false,false,near_mint,en,USD
|
||||
Firespitter Whelp,FDN,Foundations,197,normal,uncommon,2,100463,4b3a4c7d-3126-4bde-9dca-cb6a1e2f37c9,0.15,false,false,near_mint,en,USD
|
||||
Make Your Move,FDN,Foundations,143,normal,common,3,101546,7368f861-3288-4645-90a7-ca35d6da3721,0.03,false,false,near_mint,en,USD
|
||||
Treetop Snarespinner,FDN,Foundations,114,normal,common,4,101562,88e68fa3-159d-49a6-8ac6-afc9bd6f1718,0.06,false,false,near_mint,en,USD
|
||||
Vengeful Bloodwitch,FDN,Foundations,76,normal,uncommon,2,97189,bd0c12dd-f138-45c0-9614-d83a1d8e8399,0.17,false,false,near_mint,en,USD
|
||||
Evolving Wilds,FDN,Foundations,262,normal,common,4,100376,3a0b9356-5b91-4542-8802-f0f7275238e1,0.06,false,false,near_mint,en,USD
|
||||
Bite Down,FDN,Foundations,212,normal,common,3,101625,f8d70b3b-f6f9-4b3c-ad70-0ce369e812b5,0.04,false,false,near_mint,en,USD
|
||||
Elfsworn Giant,FDN,Foundations,103,normal,common,3,100497,5128a5be-ffa6-4998-8488-872d80b24cb2,0.06,false,false,near_mint,en,USD
|
||||
Apothecary Stomper,FDN,Foundations,99,normal,common,3,101537,680b7b0c-0e1b-46ce-9917-9fc6e05aa148,0.05,false,false,near_mint,en,USD
|
||||
Axgard Cavalry,FDN,Foundations,189,normal,common,3,101631,fe3cc41a-adae-4c9b-b4d3-03f3ca862fed,0.03,false,false,near_mint,en,USD
|
||||
Wary Thespian,FDN,Foundations,235,normal,common,3,101574,a3d62d04-0974-4cb5-9a35-5e996c6456e2,0.01,false,false,near_mint,en,USD
|
||||
Fleeting Flight,FDN,Foundations,13,normal,common,3,101513,55139100-9342-41fd-b10a-8e9932e605d4,0.04,false,false,near_mint,en,USD
|
||||
Quick-Draw Katana,FDN,Foundations,130,normal,common,3,101540,69beec98-c89c-4673-953c-8b3ef3d81560,0.07,false,false,near_mint,en,USD
|
||||
Goblin Surprise,FDN,Foundations,200,normal,common,3,101512,527dd5d4-5f72-40bb-8a9d-1f5ac3f81e2e,0.05,false,false,near_mint,en,USD
|
||||
Sower of Chaos,FDN,Foundations,95,normal,common,4,101556,7ff50606-491c-4946-8d03-719b01cfad77,0.01,false,false,near_mint,en,USD
|
||||
Involuntary Employment,FDN,Foundations,203,normal,common,4,101622,f3ad3d62-2f24-4562-b3fa-809213dbc4a4,0.06,false,false,near_mint,en,USD
|
||||
Burst Lightning,FDN,Foundations,192,normal,common,3,100994,aec5d380-d354-4750-931a-6c91853e2edc,0.08,false,false,near_mint,en,USD
|
||||
Banishing Light,FDN,Foundations,138,normal,common,4,101613,e38dc3b3-1629-491b-8afd-0e7a9a857713,0.03,false,false,near_mint,en,USD
|
||||
Blossoming Sands,FDN,Foundations,260,normal,common,2,100364,37676ed8-588c-4bca-8065-874b74d84807,0.05,false,false,near_mint,en,USD
|
||||
Felidar Savior,FDN,Foundations,12,normal,common,3,97191,cd092b14-d72f-4de0-8f19-1338661b9e3b,0.02,false,false,near_mint,en,USD
|
||||
Revenge of the Rats,FDN,Foundations,67,normal,uncommon,2,100232,1f463c55-39a0-4f2f-aae3-0c5540bde5b7,0.12,false,false,near_mint,en,USD
|
||||
Armasaur Guide,FDN,Foundations,3,normal,common,3,101591,c80fc380-0499-4499-8a60-c43844c02c9b,0.03,false,false,near_mint,en,USD
|
||||
Campus Guide,FDN,Foundations,251,normal,common,3,101504,43c59814-3167-4b05-bb85-6c736f3956a4,0.02,false,false,near_mint,en,USD
|
||||
Dreadwing Scavenger,FDN,Foundations,118,normal,uncommon,2,101252,e24d838b-ab48-410a-9a50-dbfea5da089b,0.04,false,false,near_mint,en,USD
|
||||
Gleaming Barrier,FDN,Foundations,252,normal,common,3,101479,1b49b009-e6f2-494a-9235-f5c25c2d70a9,0.06,false,false,near_mint,en,USD
|
||||
Scoured Barrens,FDN,Foundations,266,normal,common,2,100277,2632a4b2-9ca6-4b67-9a99-14f52ad3dc41,0.07,false,false,near_mint,en,USD
|
||||
Erudite Wizard,FDN,Foundations,37,normal,common,3,100835,9273c417-0fcd-4273-b24e-afff76336d0c,0.01,false,false,near_mint,en,USD
|
||||
Gorehorn Raider,FDN,Foundations,89,normal,common,3,101551,78ce6c40-3452-4aa0-a45b-dbfd70f8d220,0.02,false,false,near_mint,en,USD
|
||||
Cackling Prowler,FDN,Foundations,101,normal,common,3,101481,1bd8e971-c075-4203-8d83-c28f22d4f9b9,0.03,false,false,near_mint,en,USD
|
||||
Burglar Rat,FDN,Foundations,170,normal,common,4,101608,de1c8758-ce3d-49cf-8173-c0eb46f5e7bc,0.05,false,false,near_mint,en,USD
|
||||
Mocking Sprite,FDN,Foundations,159,normal,common,3,101624,f6792f63-b651-497d-8aa5-cddf4cedeca8,0.03,false,false,near_mint,en,USD
|
||||
Cathar Commando,FDN,Foundations,139,normal,common,3,100204,19cf024d-edb6-4a79-8676-73f8db0cdf1f,0.06,false,false,near_mint,en,USD
|
||||
Hungry Ghoul,FDN,Foundations,62,normal,common,3,100701,790f9433-7565-4f7f-88e8-8af762ea0296,0.04,false,false,near_mint,en,USD
|
||||
Vampire Soulcaller,FDN,Foundations,75,normal,common,3,101495,2d076293-3b45-4878-8f67-978927cc1f68,0.04,false,false,near_mint,en,USD
|
||||
Exsanguinate,FDN,Foundations,173,normal,uncommon,1,101330,f11d7311-4066-4a5d-ba28-9857fa707a0b,0.4,false,false,near_mint,en,USD
|
||||
Fanatical Firebrand,FDN,Foundations,195,normal,common,3,101598,d1296316-7781-4e98-95e6-7020648be6a5,0.03,false,false,near_mint,en,USD
|
||||
Sanguine Syphoner,FDN,Foundations,68,normal,common,4,101582,b1daf5bb-c8e9-4e79-a532-ca92a9a885cd,0.07,false,false,near_mint,en,USD
|
||||
Boltwave,FDN,Foundations,79,normal,uncommon,2,100810,8d1ec351-5e70-4eb2-b590-6bff94ef8178,4.08,false,false,near_mint,en,USD
|
||||
Nessian Hornbeetle,FDN,Foundations,229,normal,uncommon,2,100395,3d4d93de-85c6-4653-8ddd-d8bf21516d44,0.05,false,false,near_mint,en,USD
|
||||
Goldvein Pick,FDN,Foundations,253,normal,common,3,101572,a241317d-2277-467e-a8f9-aa71c944e244,0.06,false,false,near_mint,en,USD
|
||||
Icewind Elemental,FDN,Foundations,42,normal,common,3,101629,fd0eba76-3829-408b-828f-0b223c884728,0.05,false,false,near_mint,en,USD
|
||||
Fleeting Distraction,FDN,Foundations,155,normal,common,3,101587,c0b86a7b-4912-43a7-ab89-c3432385baa1,0.02,false,false,near_mint,en,USD
|
||||
Faebloom Trick,FDN,Foundations,38,normal,uncommon,2,100148,0c3bee8f-f5be-4404-a696-c902637799c3,0.17,false,false,near_mint,en,USD
|
||||
Brineborn Cutthroat,FDN,Foundations,152,normal,uncommon,2,100986,acf7aafb-931f-49e5-8691-eab8cb34b05e,0.02,false,false,near_mint,en,USD
|
||||
Gutless Plunderer,FDN,Foundations,60,normal,common,3,101567,909d7778-c7f8-4fa4-89f2-8b32e86e96e4,0.05,false,false,near_mint,en,USD
|
||||
Thornwood Falls,FDN,Foundations,269,normal,common,2,100424,42799f51-0f8c-444b-974e-dae281a5c697,0.05,false,false,near_mint,en,USD
|
||||
Tranquil Cove,FDN,Foundations,270,normal,common,2,100719,7c9cabca-5bcc-4b97-b2ac-a345ad3ee43c,0.06,false,false,near_mint,en,USD
|
||||
Fake Your Own Death,FDN,Foundations,174,normal,common,3,101539,693635a6-df50-44c5-9598-0c79b45d4df4,0.05,false,false,near_mint,en,USD
|
||||
Crypt Feaster,FDN,Foundations,59,normal,common,4,100382,3b072811-998a-4a71-b59c-6afecc0dc4b6,0.03,false,false,near_mint,en,USD
|
||||
Incinerating Blast,FDN,Foundations,90,normal,common,3,101603,d58e20ab-c5ca-4295-884d-78efdaa83243,0.03,false,false,near_mint,en,USD
|
||||
Refute,FDN,Foundations,48,normal,common,3,100368,38806934-dd9c-4ad4-a59c-a16dce03a14a,0.06,false,false,near_mint,en,USD
|
||||
Tolarian Terror,FDN,Foundations,167,normal,common,3,100270,2569d4f3-55ed-4f99-9592-34c7df0aab72,0.09,false,false,near_mint,en,USD
|
||||
Joust Through,FDN,Foundations,19,normal,uncommon,2,100767,846adb38-f9bb-4fed-b8ed-36ec7885f989,0.05,false,false,near_mint,en,USD
|
||||
Bake into a Pie,FDN,Foundations,169,normal,common,3,101494,2ab0e660-86a3-4b92-82fa-77dcb5db947d,0.03,false,false,near_mint,en,USD
|
||||
Soul-Shackled Zombie,FDN,Foundations,70,normal,common,4,101609,deea5690-6eb2-4353-b917-cbbf840e4e71,0.04,false,false,near_mint,en,USD
|
||||
Perforating Artist,FDN,Foundations,124,normal,uncommon,2,100674,72980409-53f0-43c1-965e-06f22e7bb608,0.1,false,false,near_mint,en,USD
|
||||
Serra Angel,FDN,Foundations,147,normal,uncommon,2,100391,3cee9303-9d65-45a2-93d4-ef4aba59141b,0.05,false,false,near_mint,en,USD
|
||||
Squad Rallier,FDN,Foundations,24,normal,common,3,101534,65e1ee86-6f08-4aa0-bf63-ae12028ef080,0.04,false,false,near_mint,en,USD
|
||||
Elementalist Adept,FDN,Foundations,36,normal,common,3,101605,d9768cc6-8f53-4922-ae32-376a2f32d719,0.02,false,false,near_mint,en,USD
|
||||
Elvish Regrower,FDN,Foundations,104,normal,uncommon,2,100278,2694e3cd-26ed-4a10-ae55-fb84d7800253,0.09,false,false,near_mint,en,USD
|
||||
Infestation Sage,FDN,Foundations,64,normal,common,3,101601,d40c73de-7a5f-46f2-a70b-449bc8ecfe24,0.07,false,false,near_mint,en,USD
|
||||
Inspiring Paladin,FDN,Foundations,18,normal,common,3,101472,0763be06-25b2-4d6b-ab33-a1af85aeb443,0.02,false,false,near_mint,en,USD
|
||||
Luminous Rebuke,FDN,Foundations,20,normal,common,3,101529,621839e1-2756-4cdc-a25c-5f76ea98dd87,0.07,false,false,near_mint,en,USD
|
||||
Gnarlid Colony,FDN,Foundations,224,normal,common,3,101508,47565d10-96bf-4fb0-820f-f20a44a76b6f,0.02,false,false,near_mint,en,USD
|
||||
Sure Strike,FDN,Foundations,209,normal,common,3,101525,5de6a1e4-5c66-43e6-9f2a-2635bdab03f6,0.03,false,false,near_mint,en,USD
|
||||
Helpful Hunter,FDN,Foundations,16,normal,common,3,97172,1b9a0e91-80b5-428f-8f08-931d0631be14,0.14,false,false,near_mint,en,USD
|
||||
Goblin Boarders,FDN,Foundations,87,normal,common,3,101506,4409a063-bf2a-4a49-803e-3ce6bd474353,0.04,false,false,near_mint,en,USD
|
||||
Macabre Waltz,FDN,Foundations,177,normal,common,3,101509,4d1f3c84-89ba-4426-a80b-d524f172c912,0.03,false,false,near_mint,en,USD
|
||||
Grow from the Ashes,FDN,Foundations,225,normal,common,3,101502,42525f8a-aee7-4811-8f05-471b559c2c4a,0.03,false,false,near_mint,en,USD
|
||||
Stroke of Midnight,FDN,Foundations,148,normal,uncommon,2,100970,ab135925-d924-456d-851a-6ccdaaf27271,0.17,false,false,near_mint,en,USD
|
||||
Eaten Alive,FDN,Foundations,172,normal,common,3,100216,1c4f7b20-b2a8-498c-8c36-dc296863b0b9,0.02,false,false,near_mint,en,USD
|
||||
Aetherize,FDN,Foundations,151,normal,uncommon,2,100225,1e5530fc-0291-4a17-b048-c5d24e6f51d8,0.17,false,false,near_mint,en,USD
|
||||
Giant Growth,FDN,Foundations,223,normal,common,4,101073,bd0bf74e-14c1-4428-88d8-2181a080b5d0,0.03,false,false,near_mint,en,USD
|
||||
Billowing Shriekmass,FDN,Foundations,56,normal,uncommon,2,100711,7b3587a9-0667-4d53-807b-c437bcb1d7b3,0.02,false,false,near_mint,en,USD
|
||||
Think Twice,FDN,Foundations,165,normal,common,4,101202,d88faaa1-eb41-40f7-991c-5c06e1138f3d,0.05,false,false,near_mint,en,USD
|
||||
Beast-Kin Ranger,FDN,Foundations,100,normal,common,3,100082,0102e0be-5783-4825-9489-713b1b1df0b2,0.05,false,false,near_mint,en,USD
|
||||
Spitfire Lagac,FDN,Foundations,208,normal,common,4,101496,30f600cd-b696-4f49-9cbc-5a33aa43d04c,0.02,false,false,near_mint,en,USD
|
||||
Aegis Turtle,FDN,Foundations,150,normal,common,3,101590,c7f2014a-fbc9-447c-a440-e06d01066bb9,0.08,false,false,near_mint,en,USD
|
||||
Firebrand Archer,FDN,Foundations,196,normal,common,3,101630,fe0312f1-4c98-4b7f-8a34-0059ea80edef,0.05,false,false,near_mint,en,USD
|
||||
Shivan Dragon,FDN,Foundations,206,normal,uncommon,2,100236,1fcff1e0-2745-448d-a27b-e31719e222e9,0.05,false,false,near_mint,en,USD
|
||||
Cephalid Inkmage,FDN,Foundations,32,normal,uncommon,2,101040,b7e47680-18c7-4ffb-aac4-c5db6e7095ba,0.05,false,false,near_mint,en,USD
|
||||
Prideful Parent,FDN,Foundations,21,normal,common,3,97188,b742117a-8a72-43b9-b05d-274829d138a2,0.04,false,false,near_mint,en,USD
|
||||
Uncharted Voyage,FDN,Foundations,53,normal,common,4,101611,e0846820-e595-4743-8a28-29c57d728677,0.01,false,false,near_mint,en,USD
|
||||
Eager Trufflesnout,FDN,Foundations,102,normal,uncommon,2,100940,a6e8433d-eb2a-43d1-b59b-7d70ff97c8e7,0.04,false,false,near_mint,en,USD
|
||||
Juggernaut,FDN,Foundations,255,normal,uncommon,2,101351,f4468fff-cd6f-428c-b7a0-ff89f5bbea2e,0.07,false,false,near_mint,en,USD
|
||||
Llanowar Elves,FDN,Foundations,227,normal,common,3,95583,6a0b230b-d391-4998-a3f7-7b158a0ec2cd,0.15,false,false,near_mint,en,USD
|
||||
Overrun,FDN,Foundations,230,normal,uncommon,2,100220,1d8e9cbb-8bf4-4a48-a58e-79deb3abdf7f,0.14,false,false,near_mint,en,USD
|
||||
Crackling Cyclops,FDN,Foundations,83,normal,common,3,101541,6e5b899a-52f7-471b-ad50-4fa6566758fd,0.01,false,false,near_mint,en,USD
|
||||
Mischievous Mystic,FDN,Foundations,47,normal,uncommon,2,100242,20d89cec-528b-4b2a-87db-e11ce0000622,0.14,false,false,near_mint,en,USD
|
||||
Witness Protection,FDN,Foundations,168,normal,common,3,101621,f231e981-0069-43ce-ac1c-c85ced613e93,0.08,false,false,near_mint,en,USD
|
||||
Dwynen's Elite,FDN,Foundations,218,normal,common,3,100800,89d94c28-ea2e-4a3d-935f-6b2d9f2efc7a,0.05,false,false,near_mint,en,USD
|
||||
Bushwhack,FDN,Foundations,215,normal,common,3,101469,03ebdb36-55e0-49dd-a514-785fbeb4ae19,0.1,false,false,near_mint,en,USD
|
||||
Run Away Together,FDN,Foundations,162,normal,common,3,101614,e598eb7b-10dc-49e6-ac60-2fefa987173e,0.05,false,false,near_mint,en,USD
|
||||
Strongbox Raider,FDN,Foundations,96,normal,uncommon,2,101006,b2223eb8-59f9-489b-a3f3-b6496218cb79,0.02,false,false,near_mint,en,USD
|
||||
Vanguard Seraph,FDN,Foundations,28,normal,common,4,101503,4329c861-fc16-4a96-9c03-25af6ac2adc8,0.06,false,false,near_mint,en,USD
|
||||
Self-Reflection,FDN,Foundations,163,normal,uncommon,2,101247,e1e6abc9-25b2-4d51-b519-2525079eab51,0.04,false,false,near_mint,en,USD
|
||||
Strix Lookout,FDN,Foundations,52,normal,common,3,101627,fbd2422e-8e84-4c39-af29-3b4d38baee63,0.03,false,false,near_mint,en,USD
|
||||
Cat Collector,FDN,Foundations,4,normal,uncommon,2,100507,526fe356-bff1-4211-9e88-bf913ac76b1d,0.1,false,false,near_mint,en,USD
|
||||
Courageous Goblin,FDN,Foundations,82,normal,common,3,101566,8db6819c-666a-409d-85a5-b9ac34d8dd2f,0.03,false,false,near_mint,en,USD
|
||||
"Ygra, Eater of All",BLB,Bloomburrow,241,normal,mythic,1,95825,b9ac7673-eae8-4c4b-889e-5025213a6151,11.58,false,false,near_mint,en,USD
|
||||
Lifecreed Duo,BLB,Bloomburrow,20,normal,common,1,95968,ca543405-5e12-48a0-9a77-082ac9bcb2f2,0.06,false,false,near_mint,en,USD
|
||||
Take Out the Trash,BLB,Bloomburrow,156,normal,common,1,95940,7a1c6f00-af4c-4d35-b682-6c0e759df9a5,0.04,false,false,near_mint,en,USD
|
||||
Ravine Raider,BLB,Bloomburrow,106,normal,common,1,96370,874510be-7ecd-4eff-abad-b9594eb4821a,0.02,false,false,near_mint,en,USD
|
||||
Longstalk Brawl,BLB,Bloomburrow,182,normal,common,1,95966,c7ef748c-b5e5-4e7d-bf2e-d3e6c08edb42,0.04,false,false,near_mint,en,USD
|
||||
Valley Floodcaller,BLB,Bloomburrow,79,normal,rare,1,95876,90b12da0-f666-471d-95f5-15d8c9b31c92,2.65,false,false,near_mint,en,USD
|
||||
Bandit's Talent,BLB,Bloomburrow,83,normal,uncommon,1,95917,485dc8d8-9e44-4a0f-9ff6-fa448e232290,0.47,false,false,near_mint,en,USD
|
||||
Brambleguard Veteran,BLB,Bloomburrow,165,normal,uncommon,1,95880,bac9f6f8-6797-4580-9fc4-9a825872e017,0.09,false,false,near_mint,en,USD
|
||||
Mouse Trapper,BLB,Bloomburrow,22,normal,uncommon,1,95948,8ba1bc5a-03e7-44ec-893e-44042cbc02ef,0.04,false,false,near_mint,en,USD
|
||||
Bushy Bodyguard,BLB,Bloomburrow,166,normal,uncommon,1,95997,0de60cf7-fa82-4b6f-9f88-6590fba5c863,0.08,false,false,near_mint,en,USD
|
||||
Valley Mightcaller,BLB,Bloomburrow,202,normal,rare,1,96057,7256451f-0122-452a-88e8-0fb0f6bea3f3,1.01,false,false,near_mint,en,USD
|
||||
Druid of the Spade,BLB,Bloomburrow,170,normal,common,1,96054,6b485cf7-bad0-4824-9ba7-cb112ce4769f,0.02,false,false,near_mint,en,USD
|
||||
Skyskipper Duo,BLB,Bloomburrow,71,normal,common,1,96476,d6844bad-ffbe-4c6e-b438-08562eccea52,0.04,false,false,near_mint,en,USD
|
||||
Osteomancer Adept,BLB,Bloomburrow,103,normal,rare,1,95800,7d8238dd-858f-466c-96de-986bd66861d7,0.36,false,false,near_mint,en,USD
|
||||
Tender Wildguide,BLB,Bloomburrow,196,normal,rare,1,95792,6b8bfa91-adb0-4596-8c16-d8bb64fdb26d,0.49,false,false,near_mint,en,USD
|
||||
Huskburster Swarm,BLB,Bloomburrow,98,normal,uncommon,1,95978,ed2f61d7-4eb0-41c5-8a34-a0793c2abc51,0.13,false,false,near_mint,en,USD
|
||||
Scrapshooter,BLB,Bloomburrow,191,normal,rare,1,96113,c42ab407-e72d-4c48-9a9e-2055b5e71c69,0.38,false,false,near_mint,en,USD
|
||||
Scavenger's Talent,BLB,Bloomburrow,111,normal,rare,1,96084,9a52b7fe-87ae-425b-85fd-b24e6e0395f1,1.54,false,false,near_mint,en,USD
|
||||
Valley Rotcaller,BLB,Bloomburrow,119,normal,rare,1,95781,4da80a9a-b1d5-4fc5-92f7-36946195d0c7,1.45,false,false,near_mint,en,USD
|
||||
Thornplate Intimidator,BLB,Bloomburrow,117,normal,common,1,96019,42f66c4a-feaa-4ba6-aa56-955b43329a9e,0.02,false,false,near_mint,en,USD
|
||||
Bakersbane Duo,BLB,Bloomburrow,163,normal,common,1,96035,5309354f-1ff4-4fa9-9141-01ea2f7588ab,0.1,false,false,near_mint,en,USD
|
||||
Shore Up,BLB,Bloomburrow,69,normal,common,1,96277,4dc3b49e-3674-494c-bdea-4374cefd10f4,0.08,false,false,near_mint,en,USD
|
||||
Emberheart Challenger,BLB,Bloomburrow,133,normal,rare,1,95888,0035082e-bb86-4f95-be48-ffc87fe5286d,4.13,false,false,near_mint,en,USD
|
||||
"Gev, Scaled Scorch",BLB,Bloomburrow,214,normal,rare,1,96001,131ea976-289e-4f32-896d-27bbfd423ba9,0.37,false,false,near_mint,en,USD
|
||||
Starfall Invocation,BLB,Bloomburrow,34,normal,rare,1,95904,2aea38e6-ec58-4091-b27c-2761bdd12b13,0.88,false,false,near_mint,en,USD
|
||||
Tidecaller Mentor,BLB,Bloomburrow,236,normal,uncommon,1,95859,fa10ffac-7cc2-41ef-b8a0-9431923c0542,0.04,false,false,near_mint,en,USD
|
||||
Jackdaw Savior,BLB,Bloomburrow,18,normal,rare,1,96000,121af600-6143-450a-9f87-12ce4833f1ec,0.27,false,false,near_mint,en,USD
|
||||
"Helga, Skittish Seer",BLB,Bloomburrow,217,normal,mythic,1,95914,40339715-22d0-4f99-822b-a00d9824f27a,2.0,false,false,near_mint,en,USD
|
||||
Long River Lurker,BLB,Bloomburrow,57,normal,uncommon,1,95941,7c267719-cd03-4003-b281-e732d5e42a1e,0.1,false,false,near_mint,en,USD
|
||||
Thornvault Forager,BLB,Bloomburrow,197,normal,rare,1,95807,8c2d6b02-a453-40f9-992a-5c5542987cfb,0.65,false,false,near_mint,en,USD
|
||||
Eddymurk Crab,BLB,Bloomburrow,48,normal,uncommon,1,96132,e6d45abe-4962-47d9-a54e-7e623ea8647c,0.18,false,false,near_mint,en,USD
|
||||
Moonstone Harbinger,BLB,Bloomburrow,101,normal,uncommon,1,95922,59e4aa8d-1d06-48db-b205-aa2f1392bbcb,0.03,false,false,near_mint,en,USD
|
||||
Brazen Collector,BLB,Bloomburrow,128,normal,uncommon,1,95873,78b55a58-c669-4dc6-aa63-5d9dff52e613,0.09,false,false,near_mint,en,USD
|
||||
Brightblade Stoat,BLB,Bloomburrow,4,normal,uncommon,1,95882,df7fea2e-7414-4bc8-adb0-9342e174c009,0.07,false,false,near_mint,en,USD
|
||||
Warren Warleader,BLB,Bloomburrow,38,normal,mythic,1,95849,eb5237a0-5ac3-4ded-9f92-5f782a7bbbd7,3.14,false,false,near_mint,en,USD
|
||||
Kitnap,BLB,Bloomburrow,53,normal,rare,1,95739,085be5d1-fd85-46d1-ad39-a8aa75a06a96,0.14,false,false,near_mint,en,USD
|
||||
Fountainport,BLB,Bloomburrow,253,normal,rare,1,96052,658cfcb7-81b7-48c6-9dd2-1663d06108cf,5.77,false,false,near_mint,en,USD
|
||||
Whiskervale Forerunner,BLB,Bloomburrow,40,normal,rare,1,95927,60a78d59-af31-4af9-95aa-2573fe553925,0.17,false,false,near_mint,en,USD
|
||||
Dreamdew Entrancer,BLB,Bloomburrow,211,normal,rare,1,95755,26bd6b0d-8606-4a37-8be3-a852f1a8e99c,0.28,false,false,near_mint,en,USD
|
||||
Playful Shove,BLB,Bloomburrow,145,normal,uncommon,1,95993,07956edf-34c1-4218-9784-ddbca13e380c,0.1,false,false,near_mint,en,USD
|
||||
Feed the Cycle,BLB,Bloomburrow,94,normal,uncommon,1,96067,7e017ff8-2936-4a1b-bece-00004cfbad06,0.12,false,false,near_mint,en,USD
|
||||
Hoarder's Overflow,BLB,Bloomburrow,141,normal,uncommon,1,96112,c2ed5079-07b4-4575-a2c8-5f0cbff888c3,0.04,false,false,near_mint,en,USD
|
||||
Sunspine Lynx,BLB,Bloomburrow,155,normal,rare,1,95875,8995ceaf-b7e0-423c-8f3e-25212d522502,1.8,false,false,near_mint,en,USD
|
||||
Stormcatch Mentor,BLB,Bloomburrow,234,normal,uncommon,1,95813,99754055-6d67-4fde-aff3-41f6af6ea764,0.21,false,false,near_mint,en,USD
|
||||
For the Common Good,BLB,Bloomburrow,172,normal,rare,1,95912,3ec72a27-b622-47d7-bdf3-970ccaef0d2a,0.87,false,false,near_mint,en,USD
|
||||
Dawn's Truce,BLB,Bloomburrow,295,normal,rare,1,95893,0cce7aec-f9b0-461b-8245-5286b741409d,8.43,false,false,near_mint,en,USD
|
||||
"Clement, the Worrywort",BLB,Bloomburrow,329,normal,rare,1,95835,d1a68d51-cd4e-4ee3-abc7-01435085aa26,0.55,false,false,near_mint,en,USD
|
||||
Tender Wildguide,BLB,Bloomburrow,325,normal,rare,1,95760,2dc164c8-62ca-4d59-ae1c-ef273fde9d10,0.63,false,false,near_mint,en,USD
|
||||
Valley Questcaller,BLB,Bloomburrow,299,normal,rare,1,95839,d9f25130-678d-4338-8eb4-b20d2da5bc74,1.0,false,false,near_mint,en,USD
|
||||
Heirloom Epic,BLB,Bloomburrow,246,normal,uncommon,1,96061,7839ce48-0175-494a-ab89-9bdfb7a50cb1,0.06,false,false,near_mint,en,USD
|
||||
Shrike Force,BLB,Bloomburrow,31,normal,uncommon,1,95763,306fec2c-d8b7-4f4b-8f58-10e3b9f3158f,0.14,false,false,near_mint,en,USD
|
||||
Into the Flood Maw,BLB,Bloomburrow,52,normal,uncommon,1,95919,50b9575a-53d9-4df7-b86c-cda021107d3f,1.48,false,false,near_mint,en,USD
|
||||
Salvation Swan,BLB,Bloomburrow,28,normal,rare,1,95635,b2656160-d319-4530-a6e5-c418596c3f12,0.27,false,false,near_mint,en,USD
|
||||
Hired Claw,BLB,Bloomburrow,140,normal,rare,1,95897,1ae41080-0d67-4719-adb2-49bf2a268b6c,2.43,false,false,near_mint,en,USD
|
||||
Starseer Mentor,BLB,Bloomburrow,233,normal,uncommon,1,95791,6b2f6dc5-9fe8-49c1-b24c-1d99ce1da619,0.05,false,false,near_mint,en,USD
|
||||
Mistbreath Elder,BLB,Bloomburrow,184,normal,rare,1,95975,e5246540-5a84-41d8-9e30-8e7a6c0e84e1,0.37,false,false,near_mint,en,USD
|
||||
Hivespine Wolverine,BLB,Bloomburrow,177,normal,uncommon,1,95943,821970a3-a291-4fe9-bb13-dfc54f9c3caf,0.06,false,false,near_mint,en,USD
|
||||
Patchwork Banner,BLB,Bloomburrow,247,normal,uncommon,1,96097,a8a982c8-bc08-44ba-b3ed-9e4b124615d6,4.68,false,false,near_mint,en,USD
|
||||
"Beza, the Bounding Spring",BLB,Bloomburrow,2,normal,mythic,1,95862,fc310a26-b6a0-4e42-98ab-bdfd7b06cb63,9.56,false,false,near_mint,en,USD
|
||||
Essence Channeler,BLB,Bloomburrow,12,normal,rare,1,96042,5aaf7e4c-4d5d-4acc-a834-e6c4a7629408,1.27,false,false,near_mint,en,USD
|
||||
Valley Questcaller,BLB,Bloomburrow,36,normal,rare,1,95826,ba629ca8-a368-4282-8a61-9bf6a5c217f0,1.12,false,false,near_mint,en,USD
|
||||
Conduct Electricity,BLB,Bloomburrow,130,normal,common,1,95906,2f373dd6-2412-453c-85ba-10230dfe473a,0.02,false,false,near_mint,en,USD
|
||||
Glidedive Duo,BLB,Bloomburrow,96,normal,common,1,96026,4831e7ae-54e3-4bd9-b5af-52dc29f81715,0.02,false,false,near_mint,en,USD
|
||||
Mind Spiral,BLB,Bloomburrow,59,normal,common,1,96068,7e24fe6a-607b-49b8-9fca-cecb1e40de7f,0.01,false,false,near_mint,en,USD
|
||||
Starforged Sword,BLB,Bloomburrow,249,normal,uncommon,1,96110,c23d8e96-b972-4c6c-b0c4-b6627621f048,0.03,false,false,near_mint,en,USD
|
||||
Vinereap Mentor,BLB,Bloomburrow,238,normal,uncommon,1,95902,29b615ba-45c4-42a1-8525-1535f0b55300,0.16,false,false,near_mint,en,USD
|
||||
Mindwhisker,BLB,Bloomburrow,60,normal,uncommon,1,96099,aaa10f34-5bfd-4d87-8f07-58de3b0f5663,0.08,false,false,near_mint,en,USD
|
||||
Persistent Marshstalker,BLB,Bloomburrow,104,normal,uncommon,1,95947,8b900c71-713b-4b7e-b4be-ad9f4aa0c139,0.13,false,false,near_mint,en,USD
|
||||
Portent of Calamity,BLB,Bloomburrow,66,normal,rare,1,96073,8599e2dd-9164-4da3-814f-adccef3b9497,0.14,false,false,near_mint,en,USD
|
||||
Fabled Passage,BLB,Bloomburrow,252,normal,rare,1,96075,8809830f-d8e1-4603-9652-0ad8b00234e9,5.13,false,false,near_mint,en,USD
|
||||
Stormsplitter,BLB,Bloomburrow,154,normal,mythic,1,96040,56f214d3-6b93-40db-a693-55e491c8a283,3.12,false,false,near_mint,en,USD
|
||||
Stargaze,BLB,Bloomburrow,114,normal,uncommon,1,95939,777fc599-8de7-44d2-8fdd-9bddf5948a0c,0.14,false,false,near_mint,en,USD
|
||||
Coruscation Mage,BLB,Bloomburrow,131,normal,uncommon,1,95972,dc2c1de0-6233-469a-be72-a050b97d2c8f,0.32,false,false,near_mint,en,USD
|
||||
Dour Port-Mage,BLB,Bloomburrow,47,normal,rare,1,96049,6402133e-eed1-4a46-9667-8b7a310362c1,2.17,false,false,near_mint,en,USD
|
||||
"Muerra, Trash Tactician",BLB,Bloomburrow,227,normal,rare,1,95821,b40e4658-fd68-46d0-9a89-25570a023d19,0.31,false,false,near_mint,en,USD
|
||||
Stormchaser's Talent,BLB,Bloomburrow,75,normal,rare,1,96092,a36e682d-b43d-4e08-bf5b-70d7e924dbe5,13.62,false,false,near_mint,en,USD
|
||||
Sinister Monolith,BLB,Bloomburrow,113,normal,uncommon,1,96012,2a15e06c-2608-4e7a-a16c-d35417669d86,0.08,false,false,near_mint,en,USD
|
||||
Pawpatch Formation,BLB,Bloomburrow,186,normal,uncommon,1,95963,b82c20ad-0f69-4822-ae76-770832cccdf7,1.83,false,false,near_mint,en,USD
|
||||
Plumecreed Mentor,BLB,Bloomburrow,228,normal,uncommon,1,95819,b1aa988f-547e-449a-9f1a-296c01d68d96,0.03,false,false,near_mint,en,USD
|
||||
"Baylen, the Haymaker",BLB,Bloomburrow,205,normal,rare,1,95889,00e93be2-e06b-4774-8ba5-ccf82a6da1d8,1.04,false,false,near_mint,en,USD
|
||||
Long River's Pull,BLB,Bloomburrow,58,normal,uncommon,1,95900,1c81d0fa-81a1-4f9b-a5fd-5a648fd01dea,0.23,false,false,near_mint,en,USD
|
||||
Bonecache Overseer,BLB,Bloomburrow,85,normal,uncommon,1,95944,82defb87-237f-4b77-9673-5bf00607148f,0.08,false,false,near_mint,en,USD
|
||||
Three Tree Scribe,BLB,Bloomburrow,199,normal,uncommon,1,95977,ea2ca1b3-4c1a-4be5-b321-f57db5ff0528,0.15,false,false,near_mint,en,USD
|
||||
Cruelclaw's Heist,BLB,Bloomburrow,88,normal,rare,1,96121,cab4539a-0157-4cbe-b50f-6e2575df74e9,0.48,false,false,near_mint,en,USD
|
||||
Manifold Mouse,BLB,Bloomburrow,143,normal,rare,1,95881,db3832b5-e83f-4569-bd49-fb7b86fa2d47,3.37,false,false,near_mint,en,USD
|
||||
Iridescent Vinelasher,BLB,Bloomburrow,99,normal,rare,1,95877,b2bc854c-4e72-48e0-a098-e3451d6e511d,1.11,false,false,near_mint,en,USD
|
||||
Daggerfang Duo,BLB,Bloomburrow,89,normal,common,1,96468,cea2bb34-e328-44fb-918a-72208c9457e4,0.03,false,false,near_mint,en,USD
|
||||
Stickytongue Sentinel,BLB,Bloomburrow,193,normal,common,1,96105,b5fa9651-b217-4f93-9c46-9bdb11feedcb,0.03,false,false,near_mint,en,USD
|
||||
Brave-Kin Duo,BLB,Bloomburrow,3,normal,common,1,95824,b8dd4693-424d-4d6e-86cf-24401a23d6b1,0.03,false,false,near_mint,en,USD
|
||||
Driftgloom Coyote,BLB,Bloomburrow,11,normal,uncommon,1,95969,d7ab2de3-3aea-461a-a74f-fb742cf8a198,0.03,false,false,near_mint,en,USD
|
||||
Rockface Village,BLB,Bloomburrow,259,normal,uncommon,1,95629,62799d24-39a6-4e66-8ac3-7cafa99e6e6d,0.48,false,false,near_mint,en,USD
|
||||
Flamecache Gecko,BLB,Bloomburrow,135,normal,uncommon,1,96142,fb8e7c97-8393-41b8-bb0b-3983dcc5e7f4,0.08,false,false,near_mint,en,USD
|
||||
Innkeeper's Talent,BLB,Bloomburrow,180,normal,rare,1,95954,941b0afc-0e8f-45f2-ae7f-07595e164611,19.36,false,false,near_mint,en,USD
|
||||
Repel Calamity,BLB,Bloomburrow,27,foil,uncommon,1,95834,d068192a-6270-4981-819d-4945fa4a2b83,0.08,false,false,near_mint,en,USD
|
||||
Galewind Moose,BLB,Bloomburrow,173,foil,uncommon,1,95871,58706bd8-558a-43b9-9f1e-c1ff0044203b,0.14,false,false,near_mint,en,USD
|
||||
Brave-Kin Duo,BLB,Bloomburrow,3,foil,common,1,95824,b8dd4693-424d-4d6e-86cf-24401a23d6b1,0.06,false,false,near_mint,en,USD
|
||||
Agate Assault,BLB,Bloomburrow,122,foil,common,1,96066,7dd9946b-515e-4e0d-9da2-711e126e9fa6,0.03,false,false,near_mint,en,USD
|
||||
Flamecache Gecko,BLB,Bloomburrow,135,foil,uncommon,1,96142,fb8e7c97-8393-41b8-bb0b-3983dcc5e7f4,0.12,false,false,near_mint,en,USD
|
||||
Rabid Gnaw,BLB,Bloomburrow,147,foil,uncommon,1,96014,2f815bae-820a-49f6-8eed-46f658e7b6ff,0.1,false,false,near_mint,en,USD
|
||||
Pond Prophet,BLB,Bloomburrow,229,foil,common,1,95861,fb959e74-61ea-453d-bb9f-ad0183c0e1b1,0.16,false,false,near_mint,en,USD
|
||||
Star Charter,BLB,Bloomburrow,33,foil,uncommon,1,95894,0e209237-00f7-4bf0-8287-ccde02ce8e8d,0.12,false,false,near_mint,en,USD
|
||||
Kindlespark Duo,BLB,Bloomburrow,142,foil,common,1,96096,a839fba3-1b66-4dd1-bf43-9b015b44fc81,0.07,false,false,near_mint,en,USD
|
||||
Crumb and Get It,BLB,Bloomburrow,8,foil,common,1,96259,3c7b3b25-d4b3-4451-9f5c-6eb369541175,0.04,false,false,near_mint,en,USD
|
||||
Peerless Recycling,BLB,Bloomburrow,188,foil,uncommon,1,95925,5f72466c-505b-4371-9366-0fde525a37e6,0.23,false,false,near_mint,en,USD
|
||||
Nocturnal Hunger,BLB,Bloomburrow,102,foil,common,1,96060,742c0409-9abd-4559-b52e-932cc90c531a,0.02,false,false,near_mint,en,USD
|
||||
Seedpod Squire,BLB,Bloomburrow,232,foil,common,1,95852,f3684577-51ce-490e-9b59-b19c733be466,0.03,false,false,near_mint,en,USD
|
||||
Nettle Guard,BLB,Bloomburrow,23,foil,common,1,95949,8c9c3cc3-2aa2-453e-a17c-2baeeaabe0a9,0.05,false,false,near_mint,en,USD
|
||||
Sazacap's Brew,BLB,Bloomburrow,151,foil,common,1,96330,6d963080-b3ec-467d-82f7-39db6ecd6bbc,0.05,false,false,near_mint,en,USD
|
||||
Waterspout Warden,BLB,Bloomburrow,80,foil,common,1,95909,35898b39-98e2-405b-8f18-0e054bd2c29e,0.04,false,false,near_mint,en,USD
|
||||
Mindwhisker,BLB,Bloomburrow,60,foil,uncommon,1,96099,aaa10f34-5bfd-4d87-8f07-58de3b0f5663,0.12,false,false,near_mint,en,USD
|
||||
Splash Portal,BLB,Bloomburrow,74,foil,uncommon,1,95958,adbaa356-28ba-487f-930a-a957d9960ab0,0.28,false,false,near_mint,en,USD
|
||||
Festival of Embers,BLB,Bloomburrow,134,foil,rare,1,96023,4433ee12-2013-4fdc-979f-ae065f63a527,0.2,false,false,near_mint,en,USD
|
||||
Brightblade Stoat,BLB,Bloomburrow,4,foil,uncommon,1,95882,df7fea2e-7414-4bc8-adb0-9342e174c009,0.11,false,false,near_mint,en,USD
|
||||
Mind Spiral,BLB,Bloomburrow,59,foil,common,1,96068,7e24fe6a-607b-49b8-9fca-cecb1e40de7f,0.04,false,false,near_mint,en,USD
|
||||
Rust-Shield Rampager,BLB,Bloomburrow,190,foil,common,1,96117,c96b01f5-83de-4237-a68d-f946c53e31a6,0.04,false,false,near_mint,en,USD
|
||||
Barkform Harvester,BLB,Bloomburrow,243,foil,common,1,95984,f77049a6-0f22-415b-bc89-20bcb32accf6,0.11,false,false,near_mint,en,USD
|
||||
Wax-Wane Witness,BLB,Bloomburrow,39,foil,common,1,95971,d90ea719-5320-46c6-a347-161853a14776,0.05,false,false,near_mint,en,USD
|
||||
Warren Elder,BLB,Bloomburrow,37,foil,common,1,96030,4bf20069-5a20-4f95-976b-6af2b69f3ad0,0.04,false,false,near_mint,en,USD
|
||||
Stickytongue Sentinel,BLB,Bloomburrow,193,foil,common,1,96105,b5fa9651-b217-4f93-9c46-9bdb11feedcb,0.05,false,false,near_mint,en,USD
|
||||
"Vren, the Relentless",BLB,Bloomburrow,239,foil,rare,1,95930,6506277d-f031-4db5-9d16-bf2389094785,0.71,false,false,near_mint,en,USD
|
||||
Three Tree Scribe,BLB,Bloomburrow,199,foil,uncommon,1,95977,ea2ca1b3-4c1a-4be5-b321-f57db5ff0528,0.2,false,false,near_mint,en,USD
|
||||
Glidedive Duo,BLB,Bloomburrow,96,foil,common,1,96026,4831e7ae-54e3-4bd9-b5af-52dc29f81715,0.03,false,false,near_mint,en,USD
|
||||
Bushy Bodyguard,BLB,Bloomburrow,166,foil,uncommon,1,95997,0de60cf7-fa82-4b6f-9f88-6590fba5c863,0.12,false,false,near_mint,en,USD
|
||||
Conduct Electricity,BLB,Bloomburrow,130,foil,common,1,95906,2f373dd6-2412-453c-85ba-10230dfe473a,0.03,false,false,near_mint,en,USD
|
||||
Daggerfang Duo,BLB,Bloomburrow,89,foil,common,1,96468,cea2bb34-e328-44fb-918a-72208c9457e4,0.07,false,false,near_mint,en,USD
|
||||
Shore Up,BLB,Bloomburrow,69,foil,common,1,96277,4dc3b49e-3674-494c-bdea-4374cefd10f4,0.13,false,false,near_mint,en,USD
|
||||
Hidden Grotto,BLB,Bloomburrow,254,foil,common,1,95918,4ba8f2e7-8357-4862-97dc-1942d066023a,0.17,false,false,near_mint,en,USD
|
||||
Cindering Cutthroat,BLB,Bloomburrow,208,foil,common,1,95820,b2ea10dd-21ea-4622-be27-79d03a802b85,0.01,false,false,near_mint,en,USD
|
||||
"Glarb, Calamity's Augur",BLB,Bloomburrow,215,foil,mythic,1,95864,ffc70b2d-5a3a-49ea-97db-175a62248302,4.3,false,false,near_mint,en,USD
|
||||
Kindlespark Duo,BLB,Bloomburrow,142,normal,common,5,96096,a839fba3-1b66-4dd1-bf43-9b015b44fc81,0.04,false,false,near_mint,en,USD
|
||||
Finch Formation,BLB,Bloomburrow,50,normal,common,2,95899,1c671eab-d1ef-4d79-94eb-8b85f0d18699,0.02,false,false,near_mint,en,USD
|
||||
Builder's Talent,BLB,Bloomburrow,5,normal,uncommon,2,96002,15fa581a-724e-4196-a9a3-ff84c54bdb7d,0.08,false,false,near_mint,en,USD
|
||||
Might of the Meek,BLB,Bloomburrow,144,normal,common,9,95627,509bf254-8a2b-4dfa-9ae5-386321b35e8b,0.09,false,false,near_mint,en,USD
|
||||
Nightwhorl Hermit,BLB,Bloomburrow,62,normal,common,3,95994,0928e04f-2568-41e8-b603-7a25cf5f94d0,0.02,false,false,near_mint,en,USD
|
||||
Fell,BLB,Bloomburrow,95,normal,uncommon,2,95830,c96ac326-de44-470b-a592-a4c2a052c091,0.3,false,false,near_mint,en,USD
|
||||
Sunshower Druid,BLB,Bloomburrow,195,normal,common,6,95630,7740abc5-54e1-478d-966e-0fa64e727995,0.04,false,false,near_mint,en,USD
|
||||
Wandertale Mentor,BLB,Bloomburrow,240,normal,uncommon,2,95808,8c399a55-d02e-41ed-b827-8784b738c118,0.09,false,false,near_mint,en,USD
|
||||
Thought-Stalker Warlock,BLB,Bloomburrow,118,normal,uncommon,2,96018,42e80284-d489-493b-ae92-95b742d07cb3,0.12,false,false,near_mint,en,USD
|
||||
Splash Portal,BLB,Bloomburrow,74,normal,uncommon,2,95958,adbaa356-28ba-487f-930a-a957d9960ab0,0.23,false,false,near_mint,en,USD
|
||||
Alania's Pathmaker,BLB,Bloomburrow,123,normal,common,7,96123,d3871fe6-e26e-4ab4-bd81-7e3c7b8135c1,0.02,false,false,near_mint,en,USD
|
||||
Head of the Homestead,BLB,Bloomburrow,216,normal,common,3,95762,2fc20157-edd3-484d-8864-925c071c0551,0.04,false,false,near_mint,en,USD
|
||||
Hidden Grotto,BLB,Bloomburrow,254,normal,common,4,95918,4ba8f2e7-8357-4862-97dc-1942d066023a,0.08,false,false,near_mint,en,USD
|
||||
Star Charter,BLB,Bloomburrow,33,normal,uncommon,3,95894,0e209237-00f7-4bf0-8287-ccde02ce8e8d,0.04,false,false,near_mint,en,USD
|
||||
War Squeak,BLB,Bloomburrow,160,normal,common,4,95999,105964a7-88b7-4340-aa66-e908189a3638,0.02,false,false,near_mint,en,USD
|
||||
Bellowing Crier,BLB,Bloomburrow,42,normal,common,2,96119,ca2215dd-6300-49cf-b9b2-3a840b786c31,0.04,false,false,near_mint,en,USD
|
||||
Cindering Cutthroat,BLB,Bloomburrow,208,normal,common,4,95820,b2ea10dd-21ea-4622-be27-79d03a802b85,0.02,false,false,near_mint,en,USD
|
||||
Intrepid Rabbit,BLB,Bloomburrow,17,normal,common,7,96276,4d70b99d-c8bf-4a56-8957-cf587fe60b81,0.03,false,false,near_mint,en,USD
|
||||
Carrot Cake,BLB,Bloomburrow,7,normal,common,3,95636,eb03bb4f-8b4b-417e-bfc6-294cd2186b2e,0.06,false,false,near_mint,en,USD
|
||||
Thought Shucker,BLB,Bloomburrow,77,normal,common,7,95916,44b0d83b-cc41-4f82-892c-ef6d3293228a,0.02,false,false,near_mint,en,USD
|
||||
Seasoned Warrenguard,BLB,Bloomburrow,30,normal,uncommon,2,96081,90873995-876f-4e89-8bc7-41a74f4d931f,0.09,false,false,near_mint,en,USD
|
||||
Junkblade Bruiser,BLB,Bloomburrow,220,normal,common,3,95810,918fd89b-5ab7-4ae2-920c-faca5e9da7b9,0.04,false,false,near_mint,en,USD
|
||||
Cache Grab,BLB,Bloomburrow,167,normal,common,2,95842,dfd977dc-a7c3-4d0a-aca7-b25bd154e963,0.08,false,false,near_mint,en,USD
|
||||
Lilypad Village,BLB,Bloomburrow,255,normal,uncommon,2,95631,7e95a7cc-ed77-4ca4-80db-61c0fc68bf50,0.14,false,false,near_mint,en,USD
|
||||
Agate-Blade Assassin,BLB,Bloomburrow,82,normal,common,5,96017,39ebb84a-1c52-4b07-9bd0-b360523b3a5b,0.03,false,false,near_mint,en,USD
|
||||
Repel Calamity,BLB,Bloomburrow,27,normal,uncommon,2,95834,d068192a-6270-4981-819d-4945fa4a2b83,0.07,false,false,near_mint,en,USD
|
||||
Hazel's Nocturne,BLB,Bloomburrow,97,normal,uncommon,2,96009,239363df-4de8-4b64-80fc-a1f4b5c36027,0.07,false,false,near_mint,en,USD
|
||||
Treeguard Duo,BLB,Bloomburrow,200,normal,common,4,96077,89c8456e-c971-42b7-abf3-ff5ae1320abe,0.01,false,false,near_mint,en,USD
|
||||
Calamitous Tide,BLB,Bloomburrow,43,normal,uncommon,2,96003,178bc8b2-ffa0-4549-aead-aacb3db3cf19,0.03,false,false,near_mint,en,USD
|
||||
Splash Lasher,BLB,Bloomburrow,73,normal,uncommon,2,95910,362ee125-35a0-46cd-a201-e6797d12d33a,0.04,false,false,near_mint,en,USD
|
||||
Blooming Blast,BLB,Bloomburrow,126,normal,uncommon,2,95996,0cd92a83-cec3-4085-a929-3f204e3e0140,0.06,false,false,near_mint,en,USD
|
||||
Sugar Coat,BLB,Bloomburrow,76,normal,uncommon,2,95887,fcacbe71-efb0-49e1-b2d0-3ee65ec6cf8b,0.05,false,false,near_mint,en,USD
|
||||
Dazzling Denial,BLB,Bloomburrow,45,normal,common,6,96369,8739f1ac-2e57-4b52-a7ff-cc8df5936aad,0.04,false,false,near_mint,en,USD
|
||||
Nettle Guard,BLB,Bloomburrow,23,normal,common,4,95949,8c9c3cc3-2aa2-453e-a17c-2baeeaabe0a9,0.03,false,false,near_mint,en,USD
|
||||
Raccoon Rallier,BLB,Bloomburrow,148,normal,common,5,96104,b5b5180f-5a1c-4df8-9019-195e65a50ce3,0.04,false,false,near_mint,en,USD
|
||||
High Stride,BLB,Bloomburrow,176,normal,common,8,96153,09c8cf4b-8e65-4a1c-b458-28b5ab56b390,0.04,false,false,near_mint,en,USD
|
||||
Otterball Antics,BLB,Bloomburrow,63,normal,uncommon,2,95913,3ff83ff7-e428-4ccc-8341-f223dab76bd1,0.1,false,false,near_mint,en,USD
|
||||
Frilled Sparkshooter,BLB,Bloomburrow,136,normal,common,7,95934,674bbd6d-e329-42cf-963d-88d1ce8fe51e,0.02,false,false,near_mint,en,USD
|
||||
Moonrise Cleric,BLB,Bloomburrow,226,normal,common,3,95767,35f2a71f-31e8-4b51-9dd4-51a5336b3b86,0.04,false,false,near_mint,en,USD
|
||||
Wax-Wane Witness,BLB,Bloomburrow,39,normal,common,3,95971,d90ea719-5320-46c6-a347-161853a14776,0.02,false,false,near_mint,en,USD
|
||||
Pearl of Wisdom,BLB,Bloomburrow,64,normal,common,7,95625,13cb9575-1138-4f99-8e90-0eaf00bdf4a1,0.01,false,false,near_mint,en,USD
|
||||
Run Away Together,BLB,Bloomburrow,67,normal,common,3,95799,7cb7ec70-a5a4-4188-ba1a-e88b81bdbad0,0.04,false,false,near_mint,en,USD
|
||||
Early Winter,BLB,Bloomburrow,93,normal,common,2,95626,5030e6ac-211d-4145-8c87-998a8351a467,0.05,false,false,near_mint,en,USD
|
||||
Three Tree Rootweaver,BLB,Bloomburrow,198,normal,common,2,96469,d1ab6e14-26e0-4174-b5c6-bc0f5c26b177,0.04,false,false,near_mint,en,USD
|
||||
Mudflat Village,BLB,Bloomburrow,257,normal,uncommon,2,95628,53ec4ad3-9cf0-4f1b-a9db-d63feee594ab,0.24,false,false,near_mint,en,USD
|
||||
Starlit Soothsayer,BLB,Bloomburrow,115,normal,common,6,95895,184c1eca-2991-438f-b5d2-cd2529b9c9b4,0.03,false,false,near_mint,en,USD
|
||||
Hop to It,BLB,Bloomburrow,16,normal,uncommon,2,95851,ee7207f8-5daa-42af-aeea-7a489047110b,0.07,false,false,near_mint,en,USD
|
||||
Psychic Whorl,BLB,Bloomburrow,105,normal,common,5,96127,df900308-8432-4a0a-be21-17482026012b,0.04,false,false,near_mint,en,USD
|
||||
Barkform Harvester,BLB,Bloomburrow,243,normal,common,4,95984,f77049a6-0f22-415b-bc89-20bcb32accf6,0.06,false,false,near_mint,en,USD
|
||||
Daring Waverider,BLB,Bloomburrow,44,normal,uncommon,2,95896,19422406-0c1a-497e-bed1-708bc556491a,0.06,false,false,near_mint,en,USD
|
||||
Plumecreed Escort,BLB,Bloomburrow,65,normal,uncommon,2,95983,f71320ed-2f30-49ce-bcb0-19aebba3f0e8,0.05,false,false,near_mint,en,USD
|
||||
Parting Gust,BLB,Bloomburrow,24,normal,uncommon,2,95744,1086e826-94b8-4398-8a38-d8eacca56a43,0.38,false,false,near_mint,en,USD
|
||||
Veteran Guardmouse,BLB,Bloomburrow,237,normal,common,3,95771,3db43c46-b616-4ef8-80ed-0fab345ab3d0,0.01,false,false,near_mint,en,USD
|
||||
Dire Downdraft,BLB,Bloomburrow,46,normal,common,6,96526,f1931f22-974c-43ad-911e-684bf3f9995d,0.02,false,false,near_mint,en,USD
|
||||
Waterspout Warden,BLB,Bloomburrow,80,normal,common,4,95909,35898b39-98e2-405b-8f18-0e054bd2c29e,0.01,false,false,near_mint,en,USD
|
||||
Lupinflower Village,BLB,Bloomburrow,256,normal,uncommon,2,95634,8ab9d56f-9178-4ec9-a5f6-b934f50d8d9d,0.1,false,false,near_mint,en,USD
|
||||
Heartfire Hero,BLB,Bloomburrow,138,normal,uncommon,2,95870,48ace959-66b2-40c8-9bff-fd7ed9c99a82,2.1,false,false,near_mint,en,USD
|
||||
Peerless Recycling,BLB,Bloomburrow,188,normal,uncommon,2,95925,5f72466c-505b-4371-9366-0fde525a37e6,0.1,false,false,near_mint,en,USD
|
||||
Pond Prophet,BLB,Bloomburrow,229,normal,common,4,95861,fb959e74-61ea-453d-bb9f-ad0183c0e1b1,0.09,false,false,near_mint,en,USD
|
||||
Crumb and Get It,BLB,Bloomburrow,8,normal,common,2,96259,3c7b3b25-d4b3-4451-9f5c-6eb369541175,0.03,false,false,near_mint,en,USD
|
||||
Wildfire Howl,BLB,Bloomburrow,162,normal,uncommon,2,96059,7392d397-9836-4df2-944d-c930c9566811,0.05,false,false,near_mint,en,USD
|
||||
Bark-Knuckle Boxer,BLB,Bloomburrow,164,normal,uncommon,2,95921,582637a9-6aa0-4824-bed7-d5fc91bda35e,0.03,false,false,near_mint,en,USD
|
||||
Ruthless Negotiation,BLB,Bloomburrow,108,normal,uncommon,2,95828,c7f4360c-8d68-4058-b9ec-da9948cb060d,0.1,false,false,near_mint,en,USD
|
||||
Three Tree Mascot,FDN,Foundations,682,normal,common,3,100412,40b8bf3a-1cb5-4ce2-ac25-9410f17130de,0.11,false,false,near_mint,en,USD
|
||||
Tempest Angler,BLB,Bloomburrow,235,normal,common,2,95803,850daae4-f0b7-4604-95e7-ad044ec165c3,0.04,false,false,near_mint,en,USD
|
||||
Starscape Cleric,BLB,Bloomburrow,116,normal,uncommon,2,96037,53a938a7-0154-4350-87cb-00da24ec3824,0.62,false,false,near_mint,en,USD
|
||||
Wick's Patrol,BLB,Bloomburrow,121,normal,uncommon,3,95926,5fa0c53d-fe7b-4b8b-ad81-7967ca318ff7,0.07,false,false,near_mint,en,USD
|
||||
Fireglass Mentor,BLB,Bloomburrow,213,normal,uncommon,2,95823,b78fbaa3-c580-4290-9c28-b74169aab2fc,0.08,false,false,near_mint,en,USD
|
||||
Steampath Charger,BLB,Bloomburrow,153,normal,common,2,95890,03bf1296-e347-4070-8c6f-5c362c2f9364,0.03,false,false,near_mint,en,USD
|
||||
Whiskerquill Scribe,BLB,Bloomburrow,161,normal,common,2,96124,da653996-9bd4-40bd-afb4-48c7e070a269,0.01,false,false,near_mint,en,USD
|
||||
Lilysplash Mentor,BLB,Bloomburrow,222,normal,uncommon,3,95789,64de7b1f-a03e-4407-91f1-e108a2f26735,0.12,false,false,near_mint,en,USD
|
||||
Roughshod Duo,BLB,Bloomburrow,150,normal,common,3,96343,78cdcfb9-a247-4c2d-a098-5b57570f8cd5,0.03,false,false,near_mint,en,USD
|
||||
Bonebind Orator,BLB,Bloomburrow,84,normal,common,3,96535,faf226fa-ca09-4468-8804-87b2a7de2c66,0.02,false,false,near_mint,en,USD
|
||||
Agate Assault,BLB,Bloomburrow,122,normal,common,2,96066,7dd9946b-515e-4e0d-9da2-711e126e9fa6,0.02,false,false,near_mint,en,USD
|
||||
Nocturnal Hunger,BLB,Bloomburrow,102,normal,common,3,96060,742c0409-9abd-4559-b52e-932cc90c531a,0.02,false,false,near_mint,en,USD
|
||||
Jolly Gerbils,BLB,Bloomburrow,19,normal,uncommon,2,96167,0eab51d6-ba17-4a8c-8834-25db363f2b6b,0.04,false,false,near_mint,en,USD
|
||||
Downwind Ambusher,BLB,Bloomburrow,92,normal,uncommon,2,95920,55cfd628-933a-4d3d-b2e5-70bc86960d1c,0.02,false,false,near_mint,en,USD
|
||||
Scales of Shale,BLB,Bloomburrow,110,normal,common,2,95955,9ae14276-dbbd-4257-80e9-accd6c19f5b2,0.02,false,false,near_mint,en,USD
|
||||
Treetop Sentries,BLB,Bloomburrow,201,normal,common,4,95974,e16d4d6e-1fe5-4ff6-9877-8c849a24f5e0,0.03,false,false,near_mint,en,USD
|
||||
Seedpod Squire,BLB,Bloomburrow,232,normal,common,4,95852,f3684577-51ce-490e-9b59-b19c733be466,0.01,false,false,near_mint,en,USD
|
||||
Savor,BLB,Bloomburrow,109,normal,common,4,96178,1397f689-dca1-4d35-864b-92c5606afb9a,0.04,false,false,near_mint,en,USD
|
||||
Polliwallop,BLB,Bloomburrow,189,normal,common,2,95935,6bc4963c-d90b-4588-bdb7-85956e42a623,0.03,false,false,near_mint,en,USD
|
||||
Sonar Strike,BLB,Bloomburrow,32,normal,common,2,96093,a50da179-751f-47a8-a547-8c4a291ed381,0.02,false,false,near_mint,en,USD
|
||||
Uncharted Haven,FDN,Foundations,564,normal,common,3,97170,172cd5b7-98fc-4add-b858-a0b3dfb75c19,0.14,false,false,near_mint,en,USD
|
||||
Teapot Slinger,BLB,Bloomburrow,157,normal,uncommon,2,96015,30506844-349f-4b68-8cc1-d028c1611cc7,0.06,false,false,near_mint,en,USD
|
||||
Harvestrite Host,BLB,Bloomburrow,15,normal,uncommon,2,95915,41762689-0c13-4d45-9d81-ba2afad980f8,0.07,false,false,near_mint,en,USD
|
||||
Spellgyre,BLB,Bloomburrow,72,normal,uncommon,2,96139,f6f6620a-1d40-429d-9a0c-aaeb62adaa71,0.08,false,false,near_mint,en,USD
|
||||
Oakhollow Village,BLB,Bloomburrow,258,normal,uncommon,2,95624,0d49b016-b02b-459f-85e9-c04f6bdcb94e,0.35,false,false,near_mint,en,USD
|
||||
Bumbleflower's Sharepot,BLB,Bloomburrow,244,normal,common,2,95924,5f0affd5-5dcd-4dd1-a694-37a9aedf4084,0.02,false,false,near_mint,en,USD
|
||||
Overprotect,BLB,Bloomburrow,185,normal,uncommon,2,95891,079e979f-b618-4625-989c-e0ea5b61ed8a,0.55,false,false,near_mint,en,USD
|
||||
Heaped Harvest,BLB,Bloomburrow,175,normal,common,3,96255,3b5349db-0e0a-4b15-886e-0db403ef49cb,0.1,false,false,near_mint,en,USD
|
||||
Flowerfoot Swordmaster,BLB,Bloomburrow,14,normal,uncommon,2,95812,97ff118f-9c3c-43a2-8085-980c7fe7d227,0.15,false,false,near_mint,en,USD
|
||||
Banishing Light,BLB,Bloomburrow,1,normal,common,6,96011,25a06f82-ebdb-4dd6-bfe8-958018ce557c,0.04,false,false,near_mint,en,USD
|
||||
Sazacap's Brew,BLB,Bloomburrow,151,normal,common,3,96330,6d963080-b3ec-467d-82f7-39db6ecd6bbc,0.05,false,false,near_mint,en,USD
|
||||
Diresight,BLB,Bloomburrow,91,normal,common,3,95985,fada29c0-5293-40a4-b36d-d073ee99e650,0.1,false,false,near_mint,en,USD
|
||||
Gossip's Talent,BLB,Bloomburrow,51,normal,uncommon,2,95961,b299889a-03d6-4659-b0e1-f0830842e40f,0.18,false,false,near_mint,en,USD
|
||||
Fountainport Bell,BLB,Bloomburrow,245,normal,common,3,96094,a5c94bc0-a49d-451b-8e8d-64d46b8b8603,0.04,false,false,near_mint,en,USD
|
||||
Reptilian Recruiter,BLB,Bloomburrow,149,normal,uncommon,2,96072,81dec453-c9d7-42cb-980a-c82f82bede76,0.02,false,false,near_mint,en,USD
|
||||
Thistledown Players,BLB,Bloomburrow,35,normal,common,2,95960,afa8d83f-8586-4127-8b55-9715e9547488,0.01,false,false,near_mint,en,USD
|
||||
Clifftop Lookout,BLB,Bloomburrow,168,normal,uncommon,2,95931,662d3bcc-65f3-4c69-8ea1-446870a1193d,0.16,false,false,near_mint,en,USD
|
||||
Rust-Shield Rampager,BLB,Bloomburrow,190,normal,common,2,96117,c96b01f5-83de-4237-a68d-f946c53e31a6,0.02,false,false,near_mint,en,USD
|
||||
Consumed by Greed,BLB,Bloomburrow,87,normal,uncommon,2,95884,e50acc41-3517-42db-b1d3-1bdfd7294d84,0.09,false,false,near_mint,en,USD
|
||||
Rabbit Response,BLB,Bloomburrow,26,normal,common,2,96114,c4ded450-346d-4917-917a-b62bc0267509,0.02,false,false,near_mint,en,USD
|
||||
Corpseberry Cultivator,BLB,Bloomburrow,210,normal,common,2,95829,c911a759-ed7b-452b-88a3-663478357610,0.02,false,false,near_mint,en,USD
|
||||
Mind Drill Assailant,BLB,Bloomburrow,225,normal,common,2,95783,507ba708-ca9b-453e-b4c2-23b6650eb5a8,0.05,false,false,near_mint,en,USD
|
||||
Hazardroot Herbalist,BLB,Bloomburrow,174,normal,uncommon,2,96130,e2882982-b3a3-4762-a550-6b82db1038e8,0.04,false,false,near_mint,en,USD
|
||||
Dewdrop Cure,BLB,Bloomburrow,10,normal,uncommon,2,95932,666aefc2-44e0-4c27-88d5-7906f245a71f,0.13,false,false,near_mint,en,USD
|
||||
Valley Rally,BLB,Bloomburrow,159,normal,uncommon,2,95878,b6178258-1ad6-4122-a56f-6eb7d0611e84,0.04,false,false,near_mint,en,USD
|
||||
Blacksmith's Talent,BLB,Bloomburrow,125,normal,uncommon,2,96029,4bb318fa-481d-40a7-978e-f01b49101ae0,0.17,false,false,near_mint,en,USD
|
||||
Pileated Provisioner,BLB,Bloomburrow,25,normal,common,2,96102,ae442cd6-c4df-4aad-9b1d-ccd936c5ec96,0.02,false,false,near_mint,en,USD
|
||||
Short Bow,BLB,Bloomburrow,248,normal,uncommon,2,96281,51d8b72b-fa8f-48d3-bddc-d3ce9b8ba2ea,0.15,false,false,near_mint,en,USD
|
||||
Warren Elder,BLB,Bloomburrow,37,normal,common,2,96030,4bf20069-5a20-4f95-976b-6af2b69f3ad0,0.03,false,false,near_mint,en,USD
|
|
88
app/data/test_data/tdmtest.csv
Normal file
88
app/data/test_data/tdmtest.csv
Normal file
@ -0,0 +1,88 @@
|
||||
Name,Set code,Set name,Collector number,Foil,Rarity,Quantity,ManaBox ID,Scryfall ID,Purchase price,Misprint,Altered,Condition,Language,Purchase price currency
|
||||
Sunpearl Kirin,TDM,Tarkir: Dragonstorm,29,foil,uncommon,1,104470,18292b9c-0f42-4ce2-8b85-35d06cf45a63,1.24,false,false,near_mint,en,USD
|
||||
Humbling Elder,TDM,Tarkir: Dragonstorm,48,foil,common,1,104562,3a84c3f8-0030-4653-880e-b2d19272f5fa,0.06,false,false,near_mint,en,USD
|
||||
Fortress Kin-Guard,TDM,Tarkir: Dragonstorm,12,foil,common,1,104900,b647a018-1d70-43a1-a265-928bcd863689,0.04,false,false,near_mint,en,USD
|
||||
Wild Ride,TDM,Tarkir: Dragonstorm,132,foil,common,1,105154,abc8c6f5-6135-428e-8476-1751f82623f9,0.14,false,false,near_mint,en,USD
|
||||
Jade-Cast Sentinel,TDM,Tarkir: Dragonstorm,243,foil,common,1,104293,516ce5fa-bd00-429b-ba22-b38c7dd9306c,0.07,false,false,near_mint,en,USD
|
||||
Sibsig Appraiser,TDM,Tarkir: Dragonstorm,56,foil,common,1,105135,670c5b96-bac6-449b-a2bd-cb43750d3911,0.04,false,false,near_mint,en,USD
|
||||
Channeled Dragonfire,TDM,Tarkir: Dragonstorm,102,foil,uncommon,1,104499,24204881-690c-4043-8771-20cb93385072,0.07,false,false,near_mint,en,USD
|
||||
Meticulous Artisan,TDM,Tarkir: Dragonstorm,112,foil,common,1,104912,baf4c9dd-0546-41ac-a7ba-0bc312fef31e,0.03,false,false,near_mint,en,USD
|
||||
Kishla Trawlers,TDM,Tarkir: Dragonstorm,50,foil,uncommon,1,104472,190fbc55-e8e9-4077-9532-1de7406baabf,0.08,false,false,near_mint,en,USD
|
||||
Rite of Renewal,TDM,Tarkir: Dragonstorm,153,foil,uncommon,1,104390,f737698a-d934-4851-b238-828959ef4835,0.07,false,false,near_mint,en,USD
|
||||
Twin Bolt,TDM,Tarkir: Dragonstorm,128,foil,common,1,105137,688d8e93-d071-4089-9ef9-565ac4ae9ae0,0.04,false,false,near_mint,en,USD
|
||||
Heritage Reclamation,TDM,Tarkir: Dragonstorm,145,foil,common,1,104636,4f8fee37-a050-4329-8b10-46d150e7a95e,0.33,false,false,near_mint,en,USD
|
||||
Sarkhan's Resolve,TDM,Tarkir: Dragonstorm,158,foil,common,1,104952,cae56fef-b661-4bc5-b9a1-3871ae06e491,0.04,false,false,near_mint,en,USD
|
||||
Delta Bloodflies,TDM,Tarkir: Dragonstorm,77,foil,common,1,104457,119bb72d-aed9-47dc-9285-7bc836cc3776,0.05,false,false,near_mint,en,USD
|
||||
Dirgur Island Dragon // Skimming Strike,TDM,Tarkir: Dragonstorm,40,foil,common,1,104342,b1d21a9a-6b0c-4fbc-a427-81be885d326b,0.16,false,false,near_mint,en,USD
|
||||
Kheru Goldkeeper,TDM,Tarkir: Dragonstorm,199,foil,uncommon,1,104798,8d11183a-57f5-4ddb-8a6e-15fff704b114,0.37,false,false,near_mint,en,USD
|
||||
Rainveil Rejuvenator,TDM,Tarkir: Dragonstorm,152,foil,uncommon,1,105148,9bc5c316-6a41-48ba-864b-da3030dd3e0e,0.12,false,false,near_mint,en,USD
|
||||
Rebellious Strike,TDM,Tarkir: Dragonstorm,20,foil,common,1,104949,c9bafe19-3bd6-4da0-b3e5-e0b89262504c,0.06,false,false,near_mint,en,USD
|
||||
Jeskai Brushmaster,TDM,Tarkir: Dragonstorm,195,foil,uncommon,1,104526,2eb06c36-cf7e-47a9-819e-adfc54284153,0.09,false,false,near_mint,en,USD
|
||||
Highspire Bell-Ringer,TDM,Tarkir: Dragonstorm,47,foil,common,1,105020,e75dccf7-2894-4c4a-b516-3eee73acddd3,0.06,false,false,near_mint,en,USD
|
||||
Sagu Pummeler,TDM,Tarkir: Dragonstorm,156,foil,common,1,105169,def9cb5b-4062-481e-b682-3a30443c2e56,0.03,false,false,near_mint,en,USD
|
||||
Roamer's Routine,TDM,Tarkir: Dragonstorm,154,foil,common,1,104396,fb8c2d5c-ba0c-4d50-8898-5c6574b1e974,0.13,false,false,near_mint,en,USD
|
||||
Monastery Messenger,TDM,Tarkir: Dragonstorm,208,foil,common,1,104443,0c9eeced-6464-41f0-bbea-05b3af4cc005,0.04,false,false,near_mint,en,USD
|
||||
Equilibrium Adept,TDM,Tarkir: Dragonstorm,106,foil,uncommon,1,104335,a4ba6d74-c6be-4a5e-8859-b791bb6b8f51,0.07,false,false,near_mint,en,USD
|
||||
Dispelling Exhale,TDM,Tarkir: Dragonstorm,41,foil,common,1,104477,1c9af3f1-711e-42ae-803a-1100eba3fb13,0.21,false,false,near_mint,en,USD
|
||||
Nightblade Brigade,TDM,Tarkir: Dragonstorm,85,foil,common,1,105134,648debd9-d4cf-4788-8882-f1601a3d87f5,0.08,false,false,near_mint,en,USD
|
||||
Tranquil Cove,TDM,Tarkir: Dragonstorm,270,foil,common,1,104249,1c4efa6c-4f29-41cd-a728-bf0e479ace05,0.06,false,false,near_mint,en,USD
|
||||
Rugged Highlands,TDM,Tarkir: Dragonstorm,265,foil,common,1,104267,31261eca-28ad-407c-84ef-0c124d0d7451,0.07,false,false,near_mint,en,USD
|
||||
Swiftwater Cliffs,TDM,Tarkir: Dragonstorm,268,foil,common,1,104361,ca53fb19-b8ca-485b-af1a-5117ae54bfe3,0.11,false,false,near_mint,en,USD
|
||||
Dismal Backwater,TDM,Tarkir: Dragonstorm,254,foil,common,1,104238,082b52c9-c46e-44d3-b723-546ba528e07b,0.06,false,false,near_mint,en,USD
|
||||
Wind-Scarred Crag,TDM,Tarkir: Dragonstorm,271,foil,common,1,104286,4912e4d0-b16a-4aa6-a583-3430d26bd591,0.05,false,false,near_mint,en,USD
|
||||
Jungle Hollow,TDM,Tarkir: Dragonstorm,258,foil,common,1,104375,ea13440b-3f7b-4182-9541-27c1fa3121e5,0.07,false,false,near_mint,en,USD
|
||||
Plains,TDM,Tarkir: Dragonstorm,272,foil,common,1,104240,0d0f1dd6-9564-4adc-af7d-f83252e8581a,0.39,false,false,near_mint,en,USD
|
||||
Purging Stormbrood // Absorb Essence,TDM,Tarkir: Dragonstorm,315,foil,uncommon,1,104395,fb293f4f-9ba2-48f5-a4fb-d902aa531bfc,0.18,false,false,near_mint,en,USD
|
||||
Barrensteppe Siege,TDM,Tarkir: Dragonstorm,384,foil,rare,1,104002,c09d4015-f101-4529-a603-c66192dcfd92,1.68,false,false,near_mint,en,USD
|
||||
United Battlefront,TDM,Tarkir: Dragonstorm,32,foil,rare,1,104370,dff398be-4ba4-4976-9acc-be99d2e07a61,0.8,false,false,near_mint,en,USD
|
||||
Cori-Steel Cutter,TDM,Tarkir: Dragonstorm,103,foil,rare,1,104608,490eb213-9ae2-4b45-abec-6f1dfc83792a,15.2,false,false,near_mint,en,USD
|
||||
Plains,TDM,Tarkir: Dragonstorm,272,normal,common,1,104240,0d0f1dd6-9564-4adc-af7d-f83252e8581a,0.52,false,false,near_mint,en,USD
|
||||
Skirmish Rhino,TDM,Tarkir: Dragonstorm,224,normal,uncommon,1,103992,4a2e9ba1-c254-41e3-9845-4e81f9fec38d,0.18,false,false,near_mint,en,USD
|
||||
Kheru Goldkeeper,TDM,Tarkir: Dragonstorm,199,normal,uncommon,1,104798,8d11183a-57f5-4ddb-8a6e-15fff704b114,0.17,false,false,near_mint,en,USD
|
||||
Hardened Tactician,TDM,Tarkir: Dragonstorm,191,normal,uncommon,1,104780,86b225cb-5c45-4da1-a64e-b04091e483e8,0.19,false,false,near_mint,en,USD
|
||||
Auroral Procession,TDM,Tarkir: Dragonstorm,169,normal,uncommon,1,104701,672f94ad-65d6-4c7d-925d-165ef264626f,0.2,false,false,near_mint,en,USD
|
||||
Barrensteppe Siege,TDM,Tarkir: Dragonstorm,171,normal,rare,1,103989,2556a35b-2229-42c7-8cb3-c8c668403dd2,0.46,false,false,near_mint,en,USD
|
||||
Heritage Reclamation,TDM,Tarkir: Dragonstorm,145,normal,common,1,104636,4f8fee37-a050-4329-8b10-46d150e7a95e,0.19,false,false,near_mint,en,USD
|
||||
Windcrag Siege,TDM,Tarkir: Dragonstorm,235,normal,rare,1,104534,31a8329b-23a1-4c49-a579-a5da8d01435a,1.97,false,false,near_mint,en,USD
|
||||
Ambling Stormshell,TDM,Tarkir: Dragonstorm,37,normal,rare,1,104942,c74d4a57-0f66-4965-9ed7-f88a08aa1d15,0.18,false,false,near_mint,en,USD
|
||||
"Narset, Jeskai Waymaster",TDM,Tarkir: Dragonstorm,209,normal,rare,1,103995,6b77cbc1-dbc8-44d9-aa29-15cbb19afecd,0.21,false,false,near_mint,en,USD
|
||||
Revival of the Ancestors,TDM,Tarkir: Dragonstorm,218,normal,rare,1,105074,fd742ff5-f0ea-4f4b-911e-4c09e2154dba,0.14,false,false,near_mint,en,USD
|
||||
Dragonback Assault,TDM,Tarkir: Dragonstorm,179,normal,mythic,1,104985,d54cc838-d79d-433a-99fb-d6e4d1c1431d,3.49,false,false,near_mint,en,USD
|
||||
Call the Spirit Dragons,TDM,Tarkir: Dragonstorm,174,normal,mythic,1,104888,b1ad91db-5f16-4392-baf1-f8400ec11e0a,3.99,false,false,near_mint,en,USD
|
||||
Flamehold Grappler,TDM,Tarkir: Dragonstorm,185,normal,rare,1,104958,cc8443a6-282f-4218-9dc8-144b5570d891,0.26,false,false,near_mint,en,USD
|
||||
New Way Forward,TDM,Tarkir: Dragonstorm,211,normal,rare,1,104996,d9d48f9e-79f0-478c-9db0-ff7ac4a8f401,0.2,false,false,near_mint,en,USD
|
||||
Eshki Dragonclaw,TDM,Tarkir: Dragonstorm,182,normal,rare,1,104445,0d369c44-78ee-4f3c-bf2b-cddba7fe26d4,0.22,false,false,near_mint,en,USD
|
||||
Warden of the Grove,TDM,Tarkir: Dragonstorm,166,normal,rare,1,104498,2414db96-0e2b-4f7c-9b97-41f8e310b752,0.94,false,false,near_mint,en,USD
|
||||
United Battlefront,TDM,Tarkir: Dragonstorm,32,normal,rare,1,104370,dff398be-4ba4-4976-9acc-be99d2e07a61,0.58,false,false,near_mint,en,USD
|
||||
Dalkovan Encampment,TDM,Tarkir: Dragonstorm,253,normal,rare,1,104822,98ad5f0c-8775-4e89-8e92-84a6ade93e35,0.3,false,false,near_mint,en,USD
|
||||
Marang River Regent // Coil and Catch,TDM,Tarkir: Dragonstorm,51,normal,rare,1,104393,f890bdc7-32e6-4492-bac7-7cabf54a8bfd,2.87,false,false,near_mint,en,USD
|
||||
Sage of the Skies,TDM,Tarkir: Dragonstorm,22,normal,rare,1,104710,6ade6918-6d1d-448d-ab56-93996051e9a9,0.28,false,false,near_mint,en,USD
|
||||
Tersa Lightshatter,TDM,Tarkir: Dragonstorm,127,normal,rare,1,104825,99e96b34-b1c4-4647-a38e-2cf1aedaaace,1.92,false,false,near_mint,en,USD
|
||||
Voice of Victory,TDM,Tarkir: Dragonstorm,33,normal,rare,1,104377,ec3de5f4-bb55-4ab9-995f-f3e0dc22c1bb,10.45,false,false,near_mint,en,USD
|
||||
Yathan Roadwatcher,TDM,Tarkir: Dragonstorm,236,normal,rare,1,104800,8e77339b-dd82-481c-9ee2-4156ca69ad35,0.15,false,false,near_mint,en,USD
|
||||
Great Arashin City,TDM,Tarkir: Dragonstorm,257,normal,rare,1,105033,ecba23b6-9f3a-431e-bc22-f1fb04d27b68,0.31,false,false,near_mint,en,USD
|
||||
"Taigam, Master Opportunist",TDM,Tarkir: Dragonstorm,60,normal,mythic,1,104320,8693d631-05f6-414d-9e49-6385746e8960,1.69,false,false,near_mint,en,USD
|
||||
Temur Battlecrier,TDM,Tarkir: Dragonstorm,228,normal,rare,1,104309,72184791-0767-4108-920c-763e92dae2d4,0.74,false,false,near_mint,en,USD
|
||||
Fangkeeper's Familiar,TDM,Tarkir: Dragonstorm,183,normal,rare,1,104696,655fa2e1-3e1c-424c-b17a-daa7b8fface4,0.27,false,false,near_mint,en,USD
|
||||
Nature's Rhythm,TDM,Tarkir: Dragonstorm,150,normal,rare,1,104460,1397d904-c51d-451e-8505-7f3118acc1f6,3.21,false,false,near_mint,en,USD
|
||||
Stillness in Motion,TDM,Tarkir: Dragonstorm,59,normal,rare,1,104864,a6289251-17e4-4987-96b9-2fb1a8f90e2a,0.18,false,false,near_mint,en,USD
|
||||
Stadium Headliner,TDM,Tarkir: Dragonstorm,122,normal,rare,1,104552,37d4ab2a-a06a-4768-b5e1-e1def957d7f4,0.46,false,false,near_mint,en,USD
|
||||
Sinkhole Surveyor,TDM,Tarkir: Dragonstorm,93,normal,rare,1,104551,37cb5599-7d2c-48e9-978b-902a01a74bde,0.23,false,false,near_mint,en,USD
|
||||
Naga Fleshcrafter,TDM,Tarkir: Dragonstorm,52,normal,rare,1,104675,5df17423-9fdd-4432-8660-1d267c685595,0.31,false,false,near_mint,en,USD
|
||||
Herd Heirloom,TDM,Tarkir: Dragonstorm,144,normal,rare,1,104873,a88c7713-b3a9-4685-b1d3-623d35b62365,4.48,false,false,near_mint,en,USD
|
||||
Severance Priest,TDM,Tarkir: Dragonstorm,222,normal,rare,1,104917,bc779a1b-128c-4c74-bebd-bdb687867f68,0.21,false,false,near_mint,en,USD
|
||||
Dracogenesis,TDM,Tarkir: Dragonstorm,105,normal,mythic,1,104241,0d5674f9-22b2-45f9-902d-4fd245485c60,15.05,false,false,near_mint,en,USD
|
||||
Kishla Village,TDM,Tarkir: Dragonstorm,259,normal,rare,1,104840,9f0ff90d-7312-44df-afc5-29c768fa7758,0.3,false,false,near_mint,en,USD
|
||||
"Sarkhan, Dragon Ascendant",TDM,Tarkir: Dragonstorm,302,normal,rare,1,103994,57c03255-e3dc-44c2-982b-7efa188280df,0.49,false,false,near_mint,en,USD
|
||||
Bloomvine Regent // Claim Territory,TDM,Tarkir: Dragonstorm,381,normal,rare,1,104237,081f2de5-251a-41c9-a62f-11487f54d355,1.97,false,false,near_mint,en,USD
|
||||
Kheru Goldkeeper,TDM,Tarkir: Dragonstorm,313,normal,uncommon,1,105150,9d85ba44-8f29-4c49-b77f-8a6692d23c8c,0.45,false,false,near_mint,en,USD
|
||||
Boulderborn Dragon,TDM,Tarkir: Dragonstorm,323,normal,common,1,104326,970e11f0-337a-46b5-9bff-4bcb7843ed3a,0.1,false,false,near_mint,en,USD
|
||||
Sagu Wildling // Roost Seek,TDM,Tarkir: Dragonstorm,306,normal,common,1,104903,b72ee8f9-5e79-4f77-ae7e-e4c274f78187,0.11,false,false,near_mint,en,USD
|
||||
Tempest Hawk,TDM,Tarkir: Dragonstorm,31,normal,common,1,104587,422f9453-ab12-4e3c-8c51-be87391395a1,0.66,false,false,near_mint,en,USD
|
||||
Eshki Dragonclaw,TDM,Tarkir: Dragonstorm,356,normal,rare,1,104877,aafaa59e-87e1-4953-8c04-8e7a3a509827,0.44,false,false,near_mint,en,USD
|
||||
Lotuslight Dancers,TDM,Tarkir: Dragonstorm,363,normal,rare,1,104751,79dc69dc-6245-43fc-95a2-85b2c2957182,0.32,false,false,near_mint,en,USD
|
||||
Rakshasa's Bargain,TDM,Tarkir: Dragonstorm,214,normal,uncommon,1,104299,5c409f4f-3b2c-4c33-b850-55b2a46f51ca,0.32,false,false,near_mint,en,USD
|
||||
Glacial Dragonhunt,TDM,Tarkir: Dragonstorm,188,normal,uncommon,1,104814,95994c88-e404-4a4f-8be6-b99d703d4609,0.1,false,false,near_mint,en,USD
|
||||
"Elspeth, Storm Slayer",TDM,Tarkir: Dragonstorm,11,normal,mythic,1,104311,73a065e3-b530-4e62-ab3c-4f6f908184ec,39.69,false,false,near_mint,en,USD
|
||||
Host of the Hereafter,TDM,Tarkir: Dragonstorm,193,normal,uncommon,2,104448,0f182957-8133-45a7-80a3-1944bead4d43,0.16,false,false,near_mint,en,USD
|
||||
Sunset Strikemaster,TDM,Tarkir: Dragonstorm,126,normal,uncommon,2,104394,f8f1a2f2-526d-4b2c-985b-0acfdc21a2ee,0.17,false,false,near_mint,en,USD
|
|
58
app/main.py
58
app/main.py
@ -10,6 +10,7 @@ from pathlib import Path
|
||||
from app.routes import routes
|
||||
from app.db.database import init_db, SessionLocal
|
||||
from app.services.service_manager import ServiceManager
|
||||
from app.models.tcgplayer_products import MostRecentTCGPlayerPrice
|
||||
|
||||
# Configure logging
|
||||
log_file = Path("app.log")
|
||||
@ -30,7 +31,7 @@ file_handler.setFormatter(formatter)
|
||||
|
||||
# Configure root logger
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
root_logger.addHandler(console_handler)
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
@ -57,12 +58,19 @@ async def lifespan(app: FastAPI):
|
||||
# Get a database session
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data_init_service = service_manager.get_service('data_initialization')
|
||||
data_init = await data_init_service.initialize_data(db, game_ids=[1, 3], use_cache=False, init_categories=False, init_products=False, init_groups=False, init_archived_prices=False, init_mtgjson=False, archived_prices_start_date="2024-03-05", archived_prices_end_date="2025-04-17")
|
||||
logger.info(f"Data initialization results: {data_init}")
|
||||
#data_init_service = service_manager.get_service('data_initialization')
|
||||
#data_init = await data_init_service.initialize_data(db, game_ids=[1], use_cache=False, init_categories=True, init_products=True, init_groups=True, init_archived_prices=True, init_mtgjson=False, archived_prices_start_date="2025-05-22", archived_prices_end_date="2025-05-23")
|
||||
#logger.info(f"Data initialization results: {data_init}")
|
||||
# Update most recent prices
|
||||
#MostRecentTCGPlayerPrice.update_most_recent_prices(db)
|
||||
logger.info("Most recent prices updated successfully")
|
||||
|
||||
# Create default customer, vendor, and marketplace
|
||||
#inv_data_init = await data_init_service.initialize_inventory_data(db)
|
||||
#logger.info(f"Inventory data initialization results: {inv_data_init}")
|
||||
# Start the scheduler
|
||||
scheduler = service_manager.get_service('scheduler')
|
||||
#await scheduler.refresh_tcgplayer_inventory_table(db)
|
||||
await scheduler.start_scheduled_tasks(db)
|
||||
logger.info("Scheduler started successfully")
|
||||
|
||||
@ -107,10 +115,50 @@ async def read_app_js():
|
||||
raise HTTPException(status_code=404, detail="App.js file not found")
|
||||
return FileResponse(js_path)
|
||||
|
||||
# Serve manabox.html
|
||||
@app.get("/manabox.html")
|
||||
async def read_manabox_html():
|
||||
html_path = Path('app/static/manabox.html')
|
||||
if not html_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Manabox.html file not found")
|
||||
return FileResponse(html_path)
|
||||
|
||||
# Serve manabox.js
|
||||
@app.get("/manabox.js")
|
||||
async def read_manabox_js():
|
||||
js_path = Path('app/static/manabox.js')
|
||||
if not js_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Manabox.js file not found")
|
||||
return FileResponse(js_path)
|
||||
|
||||
# serve transactions.html
|
||||
@app.get("/transactions.html")
|
||||
async def read_transactions_html():
|
||||
html_path = Path('app/static/transactions.html')
|
||||
if not html_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Transaction.html file not found")
|
||||
return FileResponse(html_path)
|
||||
|
||||
# serve transactions.js
|
||||
@app.get("/transactions.js")
|
||||
async def read_transactions_js():
|
||||
js_path = Path('app/static/transactions.js')
|
||||
if not js_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Transaction.js file not found")
|
||||
return FileResponse(js_path)
|
||||
|
||||
# serve styles.css
|
||||
@app.get("/styles.css")
|
||||
async def read_styles_css():
|
||||
css_path = Path('app/static/styles.css')
|
||||
if not css_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Styles.css file not found")
|
||||
return FileResponse(css_path)
|
||||
|
||||
# Configure CORS with specific origins in production
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000"], # Update with your frontend URL
|
||||
allow_origins=["http://localhost:3000", "http://192.168.1.124:3000"], # Update with your frontend URL
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
@ -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,
|
||||
@ -7,25 +8,32 @@ from app.models.inventory_management import (
|
||||
Vendor,
|
||||
Customer,
|
||||
Transaction,
|
||||
CostBasis
|
||||
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
|
||||
from app.models.pricing import PricingEvent
|
||||
|
||||
|
||||
# This makes all models available for Alembic to discover
|
||||
__all__ = [
|
||||
'CriticalErrorLog',
|
||||
'File',
|
||||
'PhysicalItem',
|
||||
'InventoryItem',
|
||||
@ -34,17 +42,21 @@ __all__ = [
|
||||
'Vendor',
|
||||
'Customer',
|
||||
'Transaction',
|
||||
'CostBasis',
|
||||
'SealedExpectedValue',
|
||||
'Marketplace',
|
||||
'MarketplaceListing',
|
||||
'MTGJSONCard',
|
||||
'MTGJSONSKU',
|
||||
'Product',
|
||||
'TCGPlayerProduct',
|
||||
'TCGPlayerCategory',
|
||||
'TCGPlayerGroup',
|
||||
'TCGPlayerInventory',
|
||||
'ManaboxImportStaging',
|
||||
'TCGPlayerOrder',
|
||||
'TCGPlayerOrderTransaction',
|
||||
'TCGPlayerOrderProduct',
|
||||
'TCGPlayerOrderRefund',
|
||||
'TCGPlayerPriceHistory',
|
||||
'TCGPlayerProduct'
|
||||
'MostRecentTCGPlayerPrice',
|
||||
'PricingEvent'
|
||||
]
|
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,9 +1,6 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, DateTime, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
|
||||
|
||||
@ -15,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,149 +1,271 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Table
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, CheckConstraint, Index, Boolean, Table, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from datetime import datetime
|
||||
from app.models.critical_error_log import CriticalErrorLog
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
open_event_resulting_items = Table(
|
||||
"open_event_resulting_items",
|
||||
Base.metadata,
|
||||
Column("event_id", Integer, ForeignKey("open_events.id"), primary_key=True),
|
||||
Column("item_id", Integer, ForeignKey("physical_items.id"), primary_key=True)
|
||||
)
|
||||
|
||||
class PhysicalItem(Base):
|
||||
__tablename__ = "physical_items"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
item_type = Column(String)
|
||||
product_id = Column(Integer, ForeignKey("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("Product")
|
||||
sku = relationship("MTGJSONSKU", back_populates="physical_items", primaryjoin="PhysicalItem.tcgplayer_sku_id == MTGJSONSKU.tcgplayer_sku_id")
|
||||
product_direct = relationship("TCGPlayerProduct",
|
||||
back_populates="physical_items_direct",
|
||||
primaryjoin="PhysicalItem.tcgplayer_product_id == foreign(TCGPlayerProduct.tcgplayer_product_id)", uselist=False)
|
||||
inventory_item = relationship("InventoryItem", uselist=False, back_populates="physical_item")
|
||||
transaction_items = relationship("TransactionItem", back_populates="physical_item")
|
||||
#transaction_items = relationship("TransactionItem", back_populates="physical_item")
|
||||
source_open_events = relationship(
|
||||
"OpenEvent",
|
||||
back_populates="source_item",
|
||||
foreign_keys="[OpenEvent.source_item_id]"
|
||||
)
|
||||
resulting_open_events = relationship(
|
||||
"OpenEvent",
|
||||
secondary=open_event_resulting_items,
|
||||
back_populates="resulting_items"
|
||||
)
|
||||
|
||||
class SealedCase(PhysicalItem):
|
||||
__tablename__ = "sealed_cases"
|
||||
@hybrid_property
|
||||
def is_sealed(self):
|
||||
return not self.source_open_events
|
||||
|
||||
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'sealed_case'
|
||||
}
|
||||
|
||||
# Relationships
|
||||
boxes = relationship("SealedBox", back_populates="case")
|
||||
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)
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'sealed_box'
|
||||
}
|
||||
|
||||
# Relationships
|
||||
case = relationship("SealedCase", back_populates="boxes")
|
||||
open_event = relationship("OpenEvent", uselist=False, back_populates="sealed_box")
|
||||
|
||||
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")
|
||||
cards = relationship("OpenCard", back_populates="box")
|
||||
|
||||
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")
|
||||
@hybrid_property
|
||||
def products(self):
|
||||
if self.sku and self.sku.product:
|
||||
return self.sku.product
|
||||
elif self.product_direct:
|
||||
return self.product_direct
|
||||
else:
|
||||
return None
|
||||
|
||||
class InventoryItem(Base):
|
||||
__tablename__ = "inventory_items"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
physical_item_id = Column(Integer, ForeignKey("physical_items.id"), unique=True)
|
||||
cost_basis = Column(Float) # 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))
|
||||
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])
|
||||
children = relationship("InventoryItem")
|
||||
parent = relationship("InventoryItem", remote_side=[id], back_populates="children")
|
||||
children = relationship("InventoryItem", back_populates="parent", overlaps="parent")
|
||||
marketplace_listing = relationship("MarketplaceListing", back_populates="inventory_item")
|
||||
transaction_items = relationship("TransactionItem", back_populates="inventory_item")
|
||||
|
||||
@property
|
||||
def products(self):
|
||||
"""
|
||||
Proxy access to the associated TCGPlayerProduct(s) via the linked PhysicalItem.
|
||||
Returns:
|
||||
list[TCGPlayerProduct] or [] if no physical item or no linked products.
|
||||
"""
|
||||
return self.physical_item.products if self.physical_item else None
|
||||
|
||||
def soft_delete(self, timestamp=None):
|
||||
if not timestamp:
|
||||
timestamp = datetime.now()
|
||||
self.deleted_at = timestamp
|
||||
for child in self.children:
|
||||
child.soft_delete(timestamp)
|
||||
|
||||
class Box(PhysicalItem):
|
||||
__tablename__ = "boxes"
|
||||
|
||||
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
|
||||
expected_value = Column(Float)
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'box'
|
||||
}
|
||||
|
||||
class Case(PhysicalItem):
|
||||
__tablename__ = "cases"
|
||||
|
||||
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
|
||||
expected_value = Column(Float)
|
||||
num_boxes = Column(Integer)
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'case'
|
||||
}
|
||||
|
||||
class Card(PhysicalItem):
|
||||
__tablename__ = "cards"
|
||||
|
||||
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'card'
|
||||
}
|
||||
|
||||
class OpenEvent(Base):
|
||||
__tablename__ = "open_events"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
source_item_id = Column(Integer, ForeignKey("physical_items.id"))
|
||||
open_date = Column(DateTime(timezone=True))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("source_item_id", name="uq_openevent_one_per_source"),
|
||||
)
|
||||
|
||||
# Relationships
|
||||
source_item = relationship(
|
||||
"PhysicalItem",
|
||||
back_populates="source_open_events",
|
||||
foreign_keys=[source_item_id]
|
||||
)
|
||||
resulting_items = relationship(
|
||||
"PhysicalItem",
|
||||
secondary=open_event_resulting_items,
|
||||
back_populates="resulting_open_events"
|
||||
)
|
||||
|
||||
|
||||
class SealedExpectedValue(Base):
|
||||
__tablename__ = "sealed_expected_values"
|
||||
__table_args__ = (
|
||||
Index('idx_sealed_expected_value_product_id_deleted_at', 'tcgplayer_product_id', 'deleted_at', unique=True),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
tcgplayer_product_id = Column(Integer, nullable=False)
|
||||
expected_value = Column(Float, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
product = relationship(
|
||||
"TCGPlayerProduct",
|
||||
primaryjoin="SealedExpectedValue.tcgplayer_product_id == foreign(TCGPlayerProduct.tcgplayer_product_id)",
|
||||
viewonly=True)
|
||||
|
||||
# helper for ev
|
||||
#def assign_expected_value(target, session):
|
||||
# products = target.products
|
||||
# if not products:
|
||||
# raise ValueError(f"No product found for item ID {target.id}")
|
||||
|
||||
# if len(products) > 1:
|
||||
# product_names = [p.name for p in products]
|
||||
# critical_error = CriticalErrorLog(
|
||||
# error_type="multiple_products_found",
|
||||
# error_message=f"Multiple products found when assigning expected value for item ID {target.id} product names {product_names}"
|
||||
# )
|
||||
# session.add(critical_error)
|
||||
# session.commit()
|
||||
# raise ValueError(f"Multiple products found when assigning expected value for item ID {target.id} product names {product_names}")
|
||||
|
||||
# product_id = products[0].tcgplayer_product_id # reliable lookup key
|
||||
|
||||
# expected_value_entry = session.query(SealedExpectedValue).filter(
|
||||
# SealedExpectedValue.tcgplayer_product_id == product_id,
|
||||
# SealedExpectedValue.deleted_at == None
|
||||
# ).order_by(SealedExpectedValue.created_at.desc()).first()
|
||||
|
||||
# if expected_value_entry:
|
||||
# target.expected_value = expected_value_entry.expected_value
|
||||
# else:
|
||||
# critical_error = CriticalErrorLog(
|
||||
# error_type="no_expected_value_found",
|
||||
# error_message=f"No expected value found for product {products[0].name}"
|
||||
# )
|
||||
# session.add(critical_error)
|
||||
# session.commit()
|
||||
# raise ValueError(f"No expected value found for product {products[0].name}")
|
||||
|
||||
|
||||
# event listeners
|
||||
#@event.listens_for(InventoryItem, 'before_insert')
|
||||
#def ev_before_insert(mapper, connection, target):
|
||||
# session = Session.object_session(target)
|
||||
# if session:
|
||||
# assign_expected_value(target, session)
|
||||
|
||||
class TransactionItem(Base):
|
||||
__tablename__ = "transaction_items"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
transaction_id = Column(Integer, ForeignKey("transactions.id"))
|
||||
physical_item_id = Column(Integer, ForeignKey("physical_items.id"))
|
||||
inventory_item_id = Column(Integer, ForeignKey("inventory_items.id"))
|
||||
unit_price = Column(Float, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
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 = relationship("Transaction", back_populates="transaction_items")
|
||||
physical_item = relationship("PhysicalItem", back_populates="transaction_items")
|
||||
|
||||
class OpenEvent(Base):
|
||||
__tablename__ = "open_events"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
sealed_case_id = Column(Integer, ForeignKey("sealed_cases.id"), nullable=True)
|
||||
sealed_box_id = Column(Integer, ForeignKey("sealed_boxes.id"), nullable=True)
|
||||
open_date = Column(DateTime(timezone=True))
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
updated_at = Column(DateTime(timezone=True))
|
||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
sealed_case = relationship("SealedCase", back_populates="open_event")
|
||||
sealed_box = relationship("SealedBox", back_populates="open_event")
|
||||
resulting_boxes = relationship("OpenBox", back_populates="open_event")
|
||||
resulting_cards = relationship("OpenCard", back_populates="open_event")
|
||||
inventory_item = relationship("InventoryItem", 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)
|
||||
|
||||
# Relationships
|
||||
transactions = relationship("Transaction", back_populates="vendors")
|
||||
|
||||
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)
|
||||
|
||||
# Relationships
|
||||
transactions = relationship("Transaction", back_populates="customers")
|
||||
|
||||
class Transaction(Base):
|
||||
__tablename__ = "transactions"
|
||||
@ -151,35 +273,49 @@ class Transaction(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=True)
|
||||
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=True)
|
||||
marketplace_id = Column(Integer, ForeignKey("marketplaces.id"), nullable=True)
|
||||
transaction_type = Column(String) # 'purchase' or 'sale'
|
||||
transaction_date = Column(DateTime(timezone=True))
|
||||
transaction_total_amount = Column(Float)
|
||||
transaction_notes = Column(String)
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
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")
|
||||
vendors = relationship("Vendor", back_populates="transactions")
|
||||
customers = relationship("Customer", back_populates="transactions")
|
||||
marketplaces = relationship("Marketplace", back_populates="transactions")
|
||||
|
||||
class CostBasis(Base):
|
||||
__tablename__ = "cost_basis"
|
||||
class Marketplace(Base):
|
||||
__tablename__ = "marketplaces"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
transaction_item_id = Column(Integer, ForeignKey("transaction_items.id"))
|
||||
sealed_case_id = Column(Integer, ForeignKey("sealed_cases.id"), nullable=True)
|
||||
sealed_box_id = Column(Integer, ForeignKey("sealed_boxes.id"), nullable=True)
|
||||
open_box_id = Column(Integer, ForeignKey("open_boxes.id"), nullable=True)
|
||||
open_card_id = Column(Integer, ForeignKey("open_cards.id"), nullable=True)
|
||||
quantity = Column(Integer, nullable=False, default=1)
|
||||
unit_cost = Column(Float, nullable=False)
|
||||
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
|
||||
transaction_item = relationship("TransactionItem")
|
||||
sealed_case = relationship("SealedCase")
|
||||
sealed_box = relationship("SealedBox")
|
||||
open_box = relationship("OpenBox")
|
||||
open_card = relationship("OpenCard")
|
||||
listings = relationship("MarketplaceListing", back_populates="marketplace")
|
||||
transactions = relationship("Transaction", back_populates="marketplaces")
|
||||
class MarketplaceListing(Base):
|
||||
__tablename__ = "marketplace_listings"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
inventory_item_id = Column(Integer, ForeignKey("inventory_items.id"), nullable=False)
|
||||
marketplace_id = Column(Integer, ForeignKey("marketplaces.id"), nullable=False)
|
||||
recommended_price_id = Column(Integer, ForeignKey("pricing_events.id"), nullable=True)
|
||||
listed_price_id = Column(Integer, ForeignKey("pricing_events.id"), nullable=True)
|
||||
listing_date = Column(DateTime(timezone=True), nullable=True)
|
||||
delisting_date = Column(DateTime(timezone=True), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
inventory_item = relationship("InventoryItem", back_populates="marketplace_listing")
|
||||
marketplace = relationship("Marketplace", back_populates="listings")
|
||||
recommended_price = relationship("PricingEvent", foreign_keys=[recommended_price_id])
|
||||
listed_price = relationship("PricingEvent", foreign_keys=[listed_price_id])
|
19
app/models/manabox_import_staging.py
Normal file
19
app/models/manabox_import_staging.py
Normal file
@ -0,0 +1,19 @@
|
||||
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_product_id = Column(Integer)
|
||||
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())
|
29
app/models/pricing.py
Normal file
29
app/models/pricing.py
Normal file
@ -0,0 +1,29 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, CheckConstraint, Index, Boolean, Table, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PricingEvent(Base):
|
||||
__tablename__ = "pricing_events"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
inventory_item_id = Column(Integer, ForeignKey("inventory_items.id"))
|
||||
price = Column(Float)
|
||||
price_used = Column(String)
|
||||
price_reason = Column(String)
|
||||
free_shipping_adjustment = Column(Boolean, default=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
|
||||
inventory_item = relationship(
|
||||
"InventoryItem",
|
||||
primaryjoin="PricingEvent.inventory_item_id == foreign(InventoryItem.id)",
|
||||
viewonly=True
|
||||
)
|
@ -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,12 +1,13 @@
|
||||
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
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
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, unique=True, index=True)
|
||||
product_line = Column(String)
|
||||
set_name = Column(String)
|
||||
product_name = Column(String)
|
||||
@ -22,6 +23,39 @@ 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())
|
||||
|
||||
# relationships
|
||||
unmanaged_inventory = relationship("UnmanagedTCGPlayerInventory", back_populates="tcgplayer_inventory")
|
||||
sku = relationship("MTGJSONSKU", primaryjoin="foreign(MTGJSONSKU.tcgplayer_sku_id) == TCGPlayerInventory.tcgplayer_sku_id", viewonly=True)
|
||||
|
||||
|
||||
class UnmanagedTCGPlayerInventory(Base):
|
||||
__tablename__ = "unmanaged_tcgplayer_inventory"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
tcgplayer_inventory_id = Column(Integer, ForeignKey("tcgplayer_inventory.id"), unique=True, index=True)
|
||||
tcgplayer_sku_id = Column(Integer, unique=True, index=True)
|
||||
product_line = Column(String)
|
||||
set_name = Column(String)
|
||||
product_name = Column(String)
|
||||
title = Column(String)
|
||||
number = Column(String)
|
||||
rarity = Column(String)
|
||||
condition = Column(String)
|
||||
tcg_market_price = Column(Float)
|
||||
tcg_direct_low = Column(Float)
|
||||
tcg_low_price_with_shipping = Column(Float)
|
||||
tcg_low_price = Column(Float)
|
||||
total_quantity = Column(Integer)
|
||||
add_to_quantity = Column(Integer)
|
||||
tcg_marketplace_price = Column(Float)
|
||||
photo_url = Column(String)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# relationships
|
||||
tcgplayer_inventory = relationship("TCGPlayerInventory", back_populates="unmanaged_inventory")
|
||||
sku = relationship("MTGJSONSKU", primaryjoin="foreign(MTGJSONSKU.tcgplayer_sku_id) == UnmanagedTCGPlayerInventory.tcgplayer_sku_id", viewonly=True)
|
||||
|
@ -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,33 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
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())
|
295
app/models/tcgplayer_products.py
Normal file
295
app/models/tcgplayer_products.py
Normal file
@ -0,0 +1,295 @@
|
||||
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
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_
|
||||
|
||||
# =============================================================================
|
||||
# Core Models
|
||||
# =============================================================================
|
||||
|
||||
class MTGJSONSKU(Base):
|
||||
__tablename__ = "mtgjson_skus"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
mtgjson_uuid = Column(String, ForeignKey("mtgjson_cards.mtgjson_uuid"), index=True)
|
||||
tcgplayer_sku_id = Column(Integer, index=True, unique=True)
|
||||
tcgplayer_product_id = Column(Integer, nullable=False)
|
||||
normalized_printing = Column(String, nullable=False)
|
||||
condition = Column(String)
|
||||
finish = Column(String, nullable=True)
|
||||
language = Column(String)
|
||||
printing = Column(String)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
__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'),
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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(Integer, nullable=True)
|
||||
tcgplayer_etched_product_id = Column(Integer, 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())
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("mtgjson_uuid", name="uq_card_mtgjson_uuid"),
|
||||
)
|
||||
|
||||
skus = relationship("MTGJSONSKU", back_populates="card")
|
||||
|
||||
|
||||
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"),
|
||||
Index('idx_product_subtype', 'tcgplayer_product_id', 'normalized_sub_type_name'),
|
||||
)
|
||||
|
||||
# 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",
|
||||
viewonly=True,
|
||||
uselist=False)
|
||||
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)")
|
||||
|
||||
sealed_expected_value = relationship("SealedExpectedValue",
|
||||
primaryjoin="and_(TCGPlayerProduct.tcgplayer_product_id == foreign(SealedExpectedValue.tcgplayer_product_id), "
|
||||
"foreign(SealedExpectedValue.deleted_at) == None)",
|
||||
viewonly=True,
|
||||
uselist=False)
|
||||
# =============================================================================
|
||||
# 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 ISNT A MATERIALIZED VIEW ANYMORE FUCK IT
|
||||
"""
|
||||
__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),
|
||||
)
|
||||
|
||||
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))")
|
||||
|
||||
@classmethod
|
||||
def update_most_recent_prices(cls, db: Session) -> None:
|
||||
"""Update the most recent prices from the price history table."""
|
||||
# Delete all existing records
|
||||
db.query(cls).delete()
|
||||
|
||||
|
||||
# Get the most recent price for each product and sub_type_name
|
||||
subquery = db.query(
|
||||
TCGPlayerPriceHistory.product_id,
|
||||
TCGPlayerPriceHistory.sub_type_name,
|
||||
func.max(TCGPlayerPriceHistory.date).label('max_date')
|
||||
).group_by(
|
||||
TCGPlayerPriceHistory.product_id,
|
||||
TCGPlayerPriceHistory.sub_type_name
|
||||
).subquery()
|
||||
|
||||
# Join with price history to get the full records
|
||||
latest_prices = db.query(TCGPlayerPriceHistory).join(
|
||||
subquery,
|
||||
and_(
|
||||
TCGPlayerPriceHistory.product_id == subquery.c.product_id,
|
||||
TCGPlayerPriceHistory.sub_type_name == subquery.c.sub_type_name,
|
||||
TCGPlayerPriceHistory.date == subquery.c.max_date
|
||||
)
|
||||
).all()
|
||||
|
||||
# Create new MostRecentTCGPlayerPrice records
|
||||
for price in latest_prices:
|
||||
most_recent = cls(
|
||||
product_id=price.product_id,
|
||||
sub_type_name=price.sub_type_name,
|
||||
date=price.date,
|
||||
low_price=price.low_price,
|
||||
mid_price=price.mid_price,
|
||||
high_price=price.high_price,
|
||||
market_price=price.market_price,
|
||||
direct_low_price=price.direct_low_price
|
||||
)
|
||||
db.add(most_recent)
|
||||
|
||||
db.commit()
|
488
app/routes/inventory_management_routes.py
Normal file
488
app/routes/inventory_management_routes.py
Normal file
@ -0,0 +1,488 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, func
|
||||
from app.db.database import get_db
|
||||
from app.services.service_manager import ServiceManager
|
||||
from app.contexts.inventory_item import InventoryItemContextFactory
|
||||
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, SealedExpectedValueCreate, GetAllTransactionsResponse, TransactionResponse, TransactionItemResponse, InventoryItemResponse, TCGPlayerProductResponse, OpenEventResponse, OpenEventCreate, OpenEventResultingItemsResponse, OpenEventsForInventoryItemResponse
|
||||
from app.models.inventory_management import Transaction
|
||||
from app.models.tcgplayer_products import TCGPlayerProduct
|
||||
from typing import List
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
router = APIRouter(prefix="/inventory")
|
||||
|
||||
service_manager = ServiceManager()
|
||||
|
||||
# Sealed Routes
|
||||
@router.get("/sealed/boxes")
|
||||
async def get_sealed_boxes(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
sealed_box_service = service_manager.get_service("sealed_box")
|
||||
raw_boxes = sealed_box_service.get_all(db, skip=skip, limit=limit)
|
||||
boxes = []
|
||||
for box in raw_boxes:
|
||||
inventory_item = InventoryItemContextFactory(db).get_context(box.inventory_item)
|
||||
boxes.append(inventory_item)
|
||||
return boxes
|
||||
|
||||
@router.get("/sealed/boxes/{sealed_box_id}")
|
||||
async def get_sealed_box(
|
||||
sealed_box_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
sealed_box_service = service_manager.get_service("sealed_box")
|
||||
sealed_box = sealed_box_service.get(db, sealed_box_id)
|
||||
return InventoryItemContextFactory(db).get_context(sealed_box.inventory_item)
|
||||
|
||||
@router.post("/sealed/boxes")
|
||||
async def create_sealed_box(
|
||||
product_id: int,
|
||||
cost_basis: float,
|
||||
case_id: int = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
try:
|
||||
sealed_box_service = service_manager.get_service("sealed_box")
|
||||
sealed_box = await sealed_box_service.create_sealed_box(db, product_id, cost_basis, case_id)
|
||||
return InventoryItemContextFactory(db).get_context(sealed_box.inventory_item)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/sealed/boxes/{sealed_box_id}/open")
|
||||
async def open_sealed_box(
|
||||
sealed_box_id: int,
|
||||
manabox_file_upload_ids: List[int],
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
try:
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
sealed_box_service = service_manager.get_service("sealed_box")
|
||||
file_service = service_manager.get_service("file")
|
||||
sealed_box = sealed_box_service.get(db, sealed_box_id)
|
||||
manabox_file_uploads = []
|
||||
for manabox_file_upload_id in manabox_file_upload_ids:
|
||||
manabox_file_upload = file_service.get_file(db, manabox_file_upload_id)
|
||||
manabox_file_uploads.append(manabox_file_upload)
|
||||
success = await inventory_service.process_manabox_import_staging(db, manabox_file_uploads, sealed_box)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Failed to process Manabox import staging")
|
||||
return {"message": "Manabox import staging processed successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/sealed/cases")
|
||||
async def get_sealed_cases(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
sealed_case_service = service_manager.get_service("sealed_case")
|
||||
raw_cases = sealed_case_service.get_all(db, skip=skip, limit=limit)
|
||||
cases = []
|
||||
for case in raw_cases:
|
||||
inventory_item = InventoryItemContextFactory(db).get_context(case.inventory_item)
|
||||
cases.append(inventory_item)
|
||||
return cases
|
||||
|
||||
@router.get("/sealed/cases/{sealed_case_id}")
|
||||
async def get_sealed_case(
|
||||
sealed_case_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
sealed_case_service = service_manager.get_service("sealed_case")
|
||||
sealed_case = sealed_case_service.get(db, sealed_case_id)
|
||||
return InventoryItemContextFactory(db).get_context(sealed_case.inventory_item)
|
||||
|
||||
@router.post("/sealed/cases")
|
||||
async def create_sealed_case(
|
||||
product_id: int,
|
||||
cost_basis: float,
|
||||
num_boxes: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
try:
|
||||
sealed_case_service = service_manager.get_service("sealed_case")
|
||||
sealed_case = await sealed_case_service.create_sealed_case(db, product_id, cost_basis, num_boxes)
|
||||
return InventoryItemContextFactory(db).get_context(sealed_case.inventory_item)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/sealed/cases/{sealed_case_id}/open")
|
||||
async def open_sealed_case(
|
||||
sealed_case_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
try:
|
||||
sealed_case_service = service_manager.get_service("sealed_case")
|
||||
sealed_case = sealed_case_service.get(db, sealed_case_id)
|
||||
await sealed_case_service.open_sealed_case(db, sealed_case)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Transaction Routes
|
||||
@router.post("/transactions/purchase")
|
||||
async def create_purchase_transaction(
|
||||
transaction_data: PurchaseTransactionCreate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
transaction = await inventory_service.create_purchase_transaction(db, transaction_data)
|
||||
return transaction
|
||||
|
||||
@router.post("/transactions/sale")
|
||||
async def create_sale_transaction(
|
||||
transaction_data: SaleTransactionCreate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
transaction = await inventory_service.create_sale_transaction(db, transaction_data)
|
||||
return transaction
|
||||
|
||||
@router.post("/customers")
|
||||
async def create_customer(
|
||||
customer_name: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
customer = await inventory_service.create_customer(db, customer_name)
|
||||
return customer
|
||||
|
||||
@router.post("/vendors")
|
||||
async def create_vendor(
|
||||
vendor_name: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
vendor = await inventory_service.create_vendor(db, vendor_name)
|
||||
return vendor
|
||||
|
||||
@router.get("/vendors")
|
||||
async def get_vendors(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
vendors = await inventory_service.get_vendors(db)
|
||||
return vendors
|
||||
|
||||
@router.post("/marketplaces")
|
||||
async def create_marketplace(
|
||||
marketplace_name: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
marketplace = await inventory_service.create_marketplace(db, marketplace_name)
|
||||
return marketplace
|
||||
|
||||
@router.get("/marketplaces")
|
||||
async def get_marketplaces(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
marketplaces = await inventory_service.get_marketplaces(db)
|
||||
return marketplaces
|
||||
|
||||
@router.get("/products/search")
|
||||
async def get_products(q: str, db: Session = Depends(get_db)):
|
||||
query = ' & '.join(q.lower().split()) # This ensures all terms must match
|
||||
|
||||
products = db.query(TCGPlayerProduct).filter(
|
||||
func.to_tsvector('english', TCGPlayerProduct.name)
|
||||
.op('@@')(func.to_tsquery('english', query))
|
||||
).all()
|
||||
|
||||
return products
|
||||
|
||||
@router.get("/products/{product_id}/expected-value")
|
||||
async def get_expected_value(
|
||||
product_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
expected_value = await inventory_service.get_expected_value(db, product_id)
|
||||
return expected_value
|
||||
|
||||
|
||||
@router.post("/products/expected-value")
|
||||
async def create_expected_value(
|
||||
expected_value_data: SealedExpectedValueCreate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
expected_value = await inventory_service.create_expected_value(db, expected_value_data)
|
||||
return expected_value
|
||||
|
||||
@router.post("/transactions/purchase")
|
||||
async def create_purchase_transaction(
|
||||
transaction_data: PurchaseTransactionCreate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
transaction = await inventory_service.create_purchase_transaction(db, transaction_data)
|
||||
return transaction
|
||||
|
||||
@router.get("/transactions")
|
||||
async def get_transactions(
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
total = db.query(func.count(Transaction.id)).filter(Transaction.deleted_at == None).scalar()
|
||||
transactions = await inventory_service.get_transactions(db, skip, limit)
|
||||
return GetAllTransactionsResponse(
|
||||
total=total,
|
||||
transactions=[TransactionResponse(
|
||||
id=transaction.id,
|
||||
vendor_id=transaction.vendor_id,
|
||||
customer_id=transaction.customer_id,
|
||||
marketplace_id=transaction.marketplace_id,
|
||||
transaction_type=transaction.transaction_type,
|
||||
transaction_date=transaction.transaction_date,
|
||||
transaction_total_amount=transaction.transaction_total_amount,
|
||||
transaction_notes=transaction.transaction_notes,
|
||||
created_at=transaction.created_at,
|
||||
updated_at=transaction.updated_at,
|
||||
transaction_items=[TransactionItemResponse(
|
||||
id=transaction_item.id,
|
||||
transaction_id=transaction_item.transaction_id,
|
||||
inventory_item_id=transaction_item.inventory_item_id,
|
||||
unit_price=transaction_item.unit_price,
|
||||
created_at=transaction_item.created_at,
|
||||
updated_at=transaction_item.updated_at,
|
||||
deleted_at=transaction_item.deleted_at
|
||||
) for transaction_item in transaction.transaction_items]
|
||||
) for transaction in transactions]
|
||||
)
|
||||
|
||||
@router.get("/transactions/{transaction_id}")
|
||||
async def get_transaction(
|
||||
transaction_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
transaction = await inventory_service.get_transaction(db, transaction_id)
|
||||
if not transaction:
|
||||
raise HTTPException(status_code=404, detail="Transaction not found")
|
||||
return TransactionResponse(
|
||||
id=transaction.id,
|
||||
vendor_id=transaction.vendor_id,
|
||||
customer_id=transaction.customer_id,
|
||||
marketplace_id=transaction.marketplace_id,
|
||||
transaction_type=transaction.transaction_type,
|
||||
transaction_date=transaction.transaction_date,
|
||||
transaction_total_amount=transaction.transaction_total_amount,
|
||||
transaction_notes=transaction.transaction_notes,
|
||||
created_at=transaction.created_at,
|
||||
updated_at=transaction.updated_at,
|
||||
transaction_items=[TransactionItemResponse(
|
||||
id=transaction_item.id,
|
||||
transaction_id=transaction_item.transaction_id,
|
||||
inventory_item_id=transaction_item.inventory_item_id,
|
||||
unit_price=transaction_item.unit_price,
|
||||
created_at=transaction_item.created_at,
|
||||
updated_at=transaction_item.updated_at,
|
||||
deleted_at=transaction_item.deleted_at
|
||||
) for transaction_item in transaction.transaction_items]
|
||||
)
|
||||
|
||||
@router.get("/items/{inventory_item_id}")
|
||||
async def get_inventory_item(
|
||||
inventory_item_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
|
||||
marketplace_listing_service = service_manager.get_service("marketplace_listing")
|
||||
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
|
||||
marketplace_listing = await marketplace_listing_service.get_marketplace_listing(db, inventory_item, marketplace)
|
||||
if marketplace_listing is None:
|
||||
listed_price = None
|
||||
recommended_price = None
|
||||
marketplace_listing_id = None
|
||||
else:
|
||||
if marketplace_listing.listed_price is not None:
|
||||
listed_price = marketplace_listing.listed_price.price if marketplace_listing.listed_price.price is not None else None
|
||||
else:
|
||||
listed_price = None
|
||||
if marketplace_listing.recommended_price is not None:
|
||||
recommended_price = marketplace_listing.recommended_price.price if marketplace_listing.recommended_price.price is not None else None
|
||||
else:
|
||||
recommended_price = None
|
||||
marketplace_listing_id = marketplace_listing.id
|
||||
return InventoryItemResponse(
|
||||
id=inventory_item.id,
|
||||
physical_item_id=inventory_item.physical_item_id,
|
||||
cost_basis=inventory_item.cost_basis,
|
||||
parent_id=inventory_item.parent_id,
|
||||
created_at=inventory_item.created_at,
|
||||
updated_at=inventory_item.updated_at,
|
||||
item_type=inventory_item.physical_item.item_type,
|
||||
listed_price=listed_price,
|
||||
recommended_price=recommended_price,
|
||||
marketplace_listing_id=marketplace_listing_id,
|
||||
product=TCGPlayerProductResponse(
|
||||
id=inventory_item.physical_item.product_direct.id,
|
||||
tcgplayer_product_id=inventory_item.physical_item.product_direct.tcgplayer_product_id,
|
||||
name=inventory_item.physical_item.product_direct.name,
|
||||
image_url=inventory_item.physical_item.product_direct.image_url,
|
||||
category_id=inventory_item.physical_item.product_direct.category_id,
|
||||
group_id=inventory_item.physical_item.product_direct.group_id,
|
||||
url=inventory_item.physical_item.product_direct.url,
|
||||
market_price=inventory_item.physical_item.product_direct.most_recent_tcgplayer_price.market_price,
|
||||
category_name=inventory_item.physical_item.product_direct.category.name,
|
||||
group_name=inventory_item.physical_item.product_direct.group.name
|
||||
)
|
||||
)
|
||||
|
||||
@router.post("/items/{inventory_item_id}/open")
|
||||
async def open_box_or_case(
|
||||
open_event_data: OpenEventCreate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
inventory_item = await inventory_service.get_inventory_item(db, open_event_data.inventory_item_id)
|
||||
file_service = service_manager.get_service("file")
|
||||
files = [await file_service.get_file(db, file_id) for file_id in open_event_data.manabox_file_upload_ids]
|
||||
if inventory_item.physical_item.item_type == "box":
|
||||
box_service = service_manager.get_service("box")
|
||||
open_event = await box_service.open_box(db, inventory_item.physical_item, files)
|
||||
return OpenEventResponse(
|
||||
id=open_event.id,
|
||||
source_item_id=open_event.source_item_id,
|
||||
created_at=open_event.created_at,
|
||||
updated_at=open_event.updated_at
|
||||
)
|
||||
elif inventory_item.physical_item.item_type == "case":
|
||||
case_service = service_manager.get_service("case")
|
||||
open_event = await case_service.open_case(db, inventory_item.physical_item, files)
|
||||
return OpenEventResponse(
|
||||
id=open_event.id,
|
||||
source_item_id=open_event.source_item_id,
|
||||
created_at=open_event.created_at,
|
||||
updated_at=open_event.updated_at
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Invalid item type")
|
||||
|
||||
@router.get("/items/{inventory_item_id}/open-events")
|
||||
async def get_open_events(
|
||||
inventory_item_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
|
||||
|
||||
# Don't return open events for cards
|
||||
if inventory_item.physical_item.item_type == 'card':
|
||||
return OpenEventsForInventoryItemResponse(open_events=[])
|
||||
|
||||
open_events = await inventory_service.get_open_events_for_inventory_item(db, inventory_item)
|
||||
return OpenEventsForInventoryItemResponse(
|
||||
open_events=[OpenEventResponse(
|
||||
id=open_event.id,
|
||||
source_item_id=open_event.source_item_id,
|
||||
created_at=open_event.created_at,
|
||||
updated_at=open_event.updated_at
|
||||
) for open_event in open_events]
|
||||
)
|
||||
|
||||
@router.get("/items/{inventory_item_id}/open-events/{open_event_id}/resulting-items", response_model=List[InventoryItemResponse])
|
||||
async def get_resulting_items(
|
||||
inventory_item_id: int,
|
||||
open_event_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
|
||||
open_event = await inventory_service.get_open_event(db, inventory_item, open_event_id)
|
||||
resulting_items = await inventory_service.get_resulting_items_for_open_event(db, open_event)
|
||||
marketplace_listing_service = service_manager.get_service("marketplace_listing")
|
||||
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
|
||||
marketplace_listing = await marketplace_listing_service.get_marketplace_listing(db, inventory_item, marketplace)
|
||||
if marketplace_listing is None:
|
||||
listed_price = None
|
||||
recommended_price = None
|
||||
marketplace_listing_id = None
|
||||
else:
|
||||
if marketplace_listing.listed_price is not None:
|
||||
listed_price = marketplace_listing.listed_price.price if marketplace_listing.listed_price.price is not None else None
|
||||
else:
|
||||
listed_price = None
|
||||
if marketplace_listing.recommended_price is not None:
|
||||
recommended_price = marketplace_listing.recommended_price.price if marketplace_listing.recommended_price.price is not None else None
|
||||
else:
|
||||
recommended_price = None
|
||||
marketplace_listing_id = marketplace_listing.id
|
||||
return [InventoryItemResponse(
|
||||
id=resulting_item.id,
|
||||
physical_item_id=resulting_item.physical_item_id,
|
||||
cost_basis=resulting_item.cost_basis,
|
||||
parent_id=resulting_item.parent_id,
|
||||
product=TCGPlayerProductResponse(
|
||||
id=resulting_item.physical_item.product_direct.id,
|
||||
tcgplayer_product_id=resulting_item.physical_item.product_direct.tcgplayer_product_id,
|
||||
name=resulting_item.physical_item.product_direct.name,
|
||||
image_url=resulting_item.physical_item.product_direct.image_url,
|
||||
category_id=resulting_item.physical_item.product_direct.category_id,
|
||||
group_id=resulting_item.physical_item.product_direct.group_id,
|
||||
url=resulting_item.physical_item.product_direct.url,
|
||||
market_price=resulting_item.physical_item.product_direct.most_recent_tcgplayer_price.market_price,
|
||||
category_name=resulting_item.physical_item.product_direct.category.name,
|
||||
group_name=resulting_item.physical_item.product_direct.group.name
|
||||
),
|
||||
item_type=resulting_item.physical_item.item_type,
|
||||
marketplace_listing_id=marketplace_listing_id,
|
||||
listed_price=listed_price,
|
||||
recommended_price=recommended_price,
|
||||
created_at=resulting_item.created_at,
|
||||
updated_at=resulting_item.updated_at) for resulting_item in resulting_items]
|
||||
|
||||
@router.post("/items/{inventory_item_id}/open-events/{open_event_id}/create-listings")
|
||||
async def create_marketplace_listings(
|
||||
inventory_item_id: int,
|
||||
open_event_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
|
||||
open_event = await inventory_service.get_open_event(db, inventory_item, open_event_id)
|
||||
resulting_items = await inventory_service.get_resulting_items_for_open_event(db, open_event)
|
||||
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
|
||||
marketplace_listing_service = service_manager.get_service("marketplace_listing")
|
||||
for resulting_item in resulting_items:
|
||||
await marketplace_listing_service.create_marketplace_listing(db, resulting_item, marketplace)
|
||||
return {"message": f"{len(resulting_items)} marketplace listings created successfully"}
|
||||
|
||||
@router.post("/items/{inventory_item_id}/open-events/{open_event_id}/confirm-listings")
|
||||
async def confirm_listings(
|
||||
inventory_item_id: int,
|
||||
open_event_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
inventory_service = service_manager.get_service("inventory")
|
||||
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
|
||||
open_event = await inventory_service.get_open_event(db, inventory_item, open_event_id)
|
||||
marketplace_listing_service = service_manager.get_service("marketplace_listing")
|
||||
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
|
||||
try:
|
||||
csv_string = await marketplace_listing_service.confirm_listings(db, open_event, marketplace)
|
||||
if not csv_string:
|
||||
raise ValueError("No CSV data generated")
|
||||
|
||||
# Create a streaming response with the CSV data
|
||||
return StreamingResponse(
|
||||
iter([csv_string]),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename=tcgplayer_add_file_{open_event.id}_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv"
|
||||
}
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
94
app/routes/manabox_routes.py
Normal file
94
app/routes/manabox_routes.py
Normal file
@ -0,0 +1,94 @@
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Form, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.database import get_db
|
||||
from app.services.service_manager import ServiceManager
|
||||
import csv
|
||||
import io
|
||||
|
||||
router = APIRouter(prefix="/manabox")
|
||||
|
||||
service_manager = ServiceManager()
|
||||
|
||||
REQUIRED_HEADERS = {
|
||||
'Name', 'Set code', 'Set name', 'Collector number', 'Foil', 'Rarity',
|
||||
'Quantity', 'ManaBox ID', 'Scryfall ID', 'Purchase price', 'Misprint',
|
||||
'Altered', 'Condition', 'Language', 'Purchase price currency'
|
||||
}
|
||||
|
||||
def is_valid_csv(file: UploadFile) -> tuple[bool, str]:
|
||||
# Check if filename ends with .csv
|
||||
if not file.filename.lower().endswith('.csv'):
|
||||
return False, "File must have a .csv extension"
|
||||
|
||||
try:
|
||||
# Try to read the content as CSV
|
||||
content = file.file.read()
|
||||
# Reset file pointer for later use
|
||||
file.file.seek(0)
|
||||
|
||||
# Try to parse the content as CSV
|
||||
csv_reader = csv.reader(io.StringIO(content.decode('utf-8')))
|
||||
|
||||
# Get headers from first row
|
||||
headers = set(next(csv_reader))
|
||||
|
||||
# Check for missing headers
|
||||
missing_headers = REQUIRED_HEADERS - headers
|
||||
if missing_headers:
|
||||
return False, f"Missing required columns: {', '.join(missing_headers)}"
|
||||
|
||||
# Check for extra headers
|
||||
extra_headers = headers - REQUIRED_HEADERS
|
||||
if extra_headers:
|
||||
return False, f"Unexpected columns found: {', '.join(extra_headers)}"
|
||||
|
||||
return True, "Valid CSV format"
|
||||
except (csv.Error, UnicodeDecodeError) as e:
|
||||
return False, f"Invalid CSV format: {str(e)}"
|
||||
except StopIteration:
|
||||
return False, "Empty file"
|
||||
|
||||
@router.post("/process-csv")
|
||||
async def process_manabox_csv(
|
||||
background_tasks: BackgroundTasks,
|
||||
file: UploadFile = File(...),
|
||||
source: str = Form(...),
|
||||
description: str = Form(...),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
try:
|
||||
is_valid, error_message = is_valid_csv(file)
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=error_message
|
||||
)
|
||||
|
||||
content = await file.read()
|
||||
|
||||
metadata = {
|
||||
"source": source,
|
||||
"description": description
|
||||
}
|
||||
|
||||
manabox_service = service_manager.get_service("manabox")
|
||||
|
||||
success = await manabox_service.process_manabox_csv(db, content, metadata, background_tasks)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Failed to process CSV file")
|
||||
|
||||
return {"message": "CSV processed successfully"}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/manabox-file-uploads")
|
||||
async def get_manabox_file_uploads(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
file_service = service_manager.get_service("file")
|
||||
files = await file_service.list_files(db, skip=skip, limit=limit, file_type="manabox")
|
||||
return files
|
@ -1,17 +1,13 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, UploadFile, File
|
||||
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from app.schemas.tcgplayer import TCGPlayerAPIOrderSummary, TCGPlayerAPIOrder
|
||||
from app.schemas.generate import GenerateAddressLabelsRequest, GeneratePackingSlipsRequest, GeneratePullSheetsRequest, GenerateResponse, GenerateReturnLabelsRequest
|
||||
from app.schemas.file import FileUpload
|
||||
from app.services.service_manager import ServiceManager
|
||||
from app.services.file_service import FileService
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.database import get_db
|
||||
import os
|
||||
import tempfile
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -223,3 +219,25 @@ async def print_pirate_ship_label(
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to print Pirate Ship label: {str(e)}")
|
||||
|
||||
# what even is this TODO delete
|
||||
@router.post("/process-manabox-csv")
|
||||
async def process_manabox_csv(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db)
|
||||
) -> GenerateResponse:
|
||||
try:
|
||||
# ensure csv
|
||||
if file.content_type != "text/csv":
|
||||
raise HTTPException(status_code=400, detail="File must be a CSV")
|
||||
# read file
|
||||
content = await file.read()
|
||||
# save file
|
||||
file_service = service_manager.get_service('file')
|
||||
stored_file = await file_service.save_file(db, content, f'manabox_upload_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv', "manabox_csvs", "csv")
|
||||
# process csv
|
||||
manabox_service = service_manager.get_service('manabox')
|
||||
success = await manabox_service.process_manabox_csv(db, stored_file)
|
||||
return {"success": success, "message": "Manabox CSV processed successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to process Manabox CSV: {str(e)}")
|
||||
|
@ -5,7 +5,8 @@ from app.models.file import File as FileModel
|
||||
from app.schemas.file import FileCreate, FileUpdate, FileDelete, FileList, FileInDB
|
||||
from app.routes.set_label_routes import router as set_label_router
|
||||
from app.routes.order_routes import router as order_router
|
||||
|
||||
from app.routes.manabox_routes import router as manabox_router
|
||||
from app.routes.inventory_management_routes import router as inventory_management_router
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
# Include set label routes
|
||||
@ -14,6 +15,12 @@ router.include_router(set_label_router)
|
||||
# Include order routes
|
||||
router.include_router(order_router)
|
||||
|
||||
# Include manabox routes
|
||||
router.include_router(manabox_router)
|
||||
|
||||
# Include inventory management routes
|
||||
router.include_router(inventory_management_router)
|
||||
|
||||
# ============================================================================
|
||||
# Health Check & Root Endpoints
|
||||
# ============================================================================
|
||||
|
0
app/schemas/inventory.py
Normal file
0
app/schemas/inventory.py
Normal file
109
app/schemas/transaction.py
Normal file
109
app/schemas/transaction.py
Normal file
@ -0,0 +1,109 @@
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from app.models.tcgplayer_products import TCGPlayerProduct
|
||||
class PurchaseItem(BaseModel):
|
||||
product_id: int
|
||||
unit_price: float
|
||||
quantity: int
|
||||
item_type: str
|
||||
num_boxes: Optional[int] = None
|
||||
# TODO: remove is_case and num_boxes, should derive from product_id
|
||||
|
||||
class SaleItem(BaseModel):
|
||||
inventory_item_id: int
|
||||
unit_price: float
|
||||
|
||||
class PurchaseTransactionCreate(BaseModel):
|
||||
vendor_id: int
|
||||
transaction_date: datetime
|
||||
items: List[PurchaseItem]
|
||||
transaction_notes: Optional[str] = None
|
||||
|
||||
class SaleTransactionCreate(BaseModel):
|
||||
customer_id: int
|
||||
marketplace_id: Optional[int] = None
|
||||
transaction_date: datetime
|
||||
items: List[SaleItem]
|
||||
transaction_notes: Optional[str] = None
|
||||
|
||||
class TransactionItemResponse(BaseModel):
|
||||
id: int
|
||||
transaction_id: int
|
||||
inventory_item_id: int
|
||||
unit_price: float
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
deleted_at: Optional[datetime] = None
|
||||
class TransactionResponse(BaseModel):
|
||||
id: int
|
||||
vendor_id: Optional[int] = None
|
||||
customer_id: Optional[int] = None
|
||||
marketplace_id: Optional[int] = None
|
||||
transaction_type: str
|
||||
transaction_date: datetime
|
||||
transaction_total_amount: float
|
||||
transaction_notes: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
deleted_at: Optional[datetime] = None
|
||||
transaction_items: List[TransactionItemResponse]
|
||||
|
||||
|
||||
class GetAllTransactionsResponse(BaseModel):
|
||||
total: int
|
||||
transactions: List[TransactionResponse]
|
||||
|
||||
class SealedExpectedValueResponse(BaseModel):
|
||||
id: int
|
||||
tcgplayer_product_id: int
|
||||
expected_value: float
|
||||
|
||||
class SealedExpectedValueCreate(BaseModel):
|
||||
tcgplayer_product_id: int
|
||||
expected_value: float
|
||||
|
||||
class TCGPlayerProductResponse(BaseModel):
|
||||
id: int
|
||||
tcgplayer_product_id: int
|
||||
name: str
|
||||
image_url: str
|
||||
category_id: int
|
||||
group_id: int
|
||||
url: str
|
||||
market_price: float
|
||||
category_name: str
|
||||
group_name: str
|
||||
|
||||
class InventoryItemResponse(BaseModel):
|
||||
id: int
|
||||
physical_item_id: int
|
||||
cost_basis: float
|
||||
item_type: str
|
||||
listed_price: Optional[float] = None
|
||||
marketplace_listing_id: Optional[int] = None
|
||||
recommended_price: Optional[float] = None
|
||||
parent_id: Optional[int] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
product: Optional[TCGPlayerProductResponse] = None
|
||||
|
||||
class OpenEventResponse(BaseModel):
|
||||
id: int
|
||||
source_item_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class OpenEventCreate(BaseModel):
|
||||
inventory_item_id: int
|
||||
manabox_file_upload_ids: List[int]
|
||||
|
||||
class OpenEventResultingItemsResponse(BaseModel):
|
||||
id: int
|
||||
source_item_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
resulting_items: List[InventoryItemResponse]
|
||||
|
||||
class OpenEventsForInventoryItemResponse(BaseModel):
|
||||
open_events: List[OpenEventResponse]
|
@ -14,6 +14,8 @@ from app.services.set_label_service import SetLabelService
|
||||
from app.services.scheduler.scheduler_service import SchedulerService
|
||||
from app.services.external_api.tcgplayer.order_management_service import OrderManagementService
|
||||
from app.services.external_api.tcgplayer.tcgplayer_inventory_service import TCGPlayerInventoryService
|
||||
from app.services.pricing_service import PricingService
|
||||
from app.services.inventory_service import MarketplaceListingService
|
||||
|
||||
__all__ = [
|
||||
'BaseService',
|
||||
@ -31,5 +33,7 @@ __all__ = [
|
||||
'SetLabelService',
|
||||
'SchedulerService',
|
||||
'OrderManagementService',
|
||||
'TCGPlayerInventoryService'
|
||||
'TCGPlayerInventoryService',
|
||||
'PricingService',
|
||||
'MarketplaceListingService'
|
||||
]
|
@ -1,23 +1,27 @@
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any, Union, Generator, Callable
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, List, Dict, Any, Union
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.tcgplayer_group import TCGPlayerGroup
|
||||
from app.models.tcgplayer_product import TCGPlayerProduct
|
||||
from app.models.tcgplayer_category import TCGPlayerCategory
|
||||
from app.models.tcgplayer_products import TCGPlayerProduct, TCGPlayerCategory, TCGPlayerGroup
|
||||
from app.models.inventory_management import SealedExpectedValue
|
||||
from app.services.base_service import BaseService
|
||||
from app.schemas.file import FileInDB
|
||||
from app.db.database import transaction
|
||||
from app.db.database import transaction as db_transaction
|
||||
from app.schemas.transaction import PurchaseTransactionCreate, PurchaseItem
|
||||
from app.contexts.inventory_item import InventoryItemContextFactory
|
||||
from app.models.tcgplayer_products import MTGJSONSKU, MTGJSONCard
|
||||
from app.models.tcgplayer_products import TCGPlayerPriceHistory
|
||||
from app.models.critical_error_log import CriticalErrorLog
|
||||
import csv
|
||||
import io
|
||||
import logging
|
||||
from app.models.tcgplayer_price_history import TCGPlayerPriceHistory
|
||||
from sqlalchemy import and_, bindparam, update, insert
|
||||
import py7zr
|
||||
import shutil
|
||||
import py7zr
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DataInitializationService(BaseService):
|
||||
def __init__(self):
|
||||
super().__init__(None)
|
||||
@ -54,7 +58,8 @@ class DataInitializationService(BaseService):
|
||||
file_record = await self.file_service.get_file_by_filename(db, filename)
|
||||
if file_record:
|
||||
# Check if cache is expired (7 days)
|
||||
cache_age = datetime.now() - file_record.created_at
|
||||
# Ensure both datetimes are timezone-aware
|
||||
cache_age = datetime.now(timezone.utc) - file_record.created_at
|
||||
if cache_age.days < 7:
|
||||
with open(file_record.path, 'r') as f:
|
||||
return json.load(f)
|
||||
@ -70,7 +75,7 @@ class DataInitializationService(BaseService):
|
||||
batch_size = 1000 # Process in batches of 1000
|
||||
total_categories = len(categories)
|
||||
|
||||
with transaction(db):
|
||||
with db_transaction(db):
|
||||
for i in range(0, total_categories, batch_size):
|
||||
batch = categories[i:i + batch_size]
|
||||
for category_data in batch:
|
||||
@ -150,7 +155,7 @@ class DataInitializationService(BaseService):
|
||||
batch_size = 1000 # Process in batches of 1000
|
||||
total_groups = len(groups)
|
||||
|
||||
with transaction(db):
|
||||
with db_transaction(db):
|
||||
for i in range(0, total_groups, batch_size):
|
||||
batch = groups[i:i + batch_size]
|
||||
for group_data in batch:
|
||||
@ -214,8 +219,6 @@ class DataInitializationService(BaseService):
|
||||
|
||||
async def sync_products(self, db: Session, products_data: str):
|
||||
"""Sync products data to the database using streaming for large datasets"""
|
||||
import csv
|
||||
import io
|
||||
|
||||
# Parse CSV data
|
||||
csv_reader = csv.DictReader(io.StringIO(products_data))
|
||||
@ -223,36 +226,46 @@ class DataInitializationService(BaseService):
|
||||
batch_size = 1000 # Process in batches of 1000
|
||||
total_products = len(products_list)
|
||||
|
||||
with transaction(db):
|
||||
with db_transaction(db):
|
||||
for i in range(0, total_products, batch_size):
|
||||
batch = products_list[i:i + batch_size]
|
||||
for product_data in batch:
|
||||
existing_product = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.product_id == product_data["productId"]).first()
|
||||
sub_type_name = product_data.get("subTypeName") if product_data.get("subTypeName") else "other"
|
||||
existing_product = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.tcgplayer_product_id == product_data["productId"]).filter(TCGPlayerProduct.sub_type_name == sub_type_name).first()
|
||||
if existing_product:
|
||||
# Update existing product
|
||||
for key, value in {
|
||||
"name": product_data["name"],
|
||||
"clean_name": product_data.get("cleanName"),
|
||||
"image_url": product_data.get("imageUrl"),
|
||||
"sub_type_name": product_data.get("subTypeName") if product_data.get("subTypeName") else "other",
|
||||
"normalized_sub_type_name": product_data.get("subTypeName").lower().replace(" ", "_") if product_data.get("subTypeName") else "other",
|
||||
"category_id": product_data.get("categoryId"),
|
||||
"group_id": product_data.get("groupId"),
|
||||
"url": product_data.get("url"),
|
||||
"modified_on": datetime.fromisoformat(product_data["modifiedOn"].replace("Z", "+00:00")) if product_data.get("modifiedOn") else None,
|
||||
"image_count": product_data.get("imageCount", 0),
|
||||
"ext_rarity": product_data.get("extRarity"),
|
||||
"ext_subtype": product_data.get("extSubtype"),
|
||||
"ext_oracle_text": product_data.get("extOracleText"),
|
||||
"ext_number": product_data.get("extNumber"),
|
||||
"low_price": float(product_data.get("lowPrice")) if product_data.get("lowPrice") else None,
|
||||
"mid_price": float(product_data.get("midPrice")) if product_data.get("midPrice") else None,
|
||||
"high_price": float(product_data.get("highPrice")) if product_data.get("highPrice") else None,
|
||||
"market_price": float(product_data.get("marketPrice")) if product_data.get("marketPrice") else None,
|
||||
"direct_low_price": float(product_data.get("directLowPrice")) if product_data.get("directLowPrice") else None,
|
||||
"sub_type_name": product_data.get("subTypeName")
|
||||
"ext_flavor_text": product_data.get("extFlavorText"),
|
||||
"ext_power": product_data.get("extPower"),
|
||||
"ext_toughness": product_data.get("extToughness"),
|
||||
"ext_flavor_text": product_data.get("extFlavorText")
|
||||
}.items():
|
||||
setattr(existing_product, key, value)
|
||||
else:
|
||||
logger.debug(f"Creating new product: {product_data['productId']} product name: {product_data['name']}")
|
||||
new_product = TCGPlayerProduct(
|
||||
product_id=product_data["productId"],
|
||||
tcgplayer_product_id=product_data["productId"],
|
||||
name=product_data["name"],
|
||||
normalized_sub_type_name=product_data.get("subTypeName").lower().replace(" ", "_") if product_data.get("subTypeName") else "other",
|
||||
clean_name=product_data.get("cleanName"),
|
||||
image_url=product_data.get("imageUrl"),
|
||||
category_id=product_data.get("categoryId"),
|
||||
@ -269,7 +282,7 @@ class DataInitializationService(BaseService):
|
||||
high_price=float(product_data.get("highPrice")) if product_data.get("highPrice") else None,
|
||||
market_price=float(product_data.get("marketPrice")) if product_data.get("marketPrice") else None,
|
||||
direct_low_price=float(product_data.get("directLowPrice")) if product_data.get("directLowPrice") else None,
|
||||
sub_type_name=product_data.get("subTypeName"),
|
||||
sub_type_name=product_data.get("subTypeName") if product_data.get("subTypeName") else "other",
|
||||
ext_power=product_data.get("extPower"),
|
||||
ext_toughness=product_data.get("extToughness"),
|
||||
ext_flavor_text=product_data.get("extFlavorText")
|
||||
@ -319,50 +332,81 @@ class DataInitializationService(BaseService):
|
||||
async def sync_archived_prices(self, db: Session, archived_prices_data: dict, date: datetime):
|
||||
"""Sync archived prices data to the database using bulk operations.
|
||||
Note: Historical prices are never updated, only new records are inserted."""
|
||||
from sqlalchemy import insert
|
||||
from app.models.tcgplayer_price_history import TCGPlayerPriceHistory
|
||||
|
||||
# Prepare data for bulk operations
|
||||
price_records = []
|
||||
|
||||
for price_data in archived_prices_data.get("results", []):
|
||||
record = {
|
||||
"product_id": price_data["productId"],
|
||||
"date": date,
|
||||
"sub_type_name": price_data["subTypeName"],
|
||||
"low_price": price_data.get("lowPrice"),
|
||||
"mid_price": price_data.get("midPrice"),
|
||||
"high_price": price_data.get("highPrice"),
|
||||
"market_price": price_data.get("marketPrice"),
|
||||
"direct_low_price": price_data.get("directLowPrice")
|
||||
}
|
||||
price_records.append(record)
|
||||
|
||||
if not price_records:
|
||||
if not archived_prices_data.get("success"):
|
||||
logger.error("Price data sync failed - success flag is false")
|
||||
return
|
||||
|
||||
# Get existing records in bulk to avoid duplicates
|
||||
product_ids = [r["product_id"] for r in price_records]
|
||||
sub_type_names = [r["sub_type_name"] for r in price_records]
|
||||
|
||||
# Get existing records in bulk to avoid duplicates using a composite key
|
||||
existing_records = db.query(TCGPlayerPriceHistory).filter(
|
||||
TCGPlayerPriceHistory.product_id.in_(product_ids),
|
||||
TCGPlayerPriceHistory.date == date,
|
||||
TCGPlayerPriceHistory.sub_type_name.in_(sub_type_names)
|
||||
TCGPlayerPriceHistory.date == date
|
||||
).all()
|
||||
|
||||
# Filter out existing records
|
||||
existing_keys = {(r.product_id, r.date, r.sub_type_name) for r in existing_records}
|
||||
to_insert = [
|
||||
record for record in price_records
|
||||
if (record["product_id"], record["date"], record["sub_type_name"]) not in existing_keys
|
||||
]
|
||||
|
||||
# Perform bulk insert for new records only
|
||||
if to_insert:
|
||||
stmt = insert(TCGPlayerPriceHistory)
|
||||
db.execute(stmt, to_insert)
|
||||
db.commit()
|
||||
# Prepare batch insert data
|
||||
price_history_batch = []
|
||||
|
||||
# Process price data in batches
|
||||
for price_data in archived_prices_data.get("results", []):
|
||||
try:
|
||||
# Get the subtype name from the price data
|
||||
sub_type_name = price_data.get("subTypeName", "None")
|
||||
|
||||
# First try to find product with the requested subtype
|
||||
product = db.query(TCGPlayerProduct).filter(
|
||||
TCGPlayerProduct.tcgplayer_product_id == price_data["productId"],
|
||||
TCGPlayerProduct.sub_type_name == sub_type_name
|
||||
).first()
|
||||
|
||||
# If not found and subtype isn't "other", try with "other" subtype
|
||||
if not product and sub_type_name != "other":
|
||||
product = db.query(TCGPlayerProduct).filter(
|
||||
TCGPlayerProduct.tcgplayer_product_id == price_data["productId"],
|
||||
TCGPlayerProduct.sub_type_name == "other"
|
||||
).first()
|
||||
if product:
|
||||
sub_type_name = "other"
|
||||
#logger.info(f"Found product {price_data['productId']} with 'other' subtype as fallback for {sub_type_name}")
|
||||
|
||||
if not product:
|
||||
logger.warning(f"No product found for {price_data['productId']} with subtype {sub_type_name} or 'other'")
|
||||
continue
|
||||
|
||||
# Skip if record already exists
|
||||
if (product.tcgplayer_product_id, date, sub_type_name) in existing_keys:
|
||||
continue
|
||||
|
||||
# Validate and convert price data
|
||||
try:
|
||||
price_history = TCGPlayerPriceHistory(
|
||||
product_id=product.tcgplayer_product_id,
|
||||
sub_type_name=sub_type_name,
|
||||
date=date,
|
||||
low_price=float(price_data.get("lowPrice")) if price_data.get("lowPrice") else None,
|
||||
mid_price=float(price_data.get("midPrice")) if price_data.get("midPrice") else None,
|
||||
high_price=float(price_data.get("highPrice")) if price_data.get("highPrice") else None,
|
||||
market_price=float(price_data.get("marketPrice")) if price_data.get("marketPrice") else None,
|
||||
direct_low_price=float(price_data.get("directLowPrice")) if price_data.get("directLowPrice") else None
|
||||
)
|
||||
price_history_batch.append(price_history)
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.error(f"Invalid price data for product {price_data['productId']}: {str(e)}")
|
||||
continue
|
||||
|
||||
# Process in batches of 1000
|
||||
if len(price_history_batch) >= 1000:
|
||||
with db_transaction(db):
|
||||
db.bulk_save_objects(price_history_batch)
|
||||
price_history_batch = []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing price data for product {price_data['productId']}: {str(e)}")
|
||||
continue
|
||||
|
||||
# Process any remaining records
|
||||
if price_history_batch:
|
||||
with db_transaction(db):
|
||||
db.bulk_save_objects(price_history_batch)
|
||||
|
||||
async def init_archived_prices(self, db: Session, start_date: datetime, end_date: datetime, use_cache: bool = True, game_ids: List[int] = None) -> bool:
|
||||
"""Initialize archived prices data"""
|
||||
@ -462,227 +506,194 @@ class DataInitializationService(BaseService):
|
||||
identifiers_count = 0
|
||||
skus_count = 0
|
||||
|
||||
# Process identifiers
|
||||
if use_cache:
|
||||
cached_file = await self.file_service.get_file_by_filename(db, "mtgjson_identifiers.json")
|
||||
if cached_file and os.path.exists(cached_file.path):
|
||||
logger.info("MTGJSON identifiers initialized from cache")
|
||||
identifiers_count = await self._process_streamed_data(
|
||||
db,
|
||||
self._stream_json_file(cached_file.path),
|
||||
"mtgjson_identifiers.json",
|
||||
"mtgjson",
|
||||
self.sync_mtgjson_identifiers
|
||||
)
|
||||
else:
|
||||
logger.info("Downloading MTGJSON identifiers from API")
|
||||
identifiers_count = await self._process_streamed_data(
|
||||
db,
|
||||
await mtgjson_service.get_identifiers(db),
|
||||
"mtgjson_identifiers.json",
|
||||
"mtgjson",
|
||||
self.sync_mtgjson_identifiers
|
||||
)
|
||||
else:
|
||||
logger.info("Downloading MTGJSON identifiers from API")
|
||||
identifiers_count = await self._process_streamed_data(
|
||||
db,
|
||||
await mtgjson_service.get_identifiers(db),
|
||||
"mtgjson_identifiers.json",
|
||||
"mtgjson",
|
||||
self.sync_mtgjson_identifiers
|
||||
)
|
||||
# Get identifiers data
|
||||
identifiers_data = await mtgjson_service.get_identifiers(db, use_cache)
|
||||
if identifiers_data and "data" in identifiers_data:
|
||||
identifiers_count = await self.sync_mtgjson_identifiers(db, list(identifiers_data["data"].values()))
|
||||
|
||||
# Process SKUs
|
||||
if use_cache:
|
||||
cached_file = await self.file_service.get_file_by_filename(db, "mtgjson_skus.json")
|
||||
if cached_file and os.path.exists(cached_file.path):
|
||||
logger.info("MTGJSON SKUs initialized from cache")
|
||||
skus_count = await self._process_streamed_data(
|
||||
db,
|
||||
self._stream_json_file(cached_file.path),
|
||||
"mtgjson_skus.json",
|
||||
"mtgjson",
|
||||
self.sync_mtgjson_skus
|
||||
)
|
||||
else:
|
||||
logger.info("Downloading MTGJSON SKUs from API")
|
||||
skus_count = await self._process_streamed_data(
|
||||
db,
|
||||
await mtgjson_service.get_skus(db),
|
||||
"mtgjson_skus.json",
|
||||
"mtgjson",
|
||||
self.sync_mtgjson_skus
|
||||
)
|
||||
else:
|
||||
logger.info("Downloading MTGJSON SKUs from API")
|
||||
skus_count = await self._process_streamed_data(
|
||||
db,
|
||||
await mtgjson_service.get_skus(db),
|
||||
"mtgjson_skus.json",
|
||||
"mtgjson",
|
||||
self.sync_mtgjson_skus
|
||||
)
|
||||
# Get SKUs data
|
||||
skus_data = await mtgjson_service.get_skus(db, use_cache)
|
||||
if skus_data and "data" in skus_data:
|
||||
skus_count = await self.sync_mtgjson_skus(db, skus_data)
|
||||
|
||||
return {
|
||||
"identifiers_processed": identifiers_count,
|
||||
"skus_processed": skus_count
|
||||
}
|
||||
|
||||
async def _process_streamed_data(
|
||||
self,
|
||||
db: Session,
|
||||
data_stream: Generator[Dict[str, Any], None, None],
|
||||
filename: str,
|
||||
subdir: str,
|
||||
sync_func: Callable
|
||||
) -> int:
|
||||
"""Process streamed data and sync to database"""
|
||||
async def sync_mtgjson_identifiers(self, db: Session, identifiers_data: List[dict]) -> int:
|
||||
|
||||
count = 0
|
||||
items = []
|
||||
batch_size = 1000
|
||||
with db_transaction(db):
|
||||
# Load all existing UUIDs once
|
||||
existing_cards = {
|
||||
card.mtgjson_uuid: card
|
||||
for card in db.query(MTGJSONCard).all()
|
||||
}
|
||||
|
||||
for item in data_stream:
|
||||
if item["type"] == "meta":
|
||||
# Handle meta data separately
|
||||
continue
|
||||
new_cards = []
|
||||
|
||||
count += 1
|
||||
items.append(item["data"])
|
||||
for card_data in identifiers_data:
|
||||
if not isinstance(card_data, dict):
|
||||
logger.debug(f"Skipping non-dict item: {card_data}")
|
||||
continue
|
||||
|
||||
# Process in batches
|
||||
if len(items) >= batch_size:
|
||||
await sync_func(db, items)
|
||||
items = []
|
||||
uuid = card_data.get("uuid")
|
||||
identifiers = card_data.get("identifiers", {})
|
||||
|
||||
# Process any remaining items
|
||||
if items:
|
||||
await sync_func(db, items)
|
||||
if uuid in existing_cards:
|
||||
card = existing_cards[uuid]
|
||||
updates = {
|
||||
"name": card_data.get("name"),
|
||||
"set_code": card_data.get("setCode"),
|
||||
"abu_id": identifiers.get("abuId"),
|
||||
"card_kingdom_etched_id": identifiers.get("cardKingdomEtchedId"),
|
||||
"card_kingdom_foil_id": identifiers.get("cardKingdomFoilId"),
|
||||
"card_kingdom_id": identifiers.get("cardKingdomId"),
|
||||
"cardsphere_id": identifiers.get("cardsphereId"),
|
||||
"cardsphere_foil_id": identifiers.get("cardsphereFoilId"),
|
||||
"cardtrader_id": identifiers.get("cardtraderId"),
|
||||
"csi_id": identifiers.get("csiId"),
|
||||
"mcm_id": identifiers.get("mcmId"),
|
||||
"mcm_meta_id": identifiers.get("mcmMetaId"),
|
||||
"miniaturemarket_id": identifiers.get("miniaturemarketId"),
|
||||
"mtg_arena_id": identifiers.get("mtgArenaId"),
|
||||
"mtgjson_foil_version_id": identifiers.get("mtgjsonFoilVersionId"),
|
||||
"mtgjson_non_foil_version_id": identifiers.get("mtgjsonNonFoilVersionId"),
|
||||
"mtgjson_v4_id": identifiers.get("mtgjsonV4Id"),
|
||||
"mtgo_foil_id": identifiers.get("mtgoFoilId"),
|
||||
"mtgo_id": identifiers.get("mtgoId"),
|
||||
"multiverse_id": identifiers.get("multiverseId"),
|
||||
"scg_id": identifiers.get("scgId"),
|
||||
"scryfall_id": identifiers.get("scryfallId"),
|
||||
"scryfall_card_back_id": identifiers.get("scryfallCardBackId"),
|
||||
"scryfall_oracle_id": identifiers.get("scryfallOracleId"),
|
||||
"scryfall_illustration_id": identifiers.get("scryfallIllustrationId"),
|
||||
"tcgplayer_product_id": identifiers.get("tcgplayerProductId"),
|
||||
"tcgplayer_etched_product_id": identifiers.get("tcgplayerEtchedProductId"),
|
||||
"tnt_id": identifiers.get("tntId")
|
||||
}
|
||||
|
||||
for k, v in updates.items():
|
||||
if getattr(card, k) != v:
|
||||
setattr(card, k, v)
|
||||
|
||||
else:
|
||||
new_cards.append(MTGJSONCard(
|
||||
mtgjson_uuid=uuid,
|
||||
name=card_data.get("name"),
|
||||
set_code=card_data.get("setCode"),
|
||||
abu_id=identifiers.get("abuId"),
|
||||
card_kingdom_etched_id=identifiers.get("cardKingdomEtchedId"),
|
||||
card_kingdom_foil_id=identifiers.get("cardKingdomFoilId"),
|
||||
card_kingdom_id=identifiers.get("cardKingdomId"),
|
||||
cardsphere_id=identifiers.get("cardsphereId"),
|
||||
cardsphere_foil_id=identifiers.get("cardsphereFoilId"),
|
||||
cardtrader_id=identifiers.get("cardtraderId"),
|
||||
csi_id=identifiers.get("csiId"),
|
||||
mcm_id=identifiers.get("mcmId"),
|
||||
mcm_meta_id=identifiers.get("mcmMetaId"),
|
||||
miniaturemarket_id=identifiers.get("miniaturemarketId"),
|
||||
mtg_arena_id=identifiers.get("mtgArenaId"),
|
||||
mtgjson_foil_version_id=identifiers.get("mtgjsonFoilVersionId"),
|
||||
mtgjson_non_foil_version_id=identifiers.get("mtgjsonNonFoilVersionId"),
|
||||
mtgjson_v4_id=identifiers.get("mtgjsonV4Id"),
|
||||
mtgo_foil_id=identifiers.get("mtgoFoilId"),
|
||||
mtgo_id=identifiers.get("mtgoId"),
|
||||
multiverse_id=identifiers.get("multiverseId"),
|
||||
scg_id=identifiers.get("scgId"),
|
||||
scryfall_id=identifiers.get("scryfallId"),
|
||||
scryfall_card_back_id=identifiers.get("scryfallCardBackId"),
|
||||
scryfall_oracle_id=identifiers.get("scryfallOracleId"),
|
||||
scryfall_illustration_id=identifiers.get("scryfallIllustrationId"),
|
||||
tcgplayer_product_id=identifiers.get("tcgplayerProductId"),
|
||||
tcgplayer_etched_product_id=identifiers.get("tcgplayerEtchedProductId"),
|
||||
tnt_id=identifiers.get("tntId")
|
||||
))
|
||||
|
||||
count += 1
|
||||
|
||||
if new_cards:
|
||||
db.bulk_save_objects(new_cards)
|
||||
|
||||
return count
|
||||
|
||||
async def sync_mtgjson_identifiers(self, db: Session, identifiers_data: dict):
|
||||
"""Sync MTGJSON identifiers data to the database"""
|
||||
from app.models.mtgjson_card import MTGJSONCard
|
||||
async def sync_mtgjson_skus(self, db: Session, skus_data: dict) -> int:
|
||||
count = 0
|
||||
sku_details_by_key = {}
|
||||
|
||||
with transaction(db):
|
||||
for card_id, card_data in identifiers_data.items():
|
||||
existing_card = db.query(MTGJSONCard).filter(MTGJSONCard.card_id == card_id).first()
|
||||
if existing_card:
|
||||
# Update existing card
|
||||
for key, value in {
|
||||
"name": card_data.get("name"),
|
||||
"set_code": card_data.get("setCode"),
|
||||
"uuid": card_data.get("uuid"),
|
||||
"abu_id": card_data.get("identifiers", {}).get("abuId"),
|
||||
"card_kingdom_etched_id": card_data.get("identifiers", {}).get("cardKingdomEtchedId"),
|
||||
"card_kingdom_foil_id": card_data.get("identifiers", {}).get("cardKingdomFoilId"),
|
||||
"card_kingdom_id": card_data.get("identifiers", {}).get("cardKingdomId"),
|
||||
"cardsphere_id": card_data.get("identifiers", {}).get("cardsphereId"),
|
||||
"cardsphere_foil_id": card_data.get("identifiers", {}).get("cardsphereFoilId"),
|
||||
"cardtrader_id": card_data.get("identifiers", {}).get("cardtraderId"),
|
||||
"csi_id": card_data.get("identifiers", {}).get("csiId"),
|
||||
"mcm_id": card_data.get("identifiers", {}).get("mcmId"),
|
||||
"mcm_meta_id": card_data.get("identifiers", {}).get("mcmMetaId"),
|
||||
"miniaturemarket_id": card_data.get("identifiers", {}).get("miniaturemarketId"),
|
||||
"mtg_arena_id": card_data.get("identifiers", {}).get("mtgArenaId"),
|
||||
"mtgjson_foil_version_id": card_data.get("identifiers", {}).get("mtgjsonFoilVersionId"),
|
||||
"mtgjson_non_foil_version_id": card_data.get("identifiers", {}).get("mtgjsonNonFoilVersionId"),
|
||||
"mtgjson_v4_id": card_data.get("identifiers", {}).get("mtgjsonV4Id"),
|
||||
"mtgo_foil_id": card_data.get("identifiers", {}).get("mtgoFoilId"),
|
||||
"mtgo_id": card_data.get("identifiers", {}).get("mtgoId"),
|
||||
"multiverse_id": card_data.get("identifiers", {}).get("multiverseId"),
|
||||
"scg_id": card_data.get("identifiers", {}).get("scgId"),
|
||||
"scryfall_id": card_data.get("identifiers", {}).get("scryfallId"),
|
||||
"scryfall_card_back_id": card_data.get("identifiers", {}).get("scryfallCardBackId"),
|
||||
"scryfall_oracle_id": card_data.get("identifiers", {}).get("scryfallOracleId"),
|
||||
"scryfall_illustration_id": card_data.get("identifiers", {}).get("scryfallIllustrationId"),
|
||||
"tcgplayer_product_id": card_data.get("identifiers", {}).get("tcgplayerProductId"),
|
||||
"tcgplayer_etched_product_id": card_data.get("identifiers", {}).get("tcgplayerEtchedProductId"),
|
||||
"tnt_id": card_data.get("identifiers", {}).get("tntId")
|
||||
}.items():
|
||||
setattr(existing_card, key, value)
|
||||
for mtgjson_uuid, product_data in skus_data["data"].items():
|
||||
for sku_data in product_data:
|
||||
sku_id = sku_data.get("skuId")
|
||||
if sku_id is None or sku_id in sku_details_by_key:
|
||||
continue # Skip if missing or already added
|
||||
|
||||
sku_details_by_key[sku_id] = {
|
||||
"mtgjson_uuid": mtgjson_uuid,
|
||||
"tcgplayer_sku_id": sku_id,
|
||||
"tcgplayer_product_id": sku_data.get("productId"),
|
||||
"printing": sku_data.get("printing"),
|
||||
"normalized_printing": sku_data.get("printing", "").lower().replace(" ", "_").replace("non_foil", "normal") if sku_data.get("printing") else None,
|
||||
"condition": sku_data.get("condition"),
|
||||
"finish": sku_data.get("finish"),
|
||||
"language": sku_data.get("language"),
|
||||
}
|
||||
|
||||
with db_transaction(db):
|
||||
db.flush()
|
||||
|
||||
valid_uuids = {uuid for (uuid,) in db.query(MTGJSONCard.mtgjson_uuid).all()}
|
||||
valid_product_keys = {
|
||||
(product.tcgplayer_product_id, product.normalized_sub_type_name)
|
||||
for product in db.query(TCGPlayerProduct.tcgplayer_product_id, TCGPlayerProduct.normalized_sub_type_name)
|
||||
}
|
||||
|
||||
existing_sku_ids = {
|
||||
sku.tcgplayer_sku_id
|
||||
for sku in db.query(MTGJSONSKU.tcgplayer_sku_id).all()
|
||||
}
|
||||
|
||||
existing = {
|
||||
(sku.mtgjson_uuid, sku.tcgplayer_sku_id): sku
|
||||
for sku in db.query(MTGJSONSKU).all()
|
||||
}
|
||||
|
||||
new_skus = []
|
||||
|
||||
for data in sku_details_by_key.values():
|
||||
sku_id = data["tcgplayer_sku_id"]
|
||||
|
||||
if sku_id in existing_sku_ids:
|
||||
continue
|
||||
|
||||
mtgjson_uuid = data["mtgjson_uuid"]
|
||||
product_id = data["tcgplayer_product_id"]
|
||||
normalized_printing = data["normalized_printing"]
|
||||
|
||||
if mtgjson_uuid not in valid_uuids:
|
||||
continue
|
||||
|
||||
if (product_id, normalized_printing) not in valid_product_keys:
|
||||
continue
|
||||
|
||||
key = (mtgjson_uuid, sku_id)
|
||||
|
||||
if key in existing:
|
||||
record = existing[key]
|
||||
for field, value in data.items():
|
||||
if field not in ("mtgjson_uuid", "tcgplayer_sku_id") and getattr(record, field) != value:
|
||||
setattr(record, field, value)
|
||||
else:
|
||||
new_card = MTGJSONCard(
|
||||
card_id=card_id,
|
||||
name=card_data.get("name"),
|
||||
set_code=card_data.get("setCode"),
|
||||
uuid=card_data.get("uuid"),
|
||||
abu_id=card_data.get("identifiers", {}).get("abuId"),
|
||||
card_kingdom_etched_id=card_data.get("identifiers", {}).get("cardKingdomEtchedId"),
|
||||
card_kingdom_foil_id=card_data.get("identifiers", {}).get("cardKingdomFoilId"),
|
||||
card_kingdom_id=card_data.get("identifiers", {}).get("cardKingdomId"),
|
||||
cardsphere_id=card_data.get("identifiers", {}).get("cardsphereId"),
|
||||
cardsphere_foil_id=card_data.get("identifiers", {}).get("cardsphereFoilId"),
|
||||
cardtrader_id=card_data.get("identifiers", {}).get("cardtraderId"),
|
||||
csi_id=card_data.get("identifiers", {}).get("csiId"),
|
||||
mcm_id=card_data.get("identifiers", {}).get("mcmId"),
|
||||
mcm_meta_id=card_data.get("identifiers", {}).get("mcmMetaId"),
|
||||
miniaturemarket_id=card_data.get("identifiers", {}).get("miniaturemarketId"),
|
||||
mtg_arena_id=card_data.get("identifiers", {}).get("mtgArenaId"),
|
||||
mtgjson_foil_version_id=card_data.get("identifiers", {}).get("mtgjsonFoilVersionId"),
|
||||
mtgjson_non_foil_version_id=card_data.get("identifiers", {}).get("mtgjsonNonFoilVersionId"),
|
||||
mtgjson_v4_id=card_data.get("identifiers", {}).get("mtgjsonV4Id"),
|
||||
mtgo_foil_id=card_data.get("identifiers", {}).get("mtgoFoilId"),
|
||||
mtgo_id=card_data.get("identifiers", {}).get("mtgoId"),
|
||||
multiverse_id=card_data.get("identifiers", {}).get("multiverseId"),
|
||||
scg_id=card_data.get("identifiers", {}).get("scgId"),
|
||||
scryfall_id=card_data.get("identifiers", {}).get("scryfallId"),
|
||||
scryfall_card_back_id=card_data.get("identifiers", {}).get("scryfallCardBackId"),
|
||||
scryfall_oracle_id=card_data.get("identifiers", {}).get("scryfallOracleId"),
|
||||
scryfall_illustration_id=card_data.get("identifiers", {}).get("scryfallIllustrationId"),
|
||||
tcgplayer_product_id=card_data.get("identifiers", {}).get("tcgplayerProductId"),
|
||||
tcgplayer_etched_product_id=card_data.get("identifiers", {}).get("tcgplayerEtchedProductId"),
|
||||
tnt_id=card_data.get("identifiers", {}).get("tntId")
|
||||
)
|
||||
db.add(new_card)
|
||||
new_skus.append(MTGJSONSKU(**data))
|
||||
count += 1
|
||||
|
||||
if new_skus:
|
||||
db.bulk_save_objects(new_skus)
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def sync_mtgjson_skus(self, db: Session, skus_data: dict):
|
||||
"""Sync MTGJSON SKUs data to the database"""
|
||||
from app.models.mtgjson_sku import MTGJSONSKU
|
||||
|
||||
with transaction(db):
|
||||
for card_uuid, sku_list in skus_data.items():
|
||||
for sku in sku_list:
|
||||
# Handle case where sku is a string (skuId)
|
||||
if isinstance(sku, str):
|
||||
sku_id = sku
|
||||
existing_sku = db.query(MTGJSONSKU).filter(MTGJSONSKU.sku_id == sku_id).first()
|
||||
if existing_sku:
|
||||
# Update existing SKU
|
||||
existing_sku.card_id = card_uuid
|
||||
else:
|
||||
new_sku = MTGJSONSKU(
|
||||
sku_id=sku_id,
|
||||
card_id=card_uuid
|
||||
)
|
||||
db.add(new_sku)
|
||||
# Handle case where sku is a dictionary
|
||||
else:
|
||||
sku_id = str(sku.get("skuId"))
|
||||
existing_sku = db.query(MTGJSONSKU).filter(MTGJSONSKU.sku_id == sku_id).first()
|
||||
if existing_sku:
|
||||
# Update existing SKU
|
||||
for key, value in {
|
||||
"product_id": str(sku.get("productId")),
|
||||
"condition": sku.get("condition"),
|
||||
"finish": sku.get("finish"),
|
||||
"language": sku.get("language"),
|
||||
"printing": sku.get("printing"),
|
||||
"card_id": card_uuid
|
||||
}.items():
|
||||
setattr(existing_sku, key, value)
|
||||
else:
|
||||
new_sku = MTGJSONSKU(
|
||||
sku_id=sku_id,
|
||||
product_id=str(sku.get("productId")),
|
||||
condition=sku.get("condition"),
|
||||
finish=sku.get("finish"),
|
||||
language=sku.get("language"),
|
||||
printing=sku.get("printing"),
|
||||
card_id=card_uuid
|
||||
)
|
||||
db.add(new_sku)
|
||||
|
||||
async def initialize_data(
|
||||
self,
|
||||
@ -735,3 +746,65 @@ class DataInitializationService(BaseService):
|
||||
await self.file_service.delete_file(db, file.id)
|
||||
await self.mtgjson_service.clear_cache()
|
||||
print("Cache cleared")
|
||||
|
||||
async def initialize_inventory_data(self, db: Session) -> None:
|
||||
"""Initialize inventory data"""
|
||||
with db_transaction(db):
|
||||
logger.info("Initializing inventory data...")
|
||||
# set expected value
|
||||
expected_value_box = SealedExpectedValue(
|
||||
tcgplayer_product_id=619645,
|
||||
expected_value=136.42
|
||||
)
|
||||
db.add(expected_value_box)
|
||||
#db.flush()
|
||||
#expected_value_case = SealedExpectedValue(
|
||||
# tcgplayer_product_id=562119,
|
||||
# expected_value=820.69
|
||||
#)
|
||||
#db.add(expected_value_case)
|
||||
db.flush()
|
||||
|
||||
inventory_service = self.get_service("inventory")
|
||||
customer = await inventory_service.create_customer(db, "Bob Smith")
|
||||
vendor = await inventory_service.create_vendor(db, "Joe Blow")
|
||||
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
|
||||
transaction = await inventory_service.create_purchase_transaction(db, PurchaseTransactionCreate(
|
||||
vendor_id=vendor.id,
|
||||
transaction_date=datetime.now(),
|
||||
items=[PurchaseItem(product_id=619645, unit_price=100, quantity=1, item_type="box")],
|
||||
transaction_notes="tdm real box test"
|
||||
#PurchaseItem(product_id=562119, unit_price=800.01, quantity=2, is_case=True, num_boxes=6)],
|
||||
#transaction_notes="Test Transaction: 1 case and 2 boxes of foundations"
|
||||
))
|
||||
logger.info(f"Transaction created: {transaction}")
|
||||
case_num = 0
|
||||
for item in transaction.transaction_items:
|
||||
logger.info(f"Item: {item}")
|
||||
if item.inventory_item.physical_item.item_type == "box":
|
||||
manabox_service = self.get_service("manabox")
|
||||
#file_path = 'app/data/test_data/manabox_test_file.csv'
|
||||
file_path = 'app/data/test_data/dragon.csv'
|
||||
file_bytes = open(file_path, 'rb').read()
|
||||
manabox_file = await manabox_service.process_manabox_csv(db, file_bytes, {"source": "test", "description": "test"}, wait=True)
|
||||
# Ensure manabox_file is a list before passing it
|
||||
if not isinstance(manabox_file, list):
|
||||
manabox_file = [manabox_file]
|
||||
box_service = self.get_service("box")
|
||||
open_event = await box_service.open_box(db, item.inventory_item.physical_item, manabox_file)
|
||||
# get all cards from box
|
||||
cards = open_event.resulting_items if open_event.resulting_items else []
|
||||
marketplace_listing_service = self.get_service("marketplace_listing")
|
||||
for card in cards:
|
||||
logger.info(f"card: {card}")
|
||||
# create marketplace listing
|
||||
await marketplace_listing_service.create_marketplace_listing(db, card.inventory_item, marketplace)
|
||||
elif item.inventory_item.physical_item.item_type == "case":
|
||||
if case_num == 0:
|
||||
logger.info(f"sealed case {case_num} opening...")
|
||||
case_service = self.get_service("case")
|
||||
success = await case_service.open_case(db, item.inventory_item.physical_item, 562119)
|
||||
logger.info(f"sealed case {case_num} opening success: {success}")
|
||||
case_num += 1
|
||||
|
||||
logger.info("Inventory data initialized")
|
||||
|
@ -2,7 +2,8 @@ import os
|
||||
import json
|
||||
import zipfile
|
||||
import time
|
||||
from typing import Dict, Any, Optional, Generator
|
||||
import shutil
|
||||
from typing import Dict, Any, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from app.services.external_api.base_external_service import BaseExternalService
|
||||
from app.schemas.file import FileInDB
|
||||
@ -11,32 +12,10 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MTGJSONService(BaseExternalService):
|
||||
def __init__(self, cache_dir: str = "app/data/cache/mtgjson"):
|
||||
def __init__(self):
|
||||
super().__init__(base_url="https://mtgjson.com/api/v5/")
|
||||
# Ensure the cache directory exists
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
self.cache_dir = cache_dir
|
||||
self.identifiers_dir = os.path.join(cache_dir, "identifiers")
|
||||
self.skus_dir = os.path.join(cache_dir, "skus")
|
||||
# Ensure subdirectories exist
|
||||
os.makedirs(self.identifiers_dir, exist_ok=True)
|
||||
os.makedirs(self.skus_dir, exist_ok=True)
|
||||
|
||||
def _format_progress(self, current: int, total: int, start_time: float) -> str:
|
||||
"""Format a progress message with percentage and timing information"""
|
||||
elapsed = time.time() - start_time
|
||||
if total > 0:
|
||||
percent = (current / total) * 100
|
||||
items_per_second = current / elapsed if elapsed > 0 else 0
|
||||
eta = (total - current) / items_per_second if items_per_second > 0 else 0
|
||||
return f"[{current}/{total} ({percent:.1f}%)] {items_per_second:.1f} items/sec, ETA: {eta:.1f}s"
|
||||
return f"[{current} items] {current/elapsed:.1f} items/sec"
|
||||
|
||||
def _print_progress(self, message: str, end: str = "\n") -> None:
|
||||
"""Print progress message with flush"""
|
||||
print(message, end=end, flush=True)
|
||||
|
||||
async def _download_file(self, db: Session, url: str, filename: str, subdir: str) -> FileInDB:
|
||||
async def _download_and_unzip_file(self, db: Session, url: str, filename: str, subdir: str) -> FileInDB:
|
||||
"""Download a file from the given URL and save it using FileService"""
|
||||
print(f"Downloading {url}...")
|
||||
start_time = time.time()
|
||||
@ -49,7 +28,7 @@ class MTGJSONService(BaseExternalService):
|
||||
)
|
||||
|
||||
# Save the file using the file service
|
||||
return await self.file_service.save_file(
|
||||
file_record = await self.file_service.save_file(
|
||||
db=db,
|
||||
file_data=file_data,
|
||||
filename=filename,
|
||||
@ -58,17 +37,23 @@ class MTGJSONService(BaseExternalService):
|
||||
content_type="application/zip"
|
||||
)
|
||||
|
||||
async def _unzip_file(self, file_record: FileInDB, subdir: str, db: Session) -> str:
|
||||
# Unzip the file
|
||||
await self._unzip_file(file_record, subdir, db)
|
||||
|
||||
return file_record
|
||||
|
||||
|
||||
async def _unzip_file(self, file_record: FileInDB, subdir: str, db: Session) -> FileInDB:
|
||||
"""Unzip a file to the specified subdirectory and return the path to the extracted JSON file"""
|
||||
try:
|
||||
# Use the appropriate subdirectory based on the type
|
||||
extract_path = self.identifiers_dir if subdir == "identifiers" else self.skus_dir
|
||||
os.makedirs(extract_path, exist_ok=True)
|
||||
|
||||
file_service = self.get_service('file')
|
||||
cache_dir = file_service.base_cache_dir
|
||||
temp_dir = os.path.join(cache_dir,'mtgjson', subdir, 'temp')
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
with zipfile.ZipFile(file_record.path, 'r') as zip_ref:
|
||||
json_filename = zip_ref.namelist()[0]
|
||||
zip_ref.extractall(extract_path)
|
||||
json_path = os.path.join(extract_path, json_filename)
|
||||
zip_ref.extractall(temp_dir)
|
||||
json_path = os.path.join(temp_dir, json_filename)
|
||||
|
||||
# Create a file record for the extracted JSON file
|
||||
with open(json_path, 'r') as f:
|
||||
@ -82,127 +67,61 @@ class MTGJSONService(BaseExternalService):
|
||||
content_type="application/json"
|
||||
)
|
||||
|
||||
return str(json_file_record.path)
|
||||
# remove the temp directory
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
return json_file_record
|
||||
except Exception as e:
|
||||
logger.error(f"Error unzipping file: {e}")
|
||||
raise
|
||||
|
||||
def _stream_json_file(self, file_path: str) -> Generator[Dict[str, Any], None, None]:
|
||||
"""Stream a JSON file and yield items one at a time using a streaming parser"""
|
||||
logger.info(f"Starting to stream JSON file: {file_path}")
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
# First, we need to find the start of the data section
|
||||
data_started = False
|
||||
current_key = None
|
||||
current_value = []
|
||||
brace_count = 0
|
||||
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
if not data_started:
|
||||
if '"data":' in line:
|
||||
data_started = True
|
||||
# Skip the opening brace of the data object
|
||||
line = line[line.find('"data":') + 7:].strip()
|
||||
if line.startswith('{'):
|
||||
line = line[1:].strip()
|
||||
else:
|
||||
# Yield meta data if found
|
||||
if '"meta":' in line:
|
||||
meta_start = line.find('"meta":') + 7
|
||||
meta_end = line.rfind('}')
|
||||
if meta_end > meta_start:
|
||||
meta_json = line[meta_start:meta_end + 1]
|
||||
try:
|
||||
meta_data = json.loads(meta_json)
|
||||
yield {"type": "meta", "data": meta_data}
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"Failed to parse meta data: {e}")
|
||||
continue
|
||||
|
||||
# Process the data section
|
||||
if data_started:
|
||||
if not current_key:
|
||||
# Look for a new key
|
||||
if '"' in line:
|
||||
key_start = line.find('"') + 1
|
||||
key_end = line.find('"', key_start)
|
||||
if key_end > key_start:
|
||||
current_key = line[key_start:key_end]
|
||||
# Get the rest of the line after the key
|
||||
line = line[key_end + 1:].strip()
|
||||
if ':' in line:
|
||||
line = line[line.find(':') + 1:].strip()
|
||||
|
||||
if current_key:
|
||||
# Accumulate the value
|
||||
current_value.append(line)
|
||||
brace_count += line.count('{') - line.count('}')
|
||||
|
||||
if brace_count == 0 and line.endswith(','):
|
||||
# We have a complete value
|
||||
value_str = ''.join(current_value).rstrip(',')
|
||||
try:
|
||||
value = json.loads(value_str)
|
||||
yield {"type": "item", "data": {current_key: value}}
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"Failed to parse value for key {current_key}: {e}")
|
||||
current_key = None
|
||||
current_value = []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error streaming JSON file: {e}")
|
||||
raise
|
||||
|
||||
async def get_identifiers(self, db: Session) -> Generator[Dict[str, Any], None, None]:
|
||||
async def get_identifiers(self, db: Session, use_cache: bool = True) -> Dict[str, Any]:
|
||||
"""Download and get MTGJSON identifiers data"""
|
||||
# Check if we have a cached version
|
||||
cached_file = await self.file_service.get_file_by_filename(db, "AllIdentifiers.json")
|
||||
if cached_file:
|
||||
# Ensure the file exists at the path
|
||||
if os.path.exists(cached_file.path):
|
||||
return self._stream_json_file(cached_file.path)
|
||||
if cached_file and os.path.exists(cached_file.path) and use_cache:
|
||||
with open(cached_file.path, 'r') as f:
|
||||
logger.debug(f"Loaded identifiers from cache: {cached_file.path}")
|
||||
return json.load(f)
|
||||
else:
|
||||
# Download and process the file
|
||||
logger.debug(f"Downloading identifiers from MTGJSON")
|
||||
file_record = await self._download_and_unzip_file(
|
||||
db=db,
|
||||
url="https://mtgjson.com/api/v5/AllIdentifiers.json.zip",
|
||||
filename="AllIdentifiers.json.zip",
|
||||
subdir="identifiers"
|
||||
)
|
||||
|
||||
# Download and process the file
|
||||
file_record = await self._download_file(
|
||||
db=db,
|
||||
url="https://mtgjson.com/api/v5/AllIdentifiers.json.zip",
|
||||
filename="AllIdentifiers.json.zip",
|
||||
subdir="identifiers"
|
||||
)
|
||||
json_file = await self._unzip_file(file_record, "identifiers", db)
|
||||
|
||||
# Unzip and process the file
|
||||
json_path = await self._unzip_file(file_record, "identifiers", db)
|
||||
with open(json_file.path, 'r') as f:
|
||||
logger.debug(f"Loaded identifiers from MTGJSON: {json_file.path}")
|
||||
return json.load(f)
|
||||
|
||||
# Return a generator that streams the JSON file
|
||||
return self._stream_json_file(json_path)
|
||||
|
||||
async def get_skus(self, db: Session) -> Generator[Dict[str, Any], None, None]:
|
||||
async def get_skus(self, db: Session, use_cache: bool = True) -> Dict[str, Any]:
|
||||
"""Download and get MTGJSON SKUs data"""
|
||||
# Check if we have a cached version
|
||||
cached_file = await self.file_service.get_file_by_filename(db, "TcgplayerSkus.json")
|
||||
if cached_file:
|
||||
# Ensure the file exists at the path
|
||||
if os.path.exists(cached_file.path):
|
||||
return self._stream_json_file(cached_file.path)
|
||||
if cached_file and os.path.exists(cached_file.path) and use_cache:
|
||||
with open(cached_file.path, 'r') as f:
|
||||
logger.debug(f"Loaded SKUs from cache: {cached_file.path}")
|
||||
return json.load(f)
|
||||
else:
|
||||
# Download and process the file
|
||||
logger.debug(f"Downloading SKUs from MTGJSON")
|
||||
file_record = await self._download_and_unzip_file(
|
||||
db=db,
|
||||
url="https://mtgjson.com/api/v5/TcgplayerSkus.json.zip",
|
||||
filename="TcgplayerSkus.json.zip",
|
||||
subdir="skus"
|
||||
)
|
||||
|
||||
# Download and process the file
|
||||
file_record = await self._download_file(
|
||||
db=db,
|
||||
url="https://mtgjson.com/api/v5/TcgplayerSkus.json.zip",
|
||||
filename="TcgplayerSkus.json.zip",
|
||||
subdir="skus"
|
||||
)
|
||||
json_file = await self._unzip_file(file_record, "skus", db)
|
||||
|
||||
# Unzip and process the file
|
||||
json_path = await self._unzip_file(file_record, "skus", db)
|
||||
|
||||
# Return a generator that streams the JSON file
|
||||
return self._stream_json_file(json_path)
|
||||
with open(json_file.path, 'r') as f:
|
||||
logger.debug(f"Loaded SKUs from MTGJSON: {json_file.path}")
|
||||
return json.load(f)
|
||||
|
||||
async def clear_cache(self, db: Session) -> None:
|
||||
"""Clear all cached data"""
|
||||
|
@ -16,6 +16,7 @@ from app.models.tcgplayer_order import (
|
||||
TCGPlayerOrderProduct,
|
||||
TCGPlayerOrderRefund
|
||||
)
|
||||
from app.models.tcgplayer_products import TCGPlayerProduct
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.database import transaction
|
||||
import os
|
||||
@ -190,8 +191,7 @@ class OrderManagementService(BaseTCGPlayerService):
|
||||
direct_fee_amount=api_order.transaction.directFeeAmount,
|
||||
taxes=[{"code": t.code, "amount": t.amount} for t in api_order.transaction.taxes]
|
||||
)
|
||||
|
||||
# Create products
|
||||
# Create products
|
||||
db_products = [
|
||||
TCGPlayerOrderProductCreate(
|
||||
order_number=api_order.orderNumber,
|
||||
@ -376,8 +376,8 @@ class OrderManagementService(BaseTCGPlayerService):
|
||||
('extended_price', 'extendedPrice'),
|
||||
('quantity', 'quantity'),
|
||||
('url', 'url'),
|
||||
('product_id', 'productId'),
|
||||
('sku_id', 'skuId')
|
||||
('tcgplayer_product_id', 'productId'),
|
||||
('tcgplayer_sku_id', 'skuId')
|
||||
]
|
||||
|
||||
for db_field, api_field in product_fields_to_compare:
|
||||
|
@ -2,6 +2,11 @@ from typing import Dict, List, Optional
|
||||
from app.services.external_api.tcgplayer.base_tcgplayer_service import BaseTCGPlayerService
|
||||
from sqlalchemy.orm import Session
|
||||
from app.schemas.file import FileInDB
|
||||
from app.models.tcgplayer_inventory import TCGPlayerInventory, UnmanagedTCGPlayerInventory
|
||||
import csv
|
||||
from app.db.database import transaction
|
||||
from app.models.inventory_management import MarketplaceListing, InventoryItem, Marketplace
|
||||
from sqlalchemy import func
|
||||
|
||||
class TCGPlayerInventoryService(BaseTCGPlayerService):
|
||||
def __init__(self):
|
||||
@ -24,10 +29,101 @@ class TCGPlayerInventoryService(BaseTCGPlayerService):
|
||||
raise ValueError(f"Invalid export type: {export_type}, must be 'staged', 'live', or 'pricing'")
|
||||
|
||||
file_bytes = await self._make_request("GET", endpoint, download_file=True)
|
||||
return await self.save_file(
|
||||
return await self.file_service.save_file(
|
||||
db=db,
|
||||
file_data=file_bytes,
|
||||
file_name=f"tcgplayer_{export_type}_export.csv",
|
||||
filename=f"tcgplayer_{export_type}_export.csv",
|
||||
subdir="tcgplayer/inventory",
|
||||
file_type=file_type
|
||||
)
|
||||
|
||||
async def refresh_tcgplayer_inventory_table(self, db: Session):
|
||||
"""
|
||||
Refresh the TCGPlayer inventory table
|
||||
"""
|
||||
export = await self.get_tcgplayer_export(db, "live")
|
||||
csv_string = await self.file_service.file_in_db_to_csv(db, export)
|
||||
reader = csv.DictReader(csv_string.splitlines())
|
||||
|
||||
# Convert CSV rows to list of dictionaries for bulk insert
|
||||
inventory_data = []
|
||||
for row in reader:
|
||||
if row.get("TCGplayer Id") is None:
|
||||
continue
|
||||
inventory_data.append({
|
||||
"tcgplayer_sku_id": int(row.get("TCGplayer Id")),
|
||||
"product_line": row.get("Product Line") if row.get("Product Line") else None,
|
||||
"set_name": row.get("Set Name") if row.get("Set Name") else None,
|
||||
"product_name": row.get("Product Name") if row.get("Product Name") else None,
|
||||
"title": row.get("Title") if row.get("Title") else None,
|
||||
"number": row.get("Number") if row.get("Number") else None,
|
||||
"rarity": row.get("Rarity") if row.get("Rarity") else None,
|
||||
"condition": row.get("Condition") if row.get("Condition") else None,
|
||||
"tcg_market_price": float(row.get("TCG Market Price")) if row.get("TCG Market Price") else None,
|
||||
"tcg_direct_low": float(row.get("TCG Direct Low")) if row.get("TCG Direct Low") else None,
|
||||
"tcg_low_price_with_shipping": float(row.get("TCG Low Price With Shipping")) if row.get("TCG Low Price With Shipping") else None,
|
||||
"tcg_low_price": float(row.get("TCG Low Price")) if row.get("TCG Low Price") else None,
|
||||
"total_quantity": int(row.get("Total Quantity")) if row.get("Total Quantity") else None,
|
||||
"add_to_quantity": int(row.get("Add to Quantity")) if row.get("Add to Quantity") else None,
|
||||
"tcg_marketplace_price": float(row.get("TCG Marketplace Price")) if row.get("TCG Marketplace Price") else None,
|
||||
"photo_url": row.get("Photo URL") if row.get("Photo URL") else None
|
||||
})
|
||||
|
||||
with transaction(db):
|
||||
# Bulk insert new data
|
||||
db.bulk_insert_mappings(TCGPlayerInventory, inventory_data)
|
||||
|
||||
async def refresh_unmanaged_tcgplayer_inventory_table(self, db: Session):
|
||||
"""
|
||||
Refresh the TCGPlayer unmanaged inventory table
|
||||
unmanaged inventory is any inventory that cannot be mapped to a card with a marketplace listing
|
||||
"""
|
||||
with transaction(db):
|
||||
# Get active marketplace listings with their physical items in a single query
|
||||
listed_cards = (
|
||||
db.query(MarketplaceListing)
|
||||
.join(MarketplaceListing.inventory_item)
|
||||
.join(InventoryItem.physical_item)
|
||||
.filter(
|
||||
func.lower(Marketplace.name) == func.lower("tcgplayer"),
|
||||
MarketplaceListing.delisting_date == None,
|
||||
MarketplaceListing.deleted_at == None,
|
||||
MarketplaceListing.listing_date != None
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Get current inventory and create lookup dict
|
||||
current_inventory = db.query(TCGPlayerInventory).all()
|
||||
|
||||
# Create a set of SKUs that have active listings
|
||||
listed_skus = {
|
||||
card.inventory_item.physical_item.tcgplayer_sku_id
|
||||
for card in listed_cards
|
||||
}
|
||||
|
||||
unmanaged_inventory = []
|
||||
for inventory in current_inventory:
|
||||
# Only include SKUs that have no active listings
|
||||
if inventory.tcgplayer_sku_id not in listed_skus:
|
||||
unmanaged_inventory.append({
|
||||
"tcgplayer_inventory_id": inventory.id,
|
||||
"tcgplayer_sku_id": inventory.tcgplayer_sku_id,
|
||||
"product_line": inventory.product_line,
|
||||
"set_name": inventory.set_name,
|
||||
"product_name": inventory.product_name,
|
||||
"title": inventory.title,
|
||||
"number": inventory.number,
|
||||
"rarity": inventory.rarity,
|
||||
"condition": inventory.condition,
|
||||
"tcg_market_price": inventory.tcg_market_price,
|
||||
"tcg_direct_low": inventory.tcg_direct_low,
|
||||
"tcg_low_price_with_shipping": inventory.tcg_low_price_with_shipping,
|
||||
"tcg_low_price": inventory.tcg_low_price,
|
||||
"total_quantity": inventory.total_quantity,
|
||||
"add_to_quantity": inventory.add_to_quantity,
|
||||
"tcg_marketplace_price": inventory.tcg_marketplace_price,
|
||||
"photo_url": inventory.photo_url
|
||||
})
|
||||
|
||||
db.bulk_insert_mappings(UnmanagedTCGPlayerInventory, unmanaged_inventory)
|
||||
|
@ -6,8 +6,6 @@ import json
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.database import transaction
|
||||
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
||||
from app.models.tcgplayer_product import TCGPlayerProduct
|
||||
from app.services.inventory_service import InventoryService
|
||||
|
||||
class FileProcessingService:
|
||||
|
@ -120,7 +120,7 @@ class FileService:
|
||||
"""List files with optional filtering"""
|
||||
query = db.query(File)
|
||||
if file_type:
|
||||
query = query.filter(File.type == file_type)
|
||||
query = query.filter(File.file_type == file_type).filter(File.deleted_at == None).order_by(File.created_at.desc())
|
||||
files = query.offset(skip).limit(limit).all()
|
||||
return [FileInDB.model_validate(file) for file in files]
|
||||
|
||||
@ -153,7 +153,13 @@ class FileService:
|
||||
|
||||
async def get_file_by_filename(self, db: Session, filename: str) -> Optional[FileInDB]:
|
||||
"""Get a file record from the database by filename"""
|
||||
file_record = db.query(File).filter(File.name == filename).first()
|
||||
# get most recent file by filename
|
||||
file_record = db.query(File).filter(File.name == filename).order_by(File.created_at.desc()).first()
|
||||
if file_record:
|
||||
return FileInDB.model_validate(file_record)
|
||||
return None
|
||||
|
||||
async def file_in_db_to_csv(self, db: Session, file: FileInDB) -> str:
|
||||
"""Convert a file in the database to a CSV string"""
|
||||
with open(file.path, "r") as f:
|
||||
return f.read()
|
||||
|
@ -1,63 +1,505 @@
|
||||
from typing import List, Optional, Dict
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
||||
from typing import List, Optional, Dict, TypedDict
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from decimal import Decimal
|
||||
from app.services.base_service import BaseService
|
||||
from app.models.manabox_import_staging import ManaboxImportStaging
|
||||
from app.contexts.inventory_item import InventoryItemContextFactory
|
||||
from app.models.inventory_management import (
|
||||
OpenEvent, Card, InventoryItem, Case, SealedExpectedValue,
|
||||
Transaction, TransactionItem, Customer, Vendor, Marketplace, Box, MarketplaceListing
|
||||
)
|
||||
from app.schemas.file import FileInDB
|
||||
from app.models.inventory_management import PhysicalItem
|
||||
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, TransactionResponse, SealedExpectedValueCreate
|
||||
from app.db.database import transaction as db_transaction
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
import logging
|
||||
|
||||
class InventoryService(BaseService[TCGPlayerInventory]):
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class InventoryService(BaseService):
|
||||
def __init__(self):
|
||||
super().__init__(TCGPlayerInventory)
|
||||
super().__init__(None)
|
||||
|
||||
def create(self, db: Session, obj_in: Dict) -> TCGPlayerInventory:
|
||||
async def get_resulting_items_for_open_event(self, db: Session, open_event: OpenEvent) -> List[InventoryItem]:
|
||||
# Get the IDs of resulting items
|
||||
resulting_item_ids = [item.id for item in open_event.resulting_items]
|
||||
# Query using the IDs
|
||||
return db.query(InventoryItem).filter(InventoryItem.physical_item_id.in_(resulting_item_ids)).filter(InventoryItem.deleted_at == None).all()
|
||||
|
||||
async def get_open_event(self, db: Session, inventory_item: InventoryItem, open_event_id: int) -> OpenEvent:
|
||||
return db.query(OpenEvent).filter(OpenEvent.source_item == inventory_item.physical_item).filter(OpenEvent.id == open_event_id).filter(OpenEvent.deleted_at == None).first()
|
||||
|
||||
async def get_open_events_for_inventory_item(self, db: Session, inventory_item: InventoryItem) -> List[OpenEvent]:
|
||||
return db.query(OpenEvent).filter(OpenEvent.source_item == inventory_item.physical_item).filter(OpenEvent.deleted_at == None).all()
|
||||
|
||||
async def get_inventory_item(self, db: Session, inventory_item_id: int) -> InventoryItem:
|
||||
return db.query(InventoryItem)\
|
||||
.options(
|
||||
joinedload(InventoryItem.physical_item).joinedload(PhysicalItem.product_direct)
|
||||
)\
|
||||
.filter(InventoryItem.id == inventory_item_id)\
|
||||
.first()
|
||||
|
||||
async def get_expected_value(self, db: Session, product_id: int) -> float:
|
||||
expected_value = db.query(SealedExpectedValue).filter(SealedExpectedValue.tcgplayer_product_id == product_id).filter(SealedExpectedValue.deleted_at == None).first()
|
||||
return expected_value.expected_value if expected_value else None
|
||||
|
||||
async def get_transactions(self, db: Session, skip: int, limit: int) -> List[Transaction]:
|
||||
return db.query(Transaction)\
|
||||
.filter(Transaction.deleted_at == None)\
|
||||
.order_by(Transaction.transaction_date.desc())\
|
||||
.offset(skip)\
|
||||
.limit(limit)\
|
||||
.all()
|
||||
|
||||
async def get_transaction(self, db: Session, transaction_id: int) -> Transaction:
|
||||
return db.query(Transaction)\
|
||||
.options(
|
||||
joinedload(Transaction.transaction_items).joinedload(TransactionItem.inventory_item).joinedload(InventoryItem.physical_item).joinedload(PhysicalItem.product_direct),
|
||||
joinedload(Transaction.vendors),
|
||||
joinedload(Transaction.customers),
|
||||
joinedload(Transaction.marketplaces)
|
||||
)\
|
||||
.filter(Transaction.id == transaction_id)\
|
||||
.filter(Transaction.deleted_at == None)\
|
||||
.first()
|
||||
|
||||
async def create_expected_value(self, db: Session, expected_value_data: SealedExpectedValueCreate) -> SealedExpectedValue:
|
||||
with db_transaction(db):
|
||||
expected_value = SealedExpectedValue(
|
||||
tcgplayer_product_id=expected_value_data.tcgplayer_product_id,
|
||||
expected_value=expected_value_data.expected_value
|
||||
)
|
||||
db.add(expected_value)
|
||||
db.flush()
|
||||
return expected_value
|
||||
|
||||
async def create_purchase_transaction(
|
||||
self,
|
||||
db: Session,
|
||||
transaction_data: PurchaseTransactionCreate
|
||||
) -> Transaction:
|
||||
"""
|
||||
Create a new inventory item in the database.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
obj_in: Dictionary containing inventory data
|
||||
|
||||
Returns:
|
||||
Inventory: The created inventory object
|
||||
Creates a purchase transaction from a vendor.
|
||||
For each item:
|
||||
1. Creates a PhysicalItem (SealedCase/SealedBox)
|
||||
2. Creates an InventoryItem with the purchase price as cost basis
|
||||
3. Creates TransactionItems linking the purchase to the items
|
||||
"""
|
||||
return super().create(db, obj_in)
|
||||
try:
|
||||
with db_transaction(db):
|
||||
# Create the transaction
|
||||
transaction = Transaction(
|
||||
vendor_id=transaction_data.vendor_id,
|
||||
transaction_type='purchase',
|
||||
transaction_date=transaction_data.transaction_date,
|
||||
transaction_notes=transaction_data.transaction_notes
|
||||
)
|
||||
db.add(transaction)
|
||||
db.flush()
|
||||
|
||||
def update(self, db: Session, db_obj: TCGPlayerInventory, obj_in: Dict) -> TCGPlayerInventory:
|
||||
"""
|
||||
Update an existing inventory item in the database.
|
||||
total_amount = 0
|
||||
physical_items = []
|
||||
case_service = self.get_service("case")
|
||||
box_service = self.get_service("box")
|
||||
for item in transaction_data.items:
|
||||
# Create the physical item based on type
|
||||
# TODO: remove is_case and num_boxes, should derive from product_id
|
||||
# TODO: add support for purchasing single cards
|
||||
if item.item_type == "case":
|
||||
for i in range(item.quantity):
|
||||
physical_item = await case_service.create_case(
|
||||
db=db,
|
||||
product_id=item.product_id,
|
||||
cost_basis=item.unit_price,
|
||||
num_boxes=item.num_boxes
|
||||
)
|
||||
physical_items.append(physical_item)
|
||||
elif item.item_type == "box":
|
||||
for i in range(item.quantity):
|
||||
physical_item = await box_service.create_box(
|
||||
db=db,
|
||||
product_id=item.product_id,
|
||||
cost_basis=item.unit_price
|
||||
)
|
||||
physical_items.append(physical_item)
|
||||
else:
|
||||
raise ValueError(f"Invalid item type: {item.item_type}")
|
||||
# TODO: add support for purchasing single cards
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
db_obj: The inventory object to update
|
||||
obj_in: Dictionary containing updated inventory data
|
||||
for physical_item in physical_items:
|
||||
# Create transaction item
|
||||
transaction.transaction_items.append(TransactionItem(
|
||||
inventory_item_id=physical_item.inventory_item.id,
|
||||
unit_price=item.unit_price
|
||||
))
|
||||
|
||||
Returns:
|
||||
Inventory: The updated inventory object
|
||||
"""
|
||||
return super().update(db, db_obj, obj_in)
|
||||
# Update transaction total
|
||||
transaction.transaction_total_amount = total_amount
|
||||
return transaction
|
||||
|
||||
def get_by_tcgplayer_id(self, db: Session, tcgplayer_id: str) -> Optional[TCGPlayerInventory]:
|
||||
"""
|
||||
Get an inventory item by its TCGPlayer ID.
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
tcgplayer_id: The TCGPlayer ID to find
|
||||
async def create_customer(
|
||||
self,
|
||||
db: Session,
|
||||
customer_name: str
|
||||
) -> Customer:
|
||||
try:
|
||||
# check if customer already exists
|
||||
existing_customer = db.query(Customer).filter(Customer.name == customer_name).first()
|
||||
if existing_customer:
|
||||
return existing_customer
|
||||
|
||||
Returns:
|
||||
Optional[TCGPlayerInventory]: The inventory item if found, None otherwise
|
||||
"""
|
||||
return db.query(self.model).filter(self.model.tcgplayer_id == tcgplayer_id).first()
|
||||
with db_transaction(db):
|
||||
customer = Customer(
|
||||
name=customer_name
|
||||
)
|
||||
db.add(customer)
|
||||
db.flush()
|
||||
return customer
|
||||
|
||||
def get_by_set(self, db: Session, set_name: str, skip: int = 0, limit: int = 100) -> List[TCGPlayerInventory]:
|
||||
"""
|
||||
Get all inventory items from a specific set.
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
set_name: The name of the set to filter by
|
||||
skip: Number of records to skip (for pagination)
|
||||
limit: Maximum number of records to return
|
||||
async def create_vendor(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_name: str
|
||||
) -> Vendor:
|
||||
try:
|
||||
# check if vendor already exists
|
||||
existing_vendor = db.query(Vendor).filter(Vendor.name == vendor_name).first()
|
||||
if existing_vendor:
|
||||
return existing_vendor
|
||||
|
||||
Returns:
|
||||
List[TCGPlayerInventory]: List of inventory items from the specified set
|
||||
"""
|
||||
return db.query(self.model).filter(self.model.set_name == set_name).offset(skip).limit(limit).all()
|
||||
with db_transaction(db):
|
||||
vendor = Vendor(
|
||||
name=vendor_name
|
||||
)
|
||||
db.add(vendor)
|
||||
db.flush()
|
||||
return vendor
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
async def get_vendors(
|
||||
self,
|
||||
db: Session
|
||||
) -> List[Vendor]:
|
||||
return db.query(Vendor).all()
|
||||
|
||||
async def create_marketplace(
|
||||
self,
|
||||
db: Session,
|
||||
marketplace_name: str
|
||||
) -> Marketplace:
|
||||
try:
|
||||
# check if marketplace already exists
|
||||
existing_marketplace = db.query(Marketplace).filter(Marketplace.name == marketplace_name).first()
|
||||
if existing_marketplace:
|
||||
return existing_marketplace
|
||||
|
||||
with db_transaction(db):
|
||||
marketplace = Marketplace(
|
||||
name=marketplace_name
|
||||
)
|
||||
db.add(marketplace)
|
||||
db.flush()
|
||||
return marketplace
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
async def get_marketplaces(
|
||||
self,
|
||||
db: Session
|
||||
) -> List[Marketplace]:
|
||||
return db.query(Marketplace).all()
|
||||
|
||||
class BoxService(BaseService[Box]):
|
||||
def __init__(self):
|
||||
super().__init__(Box)
|
||||
|
||||
async def create_box(
|
||||
self,
|
||||
db: Session,
|
||||
product_id: int,
|
||||
cost_basis: float
|
||||
) -> Box:
|
||||
try:
|
||||
with db_transaction(db):
|
||||
# Create the SealedBox
|
||||
box = Box(
|
||||
tcgplayer_product_id=product_id
|
||||
)
|
||||
db.add(box)
|
||||
db.flush() # Get the ID for relationships
|
||||
|
||||
expected_value = box.products.sealed_expected_value.expected_value
|
||||
box.expected_value = expected_value
|
||||
db.flush()
|
||||
|
||||
# Create the InventoryItem for the sealed box
|
||||
inventory_item = InventoryItem(
|
||||
physical_item=box,
|
||||
cost_basis=cost_basis
|
||||
)
|
||||
db.add(inventory_item)
|
||||
|
||||
return box
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
async def calculate_cost_basis_for_opened_cards(self, db: Session, open_event: OpenEvent) -> float:
|
||||
box_cost_basis = open_event.source_item.inventory_item.cost_basis
|
||||
box_expected_value = open_event.source_item.products.sealed_expected_value.expected_value
|
||||
for resulting_card in open_event.resulting_items:
|
||||
# ensure card
|
||||
if resulting_card.item_type != "card":
|
||||
raise ValueError(f"Expected card, got {resulting_card.item_type}")
|
||||
resulting_card_market_value = resulting_card.products.most_recent_tcgplayer_price.market_price
|
||||
resulting_card_cost_basis = (resulting_card_market_value / box_expected_value) * box_cost_basis
|
||||
resulting_card.inventory_item.cost_basis = resulting_card_cost_basis
|
||||
db.flush()
|
||||
|
||||
async def open_box(self, db: Session, box: Box, manabox_file_uploads: List[FileInDB]) -> bool:
|
||||
with db_transaction(db):
|
||||
# create open event
|
||||
open_event = OpenEvent(
|
||||
source_item=box,
|
||||
open_date=datetime.now()
|
||||
)
|
||||
db.add(open_event)
|
||||
db.flush()
|
||||
|
||||
manabox_upload_ids = [manabox_file_upload.id for manabox_file_upload in manabox_file_uploads]
|
||||
staging_data = db.query(ManaboxImportStaging).filter(ManaboxImportStaging.file_id.in_(manabox_upload_ids)).all()
|
||||
for record in staging_data:
|
||||
for i in range(record.quantity):
|
||||
open_card = Card(
|
||||
tcgplayer_product_id=record.tcgplayer_product_id,
|
||||
tcgplayer_sku_id=record.tcgplayer_sku_id
|
||||
)
|
||||
open_event.resulting_items.append(open_card)
|
||||
|
||||
inventory_item = InventoryItem(
|
||||
physical_item=open_card,
|
||||
cost_basis=0
|
||||
)
|
||||
db.add(inventory_item)
|
||||
db.flush()
|
||||
|
||||
# calculate cost basis for opened cards
|
||||
await self.calculate_cost_basis_for_opened_cards(db, open_event)
|
||||
|
||||
|
||||
|
||||
return open_event
|
||||
|
||||
|
||||
|
||||
class CaseService(BaseService[Case]):
|
||||
def __init__(self):
|
||||
super().__init__(Case)
|
||||
|
||||
async def create_case(self, db: Session, product_id: int, cost_basis: float, num_boxes: int) -> Case:
|
||||
try:
|
||||
with db_transaction(db):
|
||||
# Create the SealedCase
|
||||
case = Case(
|
||||
tcgplayer_product_id=product_id,
|
||||
num_boxes=num_boxes
|
||||
)
|
||||
db.add(case)
|
||||
db.flush() # Get the ID for relationships
|
||||
case.expected_value = case.products.sealed_expected_value.expected_value
|
||||
|
||||
# Create the InventoryItem for the sealed case
|
||||
inventory_item = InventoryItem(
|
||||
physical_item=case,
|
||||
cost_basis=cost_basis
|
||||
)
|
||||
db.add(inventory_item)
|
||||
|
||||
return case
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
async def open_case(self, db: Session, case: Case, child_product_id: int) -> bool:
|
||||
try:
|
||||
## TODO should be able to import a manabox file with a case
|
||||
## cost basis will be able to flow down to the card accurately
|
||||
with db_transaction(db):
|
||||
# Create the OpenEvent
|
||||
open_event = OpenEvent(
|
||||
source_item=case,
|
||||
open_date=datetime.now()
|
||||
)
|
||||
db.add(open_event)
|
||||
db.flush() # Get the ID for relationships
|
||||
|
||||
# Create num_boxes SealedBoxes
|
||||
for i in range(case.num_boxes):
|
||||
new_box = Box(
|
||||
tcgplayer_product_id=child_product_id
|
||||
)
|
||||
open_event.resulting_items.append(new_box)
|
||||
db.flush()
|
||||
|
||||
per_box_cost_basis = case.inventory_item.cost_basis / case.num_boxes
|
||||
|
||||
# Create the InventoryItem for the sealed box
|
||||
inventory_item = InventoryItem(
|
||||
physical_item=new_box,
|
||||
cost_basis=per_box_cost_basis
|
||||
)
|
||||
db.add(inventory_item)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
class MarketplaceListingService(BaseService[MarketplaceListing]):
|
||||
def __init__(self):
|
||||
super().__init__(MarketplaceListing)
|
||||
self.pricing_service = self.service_manager.get_service("pricing")
|
||||
|
||||
async def create_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing:
|
||||
try:
|
||||
with db_transaction(db):
|
||||
recommended_price = await self.pricing_service.set_price(db, inventory_item)
|
||||
logger.info(f"recommended_price: {recommended_price.price}")
|
||||
marketplace_listing = MarketplaceListing(
|
||||
inventory_item=inventory_item,
|
||||
marketplace=marketplace,
|
||||
recommended_price=recommended_price,
|
||||
listing_date=None,
|
||||
delisting_date=None
|
||||
)
|
||||
db.add(marketplace_listing)
|
||||
db.flush()
|
||||
return marketplace_listing
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
async def update_marketplace_listing_price(self, db: Session, marketplace_listing: MarketplaceListing) -> MarketplaceListing:
|
||||
try:
|
||||
with db_transaction(db):
|
||||
marketplace_listing.listed_price = self.pricing_service.set_price(marketplace_listing.inventory_item)
|
||||
db.flush()
|
||||
return marketplace_listing
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
async def get_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing:
|
||||
return db.query(MarketplaceListing).filter(MarketplaceListing.inventory_item == inventory_item).filter(MarketplaceListing.delisting_date == None).filter(MarketplaceListing.deleted_at == None).filter(MarketplaceListing.marketplace == marketplace).order_by(MarketplaceListing.created_at.desc()).first()
|
||||
|
||||
async def get_active_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing:
|
||||
return db.query(MarketplaceListing).filter(MarketplaceListing.inventory_item == inventory_item).filter(MarketplaceListing.delisting_date == None).filter(MarketplaceListing.deleted_at == None).filter(MarketplaceListing.marketplace == marketplace).filter(MarketplaceListing.listing_date != None).order_by(MarketplaceListing.created_at.desc()).first()
|
||||
|
||||
async def confirm_listings(self, db: Session, open_event: OpenEvent, marketplace: Marketplace) -> str:
|
||||
tcgplayer_add_file = await self.create_tcgplayer_add_file(db, open_event, marketplace)
|
||||
if not tcgplayer_add_file:
|
||||
raise ValueError("No TCGplayer add file created")
|
||||
with db_transaction(db):
|
||||
for resulting_item in open_event.resulting_items:
|
||||
marketplace_listing = await self.get_marketplace_listing(db, resulting_item.inventory_item, marketplace)
|
||||
if marketplace_listing is None:
|
||||
raise ValueError(f"No active marketplace listing found for inventory item id {resulting_item.inventory_item.id} in {marketplace.name}")
|
||||
marketplace_listing.listing_date = datetime.now()
|
||||
db.flush()
|
||||
return tcgplayer_add_file
|
||||
|
||||
async def create_tcgplayer_add_file(self, db: Session, open_event: OpenEvent, marketplace: Marketplace) -> str:
|
||||
# TCGplayer Id,Product Line,Set Name,Product Name,Title,Number,Rarity,Condition,TCG Market Price,TCG Direct Low,TCG Low Price With Shipping,TCG Low Price,Total Quantity,Add to Quantity,TCG Marketplace Price,Photo URL
|
||||
headers = [
|
||||
"TCGplayer Id",
|
||||
"Product Line",
|
||||
"Set Name",
|
||||
"Product Name",
|
||||
"Title",
|
||||
"Number",
|
||||
"Rarity",
|
||||
"Condition",
|
||||
"TCG Market Price",
|
||||
"TCG Direct Low",
|
||||
"TCG Low Price With Shipping",
|
||||
"TCG Low Price",
|
||||
"Total Quantity",
|
||||
"Add to Quantity",
|
||||
"TCG Marketplace Price",
|
||||
"Photo URL"
|
||||
]
|
||||
data = {}
|
||||
for resulting_item in open_event.resulting_items:
|
||||
marketplace_listing = await self.get_marketplace_listing(db, resulting_item.inventory_item, marketplace)
|
||||
if marketplace_listing is None:
|
||||
raise ValueError(f"No active marketplace listing found for inventory item id {resulting_item.inventory_item.id} in {marketplace.name}")
|
||||
tcgplayer_sku_id = resulting_item.tcgplayer_sku_id
|
||||
if tcgplayer_sku_id in data:
|
||||
data[tcgplayer_sku_id]["Add to Quantity"] += 1
|
||||
continue
|
||||
product_line = resulting_item.products.category.name
|
||||
set_name = resulting_item.products.group.name
|
||||
product_name = resulting_item.products.name
|
||||
title = ""
|
||||
number = resulting_item.products.ext_number
|
||||
rarity = resulting_item.products.ext_rarity
|
||||
condition = " ".join([condition.capitalize() for condition in resulting_item.sku.condition.split(" ")]) + (" " + resulting_item.products.sub_type_name if resulting_item.products.sub_type_name == "Foil" else "")
|
||||
tcg_market_price = resulting_item.products.most_recent_tcgplayer_price.market_price
|
||||
tcg_direct_low = resulting_item.products.most_recent_tcgplayer_price.direct_low_price
|
||||
tcg_low_price_with_shipping = resulting_item.products.most_recent_tcgplayer_price.low_price
|
||||
tcg_low_price = resulting_item.products.most_recent_tcgplayer_price.low_price
|
||||
total_quantity = ""
|
||||
add_to_quantity = 1
|
||||
# get average recommended price of product
|
||||
# get inventory items with same tcgplayer_product_id
|
||||
inventory_items = db.query(InventoryItem).filter(InventoryItem.physical_item.has(tcgplayer_sku_id=tcgplayer_sku_id)).all()
|
||||
inventory_item_ids = [inventory_item.id for inventory_item in inventory_items]
|
||||
logger.debug(f"inventory_item_ids: {inventory_item_ids}")
|
||||
valid_listings = db.query(MarketplaceListing).filter(MarketplaceListing.inventory_item_id.in_(inventory_item_ids)).filter(MarketplaceListing.delisting_date == None).filter(MarketplaceListing.deleted_at == None).filter(MarketplaceListing.listing_date == None).all()
|
||||
logger.debug(f"valid_listings: {valid_listings}")
|
||||
avg_recommended_price = sum([listing.recommended_price.price for listing in valid_listings]) / len(valid_listings)
|
||||
data[tcgplayer_sku_id] = {
|
||||
"TCGplayer Id": tcgplayer_sku_id,
|
||||
"Product Line": product_line,
|
||||
"Set Name": set_name,
|
||||
"Product Name": product_name,
|
||||
"Title": title,
|
||||
"Number": number,
|
||||
"Rarity": rarity,
|
||||
"Condition": condition,
|
||||
"TCG Market Price": tcg_market_price,
|
||||
"TCG Direct Low": tcg_direct_low,
|
||||
"TCG Low Price With Shipping": tcg_low_price_with_shipping,
|
||||
"TCG Low Price": tcg_low_price,
|
||||
"Total Quantity": total_quantity,
|
||||
"Add to Quantity": add_to_quantity,
|
||||
"TCG Marketplace Price": f"{Decimal(avg_recommended_price):.2f}",
|
||||
"Photo URL": ""
|
||||
}
|
||||
# format data into csv
|
||||
# header
|
||||
header_row = ",".join(headers)
|
||||
# data
|
||||
def escape_csv_value(value):
|
||||
if value is None:
|
||||
return ""
|
||||
value = str(value)
|
||||
if any(c in value for c in [',', '"', '\n']):
|
||||
return f'"{value.replace('"', '""')}"'
|
||||
return value
|
||||
|
||||
data_rows = [",".join([escape_csv_value(data[tcgplayer_id][header]) for header in headers]) for tcgplayer_id in data]
|
||||
csv_data = "\n".join([header_row] + data_rows)
|
||||
return csv_data
|
||||
|
139
app/services/manabox_service.py
Normal file
139
app/services/manabox_service.py
Normal file
@ -0,0 +1,139 @@
|
||||
from app.services.base_service import BaseService
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.database import transaction
|
||||
from app.schemas.file import FileInDB
|
||||
from app.models.tcgplayer_products import TCGPlayerProduct, MTGJSONCard, MTGJSONSKU
|
||||
from app.models.critical_error_log import CriticalErrorLog
|
||||
from app.models.manabox_import_staging import ManaboxImportStaging
|
||||
from typing import Dict, Any, Union, List
|
||||
import csv
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from fastapi import BackgroundTasks
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ManaboxService(BaseService):
|
||||
def __init__(self):
|
||||
super().__init__(None)
|
||||
|
||||
async def process_manabox_csv(self, db: Session, bytes: bytes, metadata: Dict[str, Any], background_tasks: BackgroundTasks, wait: bool = False) -> Union[bool, List[FileInDB]]:
|
||||
# save file
|
||||
file = await self.file_service.save_file(
|
||||
db=db,
|
||||
file_data=bytes,
|
||||
filename=f"manabox_{datetime.now().strftime('%Y%m%d%H%M%S')}.csv",
|
||||
subdir="manabox",
|
||||
file_type="manabox",
|
||||
content_type="text/csv",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
if wait:
|
||||
await self._process_file_background(db, file)
|
||||
return_value = await self.file_service.get_file(db, file.id)
|
||||
return [return_value] if return_value else []
|
||||
else:
|
||||
background_tasks.add_task(self._process_file_background, db, file)
|
||||
return True
|
||||
|
||||
async def _process_file_background(self, db: Session, file: FileInDB):
|
||||
try:
|
||||
# Read the CSV file
|
||||
with open(file.path, 'r') as csv_file:
|
||||
reader = csv.DictReader(csv_file)
|
||||
logger.debug(f"Processing file: {file.path}")
|
||||
# Pre-fetch all MTGJSONCards for Scryfall IDs in the file
|
||||
scryfall_ids = {row['Scryfall ID'] for row in reader}
|
||||
mtg_json_map = {card.scryfall_id: card for card in db.query(MTGJSONCard).filter(MTGJSONCard.scryfall_id.in_(scryfall_ids)).all()}
|
||||
logger.debug(f"len ids: {len(scryfall_ids)}")
|
||||
|
||||
# Re-read the file to process the rows
|
||||
csv_file.seek(0)
|
||||
logger.debug(f"header: {reader.fieldnames}")
|
||||
next(reader) # Skip the header row
|
||||
|
||||
staging_entries = [] # To collect all staging entries for batch insert
|
||||
critical_errors = [] # To collect errors for logging
|
||||
|
||||
for row in reader:
|
||||
|
||||
logger.debug(f"Processing row: {row}")
|
||||
mtg_json = mtg_json_map.get(row['Scryfall ID'])
|
||||
|
||||
if not mtg_json:
|
||||
error_message = f"Error: No MTGJSONCard found for scryfall id: {row['Scryfall ID']}"
|
||||
critical_errors.append(error_message)
|
||||
continue # Skip this row
|
||||
|
||||
language = 'ENGLISH' if row['Language'] == 'en' else 'JAPANESE' if row['Language'] == 'ja' else None # manabox only needs en and jp for now
|
||||
printing = 'foil' if 'foil' in row['Foil'].lower() or 'etched' in row['Foil'].lower() else 'normal'
|
||||
condition = row['Condition'].replace('_', ' ').upper()
|
||||
|
||||
# Query the correct TCGPlayer SKU
|
||||
sku_query = db.query(MTGJSONSKU).filter(
|
||||
MTGJSONSKU.tcgplayer_product_id == (mtg_json.tcgplayer_etched_product_id if row['Foil'].lower() == 'etched' else mtg_json.tcgplayer_product_id)
|
||||
).filter(
|
||||
MTGJSONSKU.condition == condition,
|
||||
MTGJSONSKU.normalized_printing == printing,
|
||||
MTGJSONSKU.language == language
|
||||
).distinct()
|
||||
|
||||
if sku_query.count() != 1:
|
||||
error_message = f"Error: Multiple TCGplayer SKUs found for mtgjson name: {mtg_json.name} condition: {row['Condition']} language: {language} printing: {printing}"
|
||||
critical_errors.append(error_message)
|
||||
continue # Skip this row
|
||||
|
||||
tcgplayer_sku = sku_query.first()
|
||||
|
||||
if not tcgplayer_sku:
|
||||
error_message = f"Error: No TCGplayer SKU found for mtgjson name: {mtg_json.name} condition: {row['Condition']} language: {language} printing: {printing}"
|
||||
critical_errors.append(error_message)
|
||||
continue # Skip this row
|
||||
|
||||
# Query TCGPlayer product data
|
||||
tcgplayer_product = db.query(TCGPlayerProduct).filter(
|
||||
TCGPlayerProduct.tcgplayer_product_id == tcgplayer_sku.tcgplayer_product_id,
|
||||
TCGPlayerProduct.normalized_sub_type_name == tcgplayer_sku.normalized_printing
|
||||
).distinct()
|
||||
|
||||
if tcgplayer_product.count() != 1:
|
||||
error_message = f"Error: Multiple TCGPlayer products found for SKU {tcgplayer_sku.tcgplayer_sku_id}"
|
||||
critical_errors.append(error_message)
|
||||
continue # Skip this row
|
||||
|
||||
tcgplayer_product = tcgplayer_product.first()
|
||||
|
||||
if not tcgplayer_product:
|
||||
error_message = f"Error: No TCGPlayer product found for SKU {tcgplayer_sku.tcgplayer_sku_id}"
|
||||
critical_errors.append(error_message)
|
||||
continue # Skip this row
|
||||
|
||||
# Prepare the staging entry
|
||||
quantity = int(row['Quantity'])
|
||||
logger.debug(f"inserting row file id: {file.id} tcgplayer_product_id: {tcgplayer_product.tcgplayer_product_id} tcgplayer_sku_id: {tcgplayer_sku.tcgplayer_sku_id} quantity: {quantity}")
|
||||
staging_entries.append(ManaboxImportStaging(
|
||||
file_id=file.id,
|
||||
tcgplayer_product_id=tcgplayer_product.tcgplayer_product_id,
|
||||
tcgplayer_sku_id=tcgplayer_sku.tcgplayer_sku_id,
|
||||
quantity=quantity
|
||||
))
|
||||
|
||||
# Bulk insert all valid ManaboxImportStaging entries
|
||||
if staging_entries:
|
||||
logger.debug(f"inserting {len(staging_entries)} rows")
|
||||
with transaction(db):
|
||||
db.bulk_save_objects(staging_entries)
|
||||
|
||||
# Log any critical errors that occurred
|
||||
for error_message in critical_errors:
|
||||
logger.debug(f"logging critical error: {error_message}")
|
||||
with transaction(db):
|
||||
critical_error_log = CriticalErrorLog(error_message=error_message)
|
||||
db.add(critical_error_log)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing file: {str(e)}")
|
||||
with transaction(db):
|
||||
critical_error_log = CriticalErrorLog(error_message=f"Error processing file: {str(e)}")
|
||||
db.add(critical_error_log)
|
182
app/services/pricing_service.py
Normal file
182
app/services/pricing_service.py
Normal file
@ -0,0 +1,182 @@
|
||||
import logging
|
||||
from sqlalchemy.orm import Session
|
||||
from app.services.base_service import BaseService
|
||||
from app.models.inventory_management import InventoryItem, MarketplaceListing
|
||||
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
||||
from app.models.pricing import PricingEvent
|
||||
from app.db.database import transaction
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PricingService(BaseService):
|
||||
def __init__(self):
|
||||
super().__init__(None)
|
||||
|
||||
|
||||
async def set_price(self, db: Session, inventory_item: InventoryItem) -> float:
|
||||
"""
|
||||
TODO This sets listed_price per inventory_item but listed_price can only be applied to a product
|
||||
however, this may be desired on other marketplaces
|
||||
when generating pricing file for tcgplayer, give the option to set min, max, avg price for product?
|
||||
"""
|
||||
# Fetch base pricing data
|
||||
cost_basis = Decimal(str(inventory_item.cost_basis))
|
||||
market_price = Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.market_price))
|
||||
tcg_low = Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.low_price))
|
||||
tcg_mid = Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.mid_price))
|
||||
listed_price = Decimal(str(inventory_item.marketplace_listing.listed_price)) if inventory_item.marketplace_listing else None
|
||||
|
||||
logger.info(f"listed_price: {listed_price}")
|
||||
logger.info(f"market_price: {market_price}")
|
||||
logger.info(f"tcg_low: {tcg_low}")
|
||||
logger.info(f"tcg_mid: {tcg_mid}")
|
||||
logger.info(f"cost_basis: {cost_basis}")
|
||||
|
||||
# TODO: Add logic to fetch lowest price for seller with same quantity in stock
|
||||
# NOT IMPLEMENTED YET
|
||||
lowest_price_for_quantity = Decimal('0.0')
|
||||
|
||||
# Hardcoded configuration values (should be parameterized later)
|
||||
shipping_cost = Decimal('1.0')
|
||||
tcgplayer_shipping_fee = Decimal('1.31')
|
||||
average_cards_per_order = Decimal('3.0')
|
||||
marketplace_fee_percentage = Decimal('0.20')
|
||||
target_margin = Decimal('0.20')
|
||||
velocity_multiplier = Decimal('0.0')
|
||||
global_margin_multiplier = Decimal('0.00')
|
||||
min_floor_price = Decimal('0.25')
|
||||
price_drop_threshold = Decimal('0.20')
|
||||
# TODO add age of inventory price decrease multiplier
|
||||
age_of_inventory_multiplier = Decimal('0.0')
|
||||
|
||||
# card cost margin multiplier
|
||||
if market_price > 0 and market_price < 2:
|
||||
card_cost_margin_multiplier = Decimal('-0.033')
|
||||
elif market_price >= 2 and market_price < 10:
|
||||
card_cost_margin_multiplier = Decimal('0.0')
|
||||
elif market_price >= 10 and market_price < 30:
|
||||
card_cost_margin_multiplier = Decimal('0.0125')
|
||||
elif market_price >= 30 and market_price < 50:
|
||||
card_cost_margin_multiplier = Decimal('0.025')
|
||||
elif market_price >= 50 and market_price < 100:
|
||||
card_cost_margin_multiplier = Decimal('0.033')
|
||||
elif market_price >= 100 and market_price < 200:
|
||||
card_cost_margin_multiplier = Decimal('0.05')
|
||||
|
||||
# Fetch current total quantity in stock for SKU
|
||||
quantity_record = db.query(TCGPlayerInventory).filter(
|
||||
TCGPlayerInventory.tcgplayer_sku_id == inventory_item.physical_item.tcgplayer_sku_id
|
||||
).first()
|
||||
quantity_in_stock = quantity_record.total_quantity if quantity_record else 0
|
||||
|
||||
# Determine quantity multiplier based on stock levels
|
||||
if quantity_in_stock < 4:
|
||||
quantity_multiplier = Decimal('0.0')
|
||||
elif quantity_in_stock == 4:
|
||||
quantity_multiplier = Decimal('0.2')
|
||||
elif 5 <= quantity_in_stock < 10:
|
||||
quantity_multiplier = Decimal('0.3')
|
||||
elif quantity_in_stock >= 10:
|
||||
quantity_multiplier = Decimal('0.4')
|
||||
else:
|
||||
quantity_multiplier = Decimal('0.0')
|
||||
|
||||
# Calculate adjusted target margin from base and global multipliers
|
||||
adjusted_target_margin = target_margin + global_margin_multiplier + card_cost_margin_multiplier
|
||||
|
||||
# limit shipping cost offset to 10% of market price
|
||||
shipping_cost_offset = min(shipping_cost / average_cards_per_order, market_price * Decimal('0.1'))
|
||||
|
||||
# Calculate base price considering cost, shipping, fees, and margin targets
|
||||
base_price = (cost_basis + shipping_cost_offset) / (
|
||||
(Decimal('1.0') - marketplace_fee_percentage) - adjusted_target_margin
|
||||
)
|
||||
|
||||
# Adjust base price by quantity and velocity multipliers, limit markup to amount of shipping fee
|
||||
adjusted_price = min(
|
||||
base_price * (Decimal('1.0') + quantity_multiplier + velocity_multiplier - age_of_inventory_multiplier),
|
||||
base_price + tcgplayer_shipping_fee
|
||||
)
|
||||
|
||||
# Enforce minimum floor price to ensure profitability
|
||||
if adjusted_price < min_floor_price:
|
||||
adjusted_price = min_floor_price
|
||||
|
||||
# Adjust price based on market prices (TCG low and TCG mid)
|
||||
if adjusted_price < tcg_low:
|
||||
adjusted_price = tcg_mid
|
||||
price_used = "tcg mid"
|
||||
price_reason = "adjusted price below tcg low"
|
||||
elif adjusted_price > tcg_low and adjusted_price < (market_price * Decimal('0.8')) and adjusted_price < (tcg_mid * Decimal('0.8')):
|
||||
adjusted_price = tcg_mid
|
||||
price_used = "tcg mid"
|
||||
price_reason = f"adjusted price below 80% of market price and tcg mid"
|
||||
else:
|
||||
price_used = "adjusted price"
|
||||
price_reason = "valid price assigned based on margin targets"
|
||||
|
||||
# TODO: Add logic to adjust price to beat competitor price with same quantity
|
||||
# NOT IMPLEMENTED YET
|
||||
if adjusted_price < lowest_price_for_quantity:
|
||||
adjusted_price = lowest_price_for_quantity - Decimal('0.01')
|
||||
price_used = "lowest price for quantity"
|
||||
price_reason = "adjusted price below lowest price for quantity"
|
||||
|
||||
# Fine-tune price to optimize for free shipping promotions
|
||||
free_shipping_adjustment = False
|
||||
for x in range(1, 5):
|
||||
quantity = Decimal(str(x))
|
||||
if Decimal('5.00') <= adjusted_price * quantity <= Decimal('5.15'):
|
||||
adjusted_price = Decimal('4.99') / quantity
|
||||
free_shipping_adjustment = True
|
||||
break
|
||||
|
||||
# prevent price drop over price drop threshold
|
||||
if listed_price and adjusted_price < (listed_price * (1 - price_drop_threshold)):
|
||||
adjusted_price = listed_price
|
||||
price_used = "listed price"
|
||||
price_reason = "adjusted price below price drop threshold"
|
||||
|
||||
# Record pricing event in database transaction
|
||||
with transaction(db):
|
||||
pricing_event = PricingEvent(
|
||||
inventory_item_id=inventory_item.id,
|
||||
price=float(adjusted_price),
|
||||
price_used=price_used,
|
||||
price_reason=price_reason,
|
||||
free_shipping_adjustment=free_shipping_adjustment
|
||||
)
|
||||
db.add(pricing_event)
|
||||
|
||||
# delete previous pricing events for inventory item
|
||||
if inventory_item.marketplace_listing and inventory_item.marketplace_listing.listed_price:
|
||||
inventory_item.marketplace_listing.listed_price.deleted_at = datetime.now()
|
||||
|
||||
return pricing_event
|
||||
|
||||
def set_price_for_unmanaged_inventory(self, db: Session, tcgplayer_sku_id: int, quantity: int) -> float:
|
||||
pass
|
||||
|
||||
def update_price_for_product(self, db: Session, tcgplayer_sku_id: int) -> list[PricingEvent]:
|
||||
# get inventory items for sku
|
||||
updated_prices = []
|
||||
inventory_items = db.query(InventoryItem).filter(
|
||||
InventoryItem.physical_item.tcgplayer_sku_id == tcgplayer_sku_id
|
||||
).all()
|
||||
for inventory_item in inventory_items:
|
||||
pricing_event = self.set_price(db, inventory_item)
|
||||
updated_prices.append(pricing_event)
|
||||
return updated_prices
|
||||
|
||||
def set_price_for_product(self, db: Session, tcgplayer_sku_id: int) -> float:
|
||||
# update price for all inventory items for sku
|
||||
prices = self.update_price_for_product(db, tcgplayer_sku_id)
|
||||
sum_prices = sum(price.price for price in prices)
|
||||
average_price = sum_prices / len(prices)
|
||||
return average_price
|
||||
|
||||
|
@ -113,7 +113,7 @@ class PullSheetService(BaseService):
|
||||
'quantity': str(int(row['Quantity'])), # Convert to string for template
|
||||
'set': row['Set'],
|
||||
'rarity': row['Rarity'],
|
||||
'card_number': str(int(row['Number'])) if 'Number' in row else ''
|
||||
'card_number': str(int(row['Number'])) if 'Number' in row and pd.notna(row['Number']) and '/' not in str(row['Number']) else str(row['Number']) if 'Number' in row and pd.notna(row['Number']) and '/' in str(row['Number']) else ''
|
||||
})
|
||||
|
||||
return items
|
@ -1,6 +1,7 @@
|
||||
from typing import Callable, Dict, Any
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
import logging
|
||||
from apscheduler.schedulers.base import SchedulerNotRunningError
|
||||
|
||||
@ -16,18 +17,28 @@ class BaseScheduler:
|
||||
self,
|
||||
task_name: str,
|
||||
func: Callable,
|
||||
interval_seconds: int,
|
||||
interval_seconds: int = None,
|
||||
cron_expression: str = None,
|
||||
*args,
|
||||
**kwargs
|
||||
) -> None:
|
||||
"""Schedule a task to run at regular intervals using APScheduler"""
|
||||
if task_name in self.jobs:
|
||||
logger.warning(f"Task {task_name} already exists. Removing existing job.")
|
||||
self.jobs[task_name].remove()
|
||||
|
||||
if interval_seconds and cron_expression:
|
||||
raise ValueError("Cannot specify both interval_seconds and cron_expression")
|
||||
elif not interval_seconds and not cron_expression:
|
||||
raise ValueError("Must specify either interval_seconds or cron_expression")
|
||||
|
||||
if interval_seconds:
|
||||
trigger = IntervalTrigger(seconds=interval_seconds)
|
||||
else:
|
||||
trigger = CronTrigger.from_crontab(cron_expression)
|
||||
|
||||
job = self.scheduler.add_job(
|
||||
func,
|
||||
trigger=IntervalTrigger(seconds=interval_seconds),
|
||||
func=func,
|
||||
trigger=trigger,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
id=task_name,
|
||||
@ -35,7 +46,12 @@ class BaseScheduler:
|
||||
)
|
||||
|
||||
self.jobs[task_name] = job
|
||||
logger.info(f"Scheduled task {task_name} to run every {interval_seconds} seconds")
|
||||
|
||||
if interval_seconds:
|
||||
logger.info(f"Scheduled task {task_name} to run every {interval_seconds} seconds")
|
||||
else:
|
||||
logger.info(f"Scheduled task {task_name} with cron expression: {cron_expression}")
|
||||
|
||||
|
||||
def remove_task(self, task_name: str) -> None:
|
||||
"""Remove a scheduled task"""
|
||||
|
@ -1,21 +1,16 @@
|
||||
from app.db.database import transaction
|
||||
from app.services.scheduler.base_scheduler import BaseScheduler
|
||||
from app.services.base_service import BaseService
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
|
||||
from app.models.tcgplayer_inventory import UnmanagedTCGPlayerInventory, TCGPlayerInventory
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SchedulerService:
|
||||
class SchedulerService(BaseService):
|
||||
def __init__(self):
|
||||
# Initialize BaseService with None as model since this service doesn't have a specific model
|
||||
super().__init__(None)
|
||||
self.scheduler = BaseScheduler()
|
||||
# Service manager will be set during initialization
|
||||
self._service_manager = None
|
||||
|
||||
@property
|
||||
def service_manager(self):
|
||||
if self._service_manager is None:
|
||||
from app.services.service_manager import ServiceManager
|
||||
self._service_manager = ServiceManager()
|
||||
return self._service_manager
|
||||
|
||||
async def update_open_orders_hourly(self, db):
|
||||
"""
|
||||
@ -61,19 +56,41 @@ class SchedulerService:
|
||||
logger.error(f"Error updating all orders: {str(e)}")
|
||||
raise
|
||||
|
||||
async def refresh_tcgplayer_inventory_table(self, db):
|
||||
"""
|
||||
Refresh the TCGPlayer inventory table
|
||||
"""
|
||||
tcgplayer_inventory_service = self.service_manager.get_service('tcgplayer_inventory')
|
||||
with transaction(db):
|
||||
db.query(UnmanagedTCGPlayerInventory).delete()
|
||||
db.query(TCGPlayerInventory).delete()
|
||||
db.flush()
|
||||
await tcgplayer_inventory_service.refresh_tcgplayer_inventory_table(db)
|
||||
db.flush()
|
||||
await tcgplayer_inventory_service.refresh_unmanaged_tcgplayer_inventory_table(db)
|
||||
|
||||
async def start_scheduled_tasks(self, db):
|
||||
"""Start all scheduled tasks"""
|
||||
# Schedule open orders update to run hourly at 00 minutes
|
||||
await self.scheduler.schedule_task(
|
||||
task_name="update_open_orders_hourly",
|
||||
func=lambda: self.update_open_orders_hourly(db),
|
||||
interval_seconds=60 * 60, # 1 hour
|
||||
func=self.update_open_orders_hourly,
|
||||
cron_expression="10 * * * *", # Run at minute 10 of every hour
|
||||
db=db
|
||||
)
|
||||
# Schedule all orders update to run daily at 1 AM
|
||||
# Schedule all orders update to run daily at 3 AM
|
||||
await self.scheduler.schedule_task(
|
||||
task_name="update_all_orders_daily",
|
||||
func=lambda: self.update_all_orders_daily(db),
|
||||
interval_seconds=24 * 60 * 60, # 24 hours
|
||||
func=self.update_all_orders_daily,
|
||||
cron_expression="0 3 * * *", # Run at 3:00 AM every day
|
||||
db=db
|
||||
)
|
||||
# Schedule TCGPlayer inventory refresh to run every 3 hours
|
||||
await self.scheduler.schedule_task(
|
||||
task_name="refresh_tcgplayer_inventory_table",
|
||||
func=self.refresh_tcgplayer_inventory_table,
|
||||
cron_expression="28 */3 * * *", # Run at minute 28 of every 3rd hour
|
||||
db=db
|
||||
)
|
||||
|
||||
self.scheduler.start()
|
||||
|
@ -28,7 +28,14 @@ class ServiceManager:
|
||||
'scheduler': 'app.services.scheduler.scheduler_service.SchedulerService',
|
||||
'file': 'app.services.file_service.FileService',
|
||||
'tcgcsv': 'app.services.external_api.tcgcsv.tcgcsv_service.TCGCSVService',
|
||||
'mtgjson': 'app.services.external_api.mtgjson.mtgjson_service.MTGJSONService'
|
||||
'mtgjson': 'app.services.external_api.mtgjson.mtgjson_service.MTGJSONService',
|
||||
'manabox': 'app.services.manabox_service.ManaboxService',
|
||||
'pricing': 'app.services.pricing_service.PricingService',
|
||||
'inventory': 'app.services.inventory_service.InventoryService',
|
||||
'box': 'app.services.inventory_service.BoxService',
|
||||
'case': 'app.services.inventory_service.CaseService',
|
||||
'marketplace_listing': 'app.services.inventory_service.MarketplaceListingService'
|
||||
|
||||
}
|
||||
self._service_configs = {
|
||||
'label_printer': {'printer_api_url': "http://192.168.1.110:8000"},
|
||||
|
@ -10,7 +10,7 @@ import aiohttp
|
||||
import jinja2
|
||||
from weasyprint import HTML
|
||||
from app.services.base_service import BaseService
|
||||
from app.models.tcgplayer_group import TCGPlayerGroup
|
||||
from app.models.tcgplayer_products import TCGPlayerProduct, TCGPlayerGroup
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -20,6 +20,22 @@
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gray-900 min-h-screen text-gray-100">
|
||||
<!-- Navigation Menu -->
|
||||
<nav class="bg-gray-800 shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="text-xl font-bold text-gray-100">TCGPlayer Manager</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="/" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Orders</a>
|
||||
<a href="/manabox.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Manabox</a>
|
||||
<a href="/transactions.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Transactions</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-100 mb-2">TCGPlayer Order Management</h1>
|
||||
|
86
app/static/manabox.html
Normal file
86
app/static/manabox.html
Normal file
@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Manabox Inventory Management</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gray-900 min-h-screen text-gray-100">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-100 mb-2">Manabox Inventory Management</h1>
|
||||
<p class="text-gray-400">Upload and manage your Manabox inventory</p>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Section -->
|
||||
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-100 mb-6">Upload Manabox CSV</h2>
|
||||
<form id="uploadForm" class="space-y-4">
|
||||
<div>
|
||||
<label for="source" class="block text-sm font-medium text-gray-300 mb-2">Source</label>
|
||||
<input type="text" id="source" name="source" required
|
||||
class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea id="description" name="description" rows="3" required
|
||||
class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="csvFile" class="block text-sm font-medium text-gray-300 mb-2">CSV File</label>
|
||||
<input type="file" id="csvFile" name="file" accept=".csv" required
|
||||
class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||
Upload CSV
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- File Uploads List Section -->
|
||||
<div class="bg-gray-800 rounded-xl shadow-sm p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-100">Recent Uploads</h2>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button onclick="selectAllUploads()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||
Select All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">
|
||||
<input type="checkbox" id="selectAll" class="rounded border-gray-600 bg-gray-800 text-blue-600 focus:ring-blue-500">
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Source</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Description</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Upload Date</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="uploadsList" class="divide-y divide-gray-700">
|
||||
<!-- Uploads will be populated here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/manabox.js"></script>
|
||||
</body>
|
||||
</html>
|
170
app/static/manabox.js
Normal file
170
app/static/manabox.js
Normal file
@ -0,0 +1,170 @@
|
||||
// API base URL
|
||||
const API_BASE_URL = '/api';
|
||||
|
||||
// Selected uploads for actions
|
||||
let selectedUploads = new Set();
|
||||
|
||||
// Show toast notification
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg text-white ${
|
||||
type === 'success' ? 'bg-green-600' : 'bg-red-600'
|
||||
} transform translate-y-0 opacity-100 transition-all duration-300`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.transform = 'translateY(100%)';
|
||||
toast.style.opacity = '0';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
function setLoading(isLoading) {
|
||||
const buttons = document.querySelectorAll('button');
|
||||
buttons.forEach(button => {
|
||||
if (isLoading) {
|
||||
button.disabled = true;
|
||||
button.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
} else {
|
||||
button.disabled = false;
|
||||
button.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', document.getElementById('csvFile').files[0]);
|
||||
formData.append('source', document.getElementById('source').value);
|
||||
formData.append('description', document.getElementById('description').value);
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`${API_BASE_URL}/manabox/process-csv`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || 'Failed to upload CSV');
|
||||
}
|
||||
|
||||
showToast('CSV uploaded successfully');
|
||||
document.getElementById('uploadForm').reset();
|
||||
fetchUploads(); // Refresh the uploads list
|
||||
} catch (error) {
|
||||
showToast('Error uploading CSV: ' + error.message, 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch uploads from the API
|
||||
async function fetchUploads() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`${API_BASE_URL}/manabox/manabox-file-uploads`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch uploads');
|
||||
}
|
||||
|
||||
const uploads = await response.json();
|
||||
displayUploads(uploads);
|
||||
} catch (error) {
|
||||
showToast('Error fetching uploads: ' + error.message, 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Display uploads in the UI
|
||||
function displayUploads(uploads) {
|
||||
const uploadsList = document.getElementById('uploadsList');
|
||||
uploadsList.innerHTML = '';
|
||||
|
||||
if (!uploads || uploads.length === 0) {
|
||||
uploadsList.innerHTML = '<tr><td colspan="5" class="px-6 py-4 text-center text-gray-400">No uploads found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
uploads.forEach(upload => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-700';
|
||||
row.dataset.uploadId = upload.id;
|
||||
row.innerHTML = `
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<input type="checkbox" class="upload-checkbox rounded border-gray-600 bg-gray-800 text-blue-600 focus:ring-blue-500">
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${upload.name || 'N/A'}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-300">${upload.file_metadata?.description || 'N/A'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${formatDate(upload.created_at)}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 py-1 text-xs rounded-full bg-green-900/50 text-green-300">Processed</span>
|
||||
</td>
|
||||
`;
|
||||
uploadsList.appendChild(row);
|
||||
|
||||
// Add click event listener to the checkbox
|
||||
const checkbox = row.querySelector('.upload-checkbox');
|
||||
checkbox.addEventListener('change', () => {
|
||||
const uploadId = row.dataset.uploadId;
|
||||
if (checkbox.checked) {
|
||||
selectedUploads.add(uploadId);
|
||||
} else {
|
||||
selectedUploads.delete(uploadId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to format date
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
// Select all uploads
|
||||
function selectAllUploads() {
|
||||
const checkboxes = document.querySelectorAll('.upload-checkbox');
|
||||
const allSelected = checkboxes.length > 0 && Array.from(checkboxes).every(checkbox => checkbox.checked);
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = !allSelected;
|
||||
const row = checkbox.closest('tr');
|
||||
const uploadId = row.dataset.uploadId;
|
||||
if (!allSelected) {
|
||||
selectedUploads.add(uploadId);
|
||||
} else {
|
||||
selectedUploads.delete(uploadId);
|
||||
}
|
||||
});
|
||||
|
||||
showToast(allSelected ? 'All uploads deselected' : 'All uploads selected');
|
||||
}
|
||||
|
||||
// Initialize the page
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchUploads();
|
||||
|
||||
// Add event listener for the select all checkbox
|
||||
document.getElementById('selectAll').addEventListener('change', (e) => {
|
||||
const checkboxes = document.querySelectorAll('.upload-checkbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = e.target.checked;
|
||||
const row = checkbox.closest('tr');
|
||||
const uploadId = row.dataset.uploadId;
|
||||
if (e.target.checked) {
|
||||
selectedUploads.add(uploadId);
|
||||
} else {
|
||||
selectedUploads.delete(uploadId);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@ -106,3 +106,125 @@ button:hover {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Transaction Page Styles */
|
||||
.transaction-form {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.transaction-form .form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.transaction-form label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.transaction-form .form-control {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.transaction-form .btn-add {
|
||||
margin-left: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.transaction-form .items-section {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.transaction-list {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.transaction-card {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.transaction-card h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.transaction-card p {
|
||||
margin-bottom: 0.25rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-content {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
body.dark-mode {
|
||||
background-color: #1a1a1a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .container {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
body.dark-mode .transaction-card {
|
||||
background-color: #2d2d2d;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
body.dark-mode .transaction-card h3 {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .transaction-card p {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
body.dark-mode .modal-content {
|
||||
background-color: #2d2d2d;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .modal-header,
|
||||
body.dark-mode .modal-footer {
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
body.dark-mode .form-control {
|
||||
background-color: #404040;
|
||||
border-color: #505050;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .form-control:focus {
|
||||
background-color: #404040;
|
||||
border-color: #007bff;
|
||||
color: #ffffff;
|
||||
}
|
179
app/static/transactions.html
Normal file
179
app/static/transactions.html
Normal file
@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Transactions - AI Giga TCG</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gray-900 min-h-screen text-gray-100">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-100 mb-2">Transactions</h1>
|
||||
<p class="text-gray-400">Manage your transactions</p>
|
||||
</div>
|
||||
|
||||
<!-- Create Transaction Button -->
|
||||
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||
<button id="createTransactionBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||
Create New Transaction
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Transaction List -->
|
||||
<div id="transactionList" class="bg-gray-800 rounded-xl shadow-sm p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-100">Recent Transactions</h2>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<label for="limitSelect" class="text-sm text-gray-300">Show:</label>
|
||||
<select id="limitSelect" class="rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="transactionsTable" class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Date</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Type</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Vendor/Customer</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Total</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-700" id="transactionsBody">
|
||||
<!-- Transactions will be loaded here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination Controls -->
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<button id="prevPageBtn" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Previous
|
||||
</button>
|
||||
<span id="pageInfo" class="text-gray-300">Page 1</span>
|
||||
<button id="nextPageBtn" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Transaction Modal -->
|
||||
<div id="createTransactionModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center">
|
||||
<div class="bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-xl font-semibold text-gray-100">Create Transaction</h3>
|
||||
<button onclick="closeTransactionModal()" class="text-gray-400 hover:text-white">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form id="transactionForm" class="space-y-4">
|
||||
<!-- Transaction Type -->
|
||||
<div>
|
||||
<label for="transactionType" class="block text-sm font-medium text-gray-300 mb-2">Transaction Type</label>
|
||||
<select id="transactionType" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="purchase" selected>Purchase</option>
|
||||
<option value="sale">Sale</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Vendor/Customer Selection -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label for="vendorSelect" class="block text-sm font-medium text-gray-300">Vendor</label>
|
||||
<button type="button" id="addVendorBtn" class="px-2 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||
Add New
|
||||
</button>
|
||||
</div>
|
||||
<select id="vendorSelect" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Select a vendor</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Marketplace Selection (for sales) -->
|
||||
<div id="marketplaceSection" class="hidden">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label for="marketplaceSelect" class="block text-sm font-medium text-gray-300">Marketplace</label>
|
||||
<button type="button" id="addMarketplaceBtn" class="px-2 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||
Add New
|
||||
</button>
|
||||
</div>
|
||||
<select id="marketplaceSelect" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Select a marketplace</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Date -->
|
||||
<div>
|
||||
<label for="transactionDate" class="block text-sm font-medium text-gray-300 mb-2">Transaction Date</label>
|
||||
<input type="datetime-local" id="transactionDate" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500" required>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Notes -->
|
||||
<div>
|
||||
<label for="transactionNotes" class="block text-sm font-medium text-gray-300 mb-2">Notes</label>
|
||||
<textarea id="transactionNotes" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Items Section -->
|
||||
<div id="itemsSection" class="border border-gray-700 rounded-lg p-4">
|
||||
<h5 class="text-lg font-medium text-gray-100 mb-4">Items</h5>
|
||||
<div id="itemsContainer" class="space-y-4">
|
||||
<!-- Items will be added here -->
|
||||
</div>
|
||||
<button type="button" id="addItemBtn" class="mt-4 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors">
|
||||
Add Item
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button onclick="closeTransactionModal()" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button id="saveTransactionBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||
Save Transaction
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Details Modal -->
|
||||
<div id="transactionDetailsModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center">
|
||||
<div class="bg-gray-800 rounded-lg p-6 max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-xl font-semibold text-gray-100">Transaction Details</h3>
|
||||
<button onclick="closeTransactionDetailsModal()" class="text-gray-400 hover:text-white">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="transactionDetails" class="space-y-4">
|
||||
<!-- Transaction details will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="transactions.js"></script>
|
||||
</body>
|
||||
</html>
|
1077
app/static/transactions.js
Normal file
1077
app/static/transactions.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
{"success": true, "errors": [], "results": [{"productId": 38444, "lowPrice": 154.95, "midPrice": 223.55, "highPrice": 275.98, "marketPrice": 218.59, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 38445, "lowPrice": 349.49, "midPrice": 374.75, "highPrice": 400.0, "marketPrice": 385.0, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 38446, "lowPrice": 97.71, "midPrice": 120.99, "highPrice": 255.61, "marketPrice": 258.98, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 38447, "lowPrice": 80.0, "midPrice": 109.99, "highPrice": 214.45, "marketPrice": 99.92, "directLowPrice": 94.98, "subTypeName": "Foil"}, {"productId": 57653, "lowPrice": 71.99, "midPrice": 75.99, "highPrice": 100.0, "marketPrice": 69.44, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 67401, "lowPrice": 28.01, "midPrice": 78.72, "highPrice": 119.99, "marketPrice": 33.73, "directLowPrice": 114.99, "subTypeName": "Foil"}, {"productId": 71898, "lowPrice": 167.25, "midPrice": 183.34, "highPrice": 223.35, "marketPrice": 167.25, "directLowPrice": 168.28, "subTypeName": "Foil"}, {"productId": 78237, "lowPrice": 36.0, "midPrice": 89.24, "highPrice": 159.91, "marketPrice": 47.42, "directLowPrice": 59.99, "subTypeName": "Foil"}, {"productId": 95046, "lowPrice": 43.0, "midPrice": 62.92, "highPrice": 97.57, "marketPrice": 64.98, "directLowPrice": 44.0, "subTypeName": "Foil"}, {"productId": 110267, "lowPrice": 23.75, "midPrice": 31.21, "highPrice": 48.4, "marketPrice": 27.89, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 125324, "lowPrice": 18.0, "midPrice": 25.99, "highPrice": 53.45, "marketPrice": 25.86, "directLowPrice": 28.58, "subTypeName": "Foil"}, {"productId": 154792, "lowPrice": 16.73, "midPrice": 25.43, "highPrice": 49.0, "marketPrice": 17.47, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 181788, "lowPrice": 23.57, "midPrice": 27.83, "highPrice": 51.0, "marketPrice": 23.83, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 205180, "lowPrice": 16.99, "midPrice": 29.99, "highPrice": 70.2, "marketPrice": 18.3, "directLowPrice": 70.58, "subTypeName": "Foil"}, {"productId": 228752, "lowPrice": 24.99, "midPrice": 29.98, "highPrice": 224.99, "marketPrice": 29.43, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 257182, "lowPrice": 30.6, "midPrice": 75.0, "highPrice": 141.0, "marketPrice": 30.6, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 455848, "lowPrice": 15.0, "midPrice": 27.98, "highPrice": 134.0, "marketPrice": 20.73, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 527738, "lowPrice": 14.69, "midPrice": 19.31, "highPrice": 80.0, "marketPrice": 16.28, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 600517, "lowPrice": 47.97, "midPrice": 61.22, "highPrice": 75.0, "marketPrice": 61.22, "directLowPrice": null, "subTypeName": "Foil"}]}
|
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
{"success": true, "errors": [], "results": [{"productId": 21705, "lowPrice": 0.18, "midPrice": 0.57, "highPrice": 29.99, "marketPrice": 0.56, "directLowPrice": 0.43, "subTypeName": "Foil"}, {"productId": 37765, "lowPrice": 0.1, "midPrice": 0.25, "highPrice": 1.6, "marketPrice": 0.14, "directLowPrice": 0.18, "subTypeName": "Normal"}, {"productId": 37767, "lowPrice": 0.18, "midPrice": 0.41, "highPrice": 4.99, "marketPrice": 0.25, "directLowPrice": 0.6, "subTypeName": "Normal"}, {"productId": 37772, "lowPrice": 0.14, "midPrice": 0.39, "highPrice": 4.99, "marketPrice": 0.15, "directLowPrice": 0.17, "subTypeName": "Normal"}, {"productId": 37773, "lowPrice": 0.05, "midPrice": 0.24, "highPrice": 3.99, "marketPrice": 0.12, "directLowPrice": 0.07, "subTypeName": "Normal"}, {"productId": 37778, "lowPrice": 0.09, "midPrice": 0.25, "highPrice": 3.13, "marketPrice": 0.22, "directLowPrice": 0.18, "subTypeName": "Normal"}, {"productId": 37780, "lowPrice": 0.04, "midPrice": 0.37, "highPrice": 4.99, "marketPrice": 0.13, "directLowPrice": 0.19, "subTypeName": "Normal"}, {"productId": 37784, "lowPrice": 0.15, "midPrice": 0.3, "highPrice": 4.99, "marketPrice": 0.22, "directLowPrice": 0.2, "subTypeName": "Normal"}, {"productId": 37785, "lowPrice": 0.15, "midPrice": 0.37, "highPrice": 3.99, "marketPrice": 0.19, "directLowPrice": 0.09, "subTypeName": "Normal"}, {"productId": 37788, "lowPrice": 0.14, "midPrice": 0.3, "highPrice": 3.0, "marketPrice": 0.31, "directLowPrice": 0.2, "subTypeName": "Normal"}, {"productId": 37789, "lowPrice": 0.15, "midPrice": 0.35, "highPrice": 4.99, "marketPrice": 0.31, "directLowPrice": 0.15, "subTypeName": "Normal"}, {"productId": 37790, "lowPrice": 0.14, "midPrice": 0.39, "highPrice": 3.99, "marketPrice": 0.22, "directLowPrice": 0.14, "subTypeName": "Normal"}, {"productId": 37793, "lowPrice": 0.05, "midPrice": 0.25, "highPrice": 3.99, "marketPrice": 0.08, "directLowPrice": 0.08, "subTypeName": "Normal"}, {"productId": 37799, "lowPrice": 0.1, "midPrice": 0.25, "highPrice": 3.0, "marketPrice": 0.2, "directLowPrice": 0.19, "subTypeName": "Normal"}, {"productId": 37802, "lowPrice": 0.1, "midPrice": 0.25, "highPrice": 5.0, "marketPrice": 0.17, "directLowPrice": 0.1, "subTypeName": "Normal"}, {"productId": 37809, "lowPrice": 0.1, "midPrice": 0.25, "highPrice": 1.67, "marketPrice": 0.19, "directLowPrice": 0.07, "subTypeName": "Normal"}, {"productId": 37810, "lowPrice": 0.09, "midPrice": 0.35, "highPrice": 5.25, "marketPrice": 0.28, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 37812, "lowPrice": 0.18, "midPrice": 0.51, "highPrice": 4.99, "marketPrice": 0.83, "directLowPrice": 0.18, "subTypeName": "Normal"}, {"productId": 37813, "lowPrice": 0.15, "midPrice": 0.25, "highPrice": 2.99, "marketPrice": 0.22, "directLowPrice": 0.14, "subTypeName": "Normal"}, {"productId": 37814, "lowPrice": 0.14, "midPrice": 0.28, "highPrice": 1.76, "marketPrice": 0.18, "directLowPrice": null, "subTypeName": "Normal"}]}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
{"success": true, "errors": [], "results": [{"productId": 70757, "lowPrice": 0.8, "midPrice": 2.48, "highPrice": 54.0, "marketPrice": 2.61, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 70758, "lowPrice": 1.14, "midPrice": 1.74, "highPrice": 12.88, "marketPrice": 1.37, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 70759, "lowPrice": 0.75, "midPrice": 2.05, "highPrice": 8.99, "marketPrice": 2.51, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 70760, "lowPrice": 0.79, "midPrice": 1.61, "highPrice": 6.0, "marketPrice": 1.17, "directLowPrice": 1.6, "subTypeName": "Foil"}, {"productId": 70761, "lowPrice": 8.0, "midPrice": 11.12, "highPrice": 49.95, "marketPrice": 11.01, "directLowPrice": 11.96, "subTypeName": "Foil"}, {"productId": 70762, "lowPrice": 0.66, "midPrice": 1.34, "highPrice": 3.99, "marketPrice": 1.22, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 70763, "lowPrice": 4.1, "midPrice": 6.0, "highPrice": 19.99, "marketPrice": 5.09, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 70764, "lowPrice": 15.53, "midPrice": 19.96, "highPrice": 159.99, "marketPrice": 19.05, "directLowPrice": 21.69, "subTypeName": "Foil"}, {"productId": 70765, "lowPrice": 100.0, "midPrice": 119.99, "highPrice": 400.0, "marketPrice": 85.88, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 70782, "lowPrice": 7.26, "midPrice": 13.24, "highPrice": 20.57, "marketPrice": 13.16, "directLowPrice": 17.05, "subTypeName": "Foil"}, {"productId": 70783, "lowPrice": 4.0, "midPrice": 6.15, "highPrice": 11.84, "marketPrice": 5.69, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 70784, "lowPrice": 3.86, "midPrice": 5.41, "highPrice": 19.55, "marketPrice": 4.66, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 70785, "lowPrice": 1.25, "midPrice": 1.9, "highPrice": 7.4, "marketPrice": 1.6, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 70786, "lowPrice": 3.0, "midPrice": 4.4, "highPrice": 10.0, "marketPrice": 3.42, "directLowPrice": 4.39, "subTypeName": "Foil"}, {"productId": 70787, "lowPrice": 9.25, "midPrice": 11.47, "highPrice": 49.88, "marketPrice": 11.19, "directLowPrice": 19.37, "subTypeName": "Foil"}, {"productId": 70789, "lowPrice": 0.5, "midPrice": 1.04, "highPrice": 16.99, "marketPrice": 0.8, "directLowPrice": 1.07, "subTypeName": "Foil"}, {"productId": 70790, "lowPrice": 0.39, "midPrice": 0.99, "highPrice": 16.45, "marketPrice": 0.68, "directLowPrice": 0.6, "subTypeName": "Foil"}, {"productId": 70791, "lowPrice": 0.23, "midPrice": 0.59, "highPrice": 3.99, "marketPrice": 0.45, "directLowPrice": 0.49, "subTypeName": "Foil"}, {"productId": 70792, "lowPrice": 1.5, "midPrice": 2.49, "highPrice": 9.99, "marketPrice": 2.26, "directLowPrice": null, "subTypeName": "Foil"}, {"productId": 70793, "lowPrice": 49.0, "midPrice": 59.02, "highPrice": 200.0, "marketPrice": 59.02, "directLowPrice": 51.49, "subTypeName": "Foil"}, {"productId": 70794, "lowPrice": 1.8, "midPrice": 3.91, "highPrice": 88.0, "marketPrice": 3.69, "directLowPrice": 2.94, "subTypeName": "Foil"}]}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
{"success": true, "errors": [], "results": [{"productId": 21603, "lowPrice": 11.0, "midPrice": 16.55, "highPrice": 97.99, "marketPrice": 17.1, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21604, "lowPrice": 4.95, "midPrice": 10.97, "highPrice": 14.97, "marketPrice": 6.99, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21605, "lowPrice": 10.0, "midPrice": 13.95, "highPrice": 79.45, "marketPrice": 14.97, "directLowPrice": 5.01, "subTypeName": "Normal"}, {"productId": 21606, "lowPrice": 18.95, "midPrice": 21.88, "highPrice": 86.37, "marketPrice": 17.0, "directLowPrice": 43.94, "subTypeName": "Normal"}, {"productId": 21607, "lowPrice": 0.38, "midPrice": 2.46, "highPrice": 5.7, "marketPrice": 1.5, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21608, "lowPrice": 1.55, "midPrice": 2.47, "highPrice": 4.95, "marketPrice": 2.0, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21609, "lowPrice": 98.95, "midPrice": 103.94, "highPrice": 108.93, "marketPrice": 57.85, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21610, "lowPrice": 4.73, "midPrice": 7.87, "highPrice": 11.0, "marketPrice": 7.61, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21611, "lowPrice": 9.0, "midPrice": 16.58, "highPrice": 22.17, "marketPrice": 14.16, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21612, "lowPrice": 5.99, "midPrice": 6.91, "highPrice": 15.74, "marketPrice": 6.29, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21613, "lowPrice": 9.55, "midPrice": 14.61, "highPrice": 18.95, "marketPrice": 13.98, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21614, "lowPrice": 1.7, "midPrice": 2.5, "highPrice": 4.95, "marketPrice": 1.4, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21615, "lowPrice": 3.86, "midPrice": 5.5, "highPrice": 8.0, "marketPrice": 6.07, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21616, "lowPrice": 8.84, "midPrice": 19.97, "highPrice": 24.95, "marketPrice": 8.5, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21617, "lowPrice": 6.92, "midPrice": 8.0, "highPrice": 13.99, "marketPrice": 7.23, "directLowPrice": 5.23, "subTypeName": "Normal"}, {"productId": 21618, "lowPrice": 25.0, "midPrice": 29.91, "highPrice": 50.0, "marketPrice": 27.0, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21619, "lowPrice": 8.0, "midPrice": 11.41, "highPrice": 18.61, "marketPrice": 9.89, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21620, "lowPrice": 15.0, "midPrice": 18.95, "highPrice": 20.59, "marketPrice": 8.76, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21621, "lowPrice": 18.5, "midPrice": 22.49, "highPrice": 49.95, "marketPrice": 25.0, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21622, "lowPrice": 7.99, "midPrice": 16.72, "highPrice": 19.95, "marketPrice": 11.91, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21623, "lowPrice": 12.0, "midPrice": 15.0, "highPrice": 16.99, "marketPrice": 15.95, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21624, "lowPrice": 0.59, "midPrice": 1.87, "highPrice": 9.95, "marketPrice": 2.5, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21625, "lowPrice": 49.95, "midPrice": 71.67, "highPrice": 99.95, "marketPrice": 66.45, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21626, "lowPrice": 3.99, "midPrice": 4.87, "highPrice": 8.12, "marketPrice": 5.5, "directLowPrice": 3.0, "subTypeName": "Normal"}, {"productId": 21627, "lowPrice": 6.77, "midPrice": 9.03, "highPrice": 24.99, "marketPrice": 5.16, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21628, "lowPrice": 1.1, "midPrice": 2.25, "highPrice": 4.23, "marketPrice": 1.5, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21629, "lowPrice": 5.0, "midPrice": 5.22, "highPrice": 13.97, "marketPrice": 5.0, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21630, "lowPrice": 16.8, "midPrice": 27.54, "highPrice": 45.0, "marketPrice": 15.95, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21631, "lowPrice": 114.95, "midPrice": 150.0, "highPrice": 154.97, "marketPrice": 42.5, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21632, "lowPrice": 16.99, "midPrice": 55.98, "highPrice": 150.0, "marketPrice": 14.79, "directLowPrice": null, "subTypeName": "Normal"}, {"productId": 21633, "lowPrice": 10.99, "midPrice": 13.84, "highPrice": 23.95, "marketPrice": 15.49, "directLowPrice": 19.88, "subTypeName": "Normal"}, {"productId": 21634, "lowPrice": 49.97, "midPrice": 74.36, "highPrice": 98.75, "marketPrice": 49.97, "directLowPrice": null, "subTypeName": "Normal"}]}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user