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>
751 lines
24 KiB
Python
751 lines
24 KiB
Python
# tests/unit/services/test_letzshop_service.py
|
|
"""
|
|
Unit tests for Letzshop integration services.
|
|
|
|
Tests cover:
|
|
- Encryption utility
|
|
- Credentials service
|
|
- GraphQL client (mocked)
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from app.services.letzshop import (
|
|
CredentialsNotFoundError,
|
|
LetzshopAPIError,
|
|
LetzshopClient,
|
|
LetzshopCredentialsService,
|
|
)
|
|
from app.utils.encryption import (
|
|
EncryptionError,
|
|
EncryptionService,
|
|
mask_api_key,
|
|
)
|
|
|
|
# ============================================================================
|
|
# Encryption Tests
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.letzshop
|
|
class TestEncryptionService:
|
|
"""Test suite for encryption utility."""
|
|
|
|
def test_encrypt_and_decrypt(self):
|
|
"""Test basic encryption and decryption."""
|
|
service = EncryptionService(secret_key="test-secret-key-12345")
|
|
original = "my-secret-api-key"
|
|
|
|
encrypted = service.encrypt(original)
|
|
decrypted = service.decrypt(encrypted)
|
|
|
|
assert encrypted != original
|
|
assert decrypted == original
|
|
|
|
def test_encrypt_empty_string_fails(self):
|
|
"""Test that encrypting empty string raises error."""
|
|
service = EncryptionService(secret_key="test-secret-key-12345")
|
|
|
|
with pytest.raises(EncryptionError):
|
|
service.encrypt("")
|
|
|
|
def test_decrypt_empty_string_fails(self):
|
|
"""Test that decrypting empty string raises error."""
|
|
service = EncryptionService(secret_key="test-secret-key-12345")
|
|
|
|
with pytest.raises(EncryptionError):
|
|
service.decrypt("")
|
|
|
|
def test_decrypt_invalid_ciphertext_fails(self):
|
|
"""Test that decrypting invalid ciphertext raises error."""
|
|
service = EncryptionService(secret_key="test-secret-key-12345")
|
|
|
|
with pytest.raises(EncryptionError):
|
|
service.decrypt("invalid-ciphertext")
|
|
|
|
def test_is_valid_ciphertext(self):
|
|
"""Test ciphertext validation."""
|
|
service = EncryptionService(secret_key="test-secret-key-12345")
|
|
encrypted = service.encrypt("test-value")
|
|
|
|
assert service.is_valid_ciphertext(encrypted) is True
|
|
assert service.is_valid_ciphertext("invalid") is False
|
|
|
|
def test_different_keys_produce_different_results(self):
|
|
"""Test that different keys produce different encryptions."""
|
|
service1 = EncryptionService(secret_key="key-one-12345")
|
|
service2 = EncryptionService(secret_key="key-two-12345")
|
|
|
|
original = "test-value"
|
|
encrypted1 = service1.encrypt(original)
|
|
encrypted2 = service2.encrypt(original)
|
|
|
|
assert encrypted1 != encrypted2
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.letzshop
|
|
class TestMaskApiKey:
|
|
"""Test suite for API key masking."""
|
|
|
|
def test_mask_api_key_default(self):
|
|
"""Test default masking (4 visible chars)."""
|
|
masked = mask_api_key("letzshop-api-key-12345")
|
|
assert masked == "letz******************"
|
|
|
|
def test_mask_api_key_custom_visible(self):
|
|
"""Test masking with custom visible chars."""
|
|
masked = mask_api_key("abcdefghij", visible_chars=6)
|
|
assert masked == "abcdef****"
|
|
|
|
def test_mask_api_key_short(self):
|
|
"""Test masking short key."""
|
|
masked = mask_api_key("abc", visible_chars=4)
|
|
assert masked == "***"
|
|
|
|
def test_mask_api_key_empty(self):
|
|
"""Test masking empty string."""
|
|
masked = mask_api_key("")
|
|
assert masked == ""
|
|
|
|
|
|
# ============================================================================
|
|
# Credentials Service Tests
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.letzshop
|
|
class TestLetzshopCredentialsService:
|
|
"""Test suite for Letzshop credentials service."""
|
|
|
|
def test_create_credentials(self, db, test_vendor):
|
|
"""Test creating credentials for a vendor."""
|
|
service = LetzshopCredentialsService(db)
|
|
|
|
credentials = service.create_credentials(
|
|
vendor_id=test_vendor.id,
|
|
api_key="test-api-key-12345",
|
|
auto_sync_enabled=False,
|
|
sync_interval_minutes=30,
|
|
)
|
|
|
|
assert credentials.vendor_id == test_vendor.id
|
|
assert credentials.api_key_encrypted != "test-api-key-12345"
|
|
assert credentials.auto_sync_enabled is False
|
|
assert credentials.sync_interval_minutes == 30
|
|
|
|
def test_get_credentials(self, db, test_vendor):
|
|
"""Test getting credentials for a vendor."""
|
|
service = LetzshopCredentialsService(db)
|
|
|
|
# Create first
|
|
service.create_credentials(
|
|
vendor_id=test_vendor.id,
|
|
api_key="test-api-key",
|
|
)
|
|
|
|
# Get
|
|
credentials = service.get_credentials(test_vendor.id)
|
|
assert credentials is not None
|
|
assert credentials.vendor_id == test_vendor.id
|
|
|
|
def test_get_credentials_not_found(self, db, test_vendor):
|
|
"""Test getting non-existent credentials returns None."""
|
|
service = LetzshopCredentialsService(db)
|
|
|
|
credentials = service.get_credentials(test_vendor.id)
|
|
assert credentials is None
|
|
|
|
def test_get_credentials_or_raise(self, db, test_vendor):
|
|
"""Test get_credentials_or_raise raises for non-existent."""
|
|
service = LetzshopCredentialsService(db)
|
|
|
|
with pytest.raises(CredentialsNotFoundError):
|
|
service.get_credentials_or_raise(test_vendor.id)
|
|
|
|
def test_update_credentials(self, db, test_vendor):
|
|
"""Test updating credentials."""
|
|
service = LetzshopCredentialsService(db)
|
|
|
|
# Create first
|
|
service.create_credentials(
|
|
vendor_id=test_vendor.id,
|
|
api_key="original-key",
|
|
auto_sync_enabled=False,
|
|
)
|
|
|
|
# Update
|
|
updated = service.update_credentials(
|
|
vendor_id=test_vendor.id,
|
|
auto_sync_enabled=True,
|
|
sync_interval_minutes=60,
|
|
)
|
|
|
|
assert updated.auto_sync_enabled is True
|
|
assert updated.sync_interval_minutes == 60
|
|
|
|
def test_delete_credentials(self, db, test_vendor):
|
|
"""Test deleting credentials."""
|
|
service = LetzshopCredentialsService(db)
|
|
|
|
# Create first
|
|
service.create_credentials(
|
|
vendor_id=test_vendor.id,
|
|
api_key="test-key",
|
|
)
|
|
|
|
# Delete
|
|
result = service.delete_credentials(test_vendor.id)
|
|
assert result is True
|
|
|
|
# Verify deleted
|
|
assert service.get_credentials(test_vendor.id) is None
|
|
|
|
def test_delete_credentials_not_found(self, db, test_vendor):
|
|
"""Test deleting non-existent credentials returns False."""
|
|
service = LetzshopCredentialsService(db)
|
|
|
|
result = service.delete_credentials(test_vendor.id)
|
|
assert result is False
|
|
|
|
def test_upsert_credentials_create(self, db, test_vendor):
|
|
"""Test upsert creates when not exists."""
|
|
service = LetzshopCredentialsService(db)
|
|
|
|
credentials = service.upsert_credentials(
|
|
vendor_id=test_vendor.id,
|
|
api_key="new-key",
|
|
)
|
|
|
|
assert credentials.vendor_id == test_vendor.id
|
|
|
|
def test_upsert_credentials_update(self, db, test_vendor):
|
|
"""Test upsert updates when exists."""
|
|
service = LetzshopCredentialsService(db)
|
|
|
|
# Create first
|
|
service.create_credentials(
|
|
vendor_id=test_vendor.id,
|
|
api_key="original-key",
|
|
auto_sync_enabled=False,
|
|
)
|
|
|
|
# Upsert with new values
|
|
credentials = service.upsert_credentials(
|
|
vendor_id=test_vendor.id,
|
|
api_key="updated-key",
|
|
auto_sync_enabled=True,
|
|
)
|
|
|
|
assert credentials.auto_sync_enabled is True
|
|
|
|
def test_get_decrypted_api_key(self, db, test_vendor):
|
|
"""Test getting decrypted API key."""
|
|
service = LetzshopCredentialsService(db)
|
|
original_key = "my-secret-api-key"
|
|
|
|
service.create_credentials(
|
|
vendor_id=test_vendor.id,
|
|
api_key=original_key,
|
|
)
|
|
|
|
decrypted = service.get_decrypted_api_key(test_vendor.id)
|
|
assert decrypted == original_key
|
|
|
|
def test_get_masked_api_key(self, db, test_vendor):
|
|
"""Test getting masked API key."""
|
|
service = LetzshopCredentialsService(db)
|
|
|
|
service.create_credentials(
|
|
vendor_id=test_vendor.id,
|
|
api_key="letzshop-api-key-12345",
|
|
)
|
|
|
|
masked = service.get_masked_api_key(test_vendor.id)
|
|
assert masked.startswith("letz")
|
|
assert "*" in masked
|
|
|
|
def test_is_configured(self, db, test_vendor):
|
|
"""Test is_configured check."""
|
|
service = LetzshopCredentialsService(db)
|
|
|
|
assert service.is_configured(test_vendor.id) is False
|
|
|
|
service.create_credentials(
|
|
vendor_id=test_vendor.id,
|
|
api_key="test-key",
|
|
)
|
|
|
|
assert service.is_configured(test_vendor.id) is True
|
|
|
|
def test_get_status(self, db, test_vendor):
|
|
"""Test getting integration status."""
|
|
service = LetzshopCredentialsService(db)
|
|
|
|
# Not configured
|
|
status = service.get_status(test_vendor.id)
|
|
assert status["is_configured"] is False
|
|
assert status["auto_sync_enabled"] is False
|
|
|
|
# Configured
|
|
service.create_credentials(
|
|
vendor_id=test_vendor.id,
|
|
api_key="test-key",
|
|
auto_sync_enabled=True,
|
|
)
|
|
|
|
status = service.get_status(test_vendor.id)
|
|
assert status["is_configured"] is True
|
|
assert status["auto_sync_enabled"] is True
|
|
|
|
|
|
# ============================================================================
|
|
# GraphQL Client Tests (Mocked)
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.letzshop
|
|
class TestLetzshopClient:
|
|
"""Test suite for Letzshop GraphQL client (mocked)."""
|
|
|
|
def test_client_initialization(self):
|
|
"""Test client initialization."""
|
|
client = LetzshopClient(
|
|
api_key="test-key",
|
|
endpoint="https://test.example.com/graphql",
|
|
timeout=60,
|
|
)
|
|
|
|
assert client.api_key == "test-key"
|
|
assert client.endpoint == "https://test.example.com/graphql"
|
|
assert client.timeout == 60
|
|
|
|
def test_client_context_manager(self):
|
|
"""Test client can be used as context manager."""
|
|
with LetzshopClient(api_key="test-key") as client:
|
|
assert client is not None
|
|
|
|
@patch("requests.Session.post")
|
|
def test_test_connection_success(self, mock_post):
|
|
"""Test successful connection test."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"data": {"__typename": "Query"}}
|
|
mock_post.return_value = mock_response
|
|
|
|
client = LetzshopClient(api_key="test-key")
|
|
success, response_time, error = client.test_connection()
|
|
|
|
assert success is True
|
|
assert response_time > 0
|
|
assert error is None
|
|
|
|
@patch("requests.Session.post")
|
|
def test_test_connection_auth_failure(self, mock_post):
|
|
"""Test connection test with auth failure."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 401
|
|
mock_post.return_value = mock_response
|
|
|
|
client = LetzshopClient(api_key="invalid-key")
|
|
success, response_time, error = client.test_connection()
|
|
|
|
assert success is False
|
|
assert "Authentication" in error
|
|
|
|
@patch("requests.Session.post")
|
|
def test_get_shipments(self, mock_post):
|
|
"""Test getting shipments."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"data": {
|
|
"shipments": {
|
|
"nodes": [
|
|
{"id": "ship_1", "state": "unconfirmed"},
|
|
{"id": "ship_2", "state": "unconfirmed"},
|
|
]
|
|
}
|
|
}
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
client = LetzshopClient(api_key="test-key")
|
|
shipments = client.get_shipments(state="unconfirmed")
|
|
|
|
assert len(shipments) == 2
|
|
assert shipments[0]["id"] == "ship_1"
|
|
|
|
@patch("requests.Session.post")
|
|
def test_confirm_inventory_units(self, mock_post):
|
|
"""Test confirming inventory units."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"data": {
|
|
"confirmInventoryUnits": {
|
|
"inventoryUnits": [
|
|
{"id": "unit_1", "state": "confirmed"},
|
|
],
|
|
"errors": [],
|
|
}
|
|
}
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
client = LetzshopClient(api_key="test-key")
|
|
result = client.confirm_inventory_units(["unit_1"])
|
|
|
|
assert result["inventoryUnits"][0]["state"] == "confirmed"
|
|
assert len(result["errors"]) == 0
|
|
|
|
@patch("requests.Session.post")
|
|
def test_set_shipment_tracking(self, mock_post):
|
|
"""Test setting shipment tracking."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"data": {
|
|
"setShipmentTracking": {
|
|
"shipment": {
|
|
"id": "ship_1",
|
|
"tracking": {"code": "1Z999AA1", "provider": "ups"},
|
|
},
|
|
"errors": [],
|
|
}
|
|
}
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
client = LetzshopClient(api_key="test-key")
|
|
result = client.set_shipment_tracking(
|
|
shipment_id="ship_1",
|
|
tracking_code="1Z999AA1",
|
|
tracking_provider="ups",
|
|
)
|
|
|
|
assert result["shipment"]["tracking"]["code"] == "1Z999AA1"
|
|
|
|
@patch("requests.Session.post")
|
|
def test_graphql_error_handling(self, mock_post):
|
|
"""Test GraphQL error response handling."""
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"errors": [{"message": "Invalid shipment ID"}]
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
client = LetzshopClient(api_key="test-key")
|
|
|
|
with pytest.raises(LetzshopAPIError) as exc_info:
|
|
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 EUR",
|
|
"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.ship_country_iso == "LU" # Correct attribute name
|
|
assert order.bill_country_iso == "FR" # Correct attribute name
|
|
|
|
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)
|
|
|
|
# Check order items (unified model uses items relationship)
|
|
assert len(order.items) == 1
|
|
item = order.items[0]
|
|
assert item.gtin == "0889698273022"
|
|
assert item.gtin_type == "gtin13"
|
|
assert item.product_sku == "SKU123"
|
|
assert item.product_name == "Test Product"
|
|
# Price is stored in cents (19.99 EUR = 1999 cents)
|
|
assert item.unit_price_cents == 1999
|
|
|
|
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
|