we are so back
This commit is contained in:
@ -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
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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__)
|
||||
|
||||
|
Reference in New Issue
Block a user