fix(billing): complete billing module — fix tier change, platform support, merchant portal

- Fix admin tier change: resolve tier_code→tier_id in update_subscription(),
  delegate to billing_service.change_tier() for Stripe-connected subs
- Add platform support to admin tiers page: platform column, filter dropdown,
  platform selector in create/edit modal, platform_name in tier API response
- Filter used platforms in create subscription modal on merchant detail page
- Enrich merchant portal API responses with tier code, tier_name, platform_name
- Add eager-load of platform relationship in get_merchant_subscription()
- Remove stale store_name/store_code references from merchant templates
- Add merchant tier change endpoint (POST /change-tier) and tier selector UI
  replacing broken requestUpgrade() button
- Fix subscription detail link to use platform_id instead of sub.id

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 20:49:48 +01:00
parent 0b37274140
commit d1fe3584ff
54 changed files with 222 additions and 52 deletions

View File

@@ -1,358 +0,0 @@
# tests/unit/models/schema/test_customer.py
"""Unit tests for customer Pydantic schemas."""
import pytest
from pydantic import ValidationError
from app.modules.customers.schemas import (
CustomerAddressCreate,
CustomerAddressResponse,
CustomerAddressUpdate,
CustomerPreferencesUpdate,
CustomerRegister,
CustomerResponse,
CustomerUpdate,
)
@pytest.mark.unit
@pytest.mark.schema
class TestCustomerRegisterSchema:
"""Test CustomerRegister schema validation."""
def test_valid_registration(self):
"""Test valid registration data."""
customer = CustomerRegister(
email="customer@example.com",
password="Password123",
first_name="John",
last_name="Doe",
)
assert customer.email == "customer@example.com"
assert customer.first_name == "John"
assert customer.last_name == "Doe"
def test_email_normalized_to_lowercase(self):
"""Test email is normalized to lowercase."""
customer = CustomerRegister(
email="Customer@Example.COM",
password="Password123",
first_name="John",
last_name="Doe",
)
assert customer.email == "customer@example.com"
def test_invalid_email(self):
"""Test invalid email raises ValidationError."""
with pytest.raises(ValidationError) as exc_info:
CustomerRegister(
email="not-an-email",
password="Password123",
first_name="John",
last_name="Doe",
)
assert "email" in str(exc_info.value).lower()
def test_password_min_length(self):
"""Test password must be at least 8 characters."""
with pytest.raises(ValidationError) as exc_info:
CustomerRegister(
email="customer@example.com",
password="Pass1",
first_name="John",
last_name="Doe",
)
assert "password" in str(exc_info.value).lower()
def test_password_requires_digit(self):
"""Test password must contain at least one digit."""
with pytest.raises(ValidationError) as exc_info:
CustomerRegister(
email="customer@example.com",
password="Password",
first_name="John",
last_name="Doe",
)
assert "digit" in str(exc_info.value).lower()
def test_password_requires_letter(self):
"""Test password must contain at least one letter."""
with pytest.raises(ValidationError) as exc_info:
CustomerRegister(
email="customer@example.com",
password="12345678",
first_name="John",
last_name="Doe",
)
assert "letter" in str(exc_info.value).lower()
def test_first_name_required(self):
"""Test first_name is required."""
with pytest.raises(ValidationError) as exc_info:
CustomerRegister(
email="customer@example.com",
password="Password123",
last_name="Doe",
)
assert "first_name" in str(exc_info.value).lower()
def test_marketing_consent_default(self):
"""Test marketing_consent defaults to False."""
customer = CustomerRegister(
email="customer@example.com",
password="Password123",
first_name="John",
last_name="Doe",
)
assert customer.marketing_consent is False
def test_optional_phone(self):
"""Test optional phone field."""
customer = CustomerRegister(
email="customer@example.com",
password="Password123",
first_name="John",
last_name="Doe",
phone="+352 123 456",
)
assert customer.phone == "+352 123 456"
@pytest.mark.unit
@pytest.mark.schema
class TestCustomerUpdateSchema:
"""Test CustomerUpdate schema validation."""
def test_partial_update(self):
"""Test partial update with only some fields."""
update = CustomerUpdate(first_name="Jane")
assert update.first_name == "Jane"
assert update.last_name is None
assert update.email is None
def test_empty_update_is_valid(self):
"""Test empty update is valid."""
update = CustomerUpdate()
assert update.model_dump(exclude_unset=True) == {}
def test_email_normalized_to_lowercase(self):
"""Test email is normalized to lowercase."""
update = CustomerUpdate(email="NewEmail@Example.COM")
assert update.email == "newemail@example.com"
@pytest.mark.unit
@pytest.mark.schema
class TestCustomerResponseSchema:
"""Test CustomerResponse schema."""
def test_from_dict(self):
"""Test creating response from dict."""
from datetime import datetime
from decimal import Decimal
data = {
"id": 1,
"store_id": 1,
"email": "customer@example.com",
"first_name": "John",
"last_name": "Doe",
"phone": None,
"customer_number": "CUST001",
"marketing_consent": False,
"preferred_language": "fr",
"last_order_date": None,
"total_orders": 5,
"total_spent": Decimal("500.00"),
"is_active": True,
"created_at": datetime.now(),
"updated_at": datetime.now(),
}
response = CustomerResponse(**data)
assert response.id == 1
assert response.customer_number == "CUST001"
assert response.total_orders == 5
@pytest.mark.unit
@pytest.mark.schema
class TestCustomerAddressCreateSchema:
"""Test CustomerAddressCreate schema validation."""
def test_valid_shipping_address(self):
"""Test valid shipping address creation."""
address = CustomerAddressCreate(
address_type="shipping",
first_name="John",
last_name="Doe",
address_line_1="123 Main St",
city="Luxembourg",
postal_code="L-1234",
country_name="Luxembourg",
country_iso="LU",
)
assert address.address_type == "shipping"
assert address.city == "Luxembourg"
def test_valid_billing_address(self):
"""Test valid billing address creation."""
address = CustomerAddressCreate(
address_type="billing",
first_name="John",
last_name="Doe",
address_line_1="123 Main St",
city="Luxembourg",
postal_code="L-1234",
country_name="Luxembourg",
country_iso="LU",
)
assert address.address_type == "billing"
def test_invalid_address_type(self):
"""Test invalid address_type raises ValidationError."""
with pytest.raises(ValidationError) as exc_info:
CustomerAddressCreate(
address_type="delivery",
first_name="John",
last_name="Doe",
address_line_1="123 Main St",
city="Luxembourg",
postal_code="L-1234",
country_name="Luxembourg",
country_iso="LU",
)
assert "address_type" in str(exc_info.value).lower()
def test_is_default_defaults_to_false(self):
"""Test is_default defaults to False."""
address = CustomerAddressCreate(
address_type="shipping",
first_name="John",
last_name="Doe",
address_line_1="123 Main St",
city="Luxembourg",
postal_code="L-1234",
country_name="Luxembourg",
country_iso="LU",
)
assert address.is_default is False
def test_optional_address_line_2(self):
"""Test optional address_line_2 field."""
address = CustomerAddressCreate(
address_type="shipping",
first_name="John",
last_name="Doe",
address_line_1="123 Main St",
address_line_2="Apt 4B",
city="Luxembourg",
postal_code="L-1234",
country_name="Luxembourg",
country_iso="LU",
)
assert address.address_line_2 == "Apt 4B"
def test_required_fields(self):
"""Test required fields."""
with pytest.raises(ValidationError):
CustomerAddressCreate(
address_type="shipping",
first_name="John",
# missing last_name, address_line_1, city, postal_code, country
)
@pytest.mark.unit
@pytest.mark.schema
class TestCustomerAddressUpdateSchema:
"""Test CustomerAddressUpdate schema validation."""
def test_partial_update(self):
"""Test partial update with only some fields."""
update = CustomerAddressUpdate(city="Esch-sur-Alzette")
assert update.city == "Esch-sur-Alzette"
assert update.first_name is None
def test_empty_update_is_valid(self):
"""Test empty update is valid."""
update = CustomerAddressUpdate()
assert update.model_dump(exclude_unset=True) == {}
def test_address_type_validation(self):
"""Test address_type validation in update."""
with pytest.raises(ValidationError):
CustomerAddressUpdate(address_type="invalid")
def test_valid_address_type_update(self):
"""Test valid address_type values in update."""
shipping = CustomerAddressUpdate(address_type="shipping")
billing = CustomerAddressUpdate(address_type="billing")
assert shipping.address_type == "shipping"
assert billing.address_type == "billing"
@pytest.mark.unit
@pytest.mark.schema
class TestCustomerAddressResponseSchema:
"""Test CustomerAddressResponse schema."""
def test_from_dict(self):
"""Test creating response from dict."""
from datetime import datetime
data = {
"id": 1,
"store_id": 1,
"customer_id": 1,
"address_type": "shipping",
"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_name": "Luxembourg",
"country_iso": "LU",
"is_default": True,
"created_at": datetime.now(),
"updated_at": datetime.now(),
}
response = CustomerAddressResponse(**data)
assert response.id == 1
assert response.is_default is True
assert response.address_type == "shipping"
@pytest.mark.unit
@pytest.mark.schema
class TestCustomerPreferencesUpdateSchema:
"""Test CustomerPreferencesUpdate schema validation."""
def test_partial_update(self):
"""Test partial update with only some fields."""
update = CustomerPreferencesUpdate(marketing_consent=True)
assert update.marketing_consent is True
assert update.preferred_language is None
def test_language_update(self):
"""Test language preference update."""
update = CustomerPreferencesUpdate(preferred_language="fr")
assert update.preferred_language == "fr"
def test_currency_update(self):
"""Test currency preference update."""
update = CustomerPreferencesUpdate(currency="EUR")
assert update.currency == "EUR"
def test_notification_preferences(self):
"""Test notification preferences dict."""
update = CustomerPreferencesUpdate(
notification_preferences={
"email": True,
"sms": False,
"push": True,
}
)
assert update.notification_preferences["email"] is True
assert update.notification_preferences["sms"] is False

