i think most of this works lole
This commit is contained in:
parent
210a033695
commit
56ba750aad
@ -1,34 +0,0 @@
|
|||||||
"""product
|
|
||||||
|
|
||||||
Revision ID: 06605c625998
|
|
||||||
Revises: 28cfdbde1796
|
|
||||||
Create Date: 2025-04-22 10:10:18.082314
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '06605c625998'
|
|
||||||
down_revision: Union[str, None] = '28cfdbde1796'
|
|
||||||
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('physical_items_product_id_fkey', 'physical_items', type_='foreignkey')
|
|
||||||
op.create_foreign_key(None, 'physical_items', 'tcgplayer_products', ['product_id'], ['id'])
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_constraint(None, 'physical_items', type_='foreignkey')
|
|
||||||
op.create_foreign_key('physical_items_product_id_fkey', 'physical_items', 'products', ['product_id'], ['id'])
|
|
||||||
# ### 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 ###
|
|
@ -1,86 +0,0 @@
|
|||||||
"""idk lol
|
|
||||||
|
|
||||||
Revision ID: 28cfdbde1796
|
|
||||||
Revises: d4d3f43ce86a
|
|
||||||
Create Date: 2025-04-19 16:17:07.109451
|
|
||||||
|
|
||||||
"""
|
|
||||||
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 = '28cfdbde1796'
|
|
||||||
down_revision: Union[str, None] = 'd4d3f43ce86a'
|
|
||||||
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_cost_basis_id', table_name='cost_basis')
|
|
||||||
op.drop_table('cost_basis')
|
|
||||||
op.drop_index('ix_cards_id', table_name='cards')
|
|
||||||
op.drop_index('ix_cards_name', table_name='cards')
|
|
||||||
op.drop_index('ix_cards_set_name', table_name='cards')
|
|
||||||
op.drop_index('ix_cards_tcgplayer_sku', table_name='cards')
|
|
||||||
op.drop_table('cards')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
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('cost_basis',
|
|
||||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
|
||||||
sa.Column('transaction_item_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('sealed_case_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('sealed_box_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('open_box_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('open_card_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('quantity', sa.INTEGER(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('unit_cost', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('deleted_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.ForeignKeyConstraint(['open_card_id'], ['open_cards.id'], name='cost_basis_open_card_id_fkey'),
|
|
||||||
sa.ForeignKeyConstraint(['sealed_box_id'], ['sealed_boxes.id'], name='cost_basis_sealed_box_id_fkey'),
|
|
||||||
sa.ForeignKeyConstraint(['sealed_case_id'], ['sealed_cases.id'], name='cost_basis_sealed_case_id_fkey'),
|
|
||||||
sa.ForeignKeyConstraint(['transaction_item_id'], ['transaction_items.id'], name='cost_basis_transaction_item_id_fkey'),
|
|
||||||
sa.PrimaryKeyConstraint('id', name='cost_basis_pkey')
|
|
||||||
)
|
|
||||||
op.create_index('ix_cost_basis_id', 'cost_basis', ['id'], unique=False)
|
|
||||||
# ### 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,97 +0,0 @@
|
|||||||
"""inventory management improvements
|
|
||||||
|
|
||||||
Revision ID: 32d21f7d33e9
|
|
||||||
Revises: d2c33dee079c
|
|
||||||
Create Date: 2025-04-22 16:43:57.844419
|
|
||||||
|
|
||||||
"""
|
|
||||||
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 = '32d21f7d33e9'
|
|
||||||
down_revision: Union[str, None] = 'd2c33dee079c'
|
|
||||||
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('marketplaces',
|
|
||||||
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_marketplaces_id'), 'marketplaces', ['id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_marketplaces_name'), 'marketplaces', ['name'], unique=True)
|
|
||||||
op.create_table('sealed_expected_values',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('product_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('expected_value', sa.Float(), 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(['product_id'], ['tcgplayer_products.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_sealed_expected_values_id'), 'sealed_expected_values', ['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('product_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), nullable=True),
|
|
||||||
sa.Column('updated_at', sa.DateTime(timezone=True), 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.ForeignKeyConstraint(['product_id'], ['tcgplayer_products.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_marketplace_listings_id'), 'marketplace_listings', ['id'], unique=False)
|
|
||||||
op.drop_index('ix_product_expected_values_id', table_name='product_expected_values')
|
|
||||||
op.drop_table('product_expected_values')
|
|
||||||
op.add_column('sealed_boxes', sa.Column('expected_value', sa.Float(), nullable=True))
|
|
||||||
op.add_column('sealed_cases', sa.Column('expected_value', sa.Float(), nullable=True))
|
|
||||||
op.add_column('sealed_cases', sa.Column('num_boxes', sa.Integer(), nullable=True))
|
|
||||||
op.add_column('transactions', sa.Column('marketplace_id', sa.Integer(), nullable=True))
|
|
||||||
op.create_foreign_key(None, 'transactions', 'marketplaces', ['marketplace_id'], ['id'])
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_constraint(None, 'transactions', type_='foreignkey')
|
|
||||||
op.drop_column('transactions', 'marketplace_id')
|
|
||||||
op.drop_column('sealed_cases', 'num_boxes')
|
|
||||||
op.drop_column('sealed_cases', 'expected_value')
|
|
||||||
op.drop_column('sealed_boxes', 'expected_value')
|
|
||||||
op.create_table('product_expected_values',
|
|
||||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
|
||||||
sa.Column('product_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('expected_value', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('deleted_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(['product_id'], ['tcgplayer_products.id'], name='product_expected_values_product_id_fkey'),
|
|
||||||
sa.PrimaryKeyConstraint('id', name='product_expected_values_pkey')
|
|
||||||
)
|
|
||||||
op.create_index('ix_product_expected_values_id', 'product_expected_values', ['id'], unique=False)
|
|
||||||
op.drop_index(op.f('ix_marketplace_listings_id'), table_name='marketplace_listings')
|
|
||||||
op.drop_table('marketplace_listings')
|
|
||||||
op.drop_index(op.f('ix_sealed_expected_values_id'), table_name='sealed_expected_values')
|
|
||||||
op.drop_table('sealed_expected_values')
|
|
||||||
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')
|
|
||||||
# ### 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,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,32 +0,0 @@
|
|||||||
"""tcg prices
|
|
||||||
|
|
||||||
Revision ID: b45c43900b56
|
|
||||||
Revises: 1746d35187a2
|
|
||||||
Create Date: 2025-04-17 22:47:44.405906
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = 'b45c43900b56'
|
|
||||||
down_revision: Union[str, None] = '1746d35187a2'
|
|
||||||
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,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
|
|
534
alembic/versions/d0792389ab20_why_god.py
Normal file
534
alembic/versions/d0792389ab20_why_god.py
Normal file
@ -0,0 +1,534 @@
|
|||||||
|
"""why god
|
||||||
|
|
||||||
|
Revision ID: d0792389ab20
|
||||||
|
Revises:
|
||||||
|
Create Date: 2025-04-24 22:36:02.509040
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'd0792389ab20'
|
||||||
|
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('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.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('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
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(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_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(['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')
|
||||||
|
)
|
||||||
|
op.create_index('idx_sku_product_printing', 'mtgjson_skus', ['tcgplayer_product_id', 'normalized_printing'], unique=True)
|
||||||
|
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('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('sealed_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('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), 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(['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('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('sealed_boxes',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('case_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('expected_value', sa.Float(), 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), 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(['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_boxes',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('open_event_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('sealed_box_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['id'], ['physical_items.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['open_event_id'], ['open_events.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['sealed_box_id'], ['sealed_boxes.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
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')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('open_cards')
|
||||||
|
op.drop_table('open_boxes')
|
||||||
|
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_marketplace_listings_id'), table_name='marketplace_listings')
|
||||||
|
op.drop_table('marketplace_listings')
|
||||||
|
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_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_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_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 ###
|
@ -1,55 +0,0 @@
|
|||||||
"""small change and mat view
|
|
||||||
|
|
||||||
Revision ID: d2c33dee079c
|
|
||||||
Revises: 06605c625998
|
|
||||||
Create Date: 2025-04-22 10:27:25.322827
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = 'd2c33dee079c'
|
|
||||||
down_revision: Union[str, None] = '06605c625998'
|
|
||||||
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('product_expected_values',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('product_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('expected_value', sa.Float(), 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(['product_id'], ['tcgplayer_products.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_product_expected_values_id'), 'product_expected_values', ['id'], unique=False)
|
|
||||||
op.execute("""
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS most_recent_tcgplayer_price AS
|
|
||||||
SELECT DISTINCT ON (product_id) *
|
|
||||||
FROM public.tcgplayer_price_history
|
|
||||||
ORDER BY product_id, date DESC;
|
|
||||||
""")
|
|
||||||
op.execute("""
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_most_recent_tcgplayer_price_product_id
|
|
||||||
ON most_recent_tcgplayer_price (product_id);
|
|
||||||
""")
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_index(op.f('ix_product_expected_values_id'), table_name='product_expected_values')
|
|
||||||
op.drop_table('product_expected_values')
|
|
||||||
op.execute("DROP INDEX IF EXISTS idx_most_recent_tcgplayer_price_product_id;")
|
|
||||||
op.execute("DROP MATERIALIZED VIEW IF EXISTS most_recent_tcgplayer_price;")
|
|
||||||
# ### 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 ###
|
|
@ -1,19 +1,17 @@
|
|||||||
from app.services.base_service import BaseService
|
|
||||||
from app.models.inventory_management import InventoryItem
|
from app.models.inventory_management import InventoryItem
|
||||||
from app.models.tcgplayer_product import TCGPlayerProduct
|
|
||||||
from app.contexts.inventory_product import InventoryProductContext
|
from app.contexts.inventory_product import InventoryProductContext
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from app.models.tcgplayer_products import TCGPlayerProduct
|
||||||
|
|
||||||
class InventoryItemContext:
|
class InventoryItemContext:
|
||||||
def __init__(self, item: InventoryItem, db: Session):
|
def __init__(self, item: InventoryItem, db: Session):
|
||||||
self.item = item
|
self.item = item
|
||||||
self.physical_item = item.physical_item
|
self.physical_item = item.physical_item
|
||||||
self.marketplace_listing = item.marketplace_listing
|
self.marketplace_listings = item.marketplace_listings
|
||||||
self.parent = item.parent
|
self.parent = item.parent
|
||||||
self.children = item.children
|
self.children = item.children
|
||||||
self.product = item.physical_item.product
|
self.product = item.product
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -26,7 +24,7 @@ class InventoryItemContext:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def product_name(self) -> str:
|
def product_name(self) -> str:
|
||||||
return self.physical_item.product_name
|
return self.product.name if self.product else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def item_type(self) -> str:
|
def item_type(self) -> str:
|
||||||
@ -34,41 +32,49 @@ class InventoryItemContext:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def market_price(self) -> float:
|
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
|
return self.product.most_recent_tcgplayer_price.market_price
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tcg_low_price(self) -> float:
|
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
|
return self.product.most_recent_tcgplayer_price.low_price
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def listed_price(self) -> float:
|
def listed_price(self) -> float:
|
||||||
return self.marketplace_listing.listed_price
|
if not self.marketplace_listings:
|
||||||
|
return 0.0
|
||||||
|
return self.marketplace_listings[0].listed_price if self.marketplace_listings else 0.0
|
||||||
|
|
||||||
def top_level_parent(self) -> "InventoryItemContext":
|
def top_level_parent(self) -> "InventoryItemContext":
|
||||||
if self.parent:
|
if self.parent:
|
||||||
return self.parent.top_level_parent()
|
return InventoryItemContext(self.parent, self.db)
|
||||||
else:
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def box_expected_value(self) -> float:
|
def box_expected_value(self) -> float:
|
||||||
top_level_parent_item = self.top_level_parent_item()
|
top_level_parent = self.top_level_parent()
|
||||||
if 'case' in top_level_parent_item.item_type:
|
if 'case' in top_level_parent.item_type:
|
||||||
return top_level_parent_item.open_event.sealed_case.expected_value
|
return top_level_parent.physical_item.open_event.sealed_case.expected_value
|
||||||
elif 'box' in top_level_parent_item.item_type:
|
elif 'box' in top_level_parent.item_type:
|
||||||
return top_level_parent_item.open_event.sealed_box.expected_value
|
return top_level_parent.physical_item.open_event.sealed_box.expected_value
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown top level parent item type")
|
raise ValueError("Unknown top level parent item type")
|
||||||
|
|
||||||
def box_acquisition_cost(self) -> float:
|
def box_acquisition_cost(self) -> float:
|
||||||
if self.physical_item.transaction_item:
|
if self.physical_item.transaction_items:
|
||||||
return self.physical_item.transaction_item.unit_price
|
return self.physical_item.transaction_items[0].unit_price
|
||||||
elif self.parent:
|
elif self.parent:
|
||||||
return self.parent.box_acquisition_cost()
|
return InventoryItemContext(self.parent, self.db).box_acquisition_cost()
|
||||||
else:
|
else:
|
||||||
raise ValueError("Cannot find transaction unit price for this item")
|
raise ValueError("Cannot find transaction unit price for this item")
|
||||||
|
|
||||||
def age_on_marketplace(self) -> int:
|
def age_on_marketplace(self) -> int:
|
||||||
return (datetime.now() - self.marketplace_listing.listing_date).days
|
if not self.marketplace_listings:
|
||||||
|
return 0
|
||||||
|
return (datetime.now() - self.marketplace_listings[0].listing_date).days
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemContextFactory:
|
class InventoryItemContextFactory:
|
||||||
def __init__(self, db: Session):
|
def __init__(self, db: Session):
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from app.models.tcgplayer_product import TCGPlayerProduct
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models.tcgplayer_products import TCGPlayerProduct
|
||||||
class InventoryProductContext:
|
class InventoryProductContext:
|
||||||
def __init__(self, product: TCGPlayerProduct, db: Session):
|
def __init__(self, product: TCGPlayerProduct, db: Session):
|
||||||
self.product = product
|
self.product = product
|
||||||
|
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
|
|
10
app/main.py
10
app/main.py
@ -10,6 +10,7 @@ from pathlib import Path
|
|||||||
from app.routes import routes
|
from app.routes import routes
|
||||||
from app.db.database import init_db, SessionLocal
|
from app.db.database import init_db, SessionLocal
|
||||||
from app.services.service_manager import ServiceManager
|
from app.services.service_manager import ServiceManager
|
||||||
|
from app.models.tcgplayer_products import refresh_view
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
log_file = Path("app.log")
|
log_file = Path("app.log")
|
||||||
@ -58,8 +59,13 @@ async def lifespan(app: FastAPI):
|
|||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
data_init_service = service_manager.get_service('data_initialization')
|
data_init_service = service_manager.get_service('data_initialization')
|
||||||
data_init = await data_init_service.initialize_data(db, game_ids=[1, 3], use_cache=True, 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")
|
# data_init = await data_init_service.initialize_data(db, game_ids=[1, 3], use_cache=False, init_categories=False, init_products=True, 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}")
|
# logger.info(f"Data initialization results: {data_init}")
|
||||||
|
data_init = await data_init_service.initialize_data(db, game_ids=[1], use_cache=False, init_categories=True, init_groups=True, init_products=True, init_archived_prices=True, archived_prices_start_date="2025-04-22", archived_prices_end_date="2025-04-24", init_mtgjson=True)
|
||||||
|
#refresh_view(db)
|
||||||
|
|
||||||
|
# Create default customer, vendor, and marketplace
|
||||||
|
#inv_data_init = await data_init_service.initialize_inventory_data(db)
|
||||||
|
|
||||||
# Start the scheduler
|
# Start the scheduler
|
||||||
scheduler = service_manager.get_service('scheduler')
|
scheduler = service_manager.get_service('scheduler')
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from app.models.critical_error_log import CriticalErrorLog
|
||||||
from app.models.file import File
|
from app.models.file import File
|
||||||
from app.models.inventory_management import (
|
from app.models.inventory_management import (
|
||||||
PhysicalItem,
|
PhysicalItem,
|
||||||
@ -6,25 +7,32 @@ from app.models.inventory_management import (
|
|||||||
OpenEvent,
|
OpenEvent,
|
||||||
Vendor,
|
Vendor,
|
||||||
Customer,
|
Customer,
|
||||||
Transaction
|
Transaction,
|
||||||
|
SealedExpectedValue,
|
||||||
|
Marketplace,
|
||||||
|
MarketplaceListing
|
||||||
|
)
|
||||||
|
from app.models.tcgplayer_products import (
|
||||||
|
MTGJSONCard,
|
||||||
|
MTGJSONSKU,
|
||||||
|
TCGPlayerProduct,
|
||||||
|
TCGPlayerCategory,
|
||||||
|
TCGPlayerGroup,
|
||||||
|
TCGPlayerPriceHistory,
|
||||||
|
MostRecentTCGPlayerPrice
|
||||||
)
|
)
|
||||||
from app.models.mtgjson_card import MTGJSONCard
|
|
||||||
from app.models.mtgjson_sku import MTGJSONSKU
|
|
||||||
from app.models.product import Product
|
|
||||||
from app.models.tcgplayer_category import TCGPlayerCategory
|
|
||||||
from app.models.tcgplayer_group import TCGPlayerGroup
|
|
||||||
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
|
||||||
from app.models.tcgplayer_order import (
|
from app.models.tcgplayer_order import (
|
||||||
TCGPlayerOrder,
|
TCGPlayerOrder,
|
||||||
TCGPlayerOrderTransaction,
|
TCGPlayerOrderTransaction,
|
||||||
TCGPlayerOrderProduct,
|
TCGPlayerOrderProduct,
|
||||||
TCGPlayerOrderRefund
|
TCGPlayerOrderRefund
|
||||||
)
|
)
|
||||||
from app.models.tcgplayer_price_history import TCGPlayerPriceHistory
|
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
||||||
from app.models.tcgplayer_product import TCGPlayerProduct
|
from app.models.manabox_import_staging import ManaboxImportStaging
|
||||||
|
|
||||||
|
|
||||||
# This makes all models available for Alembic to discover
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
'CriticalErrorLog',
|
||||||
'File',
|
'File',
|
||||||
'PhysicalItem',
|
'PhysicalItem',
|
||||||
'InventoryItem',
|
'InventoryItem',
|
||||||
@ -33,16 +41,20 @@ __all__ = [
|
|||||||
'Vendor',
|
'Vendor',
|
||||||
'Customer',
|
'Customer',
|
||||||
'Transaction',
|
'Transaction',
|
||||||
|
'SealedExpectedValue',
|
||||||
|
'Marketplace',
|
||||||
|
'MarketplaceListing',
|
||||||
'MTGJSONCard',
|
'MTGJSONCard',
|
||||||
'MTGJSONSKU',
|
'MTGJSONSKU',
|
||||||
'Product',
|
'TCGPlayerProduct',
|
||||||
'TCGPlayerCategory',
|
'TCGPlayerCategory',
|
||||||
'TCGPlayerGroup',
|
'TCGPlayerGroup',
|
||||||
'TCGPlayerInventory',
|
'TCGPlayerInventory',
|
||||||
|
'ManaboxImportStaging',
|
||||||
'TCGPlayerOrder',
|
'TCGPlayerOrder',
|
||||||
'TCGPlayerOrderTransaction',
|
'TCGPlayerOrderTransaction',
|
||||||
'TCGPlayerOrderProduct',
|
'TCGPlayerOrderProduct',
|
||||||
'TCGPlayerOrderRefund',
|
'TCGPlayerOrderRefund',
|
||||||
'TCGPlayerPriceHistory',
|
'TCGPlayerPriceHistory',
|
||||||
'TCGPlayerProduct'
|
'MostRecentTCGPlayerPrice'
|
||||||
]
|
]
|
11
app/models/critical_error_log.py
Normal file
11
app/models/critical_error_log.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.database import Base
|
||||||
|
|
||||||
|
class CriticalErrorLog(Base):
|
||||||
|
__tablename__ = "critical_error_logs"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
error_message = Column(String, nullable=False)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
@ -1,5 +1,6 @@
|
|||||||
from sqlalchemy import Column, Integer, String, DateTime, JSON
|
from sqlalchemy import Column, Integer, String, DateTime, JSON
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
from app.db.database import Base
|
from app.db.database import Base
|
||||||
|
|
||||||
|
|
||||||
@ -11,7 +12,11 @@ class File(Base):
|
|||||||
file_type = Column(String)
|
file_type = Column(String)
|
||||||
content_type = Column(String)
|
content_type = Column(String)
|
||||||
path = Column(String)
|
path = Column(String)
|
||||||
size = Column(Integer) # File size in bytes
|
size = Column(Integer)
|
||||||
file_metadata = Column(JSON)
|
file_metadata = Column(JSON)
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
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,43 +1,93 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
|
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, CheckConstraint
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from app.db.database import Base
|
from app.db.database import Base
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import func
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
|
from datetime import datetime
|
||||||
|
from app.models.critical_error_log import CriticalErrorLog
|
||||||
|
|
||||||
class PhysicalItem(Base):
|
class PhysicalItem(Base):
|
||||||
__tablename__ = "physical_items"
|
__tablename__ = "physical_items"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
item_type = Column(String)
|
item_type = Column(String)
|
||||||
product_id = Column(Integer, ForeignKey("tcgplayer_products.id"), nullable=False)
|
|
||||||
created_at = Column(DateTime(timezone=True))
|
# at least one of these must be set to pass the constraint
|
||||||
updated_at = Column(DateTime(timezone=True))
|
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)
|
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__ = {
|
__mapper_args__ = {
|
||||||
'polymorphic_on': item_type,
|
'polymorphic_on': item_type,
|
||||||
'polymorphic_identity': 'physical_item'
|
'polymorphic_identity': 'physical_item'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
product = relationship("TCGPlayerProduct")
|
sku = relationship("MTGJSONSKU", back_populates="physical_items", primaryjoin="PhysicalItem.tcgplayer_sku_id == MTGJSONSKU.tcgplayer_sku_id")
|
||||||
|
product_direct = relationship("TCGPlayerProduct",
|
||||||
|
back_populates="physical_items_direct",
|
||||||
|
primaryjoin="PhysicalItem.tcgplayer_product_id == foreign(TCGPlayerProduct.tcgplayer_product_id)")
|
||||||
inventory_item = relationship("InventoryItem", uselist=False, back_populates="physical_item")
|
inventory_item = relationship("InventoryItem", uselist=False, back_populates="physical_item")
|
||||||
transaction_item = relationship("TransactionItem", back_populates="physical_item")
|
transaction_items = relationship("TransactionItem", back_populates="physical_item")
|
||||||
|
|
||||||
class SealedCase(PhysicalItem):
|
@hybrid_property
|
||||||
__tablename__ = "sealed_cases"
|
def products(self):
|
||||||
|
"""
|
||||||
|
Dynamically resolve the associated TCGPlayerProduct(s):
|
||||||
|
- If the SKU is set, return all linked products.
|
||||||
|
- Else, return a list containing a single product from direct link.
|
||||||
|
"""
|
||||||
|
# TODO IS THIS EVEN CORRECT OH GOD
|
||||||
|
if self.sku and self.sku.products:
|
||||||
|
return self.sku.products # This is a list of TCGPlayerProduct
|
||||||
|
return [self.product_direct] if self.product_direct else []
|
||||||
|
|
||||||
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
|
class InventoryItem(Base):
|
||||||
expected_value = Column(Float)
|
__tablename__ = "inventory_items"
|
||||||
num_boxes = Column(Integer)
|
|
||||||
|
|
||||||
__mapper_args__ = {
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
'polymorphic_identity': 'sealed_case'
|
physical_item_id = Column(Integer, ForeignKey("physical_items.id"), unique=True)
|
||||||
}
|
cost_basis = Column(Float)
|
||||||
|
parent_id = Column(Integer, ForeignKey("inventory_items.id"), nullable=True)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
boxes = relationship("SealedBox", back_populates="case", foreign_keys="[SealedBox.case_id]")
|
physical_item = relationship("PhysicalItem", back_populates="inventory_item")
|
||||||
open_event = relationship("OpenEvent", uselist=False, back_populates="sealed_case")
|
parent = relationship("InventoryItem", remote_side=[id], back_populates="children")
|
||||||
|
children = relationship("InventoryItem", back_populates="parent", overlaps="parent")
|
||||||
|
marketplace_listings = relationship("MarketplaceListing", back_populates="inventory_item")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def products(self):
|
||||||
|
"""
|
||||||
|
Proxy access to the associated TCGPlayerProduct(s) via the linked PhysicalItem.
|
||||||
|
Returns:
|
||||||
|
list[TCGPlayerProduct] or [] if no physical item or no linked products.
|
||||||
|
"""
|
||||||
|
return self.physical_item.product if self.physical_item else []
|
||||||
|
|
||||||
|
def soft_delete(self, timestamp=None):
|
||||||
|
if not timestamp:
|
||||||
|
timestamp = datetime.now()
|
||||||
|
self.deleted_at = timestamp
|
||||||
|
for child in self.children:
|
||||||
|
child.soft_delete(timestamp)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SealedBox(PhysicalItem):
|
class SealedBox(PhysicalItem):
|
||||||
__tablename__ = "sealed_boxes"
|
__tablename__ = "sealed_boxes"
|
||||||
@ -54,42 +104,38 @@ class SealedBox(PhysicalItem):
|
|||||||
case = relationship("SealedCase", back_populates="boxes", foreign_keys=[case_id])
|
case = relationship("SealedCase", back_populates="boxes", foreign_keys=[case_id])
|
||||||
open_event = relationship("OpenEvent", uselist=False, back_populates="sealed_box")
|
open_event = relationship("OpenEvent", uselist=False, back_populates="sealed_box")
|
||||||
|
|
||||||
# event listeners
|
class SealedCase(PhysicalItem):
|
||||||
@event.listens_for(SealedCase, 'before_insert')
|
__tablename__ = "sealed_cases"
|
||||||
def set_expected_value(mapper, connection, target):
|
|
||||||
session = Session.object_session(target)
|
|
||||||
if session:
|
|
||||||
expected_value = session.query(SealedExpectedValue).filter(SealedExpectedValue.product_id == target.product_id).filter(SealedExpectedValue.deleted_at == None).order_by(SealedExpectedValue.created_at.desc()).first()
|
|
||||||
if expected_value:
|
|
||||||
target.expected_value = expected_value.expected_value
|
|
||||||
else:
|
|
||||||
raise ValueError("No expected value found for this product")
|
|
||||||
|
|
||||||
@event.listens_for(SealedBox, 'before_insert')
|
|
||||||
def set_expected_value(mapper, connection, target):
|
|
||||||
session = Session.object_session(target)
|
|
||||||
if session:
|
|
||||||
expected_value = session.query(SealedExpectedValue).filter(SealedExpectedValue.product_id == target.product_id).filter(SealedExpectedValue.deleted_at == None).order_by(SealedExpectedValue.created_at.desc()).first()
|
|
||||||
if expected_value:
|
|
||||||
target.expected_value = expected_value.expected_value
|
|
||||||
else:
|
|
||||||
raise ValueError("No expected value found for this product")
|
|
||||||
|
|
||||||
class OpenBox(PhysicalItem):
|
|
||||||
__tablename__ = "open_boxes"
|
|
||||||
|
|
||||||
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
|
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
|
||||||
open_event_id = Column(Integer, ForeignKey("open_events.id"))
|
expected_value = Column(Float)
|
||||||
sealed_box_id = Column(Integer, ForeignKey("sealed_boxes.id"))
|
num_boxes = Column(Integer)
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {
|
||||||
'polymorphic_identity': 'open_box'
|
'polymorphic_identity': 'sealed_case'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
open_event = relationship("OpenEvent", back_populates="resulting_boxes")
|
boxes = relationship("SealedBox", back_populates="case", foreign_keys=[SealedBox.case_id])
|
||||||
sealed_box = relationship("SealedBox", foreign_keys=[sealed_box_id])
|
open_event = relationship("OpenEvent", uselist=False, back_populates="sealed_case")
|
||||||
cards = relationship("OpenCard", back_populates="box", foreign_keys="[OpenCard.box_id]")
|
|
||||||
|
|
||||||
|
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), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
sealed_case = relationship("SealedCase", back_populates="open_event")
|
||||||
|
sealed_box = relationship("SealedBox", back_populates="open_event")
|
||||||
|
resulting_boxes = relationship("OpenBox", back_populates="open_event")
|
||||||
|
resulting_cards = relationship("OpenCard", back_populates="open_event")
|
||||||
|
|
||||||
class OpenCard(PhysicalItem):
|
class OpenCard(PhysicalItem):
|
||||||
__tablename__ = "open_cards"
|
__tablename__ = "open_cards"
|
||||||
@ -106,22 +152,90 @@ class OpenCard(PhysicalItem):
|
|||||||
open_event = relationship("OpenEvent", back_populates="resulting_cards")
|
open_event = relationship("OpenEvent", back_populates="resulting_cards")
|
||||||
box = relationship("OpenBox", back_populates="cards", foreign_keys=[box_id])
|
box = relationship("OpenBox", back_populates="cards", foreign_keys=[box_id])
|
||||||
|
|
||||||
class InventoryItem(Base):
|
class OpenBox(PhysicalItem):
|
||||||
__tablename__ = "inventory_items"
|
__tablename__ = "open_boxes"
|
||||||
|
|
||||||
|
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
|
||||||
|
open_event_id = Column(Integer, ForeignKey("open_events.id"))
|
||||||
|
sealed_box_id = Column(Integer, ForeignKey("sealed_boxes.id"))
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': 'open_box'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
open_event = relationship("OpenEvent", back_populates="resulting_boxes")
|
||||||
|
sealed_box = relationship("SealedBox", foreign_keys=[sealed_box_id])
|
||||||
|
cards = relationship("OpenCard", back_populates="box", foreign_keys=[OpenCard.box_id])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SealedExpectedValue(Base):
|
||||||
|
__tablename__ = "sealed_expected_values"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
physical_item_id = Column(Integer, ForeignKey("physical_items.id"), unique=True)
|
tcgplayer_product_id = Column(Integer, nullable=False)
|
||||||
cost_basis = Column(Float) # Current cost basis for this item
|
expected_value = Column(Float, nullable=False)
|
||||||
parent_id = Column(Integer, ForeignKey("inventory_items.id"), nullable=True) # For tracking hierarchy
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
created_at = Column(DateTime(timezone=True))
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True))
|
|
||||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
physical_item = relationship("PhysicalItem", back_populates="inventory_item")
|
product = relationship(
|
||||||
parent = relationship("InventoryItem", remote_side=[id], back_populates="children")
|
"TCGPlayerProduct",
|
||||||
children = relationship("InventoryItem", back_populates="parent", overlaps="parent")
|
primaryjoin="SealedExpectedValue.tcgplayer_product_id == foreign(TCGPlayerProduct.tcgplayer_product_id)",
|
||||||
marketplace_listing = relationship("MarketplaceListing", back_populates="inventory_item")
|
viewonly=True,
|
||||||
|
backref="sealed_expected_values"
|
||||||
|
)
|
||||||
|
|
||||||
|
# helper for ev
|
||||||
|
def assign_expected_value(target, session):
|
||||||
|
products = target.product # uses hybrid property
|
||||||
|
if not products:
|
||||||
|
raise ValueError(f"No product found for item ID {target.id}")
|
||||||
|
|
||||||
|
if len(products) > 1:
|
||||||
|
product_names = [p.name for p in products]
|
||||||
|
critical_error = CriticalErrorLog(
|
||||||
|
error_type="multiple_products_found",
|
||||||
|
error_message=f"Multiple products found when assigning expected value for item ID {target.id} product names {product_names}"
|
||||||
|
)
|
||||||
|
session.add(critical_error)
|
||||||
|
session.commit()
|
||||||
|
raise ValueError(f"Multiple products found when assigning expected value for item ID {target.id} product names {product_names}")
|
||||||
|
|
||||||
|
product_id = products[0].tcgplayer_product_id # reliable lookup key
|
||||||
|
|
||||||
|
expected_value_entry = session.query(SealedExpectedValue).filter(
|
||||||
|
SealedExpectedValue.tcgplayer_product_id == product_id,
|
||||||
|
SealedExpectedValue.deleted_at == None
|
||||||
|
).order_by(SealedExpectedValue.created_at.desc()).first()
|
||||||
|
|
||||||
|
if expected_value_entry:
|
||||||
|
target.expected_value = expected_value_entry.expected_value
|
||||||
|
else:
|
||||||
|
critical_error = CriticalErrorLog(
|
||||||
|
error_type="no_expected_value_found",
|
||||||
|
error_message=f"No expected value found for product {products[0].name}"
|
||||||
|
)
|
||||||
|
session.add(critical_error)
|
||||||
|
session.commit()
|
||||||
|
raise ValueError(f"No expected value found for product {products[0].name}")
|
||||||
|
|
||||||
|
|
||||||
|
# event listeners
|
||||||
|
@event.listens_for(SealedBox, 'before_insert')
|
||||||
|
def sealed_box_before_insert(mapper, connection, target):
|
||||||
|
session = Session.object_session(target)
|
||||||
|
if session:
|
||||||
|
assign_expected_value(target, session)
|
||||||
|
|
||||||
|
@event.listens_for(SealedCase, 'before_insert')
|
||||||
|
def sealed_case_before_insert(mapper, connection, target):
|
||||||
|
session = Session.object_session(target)
|
||||||
|
if session:
|
||||||
|
assign_expected_value(target, session)
|
||||||
|
|
||||||
class TransactionItem(Base):
|
class TransactionItem(Base):
|
||||||
__tablename__ = "transaction_items"
|
__tablename__ = "transaction_items"
|
||||||
@ -130,48 +244,31 @@ class TransactionItem(Base):
|
|||||||
transaction_id = Column(Integer, ForeignKey("transactions.id"))
|
transaction_id = Column(Integer, ForeignKey("transactions.id"))
|
||||||
physical_item_id = Column(Integer, ForeignKey("physical_items.id"))
|
physical_item_id = Column(Integer, ForeignKey("physical_items.id"))
|
||||||
unit_price = Column(Float, nullable=False)
|
unit_price = Column(Float, nullable=False)
|
||||||
created_at = Column(DateTime(timezone=True))
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True))
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
transaction = relationship("Transaction", back_populates="transaction_items")
|
transaction = relationship("Transaction", back_populates="transaction_items")
|
||||||
physical_item = relationship("PhysicalItem", 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")
|
|
||||||
|
|
||||||
class Vendor(Base):
|
class Vendor(Base):
|
||||||
__tablename__ = "vendors"
|
__tablename__ = "vendors"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
name = Column(String, unique=True, index=True)
|
name = Column(String, index=True)
|
||||||
created_at = Column(DateTime(timezone=True))
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True))
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
class Customer(Base):
|
class Customer(Base):
|
||||||
__tablename__ = "customers"
|
__tablename__ = "customers"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
name = Column(String, unique=True, index=True)
|
name = Column(String, index=True)
|
||||||
created_at = Column(DateTime(timezone=True))
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True))
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
class Transaction(Base):
|
class Transaction(Base):
|
||||||
__tablename__ = "transactions"
|
__tablename__ = "transactions"
|
||||||
|
|
||||||
@ -183,49 +280,23 @@ class Transaction(Base):
|
|||||||
transaction_date = Column(DateTime(timezone=True))
|
transaction_date = Column(DateTime(timezone=True))
|
||||||
transaction_total_amount = Column(Float)
|
transaction_total_amount = Column(Float)
|
||||||
transaction_notes = Column(String)
|
transaction_notes = Column(String)
|
||||||
created_at = Column(DateTime(timezone=True))
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True))
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
transaction_items = relationship("TransactionItem", back_populates="transaction")
|
transaction_items = relationship("TransactionItem", back_populates="transaction")
|
||||||
|
|
||||||
class SealedExpectedValue(Base):
|
|
||||||
__tablename__ = "sealed_expected_values"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
product_id = Column(Integer, ForeignKey("tcgplayer_products.id"), nullable=True)
|
|
||||||
expected_value = Column(Float)
|
|
||||||
created_at = Column(DateTime(timezone=True))
|
|
||||||
updated_at = Column(DateTime(timezone=True))
|
|
||||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
|
||||||
|
|
||||||
class MostRecentTCGPlayerPrice(Base):
|
|
||||||
__tablename__ = "most_recent_tcgplayer_price"
|
|
||||||
__table_args__ = {'extend_existing': True, 'autoload_with': None, 'info': {'is_view': True}}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
product_id = Column(Integer, ForeignKey("tcgplayer_products.id"))
|
|
||||||
date = Column(DateTime)
|
|
||||||
low_price = Column(Float)
|
|
||||||
mid_price = Column(Float)
|
|
||||||
high_price = Column(Float)
|
|
||||||
market_price = Column(Float)
|
|
||||||
direct_low_price = Column(Float)
|
|
||||||
sub_type_name = Column(String)
|
|
||||||
created_at = Column(DateTime(timezone=True))
|
|
||||||
updated_at = Column(DateTime(timezone=True))
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
product = relationship("TCGPlayerProduct", back_populates="most_recent_tcgplayer_price")
|
|
||||||
|
|
||||||
class Marketplace(Base):
|
class Marketplace(Base):
|
||||||
__tablename__ = "marketplaces"
|
__tablename__ = "marketplaces"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
name = Column(String, unique=True, index=True)
|
name = Column(String, index=True)
|
||||||
created_at = Column(DateTime(timezone=True))
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True))
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
listings = relationship("MarketplaceListing", back_populates="marketplace")
|
listings = relationship("MarketplaceListing", back_populates="marketplace")
|
||||||
|
|
||||||
class MarketplaceListing(Base):
|
class MarketplaceListing(Base):
|
||||||
@ -234,15 +305,13 @@ class MarketplaceListing(Base):
|
|||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
inventory_item_id = Column(Integer, ForeignKey("inventory_items.id"), nullable=False)
|
inventory_item_id = Column(Integer, ForeignKey("inventory_items.id"), nullable=False)
|
||||||
marketplace_id = Column(Integer, ForeignKey("marketplaces.id"), nullable=False)
|
marketplace_id = Column(Integer, ForeignKey("marketplaces.id"), nullable=False)
|
||||||
product_id = Column(Integer, ForeignKey("tcgplayer_products.id"), nullable=False)
|
|
||||||
listing_date = Column(DateTime(timezone=True))
|
listing_date = Column(DateTime(timezone=True))
|
||||||
delisting_date = Column(DateTime(timezone=True), nullable=True)
|
delisting_date = Column(DateTime(timezone=True), nullable=True)
|
||||||
listed_price = Column(Float)
|
listed_price = Column(Float)
|
||||||
created_at = Column(DateTime(timezone=True))
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True))
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
inventory_item = relationship("InventoryItem", back_populates="marketplace_listings")
|
inventory_item = relationship("InventoryItem", back_populates="marketplace_listings")
|
||||||
marketplace = relationship("Marketplace", back_populates="listings")
|
marketplace = relationship("Marketplace", back_populates="listings")
|
||||||
product = relationship("TCGPlayerProduct", back_populates="marketplace_listings")
|
|
18
app/models/manabox_import_staging.py
Normal file
18
app/models/manabox_import_staging.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from app.db.database import Base
|
||||||
|
|
||||||
|
class ManaboxImportStaging(Base):
|
||||||
|
__tablename__ = "manabox_import_staging"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
file_id = Column(Integer, ForeignKey("files.id"))
|
||||||
|
tcgplayer_sku_id = Column(Integer)
|
||||||
|
quantity = Column(Integer)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
file = relationship("File", back_populates="manabox_import_staging")
|
@ -1,43 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, String, DateTime
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from app.db.database import Base
|
|
||||||
|
|
||||||
class MTGJSONCard(Base):
|
|
||||||
__tablename__ = "mtgjson_cards"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
card_id = Column(String, unique=True, index=True)
|
|
||||||
name = Column(String)
|
|
||||||
set_code = Column(String)
|
|
||||||
uuid = Column(String)
|
|
||||||
|
|
||||||
# Identifiers
|
|
||||||
abu_id = Column(String, nullable=True)
|
|
||||||
card_kingdom_etched_id = Column(String, nullable=True)
|
|
||||||
card_kingdom_foil_id = Column(String, nullable=True)
|
|
||||||
card_kingdom_id = Column(String, nullable=True)
|
|
||||||
cardsphere_id = Column(String, nullable=True)
|
|
||||||
cardsphere_foil_id = Column(String, nullable=True)
|
|
||||||
cardtrader_id = Column(String, nullable=True)
|
|
||||||
csi_id = Column(String, nullable=True)
|
|
||||||
mcm_id = Column(String, nullable=True)
|
|
||||||
mcm_meta_id = Column(String, nullable=True)
|
|
||||||
miniaturemarket_id = Column(String, nullable=True)
|
|
||||||
mtg_arena_id = Column(String, nullable=True)
|
|
||||||
mtgjson_foil_version_id = Column(String, nullable=True)
|
|
||||||
mtgjson_non_foil_version_id = Column(String, nullable=True)
|
|
||||||
mtgjson_v4_id = Column(String, nullable=True)
|
|
||||||
mtgo_foil_id = Column(String, nullable=True)
|
|
||||||
mtgo_id = Column(String, nullable=True)
|
|
||||||
multiverse_id = Column(String, nullable=True)
|
|
||||||
scg_id = Column(String, nullable=True)
|
|
||||||
scryfall_id = Column(String, nullable=True)
|
|
||||||
scryfall_card_back_id = Column(String, nullable=True)
|
|
||||||
scryfall_oracle_id = Column(String, nullable=True)
|
|
||||||
scryfall_illustration_id = Column(String, nullable=True)
|
|
||||||
tcgplayer_product_id = Column(String, nullable=True)
|
|
||||||
tcgplayer_etched_product_id = Column(String, nullable=True)
|
|
||||||
tnt_id = Column(String, nullable=True)
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.current_timestamp())
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.current_timestamp())
|
|
@ -1,17 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, String, DateTime
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from app.db.database import Base
|
|
||||||
|
|
||||||
class MTGJSONSKU(Base):
|
|
||||||
__tablename__ = "mtgjson_skus"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
sku_id = Column(String, index=True)
|
|
||||||
product_id = Column(String, index=True)
|
|
||||||
condition = Column(String)
|
|
||||||
finish = Column(String)
|
|
||||||
language = Column(String)
|
|
||||||
printing = Column(String)
|
|
||||||
card_id = Column(String, nullable=True)
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.current_timestamp())
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.current_timestamp())
|
|
@ -1,12 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, String, DateTime
|
|
||||||
from app.db.database import Base
|
|
||||||
|
|
||||||
class Product(Base):
|
|
||||||
__tablename__ = "products"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
name = Column(String)
|
|
||||||
tcgplayer_id = Column(String)
|
|
||||||
created_at = Column(DateTime(timezone=True))
|
|
||||||
updated_at = Column(DateTime(timezone=True))
|
|
||||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
|
@ -1,23 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from app.db.database import Base
|
|
||||||
|
|
||||||
class TCGPlayerCategory(Base):
|
|
||||||
__tablename__ = "tcgplayer_categories"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
category_id = Column(Integer, unique=True, index=True)
|
|
||||||
name = Column(String, nullable=False)
|
|
||||||
display_name = Column(String)
|
|
||||||
seo_category_name = Column(String)
|
|
||||||
category_description = Column(String)
|
|
||||||
category_page_title = Column(String)
|
|
||||||
sealed_label = Column(String)
|
|
||||||
non_sealed_label = Column(String)
|
|
||||||
condition_guide_url = Column(String)
|
|
||||||
is_scannable = Column(Boolean, default=False)
|
|
||||||
popularity = Column(Integer, default=0)
|
|
||||||
is_direct = Column(Boolean, default=False)
|
|
||||||
modified_on = Column(DateTime)
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
@ -1,17 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from app.db.database import Base
|
|
||||||
|
|
||||||
class TCGPlayerGroup(Base):
|
|
||||||
__tablename__ = "tcgplayer_groups"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
group_id = Column(Integer, unique=True, index=True)
|
|
||||||
name = Column(String, nullable=False)
|
|
||||||
abbreviation = Column(String)
|
|
||||||
is_supplemental = Column(Boolean, default=False)
|
|
||||||
published_on = Column(DateTime)
|
|
||||||
modified_on = Column(DateTime)
|
|
||||||
category_id = Column(Integer)
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
@ -1,4 +1,4 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Float, DateTime
|
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.db.database import Base
|
from app.db.database import Base
|
||||||
|
|
||||||
@ -6,7 +6,7 @@ class TCGPlayerInventory(Base):
|
|||||||
__tablename__ = "tcgplayer_inventory"
|
__tablename__ = "tcgplayer_inventory"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
tcgplayer_id = Column(String, unique=True, index=True)
|
tcgplayer_sku_id = Column(Integer, ForeignKey("mtgjson_skus.tcgplayer_sku_id"), unique=True, index=True)
|
||||||
product_line = Column(String)
|
product_line = Column(String)
|
||||||
set_name = Column(String)
|
set_name = Column(String)
|
||||||
product_name = Column(String)
|
product_name = Column(String)
|
||||||
@ -22,6 +22,5 @@ class TCGPlayerInventory(Base):
|
|||||||
add_to_quantity = Column(Integer)
|
add_to_quantity = Column(Integer)
|
||||||
tcg_marketplace_price = Column(Float)
|
tcg_marketplace_price = Column(Float)
|
||||||
photo_url = Column(String)
|
photo_url = Column(String)
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.current_timestamp())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.current_timestamp())
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
|
@ -1,13 +1,14 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Float, DateTime, JSON
|
from sqlalchemy import Column, Integer, String, Float, DateTime, JSON, ForeignKey
|
||||||
from datetime import datetime, UTC
|
from sqlalchemy.sql import func
|
||||||
from app.db.database import Base
|
from app.db.database import Base
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
class TCGPlayerOrder(Base):
|
class TCGPlayerOrder(Base):
|
||||||
__tablename__ = "tcgplayer_orders"
|
__tablename__ = "tcgplayer_orders"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
order_number = Column(String, index=True)
|
order_number = Column(String, index=True)
|
||||||
order_created_at = Column(DateTime)
|
order_created_at = Column(DateTime(timezone=True))
|
||||||
status = Column(String)
|
status = Column(String)
|
||||||
channel = Column(String)
|
channel = Column(String)
|
||||||
fulfillment = Column(String)
|
fulfillment = Column(String)
|
||||||
@ -16,7 +17,7 @@ class TCGPlayerOrder(Base):
|
|||||||
payment_type = Column(String)
|
payment_type = Column(String)
|
||||||
pickup_status = Column(String)
|
pickup_status = Column(String)
|
||||||
shipping_type = Column(String)
|
shipping_type = Column(String)
|
||||||
estimated_delivery_date = Column(DateTime)
|
estimated_delivery_date = Column(DateTime(timezone=True))
|
||||||
recipient_name = Column(String)
|
recipient_name = Column(String)
|
||||||
address_line_1 = Column(String)
|
address_line_1 = Column(String)
|
||||||
address_line_2 = Column(String)
|
address_line_2 = Column(String)
|
||||||
@ -25,8 +26,8 @@ class TCGPlayerOrder(Base):
|
|||||||
zip_code = Column(String)
|
zip_code = Column(String)
|
||||||
country = Column(String)
|
country = Column(String)
|
||||||
tracking_numbers = Column(JSON)
|
tracking_numbers = Column(JSON)
|
||||||
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
class TCGPlayerOrderTransaction(Base):
|
class TCGPlayerOrderTransaction(Base):
|
||||||
@ -41,8 +42,8 @@ class TCGPlayerOrderTransaction(Base):
|
|||||||
net_amount = Column(Float)
|
net_amount = Column(Float)
|
||||||
direct_fee_amount = Column(Float)
|
direct_fee_amount = Column(Float)
|
||||||
taxes = Column(JSON)
|
taxes = Column(JSON)
|
||||||
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
class TCGPlayerOrderProduct(Base):
|
class TCGPlayerOrderProduct(Base):
|
||||||
@ -55,17 +56,18 @@ class TCGPlayerOrderProduct(Base):
|
|||||||
extended_price = Column(Float)
|
extended_price = Column(Float)
|
||||||
quantity = Column(Integer)
|
quantity = Column(Integer)
|
||||||
url = Column(String)
|
url = Column(String)
|
||||||
product_id = Column(String)
|
product_id = Column(Integer)
|
||||||
sku_id = Column(String)
|
sku_id = Column(Integer)
|
||||||
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
class TCGPlayerOrderRefund(Base):
|
class TCGPlayerOrderRefund(Base):
|
||||||
__tablename__ = "tcgplayer_order_refunds"
|
__tablename__ = "tcgplayer_order_refunds"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
order_number = Column(String, index=True)
|
order_number = Column(String, index=True)
|
||||||
refund_created_at = Column(DateTime)
|
refund_created_at = Column(DateTime(timezone=True))
|
||||||
type = Column(String)
|
type = Column(String)
|
||||||
amount = Column(Float)
|
amount = Column(Float)
|
||||||
type = Column(String)
|
type = Column(String)
|
||||||
@ -73,5 +75,5 @@ class TCGPlayerOrderRefund(Base):
|
|||||||
origin = Column(String)
|
origin = Column(String)
|
||||||
shipping_amount = Column(Float)
|
shipping_amount = Column(Float)
|
||||||
products = Column(JSON)
|
products = Column(JSON)
|
||||||
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, Float, DateTime, String
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from app.db.database import Base
|
|
||||||
|
|
||||||
class TCGPlayerPriceHistory(Base):
|
|
||||||
__tablename__ = "tcgplayer_price_history"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
product_id = Column(Integer, index=True)
|
|
||||||
date = Column(DateTime, index=True)
|
|
||||||
low_price = Column(Float)
|
|
||||||
mid_price = Column(Float)
|
|
||||||
high_price = Column(Float)
|
|
||||||
market_price = Column(Float)
|
|
||||||
direct_low_price = Column(Float)
|
|
||||||
sub_type_name = Column(String)
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
@ -1,38 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Float, DateTime
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.database import Base
|
|
||||||
|
|
||||||
class TCGPlayerProduct(Base):
|
|
||||||
__tablename__ = "tcgplayer_products"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
product_id = Column(Integer, index=True)
|
|
||||||
name = Column(String, nullable=False)
|
|
||||||
clean_name = Column(String)
|
|
||||||
image_url = Column(String)
|
|
||||||
category_id = Column(Integer)
|
|
||||||
group_id = Column(Integer)
|
|
||||||
url = Column(String)
|
|
||||||
modified_on = Column(DateTime)
|
|
||||||
image_count = Column(Integer)
|
|
||||||
ext_rarity = Column(String)
|
|
||||||
ext_subtype = Column(String)
|
|
||||||
ext_oracle_text = Column(String)
|
|
||||||
ext_number = Column(String)
|
|
||||||
low_price = Column(Float)
|
|
||||||
mid_price = Column(Float)
|
|
||||||
high_price = Column(Float)
|
|
||||||
market_price = Column(Float)
|
|
||||||
direct_low_price = Column(Float)
|
|
||||||
sub_type_name = Column(String)
|
|
||||||
ext_power = Column(String)
|
|
||||||
ext_toughness = Column(String)
|
|
||||||
ext_flavor_text = Column(String)
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
||||||
|
|
||||||
most_recent_tcgplayer_price = relationship("MostRecentTCGPlayerPrice", back_populates="product")
|
|
||||||
marketplace_listings = relationship("MarketplaceListing", back_populates="product")
|
|
||||||
inventory_items = relationship("InventoryItem", back_populates="product")
|
|
282
app/models/tcgplayer_products.py
Normal file
282
app/models/tcgplayer_products.py
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Table, Float, Boolean, Index, text, DDL, event, UniqueConstraint, ForeignKeyConstraint
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.database import Base
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Core Models
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class MTGJSONSKU(Base):
|
||||||
|
"""Represents the most granular level of card identification.
|
||||||
|
THIS WORKS EVEN IF ITS A BOX SOMEHOW
|
||||||
|
"""
|
||||||
|
__tablename__ = "mtgjson_skus"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
mtgjson_uuid = Column(String, ForeignKey("mtgjson_cards.mtgjson_uuid"), unique=True, index=True)
|
||||||
|
tcgplayer_sku_id = Column(Integer, index=True, unique=True)
|
||||||
|
tcgplayer_product_id = Column(Integer, nullable=False)
|
||||||
|
normalized_printing = Column(String, nullable=False) # normalized for FK
|
||||||
|
condition = Column(String) # for boxes, condition = unopened
|
||||||
|
finish = Column(String, nullable=True) # TODO MAKE THESE ENUMS
|
||||||
|
language = Column(String)
|
||||||
|
printing = Column(String) # original unnormalized field ##### for boxes, printing = NON FOIL
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
# Foreign key to tcgplayer_products via composite key
|
||||||
|
__table_args__ = (
|
||||||
|
ForeignKeyConstraint(
|
||||||
|
["tcgplayer_product_id", "normalized_printing"],
|
||||||
|
["tcgplayer_products.tcgplayer_product_id", "tcgplayer_products.normalized_sub_type_name"],
|
||||||
|
name="fk_sku_to_product_composite"
|
||||||
|
),
|
||||||
|
Index('idx_sku_product_printing', 'tcgplayer_product_id', 'normalized_printing', unique=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
product = relationship("TCGPlayerProduct", back_populates="skus")
|
||||||
|
physical_items = relationship("PhysicalItem", back_populates="sku", primaryjoin="PhysicalItem.tcgplayer_sku_id == MTGJSONSKU.tcgplayer_sku_id")
|
||||||
|
|
||||||
|
card = relationship("MTGJSONCard", back_populates="skus", primaryjoin="MTGJSONCard.mtgjson_uuid == MTGJSONSKU.mtgjson_uuid")
|
||||||
|
|
||||||
|
|
||||||
|
class MTGJSONCard(Base):
|
||||||
|
"""Represents a Magic: The Gathering card."""
|
||||||
|
__tablename__ = "mtgjson_cards"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
mtgjson_uuid = Column(String, ForeignKey("mtgjson_cards.mtgjson_uuid"), unique=True, index=True)
|
||||||
|
name = Column(String)
|
||||||
|
set_code = Column(String)
|
||||||
|
abu_id = Column(String, nullable=True)
|
||||||
|
card_kingdom_etched_id = Column(String, nullable=True)
|
||||||
|
card_kingdom_foil_id = Column(String, nullable=True)
|
||||||
|
card_kingdom_id = Column(String, nullable=True)
|
||||||
|
cardsphere_id = Column(String, nullable=True)
|
||||||
|
cardsphere_foil_id = Column(String, nullable=True)
|
||||||
|
cardtrader_id = Column(String, nullable=True)
|
||||||
|
csi_id = Column(String, nullable=True)
|
||||||
|
mcm_id = Column(String, nullable=True)
|
||||||
|
mcm_meta_id = Column(String, nullable=True)
|
||||||
|
miniaturemarket_id = Column(String, nullable=True)
|
||||||
|
mtg_arena_id = Column(String, nullable=True)
|
||||||
|
mtgjson_foil_version_id = Column(String, nullable=True)
|
||||||
|
mtgjson_non_foil_version_id = Column(String, nullable=True)
|
||||||
|
mtgjson_v4_id = Column(String, nullable=True)
|
||||||
|
mtgo_foil_id = Column(String, nullable=True)
|
||||||
|
mtgo_id = Column(String, nullable=True)
|
||||||
|
multiverse_id = Column(String, nullable=True)
|
||||||
|
scg_id = Column(String, nullable=True)
|
||||||
|
scryfall_id = Column(String, nullable=True)
|
||||||
|
scryfall_card_back_id = Column(String, nullable=True)
|
||||||
|
scryfall_oracle_id = Column(String, nullable=True)
|
||||||
|
scryfall_illustration_id = Column(String, nullable=True)
|
||||||
|
tcgplayer_product_id = Column(String, nullable=True)
|
||||||
|
tcgplayer_etched_product_id = Column(String, nullable=True)
|
||||||
|
tnt_id = Column(String, nullable=True)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
skus = relationship("MTGJSONSKU", back_populates="card", primaryjoin="MTGJSONSKU.mtgjson_uuid == MTGJSONCard.mtgjson_uuid")
|
||||||
|
|
||||||
|
|
||||||
|
class TCGPlayerProduct(Base):
|
||||||
|
"""Represents a higher-level TCGPlayer product concept."""
|
||||||
|
__tablename__ = "tcgplayer_products"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
tcgplayer_product_id = Column(Integer, nullable=False)
|
||||||
|
normalized_sub_type_name = Column(String, nullable=False) # normalized for FK
|
||||||
|
sub_type_name = Column(String) # original unnormalized field
|
||||||
|
|
||||||
|
name = Column(String, nullable=False)
|
||||||
|
clean_name = Column(String)
|
||||||
|
image_url = Column(String)
|
||||||
|
category_id = Column(Integer, ForeignKey("tcgplayer_categories.category_id"))
|
||||||
|
group_id = Column(Integer, ForeignKey("tcgplayer_groups.group_id"))
|
||||||
|
url = Column(String)
|
||||||
|
modified_on = Column(DateTime)
|
||||||
|
image_count = Column(Integer)
|
||||||
|
ext_rarity = Column(String)
|
||||||
|
ext_subtype = Column(String)
|
||||||
|
ext_oracle_text = Column(String)
|
||||||
|
ext_number = Column(String)
|
||||||
|
low_price = Column(Float)
|
||||||
|
mid_price = Column(Float)
|
||||||
|
high_price = Column(Float)
|
||||||
|
market_price = Column(Float)
|
||||||
|
direct_low_price = Column(Float)
|
||||||
|
ext_power = Column(String)
|
||||||
|
ext_toughness = Column(String)
|
||||||
|
ext_flavor_text = Column(String)
|
||||||
|
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
# Enforce uniqueness for composite key
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("tcgplayer_product_id", "normalized_sub_type_name", name="uq_product_subtype"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Backref to SKUs that link via composite FK
|
||||||
|
skus = relationship("MTGJSONSKU", back_populates="product")
|
||||||
|
physical_items_direct = relationship("PhysicalItem",
|
||||||
|
back_populates="product_direct",
|
||||||
|
primaryjoin="foreign(TCGPlayerProduct.tcgplayer_product_id) == PhysicalItem.tcgplayer_product_id")
|
||||||
|
category = relationship("TCGPlayerCategory", back_populates="products")
|
||||||
|
group = relationship("TCGPlayerGroup", back_populates="products")
|
||||||
|
price_history = relationship("TCGPlayerPriceHistory",
|
||||||
|
back_populates="product",
|
||||||
|
primaryjoin="and_(foreign(TCGPlayerProduct.tcgplayer_product_id) == TCGPlayerPriceHistory.product_id, "
|
||||||
|
"foreign(TCGPlayerProduct.sub_type_name) == TCGPlayerPriceHistory.sub_type_name)")
|
||||||
|
|
||||||
|
most_recent_tcgplayer_price = relationship("MostRecentTCGPlayerPrice",
|
||||||
|
back_populates="product",
|
||||||
|
primaryjoin="and_(foreign(TCGPlayerProduct.tcgplayer_product_id) == MostRecentTCGPlayerPrice.product_id, "
|
||||||
|
"foreign(TCGPlayerProduct.sub_type_name) == MostRecentTCGPlayerPrice.sub_type_name)")
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Supporting Models
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TCGPlayerCategory(Base):
|
||||||
|
"""Represents a TCGPlayer product category."""
|
||||||
|
__tablename__ = "tcgplayer_categories"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
category_id = Column(Integer, unique=True, index=True)
|
||||||
|
name = Column(String, nullable=False)
|
||||||
|
display_name = Column(String)
|
||||||
|
seo_category_name = Column(String)
|
||||||
|
category_description = Column(String)
|
||||||
|
category_page_title = Column(String)
|
||||||
|
sealed_label = Column(String)
|
||||||
|
non_sealed_label = Column(String)
|
||||||
|
condition_guide_url = Column(String)
|
||||||
|
is_scannable = Column(Boolean, default=False)
|
||||||
|
popularity = Column(Integer, default=0)
|
||||||
|
is_direct = Column(Boolean, default=False)
|
||||||
|
modified_on = Column(DateTime)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
products = relationship("TCGPlayerProduct", back_populates="category")
|
||||||
|
|
||||||
|
class TCGPlayerGroup(Base):
|
||||||
|
"""Represents a TCGPlayer product group."""
|
||||||
|
__tablename__ = "tcgplayer_groups"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
group_id = Column(Integer, unique=True, index=True)
|
||||||
|
name = Column(String, nullable=False)
|
||||||
|
abbreviation = Column(String)
|
||||||
|
is_supplemental = Column(Boolean, default=False)
|
||||||
|
published_on = Column(DateTime)
|
||||||
|
modified_on = Column(DateTime)
|
||||||
|
category_id = Column(Integer, ForeignKey("tcgplayer_categories.category_id"))
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
products = relationship("TCGPlayerProduct", back_populates="group")
|
||||||
|
|
||||||
|
class TCGPlayerPriceHistory(Base):
|
||||||
|
"""Represents the price history for a product variant (foil/non-foil).
|
||||||
|
|
||||||
|
Each product has exactly two price history records - one for foil and one for non-foil.
|
||||||
|
The relationship to TCGPlayerProduct is effectively 1:1 when considering both product_id
|
||||||
|
and sub_type_name.
|
||||||
|
"""
|
||||||
|
__tablename__ = "tcgplayer_price_history"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
product_id = Column(Integer, nullable=False, index=True)
|
||||||
|
sub_type_name = Column(String, index=True) # This indicates foil/non-foil
|
||||||
|
date = Column(DateTime, index=True)
|
||||||
|
low_price = Column(Float)
|
||||||
|
mid_price = Column(Float)
|
||||||
|
high_price = Column(Float)
|
||||||
|
market_price = Column(Float)
|
||||||
|
direct_low_price = Column(Float)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
# Add a unique constraint on (product_id, sub_type_name, date) to prevent duplicate entries
|
||||||
|
__table_args__ = (
|
||||||
|
Index('idx_price_history_product_subtype_date', 'product_id', 'sub_type_name', 'date'),
|
||||||
|
)
|
||||||
|
|
||||||
|
product = relationship("TCGPlayerProduct",
|
||||||
|
back_populates="price_history",
|
||||||
|
primaryjoin="and_(TCGPlayerPriceHistory.product_id == foreign(TCGPlayerProduct.tcgplayer_product_id), "
|
||||||
|
"TCGPlayerPriceHistory.sub_type_name == foreign(TCGPlayerProduct.sub_type_name))")
|
||||||
|
|
||||||
|
class MostRecentTCGPlayerPrice(Base):
|
||||||
|
"""Represents the most recent price for a product.
|
||||||
|
|
||||||
|
This is a materialized view that contains the latest price data for each product.
|
||||||
|
It is maintained through triggers on the price_history table.
|
||||||
|
"""
|
||||||
|
__tablename__ = "most_recent_tcgplayer_price"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
product_id = Column(Integer, nullable=False, index=True)
|
||||||
|
sub_type_name = Column(String, index=True) # This indicates foil/non-foil
|
||||||
|
date = Column(DateTime, nullable=False)
|
||||||
|
low_price = Column(Float)
|
||||||
|
mid_price = Column(Float)
|
||||||
|
high_price = Column(Float)
|
||||||
|
market_price = Column(Float)
|
||||||
|
direct_low_price = Column(Float)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index('idx_most_recent_price_product_subtype', 'product_id', 'sub_type_name', unique=True),
|
||||||
|
{'info': {'is_view': True}}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
product = relationship("TCGPlayerProduct",
|
||||||
|
back_populates="most_recent_tcgplayer_price",
|
||||||
|
primaryjoin="and_(MostRecentTCGPlayerPrice.product_id == foreign(TCGPlayerProduct.tcgplayer_product_id), "
|
||||||
|
"MostRecentTCGPlayerPrice.sub_type_name == foreign(TCGPlayerProduct.sub_type_name))")
|
||||||
|
|
||||||
|
def create_most_recent_price_view():
|
||||||
|
"""Creates the materialized view for most recent prices."""
|
||||||
|
return DDL("""
|
||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS most_recent_tcgplayer_price AS
|
||||||
|
SELECT DISTINCT ON (ph.product_id, ph.sub_type_name)
|
||||||
|
ph.id,
|
||||||
|
ph.product_id,
|
||||||
|
ph.sub_type_name,
|
||||||
|
ph.date,
|
||||||
|
ph.low_price,
|
||||||
|
ph.mid_price,
|
||||||
|
ph.high_price,
|
||||||
|
ph.market_price,
|
||||||
|
ph.direct_low_price,
|
||||||
|
ph.created_at,
|
||||||
|
ph.updated_at
|
||||||
|
FROM tcgplayer_price_history ph
|
||||||
|
ORDER BY ph.product_id, ph.sub_type_name, ph.date DESC;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_most_recent_price_product_subtype
|
||||||
|
ON most_recent_tcgplayer_price (product_id, sub_type_name);
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Register the view creation with SQLAlchemy
|
||||||
|
event.listen(
|
||||||
|
MostRecentTCGPlayerPrice.__table__,
|
||||||
|
'after_create',
|
||||||
|
create_most_recent_price_view()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add a method to refresh the view
|
||||||
|
@classmethod
|
||||||
|
def refresh_view(cls, session):
|
||||||
|
"""Refreshes the materialized view."""
|
||||||
|
session.execute(text("REFRESH MATERIALIZED VIEW CONCURRENTLY most_recent_tcgplayer_price"))
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
MostRecentTCGPlayerPrice.refresh_view = refresh_view
|
165
app/routes/inventory_management_routes.py
Normal file
165
app/routes/inventory_management_routes.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
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, TransactionResponse
|
||||||
|
from typing import List
|
||||||
|
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.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
|
@ -1,24 +1,78 @@
|
|||||||
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
|
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Form
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.db.database import get_db
|
from app.db.database import get_db
|
||||||
from app.services.service_manager import ServiceManager
|
from app.services.service_manager import ServiceManager
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
|
||||||
router = APIRouter(prefix="/manabox")
|
router = APIRouter(prefix="/manabox")
|
||||||
|
|
||||||
service_manager = ServiceManager()
|
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")
|
@router.post("/process-csv")
|
||||||
async def process_manabox_csv(
|
async def process_manabox_csv(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
|
source: str = Form(...),
|
||||||
|
description: str = Form(...),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
try:
|
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()
|
content = await file.read()
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"source": source,
|
||||||
|
"description": description
|
||||||
|
}
|
||||||
|
|
||||||
manabox_service = service_manager.get_service("manabox")
|
manabox_service = service_manager.get_service("manabox")
|
||||||
|
|
||||||
success = await manabox_service.process_manabox_csv(db, content)
|
success = await manabox_service.process_manabox_csv(db, content, metadata)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=400, detail="Failed to process CSV file")
|
raise HTTPException(status_code=400, detail="Failed to process CSV file")
|
||||||
@ -27,3 +81,13 @@ async def process_manabox_csv(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(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
|
@ -6,7 +6,7 @@ from app.schemas.file import FileCreate, FileUpdate, FileDelete, FileList, FileI
|
|||||||
from app.routes.set_label_routes import router as set_label_router
|
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.order_routes import router as order_router
|
||||||
from app.routes.manabox_routes import router as manabox_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")
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
# Include set label routes
|
# Include set label routes
|
||||||
@ -18,6 +18,9 @@ router.include_router(order_router)
|
|||||||
# Include manabox routes
|
# Include manabox routes
|
||||||
router.include_router(manabox_router)
|
router.include_router(manabox_router)
|
||||||
|
|
||||||
|
# Include inventory management routes
|
||||||
|
router.include_router(inventory_management_router)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Health Check & Root Endpoints
|
# Health Check & Root Endpoints
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
49
app/schemas/transaction.py
Normal file
49
app/schemas/transaction.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class PurchaseItem(BaseModel):
|
||||||
|
product_id: int
|
||||||
|
unit_price: float
|
||||||
|
quantity: int
|
||||||
|
is_case: bool
|
||||||
|
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
|
||||||
|
physical_item_id: int
|
||||||
|
unit_price: float
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
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
|
||||||
|
transaction_items: List[TransactionItemResponse]
|
@ -1,23 +1,26 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timezone
|
||||||
from typing import Optional, List, Dict, Any, Union, Generator, Callable, AsyncGenerator
|
from typing import Optional, List, Dict, Any, Union
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.models.tcgplayer_group import TCGPlayerGroup
|
from app.models.tcgplayer_products import TCGPlayerProduct, TCGPlayerCategory, TCGPlayerGroup
|
||||||
from app.models.tcgplayer_product import TCGPlayerProduct
|
from app.models.inventory_management import SealedExpectedValue
|
||||||
from app.models.tcgplayer_category import TCGPlayerCategory
|
|
||||||
from app.services.base_service import BaseService
|
from app.services.base_service import BaseService
|
||||||
from app.schemas.file import FileInDB
|
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
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
import logging
|
import logging
|
||||||
from app.models.tcgplayer_price_history import TCGPlayerPriceHistory
|
|
||||||
from sqlalchemy import and_, bindparam, update, insert
|
|
||||||
import py7zr
|
|
||||||
import shutil
|
import shutil
|
||||||
|
import py7zr
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DataInitializationService(BaseService):
|
class DataInitializationService(BaseService):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(None)
|
super().__init__(None)
|
||||||
@ -54,7 +57,8 @@ class DataInitializationService(BaseService):
|
|||||||
file_record = await self.file_service.get_file_by_filename(db, filename)
|
file_record = await self.file_service.get_file_by_filename(db, filename)
|
||||||
if file_record:
|
if file_record:
|
||||||
# Check if cache is expired (7 days)
|
# 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:
|
if cache_age.days < 7:
|
||||||
with open(file_record.path, 'r') as f:
|
with open(file_record.path, 'r') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
@ -70,7 +74,7 @@ class DataInitializationService(BaseService):
|
|||||||
batch_size = 1000 # Process in batches of 1000
|
batch_size = 1000 # Process in batches of 1000
|
||||||
total_categories = len(categories)
|
total_categories = len(categories)
|
||||||
|
|
||||||
with transaction(db):
|
with db_transaction(db):
|
||||||
for i in range(0, total_categories, batch_size):
|
for i in range(0, total_categories, batch_size):
|
||||||
batch = categories[i:i + batch_size]
|
batch = categories[i:i + batch_size]
|
||||||
for category_data in batch:
|
for category_data in batch:
|
||||||
@ -150,7 +154,7 @@ class DataInitializationService(BaseService):
|
|||||||
batch_size = 1000 # Process in batches of 1000
|
batch_size = 1000 # Process in batches of 1000
|
||||||
total_groups = len(groups)
|
total_groups = len(groups)
|
||||||
|
|
||||||
with transaction(db):
|
with db_transaction(db):
|
||||||
for i in range(0, total_groups, batch_size):
|
for i in range(0, total_groups, batch_size):
|
||||||
batch = groups[i:i + batch_size]
|
batch = groups[i:i + batch_size]
|
||||||
for group_data in batch:
|
for group_data in batch:
|
||||||
@ -214,8 +218,6 @@ class DataInitializationService(BaseService):
|
|||||||
|
|
||||||
async def sync_products(self, db: Session, products_data: str):
|
async def sync_products(self, db: Session, products_data: str):
|
||||||
"""Sync products data to the database using streaming for large datasets"""
|
"""Sync products data to the database using streaming for large datasets"""
|
||||||
import csv
|
|
||||||
import io
|
|
||||||
|
|
||||||
# Parse CSV data
|
# Parse CSV data
|
||||||
csv_reader = csv.DictReader(io.StringIO(products_data))
|
csv_reader = csv.DictReader(io.StringIO(products_data))
|
||||||
@ -223,36 +225,46 @@ class DataInitializationService(BaseService):
|
|||||||
batch_size = 1000 # Process in batches of 1000
|
batch_size = 1000 # Process in batches of 1000
|
||||||
total_products = len(products_list)
|
total_products = len(products_list)
|
||||||
|
|
||||||
with transaction(db):
|
with db_transaction(db):
|
||||||
for i in range(0, total_products, batch_size):
|
for i in range(0, total_products, batch_size):
|
||||||
batch = products_list[i:i + batch_size]
|
batch = products_list[i:i + batch_size]
|
||||||
for product_data in batch:
|
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:
|
if existing_product:
|
||||||
# Update existing product
|
# Update existing product
|
||||||
for key, value in {
|
for key, value in {
|
||||||
"name": product_data["name"],
|
"name": product_data["name"],
|
||||||
"clean_name": product_data.get("cleanName"),
|
"clean_name": product_data.get("cleanName"),
|
||||||
"image_url": product_data.get("imageUrl"),
|
"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"),
|
"category_id": product_data.get("categoryId"),
|
||||||
"group_id": product_data.get("groupId"),
|
"group_id": product_data.get("groupId"),
|
||||||
"url": product_data.get("url"),
|
"url": product_data.get("url"),
|
||||||
"modified_on": datetime.fromisoformat(product_data["modifiedOn"].replace("Z", "+00:00")) if product_data.get("modifiedOn") else None,
|
"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),
|
"image_count": product_data.get("imageCount", 0),
|
||||||
"ext_rarity": product_data.get("extRarity"),
|
"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"),
|
"ext_number": product_data.get("extNumber"),
|
||||||
"low_price": float(product_data.get("lowPrice")) if product_data.get("lowPrice") else None,
|
"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,
|
"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,
|
"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,
|
"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,
|
"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():
|
}.items():
|
||||||
setattr(existing_product, key, value)
|
setattr(existing_product, key, value)
|
||||||
else:
|
else:
|
||||||
|
logger.debug(f"Creating new product: {product_data['productId']} product name: {product_data['name']}")
|
||||||
new_product = TCGPlayerProduct(
|
new_product = TCGPlayerProduct(
|
||||||
product_id=product_data["productId"],
|
tcgplayer_product_id=product_data["productId"],
|
||||||
name=product_data["name"],
|
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"),
|
clean_name=product_data.get("cleanName"),
|
||||||
image_url=product_data.get("imageUrl"),
|
image_url=product_data.get("imageUrl"),
|
||||||
category_id=product_data.get("categoryId"),
|
category_id=product_data.get("categoryId"),
|
||||||
@ -269,7 +281,7 @@ class DataInitializationService(BaseService):
|
|||||||
high_price=float(product_data.get("highPrice")) if product_data.get("highPrice") 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,
|
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,
|
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_power=product_data.get("extPower"),
|
||||||
ext_toughness=product_data.get("extToughness"),
|
ext_toughness=product_data.get("extToughness"),
|
||||||
ext_flavor_text=product_data.get("extFlavorText")
|
ext_flavor_text=product_data.get("extFlavorText")
|
||||||
@ -319,50 +331,81 @@ class DataInitializationService(BaseService):
|
|||||||
async def sync_archived_prices(self, db: Session, archived_prices_data: dict, date: datetime):
|
async def sync_archived_prices(self, db: Session, archived_prices_data: dict, date: datetime):
|
||||||
"""Sync archived prices data to the database using bulk operations.
|
"""Sync archived prices data to the database using bulk operations.
|
||||||
Note: Historical prices are never updated, only new records are inserted."""
|
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
|
if not archived_prices_data.get("success"):
|
||||||
price_records = []
|
logger.error("Price data sync failed - success flag is false")
|
||||||
|
|
||||||
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:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get existing records in bulk to avoid duplicates
|
# Get existing records in bulk to avoid duplicates using a composite key
|
||||||
product_ids = [r["product_id"] for r in price_records]
|
|
||||||
sub_type_names = [r["sub_type_name"] for r in price_records]
|
|
||||||
|
|
||||||
existing_records = db.query(TCGPlayerPriceHistory).filter(
|
existing_records = db.query(TCGPlayerPriceHistory).filter(
|
||||||
TCGPlayerPriceHistory.product_id.in_(product_ids),
|
TCGPlayerPriceHistory.date == date
|
||||||
TCGPlayerPriceHistory.date == date,
|
|
||||||
TCGPlayerPriceHistory.sub_type_name.in_(sub_type_names)
|
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
# Filter out existing records
|
|
||||||
existing_keys = {(r.product_id, r.date, r.sub_type_name) for r in 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
|
# Prepare batch insert data
|
||||||
if to_insert:
|
price_history_batch = []
|
||||||
stmt = insert(TCGPlayerPriceHistory)
|
|
||||||
db.execute(stmt, to_insert)
|
# Process price data in batches
|
||||||
db.commit()
|
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", "other")
|
||||||
|
|
||||||
|
# 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:
|
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"""
|
"""Initialize archived prices data"""
|
||||||
@ -470,7 +513,7 @@ class DataInitializationService(BaseService):
|
|||||||
# Get SKUs data
|
# Get SKUs data
|
||||||
skus_data = await mtgjson_service.get_skus(db, use_cache)
|
skus_data = await mtgjson_service.get_skus(db, use_cache)
|
||||||
if skus_data and "data" in skus_data:
|
if skus_data and "data" in skus_data:
|
||||||
skus_count = await self.sync_mtgjson_skus(db, list(skus_data["data"].values()))
|
skus_count = await self.sync_mtgjson_skus(db, skus_data)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"identifiers_processed": identifiers_count,
|
"identifiers_processed": identifiers_count,
|
||||||
@ -479,27 +522,20 @@ class DataInitializationService(BaseService):
|
|||||||
|
|
||||||
async def sync_mtgjson_identifiers(self, db: Session, identifiers_data: List[dict]) -> int:
|
async def sync_mtgjson_identifiers(self, db: Session, identifiers_data: List[dict]) -> int:
|
||||||
"""Sync MTGJSON identifiers data to the database"""
|
"""Sync MTGJSON identifiers data to the database"""
|
||||||
from app.models.mtgjson_card import MTGJSONCard
|
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
with transaction(db):
|
with db_transaction(db):
|
||||||
for card_data in identifiers_data:
|
for card_data in identifiers_data:
|
||||||
if not isinstance(card_data, dict):
|
if not isinstance(card_data, dict):
|
||||||
logger.debug(f"Skipping non-dict item: {card_data}")
|
logger.debug(f"Skipping non-dict item: {card_data}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
card_id = card_data.get("uuid")
|
existing_card = db.query(MTGJSONCard).filter(MTGJSONCard.mtgjson_uuid == card_data.get("uuid")).first()
|
||||||
if not card_id:
|
|
||||||
logger.debug(f"Skipping item without UUID: {card_data}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
existing_card = db.query(MTGJSONCard).filter(MTGJSONCard.card_id == card_id).first()
|
|
||||||
if existing_card:
|
if existing_card:
|
||||||
# Update existing card
|
# Update existing card
|
||||||
for key, value in {
|
for key, value in {
|
||||||
"name": card_data.get("name"),
|
"name": card_data.get("name"),
|
||||||
"set_code": card_data.get("setCode"),
|
"set_code": card_data.get("setCode"),
|
||||||
"uuid": card_data.get("uuid"),
|
|
||||||
"abu_id": card_data.get("identifiers", {}).get("abuId"),
|
"abu_id": card_data.get("identifiers", {}).get("abuId"),
|
||||||
"card_kingdom_etched_id": card_data.get("identifiers", {}).get("cardKingdomEtchedId"),
|
"card_kingdom_etched_id": card_data.get("identifiers", {}).get("cardKingdomEtchedId"),
|
||||||
"card_kingdom_foil_id": card_data.get("identifiers", {}).get("cardKingdomFoilId"),
|
"card_kingdom_foil_id": card_data.get("identifiers", {}).get("cardKingdomFoilId"),
|
||||||
@ -530,10 +566,9 @@ class DataInitializationService(BaseService):
|
|||||||
setattr(existing_card, key, value)
|
setattr(existing_card, key, value)
|
||||||
else:
|
else:
|
||||||
new_card = MTGJSONCard(
|
new_card = MTGJSONCard(
|
||||||
card_id=card_id,
|
mtgjson_uuid=card_data.get("uuid"),
|
||||||
name=card_data.get("name"),
|
name=card_data.get("name"),
|
||||||
set_code=card_data.get("setCode"),
|
set_code=card_data.get("setCode"),
|
||||||
uuid=card_data.get("uuid"),
|
|
||||||
abu_id=card_data.get("identifiers", {}).get("abuId"),
|
abu_id=card_data.get("identifiers", {}).get("abuId"),
|
||||||
card_kingdom_etched_id=card_data.get("identifiers", {}).get("cardKingdomEtchedId"),
|
card_kingdom_etched_id=card_data.get("identifiers", {}).get("cardKingdomEtchedId"),
|
||||||
card_kingdom_foil_id=card_data.get("identifiers", {}).get("cardKingdomFoilId"),
|
card_kingdom_foil_id=card_data.get("identifiers", {}).get("cardKingdomFoilId"),
|
||||||
@ -566,38 +601,35 @@ class DataInitializationService(BaseService):
|
|||||||
|
|
||||||
return count
|
return count
|
||||||
|
|
||||||
async def sync_mtgjson_skus(self, db: Session, skus_data: List[List[dict]]) -> int:
|
async def sync_mtgjson_skus(self, db: Session, skus_data: dict) -> int:
|
||||||
"""Sync MTGJSON SKUs data to the database"""
|
"""Sync MTGJSON SKUs data to the database"""
|
||||||
from app.models.mtgjson_sku import MTGJSONSKU
|
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
with transaction(db):
|
with db_transaction(db):
|
||||||
for product_data in skus_data:
|
for mtgjson_uuid, product_data in skus_data['data'].items():
|
||||||
for sku_data in product_data:
|
for sku_data in product_data:
|
||||||
sku_id = sku_data.get("skuId")
|
existing_record = db.query(MTGJSONSKU).filter(MTGJSONSKU.mtgjson_uuid == mtgjson_uuid).filter(MTGJSONSKU.tcgplayer_sku_id == sku_data.get("skuId")).first()
|
||||||
if not sku_id:
|
if existing_record:
|
||||||
logger.debug(f"Skipping item without SKU ID: {sku_data}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
existing_sku = db.query(MTGJSONSKU).filter(MTGJSONSKU.sku_id == str(sku_id)).first()
|
|
||||||
if existing_sku:
|
|
||||||
# Update existing SKU
|
# Update existing SKU
|
||||||
for key, value in {
|
for key, value in {
|
||||||
"product_id": sku_data.get("productId"),
|
"tcgplayer_product_id": sku_data.get("productId"),
|
||||||
"condition": sku_data.get("condition"),
|
"condition": sku_data.get("condition"),
|
||||||
"finish": sku_data.get("finish"),
|
"finish": sku_data.get("finish"),
|
||||||
"language": sku_data.get("language"),
|
"language": sku_data.get("language"),
|
||||||
"printing": sku_data.get("printing"),
|
"printing": sku_data.get("printing"),
|
||||||
|
"normalized_printing": sku_data.get("printing").lower().replace(" ", "_") if sku_data.get("printing") else None
|
||||||
}.items():
|
}.items():
|
||||||
setattr(existing_sku, key, value)
|
setattr(existing_record, key, value)
|
||||||
else:
|
else:
|
||||||
new_sku = MTGJSONSKU(
|
new_sku = MTGJSONSKU(
|
||||||
sku_id=sku_id,
|
mtgjson_uuid=mtgjson_uuid,
|
||||||
product_id=sku_data.get("productId"),
|
tcgplayer_sku_id=sku_data.get("skuId"),
|
||||||
|
tcgplayer_product_id=sku_data.get("productId"),
|
||||||
condition=sku_data.get("condition"),
|
condition=sku_data.get("condition"),
|
||||||
finish=sku_data.get("finish"),
|
finish=sku_data.get("finish"),
|
||||||
language=sku_data.get("language"),
|
language=sku_data.get("language"),
|
||||||
printing=sku_data.get("printing"),
|
printing=sku_data.get("printing"),
|
||||||
|
normalized_printing=sku_data.get("printing").lower().replace(" ", "_") if sku_data.get("printing") else None
|
||||||
)
|
)
|
||||||
db.add(new_sku)
|
db.add(new_sku)
|
||||||
count += 1
|
count += 1
|
||||||
@ -655,3 +687,61 @@ class DataInitializationService(BaseService):
|
|||||||
await self.file_service.delete_file(db, file.id)
|
await self.file_service.delete_file(db, file.id)
|
||||||
await self.mtgjson_service.clear_cache()
|
await self.mtgjson_service.clear_cache()
|
||||||
print("Cache cleared")
|
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
|
||||||
|
product_id1 = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.tcgplayer_sku == "562118").first().id
|
||||||
|
expected_value_box = SealedExpectedValue(
|
||||||
|
product_id=product_id1,
|
||||||
|
expected_value=120.69
|
||||||
|
)
|
||||||
|
db.add(expected_value_box)
|
||||||
|
db.flush()
|
||||||
|
product_id2 = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.tcgplayer_sku == "562119").first().id
|
||||||
|
expected_value_case = SealedExpectedValue(
|
||||||
|
product_id=product_id2,
|
||||||
|
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=product_id1, unit_price=100.69, quantity=1, is_case=False),
|
||||||
|
PurchaseItem(product_id=product_id2, 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:
|
||||||
|
item = InventoryItemContextFactory(db).get_context(item.physical_item.inventory_item)
|
||||||
|
logger.info(f"Item: {item}")
|
||||||
|
if item.physical_item.item_type == "sealed_box":
|
||||||
|
manabox_service = self.get_service("manabox")
|
||||||
|
file_path = 'app/data/test_data/manabox_test_file.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]
|
||||||
|
sealed_box_service = self.get_service("sealed_box")
|
||||||
|
sealed_box = sealed_box_service.get(db, item.physical_item.inventory_item.id)
|
||||||
|
success = await inventory_service.process_manabox_import_staging(db, manabox_file, sealed_box)
|
||||||
|
logger.info(f"sealed box opening success: {success}")
|
||||||
|
elif item.physical_item.item_type == "sealed_case":
|
||||||
|
if case_num == 0:
|
||||||
|
logger.info(f"sealed case {case_num} opening...")
|
||||||
|
sealed_case_service = self.get_service("sealed_case")
|
||||||
|
success = await sealed_case_service.open_sealed_case(db, item.physical_item)
|
||||||
|
logger.info(f"sealed case {case_num} opening success: {success}")
|
||||||
|
case_num += 1
|
||||||
|
|
||||||
|
logger.info("Inventory data initialized")
|
||||||
|
@ -93,8 +93,10 @@ class MTGJSONService(BaseExternalService):
|
|||||||
subdir="identifiers"
|
subdir="identifiers"
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(file_record.path, 'r') as f:
|
json_file = await self._unzip_file(file_record, "identifiers", db)
|
||||||
logger.debug(f"Loaded identifiers from MTGJSON: {file_record.path}")
|
|
||||||
|
with open(json_file.path, 'r') as f:
|
||||||
|
logger.debug(f"Loaded identifiers from MTGJSON: {json_file.path}")
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
async def get_skus(self, db: Session, use_cache: bool = True) -> Dict[str, Any]:
|
async def get_skus(self, db: Session, use_cache: bool = True) -> Dict[str, Any]:
|
||||||
@ -115,8 +117,10 @@ class MTGJSONService(BaseExternalService):
|
|||||||
subdir="skus"
|
subdir="skus"
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(file_record.path, 'r') as f:
|
json_file = await self._unzip_file(file_record, "skus", db)
|
||||||
logger.debug(f"Loaded SKUs from MTGJSON: {file_record.path}")
|
|
||||||
|
with open(json_file.path, 'r') as f:
|
||||||
|
logger.debug(f"Loaded SKUs from MTGJSON: {json_file.path}")
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
async def clear_cache(self, db: Session) -> None:
|
async def clear_cache(self, db: Session) -> None:
|
||||||
|
@ -16,6 +16,7 @@ from app.models.tcgplayer_order import (
|
|||||||
TCGPlayerOrderProduct,
|
TCGPlayerOrderProduct,
|
||||||
TCGPlayerOrderRefund
|
TCGPlayerOrderRefund
|
||||||
)
|
)
|
||||||
|
from app.models.tcgplayer_products import TCGPlayerProduct
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.db.database import transaction
|
from app.db.database import transaction
|
||||||
import os
|
import os
|
||||||
@ -190,7 +191,6 @@ class OrderManagementService(BaseTCGPlayerService):
|
|||||||
direct_fee_amount=api_order.transaction.directFeeAmount,
|
direct_fee_amount=api_order.transaction.directFeeAmount,
|
||||||
taxes=[{"code": t.code, "amount": t.amount} for t in api_order.transaction.taxes]
|
taxes=[{"code": t.code, "amount": t.amount} for t in api_order.transaction.taxes]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create products
|
# Create products
|
||||||
db_products = [
|
db_products = [
|
||||||
TCGPlayerOrderProductCreate(
|
TCGPlayerOrderProductCreate(
|
||||||
@ -376,8 +376,8 @@ class OrderManagementService(BaseTCGPlayerService):
|
|||||||
('extended_price', 'extendedPrice'),
|
('extended_price', 'extendedPrice'),
|
||||||
('quantity', 'quantity'),
|
('quantity', 'quantity'),
|
||||||
('url', 'url'),
|
('url', 'url'),
|
||||||
('product_id', 'productId'),
|
('tcgplayer_product_id', 'productId'),
|
||||||
('sku_id', 'skuId')
|
('tcgplayer_sku_id', 'skuId')
|
||||||
]
|
]
|
||||||
|
|
||||||
for db_field, api_field in product_fields_to_compare:
|
for db_field, api_field in product_fields_to_compare:
|
||||||
|
@ -6,8 +6,6 @@ import json
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.db.database import transaction
|
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
|
from app.services.inventory_service import InventoryService
|
||||||
|
|
||||||
class FileProcessingService:
|
class FileProcessingService:
|
||||||
|
@ -120,7 +120,7 @@ class FileService:
|
|||||||
"""List files with optional filtering"""
|
"""List files with optional filtering"""
|
||||||
query = db.query(File)
|
query = db.query(File)
|
||||||
if file_type:
|
if file_type:
|
||||||
query = query.filter(File.type == file_type)
|
query = query.filter(File.type == file_type).order_by(File.created_at.desc())
|
||||||
files = query.offset(skip).limit(limit).all()
|
files = query.offset(skip).limit(limit).all()
|
||||||
return [FileInDB.model_validate(file) for file in files]
|
return [FileInDB.model_validate(file) for file in files]
|
||||||
|
|
||||||
|
@ -1,63 +1,418 @@
|
|||||||
from typing import List, Optional, Dict
|
from typing import List, Optional, Dict, TypedDict
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
|
||||||
from app.services.base_service import BaseService
|
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 (
|
||||||
|
SealedBox, OpenEvent, OpenBox, OpenCard, InventoryItem, SealedCase,
|
||||||
|
Transaction, TransactionItem, Customer, Vendor, Marketplace
|
||||||
|
)
|
||||||
|
from app.schemas.file import FileInDB
|
||||||
|
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, TransactionResponse
|
||||||
|
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):
|
def __init__(self):
|
||||||
super().__init__(TCGPlayerInventory)
|
super().__init__(None)
|
||||||
|
|
||||||
def create(self, db: Session, obj_in: Dict) -> TCGPlayerInventory:
|
async def process_manabox_import_staging(self, db: Session, manabox_file_uploads: List[FileInDB], sealed_box: SealedBox) -> bool:
|
||||||
|
try:
|
||||||
|
with db_transaction(db):
|
||||||
|
# Check if box is already opened
|
||||||
|
existing_open_event = db.query(OpenEvent).filter(
|
||||||
|
OpenEvent.sealed_box_id == sealed_box.id,
|
||||||
|
OpenEvent.deleted_at.is_(None)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_open_event:
|
||||||
|
raise ValueError(f"Box {sealed_box.id} has already been opened")
|
||||||
|
|
||||||
|
# 1. Get the InventoryItemContext for the sealed box
|
||||||
|
inventory_item_context = InventoryItemContextFactory(db).get_context(sealed_box.inventory_item)
|
||||||
|
|
||||||
|
# 2. Create the OpenEvent
|
||||||
|
open_event = OpenEvent(
|
||||||
|
sealed_box_id=sealed_box.id,
|
||||||
|
open_date=datetime.now(),
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(open_event)
|
||||||
|
db.flush() # Get the ID for relationships
|
||||||
|
|
||||||
|
# 3. Create the OpenBox from the SealedBox
|
||||||
|
open_box = OpenBox(
|
||||||
|
open_event_id=open_event.id,
|
||||||
|
product_id=sealed_box.product_id,
|
||||||
|
sealed_box_id=sealed_box.id,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(open_box)
|
||||||
|
|
||||||
|
# 4. Process each card from the CSV
|
||||||
|
total_market_value = 0
|
||||||
|
cards = []
|
||||||
|
|
||||||
|
manabox_file_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_file_upload_ids)).all()
|
||||||
|
|
||||||
|
for record in staging_data:
|
||||||
|
for i in range(record.quantity):
|
||||||
|
# Create the OpenCard
|
||||||
|
open_card = OpenCard(
|
||||||
|
product_id=record.product_id,
|
||||||
|
open_event_id=open_event.id,
|
||||||
|
box_id=open_box.id,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(open_card)
|
||||||
|
|
||||||
|
# Create the InventoryItem for the card
|
||||||
|
card_inventory_item = InventoryItem(
|
||||||
|
physical_item=open_card,
|
||||||
|
cost_basis=0, # Will be calculated later
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(card_inventory_item)
|
||||||
|
|
||||||
|
# Get the market value for cost basis distribution
|
||||||
|
card_context = InventoryItemContextFactory(db).get_context(card_inventory_item)
|
||||||
|
market_value = card_context.market_price
|
||||||
|
logger.debug(f"market_value: {market_value}")
|
||||||
|
total_market_value += market_value
|
||||||
|
|
||||||
|
cards.append((open_card, card_inventory_item, market_value))
|
||||||
|
|
||||||
|
# 5. Distribute the cost basis
|
||||||
|
original_cost_basis = inventory_item_context.cost_basis
|
||||||
|
|
||||||
|
for open_card, card_inventory_item, market_value in cards:
|
||||||
|
# Calculate this card's share of the cost basis
|
||||||
|
logger.debug(f"market_value: {market_value}, total_market_value: {total_market_value}, original_cost_basis: {original_cost_basis}")
|
||||||
|
cost_basis_share = (market_value / total_market_value) * original_cost_basis
|
||||||
|
card_inventory_item.cost_basis = cost_basis_share
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
async def create_purchase_transaction(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
transaction_data: PurchaseTransactionCreate
|
||||||
|
) -> Transaction:
|
||||||
"""
|
"""
|
||||||
Create a new inventory item in the database.
|
Creates a purchase transaction from a vendor.
|
||||||
|
For each item:
|
||||||
Args:
|
1. Creates a PhysicalItem (SealedCase/SealedBox)
|
||||||
db: Database session
|
2. Creates an InventoryItem with the purchase price as cost basis
|
||||||
obj_in: Dictionary containing inventory data
|
3. Creates TransactionItems linking the purchase to the items
|
||||||
|
|
||||||
Returns:
|
|
||||||
Inventory: The created inventory object
|
|
||||||
"""
|
"""
|
||||||
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,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(transaction)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
def update(self, db: Session, db_obj: TCGPlayerInventory, obj_in: Dict) -> TCGPlayerInventory:
|
total_amount = 0
|
||||||
|
physical_items = []
|
||||||
|
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.is_case:
|
||||||
|
for i in range(item.quantity):
|
||||||
|
physical_item = await SealedCaseService().create_sealed_case(
|
||||||
|
db=db,
|
||||||
|
product_id=item.product_id,
|
||||||
|
cost_basis=item.unit_price,
|
||||||
|
num_boxes=item.num_boxes or 1
|
||||||
|
)
|
||||||
|
physical_items.append(physical_item)
|
||||||
|
else:
|
||||||
|
for i in range(item.quantity):
|
||||||
|
physical_item = await SealedBoxService().create_sealed_box(
|
||||||
|
db=db,
|
||||||
|
product_id=item.product_id,
|
||||||
|
cost_basis=item.unit_price
|
||||||
|
)
|
||||||
|
physical_items.append(physical_item)
|
||||||
|
|
||||||
|
for physical_item in physical_items:
|
||||||
|
# Create transaction item
|
||||||
|
transaction_item = TransactionItem(
|
||||||
|
transaction_id=transaction.id,
|
||||||
|
physical_item_id=physical_item.id,
|
||||||
|
unit_price=item.unit_price,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(transaction_item)
|
||||||
|
total_amount += item.unit_price
|
||||||
|
|
||||||
|
# Update transaction total
|
||||||
|
transaction.transaction_total_amount = total_amount
|
||||||
|
return transaction
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
async def create_sale_transaction(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
transaction_data: SaleTransactionCreate
|
||||||
|
) -> Transaction:
|
||||||
"""
|
"""
|
||||||
Update an existing inventory item in the database.
|
this is basically psuedocode not implemented yet
|
||||||
|
|
||||||
Args:
|
|
||||||
db: Database session
|
|
||||||
db_obj: The inventory object to update
|
|
||||||
obj_in: Dictionary containing updated inventory data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Inventory: The updated inventory object
|
|
||||||
"""
|
"""
|
||||||
return super().update(db, db_obj, obj_in)
|
try:
|
||||||
|
with db_transaction(db):
|
||||||
|
# Create the transaction
|
||||||
|
transaction = Transaction(
|
||||||
|
customer_id=transaction_data.customer_id,
|
||||||
|
marketplace_id=transaction_data.marketplace_id,
|
||||||
|
transaction_type='sale',
|
||||||
|
transaction_date=transaction_data.transaction_date,
|
||||||
|
transaction_notes=transaction_data.transaction_notes,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(transaction)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
def get_by_tcgplayer_id(self, db: Session, tcgplayer_id: str) -> Optional[TCGPlayerInventory]:
|
total_amount = 0
|
||||||
"""
|
for item in transaction_data.items:
|
||||||
Get an inventory item by its TCGPlayer ID.
|
# Get the inventory item and validate
|
||||||
|
inventory_item = db.query(InventoryItem).filter(
|
||||||
|
InventoryItem.id == item.inventory_item_id,
|
||||||
|
InventoryItem.deleted_at.is_(None)
|
||||||
|
).first()
|
||||||
|
|
||||||
Args:
|
if not inventory_item:
|
||||||
db: Database session
|
raise ValueError(f"Inventory item {item.inventory_item_id} not found")
|
||||||
tcgplayer_id: The TCGPlayer ID to find
|
|
||||||
|
|
||||||
Returns:
|
# Create transaction item
|
||||||
Optional[TCGPlayerInventory]: The inventory item if found, None otherwise
|
transaction_item = TransactionItem(
|
||||||
"""
|
transaction_id=transaction.id,
|
||||||
return db.query(self.model).filter(self.model.tcgplayer_id == tcgplayer_id).first()
|
physical_item_id=inventory_item.physical_item_id,
|
||||||
|
unit_price=item.unit_price,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(transaction_item)
|
||||||
|
total_amount += item.unit_price
|
||||||
|
|
||||||
def get_by_set(self, db: Session, set_name: str, skip: int = 0, limit: int = 100) -> List[TCGPlayerInventory]:
|
# Update marketplace listing if applicable
|
||||||
"""
|
if transaction_data.marketplace_id and inventory_item.marketplace_listings:
|
||||||
Get all inventory items from a specific set.
|
listing = inventory_item.marketplace_listings
|
||||||
|
listing.delisting_date = transaction_data.transaction_date
|
||||||
|
listing.updated_at = datetime.now()
|
||||||
|
|
||||||
Args:
|
# Update transaction total
|
||||||
db: Database session
|
transaction.transaction_total_amount = total_amount
|
||||||
set_name: The name of the set to filter by
|
return transaction
|
||||||
skip: Number of records to skip (for pagination)
|
|
||||||
limit: Maximum number of records to return
|
|
||||||
|
|
||||||
Returns:
|
except Exception as e:
|
||||||
List[TCGPlayerInventory]: List of inventory items from the specified set
|
raise e
|
||||||
"""
|
|
||||||
return db.query(self.model).filter(self.model.set_name == set_name).offset(skip).limit(limit).all()
|
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
|
||||||
|
|
||||||
|
with db_transaction(db):
|
||||||
|
customer = Customer(
|
||||||
|
name=customer_name,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(customer)
|
||||||
|
db.flush()
|
||||||
|
return customer
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
with db_transaction(db):
|
||||||
|
vendor = Vendor(
|
||||||
|
name=vendor_name,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(vendor)
|
||||||
|
db.flush()
|
||||||
|
return vendor
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
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,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(marketplace)
|
||||||
|
db.flush()
|
||||||
|
return marketplace
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
class SealedBoxService(BaseService[SealedBox]):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(SealedBox)
|
||||||
|
|
||||||
|
async def create_sealed_box(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
product_id: int,
|
||||||
|
cost_basis: float,
|
||||||
|
case_id: Optional[int] = None
|
||||||
|
) -> SealedBox:
|
||||||
|
try:
|
||||||
|
with db_transaction(db):
|
||||||
|
# Create the SealedBox
|
||||||
|
sealed_box = SealedBox(
|
||||||
|
product_id=product_id,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(sealed_box)
|
||||||
|
db.flush() # Get the ID for relationships
|
||||||
|
|
||||||
|
# If this box is part of a case, link it
|
||||||
|
if case_id:
|
||||||
|
case = db.query(SealedCase).filter(SealedCase.id == case_id).first()
|
||||||
|
if not case:
|
||||||
|
raise ValueError(f"Case {case_id} not found")
|
||||||
|
sealed_box.case_id = case_id
|
||||||
|
|
||||||
|
# Create the InventoryItem for the sealed box
|
||||||
|
inventory_item = InventoryItem(
|
||||||
|
physical_item=sealed_box,
|
||||||
|
cost_basis=cost_basis,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(inventory_item)
|
||||||
|
|
||||||
|
return sealed_box
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
class SealedCaseService(BaseService[SealedCase]):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(SealedCase)
|
||||||
|
|
||||||
|
async def create_sealed_case(self, db: Session, product_id: int, cost_basis: float, num_boxes: int) -> SealedCase:
|
||||||
|
try:
|
||||||
|
with db_transaction(db):
|
||||||
|
# Create the SealedCase
|
||||||
|
sealed_case = SealedCase(
|
||||||
|
product_id=product_id,
|
||||||
|
num_boxes=num_boxes,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(sealed_case)
|
||||||
|
db.flush() # Get the ID for relationships
|
||||||
|
|
||||||
|
# Create the InventoryItem for the sealed case
|
||||||
|
inventory_item = InventoryItem(
|
||||||
|
physical_item=sealed_case,
|
||||||
|
cost_basis=cost_basis,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(inventory_item)
|
||||||
|
|
||||||
|
return sealed_case
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
async def open_sealed_case(self, db: Session, sealed_case: SealedCase) -> bool:
|
||||||
|
try:
|
||||||
|
sealed_case_context = InventoryItemContextFactory(db).get_context(sealed_case.inventory_item)
|
||||||
|
with db_transaction(db):
|
||||||
|
# Create the OpenEvent
|
||||||
|
open_event = OpenEvent(
|
||||||
|
sealed_case_id=sealed_case_context.physical_item.id,
|
||||||
|
open_date=datetime.now(),
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(open_event)
|
||||||
|
db.flush() # Get the ID for relationships
|
||||||
|
|
||||||
|
# Create num_boxes SealedBoxes
|
||||||
|
for i in range(sealed_case.num_boxes):
|
||||||
|
sealed_box = SealedBox(
|
||||||
|
product_id=sealed_case_context.physical_item.product_id,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(sealed_box)
|
||||||
|
db.flush() # Get the ID for relationships
|
||||||
|
|
||||||
|
# Create the InventoryItem for the sealed box
|
||||||
|
inventory_item = InventoryItem(
|
||||||
|
physical_item=sealed_box,
|
||||||
|
cost_basis=sealed_case_context.cost_basis,
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.add(inventory_item)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
@ -2,23 +2,22 @@ from app.services.base_service import BaseService
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.db.database import transaction
|
from app.db.database import transaction
|
||||||
from app.schemas.file import FileInDB
|
from app.schemas.file import FileInDB
|
||||||
from app.models.mtgjson_card import MTGJSONCard
|
from app.models.tcgplayer_products import TCGPlayerProduct, MTGJSONCard, MTGJSONSKU
|
||||||
from app.models.mtgjson_sku import MTGJSONSKU
|
from app.models.critical_error_log import CriticalErrorLog
|
||||||
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
from app.models.manabox_import_staging import ManaboxImportStaging
|
||||||
from app.models.tcgplayer_product import TCGPlayerProduct
|
from typing import Dict, Any, Union, List
|
||||||
from app.models.tcgplayer_category import TCGPlayerCategory
|
|
||||||
from app.models.tcgplayer_group import TCGPlayerGroup
|
|
||||||
from typing import Dict, Any
|
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import asyncio
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class ManaboxService(BaseService):
|
class ManaboxService(BaseService):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(None)
|
super().__init__(None)
|
||||||
|
|
||||||
async def process_manabox_csv(self, db: Session, bytes: bytes) -> bool:
|
async def process_manabox_csv(self, db: Session, bytes: bytes, metadata: Dict[str, Any], wait: bool = False) -> Union[bool, List[FileInDB]]:
|
||||||
# save file
|
# save file
|
||||||
file = await self.file_service.save_file(
|
file = await self.file_service.save_file(
|
||||||
db=db,
|
db=db,
|
||||||
@ -26,77 +25,88 @@ class ManaboxService(BaseService):
|
|||||||
filename=f"manabox_{datetime.now().strftime('%Y%m%d%H%M%S')}.csv",
|
filename=f"manabox_{datetime.now().strftime('%Y%m%d%H%M%S')}.csv",
|
||||||
subdir="manabox",
|
subdir="manabox",
|
||||||
file_type="manabox",
|
file_type="manabox",
|
||||||
content_type="text/csv"
|
content_type="text/csv",
|
||||||
|
metadata=metadata
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create the background task
|
||||||
|
task = asyncio.create_task(self._process_file_background(db, file))
|
||||||
|
|
||||||
|
# If wait is True, wait for the task to complete and return the file
|
||||||
|
if wait:
|
||||||
|
await task
|
||||||
|
return_value = await self.file_service.get_file(db, file.id)
|
||||||
|
return [return_value] if return_value else []
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def _process_file_background(self, db: Session, file: FileInDB):
|
||||||
|
try:
|
||||||
# Read the CSV file
|
# Read the CSV file
|
||||||
with open(file.path, 'r') as file:
|
with open(file.path, 'r') as csv_file:
|
||||||
reader = csv.DictReader(file)
|
reader = csv.DictReader(csv_file)
|
||||||
# validate headers
|
|
||||||
if reader.fieldnames != ['Name', 'Set code', 'Set name', 'Collector number', 'Foil', 'Rarity', 'Quantity', 'ManaBox ID', 'Scryfall ID', 'Purchase price', 'Misprint', 'Altered', 'Condition', 'Language', 'Purchase price currency']:
|
|
||||||
logger.error("Invalid headers")
|
|
||||||
return False
|
|
||||||
# skip header row
|
# skip header row
|
||||||
next(reader)
|
next(reader)
|
||||||
for row in reader:
|
for row in reader:
|
||||||
# match scryfall id to mtgjson scryfall id, make sure only one distinct tcgplayer id
|
# match scryfall id to mtgjson scryfall id, make sure only one distinct tcgplayer id
|
||||||
mtg_json = db.query(MTGJSONCard).filter(MTGJSONCard.scryfall_id == row['Scryfall ID']).all()
|
mtg_json = db.query(MTGJSONCard).filter(MTGJSONCard.scryfall_id == row['Scryfall ID']).all()
|
||||||
# count distinct tcgplayer ids
|
# count distinct tcgplayer ids
|
||||||
cd_tcgplayer_ids = db.query(MTGJSONCard.tcgplayer_product_id).filter(MTGJSONCard.scryfall_id == row['Scryfall ID']).distinct().count()
|
cd_tcgplayer_ids = db.query(MTGJSONCard.tcgplayer_sku_id).filter(MTGJSONCard.scryfall_id == row['Scryfall ID']).distinct().count()
|
||||||
if cd_tcgplayer_ids != 1:
|
if cd_tcgplayer_ids != 1:
|
||||||
logger.error(f"Error: {cd_tcgplayer_ids} TCGplayer IDs found for {row['Scryfall ID']}")
|
logger.error(f"Error: multiple TCGplayer IDs found for scryfall id: {row['Scryfall ID']} found {cd_tcgplayer_ids} ids expected 1")
|
||||||
return False
|
with transaction(db):
|
||||||
|
critical_error_log = CriticalErrorLog(
|
||||||
|
error_message=f"Error: multiple TCGplayer IDs found for scryfall id: {row['Scryfall ID']} found {cd_tcgplayer_ids} ids expected 1"
|
||||||
|
)
|
||||||
|
db.add(critical_error_log)
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
mtg_json = mtg_json[0]
|
mtg_json = mtg_json[0]
|
||||||
# get tcgplayer sku id from mtgjson skus
|
# get tcgplayer sku id from mtgjson skus
|
||||||
language = 'ENGLISH' if row['Language'] == 'en' else 'JAPANESE' if row['Language'] == 'ja' else None
|
language = 'ENGLISH' if row['Language'] == 'en' else 'JAPANESE' if row['Language'] == 'ja' else None
|
||||||
if row['Foil'].lower() == 'etched':
|
if row['Foil'].lower() == 'etched':
|
||||||
printing = 'FOIL'
|
printing = 'FOIL'
|
||||||
tcgplayer_sku = db.query(MTGJSONSKU.sku_id).filter(MTGJSONSKU.product_id == mtg_json.tcgplayer_etched_product_id).filter(MTGJSONSKU.condition == row['Condition'].replace('_', ' ').upper()).filter(MTGJSONSKU.printing == printing).filter(MTGJSONSKU.language == language).all().distinct()
|
tcgplayer_sku = db.query(MTGJSONSKU).filter(MTGJSONSKU.tcgplayer_sku_id == mtg_json.tcgplayer_etched_sku_id).filter(MTGJSONSKU.condition == row['Condition'].replace('_', ' ').upper()).filter(MTGJSONSKU.printing == printing).filter(MTGJSONSKU.language == language).distinct().all()
|
||||||
else:
|
else:
|
||||||
printing = 'FOIL' if row['Foil'].lower() == 'foil' else 'NON FOIL'
|
printing = 'FOIL' if row['Foil'].lower() == 'foil' else 'NON FOIL'
|
||||||
tcgplayer_sku = db.query(MTGJSONSKU.sku_id).filter(MTGJSONSKU.product_id == mtg_json.tcgplayer_product_id).filter(MTGJSONSKU.condition == row['Condition'].replace('_', ' ').upper()).filter(MTGJSONSKU.printing == printing).filter(MTGJSONSKU.language == language).all().distinct()
|
tcgplayer_sku = db.query(MTGJSONSKU).filter(MTGJSONSKU.tcgplayer_sku_id == mtg_json.tcgplayer_sku_id).filter(MTGJSONSKU.condition == row['Condition'].replace('_', ' ').upper()).filter(MTGJSONSKU.printing == printing).filter(MTGJSONSKU.language == language).distinct().all()
|
||||||
# count distinct tcgplayer skus
|
# count distinct tcgplayer skus
|
||||||
if len(tcgplayer_sku) == 0:
|
if len(tcgplayer_sku) == 0:
|
||||||
logger.error(f"Error: No TCGplayer SKU found for {mtg_json.name} {row['Condition']}")
|
logger.error(f"Error: No TCGplayer SKU found for mtgjson name: {mtg_json.name} condition: {row['Condition']} language: {language} printing: {printing}")
|
||||||
logger.debug(row)
|
with transaction(db):
|
||||||
logger.debug(language)
|
critical_error_log = CriticalErrorLog(
|
||||||
logger.debug(row['Condition'].replace('_', ' ').upper())
|
error_message=f"Error: No TCGplayer SKU found for mtgjson name: {mtg_json.name} condition: {row['Condition']} language: {language} printing: {printing}"
|
||||||
logger.debug(mtg_json.tcgplayer_product_id)
|
)
|
||||||
logger.debug(printing)
|
db.add(critical_error_log)
|
||||||
return False
|
continue
|
||||||
elif len(tcgplayer_sku) > 1:
|
elif len(tcgplayer_sku) > 1:
|
||||||
logger.error(f"Error: {len(tcgplayer_sku)} TCGplayer SKUs found for {mtg_json.name} {row['Condition']}")
|
logger.error(f"Error: {len(tcgplayer_sku)} TCGplayer SKUs found for mtgjson name: {mtg_json.name} condition: {row['Condition']} language: {language} printing: {printing}")
|
||||||
return False
|
with transaction(db):
|
||||||
|
critical_error_log = CriticalErrorLog(
|
||||||
|
error_message=f"Error: {len(tcgplayer_sku)} TCGplayer SKUs found for mtgjson name: {mtg_json.name} condition: {row['Condition']} language: {language} printing: {printing}"
|
||||||
|
)
|
||||||
|
db.add(critical_error_log)
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
tcgplayer_sku = tcgplayer_sku[0]
|
tcgplayer_sku = tcgplayer_sku[0]
|
||||||
# look up tcgplayer product data for sku
|
# look up tcgplayer product data for sku
|
||||||
tcgplayer_product = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.product_id == tcgplayer_sku.product_id).first()
|
tcgplayer_product = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.tcgplayer_product_id == tcgplayer_sku.tcgplayer_product_id).filter(TCGPlayerProduct.condition == row['Condition'].replace('_', ' ').upper()).filter(TCGPlayerProduct.language == language).filter(TCGPlayerProduct.printing == printing).first()
|
||||||
# temp just dump into tcgplayer inventory
|
|
||||||
condition = f'{tcgplayer_sku.condition.title()} Foil' if 'Foil' in tcgplayer_product.sub_type_name else f'{tcgplayer_sku.condition.title()}'
|
quantity = int(row['Quantity'])
|
||||||
# join tcgplaeyer product on tcgplayer category on category_id and get name
|
|
||||||
product_line = db.query(TCGPlayerCategory).filter(TCGPlayerCategory.category_id == tcgplayer_product.category_id).first().name
|
|
||||||
# join tcgplaeyer product on tcgplayer group on group_id and get name
|
|
||||||
set_name = db.query(TCGPlayerGroup).filter(TCGPlayerGroup.group_id == tcgplayer_product.group_id).first().name
|
|
||||||
with transaction(db):
|
with transaction(db):
|
||||||
tcgplayer_inventory = TCGPlayerInventory(
|
manabox_import_staging = ManaboxImportStaging(
|
||||||
tcgplayer_id=tcgplayer_sku.sku_id,
|
file_id=file.id,
|
||||||
product_line=product_line,
|
product_id=tcgplayer_product.id,
|
||||||
set_name=set_name,
|
quantity=quantity,
|
||||||
product_name=tcgplayer_product.name,
|
created_at=datetime.now(),
|
||||||
title=None,
|
updated_at=datetime.now()
|
||||||
number=tcgplayer_product.ext_number,
|
|
||||||
rarity=tcgplayer_product.ext_rarity,
|
|
||||||
condition=condition,
|
|
||||||
tcg_market_price=tcgplayer_product.market_price,
|
|
||||||
tcg_direct_low=tcgplayer_product.direct_low_price,
|
|
||||||
tcg_low_price_with_shipping=tcgplayer_product.low_price,
|
|
||||||
tcg_low_price=tcgplayer_product.low_price,
|
|
||||||
total_quantity=row['Quantity'],
|
|
||||||
add_to_quantity=row['Quantity']
|
|
||||||
)
|
)
|
||||||
db.add(tcgplayer_inventory)
|
db.add(manabox_import_staging)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing file: {str(e)}")
|
||||||
return True
|
with transaction(db):
|
||||||
|
critical_error_log = CriticalErrorLog(
|
||||||
# Name,Set code,Set name,Collector number,Foil,Rarity,Quantity,ManaBox ID,Scryfall ID,Purchase price,Misprint,Altered,Condition,Language,Purchase price currency
|
error_message=f"Error processing file: {str(e)}"
|
||||||
|
)
|
||||||
|
db.add(critical_error_log)
|
@ -29,7 +29,11 @@ class ServiceManager:
|
|||||||
'file': 'app.services.file_service.FileService',
|
'file': 'app.services.file_service.FileService',
|
||||||
'tcgcsv': 'app.services.external_api.tcgcsv.tcgcsv_service.TCGCSVService',
|
'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'
|
'manabox': 'app.services.manabox_service.ManaboxService',
|
||||||
|
'inventory': 'app.services.inventory_service.InventoryService',
|
||||||
|
'sealed_box': 'app.services.inventory_service.SealedBoxService',
|
||||||
|
'sealed_case': 'app.services.inventory_service.SealedCaseService'
|
||||||
|
|
||||||
}
|
}
|
||||||
self._service_configs = {
|
self._service_configs = {
|
||||||
'label_printer': {'printer_api_url': "http://192.168.1.110:8000"},
|
'label_printer': {'printer_api_url': "http://192.168.1.110:8000"},
|
||||||
|
@ -10,7 +10,7 @@ import aiohttp
|
|||||||
import jinja2
|
import jinja2
|
||||||
from weasyprint import HTML
|
from weasyprint import HTML
|
||||||
from app.services.base_service import BaseService
|
from app.services.base_service import BaseService
|
||||||
from app.models.tcgplayer_group import TCGPlayerGroup
|
from app.models.tcgplayer_products import TCGPlayerProduct
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user