part of pricing idk i dont remember

This commit is contained in:
2025-06-09 08:28:14 -04:00
parent 7bc64115f2
commit 77d6fd6e29
4 changed files with 130 additions and 110 deletions

View File

@ -72,58 +72,3 @@ class TCGPlayerInventoryService(BaseTCGPlayerService):
with transaction(db): with transaction(db):
# Bulk insert new data # Bulk insert new data
db.bulk_insert_mappings(TCGPlayerInventory, inventory_data) db.bulk_insert_mappings(TCGPlayerInventory, inventory_data)
async def refresh_unmanaged_tcgplayer_inventory_table(self, db: Session):
"""
Refresh the TCGPlayer unmanaged inventory table
unmanaged inventory is any inventory that cannot be mapped to a card with a marketplace listing
"""
with transaction(db):
# Get active marketplace listings with their physical items in a single query
listed_cards = (
db.query(MarketplaceListing)
.join(MarketplaceListing.inventory_item)
.join(InventoryItem.physical_item)
.filter(
func.lower(Marketplace.name) == func.lower("tcgplayer"),
MarketplaceListing.delisting_date == None,
MarketplaceListing.deleted_at == None,
MarketplaceListing.listing_date != None
)
.all()
)
# Get current inventory and create lookup dict
current_inventory = db.query(TCGPlayerInventory).all()
# Create a set of SKUs that have active listings
listed_skus = {
card.inventory_item.physical_item.tcgplayer_sku_id
for card in listed_cards
}
unmanaged_inventory = []
for inventory in current_inventory:
# Only include SKUs that have no active listings
if inventory.tcgplayer_sku_id not in listed_skus:
unmanaged_inventory.append({
"tcgplayer_inventory_id": inventory.id,
"tcgplayer_sku_id": inventory.tcgplayer_sku_id,
"product_line": inventory.product_line,
"set_name": inventory.set_name,
"product_name": inventory.product_name,
"title": inventory.title,
"number": inventory.number,
"rarity": inventory.rarity,
"condition": inventory.condition,
"tcg_market_price": inventory.tcg_market_price,
"tcg_direct_low": inventory.tcg_direct_low,
"tcg_low_price_with_shipping": inventory.tcg_low_price_with_shipping,
"tcg_low_price": inventory.tcg_low_price,
"total_quantity": inventory.total_quantity,
"add_to_quantity": inventory.add_to_quantity,
"tcg_marketplace_price": inventory.tcg_marketplace_price,
"photo_url": inventory.photo_url
})
db.bulk_insert_mappings(UnmanagedTCGPlayerInventory, unmanaged_inventory)

View File