View File

@@ -1,299 +0,0 @@
# tests/unit/models/schema/test_inventory.py
"""Unit tests for inventory Pydantic schemas."""
import pytest
from pydantic import ValidationError
from app.modules.inventory.schemas import (
InventoryAdjust,
InventoryCreate,
InventoryLocationResponse,
InventoryReserve,
InventoryResponse,
InventoryUpdate,
ProductInventorySummary,
)
@pytest.mark.unit
@pytest.mark.schema
class TestInventoryCreateSchema:
"""Test InventoryCreate schema validation."""
def test_valid_inventory_create(self):
"""Test valid inventory creation data."""
inventory = InventoryCreate(
product_id=1,
location="Warehouse A",
quantity=100,
)
assert inventory.product_id == 1
assert inventory.location == "Warehouse A"
assert inventory.quantity == 100
def test_product_id_required(self):
"""Test product_id is required."""
with pytest.raises(ValidationError) as exc_info:
InventoryCreate(location="Warehouse A", quantity=10)
assert "product_id" in str(exc_info.value).lower()
def test_location_required(self):
"""Test location is required."""
with pytest.raises(ValidationError) as exc_info:
InventoryCreate(product_id=1, quantity=10)
assert "location" in str(exc_info.value).lower()
def test_quantity_required(self):
"""Test quantity is required."""
with pytest.raises(ValidationError) as exc_info:
InventoryCreate(product_id=1, location="Warehouse A")
assert "quantity" in str(exc_info.value).lower()
def test_quantity_must_be_non_negative(self):
"""Test quantity must be >= 0."""
with pytest.raises(ValidationError) as exc_info:
InventoryCreate(
product_id=1,
location="Warehouse A",
quantity=-5,
)
assert "quantity" in str(exc_info.value).lower()
def test_quantity_zero_is_valid(self):
"""Test quantity of 0 is valid."""
inventory = InventoryCreate(
product_id=1,
location="Warehouse A",
quantity=0,
)
assert inventory.quantity == 0
@pytest.mark.unit
@pytest.mark.schema
class TestInventoryAdjustSchema:
"""Test InventoryAdjust schema validation."""
def test_positive_adjustment(self):
"""Test positive quantity adjustment (add stock)."""
adjustment = InventoryAdjust(
product_id=1,
location="Warehouse A",
quantity=50,
)
assert adjustment.quantity == 50
def test_negative_adjustment(self):
"""Test negative quantity adjustment (remove stock)."""
adjustment = InventoryAdjust(
product_id=1,
location="Warehouse A",
quantity=-20,
)
assert adjustment.quantity == -20
def test_zero_adjustment_is_valid(self):
"""Test zero adjustment is technically valid."""
adjustment = InventoryAdjust(
product_id=1,
location="Warehouse A",
quantity=0,
)
assert adjustment.quantity == 0
@pytest.mark.unit
@pytest.mark.schema
class TestInventoryUpdateSchema:
"""Test InventoryUpdate schema validation."""
def test_partial_update(self):
"""Test partial update with only some fields."""
update = InventoryUpdate(quantity=150)
assert update.quantity == 150
assert update.reserved_quantity is None
assert update.location is None
def test_empty_update_is_valid(self):
"""Test empty update is valid."""
update = InventoryUpdate()
assert update.model_dump(exclude_unset=True) == {}
def test_quantity_must_be_non_negative(self):
"""Test quantity must be >= 0 in update."""
with pytest.raises(ValidationError):
InventoryUpdate(quantity=-10)
def test_reserved_quantity_must_be_non_negative(self):
"""Test reserved_quantity must be >= 0."""
with pytest.raises(ValidationError):
InventoryUpdate(reserved_quantity=-5)
def test_location_update(self):
"""Test location can be updated."""
update = InventoryUpdate(location="Warehouse B")
assert update.location == "Warehouse B"
@pytest.mark.unit
@pytest.mark.schema
class TestInventoryReserveSchema:
"""Test InventoryReserve schema validation."""
def test_valid_reservation(self):
"""Test valid inventory reservation."""
reservation = InventoryReserve(
product_id=1,
location="Warehouse A",
quantity=10,
)
assert reservation.product_id == 1
assert reservation.quantity == 10
def test_quantity_must_be_positive(self):
"""Test reservation quantity must be > 0."""
with pytest.raises(ValidationError) as exc_info:
InventoryReserve(
product_id=1,
location="Warehouse A",
quantity=0,
)
assert "quantity" in str(exc_info.value).lower()
def test_negative_quantity_invalid(self):
"""Test negative reservation quantity is invalid."""
with pytest.raises(ValidationError):
InventoryReserve(
product_id=1,
location="Warehouse A",
quantity=-5,
)
@pytest.mark.unit
@pytest.mark.schema
class TestInventoryResponseSchema:
"""Test InventoryResponse schema."""
def test_from_dict(self):
"""Test creating response from dict."""
from datetime import datetime
data = {
"id": 1,
"product_id": 1,
"store_id": 1,
"location": "Warehouse A",
"quantity": 100,
"reserved_quantity": 20,
"gtin": "1234567890123",
"created_at": datetime.now(),
"updated_at": datetime.now(),
}
response = InventoryResponse(**data)
assert response.id == 1
assert response.quantity == 100
assert response.reserved_quantity == 20
def test_available_quantity_property(self):
"""Test available_quantity calculated property."""
from datetime import datetime
data = {
"id": 1,
"product_id": 1,
"store_id": 1,
"location": "Warehouse A",
"quantity": 100,
"reserved_quantity": 30,
"gtin": None,
"created_at": datetime.now(),
"updated_at": datetime.now(),
}
response = InventoryResponse(**data)
assert response.available_quantity == 70
def test_available_quantity_never_negative(self):
"""Test available_quantity is never negative."""
from datetime import datetime
data = {
"id": 1,
"product_id": 1,
"store_id": 1,
"location": "Warehouse A",
"quantity": 10,
"reserved_quantity": 50, # Over-reserved
"gtin": None,
"created_at": datetime.now(),
"updated_at": datetime.now(),
}
response = InventoryResponse(**data)
assert response.available_quantity == 0
@pytest.mark.unit
@pytest.mark.schema
class TestInventoryLocationResponseSchema:
"""Test InventoryLocationResponse schema."""
def test_valid_location_response(self):
"""Test valid location response."""
location = InventoryLocationResponse(
location="Warehouse A",
quantity=100,
reserved_quantity=20,
available_quantity=80,
)
assert location.location == "Warehouse A"
assert location.quantity == 100
assert location.available_quantity == 80
@pytest.mark.unit
@pytest.mark.schema
class TestProductInventorySummarySchema:
"""Test ProductInventorySummary schema."""
def test_valid_summary(self):
"""Test valid inventory summary."""
summary = ProductInventorySummary(
product_id=1,
store_id=1,
product_sku="SKU-001",
product_title="Test Product",
total_quantity=200,
total_reserved=50,
total_available=150,
locations=[
InventoryLocationResponse(
location="Warehouse A",
quantity=100,
reserved_quantity=25,
available_quantity=75,
),
InventoryLocationResponse(
location="Warehouse B",
quantity=100,
reserved_quantity=25,
available_quantity=75,
),
],
)
assert summary.product_id == 1
assert summary.total_quantity == 200
assert len(summary.locations) == 2
def test_empty_locations(self):
"""Test summary with no locations."""
summary = ProductInventorySummary(
product_id=1,
store_id=1,
product_sku=None,
product_title="Test Product",
total_quantity=0,
total_reserved=0,
total_available=0,
locations=[],
)
assert summary.locations == []

