PRICING
This commit is contained in:
@@ -4,8 +4,8 @@ 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
|
||||
OpenEvent, Card, InventoryItem, Case,
|
||||
Transaction, TransactionItem, Customer, Vendor, Marketplace, Box, MarketplaceListing
|
||||
)
|
||||
from app.schemas.file import FileInDB
|
||||
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, TransactionResponse
|
||||
@@ -19,92 +19,6 @@ 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,
|
||||
@@ -125,31 +39,31 @@ class InventoryService(BaseService):
|
||||
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()
|
||||
transaction_notes=transaction_data.transaction_notes
|
||||
)
|
||||
db.add(transaction)
|
||||
db.flush()
|
||||
|
||||
total_amount = 0
|
||||
physical_items = []
|
||||
case_service = self.get_service("case")
|
||||
box_service = self.get_service("box")
|
||||
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(
|
||||
physical_item = await case_service.create_case(
|
||||
db=db,
|
||||
product_id=item.product_id,
|
||||
cost_basis=item.unit_price,
|
||||
num_boxes=item.num_boxes or 1
|
||||
num_boxes=item.num_boxes
|
||||
)
|
||||
physical_items.append(physical_item)
|
||||
else:
|
||||
for i in range(item.quantity):
|
||||
physical_item = await SealedBoxService().create_sealed_box(
|
||||
physical_item = await box_service.create_box(
|
||||
db=db,
|
||||
product_id=item.product_id,
|
||||
cost_basis=item.unit_price
|
||||
@@ -158,73 +72,10 @@ class InventoryService(BaseService):
|
||||
|
||||
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()
|
||||
transaction.transaction_items.append(TransactionItem(
|
||||
inventory_item_id=physical_item.inventory_item.id,
|
||||
unit_price=item.unit_price
|
||||
))
|
||||
|
||||
# Update transaction total
|
||||
transaction.transaction_total_amount = total_amount
|
||||
@@ -246,9 +97,7 @@ class InventoryService(BaseService):
|
||||
|
||||
with db_transaction(db):
|
||||
customer = Customer(
|
||||
name=customer_name,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now()
|
||||
name=customer_name
|
||||
)
|
||||
db.add(customer)
|
||||
db.flush()
|
||||
@@ -270,9 +119,7 @@ class InventoryService(BaseService):
|
||||
|
||||
with db_transaction(db):
|
||||
vendor = Vendor(
|
||||
name=vendor_name,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now()
|
||||
name=vendor_name
|
||||
)
|
||||
db.add(vendor)
|
||||
db.flush()
|
||||
@@ -294,9 +141,7 @@ class InventoryService(BaseService):
|
||||
|
||||
with db_transaction(db):
|
||||
marketplace = Marketplace(
|
||||
name=marketplace_name,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now()
|
||||
name=marketplace_name
|
||||
)
|
||||
db.add(marketplace)
|
||||
db.flush()
|
||||
@@ -305,110 +150,144 @@ class InventoryService(BaseService):
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
class SealedBoxService(BaseService[SealedBox]):
|
||||
class BoxService(BaseService[Box]):
|
||||
def __init__(self):
|
||||
super().__init__(SealedBox)
|
||||
super().__init__(Box)
|
||||
|
||||
async def create_sealed_box(
|
||||
async def create_box(
|
||||
self,
|
||||
db: Session,
|
||||
product_id: int,
|
||||
cost_basis: float,
|
||||
case_id: Optional[int] = None
|
||||
) -> SealedBox:
|
||||
cost_basis: float
|
||||
) -> Box:
|
||||
try:
|
||||
with db_transaction(db):
|
||||
# Create the SealedBox
|
||||
sealed_box = SealedBox(
|
||||
product_id=product_id,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now()
|
||||
box = Box(
|
||||
tcgplayer_product_id=product_id
|
||||
)
|
||||
db.add(sealed_box)
|
||||
db.add(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
|
||||
expected_value = box.products.sealed_expected_value.expected_value
|
||||
box.expected_value = expected_value
|
||||
db.flush()
|
||||
|
||||
# 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()
|
||||
physical_item=box,
|
||||
cost_basis=cost_basis
|
||||
)
|
||||
db.add(inventory_item)
|
||||
|
||||
return sealed_box
|
||||
return box
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
class SealedCaseService(BaseService[SealedCase]):
|
||||
def __init__(self):
|
||||
super().__init__(SealedCase)
|
||||
async def calculate_cost_basis_for_opened_cards(self, db: Session, open_event: OpenEvent) -> float:
|
||||
box_cost_basis = open_event.source_item.inventory_item.cost_basis
|
||||
box_expected_value = open_event.source_item.products.sealed_expected_value.expected_value
|
||||
for resulting_card in open_event.resulting_items:
|
||||
# ensure card
|
||||
if resulting_card.item_type != "card":
|
||||
raise ValueError(f"Expected card, got {resulting_card.item_type}")
|
||||
resulting_card_market_value = resulting_card.products.most_recent_tcgplayer_price.market_price
|
||||
resulting_card_cost_basis = (resulting_card_market_value / box_expected_value) * box_cost_basis
|
||||
resulting_card.inventory_item.cost_basis = resulting_card_cost_basis
|
||||
db.flush()
|
||||
|
||||
async def open_box(self, db: Session, box: Box, manabox_file_uploads: List[FileInDB]) -> bool:
|
||||
with db_transaction(db):
|
||||
# create open event
|
||||
open_event = OpenEvent(
|
||||
source_item=box,
|
||||
open_date=datetime.now()
|
||||
)
|
||||
db.add(open_event)
|
||||
db.flush()
|
||||
|
||||
async def create_sealed_case(self, db: Session, product_id: int, cost_basis: float, num_boxes: int) -> SealedCase:
|
||||
manabox_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_upload_ids)).all()
|
||||
for record in staging_data:
|
||||
for i in range(record.quantity):
|
||||
open_card = Card(
|
||||
tcgplayer_product_id=record.tcgplayer_product_id,
|
||||
tcgplayer_sku_id=record.tcgplayer_sku_id
|
||||
)
|
||||
open_event.resulting_items.append(open_card)
|
||||
|
||||
inventory_item = InventoryItem(
|
||||
physical_item=open_card,
|
||||
cost_basis=0
|
||||
)
|
||||
db.add(inventory_item)
|
||||
db.flush()
|
||||
|
||||
# calculate cost basis for opened cards
|
||||
await self.calculate_cost_basis_for_opened_cards(db, open_event)
|
||||
|
||||
|
||||
|
||||
return open_event
|
||||
|
||||
|
||||
|
||||
class CaseService(BaseService[Case]):
|
||||
def __init__(self):
|
||||
super().__init__(Case)
|
||||
|
||||
async def create_case(self, db: Session, product_id: int, cost_basis: float, num_boxes: int) -> Case:
|
||||
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()
|
||||
case = Case(
|
||||
tcgplayer_product_id=product_id,
|
||||
num_boxes=num_boxes
|
||||
)
|
||||
db.add(sealed_case)
|
||||
db.add(case)
|
||||
db.flush() # Get the ID for relationships
|
||||
case.expected_value = case.products.sealed_expected_value.expected_value
|
||||
|
||||
# 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()
|
||||
physical_item=case,
|
||||
cost_basis=cost_basis
|
||||
)
|
||||
db.add(inventory_item)
|
||||
|
||||
return sealed_case
|
||||
return case
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
async def open_sealed_case(self, db: Session, sealed_case: SealedCase) -> bool:
|
||||
async def open_case(self, db: Session, case: Case, child_product_id: int) -> bool:
|
||||
try:
|
||||
sealed_case_context = InventoryItemContextFactory(db).get_context(sealed_case.inventory_item)
|
||||
## TODO should be able to import a manabox file with a case
|
||||
## cost basis will be able to flow down to the card accurately
|
||||
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()
|
||||
source_item=case,
|
||||
open_date=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()
|
||||
for i in range(case.num_boxes):
|
||||
new_box = Box(
|
||||
tcgplayer_product_id=child_product_id
|
||||
)
|
||||
db.add(sealed_box)
|
||||
db.flush() # Get the ID for relationships
|
||||
open_event.resulting_items.append(new_box)
|
||||
db.flush()
|
||||
|
||||
per_box_cost_basis = case.inventory_item.cost_basis / case.num_boxes
|
||||
|
||||
# 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()
|
||||
physical_item=new_box,
|
||||
cost_basis=per_box_cost_basis
|
||||
)
|
||||
db.add(inventory_item)
|
||||
|
||||
@@ -416,3 +295,37 @@ class SealedCaseService(BaseService[SealedCase]):
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
class MarketplaceListingService(BaseService[MarketplaceListing]):
|
||||
def __init__(self):
|
||||
super().__init__(MarketplaceListing)
|
||||
self.pricing_service = self.service_manager.get_service("pricing")
|
||||
|
||||
async def create_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing:
|
||||
try:
|
||||
with db_transaction(db):
|
||||
recommended_price = await self.pricing_service.set_price(db, inventory_item)
|
||||
logger.info(f"recommended_price: {recommended_price.price}")
|
||||
marketplace_listing = MarketplaceListing(
|
||||
inventory_item=inventory_item,
|
||||
marketplace=marketplace,
|
||||
recommended_price=recommended_price,
|
||||
listing_date=None,
|
||||
delisting_date=None
|
||||
)
|
||||
db.add(marketplace_listing)
|
||||
db.flush()
|
||||
return marketplace_listing
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
async def update_marketplace_listing_price(self, db: Session, marketplace_listing: MarketplaceListing) -> MarketplaceListing:
|
||||
try:
|
||||
with db_transaction(db):
|
||||
marketplace_listing.listed_price = self.pricing_service.set_price(marketplace_listing.inventory_item)
|
||||
db.flush()
|
||||
return marketplace_listing
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
Reference in New Issue
Block a user