Squashed commit of the following:
commit893b229cc6
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 22:14:08 2025 -0500 j commit06f539aea2
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 21:55:30 2025 -0500 fk commitd0c2960ec9
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 21:50:53 2025 -0500 frick commit6b1362c166
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 21:49:40 2025 -0500 database commit8cadc6df4c
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 21:38:09 2025 -0500 asdf commit1ca6f98684
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 21:32:50 2025 -0500 fffff commit8bb337a9c3
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 21:31:13 2025 -0500 ffff commit65aba280c5
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 21:26:16 2025 -0500 aa commit59ef03a59e
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 21:24:21 2025 -0500 asdf commitf44d5740fc
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 21:23:32 2025 -0500 aaa commit13c96b1643
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 21:18:54 2025 -0500 sdf commit949c795fd1
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 21:17:53 2025 -0500 asdf commit8c3cd423fe
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 20:56:01 2025 -0500 app2 commit78eafc739e
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 20:54:55 2025 -0500 app commitdc47eced14
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 20:43:15 2025 -0500 asdfasdfasdf commite24bcae88c
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 20:39:44 2025 -0500 a commitc894451bfe
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 20:38:20 2025 -0500 req commit3d09869562
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 20:33:27 2025 -0500 wrong number = code dont work lol i love computers commit4c93a1271b
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 20:29:39 2025 -0500 q commit1f5361da88
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 18:27:20 2025 -0500 same as original code now -5 days of my life commit511b070cbb
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 13:52:28 2025 -0500 pricey worky commit964fdd641b
Author: zman <joshua.k.rzemien@gmail.com> Date: Fri Feb 7 11:37:29 2025 -0500 prep for pricing service work commita78c3bcba3
Author: zman <joshua.k.rzemien@gmail.com> Date: Wed Feb 5 21:51:22 2025 -0500 more stuff yay commitbd9cfca7a9
Author: zman <joshua.k.rzemien@gmail.com> Date: Tue Feb 4 22:30:33 2025 -0500 GIGA FIXED EVERYTHING OMG commit85510a4671
Author: zman <joshua.k.rzemien@gmail.com> Date: Tue Feb 4 00:01:34 2025 -0500 data model change and some new services
This commit is contained in:
202
app/services/box.py
Normal file
202
app/services/box.py
Normal file
@@ -0,0 +1,202 @@
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
from uuid import uuid4
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
import logging
|
||||
|
||||
from app.db.models import (
|
||||
Box,
|
||||
File,
|
||||
StagedFileProduct,
|
||||
Product,
|
||||
OpenBoxCard,
|
||||
OpenBox,
|
||||
TCGPlayerGroups,
|
||||
Inventory
|
||||
)
|
||||
from app.db.utils import db_transaction
|
||||
from app.schemas.box import CreateBoxRequest, UpdateBoxRequest, CreateOpenBoxRequest
|
||||
from app.services.inventory import InventoryService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VALID_BOX_TYPES = {"collector", "play", "draft", "set", "commander"}
|
||||
|
||||
class BoxService:
|
||||
def __init__(self, db: Session, inventory_service: InventoryService):
|
||||
self.db = db
|
||||
self.inventory_service = inventory_service
|
||||
|
||||
def get_staged_product_data(self, file_ids: List[str]) -> List[StagedFileProduct]:
|
||||
"""Retrieve staged product data for given file IDs."""
|
||||
return self.db.query(StagedFileProduct).filter(
|
||||
StagedFileProduct.file_id.in_(file_ids)
|
||||
).all()
|
||||
|
||||
def aggregate_staged_product_data(self, staged_product_data: List[StagedFileProduct]) -> Dict[Product, int]:
|
||||
"""Aggregate staged product data by product and quantity."""
|
||||
product_data = {}
|
||||
for row in staged_product_data:
|
||||
product = self.db.query(Product).filter(Product.id == row.product_id).first()
|
||||
if product:
|
||||
product_data[product] = product_data.get(product, 0) + row.quantity
|
||||
return product_data
|
||||
|
||||
def add_products_to_open_box(self, open_box: OpenBox, product_data: Dict[Product, int]) -> None:
|
||||
"""Add products to an open box."""
|
||||
for product, quantity in product_data.items():
|
||||
open_box_card = OpenBoxCard(
|
||||
id=str(uuid4()),
|
||||
open_box_id=open_box.id,
|
||||
card_id=product.id,
|
||||
quantity=quantity
|
||||
)
|
||||
self.db.add(open_box_card)
|
||||
|
||||
def validate_box_type(self, box_type: str) -> bool:
|
||||
"""Validate if the box type is supported."""
|
||||
return box_type in VALID_BOX_TYPES
|
||||
|
||||
def validate_set_code(self, set_code: str) -> bool:
|
||||
"""Validate if the set code exists in TCGPlayer groups."""
|
||||
return self.db.query(TCGPlayerGroups).filter(
|
||||
TCGPlayerGroups.abbreviation == set_code
|
||||
).first() is not None
|
||||
|
||||
def create_box(self, create_box_data: CreateBoxRequest) -> Box:
|
||||
"""Create a new box."""
|
||||
if not self.validate_box_type(create_box_data.type):
|
||||
raise ValueError("Invalid box type")
|
||||
if not self.validate_set_code(create_box_data.set_code):
|
||||
raise ValueError("Invalid set code")
|
||||
|
||||
existing_box = self.db.query(Box).filter(
|
||||
Box.type == create_box_data.type,
|
||||
Box.set_code == create_box_data.set_code,
|
||||
or_(Box.sku == create_box_data.sku, Box.sku.is_(None))
|
||||
).first()
|
||||
|
||||
if existing_box:
|
||||
return existing_box, False
|
||||
else:
|
||||
with db_transaction(self.db):
|
||||
product = Product(
|
||||
id=str(uuid4()),
|
||||
type='box',
|
||||
product_line='mtg'
|
||||
)
|
||||
box = Box(
|
||||
product_id=product.id,
|
||||
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(product)
|
||||
self.db.add(box)
|
||||
|
||||
return box, True
|
||||
|
||||
def update_box(self, box_id: str, update_box_data: UpdateBoxRequest) -> Box:
|
||||
"""Update an existing box."""
|
||||
box = self.db.query(Box).filter(Box.product_id == box_id).first()
|
||||
if not box:
|
||||
raise ValueError("Box not found")
|
||||
|
||||
update_data = update_box_data.dict(exclude_unset=True)
|
||||
|
||||
# Validate box type if it's being updated
|
||||
if "type" in update_data and update_data["type"] is not None:
|
||||
if not self.validate_box_type(update_data["type"]):
|
||||
raise ValueError(f"Invalid box type: {update_data['type']}")
|
||||
|
||||
# Validate set code if it's being updated
|
||||
if "set_code" in update_data and update_data["set_code"] is not None:
|
||||
if not self.validate_set_code(update_data["set_code"]):
|
||||
raise ValueError(f"Invalid set code: {update_data['set_code']}")
|
||||
|
||||
with db_transaction(self.db):
|
||||
for field, value in update_data.items():
|
||||
if value is not None: # Only update non-None values
|
||||
setattr(box, field, value)
|
||||
|
||||
return box
|
||||
|
||||
def delete_box(self, box_id: str) -> Box:
|
||||
"""Delete a box."""
|
||||
box = self.db.query(Box).filter(Box.product_id == box_id).first()
|
||||
product = self.db.query(Product).filter(Product.id == box_id).first()
|
||||
if not box:
|
||||
raise ValueError("Box not found")
|
||||
|
||||
with db_transaction(self.db):
|
||||
self.db.delete(box)
|
||||
self.db.delete(product)
|
||||
return box
|
||||
|
||||
def open_box(self, box_id: str, box_data: CreateOpenBoxRequest) -> OpenBox:
|
||||
"""Open a box and process its contents."""
|
||||
box = self.db.query(Box).filter(Box.product_id == box_id).first()
|
||||
if not box:
|
||||
raise ValueError("Box not found")
|
||||
|
||||
with db_transaction(self.db):
|
||||
open_box = OpenBox(
|
||||
id=str(uuid4()),
|
||||
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 file box IDs
|
||||
self.db.query(File).filter(File.id.in_(box_data.file_ids)).update(
|
||||
{"box_id": open_box.id}, synchronize_session=False
|
||||
)
|
||||
|
||||
return open_box
|
||||
|
||||
def delete_open_box(self, box_id: str) -> OpenBox:
|
||||
# Fetch open box and related cards in one query
|
||||
open_box = (
|
||||
self.db.query(OpenBox)
|
||||
.filter(OpenBox.id == box_id)
|
||||
.first()
|
||||
)
|
||||
if not open_box:
|
||||
raise ValueError("Open box not found")
|
||||
|
||||
# Get all open box cards and related inventory items in one query
|
||||
open_box_cards = (
|
||||
self.db.query(OpenBoxCard, Inventory)
|
||||
.join(
|
||||
Inventory,
|
||||
OpenBoxCard.card_id == Inventory.product_id
|
||||
)
|
||||
.filter(OpenBoxCard.open_box_id == open_box.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Process inventory adjustments
|
||||
for open_box_card, inventory_item in open_box_cards:
|
||||
if open_box_card.quantity > inventory_item.quantity:
|
||||
raise ValueError("Open box quantity exceeds inventory quantity")
|
||||
|
||||
inventory_item.quantity -= open_box_card.quantity
|
||||
if inventory_item.quantity == 0:
|
||||
self.db.delete(inventory_item)
|
||||
|
||||
# Delete the open box card
|
||||
self.db.delete(open_box_card)
|
||||
|
||||
# Execute all database operations in a single transaction
|
||||
with db_transaction(self.db):
|
||||
self.db.delete(open_box)
|
||||
|
||||
return open_box
|
Reference in New Issue
Block a user