feat: enhance Letzshop order import with EAN matching and stats

- Add historical order import with pagination support
- Add customer_locale, shipping_country_iso, billing_country_iso columns
- Add gtin/gtin_type columns to Product table for EAN matching
- Fix order stats to count all orders server-side (not just visible page)
- Add GraphQL introspection script with tracking workaround tests
- Enrich inventory units with EAN, MPN, SKU, product name
- Add LetzshopOrderStats schema for proper status counts

Migrations:
- a9a86cef6cca: Add locale and country fields to letzshop_orders
- cb88bc9b5f86: Add gtin columns to products table

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 21:04:33 +01:00
parent 6d6c8b44d3
commit 0ab10128ae
17 changed files with 3451 additions and 94 deletions

View File

@@ -447,3 +447,303 @@ class TestLetzshopClient:
client.get_shipments()
assert "Invalid shipment ID" in str(exc_info.value)
@patch("requests.Session.post")
def test_get_all_shipments_paginated(self, mock_post):
"""Test paginated shipment fetching."""
# First page response
page1_response = MagicMock()
page1_response.status_code = 200
page1_response.json.return_value = {
"data": {
"shipments": {
"pageInfo": {
"hasNextPage": True,
"endCursor": "cursor_1",
},
"nodes": [
{"id": "ship_1", "state": "confirmed"},
{"id": "ship_2", "state": "confirmed"},
],
}
}
}
# Second page response
page2_response = MagicMock()
page2_response.status_code = 200
page2_response.json.return_value = {
"data": {
"shipments": {
"pageInfo": {
"hasNextPage": False,
"endCursor": None,
},
"nodes": [
{"id": "ship_3", "state": "confirmed"},
],
}
}
}
mock_post.side_effect = [page1_response, page2_response]
client = LetzshopClient(api_key="test-key")
shipments = client.get_all_shipments_paginated(
state="confirmed",
page_size=2,
)
assert len(shipments) == 3
assert shipments[0]["id"] == "ship_1"
assert shipments[2]["id"] == "ship_3"
@patch("requests.Session.post")
def test_get_all_shipments_paginated_with_max_pages(self, mock_post):
"""Test paginated fetching respects max_pages limit."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": {
"shipments": {
"pageInfo": {
"hasNextPage": True,
"endCursor": "cursor_1",
},
"nodes": [
{"id": "ship_1", "state": "confirmed"},
],
}
}
}
mock_post.return_value = mock_response
client = LetzshopClient(api_key="test-key")
shipments = client.get_all_shipments_paginated(
state="confirmed",
page_size=1,
max_pages=1, # Only fetch 1 page
)
assert len(shipments) == 1
assert mock_post.call_count == 1
@patch("requests.Session.post")
def test_get_all_shipments_paginated_with_callback(self, mock_post):
"""Test paginated fetching calls progress callback."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": {
"shipments": {
"pageInfo": {"hasNextPage": False, "endCursor": None},
"nodes": [{"id": "ship_1"}],
}
}
}
mock_post.return_value = mock_response
callback_calls = []
def callback(page, total):
callback_calls.append((page, total))
client = LetzshopClient(api_key="test-key")
client.get_all_shipments_paginated(
state="confirmed",
progress_callback=callback,
)
assert len(callback_calls) == 1
assert callback_calls[0] == (1, 1)
# ============================================================================
# Order Service Tests
# ============================================================================
@pytest.mark.unit
@pytest.mark.letzshop
class TestLetzshopOrderService:
"""Test suite for Letzshop order service."""
def test_create_order_extracts_locale(self, db, test_vendor):
"""Test that create_order extracts customer locale."""
from app.services.letzshop.order_service import LetzshopOrderService
service = LetzshopOrderService(db)
shipment_data = {
"id": "ship_123",
"state": "confirmed",
"order": {
"id": "order_123",
"number": "R123456",
"email": "test@example.com",
"total": 29.99,
"locale": "fr",
"shipAddress": {
"firstName": "Jean",
"lastName": "Dupont",
"country": {"iso": "LU"},
},
"billAddress": {
"country": {"iso": "FR"},
},
},
"inventoryUnits": [],
}
order = service.create_order(test_vendor.id, shipment_data)
assert order.customer_locale == "fr"
assert order.shipping_country_iso == "LU"
assert order.billing_country_iso == "FR"
def test_create_order_extracts_ean(self, db, test_vendor):
"""Test that create_order extracts EAN from tradeId."""
from app.services.letzshop.order_service import LetzshopOrderService
service = LetzshopOrderService(db)
shipment_data = {
"id": "ship_123",
"state": "confirmed",
"order": {
"id": "order_123",
"number": "R123456",
"email": "test@example.com",
"total": 29.99,
"shipAddress": {},
},
"inventoryUnits": [
{
"id": "unit_1",
"state": "confirmed",
"variant": {
"id": "var_1",
"sku": "SKU123",
"mpn": "MPN456",
"price": 19.99,
"tradeId": {
"number": "0889698273022",
"parser": "gtin13",
},
"product": {
"name": {"en": "Test Product", "fr": "Produit Test"},
},
},
}
],
}
order = service.create_order(test_vendor.id, shipment_data)
assert len(order.inventory_units) == 1
unit = order.inventory_units[0]
assert unit["ean"] == "0889698273022"
assert unit["ean_type"] == "gtin13"
assert unit["sku"] == "SKU123"
assert unit["mpn"] == "MPN456"
assert unit["product_name"] == "Test Product"
assert unit["price"] == 19.99
def test_import_historical_shipments_deduplication(self, db, test_vendor):
"""Test that historical import deduplicates existing orders."""
from app.services.letzshop.order_service import LetzshopOrderService
service = LetzshopOrderService(db)
shipment_data = {
"id": "ship_existing",
"state": "confirmed",
"order": {
"id": "order_123",
"number": "R123456",
"email": "test@example.com",
"total": 29.99,
"shipAddress": {},
},
"inventoryUnits": [],
}
# Create first order
service.create_order(test_vendor.id, shipment_data)
db.commit()
# Import same shipment again
stats = service.import_historical_shipments(
vendor_id=test_vendor.id,
shipments=[shipment_data],
match_products=False,
)
assert stats["total"] == 1
assert stats["imported"] == 0
assert stats["skipped"] == 1
def test_import_historical_shipments_new_orders(self, db, test_vendor):
"""Test that historical import creates new orders."""
from app.services.letzshop.order_service import LetzshopOrderService
service = LetzshopOrderService(db)
shipments = [
{
"id": f"ship_{i}",
"state": "confirmed",
"order": {
"id": f"order_{i}",
"number": f"R{i}",
"email": f"customer{i}@example.com",
"total": 29.99,
"shipAddress": {},
},
"inventoryUnits": [],
}
for i in range(3)
]
stats = service.import_historical_shipments(
vendor_id=test_vendor.id,
shipments=shipments,
match_products=False,
)
assert stats["total"] == 3
assert stats["imported"] == 3
assert stats["skipped"] == 0
def test_get_historical_import_summary(self, db, test_vendor):
"""Test historical import summary statistics."""
from app.services.letzshop.order_service import LetzshopOrderService
service = LetzshopOrderService(db)
# Create some orders with different locales
for i, locale in enumerate(["fr", "fr", "de", "en"]):
shipment_data = {
"id": f"ship_{i}",
"state": "confirmed",
"order": {
"id": f"order_{i}",
"number": f"R{i}",
"email": f"customer{i}@example.com",
"total": 29.99,
"locale": locale,
"shipAddress": {"country": {"iso": "LU"}},
},
"inventoryUnits": [],
}
service.create_order(test_vendor.id, shipment_data)
db.commit()
summary = service.get_historical_import_summary(test_vendor.id)
assert summary["total_orders"] == 4
assert summary["unique_customers"] == 4
assert summary["orders_by_locale"]["fr"] == 2
assert summary["orders_by_locale"]["de"] == 1
assert summary["orders_by_locale"]["en"] == 1