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_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=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): class Ledger(Base):
""" """
ledger associates financial transactions with a user ledger associates financial transactions with a user
@ -283,9 +277,6 @@ class StorageBlock(Base):
date_created = Column(DateTime, default=datetime.now) date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now) date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
## Relationships
class ProductBlock(Base): class ProductBlock(Base):
""" """
ProductBlock represents the relationship between a product and a storage block 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_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=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): class OpenBoxCard(Base):
""" """
OpenedBoxCard represents the relationship between an opened box and the cards it contains 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_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=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): class TCGPlayerGroups(Base):
__tablename__ = 'tcgplayer_groups' __tablename__ = 'tcgplayer_groups'
@ -385,101 +328,6 @@ class TCGPlayerGroups(Base):
modified_on = Column(String) modified_on = Column(String)
category_id = Column(Integer) 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 # enums
class RarityEnum(str, Enum): 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 from fastapi.middleware.cors import CORSMiddleware
import uvicorn import uvicorn
from routes.routes import router from routes.routes import router
from db.database import init_db, check_db_connection, destroy_db, get_db from db.database import init_db, check_db_connection, get_db
from db.utils import db_transaction
import logging import logging
import sys import sys
from db.models import TCGPlayerGroups
from sqlalchemy.orm import Session
# Import your dependency functions # Import your dependency functions
from dependencies import ( from dependencies import (
@ -17,6 +14,7 @@ from dependencies import (
get_file_service, get_file_service,
get_product_service, get_product_service,
get_storage_service, get_storage_service,
get_inventory_service,
) )
logging.basicConfig( logging.basicConfig(
@ -67,10 +65,11 @@ async def startup_event():
# Use dependency injection to get services # Use dependency injection to get services
file_service = get_file_service(db) file_service = get_file_service(db)
storage_service = get_storage_service(db) storage_service = get_storage_service(db)
inventory_service = get_inventory_service(db)
tcgplayer_service = get_tcgplayer_service(db, file_service) tcgplayer_service = get_tcgplayer_service(db, file_service)
pricing_service = get_pricing_service(db, file_service, tcgplayer_service) pricing_service = get_pricing_service(db, file_service, tcgplayer_service)
product_service = get_product_service(db, file_service, tcgplayer_service, storage_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 # Start task service
await task_service.start() await task_service.start()

View File

@ -1,7 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, BackgroundTasks from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, BackgroundTasks
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from typing import Optional from typing import Optional, List
from io import BytesIO
import logging import logging
from datetime import datetime
from schemas.file import ( from schemas.file import (
FileSchema, FileSchema,
@ -23,6 +25,7 @@ from schemas.box import (
from services.file import FileService from services.file import FileService
from services.box import BoxService from services.box import BoxService
from services.task import TaskService from services.task import TaskService
from services.pricing import PricingService
from dependencies import ( from dependencies import (
get_file_service, get_file_service,
get_box_service, get_box_service,
@ -30,7 +33,8 @@ from dependencies import (
get_create_file_metadata, get_create_file_metadata,
get_box_data, get_box_data,
get_box_update_data, get_box_update_data,
get_open_box_data get_open_box_data,
get_pricing_service
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -143,7 +147,9 @@ async def create_box(
) -> CreateBoxResponse: ) -> CreateBoxResponse:
"""Create a new box.""" """Create a new box."""
try: 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( return CreateBoxResponse(
status_code=201, status_code=201,
success=True, success=True,
@ -204,4 +210,69 @@ async def open_box(
) )
except Exception as e: except Exception as e:
logger.error(f"Open box failed: {str(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)) raise HTTPException(status_code=400, detail=str(e))

View File

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

View File

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

View File

@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Dict, List
from uuid import uuid4 from uuid import uuid4
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@ -12,11 +12,11 @@ from db.models import (
Product, Product,
OpenBoxCard, OpenBoxCard,
OpenBox, OpenBox,
Inventory, TCGPlayerGroups,
TCGPlayerGroups Inventory
) )
from db.utils import db_transaction 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 from services.inventory import InventoryService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -28,15 +28,6 @@ class BoxService:
self.db = db self.db = db
self.inventory_service = inventory_service 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]: def get_staged_product_data(self, file_ids: List[str]) -> List[StagedFileProduct]:
"""Retrieve staged product data for given file IDs.""" """Retrieve staged product data for given file IDs."""
return self.db.query(StagedFileProduct).filter( return self.db.query(StagedFileProduct).filter(
@ -52,51 +43,6 @@ class BoxService:
product_data[product] = product_data.get(product, 0) + row.quantity product_data[product] = product_data.get(product, 0) + row.quantity
return product_data 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: def add_products_to_open_box(self, open_box: OpenBox, product_data: Dict[Product, int]) -> None:
"""Add products to an open box.""" """Add products to an open box."""
for product, quantity in product_data.items(): for product, quantity in product_data.items():
@ -108,51 +54,6 @@ class BoxService:
) )
self.db.add(open_box_card) 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: def validate_box_type(self, box_type: str) -> bool:
"""Validate if the box type is supported.""" """Validate if the box type is supported."""
return box_type in VALID_BOX_TYPES return box_type in VALID_BOX_TYPES
@ -171,27 +72,31 @@ class BoxService:
raise ValueError("Invalid set code") raise ValueError("Invalid set code")
existing_box = self.db.query(Box).filter( existing_box = self.db.query(Box).filter(
or_( Box.type == create_box_data.type,
Box.type == create_box_data.type, Box.set_code == create_box_data.set_code,
Box.sku == create_box_data.sku or_(Box.sku == create_box_data.sku, Box.sku.is_(None))
),
Box.set_code == create_box_data.set_code
).first() ).first()
if existing_box: 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=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)
with db_transaction(self.db): return box, True
box = Box(
product_id=str(uuid4()),
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(box)
return box
def update_box(self, box_id: str, update_box_data: UpdateBoxRequest) -> Box: def update_box(self, box_id: str, update_box_data: UpdateBoxRequest) -> Box:
"""Update an existing box.""" """Update an existing box."""
@ -221,11 +126,13 @@ class BoxService:
def delete_box(self, box_id: str) -> Box: def delete_box(self, box_id: str) -> Box:
"""Delete a box.""" """Delete a box."""
box = self.db.query(Box).filter(Box.product_id == box_id).first() 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: if not box:
raise ValueError("Box not found") raise ValueError("Box not found")
with db_transaction(self.db): with db_transaction(self.db):
self.db.delete(box) self.db.delete(box)
self.db.delete(product)
return box return box
def open_box(self, box_id: str, box_data: CreateOpenBoxRequest) -> OpenBox: def open_box(self, box_id: str, box_data: CreateOpenBoxRequest) -> OpenBox:
@ -253,4 +160,43 @@ class BoxService:
{"box_id": open_box.id}, synchronize_session=False {"box_id": open_box.id}, synchronize_session=False
) )
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 return open_box

View File

@ -5,7 +5,11 @@ from services.file import FileService
from services.tcgplayer import TCGPlayerService from services.tcgplayer import TCGPlayerService
from uuid import uuid4 from uuid import uuid4
from db.utils import db_transaction from db.utils import db_transaction
from typing import List, Dict
import pandas as pd
import logging
logger = logging.getLogger(__name__)
class PricingService: class PricingService:
@ -89,4 +93,127 @@ class PricingService:
def cron_load_prices(self, file: File = None): def cron_load_prices(self, file: File = None):
file_content = self.get_pricing_export_content(file) file_content = self.get_pricing_export_content(file)
self.load_pricing_csv_content_to_db(file_content) 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 uuid import uuid4
from typing import List, TypedDict, Optional from typing import List, TypedDict
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from db.utils import db_transaction from db.utils import db_transaction

View File

@ -14,12 +14,13 @@ class TaskService:
self.tasks: Dict[str, Callable] = {} self.tasks: Dict[str, Callable] = {}
self.db = db self.db = db
self.product_service = product_service self.product_service = product_service
self.tcgplayer_service = pricing_service self.pricing_service = pricing_service
async def start(self): async def start(self):
self.scheduler.start() self.scheduler.start()
self.logger.info("Task scheduler started.") self.logger.info("Task scheduler started.")
self.register_scheduled_tasks() self.register_scheduled_tasks()
# self.pricing_service.generate_tcgplayer_inventory_update_file_with_pricing(['e20cc342-23cb-4593-89cb-56a0cb3ed3f3'])
def register_scheduled_tasks(self): def register_scheduled_tasks(self):
self.scheduler.add_job(self.hourly_pricing, 'cron', minute='0') 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 import requests
from services.util._dataframe import TCGPlayerPricingRow, DataframeUtil, ManaboxRow from services.util._dataframe import TCGPlayerPricingRow, DataframeUtil, ManaboxRow
from services.file import FileService from services.file import FileService
from services.inventory import InventoryService
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from db.utils import db_transaction from db.utils import db_transaction
from uuid import uuid4 as uuid from uuid import uuid4 as uuid
@ -15,10 +16,7 @@ import urllib.parse
import json import json
from datetime import datetime from datetime import datetime
import time import time
import csv
from typing import List, Dict, Optional from typing import List, Dict, Optional
from io import StringIO, BytesIO
from sqlalchemy.sql import exists
import pandas as pd import pandas as pd
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from schemas.file import CreateFileRequest from schemas.file import CreateFileRequest
@ -204,7 +202,7 @@ class TCGPlayerService:
# Clear existing cookies to force refresh # Clear existing cookies to force refresh
self.cookies = None self.cookies = None
def _get_inventory(self, version) -> bytes: def get_inventory_df(self, version: str) -> pd.DataFrame:
if version == 'staged': if version == 'staged':
inventory_download_url = f"{self.config.tcgplayer_base_url}{self.config.staged_inventory_download_path}" inventory_download_url = f"{self.config.tcgplayer_base_url}{self.config.staged_inventory_download_path}"
elif version == 'live': elif version == 'live':
@ -212,94 +210,10 @@ class TCGPlayerService:
else: else:
raise ValueError("Invalid inventory version") raise ValueError("Invalid inventory version")
response = self._send_request(inventory_download_url, 'GET') response = self._send_request(inventory_download_url, 'GET')
if response: df = self.df_util.csv_bytes_to_df(response.content)
return self._process_content(response.content) return df
return None
def _process_content(self, content: bytes) -> List[Dict]: def _get_export_csv(self, set_name_ids: List[str]) -> bytes:
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:
""" """
Download export CSV and save to specified path Download export CSV and save to specified path
Returns True if successful, False otherwise Returns True if successful, False otherwise
@ -308,11 +222,7 @@ class TCGPlayerService:
payload = self._set_pricing_export_payload(set_name_ids) payload = self._set_pricing_export_payload(set_name_ids)
export_csv_download_url = f"{self.config.tcgplayer_base_url}{self.config.pricing_export_path}" 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) response = self._send_request(export_csv_download_url, method='POST', data=payload)
if convert: return response.content
csv = self._process_content(response.content)
return csv
else:
return response.content
def create_tcgplayer_card(self, row: TCGPlayerPricingRow, group_id: int): def create_tcgplayer_card(self, row: TCGPlayerPricingRow, group_id: int):
# if card already exists, return none # if card already exists, return none
@ -558,7 +468,7 @@ class TCGPlayerService:
try: try:
all_group_ids = self.db.query(TCGPlayerGroups.group_id).all() all_group_ids = self.db.query(TCGPlayerGroups.group_id).all()
all_group_ids = [str(group_id) for group_id, in all_group_ids] 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( export_csv_file = self.file_service.create_file(export_csv, CreateFileRequest(
source="tcgplayer", source="tcgplayer",
type="tcgplayer_pricing_export", type="tcgplayer_pricing_export",
@ -581,4 +491,43 @@ class TCGPlayerService:
except Exception as e: except Exception as e:
logger.error(f"Failed to load prices: {e}") logger.error(f"Failed to load prices: {e}")
raise 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

@ -65,4 +65,8 @@ class DataframeUtil:
content = content.decode('utf-8') content = content.decode('utf-8')
df = pd.read_csv(StringIO(content)) df = pd.read_csv(StringIO(content))
df = self.format_df_columns(df) df = self.format_df_columns(df)
return 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.testclient import TestClient
from fastapi import BackgroundTasks from fastapi import BackgroundTasks
import pytest import pytest
from unittest.mock import Mock, patch
import asyncio
import os import os
from main import app 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.status_code == 201
assert response.json()["success"] == True 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(): def test_cleanup():
cleanup = True
# Delete all boxes created during testing # Delete all boxes created during testing
for box_id in test_boxes: if cleanup:
client.delete(f"/api/boxes/{box_id}") for box_id in test_boxes:
client.delete(f"/api/boxes/{box_id}")