update pricing i guess

This commit is contained in:
2025-06-09 12:28:15 -04:00
parent 77d6fd6e29
commit 82fd1cb2da
6 changed files with 268 additions and 61 deletions

View File

@ -59,11 +59,11 @@ 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], use_cache=False, init_categories=True, init_products=True, init_groups=True, init_archived_prices=True, init_mtgjson=False, archived_prices_start_date="2025-05-22", archived_prices_end_date="2025-05-23") #data_init = await data_init_service.initialize_data(db, game_ids=[1], use_cache=False, init_categories=True, init_products=True, init_groups=True, init_archived_prices=True, init_mtgjson=False, archived_prices_start_date="2025-06-07", archived_prices_end_date="2025-06-08")
#logger.info(f"Data initialization results: {data_init}") #logger.info(f"Data initialization results: {data_init}")
# Update most recent prices # Update most recent prices
#MostRecentTCGPlayerPrice.update_most_recent_prices(db) #MostRecentTCGPlayerPrice.update_most_recent_prices(db)
logger.info("Most recent prices updated successfully") #logger.info("Most recent prices updated successfully")
# Create default customer, vendor, and marketplace # Create default customer, vendor, and marketplace
#inv_data_init = await data_init_service.initialize_inventory_data(db) #inv_data_init = await data_init_service.initialize_inventory_data(db)

View File