@ -375,7 +375,7 @@ class MarketplaceListingService(BaseService[MarketplaceListing]):
async def create_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing: async def create_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing:
try: try:
with db_transaction(db): with db_transaction(db):
recommended_price = await self.pricing_service.set_price(db, inventory_item) recommended_price = await self.pricing_service.set_price_for_inventory_item(db, inventory_item)
logger.info(f"recommended_price: {recommended_price.price}") logger.info(f"recommended_price: {recommended_price.price}")
marketplace_listing = MarketplaceListing( marketplace_listing = MarketplaceListing(
inventory_item=inventory_item, inventory_item=inventory_item,
@ -392,9 +392,10 @@ class MarketplaceListingService(BaseService[MarketplaceListing]):
raise e raise e
async def update_marketplace_listing_price(self, db: Session, marketplace_listing: MarketplaceListing) -> MarketplaceListing: async def update_marketplace_listing_price(self, db: Session, marketplace_listing: MarketplaceListing) -> MarketplaceListing:
# idk if this was ever even finished so prob doesnt work idk
try: try:
with db_transaction(db): with db_transaction(db):
marketplace_listing.listed_price = self.pricing_service.set_price(marketplace_listing.inventory_item) marketplace_listing.listed_price = self.pricing_service.set_price_for_inventory_item(db, marketplace_listing.inventory_item)
db.flush() db.flush()
return marketplace_listing return marketplace_listing

View File

@ -1,8 +1,11 @@
import logging import logging
from dataclasses import dataclass
from typing import Optional
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.services.base_service import BaseService from app.services.base_service import BaseService
from app.models.inventory_management import InventoryItem, MarketplaceListing from app.models.inventory_management import InventoryItem, MarketplaceListing, PhysicalItem
from app.models.tcgplayer_inventory import TCGPlayerInventory from app.models.tcgplayer_inventory import TCGPlayerInventory
from app.models.tcgplayer_products import TCGPlayerPriceHistory, TCGPlayerProduct, MTGJSONSKU
from app.models.pricing import PricingEvent from app.models.pricing import PricingEvent
from app.db.database import transaction from app.db.database import transaction
from decimal import Decimal from decimal import Decimal
@ -11,24 +14,109 @@ from datetime import datetime
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@dataclass
class PriceData:
cost_basis: Optional[Decimal]
market_price: Optional[Decimal]
tcg_low: Optional[Decimal]
tcg_mid: Optional[Decimal]
direct_low: Optional[Decimal]
listed_price: Optional[Decimal]
quantity: int
lowest_price_for_qty: Optional[Decimal]
velocity: Optional[Decimal]
age_of_inventory: Optional[int]
class PricingService(BaseService): class PricingService(BaseService):
def __init__(self): def __init__(self):
super().__init__(None) super().__init__(None)
async def get_unmanaged_inventory(self, db: Session):
#select * from tcgplayer_inventory ti where ti.tcgplayer_sku_id not in (select pi2.tcgplayer_sku_id from marketplace_listings ml
# join inventory_items ii on ml.inventory_item_id = ii.id
# join physical_items pi2 on ii.physical_item_id = pi2.id
# where ml.delisting_date is null and ml.deleted_at is null and ii.deleted_at is null and pi2.deleted_at is null);
unmanaged_inventory = db.query(TCGPlayerInventory).filter(
TCGPlayerInventory.tcgplayer_sku_id.notin_(
db.query(MarketplaceListing.inventory_item.physical_item.tcgplayer_sku_id).join(
InventoryItem, MarketplaceListing.inventory_item_id == InventoryItem.id
).join(
PhysicalItem, InventoryItem.physical_item_id == PhysicalItem.id
).filter(
MarketplaceListing.delisting_date.is_(None),
MarketplaceListing.deleted_at.is_(None),
InventoryItem.deleted_at.is_(None),
PhysicalItem.deleted_at.is_(None)
)
)
).all()
return unmanaged_inventory
async def set_price(self, db: Session, inventory_item: InventoryItem) -> float: async def update_prices_for_unmanaged_inventory(self, db: Session):
unmanaged_inventory = await self.get_unmanaged_inventory(db)
for inventory in unmanaged_inventory:
inventory.tcg_marketplace_price = await self.set_price_for_unmanaged_inventory(db, inventory)
async def set_price_for_unmanaged_inventory(self, db: Session, inventory: TCGPlayerInventory):
# available columns
# id, tcgplayer_sku_id, product_line, set_name, product_name, title,
# number, rarity, condition, tcg_market_price, tcg_direct_low,
# tcg_low_price_with_shipping, tcg_low_price, total_quantity, add_to_quantity,
# tcg_marketplace_price, photo_url, created_at, updated_at
# get mtgjson sku
mtgjson_sku = db.query(MTGJSONSKU).filter(
MTGJSONSKU.tcgplayer_sku_id == inventory.tcgplayer_sku_id
).first()
if mtgjson_sku:
tcgplayer_product = mtgjson_sku.product
price_data = PriceData(
cost_basis=None,
market_price=Decimal(str(tcgplayer_product.most_recent_tcgplayer_price.market_price)) if tcgplayer_product.most_recent_tcgplayer_price else None,
tcg_low=Decimal(str(tcgplayer_product.most_recent_tcgplayer_price.low_price)) if tcgplayer_product.most_recent_tcgplayer_price else None,
tcg_mid=Decimal(str(tcgplayer_product.most_recent_tcgplayer_price.mid_price)) if tcgplayer_product.most_recent_tcgplayer_price else None,
direct_low=Decimal(str(tcgplayer_product.most_recent_tcgplayer_price.direct_low_price)) if tcgplayer_product.most_recent_tcgplayer_price else None,
listed_price=inventory.tcg_marketplace_price,
quantity=inventory.total_quantity,
lowest_price_for_qty=None,
velocity=None,
age_of_inventory=None
)
return await self.set_price(db, price_data)
else:
return None
async def set_price_for_inventory_item(self, db: Session, inventory_item: InventoryItem):
price_data = PriceData(
cost_basis=Decimal(str(inventory_item.cost_basis)),
market_price=Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.market_price)),
tcg_low=Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.low_price)),
tcg_mid=Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.mid_price)),
direct_low=Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.direct_low_price)),
listed_price=Decimal(str(inventory_item.marketplace_listing.listed_price)),
quantity=db.query(TCGPlayerInventory).filter(
TCGPlayerInventory.tcgplayer_sku_id == inventory_item.physical_item.tcgplayer_sku_id
).first().total_quantity if db.query(TCGPlayerInventory).filter(
TCGPlayerInventory.tcgplayer_sku_id == inventory_item.physical_item.tcgplayer_sku_id
).first() else 0,
lowest_price_for_qty=None,
velocity=None,
age_of_inventory=None
)
return await self.set_price(db, price_data, inventory_item)
async def set_price(self, db: Session, price_data: PriceData, inventory_item: InventoryItem=None):
""" """
TODO This sets listed_price per inventory_item but listed_price can only be applied to a product TODO This sets listed_price per inventory_item but listed_price can only be applied to a product
however, this may be desired on other marketplaces however, this may be desired on other marketplaces
when generating pricing file for tcgplayer, give the option to set min, max, avg price for product? when generating pricing file for tcgplayer, give the option to set min, max, avg price for product?
""" """
# Fetch base pricing data # Fetch base pricing data
cost_basis = Decimal(str(inventory_item.cost_basis)) cost_basis = price_data.cost_basis
market_price = Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.market_price)) market_price = price_data.market_price
tcg_low = Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.low_price)) tcg_low = price_data.tcg_low
tcg_mid = Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.mid_price)) tcg_mid = price_data.tcg_mid
listed_price = Decimal(str(inventory_item.marketplace_listing.listed_price)) if inventory_item.marketplace_listing else None listed_price = price_data.listed_price
logger.info(f"listed_price: {listed_price}") logger.info(f"listed_price: {listed_price}")
logger.info(f"market_price: {market_price}") logger.info(f"market_price: {market_price}")
@ -68,10 +156,7 @@ class PricingService(BaseService):
card_cost_margin_multiplier = Decimal('0.05') card_cost_margin_multiplier = Decimal('0.05')
# Fetch current total quantity in stock for SKU # Fetch current total quantity in stock for SKU
quantity_record = db.query(TCGPlayerInventory).filter( quantity_in_stock = price_data.quantity
TCGPlayerInventory.tcgplayer_sku_id == inventory_item.physical_item.tcgplayer_sku_id
).first()
quantity_in_stock = quantity_record.total_quantity if quantity_record else 0
# Determine quantity multiplier based on stock levels # Determine quantity multiplier based on stock levels
if quantity_in_stock < 4: if quantity_in_stock < 4:
@ -91,6 +176,8 @@ class PricingService(BaseService):
# limit shipping cost offset to 10% of market price # limit shipping cost offset to 10% of market price
shipping_cost_offset = min(shipping_cost / average_cards_per_order, market_price * Decimal('0.1')) shipping_cost_offset = min(shipping_cost / average_cards_per_order, market_price * Decimal('0.1'))
if cost_basis is None:
cost_basis = tcg_low * Decimal('0.65')
# Calculate base price considering cost, shipping, fees, and margin targets # Calculate base price considering cost, shipping, fees, and margin targets
base_price = (cost_basis + shipping_cost_offset) / ( base_price = (cost_basis + shipping_cost_offset) / (
(Decimal('1.0') - marketplace_fee_percentage) - adjusted_target_margin (Decimal('1.0') - marketplace_fee_percentage) - adjusted_target_margin
@ -111,10 +198,10 @@ class PricingService(BaseService):
adjusted_price = tcg_mid adjusted_price = tcg_mid
price_used = "tcg mid" price_used = "tcg mid"
price_reason = "adjusted price below tcg low" price_reason = "adjusted price below tcg low"
elif adjusted_price > tcg_low and adjusted_price < (market_price * Decimal('0.8')) and adjusted_price < (tcg_mid * Decimal('0.8')): elif adjusted_price > tcg_low and adjusted_price < (tcg_mid * Decimal('0.8')):
adjusted_price = tcg_mid adjusted_price = tcg_mid
price_used = "tcg mid" price_used = "tcg mid"
price_reason = f"adjusted price below 80% of market price and tcg mid" price_reason = f"adjusted price below 80% of tcg mid"
else: else:
price_used = "adjusted price" price_used = "adjusted price"
price_reason = "valid price assigned based on margin targets" price_reason = "valid price assigned based on margin targets"
@ -142,41 +229,22 @@ class PricingService(BaseService):
price_reason = "adjusted price below price drop threshold" price_reason = "adjusted price below price drop threshold"
# Record pricing event in database transaction # Record pricing event in database transaction
with transaction(db): if inventory_item:
pricing_event = PricingEvent( with transaction(db):
inventory_item_id=inventory_item.id, pricing_event = PricingEvent(
price=float(adjusted_price), inventory_item_id=inventory_item.id,
price_used=price_used, price=float(adjusted_price),
price_reason=price_reason, price_used=price_used,
free_shipping_adjustment=free_shipping_adjustment price_reason=price_reason,
) free_shipping_adjustment=free_shipping_adjustment
db.add(pricing_event) )
db.add(pricing_event)
# delete previous pricing events for inventory item
if inventory_item.marketplace_listing and inventory_item.marketplace_listing.listed_price:
inventory_item.marketplace_listing.listed_price.deleted_at = datetime.now()
return pricing_event
def set_price_for_unmanaged_inventory(self, db: Session, tcgplayer_sku_id: int, quantity: int) -> float:
pass
def update_price_for_product(self, db: Session, tcgplayer_sku_id: int) -> list[PricingEvent]:
# get inventory items for sku
updated_prices = []
inventory_items = db.query(InventoryItem).filter(
InventoryItem.physical_item.tcgplayer_sku_id == tcgplayer_sku_id
).all()
for inventory_item in inventory_items:
pricing_event = self.set_price(db, inventory_item)
updated_prices.append(pricing_event)
return updated_prices
def set_price_for_product(self, db: Session, tcgplayer_sku_id: int) -> float:
# update price for all inventory items for sku
prices = self.update_price_for_product(db, tcgplayer_sku_id)
sum_prices = sum(price.price for price in prices)
average_price = sum_prices / len(prices)
return average_price
# delete previous pricing events for inventory item
if inventory_item.marketplace_listing and inventory_item.marketplace_listing.listed_price:
inventory_item.marketplace_listing.listed_price.deleted_at = datetime.now()
return pricing_event
else:
return adjusted_price
# BAD BAD BAD FIX PLS TODO

View File

@ -4,6 +4,7 @@ from app.services.base_service import BaseService
from sqlalchemy import text from sqlalchemy import text
import logging import logging
from app.models.tcgplayer_inventory import UnmanagedTCGPlayerInventory, TCGPlayerInventory from app.models.tcgplayer_inventory import UnmanagedTCGPlayerInventory, TCGPlayerInventory
from datetime import datetime
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SchedulerService(BaseService): class SchedulerService(BaseService):
@ -67,7 +68,6 @@ class SchedulerService(BaseService):
db.flush() db.flush()
await tcgplayer_inventory_service.refresh_tcgplayer_inventory_table(db) await tcgplayer_inventory_service.refresh_tcgplayer_inventory_table(db)
db.flush() db.flush()
await tcgplayer_inventory_service.refresh_unmanaged_tcgplayer_inventory_table(db)
async def start_scheduled_tasks(self, db): async def start_scheduled_tasks(self, db):
"""Start all scheduled tasks""" """Start all scheduled tasks"""
@ -93,6 +93,12 @@ class SchedulerService(BaseService):
db=db db=db
) )
# Run initial inventory refresh on startup if inventory update was not run today
# get last inventory update date
last_inventory_update = db.query(TCGPlayerInventory).order_by(TCGPlayerInventory.created_at.desc()).first()
if last_inventory_update is None or last_inventory_update.created_at.date() != datetime.now().date():
await self.refresh_tcgplayer_inventory_table(db)
self.scheduler.start() self.scheduler.start()
logger.info("All scheduled tasks started") logger.info("All scheduled tasks started")