Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
82fd1cb2da | |||
77d6fd6e29 | |||
7bc64115f2 | |||
fa089adb53 |
@@ -90,6 +90,12 @@ tr:hover {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.color-identity {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
width: 200px;
|
||||
}
|
||||
@@ -119,6 +125,7 @@ tbody tr:hover {
|
||||
<th class="set">Set</th>
|
||||
<th class="rarity">Rarity</th>
|
||||
<th class="card-number">Card #</th>
|
||||
<th class="color-identity">Colors</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -129,6 +136,7 @@ tbody tr:hover {
|
||||
<td class="set">{{ item.set }}</td>
|
||||
<td class="rarity">{{ item.rarity }}</td>
|
||||
<td class="card-number">{{ item.card_number }}</td>
|
||||
<td class="color-identity">{{ item.color_identity }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@@ -59,11 +59,11 @@ async def lifespan(app: FastAPI):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
#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}")
|
||||
# Update most recent prices
|
||||
#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
|
||||
#inv_data_init = await data_init_service.initialize_inventory_data(db)
|
||||
|
@@ -485,4 +485,20 @@ async def confirm_listings(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(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"
|
||||
}
|
||||
)
|
@@ -32,7 +32,8 @@ router = APIRouter(prefix="/orders")
|
||||
@router.get("/", response_model=List[TCGPlayerAPIOrderSummary])
|
||||
async def get_orders(
|
||||
search_range: SearchRange = SearchRange.LAST_THREE_MONTHS,
|
||||
open_only: bool = False
|
||||
open_only: bool = False,
|
||||
db: Session = Depends(get_db)
|
||||
) -> List[TCGPlayerAPIOrderSummary]:
|
||||
"""
|
||||
Retrieve orders from TCGPlayer based on search criteria.
|
||||
@@ -47,6 +48,7 @@ async def get_orders(
|
||||
try:
|
||||
order_management = service_manager.get_service('order_management')
|
||||
orders = await order_management.get_orders(search_range, open_only)
|
||||
orders = await order_management.add_item_quantity(db, orders)
|
||||
return orders
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to fetch orders: {str(e)}")
|
||||
|
@@ -76,6 +76,7 @@ class TCGPlayerAPIOrderSummary(BaseModel):
|
||||
orderStatus: str
|
||||
buyerName: str
|
||||
shippingType: str
|
||||
itemQuantity: int
|
||||
productAmount: float
|
||||
shippingAmount: float
|
||||
totalAmount: float
|
||||
|
@@ -35,5 +35,6 @@ __all__ = [
|
||||
'OrderManagementService',
|
||||
'TCGPlayerInventoryService',
|
||||
'PricingService',
|
||||
'MarketplaceListingService'
|
||||
'MarketplaceListingService',
|
||||
'ScryfallService'
|
||||
]
|
11
app/services/external_api/scryfall/scryfall_service.py
Normal file
11
app/services/external_api/scryfall/scryfall_service.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from app.services.external_api.base_external_service import BaseExternalService
|
||||
|
||||
class ScryfallService(BaseExternalService):
|
||||
def __init__(self):
|
||||
super().__init__(base_url="https://api.scryfall.com/")
|
||||
|
||||
async def get_color_identity(self, scryfall_id: str) -> str:
|
||||
"""Get the color identity of a card from Scryfall API"""
|
||||
endpoint = f"cards/{scryfall_id}"
|
||||
results = await self._make_request("GET", endpoint)
|
||||
return results['color_identity']
|
@@ -24,6 +24,7 @@ import csv
|
||||
import io
|
||||
from app.schemas.file import FileInDB
|
||||
from datetime import datetime
|
||||
from sqlalchemy import func
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OrderManagementService(BaseTCGPlayerService):
|
||||
@@ -40,7 +41,34 @@ class OrderManagementService(BaseTCGPlayerService):
|
||||
self.pull_sheet_endpoint = f"/pull-sheets/export{self.API_VERSION}"
|
||||
self.shipping_endpoint = f"/shipping/export{self.API_VERSION}"
|
||||
|
||||
|
||||
async def add_item_quantity(self, db: Session, orders: list[TCGPlayerAPIOrderSummary]) -> list[TCGPlayerAPIOrderSummary]:
|
||||
"""
|
||||
Add item quantity to orders using SQL aggregation for better performance
|
||||
"""
|
||||
# Get order numbers from the input orders
|
||||
order_numbers = [order["orderNumber"] for order in orders]
|
||||
|
||||
# Use SQL aggregation to get the sum of quantities directly from the database
|
||||
quantity_sums = (
|
||||
db.query(
|
||||
TCGPlayerOrderProduct.order_number,
|
||||
func.sum(TCGPlayerOrderProduct.quantity).label('total_quantity')
|
||||
)
|
||||
.filter(TCGPlayerOrderProduct.order_number.in_(order_numbers))
|
||||
.group_by(TCGPlayerOrderProduct.order_number)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Create a lookup dictionary for faster access
|
||||
quantity_lookup = {order_number: total_quantity for order_number, total_quantity in quantity_sums}
|
||||
|
||||
# Update orders with quantities
|
||||
for order in orders:
|
||||
order["itemQuantity"] = quantity_lookup.get(order["orderNumber"], 0)
|
||||
|
||||
return orders
|
||||
|
||||
|
||||
async def get_orders(self, search_range: str = "LastThreeMonths", open_only: bool = False, filter_out: list[str] = [], filter_in: list[str] = []) -> list[TCGPlayerAPIOrderSummary]:
|
||||
"""
|
||||
search range options:
|
||||
@@ -79,6 +107,9 @@ class OrderManagementService(BaseTCGPlayerService):
|
||||
orders = [order for order in orders if order.get("orderNumber") not in filter_out]
|
||||
if filter_in:
|
||||
orders = [order for order in orders if order.get("orderNumber") in filter_in]
|
||||
# add item quantity to orders as none
|
||||
for order in orders:
|
||||
order["itemQuantity"] = 0
|
||||
return orders
|
||||
|
||||
async def get_order_ids(self, search_range: str = "LastThreeMonths", open_only: bool = False, filter_out: list[str] = [], filter_in: list[str] = []):
|
||||
|
@@ -71,59 +71,4 @@ class TCGPlayerInventoryService(BaseTCGPlayerService):
|
||||
|
||||
with transaction(db):
|
||||
# Bulk insert new data
|
||||
db.bulk_insert_mappings(TCGPlayerInventory, inventory_data)
|
||||
|
||||
async def refresh_unmanaged_tcgplayer_inventory_table(self, db: Session):
|
||||
"""
|
||||
Refresh the TCGPlayer unmanaged inventory table
|
||||
unmanaged inventory is any inventory that cannot be mapped to a card with a marketplace listing
|
||||
"""
|
||||
with transaction(db):
|
||||
# Get active marketplace listings with their physical items in a single query
|
||||
listed_cards = (
|
||||
db.query(MarketplaceListing)
|
||||
.join(MarketplaceListing.inventory_item)
|
||||
.join(InventoryItem.physical_item)
|
||||
.filter(
|
||||
func.lower(Marketplace.name) == func.lower("tcgplayer"),
|
||||
MarketplaceListing.delisting_date == None,
|
||||
MarketplaceListing.deleted_at == None,
|
||||
MarketplaceListing.listing_date != None
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Get current inventory and create lookup dict
|
||||
current_inventory = db.query(TCGPlayerInventory).all()
|
||||
|
||||
# Create a set of SKUs that have active listings
|
||||
listed_skus = {
|
||||
card.inventory_item.physical_item.tcgplayer_sku_id
|
||||
for card in listed_cards
|
||||
}
|
||||
|
||||
unmanaged_inventory = []
|
||||
for inventory in current_inventory:
|
||||
# Only include SKUs that have no active listings
|
||||
if inventory.tcgplayer_sku_id not in listed_skus:
|
||||
unmanaged_inventory.append({
|
||||
"tcgplayer_inventory_id": inventory.id,
|
||||
"tcgplayer_sku_id": inventory.tcgplayer_sku_id,
|
||||
"product_line": inventory.product_line,
|
||||
"set_name": inventory.set_name,
|
||||
"product_name": inventory.product_name,
|
||||
"title": inventory.title,
|
||||
"number": inventory.number,
|
||||
"rarity": inventory.rarity,
|
||||
"condition": inventory.condition,
|
||||
"tcg_market_price": inventory.tcg_market_price,
|
||||
"tcg_direct_low": inventory.tcg_direct_low,
|
||||
"tcg_low_price_with_shipping": inventory.tcg_low_price_with_shipping,
|
||||
"tcg_low_price": inventory.tcg_low_price,
|
||||
"total_quantity": inventory.total_quantity,
|
||||
"add_to_quantity": inventory.add_to_quantity,
|
||||
"tcg_marketplace_price": inventory.tcg_marketplace_price,
|
||||
"photo_url": inventory.photo_url
|
||||
})
|
||||
|
||||
db.bulk_insert_mappings(UnmanagedTCGPlayerInventory, unmanaged_inventory)
|
||||
db.bulk_insert_mappings(TCGPlayerInventory, inventory_data)
|
@@ -375,7 +375,7 @@ class MarketplaceListingService(BaseService[MarketplaceListing]):
|
||||
async def create_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing:
|
||||
try:
|
||||
with db_transaction(db):
|
||||
recommended_price = await self.pricing_service.set_price(db, inventory_item)
|
||||
recommended_price = await self.pricing_service.set_price_for_inventory_item(db, inventory_item)
|
||||
logger.info(f"recommended_price: {recommended_price.price}")
|
||||
marketplace_listing = MarketplaceListing(
|
||||
inventory_item=inventory_item,
|
||||
@@ -392,9 +392,10 @@ class MarketplaceListingService(BaseService[MarketplaceListing]):
|
||||
raise e
|
||||
|
||||
async def update_marketplace_listing_price(self, db: Session, marketplace_listing: MarketplaceListing) -> MarketplaceListing:
|
||||
# idk if this was ever even finished so prob doesnt work idk
|
||||
try:
|
||||
with db_transaction(db):
|
||||
marketplace_listing.listed_price = self.pricing_service.set_price(marketplace_listing.inventory_item)
|
||||
marketplace_listing.listed_price = self.pricing_service.set_price_for_inventory_item(db, marketplace_listing.inventory_item)
|
||||
db.flush()
|
||||
return marketplace_listing
|
||||
|
||||
@@ -503,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]
|
||||
csv_data = "\n".join([header_row] + data_rows)
|
||||
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
|
@@ -1,8 +1,12 @@
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Dict, List
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
from app.services.base_service import BaseService
|
||||
from app.models.inventory_management import InventoryItem, MarketplaceListing
|
||||
from app.models.inventory_management import InventoryItem, MarketplaceListing, PhysicalItem
|
||||
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
||||
from app.models.tcgplayer_products import TCGPlayerPriceHistory, TCGPlayerProduct, MTGJSONSKU
|
||||
from app.models.pricing import PricingEvent
|
||||
from app.db.database import transaction
|
||||
from decimal import Decimal
|
||||
@@ -11,24 +15,187 @@ from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@dataclass
|
||||
class PriceData:
|
||||
cost_basis: Optional[Decimal]
|
||||
market_price: Optional[Decimal]
|
||||
tcg_low: Optional[Decimal]
|
||||
tcg_mid: Optional[Decimal]
|
||||
direct_low: Optional[Decimal]
|
||||
listed_price: Optional[Decimal]
|
||||
quantity: int
|
||||
lowest_price_for_qty: Optional[Decimal]
|
||||
velocity: Optional[Decimal]
|
||||
age_of_inventory: Optional[int]
|
||||
|
||||
|
||||
class PricingService(BaseService):
|
||||
def __init__(self):
|
||||
super().__init__(None)
|
||||
|
||||
async def get_unmanaged_inventory(self, db: Session):
|
||||
unmanaged_inventory = db.query(TCGPlayerInventory).filter(
|
||||
TCGPlayerInventory.tcgplayer_sku_id.notin_(
|
||||
db.query(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)
|
||||
)
|
||||
),
|
||||
TCGPlayerInventory.total_quantity >= 1
|
||||
).all()
|
||||
return unmanaged_inventory
|
||||
|
||||
async def get_managed_inventory(self, db: Session):
|
||||
# First get the TCGPlayerInventory IDs that are managed
|
||||
managed_ids = select(TCGPlayerInventory.id).join(
|
||||
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)
|
||||
)
|
||||
|
||||
# Then get just the TCGPlayerInventory data for those IDs
|
||||
managed_inventory = db.query(TCGPlayerInventory).filter(
|
||||
TCGPlayerInventory.id.in_(managed_ids)
|
||||
).all()
|
||||
return managed_inventory
|
||||
|
||||
async def get_pricing_data_for_unmanaged_inventory(self, db: Session) -> Dict[int, PriceData]:
|
||||
"""Gather all pricing data for unmanaged inventory in a single query."""
|
||||
unmanaged_inventory = await self.get_unmanaged_inventory(db)
|
||||
|
||||
# Get all SKU IDs
|
||||
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(
|
||||
cost_basis=None,
|
||||
market_price=Decimal(str(recent_price.market_price)) if recent_price.market_price else None,
|
||||
tcg_low=Decimal(str(recent_price.low_price)) if recent_price.low_price else None,
|
||||
tcg_mid=Decimal(str(recent_price.mid_price)) if recent_price.mid_price else None,
|
||||
direct_low=Decimal(str(recent_price.direct_low_price)) if recent_price.direct_low_price else None,
|
||||
listed_price=Decimal(str(inventory.tcg_marketplace_price)) if inventory.tcg_marketplace_price else None,
|
||||
quantity=inventory.total_quantity,
|
||||
lowest_price_for_qty=None,
|
||||
velocity=None,
|
||||
age_of_inventory=None
|
||||
)
|
||||
price_data_map[inventory.tcgplayer_sku_id] = price_data
|
||||
|
||||
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)
|
||||
|
||||
async def set_price(self, db: Session, inventory_item: InventoryItem) -> float:
|
||||
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):
|
||||
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(
|
||||
cost_basis=Decimal(str(inventory_item.cost_basis)) if inventory_item.cost_basis is not None else None,
|
||||
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(recent_price.low_price)) if recent_price and recent_price.low_price is not None else None,
|
||||
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(recent_price.direct_low_price)) if recent_price and recent_price.direct_low_price is not None else None,
|
||||
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(
|
||||
TCGPlayerInventory.tcgplayer_sku_id == inventory_item.physical_item.tcgplayer_sku_id
|
||||
).first().total_quantity if db.query(TCGPlayerInventory).filter(
|
||||
TCGPlayerInventory.tcgplayer_sku_id == inventory_item.physical_item.tcgplayer_sku_id
|
||||
).first() else 0,
|
||||
lowest_price_for_qty=None,
|
||||
velocity=None,
|
||||
age_of_inventory=None
|
||||
)
|
||||
return await self.set_price(db, price_data, inventory_item)
|
||||
|
||||
async def set_price(self, db: Session, price_data: PriceData, inventory_item: InventoryItem=None):
|
||||
"""
|
||||
TODO This sets listed_price per inventory_item but listed_price can only be applied to a product
|
||||
however, this may be desired on other marketplaces
|
||||
when generating pricing file for tcgplayer, give the option to set min, max, avg price for product?
|
||||
"""
|
||||
# Fetch base pricing data
|
||||
cost_basis = Decimal(str(inventory_item.cost_basis))
|
||||
market_price = Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.market_price))
|
||||
tcg_low = Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.low_price))
|
||||
tcg_mid = Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.mid_price))
|
||||
listed_price = Decimal(str(inventory_item.marketplace_listing.listed_price)) if inventory_item.marketplace_listing else None
|
||||
cost_basis = price_data.cost_basis
|
||||
market_price = price_data.market_price
|
||||
tcg_low = price_data.tcg_low
|
||||
tcg_mid = price_data.tcg_mid
|
||||
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"market_price: {market_price}")
|
||||
@@ -45,11 +212,11 @@ class PricingService(BaseService):
|
||||
tcgplayer_shipping_fee = Decimal('1.31')
|
||||
average_cards_per_order = Decimal('3.0')
|
||||
marketplace_fee_percentage = Decimal('0.20')
|
||||
target_margin = Decimal('0.20')
|
||||
target_margin = Decimal('0.10')
|
||||
velocity_multiplier = Decimal('0.0')
|
||||
global_margin_multiplier = Decimal('0.00')
|
||||
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
|
||||
age_of_inventory_multiplier = Decimal('0.0')
|
||||
|
||||
@@ -66,12 +233,11 @@ class PricingService(BaseService):
|
||||
card_cost_margin_multiplier = Decimal('0.033')
|
||||
elif market_price >= 100 and market_price < 200:
|
||||
card_cost_margin_multiplier = Decimal('0.05')
|
||||
else:
|
||||
card_cost_margin_multiplier = Decimal('0.0')
|
||||
|
||||
# Fetch current total quantity in stock for SKU
|
||||
quantity_record = db.query(TCGPlayerInventory).filter(
|
||||
TCGPlayerInventory.tcgplayer_sku_id == inventory_item.physical_item.tcgplayer_sku_id
|
||||
).first()
|
||||
quantity_in_stock = quantity_record.total_quantity if quantity_record else 0
|
||||
quantity_in_stock = price_data.quantity
|
||||
|
||||
# Determine quantity multiplier based on stock levels
|
||||
if quantity_in_stock < 4:
|
||||
@@ -91,6 +257,8 @@ class PricingService(BaseService):
|
||||
# limit shipping cost offset to 10% of market price
|
||||
shipping_cost_offset = min(shipping_cost / average_cards_per_order, market_price * Decimal('0.1'))
|
||||
|
||||
if cost_basis is None:
|
||||
cost_basis = tcg_mid * Decimal('0.65')
|
||||
# Calculate base price considering cost, shipping, fees, and margin targets
|
||||
base_price = (cost_basis + shipping_cost_offset) / (
|
||||
(Decimal('1.0') - marketplace_fee_percentage) - adjusted_target_margin
|
||||
@@ -102,19 +270,19 @@ class PricingService(BaseService):
|
||||
base_price + tcgplayer_shipping_fee
|
||||
)
|
||||
|
||||
# Enforce minimum floor price to ensure profitability
|
||||
if adjusted_price < min_floor_price:
|
||||
adjusted_price = min_floor_price
|
||||
|
||||
# Adjust price based on market prices (TCG low and TCG mid)
|
||||
if adjusted_price < tcg_low:
|
||||
adjusted_price = tcg_mid
|
||||
price_used = "tcg mid"
|
||||
price_reason = "adjusted price below tcg low"
|
||||
elif adjusted_price > tcg_low and adjusted_price < (market_price * Decimal('0.8')) and adjusted_price < (tcg_mid * Decimal('0.8')):
|
||||
elif adjusted_price > tcg_low and adjusted_price < (tcg_mid * Decimal('0.85')):
|
||||
adjusted_price = tcg_mid
|
||||
price_used = "tcg mid"
|
||||
price_reason = f"adjusted price below 80% of market price and 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:
|
||||
price_used = "adjusted price"
|
||||
price_reason = "valid price assigned based on margin targets"
|
||||
@@ -140,43 +308,34 @@ class PricingService(BaseService):
|
||||
adjusted_price = listed_price
|
||||
price_used = "listed price"
|
||||
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
|
||||
with transaction(db):
|
||||
pricing_event = PricingEvent(
|
||||
inventory_item_id=inventory_item.id,
|
||||
price=float(adjusted_price),
|
||||
price_used=price_used,
|
||||
price_reason=price_reason,
|
||||
free_shipping_adjustment=free_shipping_adjustment
|
||||
)
|
||||
db.add(pricing_event)
|
||||
|
||||
# delete previous pricing events for inventory item
|
||||
if inventory_item.marketplace_listing and inventory_item.marketplace_listing.listed_price:
|
||||
inventory_item.marketplace_listing.listed_price.deleted_at = datetime.now()
|
||||
if inventory_item:
|
||||
with transaction(db):
|
||||
pricing_event = PricingEvent(
|
||||
inventory_item_id=inventory_item.id,
|
||||
price=float(adjusted_price),
|
||||
price_used=price_used,
|
||||
price_reason=price_reason,
|
||||
free_shipping_adjustment=free_shipping_adjustment
|
||||
)
|
||||
db.add(pricing_event)
|
||||
|
||||
# delete previous pricing events for inventory item
|
||||
if inventory_item.marketplace_listing:
|
||||
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
|
||||
|
||||
def set_price_for_unmanaged_inventory(self, db: Session, tcgplayer_sku_id: int, quantity: int) -> float:
|
||||
pass
|
||||
|
||||
def update_price_for_product(self, db: Session, tcgplayer_sku_id: int) -> list[PricingEvent]:
|
||||
# get inventory items for sku
|
||||
updated_prices = []
|
||||
inventory_items = db.query(InventoryItem).filter(
|
||||
InventoryItem.physical_item.tcgplayer_sku_id == tcgplayer_sku_id
|
||||
).all()
|
||||
for inventory_item in inventory_items:
|
||||
pricing_event = self.set_price(db, inventory_item)
|
||||
updated_prices.append(pricing_event)
|
||||
return updated_prices
|
||||
|
||||
def set_price_for_product(self, db: Session, tcgplayer_sku_id: int) -> float:
|
||||
# update price for all inventory items for sku
|
||||
prices = self.update_price_for_product(db, tcgplayer_sku_id)
|
||||
sum_prices = sum(price.price for price in prices)
|
||||
average_price = sum_prices / len(prices)
|
||||
return average_price
|
||||
|
||||
|
||||
return pricing_event
|
||||
else:
|
||||
return adjusted_price
|
||||
# BAD BAD BAD FIX PLS TODO
|
@@ -1,4 +1,5 @@
|
||||
from typing import List, Dict
|
||||
import json
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -9,7 +10,7 @@ import asyncio
|
||||
from app.schemas.file import FileInDB
|
||||
from app.services.base_service import BaseService
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.tcgplayer_products import TCGPlayerProduct, TCGPlayerGroup, MTGJSONSKU, MTGJSONCard
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -48,7 +49,7 @@ class PullSheetService(BaseService):
|
||||
"""
|
||||
try:
|
||||
# Read and process CSV data
|
||||
items = await self._read_and_process_csv(file.path)
|
||||
items = await self._read_and_process_csv(db, file.path)
|
||||
|
||||
# Prepare template data
|
||||
template_data = {
|
||||
@@ -79,8 +80,51 @@ class PullSheetService(BaseService):
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating pull sheet PDF: {str(e)}")
|
||||
raise
|
||||
|
||||
async def _get_color_identity(self, db: Session, row: pd.Series) -> str:
|
||||
"""Get color identity from a row.
|
||||
|
||||
Args:
|
||||
row: pandas Series
|
||||
"""
|
||||
# get category id from set name
|
||||
group_id = db.query(TCGPlayerGroup).filter(TCGPlayerGroup.name == row['Set']).first().group_id
|
||||
# format number
|
||||
number = str(int(row['Number'])) if 'Number' in row and pd.notna(row['Number']) and '/' not in str(row['Number']) else str(row['Number']) if 'Number' in row and pd.notna(row['Number']) and '/' in str(row['Number']) else ''
|
||||
# get product info from category id
|
||||
product_id = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.group_id == group_id).filter(TCGPlayerProduct.name == row['Product Name']).filter(TCGPlayerProduct.ext_number == number).filter(TCGPlayerProduct.ext_rarity == row['Rarity']).first().tcgplayer_product_id
|
||||
# get scryfall id from product id
|
||||
mtgjson_id = db.query(MTGJSONSKU).filter(MTGJSONSKU.tcgplayer_product_id == product_id).first().mtgjson_uuid
|
||||
scryfall_id = db.query(MTGJSONCard).filter(MTGJSONCard.mtgjson_uuid == mtgjson_id).first().scryfall_id
|
||||
# get color identity from scryfall
|
||||
scryfall_service = self.get_service('scryfall')
|
||||
color_identity = await scryfall_service.get_color_identity(scryfall_id)
|
||||
if color_identity is None:
|
||||
return '?'
|
||||
# color identity is str of json array, convert to human readable string of list
|
||||
color_identity = [str(color) for color in color_identity]
|
||||
# if color identity is empty, return C for colorless
|
||||
if not color_identity:
|
||||
return 'C'
|
||||
# ensure order, W > U > B > R > G
|
||||
color_identity = sorted(color_identity, key=lambda x: ['W', 'U', 'B', 'R', 'G'].index(x))
|
||||
color_identity = ''.join(color_identity)
|
||||
return color_identity
|
||||
|
||||
async def _update_row_color_identity(self, db: Session, row: pd.Series) -> pd.Series:
|
||||
"""Update color identity from a row.
|
||||
|
||||
Args:
|
||||
row: pandas Series
|
||||
"""
|
||||
# get color identity from row
|
||||
color_identity = await self._get_color_identity(db, row)
|
||||
# update row with color identity
|
||||
row['Color Identity'] = color_identity
|
||||
return row
|
||||
|
||||
|
||||
async def _read_and_process_csv(self, csv_path: str) -> List[Dict]:
|
||||
async def _read_and_process_csv(self, db: Session, csv_path: str) -> List[Dict]:
|
||||
"""Read and process CSV data using pandas.
|
||||
|
||||
Args:
|
||||
@@ -103,6 +147,15 @@ class PullSheetService(BaseService):
|
||||
|
||||
# Sort by Set Release Date (descending) and then Product Name (ascending)
|
||||
df = df.sort_values(['Set Release Date', 'Set', 'Product Name'], ascending=[False, True, True])
|
||||
|
||||
# Process color identities for all rows
|
||||
color_identities = []
|
||||
for _, row in df.iterrows():
|
||||
color_identity = await self._get_color_identity(db, row)
|
||||
color_identities.append(color_identity)
|
||||
|
||||
# Add color identity column to dataframe
|
||||
df['Color Identity'] = color_identities
|
||||
|
||||
# Convert to list of dictionaries
|
||||
items = []
|
||||
@@ -113,7 +166,8 @@ class PullSheetService(BaseService):
|
||||
'quantity': str(int(row['Quantity'])), # Convert to string for template
|
||||
'set': row['Set'],
|
||||
'rarity': row['Rarity'],
|
||||
'card_number': str(int(row['Number'])) if 'Number' in row and pd.notna(row['Number']) and '/' not in str(row['Number']) else str(row['Number']) if 'Number' in row and pd.notna(row['Number']) and '/' in str(row['Number']) else ''
|
||||
'card_number': str(int(row['Number'])) if 'Number' in row and pd.notna(row['Number']) and '/' not in str(row['Number']) else str(row['Number']) if 'Number' in row and pd.notna(row['Number']) and '/' in str(row['Number']) else '',
|
||||
'color_identity': row['Color Identity']
|
||||
})
|
||||
|
||||
return items
|
@@ -4,6 +4,7 @@ from app.services.base_service import BaseService
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
from app.models.tcgplayer_inventory import UnmanagedTCGPlayerInventory, TCGPlayerInventory
|
||||
from datetime import datetime
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SchedulerService(BaseService):
|
||||
@@ -67,7 +68,6 @@ class SchedulerService(BaseService):
|
||||
db.flush()
|
||||
await tcgplayer_inventory_service.refresh_tcgplayer_inventory_table(db)
|
||||
db.flush()
|
||||
await tcgplayer_inventory_service.refresh_unmanaged_tcgplayer_inventory_table(db)
|
||||
|
||||
async def start_scheduled_tasks(self, db):
|
||||
"""Start all scheduled tasks"""
|
||||
@@ -75,7 +75,7 @@ class SchedulerService(BaseService):
|
||||
await self.scheduler.schedule_task(
|
||||
task_name="update_open_orders_hourly",
|
||||
func=self.update_open_orders_hourly,
|
||||
cron_expression="10 * * * *", # Run at minute 10 of every hour
|
||||
cron_expression="*/10 * * * *", # Run at minute 10 of every hour
|
||||
db=db
|
||||
)
|
||||
# Schedule all orders update to run daily at 3 AM
|
||||
@@ -93,6 +93,12 @@ class SchedulerService(BaseService):
|
||||
db=db
|
||||
)
|
||||
|
||||
# Run initial inventory refresh on startup if inventory update was not run today
|
||||
# get last inventory update date
|
||||
last_inventory_update = db.query(TCGPlayerInventory).order_by(TCGPlayerInventory.created_at.desc()).first()
|
||||
if last_inventory_update is None or last_inventory_update.created_at.date() != datetime.now().date():
|
||||
await self.refresh_tcgplayer_inventory_table(db)
|
||||
|
||||
self.scheduler.start()
|
||||
logger.info("All scheduled tasks started")
|
||||
|
||||
|
@@ -34,7 +34,8 @@ class ServiceManager:
|
||||
'inventory': 'app.services.inventory_service.InventoryService',
|
||||
'box': 'app.services.inventory_service.BoxService',
|
||||
'case': 'app.services.inventory_service.CaseService',
|
||||
'marketplace_listing': 'app.services.inventory_service.MarketplaceListingService'
|
||||
'marketplace_listing': 'app.services.inventory_service.MarketplaceListingService',
|
||||
'scryfall': 'app.services.external_api.scryfall.scryfall_service.ScryfallService'
|
||||
|
||||
}
|
||||
self._service_configs = {
|
||||
|
@@ -67,16 +67,30 @@ function displayOrders(orders) {
|
||||
}
|
||||
|
||||
orders.forEach(order => {
|
||||
const hasHighQuantity = order.itemQuantity > 9;
|
||||
const hasHighAmount = order.productAmount > 40.00;
|
||||
|
||||
const orderCard = document.createElement('div');
|
||||
orderCard.className = `bg-gray-700 rounded-lg shadow-sm p-4 border border-gray-600 hover:shadow-md transition-shadow cursor-pointer ${
|
||||
selectedOrders.has(order.orderNumber) ? 'ring-2 ring-blue-500' : ''
|
||||
}`;
|
||||
} ${hasHighQuantity || hasHighAmount ? 'border-yellow-500' : ''}`;
|
||||
orderCard.dataset.orderId = order.orderNumber;
|
||||
orderCard.innerHTML = `
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-bold text-blue-400 truncate">#${order.orderNumber || 'N/A'}</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-lg font-bold text-blue-400 truncate">
|
||||
<a href="https://sellerportal.tcgplayer.com/orders/${order.orderNumber}" target="_blank" rel="noopener noreferrer" class="hover:underline" onclick="event.stopPropagation()">${order.orderNumber || 'N/A'}</a>
|
||||
</h3>
|
||||
${(hasHighQuantity || hasHighAmount) ? `
|
||||
<span class="text-yellow-400" title="${hasHighQuantity ? 'High item quantity' : ''}${hasHighQuantity && hasHighAmount ? ' and ' : ''}${hasHighAmount ? 'High product amount' : ''}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
<p class="text-sm text-gray-400">${order.buyerName || 'N/A'}</p>
|
||||
</div>
|
||||
<span class="px-2 py-1 text-xs rounded-full ${
|
||||
@@ -86,8 +100,23 @@ function displayOrders(orders) {
|
||||
<div class="mt-auto">
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="text-sm text-gray-400">${order.orderDate ? new Date(order.orderDate).toLocaleString() : 'N/A'}</p>
|
||||
<p class="text-lg font-bold text-white">$${order.totalAmount ? order.totalAmount.toFixed(2) : '0.00'}</p>
|
||||
<div class="flex items-center gap-2">
|
||||
${hasHighAmount ? `
|
||||
<span class="text-yellow-400 text-sm">⚠️</span>
|
||||
` : ''}
|
||||
<p class="text-lg font-bold ${hasHighAmount ? 'text-yellow-400' : 'text-white'}">$${order.totalAmount ? order.totalAmount.toFixed(2) : '0.00'}</p>
|
||||
</div>
|
||||
</div>
|
||||
${hasHighQuantity ? `
|
||||
<div class="mt-2 text-sm text-yellow-400">
|
||||
<span class="flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
High quantity: ${order.itemQuantity} items
|
||||
</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@@ -28,9 +28,14 @@
|
||||
|
||||
<!-- Create Transaction Button -->
|
||||
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||
<button id="createTransactionBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||
Create New Transaction
|
||||
</button>
|
||||
<div 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">
|
||||
Create New Transaction
|
||||
</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>
|
||||
|
||||
<!-- Transaction List -->
|
||||
|
@@ -4,6 +4,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
showTransactionModal();
|
||||
});
|
||||
|
||||
document.getElementById('downloadTcgplayerUpdateBtn').addEventListener('click', downloadTcgplayerUpdateFile);
|
||||
|
||||
document.getElementById('addVendorBtn').addEventListener('click', () => {
|
||||
const vendorName = prompt('Enter vendor name:');
|
||||
if (vendorName) {
|
||||
@@ -1074,4 +1076,38 @@ async function saveTransaction() {
|
||||
console.error('Error saving transaction:', error);
|
||||
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.');
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user