View File

@@ -1,240 +0,0 @@
# tests/unit/models/schema/test_marketplace_import_job.py
"""Unit tests for marketplace import job Pydantic schemas."""
import pytest
from pydantic import ValidationError
from app.modules.marketplace.schemas import (
MarketplaceImportJobListResponse,
MarketplaceImportJobRequest,
MarketplaceImportJobResponse,
MarketplaceImportJobStatusUpdate,
)
@pytest.mark.unit
@pytest.mark.schema
class TestMarketplaceImportJobRequestSchema:
"""Test MarketplaceImportJobRequest schema validation."""
def test_valid_request(self):
"""Test valid import job request."""
request = MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
marketplace="Letzshop",
)
assert request.source_url == "https://example.com/products.csv"
assert request.marketplace == "Letzshop"
def test_source_url_required(self):
"""Test source_url is required."""
with pytest.raises(ValidationError) as exc_info:
MarketplaceImportJobRequest(marketplace="Letzshop")
assert "source_url" in str(exc_info.value).lower()
def test_source_url_must_be_http_or_https(self):
"""Test source_url must start with http:// or https://."""
with pytest.raises(ValidationError) as exc_info:
MarketplaceImportJobRequest(
source_url="ftp://example.com/products.csv",
)
assert "url" in str(exc_info.value).lower()
def test_source_url_http_is_valid(self):
"""Test http:// URLs are valid."""
request = MarketplaceImportJobRequest(
source_url="http://example.com/products.csv",
)
assert request.source_url == "http://example.com/products.csv"
def test_source_url_with_leading_whitespace_invalid(self):
"""Test source_url with leading whitespace is invalid (validation before strip)."""
with pytest.raises(ValidationError) as exc_info:
MarketplaceImportJobRequest(
source_url=" https://example.com/products.csv",
)
assert "url" in str(exc_info.value).lower()
def test_marketplace_default(self):
"""Test marketplace defaults to Letzshop."""
request = MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
)
assert request.marketplace == "Letzshop"
def test_marketplace_stripped(self):
"""Test marketplace is stripped of whitespace."""
request = MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
marketplace=" CustomMarket ",
)
assert request.marketplace == "CustomMarket"
def test_batch_size_default(self):
"""Test batch_size defaults to 1000."""
request = MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
)
assert request.batch_size == 1000
def test_batch_size_minimum(self):
"""Test batch_size must be >= 100."""
with pytest.raises(ValidationError) as exc_info:
MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
batch_size=50,
)
assert "batch_size" in str(exc_info.value).lower()
def test_batch_size_maximum(self):
"""Test batch_size must be <= 10000."""
with pytest.raises(ValidationError) as exc_info:
MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
batch_size=15000,
)
assert "batch_size" in str(exc_info.value).lower()
def test_batch_size_valid_range(self):
"""Test batch_size in valid range."""
request = MarketplaceImportJobRequest(
source_url="https://example.com/products.csv",
batch_size=5000,
)
assert request.batch_size == 5000
@pytest.mark.unit
@pytest.mark.schema
class TestMarketplaceImportJobResponseSchema:
"""Test MarketplaceImportJobResponse schema."""
def test_from_dict(self):
"""Test creating response from dict."""
from datetime import datetime
data = {
"job_id": 1,
"store_id": 1,
"store_code": "TEST_STORE",
"store_name": "Test Store",
"marketplace": "Letzshop",
"source_url": "https://example.com/products.csv",
"status": "pending",
"created_at": datetime.now(),
}
response = MarketplaceImportJobResponse(**data)
assert response.job_id == 1
assert response.store_code == "TEST_STORE"
assert response.status == "pending"
def test_default_counts(self):
"""Test count fields default to 0."""
from datetime import datetime
data = {
"job_id": 1,
"store_id": 1,
"store_code": "TEST_STORE",
"store_name": "Test Store",
"marketplace": "Letzshop",
"source_url": "https://example.com/products.csv",
"status": "completed",
"created_at": datetime.now(),
}
response = MarketplaceImportJobResponse(**data)
assert response.imported == 0
assert response.updated == 0
assert response.total_processed == 0
assert response.error_count == 0
def test_optional_timestamps(self):
"""Test optional timestamp fields."""
from datetime import datetime
now = datetime.now()
data = {
"job_id": 1,
"store_id": 1,
"store_code": "TEST_STORE",
"store_name": "Test Store",
"marketplace": "Letzshop",
"source_url": "https://example.com/products.csv",
"status": "completed",
"created_at": now,
"started_at": now,
"completed_at": now,
}
response = MarketplaceImportJobResponse(**data)
assert response.started_at == now
assert response.completed_at == now
def test_error_message_optional(self):
"""Test error_message is optional."""
from datetime import datetime
data = {
"job_id": 1,
"store_id": 1,
"store_code": "TEST_STORE",
"store_name": "Test Store",
"marketplace": "Letzshop",
"source_url": "https://example.com/products.csv",
"status": "failed",
"created_at": datetime.now(),
"error_message": "Connection timeout",
}
response = MarketplaceImportJobResponse(**data)
assert response.error_message == "Connection timeout"
@pytest.mark.unit
@pytest.mark.schema
class TestMarketplaceImportJobListResponseSchema:
"""Test MarketplaceImportJobListResponse schema."""
def test_valid_list_response(self):
"""Test valid list response structure."""
response = MarketplaceImportJobListResponse(
jobs=[],
total=0,
skip=0,
limit=10,
)
assert response.jobs == []
assert response.total == 0
@pytest.mark.unit
@pytest.mark.schema
class TestMarketplaceImportJobStatusUpdateSchema:
"""Test MarketplaceImportJobStatusUpdate schema."""
def test_status_only_update(self):
"""Test update with only status."""
update = MarketplaceImportJobStatusUpdate(status="processing")
assert update.status == "processing"
assert update.imported_count is None
def test_full_update(self):
"""Test update with all fields."""
update = MarketplaceImportJobStatusUpdate(
status="completed",
imported_count=100,
updated_count=50,
error_count=2,
total_processed=152,
)
assert update.imported_count == 100
assert update.updated_count == 50
assert update.error_count == 2
assert update.total_processed == 152
def test_error_update(self):
"""Test update with error message."""
update = MarketplaceImportJobStatusUpdate(
status="failed",
error_message="File not found",
)
assert update.status == "failed"
assert update.error_message == "File not found"

