same as original code now -5 days of my life
This commit is contained in:
parent
511b070cbb
commit
1f5361da88
152
db/models.py
152
db/models.py
@ -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
11
main.py
@ -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()
|
||||
|
@ -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))
|
@ -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
|
||||
|
@ -1,5 +1,4 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from schemas.base import BaseSchema
|
||||
|
||||
|
||||
class UpdateInventoryResponse(BaseModel):
|
||||
|
168
services/box.py
168
services/box.py
@ -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
|
@ -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
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -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')
|
@ -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}")
|
||||
|
Loading…
x
Reference in New Issue
Block a user