1 Commits

Author SHA1 Message Date
9e656ef329 this is a bug - apollo 2025-04-11 11:33:03 -04:00
10 changed files with 17 additions and 408 deletions

View File

@ -29,4 +29,4 @@ jobs:
- name: Run Docker container
run: |
docker run -d -v /mnt/user/appdata/gigatcg/giga_tcg/cookies:/app/cookies -v /mnt/user/appdata/gigatcg/tmp:/app/tmp -p 8000:8000 --name giga_tcg_container giga_tcg
docker run -d -p 8000:8000 --name giga_tcg_container giga_tcg

View File

@ -4,20 +4,6 @@ WORKDIR /app
ENV DATABASE_URL=postgresql://poggers:giga!@192.168.1.41:5432/omegatcgdb
RUN apt-get update && apt-get install -y \
libglib2.0-0 \
libpango-1.0-0 \
libcairo2 \
libjpeg62-turbo \
libgif7 \
libxml2 \
fonts-liberation \
libharfbuzz0b \
libfribidi0 \
libgtk-3-0 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,87 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<style>
/* Setting up the page size for landscape orientation - 6x4 inches */
@page {
size: 6in 4in; /* Landscape orientation */
margin: 0;
}
/* Force page breaks after each label */
body {
margin: 0;
padding: 0;
width: 6in; /* Adjusted for landscape */
height: 4in; /* Adjusted for landscape */
position: relative;
font-family: Arial, sans-serif;
}
.label-container {
width: 100%;
height: 100%;
position: relative;
box-sizing: border-box;
padding: 0.25in;
page-break-after: always; /* Ensures a page break after each label */
}
/* Return address image in top left */
.return-address {
position: absolute;
top: 0.25in;
left: 0.25in;
width: 2.75in;
height: 0.85in;
}
/* Stamp area in top right */
.stamp-area {
position: absolute;
top: 0.25in;
right: 0.25in;
width: 0.8in;
height: 0.9in;
border: 1px dashed #999;
display: flex;
justify-content: center;
align-items: center;
}
.stamp-text {
font-size: 8pt;
color: #999;
text-align: center;
}
/* Main address centered in the middle */
.address {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 3in;
text-align: center;
font-size: 12pt;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="label-container">
<img src="{{ return_address_path }}" class="return-address" alt="Return Address">
<div class="stamp-area">
<span class="stamp-text">PLACE<br>STAMP<br>HERE</span>
</div>
<div class="address">
{{ recipient_name }}<br>
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif %}
{{ city }}, {{ state }} {{ zip_code }}
</div>
</div>
</body>
</html>

View File

