order and api and more

This commit is contained in:
2025-04-17 00:09:16 -04:00
parent 593e8960b7
commit 21408af48c
31 changed files with 1924 additions and 542 deletions

View File

@ -67,9 +67,6 @@ class BaseTCGPlayerService(BaseExternalService):
auth_required: bool = True,
download_file: bool = False
) -> Union[Dict[str, Any], bytes]:
session = await self._get_session()
url = f"{self.base_url}{endpoint}"
# Get the authentication cookie if required
if auth_required:
cookie = self.credentials.get_cookie()
@ -83,28 +80,26 @@ class BaseTCGPlayerService(BaseExternalService):
request_headers["Cookie"] = cookie
else:
request_headers = headers or {}
try:
async with session.request(method, url, params=params, headers=request_headers, json=data) as response:
if response.status == 401:
raise RuntimeError("TCGPlayer authentication failed. Cookie may be invalid or expired.")
response.raise_for_status()
if download_file:
return await response.read()
return await response.json()
# Use the parent class's _make_request with our custom headers and binary flag
response = await super()._make_request(
method=method,
endpoint=endpoint,
params=params,
headers=request_headers,
data=data,
binary=download_file
)
if isinstance(response, dict) and response.get('status') == 401:
raise RuntimeError("TCGPlayer authentication failed. Cookie may be invalid or expired.")
return response
except aiohttp.ClientError as e:
logger.error(f"TCGPlayer API request failed: {str(e)}")
raise
except Exception as e:
logger.error(f"Unexpected error during TCGPlayer API request: {str(e)}")
raise
async def _get_session(self) -> aiohttp.ClientSession:
if self.session is None or self.session.closed:
self.session = aiohttp.ClientSession()
return self.session
async def close(self):
if self.session and not self.session.closed:
await self.session.close()
raise

View File

