feat: add Letzshop bidirectional order integration
Add complete Letzshop marketplace integration with: - GraphQL client for order import and fulfillment operations - Encrypted credential storage per vendor (Fernet encryption) - Admin and vendor API endpoints for credentials management - Order import, confirmation, rejection, and tracking - Fulfillment queue and sync logging - Comprehensive documentation and test coverage New files: - app/services/letzshop/ - GraphQL client and services - app/utils/encryption.py - Fernet encryption utility - models/database/letzshop.py - Database models - models/schema/letzshop.py - Pydantic schemas - app/api/v1/admin/letzshop.py - Admin API endpoints - app/api/v1/vendor/letzshop.py - Vendor API endpoints - docs/guides/letzshop-order-integration.md - Documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
361
tests/integration/api/v1/admin/test_letzshop.py
Normal file
361
tests/integration/api/v1/admin/test_letzshop.py
Normal file
@@ -0,0 +1,361 @@
|
||||
# tests/integration/api/v1/admin/test_letzshop.py
|
||||
"""
|
||||
Integration tests for admin Letzshop API endpoints.
|
||||
|
||||
Tests cover:
|
||||
1. Vendor Letzshop status overview
|
||||
2. Credentials management for vendors
|
||||
3. Connection testing
|
||||
4. Order management for vendors
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.admin
|
||||
@pytest.mark.letzshop
|
||||
class TestAdminLetzshopVendorsAPI:
|
||||
"""Test admin Letzshop vendor overview endpoints."""
|
||||
|
||||
def test_list_vendors_letzshop_status(
|
||||
self, client, admin_headers, test_vendor
|
||||
):
|
||||
"""Test listing vendors with Letzshop status."""
|
||||
response = client.get(
|
||||
"/api/v1/admin/letzshop/vendors", headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "vendors" in data
|
||||
assert "total" in data
|
||||
# Find our test vendor
|
||||
vendor_found = False
|
||||
for v in data["vendors"]:
|
||||
if v["vendor_id"] == test_vendor.id:
|
||||
vendor_found = True
|
||||
assert v["is_configured"] is False # Not configured yet
|
||||
break
|
||||
# Vendor may not be found if inactive, that's ok
|
||||
|
||||
def test_list_vendors_configured_only(
|
||||
self, client, db, admin_headers, test_vendor
|
||||
):
|
||||
"""Test listing only configured vendors."""
|
||||
from models.database.letzshop import VendorLetzshopCredentials
|
||||
from app.utils.encryption import encrypt_value
|
||||
|
||||
# Configure credentials for test vendor
|
||||
credentials = VendorLetzshopCredentials(
|
||||
vendor_id=test_vendor.id,
|
||||
api_key_encrypted=encrypt_value("test-key"),
|
||||
api_endpoint="https://letzshop.lu/graphql",
|
||||
)
|
||||
db.add(credentials)
|
||||
db.commit()
|
||||
|
||||
response = client.get(
|
||||
"/api/v1/admin/letzshop/vendors?configured_only=true",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# All returned vendors should be configured
|
||||
for v in data["vendors"]:
|
||||
assert v["is_configured"] is True
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.admin
|
||||
@pytest.mark.letzshop
|
||||
class TestAdminLetzshopCredentialsAPI:
|
||||
"""Test admin Letzshop credentials management endpoints."""
|
||||
|
||||
def test_get_vendor_credentials_not_configured(
|
||||
self, client, admin_headers, test_vendor
|
||||
):
|
||||
"""Test getting credentials when not configured returns 404."""
|
||||
response = client.get(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/credentials",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_create_vendor_credentials(
|
||||
self, client, admin_headers, test_vendor
|
||||
):
|
||||
"""Test creating credentials for a vendor."""
|
||||
response = client.post(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/credentials",
|
||||
headers=admin_headers,
|
||||
json={
|
||||
"api_key": "admin-set-api-key-12345",
|
||||
"auto_sync_enabled": True,
|
||||
"sync_interval_minutes": 60,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["vendor_id"] == test_vendor.id
|
||||
assert "****" in data["api_key_masked"]
|
||||
assert data["auto_sync_enabled"] is True
|
||||
assert data["sync_interval_minutes"] == 60
|
||||
|
||||
def test_get_vendor_credentials_after_create(
|
||||
self, client, db, admin_headers, test_vendor
|
||||
):
|
||||
"""Test getting credentials after creation."""
|
||||
# Create first
|
||||
client.post(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/credentials",
|
||||
headers=admin_headers,
|
||||
json={"api_key": "test-key"},
|
||||
)
|
||||
|
||||
# Get
|
||||
response = client.get(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/credentials",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["vendor_id"] == test_vendor.id
|
||||
|
||||
def test_update_vendor_credentials(
|
||||
self, client, admin_headers, test_vendor
|
||||
):
|
||||
"""Test partial update of vendor credentials."""
|
||||
# Create first
|
||||
client.post(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/credentials",
|
||||
headers=admin_headers,
|
||||
json={"api_key": "original-key", "auto_sync_enabled": False},
|
||||
)
|
||||
|
||||
# Update
|
||||
response = client.patch(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/credentials",
|
||||
headers=admin_headers,
|
||||
json={"auto_sync_enabled": True},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["auto_sync_enabled"] is True
|
||||
|
||||
def test_delete_vendor_credentials(
|
||||
self, client, admin_headers, test_vendor
|
||||
):
|
||||
"""Test deleting vendor credentials."""
|
||||
# Create first
|
||||
client.post(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/credentials",
|
||||
headers=admin_headers,
|
||||
json={"api_key": "test-key"},
|
||||
)
|
||||
|
||||
# Delete
|
||||
response = client.delete(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/credentials",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
def test_vendor_not_found(self, client, admin_headers):
|
||||
"""Test operations on non-existent vendor return 404."""
|
||||
response = client.get(
|
||||
"/api/v1/admin/letzshop/vendors/99999/credentials",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.admin
|
||||
@pytest.mark.letzshop
|
||||
class TestAdminLetzshopConnectionAPI:
|
||||
"""Test admin Letzshop connection testing endpoints."""
|
||||
|
||||
@patch("app.services.letzshop.client.requests.Session.post")
|
||||
def test_test_vendor_connection(
|
||||
self, mock_post, client, admin_headers, test_vendor
|
||||
):
|
||||
"""Test connection for a specific vendor."""
|
||||
# Mock response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"data": {"__typename": "Query"}}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
# Create credentials
|
||||
client.post(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/credentials",
|
||||
headers=admin_headers,
|
||||
json={"api_key": "test-key"},
|
||||
)
|
||||
|
||||
# Test connection
|
||||
response = client.post(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/test",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
@patch("app.services.letzshop.client.requests.Session.post")
|
||||
def test_test_api_key_directly(
|
||||
self, mock_post, client, admin_headers
|
||||
):
|
||||
"""Test any API key without associating with vendor."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"data": {"__typename": "Query"}}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/admin/letzshop/test",
|
||||
headers=admin_headers,
|
||||
json={"api_key": "test-api-key-to-validate"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.admin
|
||||
@pytest.mark.letzshop
|
||||
class TestAdminLetzshopOrdersAPI:
|
||||
"""Test admin Letzshop order management endpoints."""
|
||||
|
||||
def test_list_vendor_orders_empty(
|
||||
self, client, admin_headers, test_vendor
|
||||
):
|
||||
"""Test listing vendor orders when none exist."""
|
||||
response = client.get(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/orders",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["orders"] == []
|
||||
assert data["total"] == 0
|
||||
|
||||
def test_list_vendor_orders_with_data(
|
||||
self, client, db, admin_headers, test_vendor
|
||||
):
|
||||
"""Test listing vendor orders with data."""
|
||||
from models.database.letzshop import LetzshopOrder
|
||||
|
||||
# Create test orders
|
||||
order = LetzshopOrder(
|
||||
vendor_id=test_vendor.id,
|
||||
letzshop_order_id="admin_order_1",
|
||||
letzshop_state="unconfirmed",
|
||||
customer_email="admin-test@example.com",
|
||||
total_amount="150.00",
|
||||
sync_status="pending",
|
||||
)
|
||||
db.add(order)
|
||||
db.commit()
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/orders",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 1
|
||||
assert data["orders"][0]["customer_email"] == "admin-test@example.com"
|
||||
|
||||
@patch("app.services.letzshop.client.requests.Session.post")
|
||||
def test_trigger_vendor_sync(
|
||||
self, mock_post, client, admin_headers, test_vendor
|
||||
):
|
||||
"""Test triggering sync for a vendor."""
|
||||
# Mock response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"data": {
|
||||
"shipments": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "gid://letzshop/Shipment/789",
|
||||
"state": "unconfirmed",
|
||||
"order": {
|
||||
"id": "gid://letzshop/Order/111",
|
||||
"number": "LS-ADMIN-001",
|
||||
"email": "sync@example.com",
|
||||
"totalPrice": {"amount": "200.00", "currency": "EUR"},
|
||||
},
|
||||
"inventoryUnits": {"nodes": []},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
# Create credentials
|
||||
client.post(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/credentials",
|
||||
headers=admin_headers,
|
||||
json={"api_key": "admin-sync-key"},
|
||||
)
|
||||
|
||||
# Trigger sync
|
||||
response = client.post(
|
||||
f"/api/v1/admin/letzshop/vendors/{test_vendor.id}/sync",
|
||||
headers=admin_headers,
|
||||
json={"operation": "order_import"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["orders_imported"] >= 0
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.admin
|
||||
@pytest.mark.letzshop
|
||||
class TestAdminLetzshopAccessControl:
|
||||
"""Test admin access control for Letzshop endpoints."""
|
||||
|
||||
def test_non_admin_cannot_access(
|
||||
self, client, auth_headers, test_vendor
|
||||
):
|
||||
"""Test that non-admin users cannot access admin endpoints."""
|
||||
response = client.get(
|
||||
"/api/v1/admin/letzshop/vendors",
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_unauthenticated_cannot_access(self, client):
|
||||
"""Test that unauthenticated requests are rejected."""
|
||||
response = client.get("/api/v1/admin/letzshop/vendors")
|
||||
|
||||
assert response.status_code == 401
|
||||
499
tests/integration/api/v1/vendor/test_letzshop.py
vendored
Normal file
499
tests/integration/api/v1/vendor/test_letzshop.py
vendored
Normal file
@@ -0,0 +1,499 @@
|
||||
# tests/integration/api/v1/vendor/test_letzshop.py
|
||||
"""
|
||||
Integration tests for vendor Letzshop API endpoints.
|
||||
|
||||
Tests cover:
|
||||
1. Credentials management (CRUD)
|
||||
2. Connection testing
|
||||
3. Order listing
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.vendor
|
||||
@pytest.mark.letzshop
|
||||
class TestVendorLetzshopCredentialsAPI:
|
||||
"""Test vendor Letzshop credentials endpoints."""
|
||||
|
||||
def test_get_status_not_configured(
|
||||
self, client, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test getting status when not configured."""
|
||||
response = client.get(
|
||||
"/api/v1/vendor/letzshop/status", headers=vendor_user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["is_configured"] is False
|
||||
assert data["auto_sync_enabled"] is False
|
||||
|
||||
def test_save_credentials(
|
||||
self, client, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test saving Letzshop credentials."""
|
||||
response = client.post(
|
||||
"/api/v1/vendor/letzshop/credentials",
|
||||
headers=vendor_user_headers,
|
||||
json={
|
||||
"api_key": "test-letzshop-api-key-12345",
|
||||
"auto_sync_enabled": False,
|
||||
"sync_interval_minutes": 30,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["vendor_id"] == test_vendor_with_vendor_user.id
|
||||
assert "****" in data["api_key_masked"]
|
||||
assert data["auto_sync_enabled"] is False
|
||||
assert data["sync_interval_minutes"] == 30
|
||||
|
||||
def test_get_credentials_after_save(
|
||||
self, client, db, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test getting credentials after saving."""
|
||||
# Save first
|
||||
client.post(
|
||||
"/api/v1/vendor/letzshop/credentials",
|
||||
headers=vendor_user_headers,
|
||||
json={"api_key": "my-test-key"},
|
||||
)
|
||||
|
||||
# Get
|
||||
response = client.get(
|
||||
"/api/v1/vendor/letzshop/credentials", headers=vendor_user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["vendor_id"] == test_vendor_with_vendor_user.id
|
||||
assert "api_key_masked" in data
|
||||
|
||||
def test_get_credentials_not_configured(
|
||||
self, client, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test getting credentials when not configured returns 404."""
|
||||
response = client.get(
|
||||
"/api/v1/vendor/letzshop/credentials", headers=vendor_user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_update_credentials(
|
||||
self, client, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test partial update of credentials."""
|
||||
# Save first
|
||||
client.post(
|
||||
"/api/v1/vendor/letzshop/credentials",
|
||||
headers=vendor_user_headers,
|
||||
json={"api_key": "original-key", "auto_sync_enabled": False},
|
||||
)
|
||||
|
||||
# Update
|
||||
response = client.patch(
|
||||
"/api/v1/vendor/letzshop/credentials",
|
||||
headers=vendor_user_headers,
|
||||
json={"auto_sync_enabled": True, "sync_interval_minutes": 60},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["auto_sync_enabled"] is True
|
||||
assert data["sync_interval_minutes"] == 60
|
||||
|
||||
def test_delete_credentials(
|
||||
self, client, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test deleting credentials."""
|
||||
# Save first
|
||||
client.post(
|
||||
"/api/v1/vendor/letzshop/credentials",
|
||||
headers=vendor_user_headers,
|
||||
json={"api_key": "test-key"},
|
||||
)
|
||||
|
||||
# Delete
|
||||
response = client.delete(
|
||||
"/api/v1/vendor/letzshop/credentials", headers=vendor_user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
# Verify deleted
|
||||
response = client.get(
|
||||
"/api/v1/vendor/letzshop/credentials", headers=vendor_user_headers
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_delete_credentials_not_configured(
|
||||
self, client, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test deleting when not configured returns 404."""
|
||||
response = client.delete(
|
||||
"/api/v1/vendor/letzshop/credentials", headers=vendor_user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.vendor
|
||||
@pytest.mark.letzshop
|
||||
class TestVendorLetzshopConnectionAPI:
|
||||
"""Test vendor Letzshop connection testing endpoints."""
|
||||
|
||||
def test_test_connection_not_configured(
|
||||
self, client, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test connection when not configured."""
|
||||
response = client.post(
|
||||
"/api/v1/vendor/letzshop/test", headers=vendor_user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert "not configured" in data["error_details"]
|
||||
|
||||
@patch("app.services.letzshop.client.requests.Session.post")
|
||||
def test_test_connection_success(
|
||||
self, mock_post, client, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test successful connection test."""
|
||||
# Mock successful response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"data": {"__typename": "Query"}}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
# Save credentials first
|
||||
client.post(
|
||||
"/api/v1/vendor/letzshop/credentials",
|
||||
headers=vendor_user_headers,
|
||||
json={"api_key": "valid-test-key"},
|
||||
)
|
||||
|
||||
# Test connection
|
||||
response = client.post(
|
||||
"/api/v1/vendor/letzshop/test", headers=vendor_user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["response_time_ms"] is not None
|
||||
|
||||
@patch("app.services.letzshop.client.requests.Session.post")
|
||||
def test_test_api_key_without_saving(
|
||||
self, mock_post, client, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test API key without saving it."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"data": {"__typename": "Query"}}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/vendor/letzshop/test-key",
|
||||
headers=vendor_user_headers,
|
||||
json={"api_key": "test-key-to-validate"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.vendor
|
||||
@pytest.mark.letzshop
|
||||
class TestVendorLetzshopOrdersAPI:
|
||||
"""Test vendor Letzshop order endpoints."""
|
||||
|
||||
def test_list_orders_empty(
|
||||
self, client, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test listing orders when none exist."""
|
||||
response = client.get(
|
||||
"/api/v1/vendor/letzshop/orders", headers=vendor_user_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["orders"] == []
|
||||
assert data["total"] == 0
|
||||
|
||||
def test_list_orders_with_filters(
|
||||
self, client, db, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test listing orders with status filter."""
|
||||
from models.database.letzshop import LetzshopOrder
|
||||
|
||||
# Create test orders
|
||||
order1 = LetzshopOrder(
|
||||
vendor_id=test_vendor_with_vendor_user.id,
|
||||
letzshop_order_id="order_1",
|
||||
letzshop_state="unconfirmed",
|
||||
sync_status="pending",
|
||||
)
|
||||
order2 = LetzshopOrder(
|
||||
vendor_id=test_vendor_with_vendor_user.id,
|
||||
letzshop_order_id="order_2",
|
||||
letzshop_state="confirmed",
|
||||
sync_status="confirmed",
|
||||
)
|
||||
db.add_all([order1, order2])
|
||||
db.commit()
|
||||
|
||||
# List pending only
|
||||
response = client.get(
|
||||
"/api/v1/vendor/letzshop/orders?sync_status=pending",
|
||||
headers=vendor_user_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 1
|
||||
assert data["orders"][0]["sync_status"] == "pending"
|
||||
|
||||
def test_get_order_detail(
|
||||
self, client, db, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test getting order detail."""
|
||||
from models.database.letzshop import LetzshopOrder
|
||||
|
||||
order = LetzshopOrder(
|
||||
vendor_id=test_vendor_with_vendor_user.id,
|
||||
letzshop_order_id="order_detail_test",
|
||||
letzshop_shipment_id="shipment_1",
|
||||
letzshop_state="unconfirmed",
|
||||
customer_email="test@example.com",
|
||||
total_amount="99.99",
|
||||
sync_status="pending",
|
||||
raw_order_data={"test": "data"},
|
||||
)
|
||||
db.add(order)
|
||||
db.commit()
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1/vendor/letzshop/orders/{order.id}",
|
||||
headers=vendor_user_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["letzshop_order_id"] == "order_detail_test"
|
||||
assert data["customer_email"] == "test@example.com"
|
||||
assert data["raw_order_data"] == {"test": "data"}
|
||||
|
||||
def test_get_order_not_found(
|
||||
self, client, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test getting non-existent order returns 404."""
|
||||
response = client.get(
|
||||
"/api/v1/vendor/letzshop/orders/99999",
|
||||
headers=vendor_user_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_import_orders_not_configured(
|
||||
self, client, vendor_user_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test importing orders when not configured."""
|
||||
response = client.post(
|
||||
"/api/v1/vendor/letzshop/orders/import",
|
||||
headers=vendor_user_headers,
|
||||
json={},
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
@patch("app.services.letzshop.client.requests.Session.post")
|
||||
def test_import_orders_success(
|
||||
self,
|
||||
mock_post,
|
||||
client,
|
||||
db,
|
||||
vendor_user_headers,
|
||||
test_vendor_with_vendor_user,
|
||||
):
|
||||
"""Test successful order import."""
|
||||
# Mock Letzshop API response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"data": {
|
||||
"shipments": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "gid://letzshop/Shipment/123",
|
||||
"state": "unconfirmed",
|
||||
"order": {
|
||||
"id": "gid://letzshop/Order/456",
|
||||
"number": "LS-2025-001",
|
||||
"email": "customer@example.com",
|
||||
"totalPrice": {"amount": "99.99", "currency": "EUR"},
|
||||
},
|
||||
"inventoryUnits": {
|
||||
"nodes": [
|
||||
{"id": "unit_1", "state": "unconfirmed"},
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
# Save credentials first
|
||||
client.post(
|
||||
"/api/v1/vendor/letzshop/credentials",
|
||||
headers=vendor_user_headers,
|
||||
json={"api_key": "test-key"},
|
||||
)
|
||||
|
||||
# Import orders
|
||||
response = client.post(
|
||||
"/api/v1/vendor/letzshop/orders/import",
|
||||
headers=vendor_user_headers,
|
||||
json={"operation": "order_import"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["orders_imported"] == 1
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.vendor
|
||||
@pytest.mark.letzshop
|
||||
class TestVendorLetzshopFulfillmentAPI:
|
||||
"""Test vendor Letzshop fulfillment endpoints."""
|
||||
|
||||
@patch("app.services.letzshop.client.requests.Session.post")
|
||||
def test_confirm_order(
|
||||
self,
|
||||
mock_post,
|
||||
client,
|
||||
db,
|
||||
vendor_user_headers,
|
||||
test_vendor_with_vendor_user,
|
||||
):
|
||||
"""Test confirming an order."""
|
||||
from models.database.letzshop import LetzshopOrder
|
||||
|
||||
# Create test order
|
||||
order = LetzshopOrder(
|
||||
vendor_id=test_vendor_with_vendor_user.id,
|
||||
letzshop_order_id="order_confirm",
|
||||
letzshop_shipment_id="shipment_1",
|
||||
letzshop_state="unconfirmed",
|
||||
sync_status="pending",
|
||||
inventory_units=[{"id": "unit_1", "state": "unconfirmed"}],
|
||||
)
|
||||
db.add(order)
|
||||
db.commit()
|
||||
|
||||
# Save credentials
|
||||
client.post(
|
||||
"/api/v1/vendor/letzshop/credentials",
|
||||
headers=vendor_user_headers,
|
||||
json={"api_key": "test-key"},
|
||||
)
|
||||
|
||||
# Mock confirm response
|
||||
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
|
||||
|
||||
# Confirm order
|
||||
response = client.post(
|
||||
f"/api/v1/vendor/letzshop/orders/{order.id}/confirm",
|
||||
headers=vendor_user_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
@patch("app.services.letzshop.client.requests.Session.post")
|
||||
def test_set_tracking(
|
||||
self,
|
||||
mock_post,
|
||||
client,
|
||||
db,
|
||||
vendor_user_headers,
|
||||
test_vendor_with_vendor_user,
|
||||
):
|
||||
"""Test setting tracking information."""
|
||||
from models.database.letzshop import LetzshopOrder
|
||||
|
||||
order = LetzshopOrder(
|
||||
vendor_id=test_vendor_with_vendor_user.id,
|
||||
letzshop_order_id="order_tracking",
|
||||
letzshop_shipment_id="shipment_track",
|
||||
letzshop_state="confirmed",
|
||||
sync_status="confirmed",
|
||||
)
|
||||
db.add(order)
|
||||
db.commit()
|
||||
|
||||
# Save credentials
|
||||
client.post(
|
||||
"/api/v1/vendor/letzshop/credentials",
|
||||
headers=vendor_user_headers,
|
||||
json={"api_key": "test-key"},
|
||||
)
|
||||
|
||||
# Mock tracking response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"data": {
|
||||
"setShipmentTracking": {
|
||||
"shipment": {
|
||||
"id": "shipment_track",
|
||||
"tracking": {"code": "1Z999AA1", "provider": "ups"},
|
||||
},
|
||||
"errors": [],
|
||||
}
|
||||
}
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
# Set tracking
|
||||
response = client.post(
|
||||
f"/api/v1/vendor/letzshop/orders/{order.id}/tracking",
|
||||
headers=vendor_user_headers,
|
||||
json={
|
||||
"tracking_number": "1Z999AA1",
|
||||
"tracking_carrier": "ups",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["tracking_number"] == "1Z999AA1"
|
||||
Reference in New Issue
Block a user