View File

@@ -1,578 +0,0 @@
# 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_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 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,
"store_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, store_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, store_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

View File

@@ -1,245 +0,0 @@
# tests/unit/models/schema/test_product.py
"""Unit tests for product Pydantic schemas."""
import pytest
from pydantic import ValidationError
from app.modules.catalog.schemas import (
ProductCreate,
ProductListResponse,
ProductResponse,
ProductUpdate,
)
@pytest.mark.unit
@pytest.mark.schema
class TestProductCreateSchema:
"""Test ProductCreate schema validation."""
def test_valid_product_create(self):
"""Test valid product creation data."""
product = ProductCreate(
marketplace_product_id=1,
store_sku="SKU-001",
price=99.99,
currency="EUR",
)
assert product.marketplace_product_id == 1
assert product.store_sku == "SKU-001"
assert product.price == 99.99
def test_marketplace_product_id_required(self):
"""Test marketplace_product_id is required."""
with pytest.raises(ValidationError) as exc_info:
ProductCreate(price=99.99)
assert "marketplace_product_id" in str(exc_info.value).lower()
def test_price_must_be_non_negative(self):
"""Test price must be >= 0."""
with pytest.raises(ValidationError) as exc_info:
ProductCreate(
marketplace_product_id=1,
price=-10.00,
)
assert "price" in str(exc_info.value).lower()
def test_price_zero_is_valid(self):
"""Test price of 0 is valid."""
product = ProductCreate(
marketplace_product_id=1,
price=0,
)
assert product.price == 0
def test_sale_price_must_be_non_negative(self):
"""Test sale_price must be >= 0."""
with pytest.raises(ValidationError) as exc_info:
ProductCreate(
marketplace_product_id=1,
sale_price=-5.00,
)
assert "sale_price" in str(exc_info.value).lower()
def test_min_quantity_default(self):
"""Test min_quantity defaults to 1."""
product = ProductCreate(marketplace_product_id=1)
assert product.min_quantity == 1
def test_min_quantity_must_be_at_least_1(self):
"""Test min_quantity must be >= 1."""
with pytest.raises(ValidationError) as exc_info:
ProductCreate(
marketplace_product_id=1,
min_quantity=0,
)
assert "min_quantity" in str(exc_info.value).lower()
def test_max_quantity_must_be_at_least_1(self):
"""Test max_quantity must be >= 1 if provided."""
with pytest.raises(ValidationError) as exc_info:
ProductCreate(
marketplace_product_id=1,
max_quantity=0,
)
assert "max_quantity" in str(exc_info.value).lower()
def test_is_featured_default(self):
"""Test is_featured defaults to False."""
product = ProductCreate(marketplace_product_id=1)
assert product.is_featured is False
def test_all_optional_fields(self):
"""Test product with all optional fields."""
product = ProductCreate(
marketplace_product_id=1,
store_sku="SKU-001",
price=100.00,
sale_price=80.00,
currency="EUR",
availability="in_stock",
condition="new",
is_featured=True,
min_quantity=2,
max_quantity=10,
)
assert product.sale_price == 80.00
assert product.availability == "in_stock"
assert product.condition == "new"
assert product.is_featured is True
assert product.max_quantity == 10
@pytest.mark.unit
@pytest.mark.schema
class TestProductUpdateSchema:
"""Test ProductUpdate schema validation."""
def test_partial_update(self):
"""Test partial update with only some fields."""
update = ProductUpdate(price=150.00)
assert update.price == 150.00
assert update.store_sku is None
assert update.is_active is None
def test_empty_update_is_valid(self):
"""Test empty update is valid (all fields optional)."""
update = ProductUpdate()
assert update.model_dump(exclude_unset=True) == {}
def test_price_validation(self):
"""Test price must be >= 0 in update."""
with pytest.raises(ValidationError):
ProductUpdate(price=-10.00)
def test_is_active_update(self):
"""Test is_active can be updated."""
update = ProductUpdate(is_active=False)
assert update.is_active is False
def test_is_featured_update(self):
"""Test is_featured can be updated."""
update = ProductUpdate(is_featured=True)
assert update.is_featured is True
@pytest.mark.unit
@pytest.mark.schema
class TestProductResponseSchema:
"""Test ProductResponse schema."""
def test_from_dict(self):
"""Test creating response from dict."""
from datetime import datetime
data = {
"id": 1,
"store_id": 1,
"marketplace_product": {
"id": 1,
"marketplace_product_id": "TEST001", # Required field
"gtin": "1234567890123",
"title": "Test Product",
"description": "A test product",
"brand": "Test Brand",
"google_product_category": "Electronics",
"image_link": "https://example.com/image.jpg",
"created_at": datetime.now(),
"updated_at": datetime.now(),
},
"store_sku": "SKU-001",
"price": 99.99,
"sale_price": None,
"currency": "EUR",
"availability": "in_stock",
"condition": "new",
"is_featured": False,
"is_active": True,
"display_order": 0,
"min_quantity": 1,
"max_quantity": None,
"created_at": datetime.now(),
"updated_at": datetime.now(),
}
response = ProductResponse(**data)
assert response.id == 1
assert response.store_id == 1
assert response.is_active is True
def test_optional_inventory_fields(self):
"""Test optional inventory summary fields."""
from datetime import datetime
data = {
"id": 1,
"store_id": 1,
"marketplace_product": {
"id": 1,
"marketplace_product_id": "TEST002", # Required field
"gtin": "1234567890123",
"title": "Test Product",
"description": None,
"brand": None,
"google_product_category": None,
"image_link": None,
"created_at": datetime.now(),
"updated_at": datetime.now(),
},
"store_sku": None,
"price": None,
"sale_price": None,
"currency": None,
"availability": None,
"condition": None,
"is_featured": False,
"is_active": True,
"display_order": 0,
"min_quantity": 1,
"max_quantity": None,
"created_at": datetime.now(),
"updated_at": datetime.now(),
"total_inventory": 100,
"available_inventory": 80,
}
response = ProductResponse(**data)
assert response.total_inventory == 100
assert response.available_inventory == 80
@pytest.mark.unit
@pytest.mark.schema
class TestProductListResponseSchema:
"""Test ProductListResponse schema."""
def test_valid_list_response(self):
"""Test valid list response structure."""
response = ProductListResponse(
products=[],
total=0,
skip=0,
limit=10,
)
assert response.products == []
assert response.total == 0
assert response.skip == 0
assert response.limit == 10

