feat: integer cents money handling, order page fixes, and vendor filter persistence
Money Handling Architecture: - Store all monetary values as integer cents (€105.91 = 10591) - Add app/utils/money.py with Money class and conversion helpers - Add static/shared/js/money.js for frontend formatting - Update all database models to use _cents columns (Product, Order, etc.) - Update CSV processor to convert prices to cents on import - Add Alembic migration for Float to Integer conversion - Create .architecture-rules/money.yaml with 7 validation rules - Add docs/architecture/money-handling.md documentation Order Details Page Fixes: - Fix customer name showing 'undefined undefined' - use flat field names - Fix vendor info empty - add vendor_name/vendor_code to OrderDetailResponse - Fix shipping address using wrong nested object structure - Enrich order detail API response with vendor info Vendor Filter Persistence Fixes: - Fix orders.js: restoreSavedVendor now sets selectedVendor and filters - Fix orders.js: init() only loads orders if no saved vendor to restore - Fix marketplace-letzshop.js: restoreSavedVendor calls selectVendor() - Fix marketplace-letzshop.js: clearVendorSelection clears TomSelect dropdown - Align vendor selector placeholder text between pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,65 @@
|
||||
# tests/unit/models/database/test_order.py
|
||||
"""Unit tests for Order and OrderItem database models."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from models.database.order import Order, OrderItem
|
||||
|
||||
|
||||
def create_order_with_snapshots(
|
||||
db,
|
||||
vendor,
|
||||
customer,
|
||||
customer_address,
|
||||
order_number,
|
||||
status="pending",
|
||||
subtotal=99.99,
|
||||
total_amount=99.99,
|
||||
**kwargs
|
||||
):
|
||||
"""Helper to create Order with required address snapshots."""
|
||||
# Remove channel from kwargs if present (we set it explicitly)
|
||||
channel = kwargs.pop("channel", "direct")
|
||||
|
||||
order = Order(
|
||||
vendor_id=vendor.id,
|
||||
customer_id=customer.id,
|
||||
order_number=order_number,
|
||||
status=status,
|
||||
channel=channel,
|
||||
subtotal=subtotal,
|
||||
total_amount=total_amount,
|
||||
currency="EUR",
|
||||
order_date=datetime.now(timezone.utc),
|
||||
# Customer snapshot
|
||||
customer_first_name=customer.first_name,
|
||||
customer_last_name=customer.last_name,
|
||||
customer_email=customer.email,
|
||||
# Shipping address snapshot
|
||||
ship_first_name=customer_address.first_name,
|
||||
ship_last_name=customer_address.last_name,
|
||||
ship_address_line_1=customer_address.address_line_1,
|
||||
ship_city=customer_address.city,
|
||||
ship_postal_code=customer_address.postal_code,
|
||||
ship_country_iso="LU",
|
||||
# Billing address snapshot
|
||||
bill_first_name=customer_address.first_name,
|
||||
bill_last_name=customer_address.last_name,
|
||||
bill_address_line_1=customer_address.address_line_1,
|
||||
bill_city=customer_address.city,
|
||||
bill_postal_code=customer_address.postal_code,
|
||||
bill_country_iso="LU",
|
||||
**kwargs
|
||||
)
|
||||
db.add(order)
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
return order
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestOrderModel:
|
||||
@@ -16,60 +69,37 @@ class TestOrderModel:
|
||||
self, db, test_vendor, test_customer, test_customer_address
|
||||
):
|
||||
"""Test Order model with customer relationship."""
|
||||
order = Order(
|
||||
vendor_id=test_vendor.id,
|
||||
customer_id=test_customer.id,
|
||||
order = create_order_with_snapshots(
|
||||
db, test_vendor, test_customer, test_customer_address,
|
||||
order_number="ORD-001",
|
||||
status="pending",
|
||||
subtotal=99.99,
|
||||
total_amount=99.99,
|
||||
currency="EUR",
|
||||
shipping_address_id=test_customer_address.id,
|
||||
billing_address_id=test_customer_address.id,
|
||||
)
|
||||
|
||||
db.add(order)
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
|
||||
assert order.id is not None
|
||||
assert order.vendor_id == test_vendor.id
|
||||
assert order.customer_id == test_customer.id
|
||||
assert order.order_number == "ORD-001"
|
||||
assert order.status == "pending"
|
||||
assert float(order.total_amount) == 99.99
|
||||
# Verify snapshots
|
||||
assert order.customer_first_name == test_customer.first_name
|
||||
assert order.ship_city == test_customer_address.city
|
||||
assert order.ship_country_iso == "LU"
|
||||
|
||||
def test_order_number_uniqueness(
|
||||
self, db, test_vendor, test_customer, test_customer_address
|
||||
):
|
||||
"""Test order_number unique constraint."""
|
||||
order1 = Order(
|
||||
vendor_id=test_vendor.id,
|
||||
customer_id=test_customer.id,
|
||||
create_order_with_snapshots(
|
||||
db, test_vendor, test_customer, test_customer_address,
|
||||
order_number="UNIQUE-ORD-001",
|
||||
status="pending",
|
||||
subtotal=50.00,
|
||||
total_amount=50.00,
|
||||
shipping_address_id=test_customer_address.id,
|
||||
billing_address_id=test_customer_address.id,
|
||||
)
|
||||
db.add(order1)
|
||||
db.commit()
|
||||
|
||||
# Duplicate order number should fail
|
||||
with pytest.raises(IntegrityError):
|
||||
order2 = Order(
|
||||
vendor_id=test_vendor.id,
|
||||
customer_id=test_customer.id,
|
||||
create_order_with_snapshots(
|
||||
db, test_vendor, test_customer, test_customer_address,
|
||||
order_number="UNIQUE-ORD-001",
|
||||
status="pending",
|
||||
subtotal=75.00,
|
||||
total_amount=75.00,
|
||||
shipping_address_id=test_customer_address.id,
|
||||
billing_address_id=test_customer_address.id,
|
||||
)
|
||||
db.add(order2)
|
||||
db.commit()
|
||||
|
||||
def test_order_status_values(
|
||||
self, db, test_vendor, test_customer, test_customer_address
|
||||
@@ -77,49 +107,32 @@ class TestOrderModel:
|
||||
"""Test Order with different status values."""
|
||||
statuses = [
|
||||
"pending",
|
||||
"confirmed",
|
||||
"processing",
|
||||
"shipped",
|
||||
"delivered",
|
||||
"cancelled",
|
||||
"refunded",
|
||||
]
|
||||
|
||||
for i, status in enumerate(statuses):
|
||||
order = Order(
|
||||
vendor_id=test_vendor.id,
|
||||
customer_id=test_customer.id,
|
||||
order = create_order_with_snapshots(
|
||||
db, test_vendor, test_customer, test_customer_address,
|
||||
order_number=f"STATUS-ORD-{i:03d}",
|
||||
status=status,
|
||||
subtotal=50.00,
|
||||
total_amount=50.00,
|
||||
shipping_address_id=test_customer_address.id,
|
||||
billing_address_id=test_customer_address.id,
|
||||
)
|
||||
db.add(order)
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
|
||||
assert order.status == status
|
||||
|
||||
def test_order_amounts(self, db, test_vendor, test_customer, test_customer_address):
|
||||
"""Test Order amount fields."""
|
||||
order = Order(
|
||||
vendor_id=test_vendor.id,
|
||||
customer_id=test_customer.id,
|
||||
order = create_order_with_snapshots(
|
||||
db, test_vendor, test_customer, test_customer_address,
|
||||
order_number="AMOUNTS-ORD-001",
|
||||
status="pending",
|
||||
subtotal=100.00,
|
||||
tax_amount=20.00,
|
||||
shipping_amount=10.00,
|
||||
discount_amount=5.00,
|
||||
total_amount=125.00,
|
||||
currency="EUR",
|
||||
shipping_address_id=test_customer_address.id,
|
||||
billing_address_id=test_customer_address.id,
|
||||
)
|
||||
db.add(order)
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
|
||||
assert float(order.subtotal) == 100.00
|
||||
assert float(order.tax_amount) == 20.00
|
||||
@@ -131,25 +144,32 @@ class TestOrderModel:
|
||||
self, db, test_vendor, test_customer, test_customer_address
|
||||
):
|
||||
"""Test Order relationships."""
|
||||
order = Order(
|
||||
vendor_id=test_vendor.id,
|
||||
customer_id=test_customer.id,
|
||||
order = create_order_with_snapshots(
|
||||
db, test_vendor, test_customer, test_customer_address,
|
||||
order_number="REL-ORD-001",
|
||||
status="pending",
|
||||
subtotal=50.00,
|
||||
total_amount=50.00,
|
||||
shipping_address_id=test_customer_address.id,
|
||||
billing_address_id=test_customer_address.id,
|
||||
)
|
||||
db.add(order)
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
|
||||
assert order.vendor is not None
|
||||
assert order.customer is not None
|
||||
assert order.vendor.id == test_vendor.id
|
||||
assert order.customer.id == test_customer.id
|
||||
|
||||
def test_order_channel_field(
|
||||
self, db, test_vendor, test_customer, test_customer_address
|
||||
):
|
||||
"""Test Order channel field for marketplace support."""
|
||||
order = create_order_with_snapshots(
|
||||
db, test_vendor, test_customer, test_customer_address,
|
||||
order_number="CHANNEL-ORD-001",
|
||||
channel="letzshop",
|
||||
external_order_id="LS-12345",
|
||||
external_shipment_id="SHIP-67890",
|
||||
)
|
||||
|
||||
assert order.channel == "letzshop"
|
||||
assert order.external_order_id == "LS-12345"
|
||||
assert order.external_shipment_id == "SHIP-67890"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
@@ -249,3 +269,29 @@ class TestOrderItemModel:
|
||||
assert item1.order_id == item2.order_id
|
||||
assert item1.id != item2.id
|
||||
assert item1.product_id == item2.product_id # Same product, different items
|
||||
|
||||
def test_order_item_needs_product_match(self, db, test_order, test_product):
|
||||
"""Test OrderItem needs_product_match flag for exceptions."""
|
||||
order_item = OrderItem(
|
||||
order_id=test_order.id,
|
||||
product_id=test_product.id,
|
||||
product_name="Unmatched Product",
|
||||
product_sku="UNMATCHED-001",
|
||||
quantity=1,
|
||||
unit_price=50.00,
|
||||
total_price=50.00,
|
||||
needs_product_match=True,
|
||||
)
|
||||
|
||||
db.add(order_item)
|
||||
db.commit()
|
||||
db.refresh(order_item)
|
||||
|
||||
assert order_item.needs_product_match is True
|
||||
|
||||
# Resolve the match
|
||||
order_item.needs_product_match = False
|
||||
db.commit()
|
||||
db.refresh(order_item)
|
||||
|
||||
assert order_item.needs_product_match is False
|
||||
|
||||
242
tests/unit/models/database/test_order_item_exception.py
Normal file
242
tests/unit/models/database/test_order_item_exception.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# tests/unit/models/database/test_order_item_exception.py
|
||||
"""Unit tests for OrderItemException database model."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from models.database.order_item_exception import OrderItemException
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestOrderItemExceptionModel:
|
||||
"""Test OrderItemException model."""
|
||||
|
||||
def test_exception_creation(self, db, test_order_item, test_vendor):
|
||||
"""Test OrderItemException model creation."""
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
vendor_id=test_vendor.id,
|
||||
original_gtin="4006381333931",
|
||||
original_product_name="Test Missing Product",
|
||||
original_sku="MISSING-SKU-001",
|
||||
exception_type="product_not_found",
|
||||
status="pending",
|
||||
)
|
||||
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.id is not None
|
||||
assert exception.order_item_id == test_order_item.id
|
||||
assert exception.vendor_id == test_vendor.id
|
||||
assert exception.original_gtin == "4006381333931"
|
||||
assert exception.original_product_name == "Test Missing Product"
|
||||
assert exception.original_sku == "MISSING-SKU-001"
|
||||
assert exception.exception_type == "product_not_found"
|
||||
assert exception.status == "pending"
|
||||
assert exception.created_at is not None
|
||||
|
||||
def test_exception_unique_order_item(self, db, test_order_item, test_vendor):
|
||||
"""Test that only one exception can exist per order item."""
|
||||
exception1 = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
vendor_id=test_vendor.id,
|
||||
original_gtin="4006381333931",
|
||||
exception_type="product_not_found",
|
||||
)
|
||||
db.add(exception1)
|
||||
db.commit()
|
||||
|
||||
# Second exception for the same order item should fail
|
||||
with pytest.raises(IntegrityError):
|
||||
exception2 = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
vendor_id=test_vendor.id,
|
||||
original_gtin="4006381333932",
|
||||
exception_type="product_not_found",
|
||||
)
|
||||
db.add(exception2)
|
||||
db.commit()
|
||||
|
||||
def test_exception_types(self, db, test_order_item, test_vendor):
|
||||
"""Test different exception types."""
|
||||
exception_types = ["product_not_found", "gtin_mismatch", "duplicate_gtin"]
|
||||
|
||||
# Create new order items for each test to avoid unique constraint
|
||||
from models.database.order import OrderItem
|
||||
|
||||
for i, exc_type in enumerate(exception_types):
|
||||
order_item = OrderItem(
|
||||
order_id=test_order_item.order_id,
|
||||
product_id=test_order_item.product_id,
|
||||
product_name=f"Product {i}",
|
||||
product_sku=f"SKU-{i}",
|
||||
quantity=1,
|
||||
unit_price=10.00,
|
||||
total_price=10.00,
|
||||
)
|
||||
db.add(order_item)
|
||||
db.commit()
|
||||
db.refresh(order_item)
|
||||
|
||||
exception = OrderItemException(
|
||||
order_item_id=order_item.id,
|
||||
vendor_id=test_vendor.id,
|
||||
original_gtin=f"400638133393{i}",
|
||||
exception_type=exc_type,
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.exception_type == exc_type
|
||||
|
||||
def test_exception_status_values(self, db, test_order_item, test_vendor):
|
||||
"""Test different status values."""
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
vendor_id=test_vendor.id,
|
||||
original_gtin="4006381333931",
|
||||
exception_type="product_not_found",
|
||||
status="pending",
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
|
||||
# Test pending status
|
||||
assert exception.status == "pending"
|
||||
assert exception.is_pending is True
|
||||
assert exception.is_resolved is False
|
||||
assert exception.is_ignored is False
|
||||
assert exception.blocks_confirmation is True
|
||||
|
||||
# Test resolved status
|
||||
exception.status = "resolved"
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.is_pending is False
|
||||
assert exception.is_resolved is True
|
||||
assert exception.is_ignored is False
|
||||
assert exception.blocks_confirmation is False
|
||||
|
||||
# Test ignored status
|
||||
exception.status = "ignored"
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.is_pending is False
|
||||
assert exception.is_resolved is False
|
||||
assert exception.is_ignored is True
|
||||
assert exception.blocks_confirmation is True # Ignored still blocks
|
||||
|
||||
def test_exception_nullable_fields(self, db, test_order_item, test_vendor):
|
||||
"""Test that GTIN and other fields can be null."""
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
vendor_id=test_vendor.id,
|
||||
original_gtin=None, # Can be null for vouchers etc.
|
||||
original_product_name="Gift Voucher",
|
||||
original_sku=None,
|
||||
exception_type="product_not_found",
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.id is not None
|
||||
assert exception.original_gtin is None
|
||||
assert exception.original_sku is None
|
||||
assert exception.original_product_name == "Gift Voucher"
|
||||
|
||||
def test_exception_resolution(self, db, test_order_item, test_vendor, test_product, test_user):
|
||||
"""Test resolving an exception with a product."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
vendor_id=test_vendor.id,
|
||||
original_gtin="4006381333931",
|
||||
exception_type="product_not_found",
|
||||
status="pending",
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
|
||||
# Resolve the exception
|
||||
now = datetime.now(timezone.utc)
|
||||
exception.status = "resolved"
|
||||
exception.resolved_product_id = test_product.id
|
||||
exception.resolved_at = now
|
||||
exception.resolved_by = test_user.id
|
||||
exception.resolution_notes = "Matched to existing product"
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.status == "resolved"
|
||||
assert exception.resolved_product_id == test_product.id
|
||||
assert exception.resolved_at is not None
|
||||
assert exception.resolved_by == test_user.id
|
||||
assert exception.resolution_notes == "Matched to existing product"
|
||||
|
||||
def test_exception_relationships(self, db, test_order_item, test_vendor):
|
||||
"""Test OrderItemException relationships."""
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
vendor_id=test_vendor.id,
|
||||
original_gtin="4006381333931",
|
||||
exception_type="product_not_found",
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.order_item is not None
|
||||
assert exception.order_item.id == test_order_item.id
|
||||
assert exception.vendor is not None
|
||||
assert exception.vendor.id == test_vendor.id
|
||||
|
||||
def test_exception_repr(self, db, test_order_item, test_vendor):
|
||||
"""Test OrderItemException __repr__ method."""
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
vendor_id=test_vendor.id,
|
||||
original_gtin="4006381333931",
|
||||
exception_type="product_not_found",
|
||||
status="pending",
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
repr_str = repr(exception)
|
||||
assert "OrderItemException" in repr_str
|
||||
assert str(exception.id) in repr_str
|
||||
assert "4006381333931" in repr_str
|
||||
assert "pending" in repr_str
|
||||
|
||||
def test_exception_cascade_delete(self, db, test_order_item, test_vendor):
|
||||
"""Test that exception is deleted when order item is deleted."""
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
vendor_id=test_vendor.id,
|
||||
original_gtin="4006381333931",
|
||||
exception_type="product_not_found",
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
exception_id = exception.id
|
||||
|
||||
# Delete the order item
|
||||
db.delete(test_order_item)
|
||||
db.commit()
|
||||
|
||||
# Exception should be cascade deleted
|
||||
deleted_exception = (
|
||||
db.query(OrderItemException)
|
||||
.filter(OrderItemException.id == exception_id)
|
||||
.first()
|
||||
)
|
||||
assert deleted_exception is None
|
||||
@@ -165,16 +165,16 @@ class TestProductModel:
|
||||
|
||||
def test_product_reset_to_source(self, db, test_vendor, test_marketplace_product):
|
||||
"""Test reset_to_source methods."""
|
||||
# Set up marketplace product values
|
||||
test_marketplace_product.price_numeric = 100.00
|
||||
# Set up marketplace product values (use cents internally)
|
||||
test_marketplace_product.price_cents = 10000 # €100.00
|
||||
test_marketplace_product.brand = "SourceBrand"
|
||||
db.commit()
|
||||
|
||||
# Create product with overrides
|
||||
# Create product with overrides (price property converts euros to cents)
|
||||
product = Product(
|
||||
vendor_id=test_vendor.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
price=89.99,
|
||||
price_cents=8999, # €89.99
|
||||
brand="OverrideBrand",
|
||||
)
|
||||
db.add(product)
|
||||
@@ -184,13 +184,14 @@ class TestProductModel:
|
||||
assert product.effective_price == 89.99
|
||||
assert product.effective_brand == "OverrideBrand"
|
||||
|
||||
# Reset price to source
|
||||
product.reset_field_to_source("price")
|
||||
# Reset price_cents to source (OVERRIDABLE_FIELDS now uses _cents names)
|
||||
product.reset_field_to_source("price_cents")
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
assert product.price is None
|
||||
assert product.effective_price == 100.00 # Now inherits
|
||||
assert product.price_cents is None
|
||||
assert product.price is None # Property returns None when cents is None
|
||||
assert product.effective_price == 100.00 # Now inherits from marketplace
|
||||
|
||||
# Reset all fields
|
||||
product.reset_all_to_source()
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
# 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 (
|
||||
OrderAddressCreate,
|
||||
AddressSnapshot,
|
||||
AddressSnapshotResponse,
|
||||
CustomerSnapshot,
|
||||
CustomerSnapshotResponse,
|
||||
OrderCreate,
|
||||
OrderItemCreate,
|
||||
OrderItemResponse,
|
||||
@@ -61,81 +66,155 @@ class TestOrderItemCreateSchema:
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestOrderAddressCreateSchema:
|
||||
"""Test OrderAddressCreate schema validation."""
|
||||
class TestAddressSnapshotSchema:
|
||||
"""Test AddressSnapshot schema validation."""
|
||||
|
||||
def test_valid_address(self):
|
||||
"""Test valid order address creation."""
|
||||
address = OrderAddressCreate(
|
||||
"""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="Luxembourg",
|
||||
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):
|
||||
OrderAddressCreate(
|
||||
AddressSnapshot(
|
||||
first_name="John",
|
||||
# missing required fields
|
||||
)
|
||||
|
||||
def test_optional_company(self):
|
||||
"""Test optional company field."""
|
||||
address = OrderAddressCreate(
|
||||
address = AddressSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
company="Tech Corp",
|
||||
address_line_1="123 Main St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country="Luxembourg",
|
||||
country_iso="LU",
|
||||
)
|
||||
assert address.company == "Tech Corp"
|
||||
|
||||
def test_optional_address_line_2(self):
|
||||
"""Test optional address_line_2 field."""
|
||||
address = OrderAddressCreate(
|
||||
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="Luxembourg",
|
||||
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):
|
||||
OrderAddressCreate(
|
||||
AddressSnapshot(
|
||||
first_name="",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Main St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country="Luxembourg",
|
||||
country_iso="LU",
|
||||
)
|
||||
|
||||
def test_country_min_length(self):
|
||||
"""Test country minimum length (2)."""
|
||||
def test_country_iso_min_length(self):
|
||||
"""Test country_iso minimum length (2)."""
|
||||
with pytest.raises(ValidationError):
|
||||
OrderAddressCreate(
|
||||
AddressSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Main St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country="L",
|
||||
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:
|
||||
@@ -148,13 +227,18 @@ class TestOrderCreateSchema:
|
||||
OrderItemCreate(product_id=1, quantity=2),
|
||||
OrderItemCreate(product_id=2, quantity=1),
|
||||
],
|
||||
shipping_address=OrderAddressCreate(
|
||||
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="Luxembourg",
|
||||
country_iso="LU",
|
||||
),
|
||||
)
|
||||
assert len(order.items) == 2
|
||||
@@ -164,13 +248,18 @@ class TestOrderCreateSchema:
|
||||
"""Test items are required."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrderCreate(
|
||||
shipping_address=OrderAddressCreate(
|
||||
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="Luxembourg",
|
||||
country_iso="LU",
|
||||
),
|
||||
)
|
||||
assert "items" in str(exc_info.value).lower()
|
||||
@@ -180,22 +269,48 @@ class TestOrderCreateSchema:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrderCreate(
|
||||
items=[],
|
||||
shipping_address=OrderAddressCreate(
|
||||
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="Luxembourg",
|
||||
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()
|
||||
|
||||
@@ -203,21 +318,26 @@ class TestOrderCreateSchema:
|
||||
"""Test billing_address is optional."""
|
||||
order = OrderCreate(
|
||||
items=[OrderItemCreate(product_id=1, quantity=1)],
|
||||
shipping_address=OrderAddressCreate(
|
||||
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="Luxembourg",
|
||||
country_iso="LU",
|
||||
),
|
||||
billing_address=OrderAddressCreate(
|
||||
billing_address=AddressSnapshot(
|
||||
first_name="Jane",
|
||||
last_name="Doe",
|
||||
address_line_1="456 Other St",
|
||||
city="Esch",
|
||||
postal_code="L-4321",
|
||||
country="Luxembourg",
|
||||
country_iso="LU",
|
||||
),
|
||||
)
|
||||
assert order.billing_address is not None
|
||||
@@ -227,13 +347,18 @@ class TestOrderCreateSchema:
|
||||
"""Test optional customer_notes."""
|
||||
order = OrderCreate(
|
||||
items=[OrderItemCreate(product_id=1, quantity=1)],
|
||||
shipping_address=OrderAddressCreate(
|
||||
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="Luxembourg",
|
||||
country_iso="LU",
|
||||
),
|
||||
customer_notes="Please leave at door",
|
||||
)
|
||||
@@ -293,13 +418,13 @@ class TestOrderResponseSchema:
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test creating response from dict."""
|
||||
from datetime import datetime
|
||||
|
||||
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,
|
||||
@@ -307,21 +432,97 @@ class TestOrderResponseSchema:
|
||||
"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,
|
||||
"created_at": datetime.now(),
|
||||
"updated_at": datetime.now(),
|
||||
"paid_at": 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
|
||||
@@ -331,26 +532,64 @@ class TestOrderItemResponseSchema:
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test creating response from dict."""
|
||||
from datetime import datetime
|
||||
|
||||
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,
|
||||
"created_at": datetime.now(),
|
||||
"updated_at": datetime.now(),
|
||||
"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
|
||||
|
||||
Reference in New Issue
Block a user