# tests/unit/models/schema/test_order.py """Unit tests for order Pydantic schemas.""" from datetime import datetime, timezone import pytest from pydantic import ValidationError from models.schema.order import ( AddressSnapshot, AddressSnapshotResponse, CustomerSnapshot, CustomerSnapshotResponse, OrderCreate, OrderItemCreate, OrderItemResponse, OrderListResponse, OrderResponse, OrderUpdate, ) @pytest.mark.unit @pytest.mark.schema class TestOrderItemCreateSchema: """Test OrderItemCreate schema validation.""" def test_valid_order_item(self): """Test valid order item creation.""" item = OrderItemCreate( product_id=1, quantity=2, ) assert item.product_id == 1 assert item.quantity == 2 def test_product_id_required(self): """Test product_id is required.""" with pytest.raises(ValidationError) as exc_info: OrderItemCreate(quantity=2) assert "product_id" in str(exc_info.value).lower() def test_quantity_required(self): """Test quantity is required.""" with pytest.raises(ValidationError) as exc_info: OrderItemCreate(product_id=1) assert "quantity" in str(exc_info.value).lower() def test_quantity_must_be_at_least_1(self): """Test quantity must be >= 1.""" with pytest.raises(ValidationError) as exc_info: OrderItemCreate( product_id=1, quantity=0, ) assert "quantity" in str(exc_info.value).lower() def test_negative_quantity_invalid(self): """Test negative quantity is invalid.""" with pytest.raises(ValidationError): OrderItemCreate( product_id=1, quantity=-1, ) @pytest.mark.unit @pytest.mark.schema class TestAddressSnapshotSchema: """Test AddressSnapshot schema validation.""" def test_valid_address(self): """Test valid address creation.""" address = AddressSnapshot( first_name="John", last_name="Doe", address_line_1="123 Main St", city="Luxembourg", postal_code="L-1234", country_iso="LU", ) assert address.first_name == "John" assert address.city == "Luxembourg" assert address.country_iso == "LU" def test_required_fields(self): """Test required fields validation.""" with pytest.raises(ValidationError): AddressSnapshot( first_name="John", # missing required fields ) def test_optional_company(self): """Test optional company field.""" address = AddressSnapshot( first_name="John", last_name="Doe", company="Tech Corp", address_line_1="123 Main St", city="Luxembourg", postal_code="L-1234", country_iso="LU", ) assert address.company == "Tech Corp" def test_optional_address_line_2(self): """Test optional address_line_2 field.""" address = AddressSnapshot( first_name="John", last_name="Doe", address_line_1="123 Main St", address_line_2="Suite 500", city="Luxembourg", postal_code="L-1234", country_iso="LU", ) assert address.address_line_2 == "Suite 500" def test_first_name_min_length(self): """Test first_name minimum length.""" with pytest.raises(ValidationError): AddressSnapshot( first_name="", last_name="Doe", address_line_1="123 Main St", city="Luxembourg", postal_code="L-1234", country_iso="LU", ) def test_country_iso_min_length(self): """Test country_iso minimum length (2).""" with pytest.raises(ValidationError): AddressSnapshot( first_name="John", last_name="Doe", address_line_1="123 Main St", city="Luxembourg", postal_code="L-1234", country_iso="L", # Too short ) @pytest.mark.unit @pytest.mark.schema class TestAddressSnapshotResponseSchema: """Test AddressSnapshotResponse schema.""" def test_full_name_property(self): """Test full_name property.""" response = AddressSnapshotResponse( first_name="John", last_name="Doe", company=None, address_line_1="123 Main St", address_line_2=None, city="Luxembourg", postal_code="L-1234", country_iso="LU", ) assert response.full_name == "John Doe" @pytest.mark.unit @pytest.mark.schema class TestCustomerSnapshotSchema: """Test CustomerSnapshot schema validation.""" def test_valid_customer(self): """Test valid customer snapshot.""" customer = CustomerSnapshot( first_name="John", last_name="Doe", email="john@example.com", phone="+352123456", locale="en", ) assert customer.first_name == "John" assert customer.email == "john@example.com" def test_optional_phone(self): """Test phone is optional.""" customer = CustomerSnapshot( first_name="John", last_name="Doe", email="john@example.com", ) assert customer.phone is None def test_optional_locale(self): """Test locale is optional.""" customer = CustomerSnapshot( first_name="John", last_name="Doe", email="john@example.com", ) assert customer.locale is None @pytest.mark.unit @pytest.mark.schema class TestCustomerSnapshotResponseSchema: """Test CustomerSnapshotResponse schema.""" def test_full_name_property(self): """Test full_name property.""" response = CustomerSnapshotResponse( first_name="John", last_name="Doe", email="john@example.com", phone=None, locale=None, ) assert response.full_name == "John Doe" @pytest.mark.unit @pytest.mark.schema class TestOrderCreateSchema: """Test OrderCreate schema validation.""" def test_valid_order(self): """Test valid order creation.""" order = OrderCreate( items=[ OrderItemCreate(product_id=1, quantity=2), OrderItemCreate(product_id=2, quantity=1), ], customer=CustomerSnapshot( first_name="John", last_name="Doe", email="john@example.com", ), shipping_address=AddressSnapshot( first_name="John", last_name="Doe", address_line_1="123 Main St", city="Luxembourg", postal_code="L-1234", country_iso="LU", ), ) assert len(order.items) == 2 assert order.customer_id is None # Optional for guest checkout def test_items_required(self): """Test items are required.""" with pytest.raises(ValidationError) as exc_info: OrderCreate( customer=CustomerSnapshot( first_name="John", last_name="Doe", email="john@example.com", ), shipping_address=AddressSnapshot( first_name="John", last_name="Doe", address_line_1="123 Main St", city="Luxembourg", postal_code="L-1234", country_iso="LU", ), ) assert "items" in str(exc_info.value).lower() def test_items_must_not_be_empty(self): """Test items list must have at least 1 item.""" with pytest.raises(ValidationError) as exc_info: OrderCreate( items=[], customer=CustomerSnapshot( first_name="John", last_name="Doe", email="john@example.com", ), shipping_address=AddressSnapshot( first_name="John", last_name="Doe", address_line_1="123 Main St", city="Luxembourg", postal_code="L-1234", country_iso="LU", ), ) assert "items" in str(exc_info.value).lower() def test_customer_required(self): """Test customer is required.""" with pytest.raises(ValidationError) as exc_info: OrderCreate( items=[OrderItemCreate(product_id=1, quantity=1)], shipping_address=AddressSnapshot( first_name="John", last_name="Doe", address_line_1="123 Main St", city="Luxembourg", postal_code="L-1234", country_iso="LU", ), ) assert "customer" in str(exc_info.value).lower() def test_shipping_address_required(self): """Test shipping_address is required.""" with pytest.raises(ValidationError) as exc_info: OrderCreate( items=[OrderItemCreate(product_id=1, quantity=1)], customer=CustomerSnapshot( first_name="John", last_name="Doe", email="john@example.com", ), ) assert "shipping_address" in str(exc_info.value).lower() def test_optional_billing_address(self): """Test billing_address is optional.""" order = OrderCreate( items=[OrderItemCreate(product_id=1, quantity=1)], customer=CustomerSnapshot( first_name="John", last_name="Doe", email="john@example.com", ), shipping_address=AddressSnapshot( first_name="John", last_name="Doe", address_line_1="123 Main St", city="Luxembourg", postal_code="L-1234", country_iso="LU", ), billing_address=AddressSnapshot( first_name="Jane", last_name="Doe", address_line_1="456 Other St", city="Esch", postal_code="L-4321", country_iso="LU", ), ) assert order.billing_address is not None assert order.billing_address.first_name == "Jane" def test_optional_customer_notes(self): """Test optional customer_notes.""" order = OrderCreate( items=[OrderItemCreate(product_id=1, quantity=1)], customer=CustomerSnapshot( first_name="John", last_name="Doe", email="john@example.com", ), shipping_address=AddressSnapshot( first_name="John", last_name="Doe", address_line_1="123 Main St", city="Luxembourg", postal_code="L-1234", country_iso="LU", ), customer_notes="Please leave at door", ) assert order.customer_notes == "Please leave at door" @pytest.mark.unit @pytest.mark.schema class TestOrderUpdateSchema: """Test OrderUpdate schema validation.""" def test_status_update(self): """Test valid status update.""" update = OrderUpdate(status="processing") assert update.status == "processing" def test_valid_status_values(self): """Test all valid status values.""" valid_statuses = [ "pending", "processing", "shipped", "delivered", "cancelled", "refunded", ] for status in valid_statuses: update = OrderUpdate(status=status) assert update.status == status def test_invalid_status(self): """Test invalid status raises ValidationError.""" with pytest.raises(ValidationError) as exc_info: OrderUpdate(status="invalid_status") assert "status" in str(exc_info.value).lower() def test_tracking_number_update(self): """Test tracking number update.""" update = OrderUpdate(tracking_number="TRACK123456") assert update.tracking_number == "TRACK123456" def test_internal_notes_update(self): """Test internal notes update.""" update = OrderUpdate(internal_notes="Customer requested expedited shipping") assert update.internal_notes == "Customer requested expedited shipping" def test_empty_update_is_valid(self): """Test empty update is valid.""" update = OrderUpdate() assert update.model_dump(exclude_unset=True) == {} @pytest.mark.unit @pytest.mark.schema class TestOrderResponseSchema: """Test OrderResponse schema.""" def test_from_dict(self): """Test creating response from dict.""" now = datetime.now(timezone.utc) data = { "id": 1, "vendor_id": 1, "customer_id": 1, "order_number": "ORD-001", "channel": "direct", "status": "pending", "subtotal": 100.00, "tax_amount": 20.00, "shipping_amount": 10.00, "discount_amount": 5.00, "total_amount": 125.00, "currency": "EUR", # Customer snapshot "customer_first_name": "John", "customer_last_name": "Doe", "customer_email": "john@example.com", "customer_phone": None, "customer_locale": "en", # Ship address snapshot "ship_first_name": "John", "ship_last_name": "Doe", "ship_company": None, "ship_address_line_1": "123 Main St", "ship_address_line_2": None, "ship_city": "Luxembourg", "ship_postal_code": "L-1234", "ship_country_iso": "LU", # Bill address snapshot "bill_first_name": "John", "bill_last_name": "Doe", "bill_company": None, "bill_address_line_1": "123 Main St", "bill_address_line_2": None, "bill_city": "Luxembourg", "bill_postal_code": "L-1234", "bill_country_iso": "LU", # Tracking "shipping_method": "standard", "tracking_number": None, "tracking_provider": None, # Notes "customer_notes": None, "internal_notes": None, # Timestamps "order_date": now, "confirmed_at": None, "shipped_at": None, "delivered_at": None, "cancelled_at": None, "created_at": now, "updated_at": now, } response = OrderResponse(**data) assert response.id == 1 assert response.order_number == "ORD-001" assert response.total_amount == 125.00 assert response.channel == "direct" assert response.customer_full_name == "John Doe" def test_is_marketplace_order(self): """Test is_marketplace_order property.""" now = datetime.now(timezone.utc) # Direct order direct_order = OrderResponse( id=1, vendor_id=1, customer_id=1, order_number="ORD-001", channel="direct", status="pending", subtotal=100.0, tax_amount=0.0, shipping_amount=0.0, discount_amount=0.0, total_amount=100.0, currency="EUR", customer_first_name="John", customer_last_name="Doe", customer_email="john@example.com", customer_phone=None, customer_locale=None, ship_first_name="John", ship_last_name="Doe", ship_company=None, ship_address_line_1="123 Main", ship_address_line_2=None, ship_city="Luxembourg", ship_postal_code="L-1234", ship_country_iso="LU", bill_first_name="John", bill_last_name="Doe", bill_company=None, bill_address_line_1="123 Main", bill_address_line_2=None, bill_city="Luxembourg", bill_postal_code="L-1234", bill_country_iso="LU", shipping_method=None, tracking_number=None, tracking_provider=None, customer_notes=None, internal_notes=None, order_date=now, confirmed_at=None, shipped_at=None, delivered_at=None, cancelled_at=None, created_at=now, updated_at=now, ) assert direct_order.is_marketplace_order is False # Marketplace order marketplace_order = OrderResponse( id=2, vendor_id=1, customer_id=1, order_number="LS-001", channel="letzshop", status="pending", subtotal=100.0, tax_amount=0.0, shipping_amount=0.0, discount_amount=0.0, total_amount=100.0, currency="EUR", customer_first_name="John", customer_last_name="Doe", customer_email="john@example.com", customer_phone=None, customer_locale=None, ship_first_name="John", ship_last_name="Doe", ship_company=None, ship_address_line_1="123 Main", ship_address_line_2=None, ship_city="Luxembourg", ship_postal_code="L-1234", ship_country_iso="LU", bill_first_name="John", bill_last_name="Doe", bill_company=None, bill_address_line_1="123 Main", bill_address_line_2=None, bill_city="Luxembourg", bill_postal_code="L-1234", bill_country_iso="LU", shipping_method=None, tracking_number=None, tracking_provider=None, customer_notes=None, internal_notes=None, order_date=now, confirmed_at=None, shipped_at=None, delivered_at=None, cancelled_at=None, created_at=now, updated_at=now, ) assert marketplace_order.is_marketplace_order is True @pytest.mark.unit @pytest.mark.schema class TestOrderItemResponseSchema: """Test OrderItemResponse schema.""" def test_from_dict(self): """Test creating response from dict.""" now = datetime.now(timezone.utc) data = { "id": 1, "order_id": 1, "product_id": 1, "product_name": "Test Product", "product_sku": "SKU-001", "gtin": "4006381333931", "gtin_type": "EAN13", "quantity": 2, "unit_price": 50.00, "total_price": 100.00, "inventory_reserved": True, "inventory_fulfilled": False, "needs_product_match": False, "created_at": now, "updated_at": now, } response = OrderItemResponse(**data) assert response.id == 1 assert response.quantity == 2 assert response.total_price == 100.00 assert response.gtin == "4006381333931" def test_has_unresolved_exception(self): """Test has_unresolved_exception property.""" now = datetime.now(timezone.utc) base_data = { "id": 1, "order_id": 1, "product_id": 1, "product_name": "Test", "product_sku": "SKU-001", "gtin": None, "gtin_type": None, "quantity": 1, "unit_price": 10.0, "total_price": 10.0, "inventory_reserved": False, "inventory_fulfilled": False, "created_at": now, "updated_at": now, } # No exception response = OrderItemResponse(**base_data, needs_product_match=False, exception=None) assert response.has_unresolved_exception is False # Pending exception from models.schema.order import OrderItemExceptionBrief pending_exc = OrderItemExceptionBrief( id=1, original_gtin="123", original_product_name="Test", exception_type="product_not_found", status="pending", resolved_product_id=None, ) response = OrderItemResponse(**base_data, needs_product_match=True, exception=pending_exc) assert response.has_unresolved_exception is True # Resolved exception resolved_exc = OrderItemExceptionBrief( id=1, original_gtin="123", original_product_name="Test", exception_type="product_not_found", status="resolved", resolved_product_id=5, ) response = OrderItemResponse(**base_data, needs_product_match=False, exception=resolved_exc) assert response.has_unresolved_exception is False @pytest.mark.unit @pytest.mark.schema class TestOrderListResponseSchema: """Test OrderListResponse schema.""" def test_valid_list_response(self): """Test valid list response structure.""" response = OrderListResponse( orders=[], total=0, skip=0, limit=10, ) assert response.orders == [] assert response.total == 0 assert response.skip == 0 assert response.limit == 10