This commit is contained in:
zman 2025-02-21 11:46:50 -05:00
parent 721b26ce97
commit 1bf255d0fe
12 changed files with 425 additions and 91 deletions

1
.gitignore vendored
View File

@ -174,3 +174,4 @@ temp/
.DS_Store
*.db-journal
cookies/
alembic/versions/*

View File

@ -328,6 +328,41 @@ class TCGPlayerGroups(Base):
modified_on = Column(String)
category_id = Column(Integer)
class Orders(Base):
__tablename__ = 'orders'
id = Column(String, primary_key=True)
order_id = Column(String, unique=True)
buyer_name = Column(String)
recipient_name = Column(String)
recipient_address_one = Column(String)
recipient_address_two = Column(String)
recipient_city = Column(String)
recipient_state = Column(String)
recipient_zip = Column(String)
recipient_country = Column(String)
order_date = Column(String)
status = Column(String)
num_products = Column(Integer)
num_cards = Column(Integer)
product_amount = Column(Float)
shipping_amount = Column(Float)
gross_amount = Column(Float)
fee_amount = Column(Float)
net_amount = Column(Float)
direct_fee_amount = Column(Float)
date_created = Column(DateTime, default=datetime.now)
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class OrderProducts(Base):
__tablename__ = 'order_products'
id = Column(String, primary_key=True)
order_id = Column(String, ForeignKey('orders.id'))
product_id = Column(String, ForeignKey('products.id'))
quantity = Column(Integer)
unit_price = Column(Float)
# enums
class RarityEnum(str, Enum):

View File

@ -10,6 +10,7 @@ from app.services.product import ProductService
from app.services.inventory import InventoryService
from app.services.task import TaskService
from app.services.storage import StorageService
from app.services.tcgplayer_api import TCGPlayerAPIService
from app.db.database import get_db
from app.schemas.file import CreateFileRequest
from app.schemas.box import CreateBoxRequest, UpdateBoxRequest, CreateOpenBoxRequest
@ -18,6 +19,10 @@ from app.schemas.box import CreateBoxRequest, UpdateBoxRequest, CreateOpenBoxReq
DB = Annotated[Session, Depends(get_db)]
# Base Services (no dependencies besides DB)
def get_tcgplayer_api_service(db: DB) -> TCGPlayerAPIService:
"""TCGPlayerAPIService with only database dependency"""
return TCGPlayerAPIService(db)
def get_file_service(db: DB) -> FileService:
"""FileService with only database dependency"""
return FileService(db)

View File

@ -25,10 +25,12 @@ from app.schemas.box import (
CreateOpenBoxResponse,
OpenBoxSchema
)
from app.schemas.orders import ProcessOrdersResponse
from app.services.file import FileService
from app.services.box import BoxService
from app.services.task import TaskService
from app.services.pricing import PricingService
from app.services.tcgplayer_api import TCGPlayerAPIService
from app.dependencies import (
get_file_service,
get_box_service,
@ -37,7 +39,8 @@ from app.dependencies import (
get_box_data,
get_box_update_data,
get_open_box_data,
get_pricing_service
get_pricing_service,
get_tcgplayer_api_service
)
logger = logging.getLogger(__name__)
@ -313,3 +316,23 @@ async def update_cookies(
status_code=500,
detail=f"Failed to update cookies: {str(e)}"
)
class TCGPlayerOrderRequest(BaseModel):
order_ids: List[str]
@router.post("/processOrders", response_model=ProcessOrdersResponse)
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)
return ProcessOrdersResponse(
status_code=200,
success=True,
orders=orders
)
except Exception as e:
logger.error(f"Process orders failed: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))

View File

@ -1,19 +0,0 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime
# FILE
class OrderSchema(BaseModel):
id: str = Field(..., title="id")
filename: str = Field(..., title="filename")
type: str = Field(..., title="type")
filesize_kb: float = Field(..., title="filesize_kb")
source: str = Field(..., title="source")
status: str = Field(..., title="status")
service: Optional[str] = Field(None, title="service")
date_created: datetime = Field(..., title="date_created")
date_modified: datetime = Field(..., title="date_modified")
# This enables ORM mode
model_config = ConfigDict(from_attributes=True)

9
app/schemas/orders.py Normal file
View File

@ -0,0 +1,9 @@
from pydantic import BaseModel
class OrderSchema(BaseModel):
order_id: str
class ProcessOrdersResponse(BaseModel):
status_code: int
success: bool
orders: list[str]

View File

@ -10,7 +10,7 @@ import io
# Printer settings
printer_model = "QL-1100"
backend = 'pyusb' # Changed from network to USB
backend = 'pyusb'
printer = 'usb://0x04f9:0x20a7'
def convert_pdf_to_image(pdf_path):
@ -45,6 +45,8 @@ def create_address_label(input_data, font_size=30, is_pdf=False):
font = ImageFont.truetype("C:\\Windows\\Fonts\\arial.ttf", size=font_size)
elif platform.system() == 'Darwin':
font = ImageFont.truetype("/Library/Fonts/Arial.ttf", size=font_size)
elif platform.system() == 'Linux':
font = ImageFont.truetype("/usr/share/fonts/truetype/msttcorefonts/arial.ttf", size=font_size)
margin = 20
lines = input_data.split('\n')
@ -71,10 +73,8 @@ def print_address_label(input_data, font_size=30, is_pdf=False, label_size='29x9
if not image:
raise Exception("Failed to create label image")
# For 4x6 shipping labels from Pirate Ship
if label_size == '4x6':
# Resize image to fit 4x6 format if needed
target_width = 1164 # Adjusted for 4x6 format
target_width = 1164
target_height = 1660
image = image.resize((target_width, target_height), Image.LANCZOS)
@ -92,7 +92,6 @@ def print_address_label(input_data, font_size=30, is_pdf=False, label_size='29x9
red=False,
dpi_600=False,
hq=True,
#cut=True
cut=False
)
@ -135,33 +134,23 @@ def process_tcg_shipping_export(file_path, require_input=False, font_size=60, pr
else:
sleep(1)
# Example usage
if __name__ == "__main__":
# Example for regular address label
address = """John Doe
123 Main Street
Apt 4B
City, State 12345"""
# Example for TCG Player export
shipping_export_file = "_TCGplayer_ShippingExport_20250201_115949.csv"
# Example for Pirate Ship PDF
pirate_ship_pdf = "C:\\Users\\joshu\\Downloads\\2025-02-10---greg-creek---9400136208070411592215.pdf"
# Choose which type to process
label_type = input("Enter label type (1 for regular, 2 for TCG, 3 for Pirate Ship): ")
if label_type == "1":
address = input("Enter the address to print: ")
preview_label(address, font_size=60)
user_input = input("Press 'p' to print the label or any other key to cancel: ")
if user_input.lower() == 'p':
print_address_label(address, font_size=60)
elif label_type == "2":
shipping_export_file = input("Enter the path to the TCG Player shipping export CSV file: ")
process_tcg_shipping_export(shipping_export_file, font_size=60, preview=False)
elif label_type == "3":
pirate_ship_pdf = input("Enter the path to the Pirate Ship PDF file: ")
process_pirate_ship_pdf(pirate_ship_pdf, preview=True)
user_input = input("Press 'p' to print the label or any other key to cancel: ")
if user_input.lower() == 'p':

View File

@ -21,6 +21,7 @@ import pandas as pd
from sqlalchemy.exc import SQLAlchemyError
from app.schemas.file import CreateFileRequest
import os
from app.services.util._docker import DockerUtil
logger = logging.getLogger(__name__)
@ -53,6 +54,7 @@ class TCGPlayerService:
self.previous_request_time = None
self.df_util = DataframeUtil()
self.file_service = file_service
self.docker_util = DockerUtil()
def _insert_groups(self, groups):
for group in groups:
@ -119,47 +121,6 @@ class TCGPlayerService:
logger.error(f"Failed to get browser cookies: {str(e)}")
return None
def is_in_docker(self) -> bool:
"""Check if we're running inside a Docker container using multiple methods"""
# Method 1: Check cgroup
try:
with open('/proc/1/cgroup', 'r') as f:
content = f.read().lower()
if any(container_id in content for container_id in ['docker', 'containerd', 'kubepods']):
logger.debug("Docker detected via cgroup")
return True
except Exception as e:
logger.debug(f"Could not read cgroup file: {e}")
# Method 2: Check /.dockerenv file
if os.path.exists('/.dockerenv'):
logger.debug("Docker detected via /.dockerenv file")
return True
# Method 3: Check environment variables
docker_env = any(os.environ.get(var, False) for var in [
'DOCKER_CONTAINER',
'IN_DOCKER',
'KUBERNETES_SERVICE_HOST', # For k8s
'DOCKER_HOST'
])
if docker_env:
logger.debug("Docker detected via environment variables")
return True
# Method 4: Check container runtime
try:
with open('/proc/self/mountinfo', 'r') as f:
content = f.read().lower()
if any(rt in content for rt in ['docker', 'containerd', 'kubernetes']):
logger.debug("Docker detected via mountinfo")
return True
except Exception as e:
logger.debug(f"Could not read mountinfo: {e}")
logger.debug("No Docker environment detected")
return False
def _send_request(self, url: str, method: str, data=None, except_302=False) -> requests.Response:
"""Send a request with the specified cookies"""
# Rate limiting logic
@ -173,7 +134,7 @@ class TCGPlayerService:
# Move cookie initialization outside and make it more explicit
if not self.cookies:
if self.is_in_docker():
if self.docker_util.is_in_docker():
logger.debug("Running in Docker - using cookies from file")
self.cookies = self.get_cookies_from_file()
else:

View File

@ -0,0 +1,105 @@
from dataclasses import dataclass
import logging
from app.db.models import Orders, OrderProducts, CardTCGPlayer
from app.services.util._requests import RequestsUtil
from app.services.util._docker import DockerUtil
from app.db.utils import db_transaction
from sqlalchemy.orm import Session
from uuid import uuid4 as uuid
logger = logging.getLogger(__name__)
@dataclass
class TCGPlayerAPIConfig:
"""Configuration for TCGPlayer API"""
ORDER_BASE_URL: str = "https://order-management-api.tcgplayer.com/orders"
API_VERSION: str = "?api-version=2.0"
class TCGPlayerAPIService:
def __init__(self, db: Session):
self.db = db
self.docker_util = DockerUtil()
self.requests_util = RequestsUtil()
self.is_in_docker = self.docker_util.is_in_docker()
self.config = TCGPlayerAPIConfig()
self.cookies = self.get_cookies()
def get_cookies(self) -> dict:
if self.is_in_docker:
return self.requests_util.get_tcgplayer_cookies_from_file()
else:
return self.requests_util.get_tcgplayer_browser_cookies()
def get_order(self, order_id: str) -> dict:
url = f"{self.config.ORDER_BASE_URL}/{order_id}{self.config.API_VERSION}"
response = self.requests_util.send_request(url, method='GET', cookies=self.cookies)
if response:
return response.json()
return None
def get_product_ids_from_sku(self, sku_ids: list[str]) -> dict:
"""Get product IDs from TCGPlayer SKU IDs"""
tcg_cards = self.db.query(CardTCGPlayer).filter(CardTCGPlayer.tcgplayer_id.in_(sku_ids)).all()
return {card.tcgplayer_id: card.product_id for card in tcg_cards}
def save_order(self, order: dict):
# check if order exists by order number
order_number = order['orderNumber']
existing_order = self.db.query(Orders).filter(Orders.order_id == order_number).first()
if existing_order:
logger.info(f"Order {order_number} already exists in database")
return existing_order
transaction = order['transaction']
shipping = order['shippingAddress']
products = order['products']
with db_transaction(self.db):
db_order = Orders(
id = str(uuid()),
order_id=order_number,
buyer_name=order['buyerName'],
recipient_name=shipping['recipientName'],
recipient_address_one=shipping['addressOne'],
recipient_address_two=shipping['addressTwo'] if 'addressTwo' in shipping else '',
recipient_city=shipping['city'],
recipient_state=shipping['territory'],
recipient_zip=shipping['postalCode'],
recipient_country=shipping['country'],
order_date=order['createdAt'],
status=order['status'],
num_products=len(products),
num_cards=sum([product['quantity'] for product in products]),
product_amount=transaction['productAmount'],
shipping_amount=transaction['shippingAmount'],
gross_amount=transaction['grossAmount'],
fee_amount=transaction['feeAmount'],
net_amount=transaction['netAmount'],
direct_fee_amount=transaction['directFeeAmount']
)
self.db.add(db_order)
product_ids = [product['skuId'] for product in products]
sku_to_product_id_mapping = self.get_product_ids_from_sku(product_ids)
order_products = []
for product in products:
product_id = sku_to_product_id_mapping.get(product['skuId'])
if product_id:
order_products.append(
OrderProducts(
id=str(uuid()),
order_id=db_order.id,
product_id=product_id,
quantity=product['quantity'],
unit_price=product['unitPrice']
)
)
self.db.add_all(order_products)
return db_order
def process_orders(self, orders: list[str]):
processed_orders = []
for order_id in orders:
order = self.get_order(order_id)
if order:
self.save_order(order)
processed_orders.append(order_id)
return processed_orders

View File

@ -0,0 +1,49 @@
import os
import logging
logger = logging.getLogger(__name__)
class DockerUtil:
def __init__(self):
pass
def is_in_docker(self) -> bool:
"""Check if we're running inside a Docker container using multiple methods"""
# Method 1: Check cgroup
try:
with open('/proc/1/cgroup', 'r') as f:
content = f.read().lower()
if any(container_id in content for container_id in ['docker', 'containerd', 'kubepods']):
logger.debug("Docker detected via cgroup")
return True
except Exception as e:
logger.debug(f"Could not read cgroup file: {e}")
# Method 2: Check /.dockerenv file
if os.path.exists('/.dockerenv'):
logger.debug("Docker detected via /.dockerenv file")
return True
# Method 3: Check environment variables
docker_env = any(os.environ.get(var, False) for var in [
'DOCKER_CONTAINER',
'IN_DOCKER',
'KUBERNETES_SERVICE_HOST', # For k8s
'DOCKER_HOST'
])
if docker_env:
logger.debug("Docker detected via environment variables")
return True
# Method 4: Check container runtime
try:
with open('/proc/self/mountinfo', 'r') as f:
content = f.read().lower()
if any(rt in content for rt in ['docker', 'containerd', 'kubernetes']):
logger.debug("Docker detected via mountinfo")
return True
except Exception as e:
logger.debug(f"Could not read mountinfo: {e}")
logger.debug("No Docker environment detected")
return False

View File

@ -0,0 +1,172 @@
from typing import Dict, Optional
from app.services.util._docker import DockerUtil
from enum import Enum
import browser_cookie3
import os
import json
import requests
import time
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class Browser(Enum):
"""Supported browser types for cookie extraction"""
BRAVE = "brave"
CHROME = "chrome"
FIREFOX = "firefox"
class Method(Enum):
"""Supported HTTP methods"""
GET = "GET"
POST = "POST"
class TCGPlayerEndpoints(Enum):
"""Supported TCGPlayer API endpoints"""
ORDERS = "https://order-management-api.tcgplayer.com/orders"
class Headers:
ACCEPT = 'application/json, text/plain, */*'
ACCEPT_ENCODING = 'gzip, deflate, br, zstd'
ACCEPT_LANGUAGE = 'en-US,en;q=0.8'
PRIORITY = 'u=1, i'
SECCHUA = '"Not(A:Brand";v="99", "Brave";v="133", "Chromium";v="133"'
SECCHUA_MOBILE = '?0'
SECCHUA_PLATFORM = '"macOS"'
SEC_FETCH_DEST = 'empty'
SEC_FETCH_MODE = 'cors'
SEC_FETCH_SITE = 'same-site'
SEC_GPC = '1'
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'
SELLER_ORIGIN = 'https://sellerportal.tcgplayer.com'
SELLER_REFERER = 'https://sellerportal.tcgplayer.com/'
class RequestHeaders:
BASE_HEADERS = {
'accept': Headers.ACCEPT,
'accept-encoding': Headers.ACCEPT_ENCODING,
'accept-language': Headers.ACCEPT_LANGUAGE,
'priority': Headers.PRIORITY,
'sec-ch-ua': Headers.SECCHUA,
'sec-ch-ua-mobile': Headers.SECCHUA_MOBILE,
'sec-ch-ua-platform': Headers.SECCHUA_PLATFORM,
'sec-fetch-dest': Headers.SEC_FETCH_DEST,
'sec-fetch-mode': Headers.SEC_FETCH_MODE,
'sec-fetch-site': Headers.SEC_FETCH_SITE,
'sec-gpc': Headers.SEC_GPC,
'user-agent': Headers.USER_AGENT
}
SELLER_HEADERS = {
'origin': Headers.SELLER_ORIGIN,
'referer': Headers.SELLER_REFERER
}
class URLHeaders:
# combine base and seller headers
ORDER_HEADERS = {**RequestHeaders.BASE_HEADERS, **RequestHeaders.SELLER_HEADERS}
class RequestsUtil:
def __init__(self, browser_type: Browser = Browser.BRAVE):
self.browser_type = browser_type
self.docker_util = DockerUtil()
def get_tcgplayer_cookies_from_file(self) -> Dict:
# check if cookies file exists
if not os.path.exists('cookies/tcg_cookies.json'):
raise ValueError("Cookies file not found")
with open('cookies/tcg_cookies.json', 'r') as f:
logger.debug("Loading cookies from file")
cookies = json.load(f)
return cookies
def get_tcgplayer_browser_cookies(self) -> Optional[Dict]:
"""Retrieve cookies from the specified browser"""
try:
cookie_getter = getattr(browser_cookie3, self.browser_type.value, None)
if not cookie_getter:
raise ValueError(f"Unsupported browser type: {self.browser_type.value}")
return cookie_getter()
except Exception as e:
logger.error(f"Failed to get browser cookies: {str(e)}")
return None
def rate_limit(self, time_between_requests: int = 10):
"""Rate limit requests by waiting for a specified time between requests"""
time_diff = (datetime.now() - self.previous_request_time).total_seconds()
if time_diff < time_between_requests:
# logger.info(f"Waiting {time_between_requests - time_diff} seconds before next request...")
time.sleep(time_between_requests - time_diff)
def send_request(self, url: str, method: str, cookies: dict, data=None) -> requests.Response:
"""Send a request with the specified cookies"""
headers = self.set_headers(url)
if not headers:
raise ValueError("Headers not set")
try:
response = requests.request(method, url, headers=headers, cookies=cookies, data=data)
response.raise_for_status()
self.previous_request_time = datetime.now()
return response
except requests.RequestException as e:
logger.error(f"Request failed: {str(e)}")
return None
def set_headers(self, url: str):
# use tcgplayerendpoints enum to set headers where url partially matches enum value
for endpoint in TCGPlayerEndpoints:
if endpoint.value in url:
return URLHeaders.ORDER_HEADERS
else:
raise ValueError(f"Endpoint not found in TCGPlayerEndpoints: {url}")
def old_set_headers(self, method: str) -> Dict:
base_headers = {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'accept-language': 'en-US,en;q=0.8',
'priority': 'u=0, i',
'referer': 'https://store.tcgplayer.com/admin/pricing',
'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Brave";v="132"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'same-origin',
'sec-fetch-user': '?1',
'sec-gpc': '1',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36'
}
if method == 'POST':
post_headers = {
'cache-control': 'max-age=0',
'content-type': 'application/x-www-form-urlencoded',
'origin': 'https://store.tcgplayer.com'
}
base_headers.update(post_headers)
return base_headers
"""
curl 'https://order-management-api.tcgplayer.com/orders/E576ED4C-38871F-B0277?api-version=2.0' \
-H 'accept: application/json, text/plain, */*' \
-H 'accept-language: en-US,en;q=0.8' \
-b 'tcgpartner=PK=TRADECARDS&M=1; valid=set=true; product-display-settings=sort=price+shipping&size=10; TCG_Data=M=1&SearchGameNameID=magic; tcg-uuid=ab16b5f8-dd66-446d-b217-d394328a5cf1; setting=CD=US&M=1; tracking-preferences={%22version%22:1%2C%22destinations%22:{%22Actions%20Amplitude%22:false%2C%22AdWords%22:false%2C%22Google%20AdWords%20New%22:false%2C%22Google%20Enhanced%20Conversions%22:false%2C%22Google%20Tag%20Manager%22:false%2C%22Impact%20Partnership%20Cloud%22:false%2C%22Optimizely%22:false}%2C%22custom%22:{%22advertising%22:false%2C%22functional%22:false%2C%22marketingAndAnalytics%22:false}}; OAuthLoginSessionId=63b1a89d-1ac2-43f7-8e79-55a9ca5e761d; TCGAuthTicket_Production=F453BFF2FA3FAA3D1ACA23F319314F8273713DECEB06C69BB7C208A77C81559B46E1AA22A7E70FABC8D7F681A423C86870FAE318B76048CDE7BF6D73D220631B899BEBA86C422E1EBBF2ACD1921E0846F708AFE203C844031364E13B047465E7B41CB6460E4F4AAB278B614445B93E722E976688; BuyerRevalidationKey=; ASP.NET_SessionId=oouwzrh3jkhdrmaioooqhr4k; TCG_VisitorKey=431efcca-2d5b-404d-a04f-3ae979696051; __RequestVerificationToken_L2FkbWlu0=Lw1sfWh823UeJ7zRux0b1ZTI4Vg4i_dFt97a55aQpf-qBURVuwWDCJyuCxSwgLNLe9nPlfDSc1AMV5nyqhY4Q4jurxs1; fileDownloadToken=1740145585435; spDisabledUIFeatures=' \
-H 'origin: https://sellerportal.tcgplayer.com' \
-H 'priority: u=1, i' \
-H 'referer: https://sellerportal.tcgplayer.com/' \
-H 'sec-ch-ua: "Not(A:Brand";v="99", "Brave";v="133", "Chromium";v="133"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "macOS"' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: same-site' \
-H 'sec-gpc: 1' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'
"""

