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