@ -486,3 +486,19 @@ async def confirm_listings(
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
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("/tcgplayer/update-file")
async def get_tcgplayer_update_file(
db: Session = Depends(get_db)
):
inventory_service = service_manager.get_service("inventory")
marketplace_listing_service = service_manager.get_service("marketplace_listing")
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
csv_string = await marketplace_listing_service.create_tcgplayer_update_file(db, marketplace)
return StreamingResponse(
iter([csv_string]),
media_type="text/csv",
headers={
"Content-Disposition": f"attachment; filename=tcgplayer_update_file_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv"
}
)

View File

@ -504,3 +504,62 @@ class MarketplaceListingService(BaseService[MarketplaceListing]):
data_rows = [",".join([escape_csv_value(data[tcgplayer_id][header]) for header in headers]) for tcgplayer_id in data] data_rows = [",".join([escape_csv_value(data[tcgplayer_id][header]) for header in headers]) for tcgplayer_id in data]
csv_data = "\n".join([header_row] + data_rows) csv_data = "\n".join([header_row] + data_rows)
return csv_data return csv_data
async def create_tcgplayer_update_file(self, db: Session, marketplace: Marketplace=None) -> str:
# TCGplayer Id,Product Line,Set Name,Product Name,Title,Number,Rarity,Condition,TCG Market Price,TCG Direct Low,TCG Low Price With Shipping,TCG Low Price,Total Quantity,Add to Quantity,TCG Marketplace Price,Photo URL
headers = [
"TCGplayer Id",
"Product Line",
"Set Name",
"Product Name",
"Title",
"Number",
"Rarity",
"Condition",
"TCG Market Price",
"TCG Direct Low",
"TCG Low Price With Shipping",
"TCG Low Price",
"Total Quantity",
"Add to Quantity",
"TCG Marketplace Price",
"Photo URL"
]
unmanaged_inventory = await self.pricing_service.update_prices_for_unmanaged_inventory(db)
managed_inventory = await self.pricing_service.update_prices_for_managed_inventory(db)
# combine and convert to csv
inventory = unmanaged_inventory + managed_inventory
data = {}
for inventory_item in inventory:
data[inventory_item.tcgplayer_sku_id] = {
"TCGplayer Id": inventory_item.tcgplayer_sku_id,
"Product Line": inventory_item.product_line,
"Set Name": inventory_item.set_name,
"Product Name": inventory_item.product_name,
"Title": inventory_item.title,
"Number": inventory_item.number,
"Rarity": inventory_item.rarity,
"Condition": inventory_item.condition,
"TCG Market Price": inventory_item.tcg_market_price,
"TCG Direct Low": inventory_item.tcg_direct_low,
"TCG Low Price With Shipping": inventory_item.tcg_low_price_with_shipping,
"TCG Low Price": inventory_item.tcg_low_price,
"Total Quantity": "",
"Add to Quantity": "0",
"TCG Marketplace Price": f"{Decimal(inventory_item.tcg_marketplace_price):.2f}",
"Photo URL": ""
}
# format data into csv
# header
header_row = ",".join(headers)
# data
def escape_csv_value(value):
if value is None:
return ""
value = str(value)
if any(c in value for c in [',', '"', '\n']):
return f'"{value.replace('"', '""')}"'
return value
data_rows = [",".join([escape_csv_value(data[tcgplayer_id][header]) for header in headers]) for tcgplayer_id in data]
csv_data = "\n".join([header_row] + data_rows)
return csv_data

View File

@ -1,7 +1,8 @@
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional, Dict, List
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import select
from app.services.base_service import BaseService from app.services.base_service import BaseService
from app.models.inventory_management import InventoryItem, MarketplaceListing, PhysicalItem from app.models.inventory_management import InventoryItem, MarketplaceListing, PhysicalItem
from app.models.tcgplayer_inventory import TCGPlayerInventory from app.models.tcgplayer_inventory import TCGPlayerInventory
@ -33,67 +34,136 @@ class PricingService(BaseService):
super().__init__(None) super().__init__(None)
async def get_unmanaged_inventory(self, db: Session): async def get_unmanaged_inventory(self, db: Session):
#select * from tcgplayer_inventory ti where ti.tcgplayer_sku_id not in (select pi2.tcgplayer_sku_id from marketplace_listings ml
# join inventory_items ii on ml.inventory_item_id = ii.id
# join physical_items pi2 on ii.physical_item_id = pi2.id
# where ml.delisting_date is null and ml.deleted_at is null and ii.deleted_at is null and pi2.deleted_at is null);
unmanaged_inventory = db.query(TCGPlayerInventory).filter( unmanaged_inventory = db.query(TCGPlayerInventory).filter(
TCGPlayerInventory.tcgplayer_sku_id.notin_( TCGPlayerInventory.tcgplayer_sku_id.notin_(
db.query(MarketplaceListing.inventory_item.physical_item.tcgplayer_sku_id).join( db.query(PhysicalItem.tcgplayer_sku_id).join(
InventoryItem, MarketplaceListing.inventory_item_id == InventoryItem.id InventoryItem, PhysicalItem.id == InventoryItem.physical_item_id
).join( ).join(
PhysicalItem, InventoryItem.physical_item_id == PhysicalItem.id MarketplaceListing, InventoryItem.id == MarketplaceListing.inventory_item_id
).filter( ).filter(
MarketplaceListing.delisting_date.is_(None), MarketplaceListing.delisting_date.is_(None),
MarketplaceListing.deleted_at.is_(None), MarketplaceListing.deleted_at.is_(None),
InventoryItem.deleted_at.is_(None), InventoryItem.deleted_at.is_(None),
PhysicalItem.deleted_at.is_(None) PhysicalItem.deleted_at.is_(None)
) )
) ),
TCGPlayerInventory.total_quantity >= 1
).all() ).all()
return unmanaged_inventory return unmanaged_inventory
async def update_prices_for_unmanaged_inventory(self, db: Session): async def get_managed_inventory(self, db: Session):
unmanaged_inventory = await self.get_unmanaged_inventory(db) # First get the TCGPlayerInventory IDs that are managed
for inventory in unmanaged_inventory: managed_ids = select(TCGPlayerInventory.id).join(
inventory.tcg_marketplace_price = await self.set_price_for_unmanaged_inventory(db, inventory) PhysicalItem, TCGPlayerInventory.tcgplayer_sku_id == PhysicalItem.tcgplayer_sku_id
).join(
InventoryItem, PhysicalItem.id == InventoryItem.physical_item_id
).join(
MarketplaceListing, InventoryItem.id == MarketplaceListing.inventory_item_id
).filter(
MarketplaceListing.delisting_date.is_(None),
MarketplaceListing.deleted_at.is_(None),
InventoryItem.deleted_at.is_(None),
PhysicalItem.deleted_at.is_(None)
)
async def set_price_for_unmanaged_inventory(self, db: Session, inventory: TCGPlayerInventory): # Then get just the TCGPlayerInventory data for those IDs
# available columns managed_inventory = db.query(TCGPlayerInventory).filter(
# id, tcgplayer_sku_id, product_line, set_name, product_name, title, TCGPlayerInventory.id.in_(managed_ids)
# number, rarity, condition, tcg_market_price, tcg_direct_low, ).all()
# tcg_low_price_with_shipping, tcg_low_price, total_quantity, add_to_quantity, return managed_inventory
# tcg_marketplace_price, photo_url, created_at, updated_at
# get mtgjson sku async def get_pricing_data_for_unmanaged_inventory(self, db: Session) -> Dict[int, PriceData]:
mtgjson_sku = db.query(MTGJSONSKU).filter( """Gather all pricing data for unmanaged inventory in a single query."""
MTGJSONSKU.tcgplayer_sku_id == inventory.tcgplayer_sku_id unmanaged_inventory = await self.get_unmanaged_inventory(db)
).first()
if mtgjson_sku: # Get all SKU IDs
tcgplayer_product = mtgjson_sku.product sku_ids = [inv.tcgplayer_sku_id for inv in unmanaged_inventory]
# Fetch all MTGJSON SKUs and their products in one query
mtgjson_skus = db.query(MTGJSONSKU).filter(
MTGJSONSKU.tcgplayer_sku_id.in_(sku_ids)
).all()
# Create a mapping of SKU ID to MTGJSON SKU
sku_map = {sku.tcgplayer_sku_id: sku for sku in mtgjson_skus}
# Create price data for each inventory item
price_data_map = {}
for inventory in unmanaged_inventory:
mtgjson_sku = sku_map.get(inventory.tcgplayer_sku_id)
if mtgjson_sku and mtgjson_sku.product and mtgjson_sku.product.most_recent_tcgplayer_price:
recent_price = mtgjson_sku.product.most_recent_tcgplayer_price
price_data = PriceData( price_data = PriceData(
cost_basis=None, cost_basis=None,
market_price=Decimal(str(tcgplayer_product.most_recent_tcgplayer_price.market_price)) if tcgplayer_product.most_recent_tcgplayer_price else None, market_price=Decimal(str(recent_price.market_price)) if recent_price.market_price else None,
tcg_low=Decimal(str(tcgplayer_product.most_recent_tcgplayer_price.low_price)) if tcgplayer_product.most_recent_tcgplayer_price else None, tcg_low=Decimal(str(recent_price.low_price)) if recent_price.low_price else None,
tcg_mid=Decimal(str(tcgplayer_product.most_recent_tcgplayer_price.mid_price)) if tcgplayer_product.most_recent_tcgplayer_price else None, tcg_mid=Decimal(str(recent_price.mid_price)) if recent_price.mid_price else None,
direct_low=Decimal(str(tcgplayer_product.most_recent_tcgplayer_price.direct_low_price)) if tcgplayer_product.most_recent_tcgplayer_price else None, direct_low=Decimal(str(recent_price.direct_low_price)) if recent_price.direct_low_price else None,
listed_price=inventory.tcg_marketplace_price, listed_price=Decimal(str(inventory.tcg_marketplace_price)) if inventory.tcg_marketplace_price else None,
quantity=inventory.total_quantity, quantity=inventory.total_quantity,
lowest_price_for_qty=None, lowest_price_for_qty=None,
velocity=None, velocity=None,
age_of_inventory=None age_of_inventory=None
) )
return await self.set_price(db, price_data) price_data_map[inventory.tcgplayer_sku_id] = price_data
else:
return None return price_data_map
async def update_prices_for_unmanaged_inventory(self, db: Session):
# Get all pricing data upfront
price_data_map = await self.get_pricing_data_for_unmanaged_inventory(db)
# Update prices using the pre-fetched data
unmanaged_inventory = await self.get_unmanaged_inventory(db)
for inventory in unmanaged_inventory:
price_data = price_data_map.get(inventory.tcgplayer_sku_id)
if price_data:
inventory.tcg_marketplace_price = await self.set_price(db, price_data)
return unmanaged_inventory
async def update_prices_for_managed_inventory(self, db: Session):
"""Update prices for managed inventory items and return updated TCGPlayerInventory data."""
managed_inventory = await self.get_managed_inventory(db)
# Get all the inventory items we need in one query
inventory_items = db.query(InventoryItem).join(
PhysicalItem, InventoryItem.physical_item_id == PhysicalItem.id
).filter(
PhysicalItem.tcgplayer_sku_id.in_([inv.tcgplayer_sku_id for inv in managed_inventory]),
InventoryItem.deleted_at.is_(None)
).all()
# Create a map of sku_id to inventory_item for easy lookup
inventory_map = {item.physical_item.tcgplayer_sku_id: item for item in inventory_items}
for tcg_inventory in managed_inventory:
inventory_item = inventory_map.get(tcg_inventory.tcgplayer_sku_id)
if inventory_item:
pricing_event = await self.set_price_for_inventory_item(db, inventory_item)
if pricing_event:
tcg_inventory.tcg_marketplace_price = pricing_event.price
return managed_inventory
async def set_price_for_inventory_item(self, db: Session, inventory_item: InventoryItem): async def set_price_for_inventory_item(self, db: Session, inventory_item: InventoryItem):
recent_price = inventory_item.physical_item.sku.product.most_recent_tcgplayer_price
# Get the most recent active marketplace listing
active_listing = None
if inventory_item.marketplace_listing:
active_listings = [listing for listing in inventory_item.marketplace_listing
if listing.delisting_date is None and listing.deleted_at is None]
if active_listings:
active_listing = active_listings[0] # Get the first active listing
price_data = PriceData( price_data = PriceData(
cost_basis=Decimal(str(inventory_item.cost_basis)), cost_basis=Decimal(str(inventory_item.cost_basis)) if inventory_item.cost_basis is not None else None,
market_price=Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.market_price)), market_price=Decimal(str(recent_price.market_price)) if recent_price and recent_price.market_price is not None else None,
tcg_low=Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.low_price)), tcg_low=Decimal(str(recent_price.low_price)) if recent_price and recent_price.low_price is not None else None,
tcg_mid=Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.mid_price)), tcg_mid=Decimal(str(recent_price.mid_price)) if recent_price and recent_price.mid_price is not None else None,
direct_low=Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.direct_low_price)), direct_low=Decimal(str(recent_price.direct_low_price)) if recent_price and recent_price.direct_low_price is not None else None,
listed_price=Decimal(str(inventory_item.marketplace_listing.listed_price)), listed_price=Decimal(str(active_listing.listed_price.price)) if active_listing and active_listing.listed_price and active_listing.listed_price.price is not None else None,
quantity=db.query(TCGPlayerInventory).filter( quantity=db.query(TCGPlayerInventory).filter(
TCGPlayerInventory.tcgplayer_sku_id == inventory_item.physical_item.tcgplayer_sku_id TCGPlayerInventory.tcgplayer_sku_id == inventory_item.physical_item.tcgplayer_sku_id
).first().total_quantity if db.query(TCGPlayerInventory).filter( ).first().total_quantity if db.query(TCGPlayerInventory).filter(
@ -118,6 +188,15 @@ class PricingService(BaseService):
tcg_mid = price_data.tcg_mid tcg_mid = price_data.tcg_mid
listed_price = price_data.listed_price listed_price = price_data.listed_price
if inventory_item:
# average cost basis for all inventory items with the same tcgplayer_sku_id
average_cost_basis = db.query(InventoryItem.cost_basis).filter(
InventoryItem.physical_item_id == inventory_item.physical_item_id
).all()
cost_basis_values = [row[0] for row in average_cost_basis if row[0] is not None]
if cost_basis_values:
cost_basis = Decimal(str(sum(cost_basis_values) / len(cost_basis_values)))
logger.info(f"listed_price: {listed_price}") logger.info(f"listed_price: {listed_price}")
logger.info(f"market_price: {market_price}") logger.info(f"market_price: {market_price}")
logger.info(f"tcg_low: {tcg_low}") logger.info(f"tcg_low: {tcg_low}")
@ -133,11 +212,11 @@ class PricingService(BaseService):
tcgplayer_shipping_fee = Decimal('1.31') tcgplayer_shipping_fee = Decimal('1.31')
average_cards_per_order = Decimal('3.0') average_cards_per_order = Decimal('3.0')
marketplace_fee_percentage = Decimal('0.20') marketplace_fee_percentage = Decimal('0.20')
target_margin = Decimal('0.20') target_margin = Decimal('0.10')
velocity_multiplier = Decimal('0.0') velocity_multiplier = Decimal('0.0')
global_margin_multiplier = Decimal('0.00') global_margin_multiplier = Decimal('0.00')
min_floor_price = Decimal('0.25') min_floor_price = Decimal('0.25')
price_drop_threshold = Decimal('0.20') price_drop_threshold = Decimal('0.50')
# TODO add age of inventory price decrease multiplier # TODO add age of inventory price decrease multiplier
age_of_inventory_multiplier = Decimal('0.0') age_of_inventory_multiplier = Decimal('0.0')
@ -154,6 +233,8 @@ class PricingService(BaseService):
card_cost_margin_multiplier = Decimal('0.033') card_cost_margin_multiplier = Decimal('0.033')
elif market_price >= 100 and market_price < 200: elif market_price >= 100 and market_price < 200:
card_cost_margin_multiplier = Decimal('0.05') card_cost_margin_multiplier = Decimal('0.05')
else:
card_cost_margin_multiplier = Decimal('0.0')
# Fetch current total quantity in stock for SKU # Fetch current total quantity in stock for SKU
quantity_in_stock = price_data.quantity quantity_in_stock = price_data.quantity
@ -177,7 +258,7 @@ class PricingService(BaseService):
shipping_cost_offset = min(shipping_cost / average_cards_per_order, market_price * Decimal('0.1')) shipping_cost_offset = min(shipping_cost / average_cards_per_order, market_price * Decimal('0.1'))
if cost_basis is None: if cost_basis is None:
cost_basis = tcg_low * Decimal('0.65') cost_basis = tcg_mid * Decimal('0.65')
# Calculate base price considering cost, shipping, fees, and margin targets # Calculate base price considering cost, shipping, fees, and margin targets
base_price = (cost_basis + shipping_cost_offset) / ( base_price = (cost_basis + shipping_cost_offset) / (
(Decimal('1.0') - marketplace_fee_percentage) - adjusted_target_margin (Decimal('1.0') - marketplace_fee_percentage) - adjusted_target_margin
@ -189,19 +270,19 @@ class PricingService(BaseService):
base_price + tcgplayer_shipping_fee base_price + tcgplayer_shipping_fee
) )
# Enforce minimum floor price to ensure profitability
if adjusted_price < min_floor_price:
adjusted_price = min_floor_price
# Adjust price based on market prices (TCG low and TCG mid) # Adjust price based on market prices (TCG low and TCG mid)
if adjusted_price < tcg_low: if adjusted_price < tcg_low:
adjusted_price = tcg_mid adjusted_price = tcg_mid
price_used = "tcg mid" price_used = "tcg mid"
price_reason = "adjusted price below tcg low" price_reason = "adjusted price below tcg low"
elif adjusted_price > tcg_low and adjusted_price < (tcg_mid * Decimal('0.8')): elif adjusted_price > tcg_low and adjusted_price < (tcg_mid * Decimal('0.85')):
adjusted_price = tcg_mid adjusted_price = tcg_mid
price_used = "tcg mid" price_used = "tcg mid"
price_reason = f"adjusted price below 80% of tcg mid" price_reason = f"adjusted price below 80% of tcg mid"
elif adjusted_price > (tcg_mid * Decimal('1.1')):
adjusted_price = max(tcg_mid, cost_basis)
price_used = "max tcg mid/cost basis"
price_reason = f"adjusted price above 110% of tcg mid, using max of tcg mid and cost basis"
else: else:
price_used = "adjusted price" price_used = "adjusted price"
price_reason = "valid price assigned based on margin targets" price_reason = "valid price assigned based on margin targets"
@ -228,6 +309,12 @@ class PricingService(BaseService):
price_used = "listed price" price_used = "listed price"
price_reason = "adjusted price below price drop threshold" price_reason = "adjusted price below price drop threshold"
# Enforce minimum floor price
if adjusted_price < min_floor_price:
adjusted_price = min_floor_price
price_used = "min floor price"
price_reason = "adjusted price below min floor price"
# Record pricing event in database transaction # Record pricing event in database transaction
if inventory_item: if inventory_item:
with transaction(db): with transaction(db):
@ -241,8 +328,12 @@ class PricingService(BaseService):
db.add(pricing_event) db.add(pricing_event)
# delete previous pricing events for inventory item # delete previous pricing events for inventory item
if inventory_item.marketplace_listing and inventory_item.marketplace_listing.listed_price: if inventory_item.marketplace_listing:
inventory_item.marketplace_listing.listed_price.deleted_at = datetime.now() for listing in inventory_item.marketplace_listing:
if listing.listed_price:
listing.listed_price.deleted_at = datetime.now()
db.flush()
listing.listed_price = pricing_event
return pricing_event return pricing_event
else: else:

View File

@ -28,9 +28,14 @@
<!-- Create Transaction Button --> <!-- Create Transaction Button -->
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8"> <div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
<div class="flex space-x-4">
<button id="createTransactionBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"> <button id="createTransactionBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
Create New Transaction Create New Transaction
</button> </button>
<button id="downloadTcgplayerUpdateBtn" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors">
Download TCGPlayer Update File
</button>
</div>
</div> </div>
<!-- Transaction List --> <!-- Transaction List -->

View File

@ -4,6 +4,8 @@ document.addEventListener('DOMContentLoaded', function() {
showTransactionModal(); showTransactionModal();
}); });
document.getElementById('downloadTcgplayerUpdateBtn').addEventListener('click', downloadTcgplayerUpdateFile);
document.getElementById('addVendorBtn').addEventListener('click', () => { document.getElementById('addVendorBtn').addEventListener('click', () => {
const vendorName = prompt('Enter vendor name:'); const vendorName = prompt('Enter vendor name:');
if (vendorName) { if (vendorName) {
@ -1075,3 +1077,37 @@ async function saveTransaction() {
alert('Failed to save transaction. Please try again.'); alert('Failed to save transaction. Please try again.');
} }
} }
// TCGPlayer Update File Functions
async function downloadTcgplayerUpdateFile() {
try {
const response = await fetch('/api/inventory/tcgplayer/update-file');
if (!response.ok) {
throw new Error('Failed to download TCGPlayer update file');
}
// Get the filename from the Content-Disposition header
const contentDisposition = response.headers.get('Content-Disposition');
let filename = 'tcgplayer_update_file.csv';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename=(.+)/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
// Create a blob from the response and download it
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Error downloading TCGPlayer update file:', error);
alert('Failed to download TCGPlayer update file. Please try again.');
}
}