# 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 False # Default inactive 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)