ai_giga_tcg/app/services/inventory_service.py

419 lines
16 KiB
Python

from typing import List, Optional, Dict, TypedDict
from sqlalchemy.orm import Session
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 (
SealedBox, OpenEvent, OpenBox, OpenCard, InventoryItem, SealedCase,
Transaction, TransactionItem, Customer, Vendor, Marketplace
)
from app.schemas.file import FileInDB
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, TransactionResponse
from app.db.database import transaction as db_transaction
from datetime import datetime
from typing import Any
import logging
logger = logging.getLogger(__name__)
class InventoryService(BaseService):
def __init__(self):
super().__init__(None)
async def process_manabox_import_staging(self, db: Session, manabox_file_uploads: List[FileInDB], sealed_box: SealedBox) -> bool:
try:
with db_transaction(db):
# Check if box is already opened
existing_open_event = db.query(OpenEvent).filter(
OpenEvent.sealed_box_id == sealed_box.id,
OpenEvent.deleted_at.is_(None)
).first()
if existing_open_event:
raise ValueError(f"Box {sealed_box.id} has already been opened")
# 1. Get the InventoryItemContext for the sealed box
inventory_item_context = InventoryItemContextFactory(db).get_context(sealed_box.inventory_item)
# 2. Create the OpenEvent
open_event = OpenEvent(
sealed_box_id=sealed_box.id,
open_date=datetime.now(),
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(open_event)
db.flush() # Get the ID for relationships
# 3. Create the OpenBox from the SealedBox
open_box = OpenBox(
open_event_id=open_event.id,
product_id=sealed_box.product_id,
sealed_box_id=sealed_box.id,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(open_box)
# 4. Process each card from the CSV
total_market_value = 0
cards = []
manabox_file_upload_ids = [manabox_file_upload.id for manabox_file_upload in manabox_file_uploads]
staging_data = db.query(ManaboxImportStaging).filter(ManaboxImportStaging.file_id.in_(manabox_file_upload_ids)).all()
for record in staging_data:
for i in range(record.quantity):
# Create the OpenCard
open_card = OpenCard(
product_id=record.product_id,
open_event_id=open_event.id,
box_id=open_box.id,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(open_card)
# Create the InventoryItem for the card
card_inventory_item = InventoryItem(
physical_item=open_card,
cost_basis=0, # Will be calculated later
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(card_inventory_item)
# Get the market value for cost basis distribution
card_context = InventoryItemContextFactory(db).get_context(card_inventory_item)
market_value = card_context.market_price
logger.debug(f"market_value: {market_value}")
total_market_value += market_value
cards.append((open_card, card_inventory_item, market_value))
# 5. Distribute the cost basis
original_cost_basis = inventory_item_context.cost_basis
for open_card, card_inventory_item, market_value in cards:
# Calculate this card's share of the cost basis
logger.debug(f"market_value: {market_value}, total_market_value: {total_market_value}, original_cost_basis: {original_cost_basis}")
cost_basis_share = (market_value / total_market_value) * original_cost_basis
card_inventory_item.cost_basis = cost_basis_share
return True
except Exception as e:
raise e
async def create_purchase_transaction(
self,
db: Session,
transaction_data: PurchaseTransactionCreate
) -> Transaction:
"""
Creates a purchase transaction from a vendor.
For each item:
1. Creates a PhysicalItem (SealedCase/SealedBox)
2. Creates an InventoryItem with the purchase price as cost basis
3. Creates TransactionItems linking the purchase to the items
"""
try:
with db_transaction(db):
# Create the transaction
transaction = Transaction(
vendor_id=transaction_data.vendor_id,
transaction_type='purchase',
transaction_date=transaction_data.transaction_date,
transaction_notes=transaction_data.transaction_notes,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(transaction)
db.flush()
total_amount = 0
physical_items = []
for item in transaction_data.items:
# 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:
for i in range(item.quantity):
physical_item = await SealedCaseService().create_sealed_case(
db=db,
product_id=item.product_id,
cost_basis=item.unit_price,
num_boxes=item.num_boxes or 1
)
physical_items.append(physical_item)
else:
for i in range(item.quantity):
physical_item = await SealedBoxService().create_sealed_box(
db=db,
product_id=item.product_id,
cost_basis=item.unit_price
)
physical_items.append(physical_item)
for physical_item in physical_items:
# Create transaction item
transaction_item = TransactionItem(
transaction_id=transaction.id,
physical_item_id=physical_item.id,
unit_price=item.unit_price,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(transaction_item)
total_amount += item.unit_price
# Update transaction total
transaction.transaction_total_amount = total_amount
return transaction
except Exception as e:
raise e
async def create_sale_transaction(
self,
db: Session,
transaction_data: SaleTransactionCreate
) -> Transaction:
"""
this is basically psuedocode not implemented yet
"""
try:
with db_transaction(db):
# Create the transaction
transaction = Transaction(
customer_id=transaction_data.customer_id,
marketplace_id=transaction_data.marketplace_id,
transaction_type='sale',
transaction_date=transaction_data.transaction_date,
transaction_notes=transaction_data.transaction_notes,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(transaction)
db.flush()
total_amount = 0
for item in transaction_data.items:
# Get the inventory item and validate
inventory_item = db.query(InventoryItem).filter(
InventoryItem.id == item.inventory_item_id,
InventoryItem.deleted_at.is_(None)
).first()
if not inventory_item:
raise ValueError(f"Inventory item {item.inventory_item_id} not found")
# Create transaction item
transaction_item = TransactionItem(
transaction_id=transaction.id,
physical_item_id=inventory_item.physical_item_id,
unit_price=item.unit_price,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(transaction_item)
total_amount += item.unit_price
# Update marketplace listing if applicable
if transaction_data.marketplace_id and inventory_item.marketplace_listings:
listing = inventory_item.marketplace_listings
listing.delisting_date = transaction_data.transaction_date
listing.updated_at = datetime.now()
# Update transaction total
transaction.transaction_total_amount = total_amount
return transaction
except Exception as e:
raise e
async def create_customer(
self,
db: Session,
customer_name: str
) -> Customer:
try:
# check if customer already exists
existing_customer = db.query(Customer).filter(Customer.name == customer_name).first()
if existing_customer:
return existing_customer
with db_transaction(db):
customer = Customer(
name=customer_name,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(customer)
db.flush()
return customer
except Exception as e:
raise e
async def create_vendor(
self,
db: Session,
vendor_name: str
) -> Vendor:
try:
# check if vendor already exists
existing_vendor = db.query(Vendor).filter(Vendor.name == vendor_name).first()
if existing_vendor:
return existing_vendor
with db_transaction(db):
vendor = Vendor(
name=vendor_name,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(vendor)
db.flush()
return vendor
except Exception as e:
raise e
async def create_marketplace(
self,
db: Session,
marketplace_name: str
) -> Marketplace:
try:
# check if marketplace already exists
existing_marketplace = db.query(Marketplace).filter(Marketplace.name == marketplace_name).first()
if existing_marketplace:
return existing_marketplace
with db_transaction(db):
marketplace = Marketplace(
name=marketplace_name,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(marketplace)
db.flush()
return marketplace
except Exception as e:
raise e
class SealedBoxService(BaseService[SealedBox]):
def __init__(self):
super().__init__(SealedBox)
async def create_sealed_box(
self,
db: Session,
product_id: int,
cost_basis: float,
case_id: Optional[int] = None
) -> SealedBox:
try:
with db_transaction(db):
# Create the SealedBox
sealed_box = SealedBox(
product_id=product_id,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(sealed_box)
db.flush() # Get the ID for relationships
# If this box is part of a case, link it
if case_id:
case = db.query(SealedCase).filter(SealedCase.id == case_id).first()
if not case:
raise ValueError(f"Case {case_id} not found")
sealed_box.case_id = case_id
# Create the InventoryItem for the sealed box
inventory_item = InventoryItem(
physical_item=sealed_box,
cost_basis=cost_basis,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(inventory_item)
return sealed_box
except Exception as e:
raise e
class SealedCaseService(BaseService[SealedCase]):
def __init__(self):
super().__init__(SealedCase)
async def create_sealed_case(self, db: Session, product_id: int, cost_basis: float, num_boxes: int) -> SealedCase:
try:
with db_transaction(db):
# Create the SealedCase
sealed_case = SealedCase(
product_id=product_id,
num_boxes=num_boxes,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(sealed_case)
db.flush() # Get the ID for relationships
# Create the InventoryItem for the sealed case
inventory_item = InventoryItem(
physical_item=sealed_case,
cost_basis=cost_basis,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(inventory_item)
return sealed_case
except Exception as e:
raise e
async def open_sealed_case(self, db: Session, sealed_case: SealedCase) -> bool:
try:
sealed_case_context = InventoryItemContextFactory(db).get_context(sealed_case.inventory_item)
with db_transaction(db):
# Create the OpenEvent
open_event = OpenEvent(
sealed_case_id=sealed_case_context.physical_item.id,
open_date=datetime.now(),
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(open_event)
db.flush() # Get the ID for relationships
# Create num_boxes SealedBoxes
for i in range(sealed_case.num_boxes):
sealed_box = SealedBox(
product_id=sealed_case_context.physical_item.product_id,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(sealed_box)
db.flush() # Get the ID for relationships
# Create the InventoryItem for the sealed box
inventory_item = InventoryItem(
physical_item=sealed_box,
cost_basis=sealed_case_context.cost_basis,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(inventory_item)
return True
except Exception as e:
raise e