@ -317,9 +317,9 @@ async def update_cookies(
detail=f"Failed to update cookies: {str(e)}"
)
### DEPRECATED ###
class TCGPlayerOrderRequest(BaseModel):
# optional
order_ids: Optional[List[str]] = None
order_ids: List[str]
@router.post("/processOrders", response_model=ProcessOrdersResponse)
async def process_orders(
@ -328,7 +328,7 @@ async def process_orders(
) -> ProcessOrdersResponse:
"""Process TCGPlayer orders."""
try:
orders = tcgplayer_api_service.process_open_orders(body.order_ids)
orders = tcgplayer_api_service.process_orders(body.order_ids)
return ProcessOrdersResponse(
status_code=200,
success=True,
@ -337,3 +337,5 @@ 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 ###

View File

@ -8,14 +8,10 @@ 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):
@ -119,98 +115,6 @@ 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"""
@ -263,13 +167,8 @@ 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 or ACTIVE_PRICING_ALGORITHIM == 'default_pricing_algo':
if pricing_algo is None:
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:
@ -299,8 +198,13 @@ class PricingService:
.all()
}
# Map the ids using the dictionary
df['product_id'] = df['tcgplayer_id'].map(product_id_mapping)
# Map tcgplayer_id to product_id, ensure strings, keep None if not found
df['product_id'] = df['tcgplayer_id'].map(product_id_mapping).apply(lambda x: str(x) if pd.notnull(x) else None)
# Log any tcgplayer_ids that didn't map to a product_id
null_product_ids = df[df['product_id'].isnull()]['tcgplayer_id'].tolist()
if null_product_ids:
logger.warning(f"The following tcgplayer_ids could not be mapped to a product_id: {null_product_ids}")
price_lookup = self.get_all_prices_for_products(df['product_id'].unique())

View File

@ -7,12 +7,7 @@ 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
import time
import re
import csv
logger = logging.getLogger(__name__)
@ -32,10 +27,6 @@ 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 = "file:///app/app/assets/images/ccrcardsaddress.png"
def get_cookies(self) -> dict:
if self.is_in_docker:
@ -230,161 +221,6 @@ class TCGPlayerAPIService:
pricing_data = self.get_tcgplayer_pricing_data_for_product(card.product_id)
if pricing_data:
self.save_tcgplayer_pricing_data(card.product_id, pricing_data)
def get_packing_slip_pdf_for_orders(self, order_ids: list[str]):
url = f"{self.config.ORDER_BASE_URL}/packing-slips/export{self.config.API_VERSION}"
payload = {
"sortingType": "byRelease",
"format": "default",
"timezoneOffset": -4,
"orderNumbers": order_ids
}
response = self.requests_util.send_request(url, method='POST', cookies=self.cookies, json=payload)
if response:
# get filename from response headers
header = response.headers.get('Content-Disposition', '')
match = re.search(r'filename="?([^";]+)"?', header)
filename = match.group(1) if match else f'packingslip{datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf'
output_filename = f'/app/tmp/{filename}'
# save file to disk
with open(output_filename, 'wb') as f:
f.write(response.content)
return output_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
header = response.headers.get('Content-Disposition', '')
match = re.search(r'filename="?([^";]+)"?', header)
filename = match.group(1) if match else f'packingslip{datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf'
output_filename = f'/app/tmp/{filename}'
# save file to disk
with open(output_filename, 'wb') as f:
f.write(response.content)
return output_filename
def get_address_labels_csv(self, order_ids: list[str]):
url = f"{self.config.ORDER_BASE_URL}/shipping/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
header = response.headers.get('Content-Disposition', '')
match = re.search(r'filename="?([^";]+)"?', header)
filename = match.group(1) if match else f'shipping{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
output_filename = f'/app/tmp/{filename}'
# save file to disk
with open(output_filename, 'wb') as f:
f.write(response.content)
return output_filename
def get_address_labels_pdf(self, order_ids: list[str]):
shipping_csv_filename = self.get_address_labels_csv(order_ids)
with open(shipping_csv_filename, 'r') as f:
reader = csv.DictReader(f)
orders = {}
for row in reader:
order_id = row.pop('Order #')
orders[order_id] = row
labels_html = []
for order_id in orders:
order = orders[order_id]
#if float(order['Value of Products']) >49.99:
#continue
# Extract relevant information from the order
order_info = {
"recipient_name": order['FirstName'] + ' ' + order['LastName'],
"address_line1": order['Address1'],
"address_line2": order['Address2'] if 'Address2' in order else '',
"city": order['City'],
"state": order['State'],
"zip_code": order['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))
if labels_html:
# Combine the rendered labels into one HTML string
full_html = "<html><body>" + "\n".join(labels_html) + "</body></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, order_ids: list[str]=None):
# 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:
if order_ids is None:
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="http://192.168.1.110:8000/upload",
method='POST',
files={'file': file}
)
time.sleep(10)
return order_ids
return None

View File

@ -83,10 +83,10 @@ class RequestsUtil:
session.cookies.update(cookies)
return session
def bare_request(self, url: str, method: str, cookies: dict = None, data=None, files=None) -> requests.Response:
def bare_request(self, url: str, method: str, cookies: dict = None, data=None) -> requests.Response:
"""Send a request without any additional processing"""
try:
response = requests.request(method, url, cookies=cookies, data=data, files=files)
response = requests.request(method, url, cookies=cookies, data=data)
response.raise_for_status()
return response
except requests.RequestException as e:

View File

@ -18,8 +18,4 @@ curl -X POST "http://192.168.1.41:8000/api/boxes/d95d26a8-1f82-47f2-89fa-3f88a46
curl -X POST "http://192.168.1.41:8000/api/processOrders" \
-H "Content-Type: application/json" \
-d '{"order_ids": ["E576ED4C-AD8CC8-9E57E","E576ED4C-2EC48E-7F185","E576ED4C-9B72C2-5E208","E576ED4C-67DAA6-B06B5","E576ED4C-7E52E0-CCE11","E576ED4C-572216-87C96","E576ED4C-37BADB-8BE45","E576ED4C-2D9E83-89C64","E576ED4C-C8080B-8066C","E576ED4C-A41099-E4F92","E576ED4C-2B5122-5E719","E576ED4C-95BB1D-07DDA","E576ED4C-1CF99A-20072","E576ED4C-342542-28CDB","E576ED4C-42720B-514DB","E576ED4C-911CB9-15174","E576ED4C-EBD55A-27AE6","E576ED4C-CC32F2-76408","E576ED4C-45328B-E65B4","E576ED4C-1F26F5-84367","E576ED4C-1D4FE5-71100","E576ED4C-BCEC53-A7CF2","E576ED4C-132791-5AF2E"]}'
curl -X POST "http://192.168.1.41:8000/api/processOrders" \
-H "Content-Type: application/json" \
-d '{}'
-d '{"order_ids": ["E576ED4C-A472E7-36237","E576ED4C-16EDF6-A340B","E576ED4C-A25B9C-A8855","E576ED4C-0E1A20-C6350","E576ED4C-9E8C21-3A249","E576ED4C-3825F0-5A5CC","E576ED4C-628925-6348B","E576ED4C-5F4314-6E3D2","E576ED4C-60E0B9-69D1D","E576ED4C-4BEC42-B2D0A","E576ED4C-5253F2-E2E16","E576ED4C-C08EA2-F51B4","E576ED4C-EE350E-BA82C","E576ED4C-CB067C-21150","E576ED4C-85DE3E-4E518","E576ED4C-27DB4A-A7729","E576ED4C-91A537-2AEBA","E576ED4C-3961D0-4F5A9","E576ED4C-EB7B7D-DBE0D","E576ED4C-1F9576-A9351","E576ED4C-7EBF1E-6FDB9","E576ED4C-F549E2-C558B","E576ED4C-215B45-4F177","E576ED4C-572FAA-004F7","E576ED4C-9D5F33-1A3C4","E576ED4C-87276B-63EC8","E576ED4C-2143E7-4DE1B","E576ED4C-41E56A-04D55","E576ED4C-789397-BF6AD","E576ED4C-2F3F46-154FE","E576ED4C-EFCBEE-3FE93","E576ED4C-3ADBAE-7CA1B","E576ED4C-D9F68F-A5E6F","E576ED4C-DEA6E2-8B590","E576ED4C-86D96B-DC5C4","E576ED4C-EDFABA-67C3C","E576ED4C-C57373-3F638","E576ED4C-B2C2B4-FF53B","E576ED4C-3788E5-B3653","E576ED4C-8A573A-BB51B","E576ED4C-497380-63F5C","E576ED4C-A6C3F2-C7BF2","E576ED4C-FAC80B-148F3","E576ED4C-ECF1F3-AF3A4"]}'

View File

@ -2,69 +2,41 @@ alembic==1.14.1
annotated-types==0.7.0
anyio==4.8.0
APScheduler==3.11.0
attrs==25.3.0
blabel==0.1.6
brother_ql_next==0.11.3
Brotli==1.1.0
browser-cookie3==0.20.1
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
click==8.1.8
coverage==7.6.10
cssselect2==0.8.0
fastapi==0.115.8
fonttools==4.57.0
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
Jinja2==3.1.6
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
pycparser==2.22
pycryptodomex==3.21.0
pydantic==2.10.6
pydantic_core==2.27.2
pydyf==0.11.0
pyphen==0.17.2
pyStrich==0.9
pytest==8.3.4
pytest-asyncio==0.25.3
pytest-cov==6.0.0
python-barcode==0.15.1
python-dateutil==2.9.0.post0
python-multipart==0.0.20
pytz==2025.1
pyusb==1.3.1
qrcode==8.1
requests==2.32.3
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.37
starlette==0.45.3
tinycss2==1.4.0
tinyhtml5==2.0.0
typing_extensions==4.12.2
typish==1.9.3
tzdata==2025.1
tzlocal==5.2
urllib3==2.3.0
uvicorn==0.34.0
weasyprint==65.0
webencodings==0.5.1
zopfli==0.2.3.post1