PRICING
This commit is contained in:
155
app/services/pricing_service.py
Normal file
155
app/services/pricing_service.py
Normal file
@ -0,0 +1,155 @@
|
||||
import logging
|
||||
from sqlalchemy.orm import Session
|
||||
from app.services.base_service import BaseService
|
||||
from app.models.inventory_management import InventoryItem
|
||||
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
||||
from app.models.pricing import PricingEvent
|
||||
from app.db.database import transaction
|
||||
from decimal import Decimal
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PricingService(BaseService):
|
||||
def __init__(self):
|
||||
super().__init__(None)
|
||||
|
||||
|
||||
async def set_price(self, db: Session, inventory_item: InventoryItem) -> float:
|
||||
"""
|
||||
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
|
||||
when generating pricing file for tcgplayer, give the option to set min, max, avg price for product?
|
||||
"""
|
||||
# Fetch base pricing data
|
||||
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))
|
||||
listed_price = Decimal(str(inventory_item.marketplace_listing.listed_price)) if inventory_item.marketplace_listing else None
|
||||
|
||||
logger.info(f"listed_price: {listed_price}")
|
||||
logger.info(f"market_price: {market_price}")
|
||||
logger.info(f"tcg_low: {tcg_low}")
|
||||
logger.info(f"tcg_mid: {tcg_mid}")
|
||||
logger.info(f"cost_basis: {cost_basis}")
|
||||
|
||||
# TODO: Add logic to fetch lowest price for seller with same quantity in stock
|
||||
# NOT IMPLEMENTED YET
|
||||
lowest_price_for_quantity = Decimal('0.0')
|
||||
|
||||
# Hardcoded configuration values (should be parameterized later)
|
||||
shipping_cost = Decimal('1.0')
|
||||
tcgplayer_shipping_fee = Decimal('1.31')
|
||||
average_cards_per_order = Decimal('3.0')
|
||||
marketplace_fee_percentage = Decimal('0.20')
|
||||
target_margin = Decimal('0.10')
|
||||
velocity_multiplier = Decimal('0.0')
|
||||
global_margin_multiplier = Decimal('0.00')
|
||||
min_floor_price = Decimal('0.25')
|
||||
price_drop_threshold = Decimal('0.20')
|
||||
# TODO add age of inventory price decrease multiplier
|
||||
age_of_inventory_multiplier = Decimal('0.0')
|
||||
|
||||
# card cost margin multiplier
|
||||
if market_price > 0 and market_price < 2:
|
||||
card_cost_margin_multiplier = Decimal('-0.075')
|
||||
elif market_price >= 2 and market_price < 10:
|
||||
card_cost_margin_multiplier = Decimal('-0.025')
|
||||
elif market_price >= 10 and market_price < 30:
|
||||
card_cost_margin_multiplier = Decimal('0.025')
|
||||
elif market_price >= 30 and market_price < 50:
|
||||
card_cost_margin_multiplier = Decimal('0.05')
|
||||
elif market_price >= 50 and market_price < 100:
|
||||
card_cost_margin_multiplier = Decimal('0.075')
|
||||
elif market_price >= 100 and market_price < 200:
|
||||
card_cost_margin_multiplier = Decimal('0.10')
|
||||
|
||||
# Fetch current total quantity in stock for SKU
|
||||
quantity_record = db.query(TCGPlayerInventory).filter(
|
||||
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
|
||||
if quantity_in_stock < 4:
|
||||
quantity_multiplier = Decimal('0.0')
|
||||
elif quantity_in_stock == 4:
|
||||
quantity_multiplier = Decimal('0.1')
|
||||
elif 5 <= quantity_in_stock < 10:
|
||||
quantity_multiplier = Decimal('0.2')
|
||||
elif quantity_in_stock >= 10:
|
||||
quantity_multiplier = Decimal('0.3')
|
||||
else:
|
||||
quantity_multiplier = Decimal('0.0')
|
||||
|
||||
# Calculate adjusted target margin from base and global multipliers
|
||||
adjusted_target_margin = target_margin + global_margin_multiplier + card_cost_margin_multiplier
|
||||
|
||||
# limit shipping cost offset to 10% of market price
|
||||
shipping_cost_offset = min(shipping_cost / average_cards_per_order, market_price * Decimal('0.1'))
|
||||
|
||||
# Calculate base price considering cost, shipping, fees, and margin targets
|
||||
base_price = (cost_basis + shipping_cost_offset) / (
|
||||
(Decimal('1.0') - marketplace_fee_percentage) - adjusted_target_margin
|
||||
)
|
||||
|
||||
# Adjust base price by quantity and velocity multipliers, limit markup to amount of shipping fee
|
||||
adjusted_price = min(
|
||||
base_price * (Decimal('1.0') + quantity_multiplier + velocity_multiplier - age_of_inventory_multiplier),
|
||||
base_price + tcgplayer_shipping_fee
|
||||
)
|
||||
|
||||
# Enforce minimum floor price to ensure profitability
|
||||
if adjusted_price < min_floor_price:
|
||||
adjusted_price = min_floor_price
|
||||
|
||||
# Adjust price based on market prices (TCG low and TCG mid)
|
||||
if adjusted_price < tcg_low:
|
||||
adjusted_price = tcg_mid
|
||||
price_used = "tcg mid"
|
||||
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')):
|
||||
adjusted_price = tcg_mid
|
||||
price_used = "tcg mid"
|
||||
price_reason = f"adjusted price below 80% of market price and tcg mid"
|
||||
else:
|
||||
price_used = "adjusted price"
|
||||
price_reason = "valid price assigned based on margin targets"
|
||||
|
||||
# TODO: Add logic to adjust price to beat competitor price with same quantity
|
||||
# NOT IMPLEMENTED YET
|
||||
if adjusted_price < lowest_price_for_quantity:
|
||||
adjusted_price = lowest_price_for_quantity - Decimal('0.01')
|
||||
price_used = "lowest price for quantity"
|
||||
price_reason = "adjusted price below lowest price for quantity"
|
||||
|
||||
# Fine-tune price to optimize for free shipping promotions
|
||||
free_shipping_adjustment = False
|
||||
for x in range(1, 5):
|
||||
quantity = Decimal(str(x))
|
||||
if Decimal('5.00') <= adjusted_price * quantity <= Decimal('5.05'):
|
||||
adjusted_price = Decimal('4.99') / quantity
|
||||
free_shipping_adjustment = True
|
||||
break
|
||||
|
||||
# prevent price drop over price drop threshold
|
||||
if listed_price and adjusted_price < (listed_price * (1 - price_drop_threshold)):
|
||||
adjusted_price = listed_price
|
||||
price_used = "listed price"
|
||||
price_reason = "adjusted price below price drop threshold"
|
||||
|
||||
# Record pricing event in database transaction
|
||||
with transaction(db):
|
||||
pricing_event = PricingEvent(
|
||||
inventory_item_id=inventory_item.id,
|
||||
price=float(adjusted_price),
|
||||
price_used=price_used,
|
||||
price_reason=price_reason,
|
||||
free_shipping_adjustment=free_shipping_adjustment
|
||||
)
|
||||
db.add(pricing_event)
|
||||
|
||||
return pricing_event
|
||||
|
||||
def set_price_for_unmanaged_inventory(self, db: Session, tcgplayer_sku_id: int, quantity: int) -> float:
|
||||
pass
|
Reference in New Issue
Block a user