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_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
11
main.py
@ -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()
|
||||||
|
@ -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))
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
186
services/box.py
186
services/box.py
@ -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
|
@ -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
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -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')
|
@ -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}")
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user