order and api and more
This commit is contained in:
@ -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
|
@ -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
|
||||
|
Reference in New Issue
Block a user