Files
orion/tests/unit/models/schema/test_order.py
Samir Boulahtit 4adc35a674 refactor: update test imports to use module locations
Update all test files to import from canonical module locations:
- Integration tests: orders, inventory, messages, invoices
- Unit tests: services and models
- Fixtures: customer, vendor, message fixtures

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 09:02:50 +01:00

612 lines
21 KiB
Python

# 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 app.modules.orders.schemas 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 app.modules.orders.schemas 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