from db.models import Box, File, StagedFileProduct, Product, OpenBoxCard, OpenBox, Inventory, TCGPlayerGroups from db.utils import db_transaction from uuid import uuid4 as uuid from datetime import datetime from sqlalchemy.orm import Session from sqlalchemy.engine.result import Row from sqlalchemy import or_ from schemas.box import CreateBoxRequest, CreateBoxResponse, UpdateBoxRequest, CreateOpenBoxRequest import logging from typing import Any from db.utils import db_transaction from services.inventory import InventoryService logger = logging.getLogger(__name__) class BoxService: def __init__(self, db: Session, inventory_service: InventoryService): self.db = db self.inventory_service = inventory_service def validate_file_ids(self, file_ids: list[str]): # check if all file_ids are valid for file_id in file_ids: if self.db.query(File).filter(File.id == file_id).first() is None: raise Exception(f"File ID {file_id} not found") def get_staged_product_data(self, file_ids: list[str]) -> StagedFileProduct: staged_product_data = self.db.query(StagedFileProduct).filter(StagedFileProduct.file_id.in_(file_ids)).all() return staged_product_data def aggregate_staged_product_data(self, staged_product_data: list[Row]) -> dict[Product, int]: product_data = {} for row in staged_product_data: product = self.db.query(Product).filter(Product.id == row.product_id).first() if product not in product_data: product_data[product] = 0 product_data[product] += row.quantity return product_data def find_product_for_box_data(self, create_box_data: dict[str, Any]) -> Product: existing_product = self.db.query(Product).filter( Product.name == create_box_data["name"], # TODO: needs complex enum Product.type == "box", Product.set_code == create_box_data["set_code"], # TODO: needs complex enum Product.set_name == create_box_data["set_name"], # TODO: needs complex enum Product.product_line == create_box_data["product_line"]).first() return existing_product def create_product_for_box(self, create_box_data: dict[str, Any]) -> Product: product = Product( id=str(uuid()), 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: 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: open_box = OpenBox( id = str(uuid()), 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: for product, quantity in product_data.items(): open_box_card = OpenBoxCard( id=str(uuid()), open_box_id=open_box.id, card_id=product.id, quantity=quantity ) self.db.add(open_box_card) def format_response(self, open_box: OpenBox = None, inventory: Inventory = None) -> CreateBoxResponse: response = CreateBoxResponse(success=True) return response def _create_box(self, create_box_data: dict[str, Any], file_ids: list[str] = None) -> CreateBoxResponse: sealed = create_box_data["sealed"] assert isinstance(sealed, bool) 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) elif file_ids and sealed: raise Exception("Cannot add cards with a sealed box") # find product with all same box data existing_product = self.find_product_for_box_data(create_box_data) if existing_product: box_product = existing_product try: with db_transaction(self.db): if not existing_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: process_staged_products = self.inventory_service.process_staged_products(product_data) self.add_products_to_open_box(open_box, product_data) # should be the file service handling this but im about to die irl # update file id status to processed for file_id in file_ids: file = self.db.query(File).filter(File.id == file_id).first() file.status = "processed" self.db.add(file) return self.format_response(open_box=open_box) elif not file_ids and sealed: # add sealed box to inventory 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 e def validate_box_type(self, box_type: str) -> bool: return box_type in ["collector", "play", "draft", "set", "commander"] def validate_set_code(self, set_code: str) -> bool: exists = self.db.query(TCGPlayerGroups).filter( TCGPlayerGroups.abbreviation == set_code ).first() is not None return exists def create_box(self, create_box_data: CreateBoxRequest) -> Box: # validate box data if not self.validate_box_type(create_box_data.type): raise Exception("Invalid box type") if not self.validate_set_code(create_box_data.set_code): raise Exception("Invalid set code") # check if box exists by type and set code or sku existing_box = self.db.query(Box).filter( or_( Box.type == create_box_data.type, Box.sku == create_box_data.sku ), Box.set_code == create_box_data.set_code ).first() if existing_box: raise Exception("Box already exists") # create box with db_transaction(self.db): box = Box( product_id=str(uuid()), 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: box = self.db.query(Box).filter(Box.product_id == box_id).first() if not box: raise Exception("Box not found") with db_transaction(self.db): if update_box_data.type: box.type = update_box_data.type if update_box_data.set_code: box.set_code = update_box_data.set_code if update_box_data.sku: box.sku = update_box_data.sku if update_box_data.num_cards_expected: box.num_cards_expected = update_box_data.num_cards_expected return box def delete_box(self, box_id: str) -> Box: box = self.db.query(Box).filter(Box.product_id == box_id).first() if not box: raise Exception("Box not found") with db_transaction(self.db): self.db.delete(box) return box def open_box(self, box_id: str, box_data: CreateOpenBoxRequest): box = self.db.query(Box).filter(Box.product_id == box_id).first() if not box: raise Exception("Box not found") with db_transaction(self.db): open_box = OpenBox( id=str(uuid()), product_id=box_id, num_cards_actual=box_data.num_cards_actual, date_opened=datetime.strptime(box_data.date_opened, "%Y-%m-%d") if box_data.date_opened else datetime.now() ) self.db.add(open_box) staged_product_data = self.get_staged_product_data(box_data.file_ids) product_data = self.aggregate_staged_product_data(staged_product_data) self.inventory_service.process_staged_products(product_data) self.add_products_to_open_box(open_box, product_data) # update box_id for files for file_id in box_data.file_ids: file = self.db.query(File).filter(File.id == file_id).first() file.box_id = open_box.id self.db.add(file) return open_box