Files
orion/scripts/check_letzshop_shipment.py
Samir Boulahtit a19c84ea4e feat: integer cents money handling, order page fixes, and vendor filter persistence
Money Handling Architecture:
- Store all monetary values as integer cents (€105.91 = 10591)
- Add app/utils/money.py with Money class and conversion helpers
- Add static/shared/js/money.js for frontend formatting
- Update all database models to use _cents columns (Product, Order, etc.)
- Update CSV processor to convert prices to cents on import
- Add Alembic migration for Float to Integer conversion
- Create .architecture-rules/money.yaml with 7 validation rules
- Add docs/architecture/money-handling.md documentation

Order Details Page Fixes:
- Fix customer name showing 'undefined undefined' - use flat field names
- Fix vendor info empty - add vendor_name/vendor_code to OrderDetailResponse
- Fix shipping address using wrong nested object structure
- Enrich order detail API response with vendor info

Vendor Filter Persistence Fixes:
- Fix orders.js: restoreSavedVendor now sets selectedVendor and filters
- Fix orders.js: init() only loads orders if no saved vendor to restore
- Fix marketplace-letzshop.js: restoreSavedVendor calls selectVendor()
- Fix marketplace-letzshop.js: clearVendorSelection clears TomSelect dropdown
- Align vendor selector placeholder text between pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 20:33:48 +01:00

207 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""
Check shipment status directly from Letzshop API.
Usage:
python scripts/check_letzshop_shipment.py YOUR_API_KEY [SHIPMENT_ID_OR_ORDER_NUMBER]
Example:
python scripts/check_letzshop_shipment.py abc123 nvDv5RQEmCwbjo
python scripts/check_letzshop_shipment.py abc123 R532332163
"""
import sys
import json
import requests
ENDPOINT = "https://letzshop.lu/graphql"
# Query template - state is interpolated since Letzshop has enum issues
QUERY_SHIPMENTS_TEMPLATE = """
query GetShipmentsPaginated($first: Int!, $after: String) {{
shipments(state: {state}, first: $first, after: $after) {{
pageInfo {{
hasNextPage
endCursor
}}
nodes {{
id
number
state
order {{
id
number
email
total
completedAt
locale
shipAddress {{
firstName
lastName
company
streetName
streetNumber
city
zipCode
phone
country {{
iso
}}
}}
}}
inventoryUnits {{
id
state
variant {{
id
sku
mpn
price
tradeId {{
number
parser
}}
product {{
name {{
en
fr
de
}}
}}
}}
}}
}}
}}
}}
"""
def search_shipment(api_key: str, search_term: str):
"""Search for a shipment across all states."""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
# States to search through
states = ["shipped", "confirmed", "unconfirmed", "declined"]
print(f"Searching for: {search_term}")
print("=" * 60)
for state in states:
print(f"\nSearching in state: {state}...")
query = QUERY_SHIPMENTS_TEMPLATE.format(state=state)
# Paginate through results
has_next = True
after = None
page = 0
while has_next and page < 20: # Max 20 pages to avoid infinite loop
page += 1
variables = {"first": 50, "after": after}
response = requests.post(
ENDPOINT,
headers=headers,
json={"query": query, "variables": variables},
)
if response.status_code != 200:
print(f" Error: HTTP {response.status_code}")
break
data = response.json()
if "errors" in data:
print(f" GraphQL errors: {data['errors']}")
break
result = data.get("data", {}).get("shipments", {})
nodes = result.get("nodes", [])
page_info = result.get("pageInfo", {})
# Search for matching shipment
for shipment in nodes:
shipment_id = shipment.get("id", "")
shipment_number = shipment.get("number", "")
order = shipment.get("order", {})
order_number = order.get("number", "")
# Check if this matches our search term
if (search_term in shipment_id or
search_term in shipment_number or
search_term in order_number or
search_term == shipment_id or
search_term == order_number):
print(f"\n{'=' * 60}")
print(f"FOUND SHIPMENT!")
print(f"{'=' * 60}")
print(f"\n--- Shipment Info ---")
print(f" ID: {shipment.get('id')}")
print(f" Number: {shipment.get('number')}")
print(f" State: {shipment.get('state')}")
print(f"\n--- Order Info ---")
print(f" Order ID: {order.get('id')}")
print(f" Order Number: {order.get('number')}")
print(f" Email: {order.get('email')}")
print(f" Total: {order.get('total')}")
print(f" Completed At: {order.get('completedAt')}")
ship_addr = order.get('shipAddress', {})
if ship_addr:
print(f"\n--- Shipping Address ---")
print(f" Name: {ship_addr.get('firstName')} {ship_addr.get('lastName')}")
print(f" Street: {ship_addr.get('streetName')} {ship_addr.get('streetNumber')}")
print(f" City: {ship_addr.get('zipCode')} {ship_addr.get('city')}")
country = ship_addr.get('country', {})
print(f" Country: {country.get('iso')}")
print(f"\n--- Inventory Units ---")
units = shipment.get('inventoryUnits', [])
for i, unit in enumerate(units, 1):
print(f" Unit {i}:")
print(f" ID: {unit.get('id')}")
print(f" State: {unit.get('state')}")
variant = unit.get('variant', {})
print(f" SKU: {variant.get('sku')}")
trade_id = variant.get('tradeId', {})
print(f" GTIN: {trade_id.get('number')}")
product = variant.get('product', {})
name = product.get('name', {})
print(f" Product: {name.get('en')}")
print(f"\n--- Raw Response ---")
print(json.dumps(shipment, indent=2, default=str))
return shipment
has_next = page_info.get("hasNextPage", False)
after = page_info.get("endCursor")
if not has_next:
print(f" Searched {page} page(s), {len(nodes)} shipments in last page")
print(f"\nShipment not found for: {search_term}")
return None
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python scripts/check_letzshop_shipment.py YOUR_API_KEY [SHIPMENT_ID_OR_ORDER_NUMBER]")
print("\nExample:")
print(" python scripts/check_letzshop_shipment.py abc123 nvDv5RQEmCwbjo")
print(" python scripts/check_letzshop_shipment.py abc123 R532332163")
sys.exit(1)
api_key = sys.argv[1]
# Default to the order number R532332163
search_term = sys.argv[2] if len(sys.argv) > 2 else "R532332163"
search_shipment(api_key, search_term)