View File

@ -2,16 +2,20 @@ curl -J http://192.168.1.41:8000/api/tcgplayer/inventory/update --remote-name
curl -J -X POST http://192.168.1.41:8000/api/tcgplayer/inventory/add \
-H "Content-Type: application/json" \
-d '{"open_box_ids": ["fb629d9d-13d2-405e-9a69-6c44294d55de"]}' \
-d '{"open_box_ids": ["02d14109-ad0c-41f9-b49f-24134f962c1a"]}' \
--remote-name
curl -X POST http://192.168.1.41:8000/api/boxes \
-F "type=draft" \
-F "set_code=CLB" \
-F "sku=195166181127" \
-F "num_cards_expected=480"
-F "type=play" \
-F "set_code=DFT" \
-F "sku=195166278636" \
-F "num_cards_expected=422"
curl -X POST "http://192.168.1.41:8000/api/boxes/0d31b9c3-3093-438a-9e8c-b6b70a2d437e/open" \
-F "product_id=0d31b9c3-3093-438a-9e8c-b6b70a2d437e" \
-F "file_ids=bb4a022c-5427-49b5-b57c-0147b5e9c4a9" \
-F "date_opened=2025-02-15"
curl -X POST "http://192.168.1.41:8000/api/boxes/0d605f46-25b5-4784-8d45-38dec144ec8e/open" \
-F "product_id=0d605f46-25b5-4784-8d45-38dec144ec8e" \
-F "file_ids=61124a9d-8db3-49aa-bf56-ec0d784ca817" \
-F "date_opened=2025-02-17"
curl -X POST "http://127.0.0.1:8000/api/processOrders" \
-H "Content-Type: application/json" \
-d '{"order_ids": ["E576ED4C-A1117E-6904E"]}'