same as original code now -5 days of my life

This commit is contained in:
zman 2025-02-07 18:27:20 -05:00
parent 511b070cbb
commit 1f5361da88
12 changed files with 394 additions and 394 deletions

View File

@ -50,12 +50,6 @@ class Sale(Base):
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class Order(Base):
__tablename__ = "orders"
id = Column(String, primary_key=True)
sale_id = Column(String, ForeignKey("sales.id"))
class Ledger(Base):
"""
ledger associates financial transactions with a user
@ -283,9 +277,6 @@ class StorageBlock(Base):
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
## Relationships
class ProductBlock(Base):
"""
ProductBlock represents the relationship between a product and a storage block
@ -300,16 +291,6 @@ class ProductBlock(Base):
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class ProductMarketPrice(Base):
__tablename__ = "product_market_price"
id = Column(String, primary_key=True)
product_id = Column(String, ForeignKey("products.id"))
marketplace_id = Column(String, ForeignKey("marketplaces.id"))
price_id = Column(String, ForeignKey("price.id"))
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class OpenBoxCard(Base):
"""
OpenedBoxCard represents the relationship between an opened box and the cards it contains
@ -335,44 +316,6 @@ class ProductSale(Base):
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
## older
class ManaboxExportData(Base):
__tablename__ = "manabox_export_data"
id = Column(String, primary_key=True)
# upload_id = Column(String)
# box_id = Column(String, nullable=True)
name = Column(String)
set_code = Column(String)
set_name = Column(String)
collector_number = Column(String)
foil = Column(String)
rarity = Column(String)
quantity = Column(Integer)
manabox_id = Column(String)
scryfall_id = Column(String)
purchase_price = Column(Float)
misprint = Column(String)
altered = Column(String)
condition = Column(String)
language = Column(String)
purchase_price_currency = Column(String)
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class UploadHistory(Base):
__tablename__ = "upload_history"
id = Column(String, primary_key=True)
upload_id = Column(String)
filename = Column(String)
file_size_kb = Column(Float)
num_rows = Column(Integer)
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
status = Column(String)
class TCGPlayerGroups(Base):
__tablename__ = 'tcgplayer_groups'
@ -385,101 +328,6 @@ class TCGPlayerGroups(Base):
modified_on = Column(String)
category_id = Column(Integer)
class TCGPlayerInventory(Base):
__tablename__ = 'tcgplayer_inventory'
# 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
id = Column(String, primary_key=True)
export_id = Column(String)
tcgplayer_product_id = Column(String, ForeignKey("tcgplayer_product.id"), nullable=True)
tcgplayer_id = Column(Integer)
product_line = Column(String)
set_name = Column(String)
product_name = Column(String)
title = Column(String)
number = Column(String)
rarity = Column(String)
condition = Column(String)
tcg_market_price = Column(Float)
tcg_direct_low = Column(Float)
tcg_low_price_with_shipping = Column(Float)
tcg_low_price = Column(Float)
total_quantity = Column(Integer)
add_to_quantity = Column(Integer)
tcg_marketplace_price = Column(Float)
photo_url = Column(String)
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class TCGPlayerExportHistory(Base):
__tablename__ = 'tcgplayer_export_history'
id = Column(String, primary_key=True)
type = Column(String)
pricing_export_id = Column(String)
inventory_export_id = Column(String)
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class TCGPlayerPricingHistory(Base):
__tablename__ = 'tcgplayer_pricing_history'
id = Column(String, primary_key=True)
tcgplayer_product_id = Column(String, ForeignKey("tcgplayer_product.id"))
export_id = Column(String)
group_id = Column(Integer)
tcgplayer_id = Column(Integer)
tcg_market_price = Column(Float)
tcg_direct_low = Column(Float)
tcg_low_price_with_shipping = Column(Float)
tcg_low_price = Column(Float)
tcg_marketplace_price = Column(Float)
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class TCGPlayerProduct(Base):
__tablename__ = 'tcgplayer_product'
id = Column(String, primary_key=True)
group_id = Column(Integer)
tcgplayer_id = Column(Integer)
product_line = Column(String)
set_name = Column(String)
product_name = Column(String)
title = Column(String)
number = Column(String)
rarity = Column(String)
condition = Column(String)
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class ManaboxTCGPlayerMapping(Base):
__tablename__ = 'manabox_tcgplayer_mapping'
id = Column(String, primary_key=True)
manabox_id = Column(String, ForeignKey("manabox_export_data.id"))
tcgplayer_id = Column(Integer, ForeignKey("tcgplayer_inventory.tcgplayer_id"))
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class SetCodeGroupIdMapping(Base):
__tablename__ = 'set_code_group_id_mapping'
id = Column(String, primary_key=True)
set_code = Column(String)
group_id = Column(Integer)
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class UnmatchedManaboxData(Base):
__tablename__ = 'unmatched_manabox_data'
id = Column(String, primary_key=True)
manabox_id = Column(String, ForeignKey("manabox_export_data.id"))
reason = Column(String)
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
# enums
class RarityEnum(str, Enum):

11
main.py
View File

@ -1,13 +1,10 @@
from fastapi import FastAPI, Depends
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from routes.routes import router
from db.database import init_db, check_db_connection, destroy_db, get_db
from db.utils import db_transaction
from db.database import init_db, check_db_connection, get_db
import logging
import sys
from db.models import TCGPlayerGroups
from sqlalchemy.orm import Session
# Import your dependency functions
from dependencies import (
@ -17,6 +14,7 @@ from dependencies import (
get_file_service,
get_product_service,
get_storage_service,
get_inventory_service,
)
logging.basicConfig(
@ -67,10 +65,11 @@ async def startup_event():
# Use dependency injection to get services
file_service = get_file_service(db)
storage_service = get_storage_service(db)
inventory_service = get_inventory_service(db)
tcgplayer_service = get_tcgplayer_service(db, file_service)
pricing_service = get_pricing_service(db, file_service, tcgplayer_service)
product_service = get_product_service(db, file_service, tcgplayer_service, storage_service)
task_service = get_task_service(db, product_service, tcgplayer_service)
task_service = get_task_service(db, product_service, pricing_service)
# Start task service
await task_service.start()

View File

@ -1,7 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, BackgroundTasks
from fastapi.responses import StreamingResponse
from typing import Optional
from typing import Optional, List
from io import BytesIO
import logging
from datetime import datetime
from schemas.file import (
FileSchema,
@ -23,6 +25,7 @@ from schemas.box import (
from services.file import FileService
from services.box import BoxService
from services.task import TaskService
from services.pricing import PricingService
from dependencies import (
get_file_service,
get_box_service,
@ -30,7 +33,8 @@ from dependencies import (
get_create_file_metadata,
get_box_data,
get_box_update_data,
get_open_box_data
get_open_box_data,
get_pricing_service
)
logger = logging.getLogger(__name__)
@ -143,7 +147,9 @@ async def create_box(
) -> CreateBoxResponse:
"""Create a new box."""
try:
result = box_service.create_box(box_data)
result, success = box_service.create_box(box_data)
if not success:
raise HTTPException(status_code=400, detail="Box creation failed, box already exists")
return CreateBoxResponse(
status_code=201,
success=True,
@ -205,3 +211,68 @@ async def open_box(
except Exception as e:
logger.error(f"Open box failed: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/boxes/{box_id}/open", response_model=CreateOpenBoxResponse, status_code=200)
async def delete_open_box(
box_id: str,
box_service: BoxService = Depends(get_box_service)
) -> CreateOpenBoxResponse:
"""Delete an open box by ID."""
try:
result = box_service.delete_open_box(box_id)
return CreateOpenBoxResponse(
status_code=201,
success=True,
open_box=[OpenBoxSchema.from_orm(result)]
)
except Exception as e:
logger.error(f"Delete open box failed: {str(e)}")
raise HTTPException(status_code=400, detail=str(e)
)
@router.post("/tcgplayer/inventory/add", response_class=StreamingResponse)
async def create_inventory_add_file(
request: dict, # Just use a dict instead
pricing_service: PricingService = Depends(get_pricing_service),
):
"""Create a new inventory add file for download."""
try:
# Get IDs directly from the dict
open_box_ids = request.get('open_box_ids', [])
content = pricing_service.generate_tcgplayer_inventory_update_file_with_pricing(open_box_ids)
stream = BytesIO(content)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
return StreamingResponse(
iter([stream.getvalue()]),
media_type="text/csv",
headers={
'Content-Disposition': f'attachment; filename="inventory_add_{timestamp}.csv"'
}
)
except Exception as e:
logger.error(f"Create inventory add file failed: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
@router.get("/tcgplayer/inventory/update", response_class=StreamingResponse)
async def create_inventory_update_file(
pricing_service: PricingService = Depends(get_pricing_service),
):
"""Create a new inventory update file for download."""
try:
content = pricing_service.generate_tcgplayer_inventory_update_file_with_pricing()
stream = BytesIO(content)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
return StreamingResponse(
iter([stream.getvalue()]),
media_type="text/csv",
headers={
'Content-Disposition': f'attachment; filename="inventory_update_{timestamp}.csv"'
}
)
except Exception as e:
logger.error(f"Create inventory update file failed: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))

View File

@ -1,7 +1,5 @@
from pydantic import BaseModel, Field, validator
from typing import Optional, List
from pydantic import BaseModel
from datetime import datetime
from uuid import UUID
# Base schemas with shared attributes

View File

@ -1,5 +1,4 @@
from pydantic import BaseModel, Field
from schemas.base import BaseSchema
class UpdateInventoryResponse(BaseModel):

View File

@ -1,5 +1,5 @@
from datetime import datetime
from typing import Any, Dict, List, Optional
from typing import Dict, List
from uuid import uuid4
from sqlalchemy import or_
from sqlalchemy.orm import Session
@ -12,11 +12,11 @@ from db.models import (
Product,
OpenBoxCard,
OpenBox,
Inventory,
TCGPlayerGroups
TCGPlayerGroups,
Inventory
)
from db.utils import db_transaction
from schemas.box import CreateBoxRequest, CreateBoxResponse, UpdateBoxRequest, CreateOpenBoxRequest
from schemas.box import CreateBoxRequest, UpdateBoxRequest, CreateOpenBoxRequest
from services.inventory import InventoryService
logger = logging.getLogger(__name__)
@ -28,15 +28,6 @@ class BoxService:
self.db = db
self.inventory_service = inventory_service
def validate_file_ids(self, file_ids: List[str]) -> None:
"""Validate that all provided file IDs exist in the database."""
invalid_files = [
file_id for file_id in file_ids
if not self.db.query(File).filter(File.id == file_id).first()
]
if invalid_files:
raise ValueError(f"File IDs not found: {', '.join(invalid_files)}")
def get_staged_product_data(self, file_ids: List[str]) -> List[StagedFileProduct]:
"""Retrieve staged product data for given file IDs."""
return self.db.query(StagedFileProduct).filter(
@ -52,51 +43,6 @@ class BoxService:
product_data[product] = product_data.get(product, 0) + row.quantity
return product_data
def find_product_for_box_data(self, create_box_data: Dict[str, Any]) -> Optional[Product]:
"""Find existing product matching box data."""
return self.db.query(Product).filter(
Product.name == create_box_data["name"],
Product.type == "box",
Product.set_code == create_box_data["set_code"],
Product.set_name == create_box_data["set_name"],
Product.product_line == create_box_data["product_line"]
).first()
def create_product_for_box(self, create_box_data: Dict[str, Any]) -> Product:
"""Create a new product for a box."""
product = Product(
id=str(uuid4()),
name=create_box_data["name"],
type="box",
set_code=create_box_data["set_code"],
set_name=create_box_data["set_name"],
product_line=create_box_data["product_line"]
)
self.db.add(product)
return product
def create_box_db(self, product: Product, create_box_data: Dict[str, Any]) -> Box:
"""Create a new box record in the database."""
box = Box(
product_id=product.id,
type=create_box_data["type"],
sku=create_box_data["sku"],
num_cards_expected=create_box_data["num_cards_expected"]
)
self.db.add(box)
return box
def create_open_box(self, product: Product, create_box_data: Dict[str, Any]) -> OpenBox:
"""Create a new open box record."""
open_box = OpenBox(
id=str(uuid4()),
product_id=product.id,
num_cards_actual=create_box_data["num_cards_actual"],
date_opened=datetime.strptime(create_box_data["date_opened"], "%Y-%m-%d")
)
self.db.add(open_box)
return open_box
def add_products_to_open_box(self, open_box: OpenBox, product_data: Dict[Product, int]) -> None:
"""Add products to an open box."""
for product, quantity in product_data.items():
@ -108,51 +54,6 @@ class BoxService:
)
self.db.add(open_box_card)
def format_response(self, open_box: Optional[OpenBox] = None, inventory: Optional[Inventory] = None) -> CreateBoxResponse:
"""Format the response for box creation."""
return CreateBoxResponse(success=True)
def _create_box(self, create_box_data: Dict[str, Any], file_ids: Optional[List[str]] = None) -> CreateBoxResponse:
"""Internal method to handle box creation logic."""
sealed = create_box_data["sealed"]
if file_ids and sealed:
raise ValueError("Cannot add cards with a sealed box")
if file_ids and not sealed:
self.validate_file_ids(file_ids)
staged_product_data = self.get_staged_product_data(file_ids)
product_data = self.aggregate_staged_product_data(staged_product_data)
box_product = self.find_product_for_box_data(create_box_data)
try:
with db_transaction(self.db):
if not box_product:
box_product = self.create_product_for_box(create_box_data)
box = self.create_box_db(box_product, create_box_data)
if not sealed:
open_box = self.create_open_box(box_product, create_box_data)
if file_ids:
self.inventory_service.process_staged_products(product_data)
self.add_products_to_open_box(open_box, product_data)
# Update file statuses to processed
self.db.query(File).filter(File.id.in_(file_ids)).update(
{"status": "processed"}, synchronize_session=False
)
return self.format_response(open_box=open_box)
elif sealed:
inventory = self.inventory_service.add_sealed_box_to_inventory(box_product, 1)
return self.format_response(inventory=inventory)
except Exception as e:
logger.error(f"Error creating box: {str(e)}")
raise
def validate_box_type(self, box_type: str) -> bool:
"""Validate if the box type is supported."""
return box_type in VALID_BOX_TYPES
@ -171,27 +72,31 @@ class BoxService:
raise ValueError("Invalid set code")
existing_box = self.db.query(Box).filter(
or_(
Box.type == create_box_data.type,
Box.sku == create_box_data.sku
),
Box.set_code == create_box_data.set_code
Box.set_code == create_box_data.set_code,
or_(Box.sku == create_box_data.sku, Box.sku.is_(None))
).first()
if existing_box:
raise ValueError("Box already exists")
return existing_box, False
else:
with db_transaction(self.db):
product = Product(
id=str(uuid4()),
type='box',
product_line='mtg'
)
box = Box(
product_id=str(uuid4()),
product_id=product.id,
type=create_box_data.type,
set_code=create_box_data.set_code,
sku=create_box_data.sku,
num_cards_expected=create_box_data.num_cards_expected
)
self.db.add(product)
self.db.add(box)
return box
return box, True
def update_box(self, box_id: str, update_box_data: UpdateBoxRequest) -> Box:
"""Update an existing box."""
@ -221,11 +126,13 @@ class BoxService:
def delete_box(self, box_id: str) -> Box:
"""Delete a box."""
box = self.db.query(Box).filter(Box.product_id == box_id).first()
product = self.db.query(Product).filter(Product.id == box_id).first()
if not box:
raise ValueError("Box not found")
with db_transaction(self.db):
self.db.delete(box)
self.db.delete(product)
return box
def open_box(self, box_id: str, box_data: CreateOpenBoxRequest) -> OpenBox:
@ -254,3 +161,42 @@ class BoxService:
)
return open_box
def delete_open_box(self, box_id: str) -> OpenBox:
# Fetch open box and related cards in one query
open_box = (
self.db.query(OpenBox)
.filter(OpenBox.id == box_id)
.first()
)
if not open_box:
raise ValueError("Open box not found")
# Get all open box cards and related inventory items in one query
open_box_cards = (
self.db.query(OpenBoxCard, Inventory)
.join(
Inventory,
OpenBoxCard.card_id == Inventory.product_id
)
.filter(OpenBoxCard.open_box_id == open_box.id)
.all()
)
# Process inventory adjustments
for open_box_card, inventory_item in open_box_cards:
if open_box_card.quantity > inventory_item.quantity:
raise ValueError("Open box quantity exceeds inventory quantity")
inventory_item.quantity -= open_box_card.quantity
if inventory_item.quantity == 0:
self.db.delete(inventory_item)
# Delete the open box card
self.db.delete(open_box_card)
# Execute all database operations in a single transaction
with db_transaction(self.db):
self.db.delete(open_box)
return open_box

View File

@ -5,7 +5,11 @@ from services.file import FileService
from services.tcgplayer import TCGPlayerService
from uuid import uuid4
from db.utils import db_transaction
from typing import List, Dict
import pandas as pd
import logging
logger = logging.getLogger(__name__)
class PricingService:
@ -90,3 +94,126 @@ class PricingService:
def cron_load_prices(self, file: File = None):
file_content = self.get_pricing_export_content(file)
self.load_pricing_csv_content_to_db(file_content)
def get_all_prices_for_products(self, product_ids: List[str]) -> Dict[str, Dict[str, float]]:
all_prices = self.db.query(Price).filter(
Price.product_id.in_(product_ids)
).all()
price_lookup = {}
for price in all_prices:
if price.product_id not in price_lookup:
price_lookup[price.product_id] = {}
price_lookup[price.product_id][price.type] = price.price
return price_lookup
def apply_price_to_df_columns(self, row: pd.Series, price_lookup: Dict[str, Dict[str, float]]) -> pd.Series:
product_prices = price_lookup.get(row['product_id'], {})
for price_type, price in product_prices.items():
row[price_type] = price
return row
def default_pricing_algo(self, row: pd.Series) -> pd.Series:
"""Default pricing algorithm with complex pricing rules"""
tcg_low = row.get('tcg_low_price')
tcg_low_shipping = row.get('tcg_low_price_with_shipping')
if pd.isna(tcg_low) or pd.isna(tcg_low_shipping):
logger.warning(f"Missing pricing data for row: {row}")
row['new_price'] = None
return row
# Apply pricing rules
if tcg_low < 0.35:
new_price = 0.35
elif tcg_low < 5 or tcg_low_shipping < 5:
new_price = round(tcg_low * 1.25, 2)
elif tcg_low_shipping > 25:
new_price = round(tcg_low_shipping * 1.025, 2)
else:
new_price = round(tcg_low_shipping * 1.10, 2)
row['new_price'] = new_price
return row
def apply_pricing_algo(self, row: pd.Series, pricing_algo: callable = None) -> pd.Series:
"""Modified to handle the pricing algorithm as an instance method"""
if pricing_algo is None:
pricing_algo = self.default_pricing_algo
return pricing_algo(row)
def generate_tcgplayer_inventory_update_file_with_pricing(self, open_box_ids: List[str] = None) -> bytes:
desired_columns = [
'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'
]
if open_box_ids:
# Get initial dataframe
update_type = 'add'
df = self.tcgplayer_service.open_box_cards_to_tcgplayer_inventory_df(open_box_ids)
else:
update_type = 'update'
df = self.tcgplayer_service.get_inventory_df('live')
# remove rows with total quantity of 0
df = df[df['total_quantity'] != 0]
tcgplayer_ids = df['tcgplayer_id'].unique().tolist()
# Make a single query to get all matching records
product_id_mapping = {
card.tcgplayer_id: card.product_id
for card in self.db.query(CardTCGPlayer)
.filter(CardTCGPlayer.tcgplayer_id.in_(tcgplayer_ids))
.all()
}
# Map the ids using the dictionary
df['product_id'] = df['tcgplayer_id'].map(product_id_mapping)
price_lookup = self.get_all_prices_for_products(df['product_id'].unique())
# Apply price columns
df = df.apply(lambda row: self.apply_price_to_df_columns(row, price_lookup), axis=1)
# Apply pricing algorithm
df = df.apply(self.apply_pricing_algo, axis=1)
# if update type is update, remove rows where new_price == listed_price
if update_type == 'update':
df = df[df['new_price'] != df['listed_price']]
# Set marketplace price
df['TCG Marketplace Price'] = df['new_price']
column_mapping = {
'tcgplayer_id': 'TCGplayer Id',
'product_line': 'Product Line',
'set_name': 'Set Name',
'product_name': 'Product Name',
'title': 'Title',
'number': 'Number',
'rarity': 'Rarity',
'condition': 'Condition',
'tcg_market_price': 'TCG Market Price',
'tcg_direct_low': 'TCG Direct Low',
'tcg_low_price_with_shipping': 'TCG Low Price With Shipping',
'tcg_low_price': 'TCG Low Price',
'total_quantity': 'Total Quantity',
'add_to_quantity': 'Add to Quantity',
'photo_url': 'Photo URL'
}
df = df.rename(columns=column_mapping)
# Now do your column selection
df = df[desired_columns]
# remove any rows with no price
#df = df[df['TCG Marketplace Price'] != 0]
#df = df[df['TCG Marketplace Price'].notna()]
# Convert to CSV bytes
csv_bytes = self.df_util.df_to_csv_bytes(df)
return csv_bytes

View File

@ -1,5 +1,5 @@
from uuid import uuid4
from typing import List, TypedDict, Optional
from typing import List, TypedDict
from sqlalchemy.orm import Session
from db.utils import db_transaction

View File

@ -14,12 +14,13 @@ class TaskService:
self.tasks: Dict[str, Callable] = {}
self.db = db
self.product_service = product_service
self.tcgplayer_service = pricing_service
self.pricing_service = pricing_service
async def start(self):
self.scheduler.start()
self.logger.info("Task scheduler started.")
self.register_scheduled_tasks()
# self.pricing_service.generate_tcgplayer_inventory_update_file_with_pricing(['e20cc342-23cb-4593-89cb-56a0cb3ed3f3'])
def register_scheduled_tasks(self):
self.scheduler.add_job(self.hourly_pricing, 'cron', minute='0')

View File

@ -1,7 +1,8 @@
from db.models import ManaboxExportData, Box, TCGPlayerGroups, TCGPlayerInventory, TCGPlayerExportHistory, TCGPlayerPricingHistory, TCGPlayerProduct, ManaboxTCGPlayerMapping, CardManabox, CardTCGPlayer, Price, Product, Card, File
from db.models import TCGPlayerGroups, CardTCGPlayer, Product, Card, File, Inventory, OpenBox, OpenBoxCard
import requests
from services.util._dataframe import TCGPlayerPricingRow, DataframeUtil, ManaboxRow
from services.file import FileService
from services.inventory import InventoryService
from sqlalchemy.orm import Session
from db.utils import db_transaction
from uuid import uuid4 as uuid
@ -15,10 +16,7 @@ import urllib.parse
import json
from datetime import datetime
import time
import csv
from typing import List, Dict, Optional
from io import StringIO, BytesIO
from sqlalchemy.sql import exists
import pandas as pd
from sqlalchemy.exc import SQLAlchemyError
from schemas.file import CreateFileRequest
@ -204,7 +202,7 @@ class TCGPlayerService:
# Clear existing cookies to force refresh
self.cookies = None
def _get_inventory(self, version) -> bytes:
def get_inventory_df(self, version: str) -> pd.DataFrame:
if version == 'staged':
inventory_download_url = f"{self.config.tcgplayer_base_url}{self.config.staged_inventory_download_path}"
elif version == 'live':
@ -212,94 +210,10 @@ class TCGPlayerService:
else:
raise ValueError("Invalid inventory version")
response = self._send_request(inventory_download_url, 'GET')
if response:
return self._process_content(response.content)
return None
df = self.df_util.csv_bytes_to_df(response.content)
return df
def _process_content(self, content: bytes) -> List[Dict]:
if not content:
return []
try:
text_content = content.decode('utf-8')
except UnicodeDecodeError:
for encoding in ['latin-1', 'cp1252', 'iso-8859-1']:
try:
text_content = content.decode(encoding)
break
except UnicodeDecodeError:
continue
else:
raise
csv_file = StringIO(text_content)
try:
reader = csv.DictReader(csv_file)
inventory = [
{k: v.strip() if v else None for k, v in row.items()}
for row in reader
if any(v.strip() for v in row.values())
]
return inventory
finally:
csv_file.close()
def update_inventory(self, version: str) -> Dict:
if version not in ['staged', 'live']:
raise ValueError("Invalid inventory version")
export_id = str(uuid())
inventory = self._get_inventory(version)
if not inventory:
return {"message": "No inventory to update"}
# add snapshot id
for item in inventory:
item['export_id'] = export_id
# check if product exists for tcgplayer_id
product_exists = self.db.query(TCGPlayerProduct).filter_by(tcgplayer_id=item['TCGplayer Id']).first()
if product_exists:
item['tcgplayer_product_id'] = product_exists.id
else:
item['tcgplayer_product_id'] = None
inventory_fields = {
'TCGplayer Id': 'tcgplayer_id',
'tcgplayer_product_id': 'tcgplayer_product_id',
'export_id': 'export_id',
'Product Line': 'product_line',
'Set Name': 'set_name',
'Product Name': 'product_name',
'Title': 'title',
'Number': 'number',
'Rarity': 'rarity',
'Condition': 'condition',
'TCG Market Price': 'tcg_market_price',
'TCG Direct Low': 'tcg_direct_low',
'TCG Low Price With Shipping': 'tcg_low_price_with_shipping',
'TCG Low Price': 'tcg_low_price',
'Total Quantity': 'total_quantity',
'Add to Quantity': 'add_to_quantity',
'TCG Marketplace Price': 'tcg_marketplace_price'
}
with db_transaction(self.db):
export_history = TCGPlayerExportHistory(
id=str(uuid()),
type=version + '_inventory',
inventory_export_id=export_id
)
self.db.add(export_history)
for item in inventory:
db_item = TCGPlayerInventory(
id=str(uuid()),
**{db_field: item.get(csv_field)
for csv_field, db_field in inventory_fields.items()}
)
self.db.add(db_item)
return {"message": "Inventory updated successfully", "export_id": export_id}
def _get_export_csv(self, set_name_ids: List[str], convert=True) -> bytes:
def _get_export_csv(self, set_name_ids: List[str]) -> bytes:
"""
Download export CSV and save to specified path
Returns True if successful, False otherwise
@ -308,10 +222,6 @@ class TCGPlayerService:
payload = self._set_pricing_export_payload(set_name_ids)
export_csv_download_url = f"{self.config.tcgplayer_base_url}{self.config.pricing_export_path}"
response = self._send_request(export_csv_download_url, method='POST', data=payload)
if convert:
csv = self._process_content(response.content)
return csv
else:
return response.content
def create_tcgplayer_card(self, row: TCGPlayerPricingRow, group_id: int):
@ -558,7 +468,7 @@ class TCGPlayerService:
try:
all_group_ids = self.db.query(TCGPlayerGroups.group_id).all()
all_group_ids = [str(group_id) for group_id, in all_group_ids]
export_csv = self._get_export_csv(all_group_ids, convert=False)
export_csv = self._get_export_csv(all_group_ids)
export_csv_file = self.file_service.create_file(export_csv, CreateFileRequest(
source="tcgplayer",
type="tcgplayer_pricing_export",
@ -582,3 +492,42 @@ class TCGPlayerService:
except Exception as e:
logger.error(f"Failed to load prices: {e}")
raise
def open_box_cards_to_tcgplayer_inventory_df(self, open_box_ids: List[str]) -> pd.DataFrame:
tcgcards = (self.db.query(OpenBoxCard, CardTCGPlayer)
.filter(OpenBoxCard.open_box_id.in_(open_box_ids))
.join(CardTCGPlayer, OpenBoxCard.card_id == CardTCGPlayer.product_id)
.all())
if not tcgcards:
return None
# Create dataframe
df = pd.DataFrame([(tcg.product_id, tcg.tcgplayer_id, tcg.product_line, tcg.set_name, tcg.product_name,
tcg.title, tcg.number, tcg.rarity, tcg.condition, obc.quantity)
for obc, tcg in tcgcards],
columns=['product_id', 'tcgplayer_id', 'product_line', 'set_name', 'product_name',
'title', 'number', 'rarity', 'condition', 'quantity'])
# Add empty columns
df['Total Quantity'] = ''
df['Add to Quantity'] = df['quantity']
df['TCG Marketplace Price'] = ''
df['Photo URL'] = ''
# Rename columns
df = df.rename(columns={
'tcgplayer_id': 'TCGplayer Id',
'product_line': 'Product Line',
'set_name': 'Set Name',
'product_name': 'Product Name',
'title': 'Title',
'number': 'Number',
'rarity': 'Rarity',
'condition': 'Condition'
})
return df

View File

@ -66,3 +66,7 @@ class DataframeUtil:
df = pd.read_csv(StringIO(content))
df = self.format_df_columns(df)
return df
def df_to_csv_bytes(self, df: pd.DataFrame) -> bytes:
csv = df.to_csv(index=False)
return csv.encode('utf-8')

View File

@ -1,12 +1,8 @@
from fastapi.testclient import TestClient
from fastapi import BackgroundTasks
import pytest
from unittest.mock import Mock, patch
import asyncio
import os
from main import app
from services.file import FileService
from services.task import TaskService
@ -146,9 +142,71 @@ async def test_open_box():
assert response.status_code == 201
assert response.json()["success"] == True
@pytest.mark.asyncio
async def test_delete_open_box():
with open(TEST_FILE_PATH, "rb") as test_file:
files = {"file": test_file}
# Make request
response = client.post("/api/files", data=DEFAULT_METADATA, files=files)
file_id = response.json()["files"][0]["id"]
# Check response
assert response.status_code == 201
assert response.json()["success"] == True
file_data = response.json()["files"][0]
assert file_data["source"] == DEFAULT_METADATA["source"]
assert file_data["type"] == DEFAULT_METADATA["type"]
assert file_data["status"] == "pending"
assert file_data["service"] == None
assert file_data["filename"] == "manabox_test_file.csv"
assert file_data["filesize_kb"] == get_file_size_kb(TEST_FILE_PATH)
assert file_data["id"] is not None
# Execute background tasks if they were added
background_tasks = BackgroundTasks()
for task in background_tasks.tasks:
await task()
# Create a box first
create_response = client.post("/api/boxes",
data={
"type": "play",
"set_code": "INR",
"sku": "1423",
"num_cards_expected": 504
}
)
box_id = create_response.json()["box"][0]["product_id"]
# Open the box
open_response = client.post(f"/api/boxes/{box_id}/open",
data={
"product_id": box_id,
"file_ids": [file_id],
"num_cards_actual": 500
}
)
# Check if the box is opened
assert open_response.status_code == 201
assert open_response.json()["success"] == True
# Get the open box ID
open_box_id = open_response.json()["open_box"][0]["id"]
# Delete the open box
response = client.delete(f"/api/boxes/{open_box_id}/open")
assert response.status_code == 200
assert response.json()["success"] == True
def test_cleanup():
cleanup = True
# Delete all boxes created during testing
if cleanup:
for box_id in test_boxes:
client.delete(f"/api/boxes/{box_id}")