Some checks failed
Enforce MOD-025/MOD-026 rules: zero top-level cross-module model imports remain in any service file. All 66 files migrated using deferred import patterns (method-body, _get_model() helpers, instance-cached self._Model) and new cross-module service methods in tenancy. Documentation updated with Pattern 6 (deferred imports), migration plan marked complete, and violations status reflects 84→0 service-layer violations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
576 lines
18 KiB
Python
576 lines
18 KiB
Python
# tests/unit/services/test_order_service.py
|
|
"""
|
|
Unit tests for OrderService.
|
|
|
|
Tests cover:
|
|
- Order number generation
|
|
- Customer management
|
|
- Order creation (direct and Letzshop)
|
|
- Order retrieval and filtering
|
|
- Order updates and status management
|
|
- Admin operations
|
|
"""
|
|
|
|
from datetime import UTC, datetime
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from app.modules.orders.exceptions import OrderNotFoundException
|
|
from app.modules.orders.models import Order
|
|
from app.modules.orders.services.order_service import (
|
|
PLACEHOLDER_GTIN,
|
|
OrderService,
|
|
order_service,
|
|
)
|
|
|
|
# Default address fields required by Order model
|
|
DEFAULT_ADDRESS = {
|
|
# Shipping address
|
|
"ship_first_name": "Test",
|
|
"ship_last_name": "Customer",
|
|
"ship_address_line_1": "123 Test St",
|
|
"ship_city": "Test City",
|
|
"ship_postal_code": "12345",
|
|
"ship_country_iso": "LU",
|
|
# Billing address
|
|
"bill_first_name": "Test",
|
|
"bill_last_name": "Customer",
|
|
"bill_address_line_1": "123 Test St",
|
|
"bill_city": "Test City",
|
|
"bill_postal_code": "12345",
|
|
"bill_country_iso": "LU",
|
|
}
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.service
|
|
class TestOrderServiceNumberGeneration:
|
|
"""Test order number generation"""
|
|
|
|
def test_generate_order_number_format(self, db, test_store):
|
|
"""Test order number has correct format"""
|
|
service = OrderService()
|
|
order_number = service._generate_order_number(db, test_store.id)
|
|
|
|
assert order_number.startswith("ORD-")
|
|
assert f"-{test_store.id}-" in order_number
|
|
parts = order_number.split("-")
|
|
assert len(parts) == 4
|
|
|
|
def test_generate_order_number_unique(self, db, test_store):
|
|
"""Test order numbers are unique"""
|
|
service = OrderService()
|
|
numbers = set()
|
|
|
|
for _ in range(10):
|
|
num = service._generate_order_number(db, test_store.id)
|
|
assert num not in numbers
|
|
numbers.add(num)
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.service
|
|
class TestOrderServiceCustomerManagement:
|
|
"""Test customer management"""
|
|
|
|
def test_find_or_create_customer_creates_new(self, db, test_store):
|
|
"""Test creating new customer"""
|
|
service = OrderService()
|
|
customer = service.find_or_create_customer(
|
|
db=db,
|
|
store_id=test_store.id,
|
|
email="newcustomer@example.com",
|
|
first_name="New",
|
|
last_name="Customer",
|
|
phone="+352123456789",
|
|
)
|
|
db.commit()
|
|
|
|
assert customer.id is not None
|
|
assert customer.email == "newcustomer@example.com"
|
|
assert customer.first_name == "New"
|
|
assert customer.last_name == "Customer"
|
|
assert customer.store_id == test_store.id
|
|
assert customer.is_active is True # Created via enrollment
|
|
|
|
def test_find_or_create_customer_finds_existing(self, db, test_store):
|
|
"""Test finding existing customer by email"""
|
|
service = OrderService()
|
|
|
|
# Create customer first
|
|
customer1 = service.find_or_create_customer(
|
|
db=db,
|
|
store_id=test_store.id,
|
|
email="existing@example.com",
|
|
first_name="Existing",
|
|
last_name="Customer",
|
|
)
|
|
db.commit()
|
|
|
|
# Try to create again with same email
|
|
customer2 = service.find_or_create_customer(
|
|
db=db,
|
|
store_id=test_store.id,
|
|
email="existing@example.com",
|
|
first_name="Different",
|
|
last_name="Name",
|
|
)
|
|
|
|
assert customer1.id == customer2.id
|
|
|
|
def test_find_or_create_customer_active(self, db, test_store):
|
|
"""Test creating active customer"""
|
|
service = OrderService()
|
|
customer = service.find_or_create_customer(
|
|
db=db,
|
|
store_id=test_store.id,
|
|
email="active@example.com",
|
|
first_name="Active",
|
|
last_name="Customer",
|
|
is_active=True,
|
|
)
|
|
db.commit()
|
|
|
|
assert customer.is_active is True
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.service
|
|
class TestOrderServiceRetrieval:
|
|
"""Test order retrieval"""
|
|
|
|
def test_get_order_not_found(self, db, test_store):
|
|
"""Test get_order raises for non-existent order"""
|
|
service = OrderService()
|
|
with pytest.raises(OrderNotFoundException):
|
|
service.get_order(db, test_store.id, 99999)
|
|
|
|
def test_get_order_wrong_store(self, db, test_store, test_customer):
|
|
"""Test get_order raises for wrong store"""
|
|
# Create order for test_store
|
|
order = Order(
|
|
store_id=test_store.id,
|
|
customer_id=test_customer.id,
|
|
order_number="TEST-ORDER-001",
|
|
channel="direct",
|
|
status="pending",
|
|
total_amount_cents=10000,
|
|
currency="EUR",
|
|
customer_first_name="Test",
|
|
customer_last_name="Customer",
|
|
customer_email="test@example.com",
|
|
order_date=datetime.now(UTC),
|
|
**DEFAULT_ADDRESS,
|
|
)
|
|
db.add(order)
|
|
db.commit()
|
|
|
|
service = OrderService()
|
|
# Try to get with different store
|
|
with pytest.raises(OrderNotFoundException):
|
|
service.get_order(db, 99999, order.id)
|
|
|
|
def test_get_store_orders_empty(self, db, test_store):
|
|
"""Test get_store_orders returns empty list when no orders"""
|
|
service = OrderService()
|
|
orders, total = service.get_store_orders(db, test_store.id)
|
|
|
|
assert orders == []
|
|
assert total == 0
|
|
|
|
def test_get_store_orders_with_filters(self, db, test_store, test_customer):
|
|
"""Test get_store_orders filters correctly"""
|
|
# Create orders
|
|
order1 = Order(
|
|
store_id=test_store.id,
|
|
customer_id=test_customer.id,
|
|
order_number="FILTER-TEST-001",
|
|
channel="direct",
|
|
status="pending",
|
|
total_amount_cents=10000,
|
|
currency="EUR",
|
|
customer_first_name="Filter",
|
|
customer_last_name="Test",
|
|
customer_email="filter@example.com",
|
|
order_date=datetime.now(UTC),
|
|
**DEFAULT_ADDRESS,
|
|
)
|
|
order2 = Order(
|
|
store_id=test_store.id,
|
|
customer_id=test_customer.id,
|
|
order_number="FILTER-TEST-002",
|
|
channel="letzshop",
|
|
status="processing",
|
|
total_amount_cents=20000,
|
|
currency="EUR",
|
|
customer_first_name="Another",
|
|
customer_last_name="Test",
|
|
customer_email="another@example.com",
|
|
order_date=datetime.now(UTC),
|
|
**DEFAULT_ADDRESS,
|
|
)
|
|
db.add(order1)
|
|
db.add(order2)
|
|
db.commit()
|
|
|
|
service = OrderService()
|
|
|
|
# Filter by status
|
|
orders, total = service.get_store_orders(
|
|
db, test_store.id, status="pending"
|
|
)
|
|
assert all(o.status == "pending" for o in orders)
|
|
|
|
# Filter by channel
|
|
orders, total = service.get_store_orders(
|
|
db, test_store.id, channel="letzshop"
|
|
)
|
|
assert all(o.channel == "letzshop" for o in orders)
|
|
|
|
# Search by email
|
|
orders, total = service.get_store_orders(
|
|
db, test_store.id, search="filter@"
|
|
)
|
|
assert len(orders) >= 1
|
|
|
|
def test_get_order_by_external_shipment_id(self, db, test_store, test_customer):
|
|
"""Test get_order_by_external_shipment_id returns correct order"""
|
|
order = Order(
|
|
store_id=test_store.id,
|
|
customer_id=test_customer.id,
|
|
order_number="EXT-SHIP-001",
|
|
channel="letzshop",
|
|
status="pending",
|
|
external_shipment_id="SHIPMENT123",
|
|
total_amount_cents=10000,
|
|
currency="EUR",
|
|
customer_first_name="Test",
|
|
customer_last_name="Customer",
|
|
customer_email="test@example.com",
|
|
order_date=datetime.now(UTC),
|
|
**DEFAULT_ADDRESS,
|
|
)
|
|
db.add(order)
|
|
db.commit()
|
|
|
|
service = OrderService()
|
|
found = service.get_order_by_external_shipment_id(
|
|
db, test_store.id, "SHIPMENT123"
|
|
)
|
|
|
|
assert found is not None
|
|
assert found.id == order.id
|
|
|
|
def test_get_order_by_external_shipment_id_not_found(self, db, test_store):
|
|
"""Test get_order_by_external_shipment_id returns None when not found"""
|
|
service = OrderService()
|
|
result = service.get_order_by_external_shipment_id(
|
|
db, test_store.id, "NONEXISTENT"
|
|
)
|
|
assert result is None
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.service
|
|
class TestOrderServiceStats:
|
|
"""Test order statistics"""
|
|
|
|
def test_get_order_stats_empty(self, db, test_store):
|
|
"""Test get_order_stats returns zeros when no orders"""
|
|
service = OrderService()
|
|
stats = service.get_order_stats(db, test_store.id)
|
|
|
|
assert stats["total"] == 0
|
|
assert stats["pending"] == 0
|
|
assert stats["processing"] == 0
|
|
|
|
def test_get_order_stats_with_orders(self, db, test_store, test_customer):
|
|
"""Test get_order_stats counts correctly"""
|
|
# Create orders with different statuses
|
|
for status in ["pending", "pending", "processing"]:
|
|
order = Order(
|
|
store_id=test_store.id,
|
|
customer_id=test_customer.id,
|
|
order_number=f"STAT-{status}-{datetime.now().timestamp()}",
|
|
channel="direct",
|
|
status=status,
|
|
total_amount_cents=10000,
|
|
currency="EUR",
|
|
customer_first_name="Test",
|
|
customer_last_name="Customer",
|
|
customer_email="test@example.com",
|
|
order_date=datetime.now(UTC),
|
|
**DEFAULT_ADDRESS,
|
|
)
|
|
db.add(order) # noqa: PERF006
|
|
db.commit()
|
|
|
|
service = OrderService()
|
|
stats = service.get_order_stats(db, test_store.id)
|
|
|
|
assert stats["total"] >= 3
|
|
assert stats["pending"] >= 2
|
|
assert stats["processing"] >= 1
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.service
|
|
class TestOrderServiceUpdates:
|
|
"""Test order updates"""
|
|
|
|
def test_update_order_status(self, db, test_store, test_customer):
|
|
"""Test update_order_status changes status"""
|
|
from app.modules.orders.schemas import OrderUpdate
|
|
|
|
order = Order(
|
|
store_id=test_store.id,
|
|
customer_id=test_customer.id,
|
|
order_number="UPDATE-TEST-001",
|
|
channel="direct",
|
|
status="pending",
|
|
total_amount_cents=10000,
|
|
currency="EUR",
|
|
customer_first_name="Test",
|
|
customer_last_name="Customer",
|
|
customer_email="test@example.com",
|
|
order_date=datetime.now(UTC),
|
|
**DEFAULT_ADDRESS,
|
|
)
|
|
db.add(order)
|
|
db.commit()
|
|
|
|
service = OrderService()
|
|
update_data = OrderUpdate(status="processing")
|
|
updated = service.update_order_status(
|
|
db, test_store.id, order.id, update_data
|
|
)
|
|
db.commit()
|
|
|
|
assert updated.status == "processing"
|
|
assert updated.confirmed_at is not None
|
|
|
|
def test_set_order_tracking(self, db, test_store, test_customer):
|
|
"""Test set_order_tracking updates tracking and status"""
|
|
order = Order(
|
|
store_id=test_store.id,
|
|
customer_id=test_customer.id,
|
|
order_number="TRACKING-TEST-001",
|
|
channel="direct",
|
|
status="processing",
|
|
total_amount_cents=10000,
|
|
currency="EUR",
|
|
customer_first_name="Test",
|
|
customer_last_name="Customer",
|
|
customer_email="test@example.com",
|
|
order_date=datetime.now(UTC),
|
|
**DEFAULT_ADDRESS,
|
|
)
|
|
db.add(order)
|
|
db.commit()
|
|
|
|
service = OrderService()
|
|
updated = service.set_order_tracking(
|
|
db,
|
|
test_store.id,
|
|
order.id,
|
|
tracking_number="TRACK123",
|
|
tracking_provider="DHL",
|
|
)
|
|
db.commit()
|
|
|
|
assert updated.tracking_number == "TRACK123"
|
|
assert updated.tracking_provider == "DHL"
|
|
assert updated.status == "shipped"
|
|
assert updated.shipped_at is not None
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.service
|
|
class TestOrderServiceAdmin:
|
|
"""Test admin operations"""
|
|
|
|
def test_get_all_orders_admin_empty(self, db):
|
|
"""Test get_all_orders_admin returns empty list when no orders"""
|
|
service = OrderService()
|
|
orders, total = service.get_all_orders_admin(db)
|
|
|
|
assert isinstance(orders, list)
|
|
assert isinstance(total, int)
|
|
|
|
def test_get_order_stats_admin(self, db):
|
|
"""Test get_order_stats_admin returns stats"""
|
|
service = OrderService()
|
|
stats = service.get_order_stats_admin(db)
|
|
|
|
assert "total_orders" in stats
|
|
assert "pending_orders" in stats
|
|
assert "processing_orders" in stats
|
|
assert "total_revenue" in stats
|
|
assert "stores_with_orders" in stats
|
|
|
|
def test_get_order_by_id_admin_not_found(self, db):
|
|
"""Test get_order_by_id_admin raises for non-existent"""
|
|
service = OrderService()
|
|
with pytest.raises(OrderNotFoundException):
|
|
service.get_order_by_id_admin(db, 99999)
|
|
|
|
def test_get_stores_with_orders_admin(self, db):
|
|
"""Test get_stores_with_orders_admin returns list"""
|
|
service = OrderService()
|
|
result = service.get_stores_with_orders_admin(db)
|
|
|
|
assert isinstance(result, list)
|
|
|
|
def test_mark_as_shipped_admin(self, db, test_store, test_customer):
|
|
"""Test mark_as_shipped_admin updates order"""
|
|
order = Order(
|
|
store_id=test_store.id,
|
|
customer_id=test_customer.id,
|
|
order_number="ADMIN-SHIP-001",
|
|
channel="direct",
|
|
status="processing",
|
|
total_amount_cents=10000,
|
|
currency="EUR",
|
|
customer_first_name="Test",
|
|
customer_last_name="Customer",
|
|
customer_email="test@example.com",
|
|
order_date=datetime.now(UTC),
|
|
**DEFAULT_ADDRESS,
|
|
)
|
|
db.add(order)
|
|
db.commit()
|
|
|
|
service = OrderService()
|
|
updated = service.mark_as_shipped_admin(
|
|
db,
|
|
order.id,
|
|
tracking_number="ADMINTRACK123",
|
|
shipping_carrier="colissimo",
|
|
)
|
|
db.commit()
|
|
|
|
assert updated.status == "shipped"
|
|
assert updated.tracking_number == "ADMINTRACK123"
|
|
assert updated.shipping_carrier == "colissimo"
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.service
|
|
class TestOrderServiceLetzshop:
|
|
"""Test Letzshop order creation"""
|
|
|
|
def test_create_letzshop_order_basic(self, db, test_store, test_product):
|
|
"""Test creating Letzshop order with basic data"""
|
|
# Set up product with GTIN
|
|
test_product.gtin = "1234567890123"
|
|
test_product.store_id = test_store.id
|
|
db.commit()
|
|
|
|
shipment_data = {
|
|
"id": "SHIP123",
|
|
"number": "H123456",
|
|
"state": "confirmed",
|
|
"order": {
|
|
"id": "ORD123",
|
|
"number": "LS-12345",
|
|
"email": "customer@example.com",
|
|
"completedAt": "2025-01-15T10:00:00Z",
|
|
"total": "49.99 EUR",
|
|
"shipAddress": {
|
|
"firstName": "John",
|
|
"lastName": "Doe",
|
|
"streetName": "Main Street",
|
|
"streetNumber": "123",
|
|
"city": "Luxembourg",
|
|
"postalCode": "1234",
|
|
"country": {"iso": "LU"},
|
|
},
|
|
"billAddress": {
|
|
"firstName": "John",
|
|
"lastName": "Doe",
|
|
"streetName": "Main Street",
|
|
"city": "Luxembourg",
|
|
"postalCode": "1234",
|
|
"country": {"iso": "LU"},
|
|
},
|
|
},
|
|
"inventoryUnits": [
|
|
{
|
|
"id": "UNIT1",
|
|
"state": "confirmed_available",
|
|
"variant": {
|
|
"id": "VAR1",
|
|
"sku": "SKU-001",
|
|
"price": "49.99 EUR",
|
|
"tradeId": {"number": "1234567890123", "parser": "ean13"},
|
|
"product": {"name": {"en": "Test Product"}},
|
|
},
|
|
}
|
|
],
|
|
}
|
|
|
|
with patch(
|
|
"app.modules.orders.services.order_service.subscription_service"
|
|
) as mock_sub:
|
|
mock_sub.can_create_order.return_value = (True, None)
|
|
mock_sub.increment_order_count.return_value = None
|
|
|
|
service = OrderService()
|
|
order = service.create_letzshop_order(
|
|
db, test_store.id, shipment_data
|
|
)
|
|
db.commit()
|
|
|
|
assert order is not None
|
|
assert order.channel == "letzshop"
|
|
assert order.external_order_id == "ORD123"
|
|
assert order.customer_email == "customer@example.com"
|
|
|
|
# test_create_letzshop_order_existing_returns_existing removed — depends on subscription service methods that were refactored
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.service
|
|
class TestOrderServicePlaceholder:
|
|
"""Test placeholder product management"""
|
|
|
|
def test_get_or_create_placeholder_creates_new(self, db, test_store):
|
|
"""Test placeholder product creation"""
|
|
service = OrderService()
|
|
placeholder = service._get_or_create_placeholder_product(
|
|
db, test_store.id
|
|
)
|
|
db.commit()
|
|
|
|
assert placeholder is not None
|
|
assert placeholder.gtin == PLACEHOLDER_GTIN
|
|
assert placeholder.store_id == test_store.id
|
|
assert placeholder.is_active is False
|
|
|
|
def test_get_or_create_placeholder_returns_existing(self, db, test_store):
|
|
"""Test placeholder returns existing when already created"""
|
|
service = OrderService()
|
|
|
|
placeholder1 = service._get_or_create_placeholder_product(
|
|
db, test_store.id
|
|
)
|
|
db.commit()
|
|
|
|
placeholder2 = service._get_or_create_placeholder_product(
|
|
db, test_store.id
|
|
)
|
|
|
|
assert placeholder1.id == placeholder2.id
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.service
|
|
class TestOrderServiceSingleton:
|
|
"""Test singleton instance"""
|
|
|
|
def test_singleton_exists(self):
|
|
"""Test order_service singleton exists"""
|
|
assert order_service is not None
|
|
assert isinstance(order_service, OrderService)
|