update pricing i guess
This commit is contained in:
@ -59,11 +59,11 @@ async def lifespan(app: FastAPI):
|
|||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
#data_init_service = service_manager.get_service('data_initialization')
|
#data_init_service = service_manager.get_service('data_initialization')
|
||||||
#data_init = await data_init_service.initialize_data(db, game_ids=[1], use_cache=False, init_categories=True, init_products=True, init_groups=True, init_archived_prices=True, init_mtgjson=False, archived_prices_start_date="2025-05-22", archived_prices_end_date="2025-05-23")
|
#data_init = await data_init_service.initialize_data(db, game_ids=[1], use_cache=False, init_categories=True, init_products=True, init_groups=True, init_archived_prices=True, init_mtgjson=False, archived_prices_start_date="2025-06-07", archived_prices_end_date="2025-06-08")
|
||||||
#logger.info(f"Data initialization results: {data_init}")
|
#logger.info(f"Data initialization results: {data_init}")
|
||||||
# Update most recent prices
|
# Update most recent prices
|
||||||
#MostRecentTCGPlayerPrice.update_most_recent_prices(db)
|
#MostRecentTCGPlayerPrice.update_most_recent_prices(db)
|
||||||
logger.info("Most recent prices updated successfully")
|
#logger.info("Most recent prices updated successfully")
|
||||||
|
|
||||||
# Create default customer, vendor, and marketplace
|
# Create default customer, vendor, and marketplace
|
||||||
#inv_data_init = await data_init_service.initialize_inventory_data(db)
|
#inv_data_init = await data_init_service.initialize_inventory_data(db)
|
||||||
|
@ -485,4 +485,20 @@ async def confirm_listings(
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get("/tcgplayer/update-file")
|
||||||
|
async def get_tcgplayer_update_file(
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
marketplace_listing_service = service_manager.get_service("marketplace_listing")
|
||||||
|
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
|
||||||
|
csv_string = await marketplace_listing_service.create_tcgplayer_update_file(db, marketplace)
|
||||||
|
return StreamingResponse(
|
||||||
|
iter([csv_string]),
|
||||||
|
media_type="text/csv",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f"attachment; filename=tcgplayer_update_file_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv"
|
||||||
|
}
|
||||||
|
)
|
@ -504,3 +504,62 @@ class MarketplaceListingService(BaseService[MarketplaceListing]):
|
|||||||
data_rows = [",".join([escape_csv_value(data[tcgplayer_id][header]) for header in headers]) for tcgplayer_id in data]
|
data_rows = [",".join([escape_csv_value(data[tcgplayer_id][header]) for header in headers]) for tcgplayer_id in data]
|
||||||
csv_data = "\n".join([header_row] + data_rows)
|
csv_data = "\n".join([header_row] + data_rows)
|
||||||
return csv_data
|
return csv_data
|
||||||
|
|
||||||
|
async def create_tcgplayer_update_file(self, db: Session, marketplace: Marketplace=None) -> str:
|
||||||
|
# TCGplayer 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
|
||||||
|
headers = [
|
||||||
|
"TCGplayer 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"
|
||||||
|
]
|
||||||
|
unmanaged_inventory = await self.pricing_service.update_prices_for_unmanaged_inventory(db)
|
||||||
|
managed_inventory = await self.pricing_service.update_prices_for_managed_inventory(db)
|
||||||
|
# combine and convert to csv
|
||||||
|
inventory = unmanaged_inventory + managed_inventory
|
||||||
|
data = {}
|
||||||
|
for inventory_item in inventory:
|
||||||
|
data[inventory_item.tcgplayer_sku_id] = {
|
||||||
|
"TCGplayer Id": inventory_item.tcgplayer_sku_id,
|
||||||
|
"Product Line": inventory_item.product_line,
|
||||||
|
"Set Name": inventory_item.set_name,
|
||||||
|
"Product Name": inventory_item.product_name,
|
||||||
|
"Title": inventory_item.title,
|
||||||
|
"Number": inventory_item.number,
|
||||||
|
"Rarity": inventory_item.rarity,
|
||||||
|
"Condition": inventory_item.condition,
|
||||||
|
"TCG Market Price": inventory_item.tcg_market_price,
|
||||||
|
"TCG Direct Low": inventory_item.tcg_direct_low,
|
||||||
|
"TCG Low Price With Shipping": inventory_item.tcg_low_price_with_shipping,
|
||||||
|
"TCG Low Price": inventory_item.tcg_low_price,
|
||||||
|
"Total Quantity": "",
|
||||||
|
"Add to Quantity": "0",
|
||||||
|
"TCG Marketplace Price": f"{Decimal(inventory_item.tcg_marketplace_price):.2f}",
|
||||||
|
"Photo URL": ""
|
||||||
|
}
|
||||||
|
# format data into csv
|
||||||
|
# header
|
||||||
|
header_row = ",".join(headers)
|
||||||
|
# data
|
||||||
|
def escape_csv_value(value):
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
value = str(value)
|
||||||
|
if any(c in value for c in [',', '"', '\n']):
|
||||||
|
return f'"{value.replace('"', '""')}"'
|
||||||
|
return value
|
||||||
|
data_rows = [",".join([escape_csv_value(data[tcgplayer_id][header]) for header in headers]) for tcgplayer_id in data]
|
||||||
|
csv_data = "\n".join([header_row] + data_rows)
|
||||||
|
return csv_data
|
@ -1,7 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional, Dict, List
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import select
|
||||||
from app.services.base_service import BaseService
|
from app.services.base_service import BaseService
|
||||||
from app.models.inventory_management import InventoryItem, MarketplaceListing, PhysicalItem
|
from app.models.inventory_management import InventoryItem, MarketplaceListing, PhysicalItem
|
||||||
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
||||||
@ -33,67 +34,136 @@ class PricingService(BaseService):
|
|||||||
super().__init__(None)
|
super().__init__(None)
|
||||||
|
|
||||||
async def get_unmanaged_inventory(self, db: Session):
|
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(
|
unmanaged_inventory = db.query(TCGPlayerInventory).filter(
|
||||||
TCGPlayerInventory.tcgplayer_sku_id.notin_(
|
TCGPlayerInventory.tcgplayer_sku_id.notin_(
|
||||||
db.query(MarketplaceListing.inventory_item.physical_item.tcgplayer_sku_id).join(
|
db.query(PhysicalItem.tcgplayer_sku_id).join(
|
||||||
InventoryItem, MarketplaceListing.inventory_item_id == InventoryItem.id
|
InventoryItem, PhysicalItem.id == InventoryItem.physical_item_id
|
||||||
).join(
|
).join(
|
||||||
PhysicalItem, InventoryItem.physical_item_id == PhysicalItem.id
|
MarketplaceListing, InventoryItem.id == MarketplaceListing.inventory_item_id
|
||||||
).filter(
|
).filter(
|
||||||
MarketplaceListing.delisting_date.is_(None),
|
MarketplaceListing.delisting_date.is_(None),
|
||||||
MarketplaceListing.deleted_at.is_(None),
|
MarketplaceListing.deleted_at.is_(None),
|
||||||
InventoryItem.deleted_at.is_(None),
|
InventoryItem.deleted_at.is_(None),
|
||||||
PhysicalItem.deleted_at.is_(None)
|
PhysicalItem.deleted_at.is_(None)
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
|
TCGPlayerInventory.total_quantity >= 1
|
||||||
).all()
|
).all()
|
||||||
return unmanaged_inventory
|
return unmanaged_inventory
|
||||||
|
|
||||||
|
async def get_managed_inventory(self, db: Session):
|
||||||
|
# First get the TCGPlayerInventory IDs that are managed
|
||||||
|
managed_ids = select(TCGPlayerInventory.id).join(
|
||||||
|
PhysicalItem, TCGPlayerInventory.tcgplayer_sku_id == PhysicalItem.tcgplayer_sku_id
|
||||||
|
).join(
|
||||||
|
InventoryItem, PhysicalItem.id == InventoryItem.physical_item_id
|
||||||
|
).join(
|
||||||
|
MarketplaceListing, InventoryItem.id == MarketplaceListing.inventory_item_id
|
||||||
|
).filter(
|
||||||
|
MarketplaceListing.delisting_date.is_(None),
|
||||||
|
MarketplaceListing.deleted_at.is_(None),
|
||||||
|
InventoryItem.deleted_at.is_(None),
|
||||||
|
PhysicalItem.deleted_at.is_(None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then get just the TCGPlayerInventory data for those IDs
|
||||||
|
managed_inventory = db.query(TCGPlayerInventory).filter(
|
||||||
|
TCGPlayerInventory.id.in_(managed_ids)
|
||||||
|
).all()
|
||||||
|
return managed_inventory
|
||||||
|
|
||||||
|
async def get_pricing_data_for_unmanaged_inventory(self, db: Session) -> Dict[int, PriceData]:
|
||||||
|
"""Gather all pricing data for unmanaged inventory in a single query."""
|
||||||
|
unmanaged_inventory = await self.get_unmanaged_inventory(db)
|
||||||
|
|
||||||
|
# Get all SKU IDs
|
||||||
|
sku_ids = [inv.tcgplayer_sku_id for inv in unmanaged_inventory]
|
||||||
|
|
||||||
|
# Fetch all MTGJSON SKUs and their products in one query
|
||||||
|
mtgjson_skus = db.query(MTGJSONSKU).filter(
|
||||||
|
MTGJSONSKU.tcgplayer_sku_id.in_(sku_ids)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Create a mapping of SKU ID to MTGJSON SKU
|
||||||
|
sku_map = {sku.tcgplayer_sku_id: sku for sku in mtgjson_skus}
|
||||||
|
|
||||||
|
# Create price data for each inventory item
|
||||||
|
price_data_map = {}
|
||||||
|
for inventory in unmanaged_inventory:
|
||||||
|
mtgjson_sku = sku_map.get(inventory.tcgplayer_sku_id)
|
||||||
|
if mtgjson_sku and mtgjson_sku.product and mtgjson_sku.product.most_recent_tcgplayer_price:
|
||||||
|
recent_price = mtgjson_sku.product.most_recent_tcgplayer_price
|
||||||
|
price_data = PriceData(
|
||||||
|
cost_basis=None,
|
||||||
|
market_price=Decimal(str(recent_price.market_price)) if recent_price.market_price else None,
|
||||||
|
tcg_low=Decimal(str(recent_price.low_price)) if recent_price.low_price else None,
|
||||||
|
tcg_mid=Decimal(str(recent_price.mid_price)) if recent_price.mid_price else None,
|
||||||
|
direct_low=Decimal(str(recent_price.direct_low_price)) if recent_price.direct_low_price else None,
|
||||||
|
listed_price=Decimal(str(inventory.tcg_marketplace_price)) if inventory.tcg_marketplace_price else None,
|
||||||
|
quantity=inventory.total_quantity,
|
||||||
|
lowest_price_for_qty=None,
|
||||||
|
velocity=None,
|
||||||
|
age_of_inventory=None
|
||||||
|
)
|
||||||
|
price_data_map[inventory.tcgplayer_sku_id] = price_data
|
||||||
|
|
||||||
|
return price_data_map
|
||||||
|
|
||||||
async def update_prices_for_unmanaged_inventory(self, db: Session):
|
async def update_prices_for_unmanaged_inventory(self, db: Session):
|
||||||
|
# Get all pricing data upfront
|
||||||
|
price_data_map = await self.get_pricing_data_for_unmanaged_inventory(db)
|
||||||
|
|
||||||
|
# Update prices using the pre-fetched data
|
||||||
unmanaged_inventory = await self.get_unmanaged_inventory(db)
|
unmanaged_inventory = await self.get_unmanaged_inventory(db)
|
||||||
for inventory in unmanaged_inventory:
|
for inventory in unmanaged_inventory:
|
||||||
inventory.tcg_marketplace_price = await self.set_price_for_unmanaged_inventory(db, inventory)
|
price_data = price_data_map.get(inventory.tcgplayer_sku_id)
|
||||||
|
if price_data:
|
||||||
async def set_price_for_unmanaged_inventory(self, db: Session, inventory: TCGPlayerInventory):
|
inventory.tcg_marketplace_price = await self.set_price(db, price_data)
|
||||||
# available columns
|
|
||||||
# id, tcgplayer_sku_id, product_line, set_name, product_name, title,
|
return unmanaged_inventory
|
||||||
# number, rarity, condition, tcg_market_price, tcg_direct_low,
|
|
||||||
# tcg_low_price_with_shipping, tcg_low_price, total_quantity, add_to_quantity,
|
async def update_prices_for_managed_inventory(self, db: Session):
|
||||||
# tcg_marketplace_price, photo_url, created_at, updated_at
|
"""Update prices for managed inventory items and return updated TCGPlayerInventory data."""
|
||||||
# get mtgjson sku
|
managed_inventory = await self.get_managed_inventory(db)
|
||||||
mtgjson_sku = db.query(MTGJSONSKU).filter(
|
|
||||||
MTGJSONSKU.tcgplayer_sku_id == inventory.tcgplayer_sku_id
|
# Get all the inventory items we need in one query
|
||||||
).first()
|
inventory_items = db.query(InventoryItem).join(
|
||||||
if mtgjson_sku:
|
PhysicalItem, InventoryItem.physical_item_id == PhysicalItem.id
|
||||||
tcgplayer_product = mtgjson_sku.product
|
).filter(
|
||||||
price_data = PriceData(
|
PhysicalItem.tcgplayer_sku_id.in_([inv.tcgplayer_sku_id for inv in managed_inventory]),
|
||||||
cost_basis=None,
|
InventoryItem.deleted_at.is_(None)
|
||||||
market_price=Decimal(str(tcgplayer_product.most_recent_tcgplayer_price.market_price)) if tcgplayer_product.most_recent_tcgplayer_price else None,
|
).all()
|
||||||
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,
|
# Create a map of sku_id to inventory_item for easy lookup
|
||||||
direct_low=Decimal(str(tcgplayer_product.most_recent_tcgplayer_price.direct_low_price)) if tcgplayer_product.most_recent_tcgplayer_price else None,
|
inventory_map = {item.physical_item.tcgplayer_sku_id: item for item in inventory_items}
|
||||||
listed_price=inventory.tcg_marketplace_price,
|
|
||||||
quantity=inventory.total_quantity,
|
for tcg_inventory in managed_inventory:
|
||||||
lowest_price_for_qty=None,
|
inventory_item = inventory_map.get(tcg_inventory.tcgplayer_sku_id)
|
||||||
velocity=None,
|
if inventory_item:
|
||||||
age_of_inventory=None
|
pricing_event = await self.set_price_for_inventory_item(db, inventory_item)
|
||||||
)
|
if pricing_event:
|
||||||
return await self.set_price(db, price_data)
|
tcg_inventory.tcg_marketplace_price = pricing_event.price
|
||||||
else:
|
|
||||||
return None
|
return managed_inventory
|
||||||
|
|
||||||
async def set_price_for_inventory_item(self, db: Session, inventory_item: InventoryItem):
|
async def set_price_for_inventory_item(self, db: Session, inventory_item: InventoryItem):
|
||||||
|
recent_price = inventory_item.physical_item.sku.product.most_recent_tcgplayer_price
|
||||||
|
|
||||||
|
# Get the most recent active marketplace listing
|
||||||
|
active_listing = None
|
||||||
|
if inventory_item.marketplace_listing:
|
||||||
|
active_listings = [listing for listing in inventory_item.marketplace_listing
|
||||||
|
if listing.delisting_date is None and listing.deleted_at is None]
|
||||||
|
if active_listings:
|
||||||
|
active_listing = active_listings[0] # Get the first active listing
|
||||||
|
|
||||||
price_data = PriceData(
|
price_data = PriceData(
|
||||||
cost_basis=Decimal(str(inventory_item.cost_basis)),
|
cost_basis=Decimal(str(inventory_item.cost_basis)) if inventory_item.cost_basis is not None else None,
|
||||||
market_price=Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.market_price)),
|
market_price=Decimal(str(recent_price.market_price)) if recent_price and recent_price.market_price is not None else None,
|
||||||
tcg_low=Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.low_price)),
|
tcg_low=Decimal(str(recent_price.low_price)) if recent_price and recent_price.low_price is not None else None,
|
||||||
tcg_mid=Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.mid_price)),
|
tcg_mid=Decimal(str(recent_price.mid_price)) if recent_price and recent_price.mid_price is not None else None,
|
||||||
direct_low=Decimal(str(inventory_item.physical_item.sku.product.most_recent_tcgplayer_price.direct_low_price)),
|
direct_low=Decimal(str(recent_price.direct_low_price)) if recent_price and recent_price.direct_low_price is not None else None,
|
||||||
listed_price=Decimal(str(inventory_item.marketplace_listing.listed_price)),
|
listed_price=Decimal(str(active_listing.listed_price.price)) if active_listing and active_listing.listed_price and active_listing.listed_price.price is not None else None,
|
||||||
quantity=db.query(TCGPlayerInventory).filter(
|
quantity=db.query(TCGPlayerInventory).filter(
|
||||||
TCGPlayerInventory.tcgplayer_sku_id == inventory_item.physical_item.tcgplayer_sku_id
|
TCGPlayerInventory.tcgplayer_sku_id == inventory_item.physical_item.tcgplayer_sku_id
|
||||||
).first().total_quantity if db.query(TCGPlayerInventory).filter(
|
).first().total_quantity if db.query(TCGPlayerInventory).filter(
|
||||||
@ -118,6 +188,15 @@ class PricingService(BaseService):
|
|||||||
tcg_mid = price_data.tcg_mid
|
tcg_mid = price_data.tcg_mid
|
||||||
listed_price = price_data.listed_price
|
listed_price = price_data.listed_price
|
||||||
|
|
||||||
|
if inventory_item:
|
||||||
|
# average cost basis for all inventory items with the same tcgplayer_sku_id
|
||||||
|
average_cost_basis = db.query(InventoryItem.cost_basis).filter(
|
||||||
|
InventoryItem.physical_item_id == inventory_item.physical_item_id
|
||||||
|
).all()
|
||||||
|
cost_basis_values = [row[0] for row in average_cost_basis if row[0] is not None]
|
||||||
|
if cost_basis_values:
|
||||||
|
cost_basis = Decimal(str(sum(cost_basis_values) / len(cost_basis_values)))
|
||||||
|
|
||||||
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}")
|
||||||
logger.info(f"tcg_low: {tcg_low}")
|
logger.info(f"tcg_low: {tcg_low}")
|
||||||
@ -133,11 +212,11 @@ class PricingService(BaseService):
|
|||||||
tcgplayer_shipping_fee = Decimal('1.31')
|
tcgplayer_shipping_fee = Decimal('1.31')
|
||||||
average_cards_per_order = Decimal('3.0')
|
average_cards_per_order = Decimal('3.0')
|
||||||
marketplace_fee_percentage = Decimal('0.20')
|
marketplace_fee_percentage = Decimal('0.20')
|
||||||
target_margin = Decimal('0.20')
|
target_margin = Decimal('0.10')
|
||||||
velocity_multiplier = Decimal('0.0')
|
velocity_multiplier = Decimal('0.0')
|
||||||
global_margin_multiplier = Decimal('0.00')
|
global_margin_multiplier = Decimal('0.00')
|
||||||
min_floor_price = Decimal('0.25')
|
min_floor_price = Decimal('0.25')
|
||||||
price_drop_threshold = Decimal('0.20')
|
price_drop_threshold = Decimal('0.50')
|
||||||
# TODO add age of inventory price decrease multiplier
|
# TODO add age of inventory price decrease multiplier
|
||||||
age_of_inventory_multiplier = Decimal('0.0')
|
age_of_inventory_multiplier = Decimal('0.0')
|
||||||
|
|
||||||
@ -154,6 +233,8 @@ class PricingService(BaseService):
|
|||||||
card_cost_margin_multiplier = Decimal('0.033')
|
card_cost_margin_multiplier = Decimal('0.033')
|
||||||
elif market_price >= 100 and market_price < 200:
|
elif market_price >= 100 and market_price < 200:
|
||||||
card_cost_margin_multiplier = Decimal('0.05')
|
card_cost_margin_multiplier = Decimal('0.05')
|
||||||
|
else:
|
||||||
|
card_cost_margin_multiplier = Decimal('0.0')
|
||||||
|
|
||||||
# Fetch current total quantity in stock for SKU
|
# Fetch current total quantity in stock for SKU
|
||||||
quantity_in_stock = price_data.quantity
|
quantity_in_stock = price_data.quantity
|
||||||
@ -177,7 +258,7 @@ class PricingService(BaseService):
|
|||||||
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:
|
if cost_basis is None:
|
||||||
cost_basis = tcg_low * Decimal('0.65')
|
cost_basis = tcg_mid * 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
|
||||||
@ -189,19 +270,19 @@ class PricingService(BaseService):
|
|||||||
base_price + tcgplayer_shipping_fee
|
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)
|
# Adjust price based on market prices (TCG low and TCG mid)
|
||||||
if adjusted_price < tcg_low:
|
if adjusted_price < tcg_low:
|
||||||
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 < (tcg_mid * Decimal('0.8')):
|
elif adjusted_price > tcg_low and adjusted_price < (tcg_mid * Decimal('0.85')):
|
||||||
adjusted_price = tcg_mid
|
adjusted_price = tcg_mid
|
||||||
price_used = "tcg mid"
|
price_used = "tcg mid"
|
||||||
price_reason = f"adjusted price below 80% of tcg mid"
|
price_reason = f"adjusted price below 80% of tcg mid"
|
||||||
|
elif adjusted_price > (tcg_mid * Decimal('1.1')):
|
||||||
|
adjusted_price = max(tcg_mid, cost_basis)
|
||||||
|
price_used = "max tcg mid/cost basis"
|
||||||
|
price_reason = f"adjusted price above 110% of tcg mid, using max of tcg mid and cost basis"
|
||||||
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"
|
||||||
@ -227,6 +308,12 @@ class PricingService(BaseService):
|
|||||||
adjusted_price = listed_price
|
adjusted_price = listed_price
|
||||||
price_used = "listed price"
|
price_used = "listed price"
|
||||||
price_reason = "adjusted price below price drop threshold"
|
price_reason = "adjusted price below price drop threshold"
|
||||||
|
|
||||||
|
# Enforce minimum floor price
|
||||||
|
if adjusted_price < min_floor_price:
|
||||||
|
adjusted_price = min_floor_price
|
||||||
|
price_used = "min floor price"
|
||||||
|
price_reason = "adjusted price below min floor price"
|
||||||
|
|
||||||
# Record pricing event in database transaction
|
# Record pricing event in database transaction
|
||||||
if inventory_item:
|
if inventory_item:
|
||||||
@ -241,8 +328,12 @@ class PricingService(BaseService):
|
|||||||
db.add(pricing_event)
|
db.add(pricing_event)
|
||||||
|
|
||||||
# delete previous pricing events for inventory item
|
# delete previous pricing events for inventory item
|
||||||
if inventory_item.marketplace_listing and inventory_item.marketplace_listing.listed_price:
|
if inventory_item.marketplace_listing:
|
||||||
inventory_item.marketplace_listing.listed_price.deleted_at = datetime.now()
|
for listing in inventory_item.marketplace_listing:
|
||||||
|
if listing.listed_price:
|
||||||
|
listing.listed_price.deleted_at = datetime.now()
|
||||||
|
db.flush()
|
||||||
|
listing.listed_price = pricing_event
|
||||||
|
|
||||||
return pricing_event
|
return pricing_event
|
||||||
else:
|
else:
|
||||||
|
@ -28,9 +28,14 @@
|
|||||||
|
|
||||||
<!-- Create Transaction Button -->
|
<!-- Create Transaction Button -->
|
||||||
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||||
<button id="createTransactionBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
<div class="flex space-x-4">
|
||||||
Create New Transaction
|
<button id="createTransactionBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||||
</button>
|
Create New Transaction
|
||||||
|
</button>
|
||||||
|
<button id="downloadTcgplayerUpdateBtn" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors">
|
||||||
|
Download TCGPlayer Update File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Transaction List -->
|
<!-- Transaction List -->
|
||||||
|
@ -4,6 +4,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
showTransactionModal();
|
showTransactionModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('downloadTcgplayerUpdateBtn').addEventListener('click', downloadTcgplayerUpdateFile);
|
||||||
|
|
||||||
document.getElementById('addVendorBtn').addEventListener('click', () => {
|
document.getElementById('addVendorBtn').addEventListener('click', () => {
|
||||||
const vendorName = prompt('Enter vendor name:');
|
const vendorName = prompt('Enter vendor name:');
|
||||||
if (vendorName) {
|
if (vendorName) {
|
||||||
@ -1074,4 +1076,38 @@ async function saveTransaction() {
|
|||||||
console.error('Error saving transaction:', error);
|
console.error('Error saving transaction:', error);
|
||||||
alert('Failed to save transaction. Please try again.');
|
alert('Failed to save transaction. Please try again.');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TCGPlayer Update File Functions
|
||||||
|
async function downloadTcgplayerUpdateFile() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/inventory/tcgplayer/update-file');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to download TCGPlayer update file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the filename from the Content-Disposition header
|
||||||
|
const contentDisposition = response.headers.get('Content-Disposition');
|
||||||
|
let filename = 'tcgplayer_update_file.csv';
|
||||||
|
if (contentDisposition) {
|
||||||
|
const filenameMatch = contentDisposition.match(/filename=(.+)/);
|
||||||
|
if (filenameMatch) {
|
||||||
|
filename = filenameMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a blob from the response and download it
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading TCGPlayer update file:', error);
|
||||||
|
alert('Failed to download TCGPlayer update file. Please try again.');
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user