View File

@@ -1,316 +0,0 @@
# tests/unit/models/schema/test_store.py
"""Unit tests for store Pydantic schemas."""
import pytest
from pydantic import ValidationError
from app.modules.tenancy.schemas.store import (
StoreCreate,
StoreDetailResponse,
StoreListResponse,
StoreResponse,
StoreSummary,
StoreUpdate,
)
@pytest.mark.unit
@pytest.mark.schema
class TestStoreCreateSchema:
"""Test StoreCreate schema validation."""
def test_valid_store_create(self):
"""Test valid store creation data."""
store = StoreCreate(
merchant_id=1,
store_code="TECHSTORE",
subdomain="techstore",
name="Tech Store",
)
assert store.merchant_id == 1
assert store.store_code == "TECHSTORE"
assert store.subdomain == "techstore"
assert store.name == "Tech Store"
def test_merchant_id_required(self):
"""Test merchant_id is required."""
with pytest.raises(ValidationError) as exc_info:
StoreCreate(
store_code="TECHSTORE",
subdomain="techstore",
name="Tech Store",
)
assert "merchant_id" in str(exc_info.value).lower()
def test_merchant_id_must_be_positive(self):
"""Test merchant_id must be > 0."""
with pytest.raises(ValidationError) as exc_info:
StoreCreate(
merchant_id=0,
store_code="TECHSTORE",
subdomain="techstore",
name="Tech Store",
)
assert "merchant_id" in str(exc_info.value).lower()
def test_store_code_required(self):
"""Test store_code is required."""
with pytest.raises(ValidationError) as exc_info:
StoreCreate(
merchant_id=1,
subdomain="techstore",
name="Tech Store",
)
assert "store_code" in str(exc_info.value).lower()
def test_store_code_uppercase_normalized(self):
"""Test store_code is normalized to uppercase."""
store = StoreCreate(
merchant_id=1,
store_code="techstore",
subdomain="techstore",
name="Tech Store",
)
assert store.store_code == "TECHSTORE"
def test_store_code_min_length(self):
"""Test store_code minimum length (2)."""
with pytest.raises(ValidationError) as exc_info:
StoreCreate(
merchant_id=1,
store_code="T",
subdomain="techstore",
name="Tech Store",
)
assert "store_code" in str(exc_info.value).lower()
def test_subdomain_required(self):
"""Test subdomain is required."""
with pytest.raises(ValidationError) as exc_info:
StoreCreate(
merchant_id=1,
store_code="TECHSTORE",
name="Tech Store",
)
assert "subdomain" in str(exc_info.value).lower()
def test_subdomain_uppercase_invalid(self):
"""Test subdomain with uppercase is invalid (validated before normalization)."""
with pytest.raises(ValidationError) as exc_info:
StoreCreate(
merchant_id=1,
store_code="TECHSTORE",
subdomain="TechStore",
name="Tech Store",
)
assert "subdomain" in str(exc_info.value).lower()
def test_subdomain_valid_format(self):
"""Test subdomain with valid format."""
store = StoreCreate(
merchant_id=1,
store_code="TECHSTORE",
subdomain="tech-store-123",
name="Tech Store",
)
assert store.subdomain == "tech-store-123"
def test_subdomain_invalid_format(self):
"""Test subdomain with invalid characters raises ValidationError."""
with pytest.raises(ValidationError) as exc_info:
StoreCreate(
merchant_id=1,
store_code="TECHSTORE",
subdomain="tech_store!",
name="Tech Store",
)
assert "subdomain" in str(exc_info.value).lower()
def test_name_required(self):
"""Test name is required."""
with pytest.raises(ValidationError) as exc_info:
StoreCreate(
merchant_id=1,
store_code="TECHSTORE",
subdomain="techstore",
)
assert "name" in str(exc_info.value).lower()
def test_name_min_length(self):
"""Test name minimum length (2)."""
with pytest.raises(ValidationError) as exc_info:
StoreCreate(
merchant_id=1,
store_code="TECHSTORE",
subdomain="techstore",
name="T",
)
assert "name" in str(exc_info.value).lower()
def test_optional_fields(self):
"""Test optional fields."""
store = StoreCreate(
merchant_id=1,
store_code="TECHSTORE",
subdomain="techstore",
name="Tech Store",
description="Best tech store",
letzshop_csv_url_fr="https://example.com/fr.csv",
contact_email="contact@techstore.com",
website="https://techstore.com",
)
assert store.description == "Best tech store"
assert store.letzshop_csv_url_fr == "https://example.com/fr.csv"
assert store.contact_email == "contact@techstore.com"
@pytest.mark.unit
@pytest.mark.schema
class TestStoreUpdateSchema:
"""Test StoreUpdate schema validation."""
def test_partial_update(self):
"""Test partial update with only some fields."""
update = StoreUpdate(name="New Tech Store")
assert update.name == "New Tech Store"
assert update.subdomain is None
assert update.is_active is None
def test_empty_update_is_valid(self):
"""Test empty update is valid."""
update = StoreUpdate()
assert update.model_dump(exclude_unset=True) == {}
def test_subdomain_normalized_to_lowercase(self):
"""Test subdomain is normalized to lowercase."""
update = StoreUpdate(subdomain="NewSubdomain")
assert update.subdomain == "newsubdomain"
def test_subdomain_stripped(self):
"""Test subdomain is stripped of whitespace."""
update = StoreUpdate(subdomain=" newsubdomain ")
assert update.subdomain == "newsubdomain"
def test_name_min_length(self):
"""Test name minimum length (2)."""
with pytest.raises(ValidationError):
StoreUpdate(name="X")
def test_is_active_update(self):
"""Test is_active can be updated."""
update = StoreUpdate(is_active=False)
assert update.is_active is False
def test_is_verified_update(self):
"""Test is_verified can be updated."""
update = StoreUpdate(is_verified=True)
assert update.is_verified is True
def test_reset_contact_to_merchant_flag(self):
"""Test reset_contact_to_merchant flag."""
update = StoreUpdate(reset_contact_to_merchant=True)
assert update.reset_contact_to_merchant is True
@pytest.mark.unit
@pytest.mark.schema
class TestStoreResponseSchema:
"""Test StoreResponse schema."""
def test_from_dict(self):
"""Test creating response from dict."""
from datetime import datetime
data = {
"id": 1,
"store_code": "TECHSTORE",
"subdomain": "techstore",
"name": "Tech Store",
"description": "Best tech store",
"merchant_id": 1,
"letzshop_csv_url_fr": None,
"letzshop_csv_url_en": None,
"letzshop_csv_url_de": None,
"is_active": True,
"is_verified": False,
"created_at": datetime.now(),
"updated_at": datetime.now(),
}
response = StoreResponse(**data)
assert response.id == 1
assert response.store_code == "TECHSTORE"
assert response.is_active is True
@pytest.mark.unit
@pytest.mark.schema
class TestStoreDetailResponseSchema:
"""Test StoreDetailResponse schema."""
def test_from_dict(self):
"""Test creating detail response from dict."""
from datetime import datetime
data = {
"id": 1,
"store_code": "TECHSTORE",
"subdomain": "techstore",
"name": "Tech Store",
"description": None,
"merchant_id": 1,
"letzshop_csv_url_fr": None,
"letzshop_csv_url_en": None,
"letzshop_csv_url_de": None,
"is_active": True,
"is_verified": True,
"created_at": datetime.now(),
"updated_at": datetime.now(),
# Additional detail fields
"merchant_name": "Tech Corp",
"owner_email": "owner@techcorp.com",
"owner_username": "owner",
"contact_email": "contact@techstore.com",
"contact_email_inherited": False,
}
response = StoreDetailResponse(**data)
assert response.merchant_name == "Tech Corp"
assert response.owner_email == "owner@techcorp.com"
assert response.contact_email_inherited is False
@pytest.mark.unit
@pytest.mark.schema
class TestStoreListResponseSchema:
"""Test StoreListResponse schema."""
def test_valid_list_response(self):
"""Test valid list response structure."""
response = StoreListResponse(
stores=[],
total=0,
skip=0,
limit=10,
)
assert response.stores == []
assert response.total == 0
@pytest.mark.unit
@pytest.mark.schema
class TestStoreSummarySchema:
"""Test StoreSummary schema."""
def test_from_dict(self):
"""Test creating summary from dict."""
data = {
"id": 1,
"store_code": "TECHSTORE",
"subdomain": "techstore",
"name": "Tech Store",
"merchant_id": 1,
"is_active": True,
}
summary = StoreSummary(**data)
assert summary.id == 1
assert summary.store_code == "TECHSTORE"
assert summary.is_active is True