@ -1,7 +1,26 @@
from typing import Any, Dict, Optional, Union
import logging
from app.services.external_api.tcgplayer.base_tcgplayer_service import BaseTCGPlayerService
from app.schemas.tcgplayer import (
TCGPlayerAPIOrder,
TCGPlayerOrderCreate,
TCGPlayerOrderTransactionCreate,
TCGPlayerOrderProductCreate,
TCGPlayerOrderRefundCreate,
TCGPlayerAPIOrderSummary,
TCGPlayerAPIOrderSearchResponse
)
from app.models.tcgplayer_order import (
TCGPlayerOrder,
TCGPlayerOrderTransaction,
TCGPlayerOrderProduct,
TCGPlayerOrderRefund
)
from sqlalchemy.orm import Session
from app.db.database import transaction
import os
import csv
import io
logger = logging.getLogger(__name__)
@ -20,12 +39,20 @@ class OrderManagementService(BaseTCGPlayerService):
self.shipping_endpoint = f"/shipping/export{self.API_VERSION}"
async def get_orders(self, open_only: bool = False):
async def get_orders(self, search_range: str = "LastThreeMonths", open_only: bool = False, filter_out: list[str] = [], filter_in: list[str] = []) -> list[TCGPlayerAPIOrderSummary]:
"""
search range options:
LastWeek
LastMonth
LastThreeMonths
LastFourMonths
LastTwoYears
"""
search_from = 0
orders = []
while True:
payload = {
"searchRange": "LastThreeMonths",
"searchRange": search_range,
"filters": {
"sellerKey": self.SELLER_KEY
},
@ -37,17 +64,27 @@ class OrderManagementService(BaseTCGPlayerService):
"size": 25
}
if open_only:
payload["filters"]["orderStatus"] = ["Processing","ReadyToShip","Received","Pulling","ReadyForPickup"]
payload["filters"]["orderStatuses"] = ["Processing","ReadyToShip","Received","Pulling","ReadyForPickup"]
payload["filters"]["fulfillmentTypes"] = ["Normal"]
logger.info(f"Getting orders from {search_from} to {search_from + 25}")
response = await self._make_request("POST", self.order_search_endpoint, data=payload, headers=self._get_headers("POST", "application/json"), auth_required=True)
logger.info(f"Got {len(response.get('orders'))} orders")
if len(response.get("orders")) == 0:
break
search_from += 25
orders.extend(response.get("orders"))
if filter_out:
orders = [order for order in orders if order.get("orderNumber") not in filter_out]
if filter_in:
orders = [order for order in orders if order.get("orderNumber") in filter_in]
return orders
async def get_order_ids(self, search_range: str = "LastThreeMonths", open_only: bool = False, filter_out: list[str] = [], filter_in: list[str] = []):
orders = await self.get_orders(search_range, open_only, filter_out, filter_in)
return [order.get("orderNumber") for order in orders]
async def get_order(self, order_id: str):
response = await self._make_request("GET", f"{self.ORDER_MANAGEMENT_BASE_URL}/{order_id}{self.API_VERSION}")
response = await self._make_request("GET", f"/{order_id}{self.API_VERSION}")
return response
async def get_packing_slip(self, order_ids: list[str]):
@ -59,6 +96,7 @@ class OrderManagementService(BaseTCGPlayerService):
}
response = await self._make_request("POST", self.packing_slip_endpoint, data=payload, headers=self._get_headers("POST", "application/json"), auth_required=True, download_file=True)
return response
async def get_pull_sheet(self, order_ids: list[str]):
payload = {
"orderNumbers": order_ids,
@ -75,10 +113,283 @@ class OrderManagementService(BaseTCGPlayerService):
response = await self._make_request("POST", self.shipping_endpoint, data=payload, headers=self._get_headers("POST", "application/json"), auth_required=True, download_file=True)
return response
async def save_file(self, file_data: bytes, file_name: str) -> str:
async def save_file(self, file_data: Union[bytes, list[dict]], file_name: str) -> str:
if not os.path.exists("app/data/cache/tcgplayer/orders"):
os.makedirs("app/data/cache/tcgplayer/orders")
file_path = f"app/data/cache/tcgplayer/orders/{file_name}"
if isinstance(file_data, list):
# Convert list of dictionaries to CSV bytes
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=file_data[0].keys())
writer.writeheader()
writer.writerows(file_data)
file_data = output.getvalue().encode('utf-8')
with open(file_path, "wb") as f:
f.write(file_data)
return file_path
return file_path
async def save_order_to_db(self, order: dict, db: Session):
# Parse API response using our API schema
api_order = TCGPlayerAPIOrder.model_validate(order)
# Check if order exists
existing_order = db.query(TCGPlayerOrder).filter(TCGPlayerOrder.order_number == api_order.orderNumber).first()
# Create database models
db_order = TCGPlayerOrderCreate(
order_number=api_order.orderNumber,
order_created_at=api_order.createdAt,
status=api_order.status,
channel=api_order.orderChannel,
fulfillment=api_order.orderFulfillment,
seller_name=api_order.sellerName,
buyer_name=api_order.buyerName,
payment_type=api_order.paymentType,
pickup_status=api_order.pickupStatus,
shipping_type=api_order.shippingType,
estimated_delivery_date=api_order.estimatedDeliveryDate,
recipient_name=api_order.shippingAddress.recipientName,
address_line_1=api_order.shippingAddress.addressOne,
address_line_2=api_order.shippingAddress.addressTwo,
city=api_order.shippingAddress.city,
state=api_order.shippingAddress.territory,
zip_code=api_order.shippingAddress.postalCode,
country=api_order.shippingAddress.country,
tracking_numbers=[t.trackingNumber for t in api_order.trackingNumbers]
)
# Create transaction
db_transaction = TCGPlayerOrderTransactionCreate(
order_number=api_order.orderNumber,
product_amount=api_order.transaction.productAmount,
shipping_amount=api_order.transaction.shippingAmount,
gross_amount=api_order.transaction.grossAmount,
fee_amount=api_order.transaction.feeAmount,
net_amount=api_order.transaction.netAmount,
direct_fee_amount=api_order.transaction.directFeeAmount,
taxes=[{"code": t.code, "amount": t.amount} for t in api_order.transaction.taxes]
)
# Create products
db_products = [
TCGPlayerOrderProductCreate(
order_number=api_order.orderNumber,
product_name=p.name,
unit_price=p.unitPrice,
extended_price=p.extendedPrice,
quantity=p.quantity,
url=p.url,
product_id=p.productId,
sku_id=p.skuId
)
for p in api_order.products
]
# Create refunds if they exist
db_refunds = []
if api_order.refunds:
db_refunds = [
TCGPlayerOrderRefundCreate(
order_number=api_order.orderNumber,
refund_created_at=r.createdAt,
type=r.type,
amount=r.amount,
description=r.description,
origin=r.origin,
shipping_amount=r.shippingAmount,
products=r.products
)
for r in api_order.refunds
]
with transaction(db):
try:
if existing_order:
# Check if order needs to be updated
if not self._order_needs_update(existing_order, api_order, db):
logger.info(f"Order {api_order.orderNumber} is up to date, skipping update")
return existing_order
logger.info(f"Updating existing order {api_order.orderNumber}")
# Update existing order
for key, value in db_order.model_dump().items():
setattr(existing_order, key, value)
db_order_model = existing_order
# Update transaction
existing_transaction = db.query(TCGPlayerOrderTransaction).filter(
TCGPlayerOrderTransaction.order_number == api_order.orderNumber
).first()
if existing_transaction:
for key, value in db_transaction.model_dump().items():
setattr(existing_transaction, key, value)
else:
db_transaction_model = TCGPlayerOrderTransaction(**db_transaction.model_dump())
db.add(db_transaction_model)
# Delete existing products and refunds
db.query(TCGPlayerOrderProduct).filter(
TCGPlayerOrderProduct.order_number == api_order.orderNumber
).delete()
db.query(TCGPlayerOrderRefund).filter(
TCGPlayerOrderRefund.order_number == api_order.orderNumber
).delete()
else:
logger.info(f"Creating new order {api_order.orderNumber}")
# Create new order
db_order_model = TCGPlayerOrder(**db_order.model_dump())
db.add(db_order_model)
db.flush() # Get the order ID
# Create transaction
db_transaction_model = TCGPlayerOrderTransaction(**db_transaction.model_dump())
db.add(db_transaction_model)
# Create products
for product in db_products:
db_product_model = TCGPlayerOrderProduct(**product.model_dump())
db.add(db_product_model)
# Create refunds
for refund in db_refunds:
db_refund_model = TCGPlayerOrderRefund(**refund.model_dump())
db.add(db_refund_model)
db.commit()
return db_order_model
except Exception as e:
logger.error(f"Error saving/updating order {api_order.orderNumber} to database: {str(e)}")
raise
def _order_needs_update(self, existing_order: TCGPlayerOrder, new_order: TCGPlayerAPIOrder, db: Session) -> bool:
"""
Compare existing order data with new order data to determine if an update is needed.
Returns True if the order needs to be updated, False otherwise.
"""
# Compare basic order fields
order_fields_to_compare = [
('status', 'status'),
('channel', 'orderChannel'),
('fulfillment', 'orderFulfillment'),
('seller_name', 'sellerName'),
('buyer_name', 'buyerName'),
('payment_type', 'paymentType'),
('pickup_status', 'pickupStatus'),
('shipping_type', 'shippingType'),
('recipient_name', 'shippingAddress.recipientName'),
('address_line_1', 'shippingAddress.addressOne'),
('address_line_2', 'shippingAddress.addressTwo'),
('city', 'shippingAddress.city'),
('state', 'shippingAddress.territory'),
('zip_code', 'shippingAddress.postalCode'),
('country', 'shippingAddress.country'),
('tracking_numbers', 'trackingNumbers')
]
for db_field, api_field in order_fields_to_compare:
existing_value = getattr(existing_order, db_field)
# Handle nested fields
if '.' in api_field:
parts = api_field.split('.')
new_value = new_order
for part in parts:
new_value = getattr(new_value, part)
else:
new_value = getattr(new_order, api_field)
# Handle special cases for tracking numbers
if db_field == 'tracking_numbers':
if set(existing_value or []) != set([t.trackingNumber for t in new_value or []]):
return True
continue
if existing_value != new_value:
return True
# Compare transaction data
existing_transaction = db.query(TCGPlayerOrderTransaction).filter(
TCGPlayerOrderTransaction.order_number == existing_order.order_number
).first()
if existing_transaction:
transaction_fields_to_compare = [
('product_amount', 'productAmount'),
('shipping_amount', 'shippingAmount'),
('gross_amount', 'grossAmount'),
('fee_amount', 'feeAmount'),
('net_amount', 'netAmount'),
('direct_fee_amount', 'directFeeAmount')
]
for db_field, api_field in transaction_fields_to_compare:
if getattr(existing_transaction, db_field) != getattr(new_order.transaction, api_field):
return True
# Compare taxes
existing_taxes = sorted(existing_transaction.taxes, key=lambda x: x['code'])
new_taxes = sorted(new_order.transaction.taxes, key=lambda x: x.code)
if len(existing_taxes) != len(new_taxes):
return True
for existing_tax, new_tax in zip(existing_taxes, new_taxes):
if existing_tax['code'] != new_tax.code or existing_tax['amount'] != new_tax.amount:
return True
# Compare products
existing_products = db.query(TCGPlayerOrderProduct).filter(
TCGPlayerOrderProduct.order_number == existing_order.order_number
).all()
if len(existing_products) != len(new_order.products):
return True
# Sort products by product_id for comparison
existing_products_sorted = sorted(existing_products, key=lambda x: x.product_id)
new_products_sorted = sorted(new_order.products, key=lambda x: x.productId)
for existing_product, new_product in zip(existing_products_sorted, new_products_sorted):
product_fields_to_compare = [
('product_name', 'name'),
('unit_price', 'unitPrice'),
('extended_price', 'extendedPrice'),
('quantity', 'quantity'),
('url', 'url'),
('product_id', 'productId'),
('sku_id', 'skuId')
]
for db_field, api_field in product_fields_to_compare:
if getattr(existing_product, db_field) != getattr(new_product, api_field):
return True
# Compare refunds
existing_refunds = db.query(TCGPlayerOrderRefund).filter(
TCGPlayerOrderRefund.order_number == existing_order.order_number
).all()
if len(existing_refunds) != len(new_order.refunds or []):
return True
# Sort refunds by created_at for comparison
existing_refunds_sorted = sorted(existing_refunds, key=lambda x: x.refund_created_at)
new_refunds_sorted = sorted(new_order.refunds or [], key=lambda x: x.createdAt)
for existing_refund, new_refund in zip(existing_refunds_sorted, new_refunds_sorted):
refund_fields_to_compare = [
('type', 'type'),
('amount', 'amount'),
('description', 'description'),
('origin', 'origin'),
('shipping_amount', 'shippingAmount'),
('products', 'products')
]
for db_field, api_field in refund_fields_to_compare:
if getattr(existing_refund, db_field) != getattr(new_refund, api_field):
return True
return False