diff --git a/Dockerfile b/Dockerfile
index 1036041..a9898b0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,6 +4,11 @@ WORKDIR /app
ENV DATABASE_URL=postgresql://poggers:giga!@192.168.1.41:5432/omegatcgdb
+RUN apt-get update && \
+ apt-get install -y fonts-liberation && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
+
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
diff --git a/app/assets/images/ccrcardsaddress.png b/app/assets/images/ccrcardsaddress.png
new file mode 100644
index 0000000..9e95460
Binary files /dev/null and b/app/assets/images/ccrcardsaddress.png differ
diff --git a/app/assets/templates/address_label.html b/app/assets/templates/address_label.html
new file mode 100644
index 0000000..477e52d
--- /dev/null
+++ b/app/assets/templates/address_label.html
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+

+
+
+ PLACE
STAMP
HERE
+
+
+
+ {{ recipient_name }}
+ {{ address_line1 }}
+ {% if address_line2 %}{{ address_line2 }}
{% endif %}
+ {{ city }}, {{ state }} {{ zip_code }}
+
+
+
+
diff --git a/app/routes/routes.py b/app/routes/routes.py
index 9d81ac9..902264f 100644
--- a/app/routes/routes.py
+++ b/app/routes/routes.py
@@ -317,18 +317,16 @@ async def update_cookies(
detail=f"Failed to update cookies: {str(e)}"
)
-### DEPRECATED ###
class TCGPlayerOrderRequest(BaseModel):
order_ids: List[str]
-@router.post("/processOrders", response_model=ProcessOrdersResponse)
+@router.get("/processOrders", response_model=dict)
async def process_orders(
- body: TCGPlayerOrderRequest,
tcgplayer_api_service: TCGPlayerAPIService = Depends(get_tcgplayer_api_service),
) -> ProcessOrdersResponse:
"""Process TCGPlayer orders."""
try:
- orders = tcgplayer_api_service.process_orders(body.order_ids)
+ orders = tcgplayer_api_service.process_open_orders()
return ProcessOrdersResponse(
status_code=200,
success=True,
@@ -337,5 +335,3 @@ async def process_orders(
except Exception as e:
logger.error(f"Process orders failed: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
-
-### END DEPRECATED ###
\ No newline at end of file
diff --git a/app/services/pricing.py b/app/services/pricing.py
index 454d328..52412a0 100644
--- a/app/services/pricing.py
+++ b/app/services/pricing.py
@@ -8,10 +8,14 @@ from app.db.utils import db_transaction
from typing import List, Dict
from decimal import Decimal, ROUND_HALF_UP
import pandas as pd
+import numpy as np
import logging
logger = logging.getLogger(__name__)
+ACTIVE_PRICING_ALGORITHIM = 'tcgplayer_recommended_algo' # 'default_pricing_algo' or 'tcgplayer_recommended_algo'
+FREE_SHIPPING = True
+
class PricingService:
def __init__(self, db: Session, file_service: FileService, tcgplayer_service: TCGPlayerService):
@@ -115,6 +119,98 @@ class PricingService:
row[price_type] = price
return row
+ def smooth_markup(price, markup_bands):
+ """
+ Applies a smoothed markup based on the given price and markup bands.
+ Uses numpy for smooth transitions.
+ """
+ # Convert markup bands to lists for easy lookup
+ markups = np.array(list(markup_bands.keys()))
+ min_prices = np.array([x[0] for x in markup_bands.values()])
+ max_prices = np.array([x[1] for x in markup_bands.values()])
+
+ # Find the index of the price's range
+ idx = np.where((min_prices <= price) & (max_prices >= price))[0]
+
+ if len(idx) > 0:
+ # If price is within a defined range, return the markup
+ markup = markups[idx[0]]
+ else:
+ # If price is not directly within any range, check smooth transitions
+ # Find the closest two bands for interpolation
+ idx_lower = np.argmax(max_prices <= price) # Closest range below the price
+ idx_upper = np.argmax(min_prices > price) # Closest range above the price
+
+ if idx_lower != idx_upper:
+ # Linear interpolation between the two neighboring markups
+ price_diff = (price - max_prices[idx_lower]) / (min_prices[idx_upper] - max_prices[idx_lower])
+ markup = np.interp(price_diff, [0, 1], [markups[idx_lower], markups[idx_upper]])
+
+ # Apply the markup to the price
+ return price * markup
+
+ def tcgplayer_recommended_algo(self, row: pd.Series) -> pd.Series:
+ # Convert input values to Decimal for precise arithmetic
+ tcg_low = Decimal(str(row.get('tcg_low_price'))) if not pd.isna(row.get('tcg_low_price')) else None
+ tcg_low_shipping = Decimal(str(row.get('tcg_low_price_with_shipping'))) if not pd.isna(row.get('tcg_low_price_with_shipping')) else None
+ tcg_market_price = Decimal(str(row.get('tcg_market_price'))) if not pd.isna(row.get('tcg_market_price')) else None
+ current_price = Decimal(str(row.get('tcg_marketplace_price'))) if not pd.isna(row.get('tcgplayer_marketplace_price')) else None
+ total_quantity = str(row.get('total_quantity')) if not pd.isna(row.get('total_quantity')) else "0"
+ added_quantity = str(row.get('add_to_quantity')) if not pd.isna(row.get('add_to_quantity')) else "0"
+ quantity = int(total_quantity) + int(added_quantity)
+
+ if tcg_market_price is None:
+ logger.warning(f"Missing pricing data for row: {row}")
+ row['new_price'] = None
+ return row
+ # Define precision for rounding
+ TWO_PLACES = Decimal('0.01')
+
+ # Apply pricing rules
+ markup_bands = {
+ 2.53: (Decimal('0.01'), Decimal('0.50')),
+ 1.42: (Decimal('0.51'), Decimal('1.00')),
+ 1.29: (Decimal('1.01'), Decimal('3.00')),
+ 1.17: (Decimal('3.01'), Decimal('20.00')),
+ 1.07: (Decimal('20.01'), Decimal('35.00')),
+ 1.05: (Decimal('35.01'), Decimal('50.00')),
+ 1.03: (Decimal('50.01'), Decimal('100.00')),
+ 1.02: (Decimal('100.01'), Decimal('200.00')),
+ 1.01: (Decimal('200.01'), Decimal('1000.00'))
+ }
+
+ if quantity > 3:
+ quantity_markup = Decimal('0.1')
+ for markup in markup_bands:
+ markup = markup + quantity_markup
+ quantity_markup = quantity_markup - Decimal('0.01')
+
+ if FREE_SHIPPING:
+ free_shipping_markup = Decimal('0.05')
+ for markup in markup_bands:
+ markup = markup + free_shipping_markup
+ free_shipping_markup = free_shipping_markup - Decimal('0.005')
+
+ # Apply the smoothed markup
+ new_price = self.smooth_markup(tcg_market_price, markup_bands)
+
+ if tcg_low_shipping is not None and tcg_low_shipping < new_price:
+ new_price = tcg_low_shipping
+
+ if new_price < Decimal('0.25'):
+ new_price = Decimal('0.25')
+
+ if current_price / new_price > Decimal('0.25'):
+ logger.warning(f"Price drop too large for row: {row}")
+ new_price = current_price
+
+ # Ensure exactly 2 decimal places
+ new_price = new_price.quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
+ # Convert back to float or string as needed for your dataframe
+ row['new_price'] = float(new_price)
+
+ return row
+
def default_pricing_algo(self, row: pd.Series) -> pd.Series:
"""Default pricing algorithm with complex pricing rules"""
@@ -167,8 +263,13 @@ class PricingService:
def apply_pricing_algo(self, row: pd.Series, pricing_algo: callable = None) -> pd.Series:
"""Modified to handle the pricing algorithm as an instance method"""
- if pricing_algo is None:
+ if pricing_algo is None or ACTIVE_PRICING_ALGORITHIM == 'default_pricing_algo':
pricing_algo = self.default_pricing_algo
+ elif ACTIVE_PRICING_ALGORITHIM == 'tcgplayer_recommended_algo':
+ pricing_algo = self.tcgplayer_recommended_algo
+ else:
+ pricing_algo = self.default_pricing_algo
+
return pricing_algo(row)
def generate_tcgplayer_inventory_update_file_with_pricing(self, open_box_ids: List[str] = None) -> bytes:
diff --git a/app/services/tcgplayer_api.py b/app/services/tcgplayer_api.py
index 4878b76..43f9530 100644
--- a/app/services/tcgplayer_api.py
+++ b/app/services/tcgplayer_api.py
@@ -7,6 +7,8 @@ from app.db.utils import db_transaction
from sqlalchemy.orm import Session
from datetime import datetime
from uuid import uuid4 as uuid
+from jinja2 import Environment, FileSystemLoader
+from weasyprint import HTML
import json
logger = logging.getLogger(__name__)
@@ -27,6 +29,10 @@ class TCGPlayerAPIService:
self.config = TCGPlayerAPIConfig()
self.cookies = self.get_cookies()
self.session = None
+ self.template_dir = "/app/app/assets/templates"
+ self.env = Environment(loader=FileSystemLoader(self.template_dir))
+ self.address_label_template = self.env.get_template("address_label.html")
+ self.return_address_png = "/app/app/assets/images/ccrcardsaddress.png"
def get_cookies(self) -> dict:
if self.is_in_docker:
@@ -228,15 +234,120 @@ class TCGPlayerAPIService:
"sortingType": "byRelease",
"format": "default",
"timezoneOffset": -4,
- "orderNumbers": [order_ids]
+ "orderNumbers": order_ids
}
response = self.requests_util.send_request(url, method='POST', cookies=self.cookies, json=payload)
if response:
# get filename from response headers
filename = response.headers.get('Content-Disposition').split('filename=')[1].strip('"')
# save file to disk
- with open(filename, 'wb') as f:
+ with open('/app/tmp' + filename, 'wb') as f:
f.write(response.content)
+
+ return filename
+
+ def get_pull_sheet_for_orders(self, order_ids: list[str]):
+ url = f"{self.config.ORDER_BASE_URL}/pull-sheets/export{self.config.API_VERSION}"
+ payload = {
+ "orderNumbers": order_ids,
+ "timezoneOffset": -4,
+ }
+ response = self.requests_util.send_request(url, method='POST', cookies=self.cookies, json=payload)
+ if response:
+ # get filename from response headers
+ filename = response.headers.get('Content-Disposition').split('filename=')[1].strip('"')
+ # save file to disk
+ with open('/app/tmp' + filename, 'wb') as f:
+ f.write(response.content)
+
+ return filename
+
+ def get_address_labels_pdf(self, order_ids: list[str]):
+ labels_html = []
+
+ for order_id in order_ids:
+ order = self.get_order(order_id)
+
+ if order:
+ try:
+ # Extract relevant information from the order
+ order_info = {
+ "recipient_name": order['shippingAddress']['recipientName'],
+ "address_line1": order['shippingAddress']['addressOne'],
+ "address_line2": order['shippingAddress'].get('addressTwo', ''),
+ "city": order['shippingAddress']['city'],
+ "state": order['shippingAddress']['territory'],
+ "zip_code": order['shippingAddress']['postalCode'],
+ "return_address_path": self.return_address_png
+ }
+
+ # Render the label HTML using the template
+ labels_html.append(self.address_label_template.render(order_info))
+ except KeyError as e:
+ print(f"Missing field in order {order_id}: {e}")
+ continue
+
+ if labels_html:
+ # Combine the rendered labels into one HTML string
+ full_html = "" + "\n".join(labels_html) + ""
+
+ # Generate a unique output filename with a timestamp
+ output_filename = f'/app/tmp/address_labels_{datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf'
+
+ # Generate the PDF from the HTML string
+ HTML(string=full_html).write_pdf(output_filename)
+
+ return output_filename
+ else:
+ print("No orders found or no valid labels generated.")
+ return None
+
+ def process_open_orders(self):
+ # get all open orders
+ url = f"{self.config.ORDER_BASE_URL}/search{self.config.API_VERSION}"
+ """{"searchRange":"LastThreeMonths","filters":{"sellerKey":"e576ed4c","orderStatuses":["Processing","ReadyToShip","Received","Pulling","ReadyForPickup"],"fulfillmentTypes":["Normal"]},"sortBy":[{"sortingType":"orderStatus","direction":"ascending"},{"sortingType":"orderDate","direction":"ascending"}],"from":0,"size":25}"""
+ payload = {
+ "searchRange": "LastThreeMonths",
+ "filters": {
+ "sellerKey": self.config.SELLER_KEY,
+ "orderStatuses": ["Processing", "ReadyToShip", "Received", "Pulling", "ReadyForPickup"],
+ "fulfillmentTypes": ["Normal"]
+ },
+ "sortBy": [
+ {"sortingType": "orderStatus", "direction": "ascending"},
+ {"sortingType": "orderDate", "direction": "ascending"}
+ ],
+ "from": 0,
+ "size": 100
+ }
+ response = self.requests_util.send_request(url, method='POST', cookies=self.cookies, json=payload)
+ if response:
+ orders = response.json()
+ if orders and 'orders' in orders:
+ order_ids = [order['orderNumber'] for order in orders['orders']]
+ # get packing slip pdf
+ packing_slip_filename = self.get_packing_slip_pdf_for_orders(order_ids)
+ # get pull sheet pdf
+ pull_sheet_filename = self.get_pull_sheet_for_orders(order_ids)
+ # get address labels pdf
+ address_labels_filename = self.get_address_labels_pdf(order_ids)
+ with open(packing_slip_filename, 'rb') as packing_slip_file, \
+ open(pull_sheet_filename, 'rb') as pull_sheet_file, \
+ open(address_labels_filename, 'rb') as address_labels_file:
+ files = [
+ packing_slip_file,
+ # pull_sheet_file,
+ address_labels_file
+ ]
+ # request post pdfs
+ for file in files:
+ self.requests_util.bare_request(
+ url="tcgportal.local:8000/upload",
+ method='POST',
+ files=file
+ )
+
+
diff --git a/requirements.txt b/requirements.txt
index dec4795..4629454 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,23 +2,32 @@ alembic==1.14.1
annotated-types==0.7.0
anyio==4.8.0
APScheduler==3.11.0
+attrs==25.3.0
+brother_ql_next==0.11.3
browser-cookie3==0.20.1
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
coverage==7.6.10
fastapi==0.115.8
+future==1.0.0
+greenlet==3.1.1
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
idna==3.10
iniconfig==2.0.0
+jeepney==0.9.0
+jsons==1.6.3
lz4==4.4.3
Mako==1.3.9
MarkupSafe==3.0.2
numpy==2.2.2
packaging==24.2
+packbits==0.6
pandas==2.2.3
+pdf2image==1.17.0
+pillow==11.1.0
pluggy==1.5.0
psycopg2-binary==2.9.10
pycryptodomex==3.21.0
@@ -30,12 +39,14 @@ pytest-cov==6.0.0
python-dateutil==2.9.0.post0
python-multipart==0.0.20
pytz==2025.1
+pyusb==1.3.1
requests==2.32.3
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.37
starlette==0.45.3
typing_extensions==4.12.2
+typish==1.9.3
tzdata==2025.1
tzlocal==5.2
urllib3==2.3.0