we are so back

This commit is contained in:
2025-05-05 14:05:12 -04:00
parent 11aa4cda16
commit 5c85411c69
20 changed files with 2417 additions and 33 deletions

View File

@ -772,7 +772,7 @@ class DataInitializationService(BaseService):
transaction = await inventory_service.create_purchase_transaction(db, PurchaseTransactionCreate(
vendor_id=vendor.id,
transaction_date=datetime.now(),
items=[PurchaseItem(product_id=619645, unit_price=100, quantity=1, is_case=False)],
items=[PurchaseItem(product_id=619645, unit_price=100, quantity=1, item_type="box")],
transaction_notes="tdm real box test"
#PurchaseItem(product_id=562119, unit_price=800.01, quantity=2, is_case=True, num_boxes=6)],
#transaction_notes="Test Transaction: 1 case and 2 boxes of foundations"
@ -784,7 +784,7 @@ class DataInitializationService(BaseService):
if item.inventory_item.physical_item.item_type == "box":
manabox_service = self.get_service("manabox")
#file_path = 'app/data/test_data/manabox_test_file.csv'
file_path = 'app/data/test_data/tdmtest.csv'
file_path = 'app/data/test_data/dragon.csv'
file_bytes = open(file_path, 'rb').read()
manabox_file = await manabox_service.process_manabox_csv(db, file_bytes, {"source": "test", "description": "test"}, wait=True)
# Ensure manabox_file is a list before passing it

View File

@ -120,7 +120,7 @@ class FileService:
"""List files with optional filtering"""
query = db.query(File)
if file_type:
query = query.filter(File.type == file_type).order_by(File.created_at.desc())
query = query.filter(File.file_type == file_type).order_by(File.created_at.desc())
files = query.offset(skip).limit(limit).all()
return [FileInDB.model_validate(file) for file in files]

View File

@ -1,14 +1,16 @@
from typing import List, Optional, Dict, TypedDict
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload
from decimal import Decimal
from app.services.base_service import BaseService
from app.models.manabox_import_staging import ManaboxImportStaging
from app.contexts.inventory_item import InventoryItemContextFactory
from app.models.inventory_management import (
OpenEvent, Card, InventoryItem, Case,
OpenEvent, Card, InventoryItem, Case, SealedExpectedValue,
Transaction, TransactionItem, Customer, Vendor, Marketplace, Box, MarketplaceListing
)
from app.schemas.file import FileInDB
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, TransactionResponse
from app.models.inventory_management import PhysicalItem
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, TransactionResponse, SealedExpectedValueCreate
from app.db.database import transaction as db_transaction
from datetime import datetime
from typing import Any
@ -20,6 +22,58 @@ class InventoryService(BaseService):
def __init__(self):
super().__init__(None)
async def get_resulting_items_for_open_event(self, db: Session, open_event: OpenEvent) -> List[InventoryItem]:
# Get the IDs of resulting items
resulting_item_ids = [item.id for item in open_event.resulting_items]
# Query using the IDs
return db.query(InventoryItem).filter(InventoryItem.physical_item_id.in_(resulting_item_ids)).all()
async def get_open_event(self, db: Session, inventory_item: InventoryItem, open_event_id: int) -> OpenEvent:
return db.query(OpenEvent).filter(OpenEvent.source_item == inventory_item.physical_item).filter(OpenEvent.id == open_event_id).first()
async def get_open_events_for_inventory_item(self, db: Session, inventory_item: InventoryItem) -> List[OpenEvent]:
return db.query(OpenEvent).filter(OpenEvent.source_item == inventory_item.physical_item).all()
async def get_inventory_item(self, db: Session, inventory_item_id: int) -> InventoryItem:
return db.query(InventoryItem)\
.options(
joinedload(InventoryItem.physical_item).joinedload(PhysicalItem.product_direct)
)\
.filter(InventoryItem.id == inventory_item_id)\
.first()
async def get_expected_value(self, db: Session, product_id: int) -> float:
expected_value = db.query(SealedExpectedValue).filter(SealedExpectedValue.tcgplayer_product_id == product_id).first()
return expected_value.expected_value if expected_value else None
async def get_transactions(self, db: Session, skip: int, limit: int) -> List[Transaction]:
return db.query(Transaction)\
.order_by(Transaction.transaction_date.desc())\
.offset(skip)\
.limit(limit)\
.all()
async def get_transaction(self, db: Session, transaction_id: int) -> Transaction:
return db.query(Transaction)\
.options(
joinedload(Transaction.transaction_items).joinedload(TransactionItem.inventory_item).joinedload(InventoryItem.physical_item).joinedload(PhysicalItem.product_direct),
joinedload(Transaction.vendors),
joinedload(Transaction.customers),
joinedload(Transaction.marketplaces)
)\
.filter(Transaction.id == transaction_id)\
.first()
async def create_expected_value(self, db: Session, expected_value_data: SealedExpectedValueCreate) -> SealedExpectedValue:
with db_transaction(db):
expected_value = SealedExpectedValue(
tcgplayer_product_id=expected_value_data.tcgplayer_product_id,
expected_value=expected_value_data.expected_value
)
db.add(expected_value)
db.flush()
return expected_value
async def create_purchase_transaction(
self,
db: Session,
@ -52,7 +106,7 @@ class InventoryService(BaseService):
# Create the physical item based on type
# TODO: remove is_case and num_boxes, should derive from product_id
# TODO: add support for purchasing single cards
if item.is_case:
if item.item_type == "case":
for i in range(item.quantity):
physical_item = await case_service.create_case(
db=db,
@ -61,7 +115,7 @@ class InventoryService(BaseService):
num_boxes=item.num_boxes
)
physical_items.append(physical_item)
else:
elif item.item_type == "box":
for i in range(item.quantity):
physical_item = await box_service.create_box(
db=db,
@ -69,6 +123,9 @@ class InventoryService(BaseService):
cost_basis=item.unit_price
)
physical_items.append(physical_item)
else:
raise ValueError(f"Invalid item type: {item.item_type}")
# TODO: add support for purchasing single cards
for physical_item in physical_items:
# Create transaction item
@ -128,6 +185,12 @@ class InventoryService(BaseService):
except Exception as e:
raise e
async def get_vendors(
self,
db: Session
) -> List[Vendor]:
return db.query(Vendor).all()
async def create_marketplace(
self,
db: Session,
@ -149,6 +212,12 @@ class InventoryService(BaseService):
except Exception as e:
raise e
async def get_marketplaces(
self,
db: Session
) -> List[Marketplace]:
return db.query(Marketplace).all()
class BoxService(BaseService[Box]):
def __init__(self):
@ -329,3 +398,106 @@ class MarketplaceListingService(BaseService[MarketplaceListing]):
except Exception as e:
raise e
async def get_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing:
return db.query(MarketplaceListing).filter(MarketplaceListing.inventory_item == inventory_item).filter(MarketplaceListing.delisting_date == None).filter(MarketplaceListing.deleted_at == None).filter(MarketplaceListing.marketplace == marketplace).order_by(MarketplaceListing.created_at.desc()).first()
async def get_active_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing:
return db.query(MarketplaceListing).filter(MarketplaceListing.inventory_item == inventory_item).filter(MarketplaceListing.delisting_date == None).filter(MarketplaceListing.deleted_at == None).filter(MarketplaceListing.marketplace == marketplace).filter(MarketplaceListing.listing_date != None).order_by(MarketplaceListing.created_at.desc()).first()
async def confirm_listings(self, db: Session, open_event: OpenEvent, marketplace: Marketplace) -> str:
tcgplayer_add_file = await self.create_tcgplayer_add_file(db, open_event, marketplace)
if not tcgplayer_add_file:
raise ValueError("No TCGplayer add file created")
with db_transaction(db):
for resulting_item in open_event.resulting_items:
marketplace_listing = await self.get_marketplace_listing(db, resulting_item.inventory_item, marketplace)
if marketplace_listing is None:
raise ValueError(f"No active marketplace listing found for inventory item id {resulting_item.inventory_item.id} in {marketplace.name}")
marketplace_listing.listing_date = datetime.now()
db.flush()
return tcgplayer_add_file
async def create_tcgplayer_add_file(self, db: Session, open_event: OpenEvent, marketplace: Marketplace) -> str:
# TCGplayer Id,Product Line,Set Name,Product Name,Title,Number,Rarity,Condition,TCG Market Price,TCG Direct Low,TCG Low Price With Shipping,TCG Low Price,Total Quantity,Add to Quantity,TCG Marketplace Price,Photo URL
headers = [
"TCGplayer Id",
"Product Line",
"Set Name",
"Product Name",
"Title",
"Number",
"Rarity",
"Condition",
"TCG Market Price",
"TCG Direct Low",
"TCG Low Price With Shipping",
"TCG Low Price",
"Total Quantity",
"Add to Quantity",
"TCG Marketplace Price",
"Photo URL"
]
data = {}
for resulting_item in open_event.resulting_items:
marketplace_listing = await self.get_marketplace_listing(db, resulting_item.inventory_item, marketplace)
if marketplace_listing is None:
raise ValueError(f"No active marketplace listing found for inventory item id {resulting_item.inventory_item.id} in {marketplace.name}")
tcgplayer_sku_id = resulting_item.tcgplayer_sku_id
if tcgplayer_sku_id in data:
data[tcgplayer_sku_id]["Add to Quantity"] += 1
continue
product_line = resulting_item.products.category.name
set_name = resulting_item.products.group.name
product_name = resulting_item.products.name
title = ""
number = resulting_item.products.ext_number
rarity = resulting_item.products.ext_rarity
condition = " ".join([condition.capitalize() for condition in resulting_item.sku.condition.split(" ")]) + (" " + resulting_item.products.sub_type_name if resulting_item.products.sub_type_name == "Foil" else "")
tcg_market_price = resulting_item.products.most_recent_tcgplayer_price.market_price
tcg_direct_low = resulting_item.products.most_recent_tcgplayer_price.direct_low_price
tcg_low_price_with_shipping = resulting_item.products.most_recent_tcgplayer_price.low_price
tcg_low_price = resulting_item.products.most_recent_tcgplayer_price.low_price
total_quantity = ""
add_to_quantity = 1
# get average recommended price of product
# get inventory items with same tcgplayer_product_id
inventory_items = db.query(InventoryItem).filter(InventoryItem.physical_item.has(tcgplayer_sku_id=tcgplayer_sku_id)).all()
inventory_item_ids = [inventory_item.id for inventory_item in inventory_items]
logger.debug(f"inventory_item_ids: {inventory_item_ids}")
valid_listings = db.query(MarketplaceListing).filter(MarketplaceListing.inventory_item_id.in_(inventory_item_ids)).filter(MarketplaceListing.delisting_date == None).filter(MarketplaceListing.deleted_at == None).filter(MarketplaceListing.listing_date == None).all()
logger.debug(f"valid_listings: {valid_listings}")
avg_recommended_price = sum([listing.recommended_price.price for listing in valid_listings]) / len(valid_listings)
data[tcgplayer_sku_id] = {
"TCGplayer Id": tcgplayer_sku_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": f"{Decimal(avg_recommended_price):.2f}",
"Photo URL": ""
}
# format data into csv
# header
header_row = ",".join(headers)
# data
def escape_csv_value(value):
if value is None:
return ""
value = str(value)
if any(c in value for c in [',', '"', '\n']):
return f'"{value.replace('"', '""')}"'
return value
data_rows = [",".join([escape_csv_value(data[tcgplayer_id][header]) for header in headers]) for tcgplayer_id in data]
csv_data = "\n".join([header_row] + data_rows)
return csv_data

View File

@ -9,7 +9,7 @@ from typing import Dict, Any, Union, List
import csv
import logging
from datetime import datetime
import asyncio
from fastapi import BackgroundTasks
logger = logging.getLogger(__name__)
@ -17,7 +17,7 @@ class ManaboxService(BaseService):
def __init__(self):
super().__init__(None)
async def process_manabox_csv(self, db: Session, bytes: bytes, metadata: Dict[str, Any], wait: bool = False) -> Union[bool, List[FileInDB]]:
async def process_manabox_csv(self, db: Session, bytes: bytes, metadata: Dict[str, Any], background_tasks: BackgroundTasks, wait: bool = False) -> Union[bool, List[FileInDB]]:
# save file
file = await self.file_service.save_file(
db=db,
@ -29,34 +29,36 @@ class ManaboxService(BaseService):
metadata=metadata
)
# Create the background task
task = asyncio.create_task(self._process_file_background(db, file))
# If wait is True, wait for the task to complete and return the file
if wait:
await task
await self._process_file_background(db, file)
return_value = await self.file_service.get_file(db, file.id)
return [return_value] if return_value else []
return True
else:
background_tasks.add_task(self._process_file_background, db, file)
return True
async def _process_file_background(self, db: Session, file: FileInDB):
try:
# Read the CSV file
with open(file.path, 'r') as csv_file:
reader = csv.DictReader(csv_file)
logger.debug(f"Processing file: {file.path}")
# Pre-fetch all MTGJSONCards for Scryfall IDs in the file
scryfall_ids = {row['Scryfall ID'] for row in reader}
mtg_json_map = {card.scryfall_id: card for card in db.query(MTGJSONCard).filter(MTGJSONCard.scryfall_id.in_(scryfall_ids)).all()}
logger.debug(f"len ids: {len(scryfall_ids)}")
# Re-read the file to process the rows
csv_file.seek(0)
logger.debug(f"header: {reader.fieldnames}")
next(reader) # Skip the header row
staging_entries = [] # To collect all staging entries for batch insert
critical_errors = [] # To collect errors for logging
for row in reader:
logger.debug(f"Processing row: {row}")
mtg_json = mtg_json_map.get(row['Scryfall ID'])
if not mtg_json:
@ -109,6 +111,7 @@ class ManaboxService(BaseService):
# Prepare the staging entry
quantity = int(row['Quantity'])
logger.debug(f"inserting row file id: {file.id} tcgplayer_product_id: {tcgplayer_product.tcgplayer_product_id} tcgplayer_sku_id: {tcgplayer_sku.tcgplayer_sku_id} quantity: {quantity}")
staging_entries.append(ManaboxImportStaging(
file_id=file.id,
tcgplayer_product_id=tcgplayer_product.tcgplayer_product_id,
@ -118,10 +121,13 @@ class ManaboxService(BaseService):
# Bulk insert all valid ManaboxImportStaging entries
if staging_entries:
db.bulk_save_objects(staging_entries)
logger.debug(f"inserting {len(staging_entries)} rows")
with transaction(db):
db.bulk_save_objects(staging_entries)
# Log any critical errors that occurred
for error_message in critical_errors:
logger.debug(f"logging critical error: {error_message}")
with transaction(db):
critical_error_log = CriticalErrorLog(error_message=error_message)
db.add(critical_error_log)

View File

@ -1,11 +1,14 @@
import logging
from sqlalchemy.orm import Session
from app.services.base_service import BaseService
from app.models.inventory_management import InventoryItem
from app.models.inventory_management import InventoryItem, MarketplaceListing
from app.models.tcgplayer_inventory import TCGPlayerInventory
from app.models.pricing import PricingEvent
from app.db.database import transaction
from decimal import Decimal
from datetime import datetime
logger = logging.getLogger(__name__)
@ -148,8 +151,32 @@ class PricingService(BaseService):
free_shipping_adjustment=free_shipping_adjustment
)
db.add(pricing_event)
# delete previous pricing events for inventory item
if inventory_item.marketplace_listing and inventory_item.marketplace_listing.listed_price:
inventory_item.marketplace_listing.listed_price.deleted_at = datetime.now()
return pricing_event
def set_price_for_unmanaged_inventory(self, db: Session, tcgplayer_sku_id: int, quantity: int) -> float:
pass
pass
def update_price_for_product(self, db: Session, tcgplayer_sku_id: int) -> list[PricingEvent]:
# get inventory items for sku
updated_prices = []
inventory_items = db.query(InventoryItem).filter(
InventoryItem.physical_item.tcgplayer_sku_id == tcgplayer_sku_id
).all()
for inventory_item in inventory_items:
pricing_event = self.set_price(db, inventory_item)
updated_prices.append(pricing_event)
return updated_prices
def set_price_for_product(self, db: Session, tcgplayer_sku_id: int) -> float:
# update price for all inventory items for sku
prices = self.update_price_for_product(db, tcgplayer_sku_id)
sum_prices = sum(price.price for price in prices)
average_price = sum_prices / len(prices)
return average_price

View File

@ -10,7 +10,7 @@ import aiohttp
import jinja2
from weasyprint import HTML
from app.services.base_service import BaseService
from app.models.tcgplayer_products import TCGPlayerProduct
from app.models.tcgplayer_products import TCGPlayerProduct, TCGPlayerGroup
log = logging.getLogger(__name__)