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,271 +0,0 @@
# tests/unit/models/database/test_admin_platform.py
"""
Unit tests for AdminPlatform model.
Tests the admin-platform junction table model and its relationships.
"""
import pytest
from sqlalchemy.exc import IntegrityError
from app.modules.tenancy.models import AdminPlatform
@pytest.mark.unit
@pytest.mark.database
@pytest.mark.admin
class TestAdminPlatformModel:
"""Test AdminPlatform model creation and constraints."""
def test_create_admin_platform_assignment(
self, db, test_platform_admin, test_platform, test_super_admin
):
"""Test creating an admin platform assignment."""
assignment = AdminPlatform(
user_id=test_platform_admin.id,
platform_id=test_platform.id,
is_active=True,
assigned_by_user_id=test_super_admin.id,
)
db.add(assignment)
db.commit()
db.refresh(assignment)
assert assignment.id is not None
assert assignment.user_id == test_platform_admin.id
assert assignment.platform_id == test_platform.id
assert assignment.is_active is True
assert assignment.assigned_by_user_id == test_super_admin.id
assert assignment.assigned_at is not None
def test_admin_platform_unique_constraint(
self, db, test_platform_admin, test_platform, test_super_admin
):
"""Test that an admin can only be assigned to a platform once."""
# Create first assignment
assignment1 = AdminPlatform(
user_id=test_platform_admin.id,
platform_id=test_platform.id,
is_active=True,
assigned_by_user_id=test_super_admin.id,
)
db.add(assignment1)
db.commit()
# Try to create duplicate assignment
assignment2 = AdminPlatform(
user_id=test_platform_admin.id,
platform_id=test_platform.id,
is_active=True,
assigned_by_user_id=test_super_admin.id,
)
db.add(assignment2)
with pytest.raises(IntegrityError):
db.commit()
def test_admin_platform_cascade_delete_user(
self, db, auth_manager, test_platform, test_super_admin
):
"""Test that deleting user cascades to admin platform assignments."""
from app.modules.tenancy.models import User
# Create a temporary admin
temp_admin = User(
email="temp_admin@example.com",
username="temp_admin",
hashed_password=auth_manager.hash_password("temppass"),
role="admin",
is_active=True,
is_super_admin=False,
)
db.add(temp_admin)
db.flush()
# Create assignment
assignment = AdminPlatform(
user_id=temp_admin.id,
platform_id=test_platform.id,
is_active=True,
assigned_by_user_id=test_super_admin.id,
)
db.add(assignment)
db.commit()
assignment_id = assignment.id
# Delete user - should cascade to assignment
db.delete(temp_admin)
db.commit()
# Verify assignment is gone
remaining = db.query(AdminPlatform).filter(AdminPlatform.id == assignment_id).first()
assert remaining is None
def test_admin_platform_relationships(
self, db, test_platform_admin, test_platform, test_super_admin
):
"""Test AdminPlatform relationships are loaded correctly."""
assignment = AdminPlatform(
user_id=test_platform_admin.id,
platform_id=test_platform.id,
is_active=True,
assigned_by_user_id=test_super_admin.id,
)
db.add(assignment)
db.commit()
db.refresh(assignment)
# Test relationships
assert assignment.user is not None
assert assignment.user.id == test_platform_admin.id
assert assignment.platform is not None
assert assignment.platform.id == test_platform.id
assert assignment.assigned_by is not None
assert assignment.assigned_by.id == test_super_admin.id
def test_admin_platform_properties(
self, db, test_platform_admin, test_platform, test_super_admin
):
"""Test AdminPlatform computed properties."""
assignment = AdminPlatform(
user_id=test_platform_admin.id,
platform_id=test_platform.id,
is_active=True,
assigned_by_user_id=test_super_admin.id,
)
db.add(assignment)
db.commit()
db.refresh(assignment)
# Test properties
assert assignment.platform_code == test_platform.code
assert assignment.platform_name == test_platform.name
def test_admin_platform_repr(
self, db, test_platform_admin, test_platform, test_super_admin
):
"""Test AdminPlatform string representation."""
assignment = AdminPlatform(
user_id=test_platform_admin.id,
platform_id=test_platform.id,
is_active=True,
assigned_by_user_id=test_super_admin.id,
)
db.add(assignment)
db.commit()
db.refresh(assignment)
repr_str = repr(assignment)
assert "AdminPlatform" in repr_str
assert str(test_platform_admin.id) in repr_str
assert str(test_platform.id) in repr_str
@pytest.mark.unit
@pytest.mark.database
@pytest.mark.admin
class TestUserAdminMethods:
"""Test User model admin-related methods."""
def test_is_super_admin_user_true(self, db, test_super_admin):
"""Test is_super_admin_user property for super admin."""
assert test_super_admin.is_super_admin_user is True
def test_is_super_admin_user_false_for_platform_admin(self, db, test_platform_admin):
"""Test is_super_admin_user property for platform admin."""
assert test_platform_admin.is_super_admin_user is False
def test_is_platform_admin_true(self, db, test_platform_admin):
"""Test is_platform_admin property for platform admin."""
assert test_platform_admin.is_platform_admin is True
def test_is_platform_admin_false_for_super_admin(self, db, test_super_admin):
"""Test is_platform_admin property for super admin."""
assert test_super_admin.is_platform_admin is False
def test_can_access_platform_super_admin(self, db, test_super_admin, test_platform):
"""Test that super admin can access any platform."""
assert test_super_admin.can_access_platform(test_platform.id) is True
def test_can_access_platform_assigned(
self, db, test_platform_admin, test_platform, test_super_admin
):
"""Test that platform admin can access assigned platform."""
# Create assignment
assignment = AdminPlatform(
user_id=test_platform_admin.id,
platform_id=test_platform.id,
is_active=True,
assigned_by_user_id=test_super_admin.id,
)
db.add(assignment)
db.commit()
db.refresh(test_platform_admin)
assert test_platform_admin.can_access_platform(test_platform.id) is True
def test_can_access_platform_not_assigned(
self, db, test_platform_admin, test_platform
):
"""Test that platform admin cannot access unassigned platform."""
# No assignment created
assert test_platform_admin.can_access_platform(test_platform.id) is False
def test_can_access_platform_inactive_assignment(
self, db, test_platform_admin, test_platform, test_super_admin
):
"""Test that platform admin cannot access platform with inactive assignment."""
# Create inactive assignment
assignment = AdminPlatform(
user_id=test_platform_admin.id,
platform_id=test_platform.id,
is_active=False, # Inactive
assigned_by_user_id=test_super_admin.id,
)
db.add(assignment)
db.commit()
db.refresh(test_platform_admin)
assert test_platform_admin.can_access_platform(test_platform.id) is False
def test_get_accessible_platform_ids_super_admin(self, db, test_super_admin):
"""Test get_accessible_platform_ids returns None for super admin."""
result = test_super_admin.get_accessible_platform_ids()
assert result is None # None means all platforms
def test_get_accessible_platform_ids_platform_admin(
self, db, test_platform_admin, test_platform, another_platform, test_super_admin
):
"""Test get_accessible_platform_ids returns correct list for platform admin."""
# Create assignments for both platforms
assignment1 = AdminPlatform(
user_id=test_platform_admin.id,
platform_id=test_platform.id,
is_active=True,
assigned_by_user_id=test_super_admin.id,
)
assignment2 = AdminPlatform(
user_id=test_platform_admin.id,
platform_id=another_platform.id,
is_active=True,
assigned_by_user_id=test_super_admin.id,
)
db.add_all([assignment1, assignment2])
db.commit()
db.refresh(test_platform_admin)
result = test_platform_admin.get_accessible_platform_ids()
assert len(result) == 2
assert test_platform.id in result
assert another_platform.id in result
def test_get_accessible_platform_ids_no_assignments(self, db, test_platform_admin):
"""Test get_accessible_platform_ids returns empty list when no assignments."""
result = test_platform_admin.get_accessible_platform_ids()
assert result == []
def test_get_accessible_platform_ids_store_user(self, db, test_store_user):
"""Test get_accessible_platform_ids returns empty list for non-admin."""
result = test_store_user.get_accessible_platform_ids()
assert result == []

View File

@@ -1,247 +0,0 @@
# tests/unit/models/database/test_customer.py
"""Unit tests for Customer and CustomerAddress database models."""
import pytest
from app.modules.customers.models.customer import Customer, CustomerAddress
@pytest.mark.unit
@pytest.mark.database
class TestCustomerModel:
"""Test Customer model."""
def test_customer_creation(self, db, test_store):
"""Test Customer model with store isolation."""
customer = Customer(
store_id=test_store.id,
email="customer@example.com",
hashed_password="hashed_password",
first_name="John",
last_name="Doe",
customer_number="CUST001",
is_active=True,
)
db.add(customer)
db.commit()
db.refresh(customer)
assert customer.id is not None
assert customer.store_id == test_store.id
assert customer.email == "customer@example.com"
assert customer.customer_number == "CUST001"
assert customer.first_name == "John"
assert customer.last_name == "Doe"
assert customer.store.store_code == test_store.store_code
def test_customer_default_values(self, db, test_store):
"""Test Customer model default values."""
customer = Customer(
store_id=test_store.id,
email="defaults@example.com",
hashed_password="hash",
customer_number="CUST_DEFAULTS",
)
db.add(customer)
db.commit()
db.refresh(customer)
assert customer.is_active is True # Default
assert customer.marketing_consent is False # Default
assert customer.total_orders == 0 # Default
assert customer.total_spent == 0 # Default
def test_customer_full_name_property(self, db, test_store):
"""Test Customer full_name computed property."""
customer = Customer(
store_id=test_store.id,
email="fullname@example.com",
hashed_password="hash",
customer_number="CUST_FULLNAME",
first_name="Jane",
last_name="Smith",
)
db.add(customer)
db.commit()
db.refresh(customer)
assert customer.full_name == "Jane Smith"
def test_customer_full_name_fallback_to_email(self, db, test_store):
"""Test Customer full_name falls back to email when names not set."""
customer = Customer(
store_id=test_store.id,
email="noname@example.com",
hashed_password="hash",
customer_number="CUST_NONAME",
)
db.add(customer)
db.commit()
db.refresh(customer)
assert customer.full_name == "noname@example.com"
def test_customer_optional_fields(self, db, test_store):
"""Test Customer with optional fields."""
customer = Customer(
store_id=test_store.id,
email="optional@example.com",
hashed_password="hash",
customer_number="CUST_OPT",
phone="+352123456789",
preferences={"language": "en", "currency": "EUR"},
marketing_consent=True,
)
db.add(customer)
db.commit()
db.refresh(customer)
assert customer.phone == "+352123456789"
assert customer.preferences == {"language": "en", "currency": "EUR"}
assert customer.marketing_consent is True
def test_customer_store_relationship(self, db, test_store):
"""Test Customer-Store relationship."""
customer = Customer(
store_id=test_store.id,
email="relationship@example.com",
hashed_password="hash",
customer_number="CUST_REL",
)
db.add(customer)
db.commit()
db.refresh(customer)
assert customer.store is not None
assert customer.store.id == test_store.id
@pytest.mark.unit
@pytest.mark.database
class TestCustomerAddressModel:
"""Test CustomerAddress model."""
def test_customer_address_creation(self, db, test_store, test_customer):
"""Test CustomerAddress model."""
address = CustomerAddress(
store_id=test_store.id,
customer_id=test_customer.id,
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",
is_default=True,
)
db.add(address)
db.commit()
db.refresh(address)
assert address.id is not None
assert address.store_id == test_store.id
assert address.customer_id == test_customer.id
assert address.address_type == "shipping"
assert address.is_default is True
def test_customer_address_types(self, db, test_store, test_customer):
"""Test CustomerAddress with different address types."""
shipping_address = CustomerAddress(
store_id=test_store.id,
customer_id=test_customer.id,
address_type="shipping",
first_name="John",
last_name="Doe",
address_line_1="123 Shipping St",
city="Luxembourg",
postal_code="L-1234",
country_name="Luxembourg",
country_iso="LU",
)
db.add(shipping_address)
billing_address = CustomerAddress(
store_id=test_store.id,
customer_id=test_customer.id,
address_type="billing",
first_name="John",
last_name="Doe",
address_line_1="456 Billing Ave",
city="Luxembourg",
postal_code="L-5678",
country_name="Luxembourg",
country_iso="LU",
)
db.add(billing_address)
db.commit()
assert shipping_address.address_type == "shipping"
assert billing_address.address_type == "billing"
def test_customer_address_optional_fields(self, db, test_store, test_customer):
"""Test CustomerAddress with optional fields."""
address = CustomerAddress(
store_id=test_store.id,
customer_id=test_customer.id,
address_type="shipping",
first_name="John",
last_name="Doe",
company="ACME Corp",
address_line_1="123 Main St",
address_line_2="Suite 100",
city="Luxembourg",
postal_code="L-1234",
country_name="Luxembourg",
country_iso="LU",
)
db.add(address)
db.commit()
db.refresh(address)
assert address.company == "ACME Corp"
assert address.address_line_2 == "Suite 100"
def test_customer_address_default_values(self, db, test_store, test_customer):
"""Test CustomerAddress default values."""
address = CustomerAddress(
store_id=test_store.id,
customer_id=test_customer.id,
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",
)
db.add(address)
db.commit()
db.refresh(address)
assert address.is_default is False # Default
def test_customer_address_relationships(self, db, test_store, test_customer):
"""Test CustomerAddress relationships."""
address = CustomerAddress(
store_id=test_store.id,
customer_id=test_customer.id,
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",
)
db.add(address)
db.commit()
db.refresh(address)
assert address.customer is not None
assert address.customer.id == test_customer.id

View File

@@ -1,165 +0,0 @@
# tests/unit/models/database/test_inventory.py
"""Unit tests for Inventory database model."""
import pytest
from sqlalchemy.exc import IntegrityError
from app.modules.inventory.models import Inventory
@pytest.mark.unit
@pytest.mark.database
class TestInventoryModel:
"""Test Inventory model."""
def test_inventory_creation_with_product(self, db, test_store, test_product):
"""Test Inventory model linked to product."""
inventory = Inventory(
product_id=test_product.id,
store_id=test_store.id,
warehouse="strassen",
bin_location="SA-10-01",
location="WAREHOUSE_A",
quantity=150,
reserved_quantity=10,
gtin=test_product.marketplace_product.gtin,
)
db.add(inventory)
db.commit()
db.refresh(inventory)
assert inventory.id is not None
assert inventory.product_id == test_product.id
assert inventory.store_id == test_store.id
assert inventory.location == "WAREHOUSE_A"
assert inventory.bin_location == "SA-10-01"
assert inventory.quantity == 150
assert inventory.reserved_quantity == 10
assert inventory.available_quantity == 140 # 150 - 10
def test_inventory_unique_product_location(self, db, test_store, test_product):
"""Test unique constraint on product_id + warehouse + bin_location."""
inventory1 = Inventory(
product_id=test_product.id,
store_id=test_store.id,
warehouse="strassen",
bin_location="SA-10-01",
location="WAREHOUSE_A",
quantity=100,
)
db.add(inventory1)
db.commit()
# Same product + warehouse + bin_location should fail
with pytest.raises(IntegrityError):
inventory2 = Inventory(
product_id=test_product.id,
store_id=test_store.id,
warehouse="strassen",
bin_location="SA-10-01",
location="WAREHOUSE_A",
quantity=50,
)
db.add(inventory2)
db.commit()
def test_inventory_same_product_different_location(
self, db, test_store, test_product
):
"""Test same product can have inventory in different locations."""
inventory1 = Inventory(
product_id=test_product.id,
store_id=test_store.id,
warehouse="strassen",
bin_location="SA-10-01",
location="WAREHOUSE_A",
quantity=100,
)
db.add(inventory1)
db.commit()
# Same product in different bin_location should succeed
inventory2 = Inventory(
product_id=test_product.id,
store_id=test_store.id,
warehouse="strassen",
bin_location="SA-10-02",
location="WAREHOUSE_B",
quantity=50,
)
db.add(inventory2)
db.commit()
db.refresh(inventory2)
assert inventory2.id is not None
assert inventory2.bin_location == "SA-10-02"
def test_inventory_default_values(self, db, test_store, test_product):
"""Test Inventory model default values."""
inventory = Inventory(
product_id=test_product.id,
store_id=test_store.id,
warehouse="strassen",
bin_location="DEF-01-01",
location="DEFAULT_LOC",
quantity=100,
)
db.add(inventory)
db.commit()
db.refresh(inventory)
assert inventory.reserved_quantity == 0 # Default
assert inventory.available_quantity == 100 # quantity - reserved
def test_inventory_available_quantity_property(self, db, test_store, test_product):
"""Test available_quantity computed property."""
inventory = Inventory(
product_id=test_product.id,
store_id=test_store.id,
warehouse="strassen",
bin_location="PROP-01-01",
location="PROP_TEST",
quantity=200,
reserved_quantity=50,
)
db.add(inventory)
db.commit()
db.refresh(inventory)
assert inventory.available_quantity == 150 # 200 - 50
def test_inventory_relationships(self, db, test_store, test_product):
"""Test Inventory relationships."""
inventory = Inventory(
product_id=test_product.id,
store_id=test_store.id,
warehouse="strassen",
bin_location="REL-01-01",
location="REL_TEST",
quantity=100,
)
db.add(inventory)
db.commit()
db.refresh(inventory)
assert inventory.product is not None
assert inventory.store is not None
assert inventory.product.id == test_product.id
assert inventory.store.id == test_store.id
def test_inventory_without_gtin(self, db, test_store, test_product):
"""Test Inventory can be created without GTIN."""
inventory = Inventory(
product_id=test_product.id,
store_id=test_store.id,
warehouse="strassen",
bin_location="NOGTIN-01-01",
location="NO_GTIN",
quantity=100,
)
db.add(inventory)
db.commit()
db.refresh(inventory)
assert inventory.gtin is None

View File

@@ -1,136 +0,0 @@
# tests/unit/models/database/test_marketplace_import_job.py
"""Unit tests for MarketplaceImportJob database model."""
import pytest
from app.modules.marketplace.models import MarketplaceImportJob
@pytest.mark.unit
@pytest.mark.database
class TestMarketplaceImportJobModel:
"""Test MarketplaceImportJob model."""
def test_import_job_creation(self, db, test_user, test_store):
"""Test MarketplaceImportJob model with relationships."""
import_job = MarketplaceImportJob(
store_id=test_store.id,
user_id=test_user.id,
marketplace="Letzshop",
source_url="https://example.com/feed.csv",
status="pending",
imported_count=0,
updated_count=0,
error_count=0,
total_processed=0,
)
db.add(import_job)
db.commit()
db.refresh(import_job)
assert import_job.id is not None
assert import_job.store_id == test_store.id
assert import_job.user_id == test_user.id
assert import_job.marketplace == "Letzshop"
assert import_job.source_url == "https://example.com/feed.csv"
assert import_job.status == "pending"
assert import_job.store.store_code == test_store.store_code
assert import_job.user.username == test_user.username
def test_import_job_default_values(self, db, test_user, test_store):
"""Test MarketplaceImportJob default values."""
import_job = MarketplaceImportJob(
store_id=test_store.id,
user_id=test_user.id,
source_url="https://example.com/feed.csv",
)
db.add(import_job)
db.commit()
db.refresh(import_job)
assert import_job.marketplace == "Letzshop" # Default
assert import_job.status == "pending" # Default
assert import_job.imported_count == 0 # Default
assert import_job.updated_count == 0 # Default
assert import_job.error_count == 0 # Default
assert import_job.total_processed == 0 # Default
def test_import_job_status_values(self, db, test_user, test_store):
"""Test MarketplaceImportJob with different status values."""
statuses = [
"pending",
"processing",
"completed",
"failed",
"completed_with_errors",
]
for i, status in enumerate(statuses):
import_job = MarketplaceImportJob(
store_id=test_store.id,
user_id=test_user.id,
source_url=f"https://example.com/feed_{i}.csv",
status=status,
)
db.add(import_job)
db.commit()
db.refresh(import_job)
assert import_job.status == status
def test_import_job_counts(self, db, test_user, test_store):
"""Test MarketplaceImportJob count fields."""
import_job = MarketplaceImportJob(
store_id=test_store.id,
user_id=test_user.id,
source_url="https://example.com/feed.csv",
status="completed",
imported_count=100,
updated_count=50,
error_count=5,
total_processed=155,
)
db.add(import_job)
db.commit()
db.refresh(import_job)
assert import_job.imported_count == 100
assert import_job.updated_count == 50
assert import_job.error_count == 5
assert import_job.total_processed == 155
def test_import_job_error_message(self, db, test_user, test_store):
"""Test MarketplaceImportJob with error message."""
import_job = MarketplaceImportJob(
store_id=test_store.id,
user_id=test_user.id,
source_url="https://example.com/feed.csv",
status="failed",
error_message="Connection timeout while fetching CSV",
)
db.add(import_job)
db.commit()
db.refresh(import_job)
assert import_job.error_message == "Connection timeout while fetching CSV"
def test_import_job_relationships(self, db, test_user, test_store):
"""Test MarketplaceImportJob relationships."""
import_job = MarketplaceImportJob(
store_id=test_store.id,
user_id=test_user.id,
source_url="https://example.com/feed.csv",
)
db.add(import_job)
db.commit()
db.refresh(import_job)
assert import_job.store is not None
assert import_job.user is not None
assert import_job.store.id == test_store.id
assert import_job.user.id == test_user.id

View File

@@ -1,244 +0,0 @@
# tests/unit/models/database/test_marketplace_product.py
"""Unit tests for MarketplaceProduct database model."""
import pytest
from sqlalchemy.exc import IntegrityError
from app.modules.marketplace.models import (
MarketplaceProduct,
MarketplaceProductTranslation,
)
def _create_with_translation(db, marketplace_product_id, title, **kwargs):
"""Helper to create MarketplaceProduct with translation."""
mp = MarketplaceProduct(marketplace_product_id=marketplace_product_id, **kwargs)
db.add(mp)
db.flush()
translation = MarketplaceProductTranslation(
marketplace_product_id=mp.id,
language="en",
title=title,
)
db.add(translation)
db.commit()
db.refresh(mp)
return mp
@pytest.mark.unit
@pytest.mark.database
class TestMarketplaceProductModel:
"""Test MarketplaceProduct model."""
def test_marketplace_product_creation(self, db):
"""Test MarketplaceProduct model creation."""
marketplace_product = _create_with_translation(
db,
marketplace_product_id="DB_TEST_001",
title="Database Test Product",
price="25.99",
currency="USD",
brand="DBTest",
gtin="1234567890123",
availability="in stock",
marketplace="Letzshop",
store_name="Test Store",
)
assert marketplace_product.id is not None
assert marketplace_product.marketplace_product_id == "DB_TEST_001"
assert marketplace_product.get_title("en") == "Database Test Product"
assert marketplace_product.marketplace == "Letzshop"
assert marketplace_product.created_at is not None
def test_marketplace_product_id_uniqueness(self, db):
"""Test unique marketplace_product_id constraint."""
_create_with_translation(
db,
marketplace_product_id="UNIQUE_001",
title="Product 1",
marketplace="Letzshop",
)
# Duplicate marketplace_product_id should raise error
with pytest.raises(IntegrityError):
_create_with_translation(
db,
marketplace_product_id="UNIQUE_001",
title="Product 2",
marketplace="Letzshop",
)
def test_marketplace_product_all_fields(self, db):
"""Test MarketplaceProduct with all optional fields."""
marketplace_product = _create_with_translation(
db,
marketplace_product_id="FULL_001",
title="Full Product",
link="https://example.com/product",
image_link="https://example.com/image.jpg",
availability="in stock",
price="99.99",
brand="TestBrand",
gtin="9876543210123",
mpn="MPN123",
condition="new",
adult="no",
age_group="adult",
color="blue",
gender="unisex",
material="cotton",
pattern="solid",
size="M",
google_product_category="Apparel & Accessories",
product_type_raw="Clothing",
currency="EUR",
marketplace="Letzshop",
store_name="Full Store",
)
assert marketplace_product.brand == "TestBrand"
assert marketplace_product.gtin == "9876543210123"
assert marketplace_product.color == "blue"
assert marketplace_product.size == "M"
def test_marketplace_product_custom_labels(self, db):
"""Test MarketplaceProduct with custom labels."""
marketplace_product = _create_with_translation(
db,
marketplace_product_id="LABELS_001",
title="Labeled Product",
marketplace="Letzshop",
custom_label_0="Label0",
custom_label_1="Label1",
custom_label_2="Label2",
custom_label_3="Label3",
custom_label_4="Label4",
)
assert marketplace_product.custom_label_0 == "Label0"
assert marketplace_product.custom_label_4 == "Label4"
def test_marketplace_product_minimal_fields(self, db):
"""Test MarketplaceProduct with only required fields."""
marketplace_product = _create_with_translation(
db,
marketplace_product_id="MINIMAL_001",
title="Minimal Product",
)
assert marketplace_product.id is not None
assert marketplace_product.marketplace_product_id == "MINIMAL_001"
assert marketplace_product.get_title("en") == "Minimal Product"
assert marketplace_product.get_description("en") is None
assert marketplace_product.price is None
def test_marketplace_product_digital_fields(self, db):
"""Test MarketplaceProduct with digital product fields."""
marketplace_product = _create_with_translation(
db,
marketplace_product_id="DIGITAL_001",
title="Digital Product",
product_type_enum="digital",
is_digital=True,
digital_delivery_method="license_key",
platform="steam",
region_restrictions=["EU", "US"],
license_type="single_use",
)
assert marketplace_product.product_type_enum == "digital"
assert marketplace_product.is_digital is True
assert marketplace_product.digital_delivery_method == "license_key"
assert marketplace_product.platform == "steam"
assert marketplace_product.region_restrictions == ["EU", "US"]
assert marketplace_product.license_type == "single_use"
def test_marketplace_product_translation_methods(self, db):
"""Test translation helper methods."""
mp = MarketplaceProduct(marketplace_product_id="TRANS_001")
db.add(mp)
db.flush()
# Add English translation
en_trans = MarketplaceProductTranslation(
marketplace_product_id=mp.id,
language="en",
title="English Title",
description="English Description",
)
db.add(en_trans)
# Add French translation
fr_trans = MarketplaceProductTranslation(
marketplace_product_id=mp.id,
language="fr",
title="Titre Français",
description="Description Française",
)
db.add(fr_trans)
db.commit()
db.refresh(mp)
assert mp.get_title("en") == "English Title"
assert mp.get_title("fr") == "Titre Français"
assert mp.get_description("en") == "English Description"
assert mp.get_description("fr") == "Description Française"
# Test fallback to English for unknown language
assert mp.get_title("de") == "English Title" # Falls back to 'en'
assert mp.get_description("de") == "English Description"
def test_marketplace_product_numeric_prices(self, db):
"""Test numeric price fields."""
marketplace_product = _create_with_translation(
db,
marketplace_product_id="PRICES_001",
title="Priced Product",
price="99.99 EUR",
price_numeric=99.99,
sale_price="79.99 EUR",
sale_price_numeric=79.99,
)
assert marketplace_product.price == "99.99 EUR"
assert marketplace_product.price_numeric == 99.99
assert marketplace_product.sale_price_numeric == 79.99
assert marketplace_product.effective_price == 99.99
assert marketplace_product.effective_sale_price == 79.99
def test_marketplace_product_attributes_json(self, db):
"""Test flexible attributes JSON field."""
marketplace_product = _create_with_translation(
db,
marketplace_product_id="ATTRS_001",
title="Product with Attributes",
attributes={
"custom_field": "custom_value",
"nested": {"key": "value"},
},
)
assert marketplace_product.attributes["custom_field"] == "custom_value"
assert marketplace_product.attributes["nested"]["key"] == "value"
def test_marketplace_product_all_images_property(self, db):
"""Test all_images property."""
marketplace_product = _create_with_translation(
db,
marketplace_product_id="IMAGES_001",
title="Product with Images",
image_link="https://example.com/main.jpg",
additional_images=[
"https://example.com/img1.jpg",
"https://example.com/img2.jpg",
],
)
images = marketplace_product.all_images
assert len(images) == 3
assert images[0] == "https://example.com/main.jpg"
assert "https://example.com/img1.jpg" in images
assert "https://example.com/img2.jpg" in images

View File

@@ -1,256 +0,0 @@
# 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 app.modules.orders.models import Order, OrderItem
def create_order_with_snapshots(
db,
store,
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(
store_id=store.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:
"""Test Order model."""
def test_order_creation(
self, db, test_store, test_customer, test_customer_address
):
"""Test Order model with customer relationship."""
order = create_order_with_snapshots(
db, test_store, test_customer, test_customer_address,
order_number="ORD-001",
)
assert order.id is not None
assert order.store_id == test_store.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_store, test_customer, test_customer_address
):
"""Test order_number unique constraint."""
create_order_with_snapshots(
db, test_store, test_customer, test_customer_address,
order_number="UNIQUE-ORD-001",
)
# Duplicate order number should fail
with pytest.raises(IntegrityError):
create_order_with_snapshots(
db, test_store, test_customer, test_customer_address,
order_number="UNIQUE-ORD-001",
)
def test_order_status_values(
self, db, test_store, test_customer, test_customer_address
):
"""Test Order with different status values."""
statuses = [
"pending",
"processing",
"shipped",
"delivered",
"cancelled",
"refunded",
]
for i, status in enumerate(statuses):
order = create_order_with_snapshots(
db, test_store, test_customer, test_customer_address,
order_number=f"STATUS-ORD-{i:03d}",
status=status,
)
assert order.status == status
def test_order_amounts(self, db, test_store, test_customer, test_customer_address):
"""Test Order amount fields."""
order = create_order_with_snapshots(
db, test_store, test_customer, test_customer_address,
order_number="AMOUNTS-ORD-001",
subtotal=100.00,
tax_amount=20.00,
shipping_amount=10.00,
discount_amount=5.00,
total_amount=125.00,
)
assert float(order.subtotal) == 100.00
assert float(order.tax_amount) == 20.00
assert float(order.shipping_amount) == 10.00
assert float(order.discount_amount) == 5.00
assert float(order.total_amount) == 125.00
def test_order_relationships(
self, db, test_store, test_customer, test_customer_address
):
"""Test Order relationships."""
order = create_order_with_snapshots(
db, test_store, test_customer, test_customer_address,
order_number="REL-ORD-001",
)
assert order.store is not None
assert order.customer is not None
assert order.store.id == test_store.id
assert order.customer.id == test_customer.id
@pytest.mark.unit
@pytest.mark.database
class TestOrderItemModel:
"""Test OrderItem model."""
def test_order_item_creation(self, db, test_order, test_product):
"""Test OrderItem model."""
# Get title from translation
product_title = test_product.marketplace_product.get_title("en")
order_item = OrderItem(
order_id=test_order.id,
product_id=test_product.id,
product_name=product_title,
product_sku=test_product.store_sku or "SKU001",
quantity=2,
unit_price=49.99,
total_price=99.98,
)
db.add(order_item)
db.commit()
db.refresh(order_item)
assert order_item.id is not None
assert order_item.order_id == test_order.id
assert order_item.product_id == test_product.id
assert order_item.quantity == 2
assert float(order_item.unit_price) == 49.99
assert float(order_item.total_price) == 99.98
def test_order_item_stores_product_snapshot(self, db, test_order, test_product):
"""Test OrderItem stores product name and SKU as snapshot."""
order_item = OrderItem(
order_id=test_order.id,
product_id=test_product.id,
product_name="Snapshot Product Name",
product_sku="SNAPSHOT-SKU-001",
quantity=1,
unit_price=25.00,
total_price=25.00,
)
db.add(order_item)
db.commit()
db.refresh(order_item)
assert order_item.id is not None
assert order_item.product_name == "Snapshot Product Name"
assert order_item.product_sku == "SNAPSHOT-SKU-001"
def test_order_item_relationships(self, db, test_order, test_product):
"""Test OrderItem relationships."""
order_item = OrderItem(
order_id=test_order.id,
product_id=test_product.id,
product_name="Test Product",
product_sku="SKU001",
quantity=1,
unit_price=50.00,
total_price=50.00,
)
db.add(order_item)
db.commit()
db.refresh(order_item)
assert order_item.order is not None
assert order_item.order.id == test_order.id
def test_multiple_items_per_order(self, db, test_order, test_product):
"""Test multiple OrderItems can belong to same Order."""
# Create two order items for the same product (different quantities)
item1 = OrderItem(
order_id=test_order.id,
product_id=test_product.id,
product_name="Product - Size M",
product_sku="SKU001-M",
quantity=1,
unit_price=25.00,
total_price=25.00,
)
item2 = OrderItem(
order_id=test_order.id,
product_id=test_product.id,
product_name="Product - Size L",
product_sku="SKU001-L",
quantity=2,
unit_price=30.00,
total_price=60.00,
)
db.add_all([item1, item2])
db.commit()
assert item1.order_id == item2.order_id
assert item1.id != item2.id
assert item1.product_id == item2.product_id # Same product, different items

View File

@@ -1,242 +0,0 @@
# tests/unit/models/database/test_order_item_exception.py
"""Unit tests for OrderItemException database model."""
import pytest
from sqlalchemy.exc import IntegrityError
from app.modules.orders.models import OrderItemException
@pytest.mark.unit
@pytest.mark.database
class TestOrderItemExceptionModel:
"""Test OrderItemException model."""
def test_exception_creation(self, db, test_order_item, test_store):
"""Test OrderItemException model creation."""
exception = OrderItemException(
order_item_id=test_order_item.id,
store_id=test_store.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.store_id == test_store.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_store):
"""Test that only one exception can exist per order item."""
exception1 = OrderItemException(
order_item_id=test_order_item.id,
store_id=test_store.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,
store_id=test_store.id,
original_gtin="4006381333932",
exception_type="product_not_found",
)
db.add(exception2)
db.commit()
def test_exception_types(self, db, test_order_item, test_store):
"""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 app.modules.orders.models 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,
store_id=test_store.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_store):
"""Test different status values."""
exception = OrderItemException(
order_item_id=test_order_item.id,
store_id=test_store.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_store):
"""Test that GTIN and other fields can be null."""
exception = OrderItemException(
order_item_id=test_order_item.id,
store_id=test_store.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_store, 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,
store_id=test_store.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_store):
"""Test OrderItemException relationships."""
exception = OrderItemException(
order_item_id=test_order_item.id,
store_id=test_store.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.store is not None
assert exception.store.id == test_store.id
def test_exception_repr(self, db, test_order_item, test_store):
"""Test OrderItemException __repr__ method."""
exception = OrderItemException(
order_item_id=test_order_item.id,
store_id=test_store.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_store):
"""Test that exception is deleted when order item is deleted."""
exception = OrderItemException(
order_item_id=test_order_item.id,
store_id=test_store.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

View File

@@ -1,402 +0,0 @@
# tests/unit/models/database/test_product.py
"""Unit tests for Product (store catalog) database model."""
import pytest
from sqlalchemy.exc import IntegrityError
from app.modules.catalog.models import Product
@pytest.mark.unit
@pytest.mark.database
class TestProductModel:
"""Test Product (store catalog) model."""
def test_product_creation(self, db, test_store, test_marketplace_product):
"""Test Product model linking store catalog to marketplace product."""
product = Product(
store_id=test_store.id,
marketplace_product_id=test_marketplace_product.id,
store_sku="STORE_PROD_001",
price=89.99,
currency="EUR",
availability="in stock",
is_featured=True,
is_active=True,
)
db.add(product)
db.commit()
db.refresh(product)
assert product.id is not None
assert product.store_id == test_store.id
assert product.marketplace_product_id == test_marketplace_product.id
assert product.price == 89.99
assert product.is_featured is True
assert product.store.store_code == test_store.store_code
# Use get_title() method instead of .title attribute
assert product.marketplace_product.get_title(
"en"
) == test_marketplace_product.get_title("en")
def test_product_unique_per_store(self, db, test_store, test_marketplace_product):
"""Test that same marketplace product can't be added twice to store catalog."""
product1 = Product(
store_id=test_store.id,
marketplace_product_id=test_marketplace_product.id,
is_active=True,
)
db.add(product1)
db.commit()
# Same marketplace product to same store should fail
with pytest.raises(IntegrityError):
product2 = Product(
store_id=test_store.id,
marketplace_product_id=test_marketplace_product.id,
is_active=True,
)
db.add(product2)
db.commit()
def test_product_default_values(self, db, test_store, test_marketplace_product):
"""Test Product model default values."""
product = Product(
store_id=test_store.id,
marketplace_product_id=test_marketplace_product.id,
)
db.add(product)
db.commit()
db.refresh(product)
assert product.is_active is True # Default
assert product.is_featured is False # Default
assert product.is_digital is False # Default
assert product.product_type == "physical" # Default
assert product.min_quantity == 1 # Default
assert product.display_order == 0 # Default
def test_product_store_override_fields(
self, db, test_store, test_marketplace_product
):
"""Test Product model store-specific override fields."""
product = Product(
store_id=test_store.id,
marketplace_product_id=test_marketplace_product.id,
store_sku="CUSTOM_SKU_001",
price=49.99,
sale_price=39.99,
currency="USD",
availability="limited",
condition="new",
)
db.add(product)
db.commit()
db.refresh(product)
assert product.store_sku == "CUSTOM_SKU_001"
assert product.price == 49.99
assert product.sale_price == 39.99
assert product.currency == "USD"
assert product.availability == "limited"
def test_product_inventory_settings(
self, db, test_store, test_marketplace_product
):
"""Test Product model inventory settings."""
product = Product(
store_id=test_store.id,
marketplace_product_id=test_marketplace_product.id,
min_quantity=2,
max_quantity=10,
)
db.add(product)
db.commit()
db.refresh(product)
assert product.min_quantity == 2
assert product.max_quantity == 10
def test_product_relationships(self, db, test_store, test_marketplace_product):
"""Test Product relationships."""
product = Product(
store_id=test_store.id,
marketplace_product_id=test_marketplace_product.id,
)
db.add(product)
db.commit()
db.refresh(product)
assert product.store is not None
assert product.marketplace_product is not None
assert product.inventory_entries == [] # No inventory yet
def test_product_get_source_comparison_info(
self, db, test_store, test_marketplace_product
):
"""Test get_source_comparison_info method for 'view original source' feature.
Products are independent entities with all fields populated at creation.
Source values are kept for comparison only, not inheritance.
"""
# Set up marketplace product values
test_marketplace_product.price_cents = 10000 # €100.00
test_marketplace_product.brand = "SourceBrand"
db.commit()
# Create product with its own values (independent copy pattern)
product = Product(
store_id=test_store.id,
marketplace_product_id=test_marketplace_product.id,
price_cents=8999, # €89.99 - store's price
brand="StoreBrand", # Store's brand
)
db.add(product)
db.commit()
db.refresh(product)
info = product.get_source_comparison_info()
# Product has its own price
assert info["price"] == 89.99
assert info["price_cents"] == 8999
assert info["price_source"] == 100.00 # Original marketplace price
# Product has its own brand
assert info["brand"] == "StoreBrand"
assert info["brand_source"] == "SourceBrand" # Original marketplace brand
# No more *_overridden keys in the pattern
assert "price_overridden" not in info
assert "brand_overridden" not in info
def test_product_fields_are_independent(
self, db, test_store, test_marketplace_product
):
"""Test that product fields don't inherit from marketplace product.
Products are independent entities - NULL fields stay NULL,
no inheritance/fallback logic.
"""
# Set up marketplace product values
test_marketplace_product.price_cents = 10000
test_marketplace_product.brand = "SourceBrand"
db.commit()
# Create product without copying values
product = Product(
store_id=test_store.id,
marketplace_product_id=test_marketplace_product.id,
# Not copying price_cents or brand
)
db.add(product)
db.commit()
db.refresh(product)
# Fields should be NULL (not inherited)
assert product.price_cents is None
assert product.price is None
assert product.brand is None
# But we can still see the source values for comparison
info = product.get_source_comparison_info()
assert info["price_source"] == 100.00
assert info["brand_source"] == "SourceBrand"
def test_product_direct_creation_without_marketplace(self, db, test_store):
"""Test creating a product directly without a marketplace source.
Products can be created directly without a marketplace_product_id,
making them fully independent store products.
"""
product = Product(
store_id=test_store.id,
marketplace_product_id=None, # No marketplace source
store_sku="DIRECT_001",
brand="DirectBrand",
price=59.99,
currency="EUR",
is_digital=True,
product_type="digital",
is_active=True,
)
db.add(product)
db.commit()
db.refresh(product)
assert product.id is not None
assert product.marketplace_product_id is None
assert product.marketplace_product is None
assert product.store_sku == "DIRECT_001"
assert product.brand == "DirectBrand"
assert product.is_digital is True
assert product.product_type == "digital"
def test_product_is_digital_column(self, db, test_store):
"""Test is_digital is an independent column, not derived from marketplace."""
# Create digital product without marketplace source
digital_product = Product(
store_id=test_store.id,
store_sku="DIGITAL_001",
is_digital=True,
product_type="digital",
)
db.add(digital_product)
db.commit()
db.refresh(digital_product)
assert digital_product.is_digital is True
assert digital_product.product_type == "digital"
# Create physical product without marketplace source
physical_product = Product(
store_id=test_store.id,
store_sku="PHYSICAL_001",
is_digital=False,
product_type="physical",
)
db.add(physical_product)
db.commit()
db.refresh(physical_product)
assert physical_product.is_digital is False
assert physical_product.product_type == "physical"
def test_product_type_values(self, db, test_store):
"""Test product_type can be set to various values."""
product_types = ["physical", "digital", "service", "subscription"]
for ptype in product_types:
product = Product(
store_id=test_store.id,
store_sku=f"TYPE_{ptype.upper()}",
product_type=ptype,
is_digital=(ptype == "digital"),
)
db.add(product)
db.commit()
db.refresh(product)
assert product.product_type == ptype
@pytest.mark.unit
@pytest.mark.database
@pytest.mark.inventory
class TestProductInventoryProperties:
"""Test Product inventory properties including digital product handling."""
def test_physical_product_no_inventory_returns_zero(self, db, test_store):
"""Test physical product with no inventory entries returns 0."""
product = Product(
store_id=test_store.id,
store_sku="PHYS_INV_001",
is_digital=False,
product_type="physical",
)
db.add(product)
db.commit()
db.refresh(product)
assert product.is_digital is False
assert product.has_unlimited_inventory is False
assert product.total_inventory == 0
assert product.available_inventory == 0
def test_physical_product_with_inventory(self, db, test_store):
"""Test physical product calculates inventory from entries."""
from app.modules.inventory.models import Inventory
product = Product(
store_id=test_store.id,
store_sku="PHYS_INV_002",
is_digital=False,
product_type="physical",
)
db.add(product)
db.commit()
db.refresh(product)
# Add inventory entries
inv1 = Inventory(
product_id=product.id,
store_id=test_store.id,
warehouse="strassen",
bin_location="SA-01-01",
location="WAREHOUSE_A",
quantity=100,
reserved_quantity=10,
)
inv2 = Inventory(
product_id=product.id,
store_id=test_store.id,
warehouse="strassen",
bin_location="SA-01-02",
location="WAREHOUSE_B",
quantity=50,
reserved_quantity=5,
)
db.add_all([inv1, inv2])
db.commit()
db.refresh(product)
assert product.has_unlimited_inventory is False
assert product.total_inventory == 150 # 100 + 50
assert product.available_inventory == 135 # (100-10) + (50-5)
def test_digital_product_has_unlimited_inventory(self, db, test_store):
"""Test digital product returns unlimited inventory."""
product = Product(
store_id=test_store.id,
store_sku="DIG_INV_001",
is_digital=True,
product_type="digital",
)
db.add(product)
db.commit()
db.refresh(product)
assert product.is_digital is True
assert product.has_unlimited_inventory is True
assert product.total_inventory == Product.UNLIMITED_INVENTORY
assert product.available_inventory == Product.UNLIMITED_INVENTORY
def test_digital_product_ignores_inventory_entries(self, db, test_store):
"""Test digital product returns unlimited even with inventory entries."""
from app.modules.inventory.models import Inventory
product = Product(
store_id=test_store.id,
store_sku="DIG_INV_002",
is_digital=True,
product_type="digital",
)
db.add(product)
db.commit()
db.refresh(product)
# Add inventory entries (e.g., for license keys)
inv = Inventory(
product_id=product.id,
store_id=test_store.id,
warehouse="strassen",
bin_location="DIG-01-01",
location="DIGITAL_LICENSES",
quantity=10,
reserved_quantity=2,
)
db.add(inv)
db.commit()
db.refresh(product)
# Digital product should still return unlimited
assert product.has_unlimited_inventory is True
assert product.total_inventory == Product.UNLIMITED_INVENTORY
assert product.available_inventory == Product.UNLIMITED_INVENTORY
def test_unlimited_inventory_constant(self):
"""Test UNLIMITED_INVENTORY constant value."""
assert Product.UNLIMITED_INVENTORY == 999999
# Should be large enough to never cause "insufficient inventory"
assert Product.UNLIMITED_INVENTORY > 100000

View File

@@ -1,138 +0,0 @@
# tests/unit/models/database/test_store.py
"""Unit tests for Store database model."""
import pytest
from sqlalchemy.exc import IntegrityError
from app.modules.tenancy.models import Store
@pytest.mark.unit
@pytest.mark.database
class TestStoreModel:
"""Test Store model."""
def test_store_creation(self, db, test_merchant):
"""Test Store model creation with merchant relationship."""
store = Store(
merchant_id=test_merchant.id,
store_code="DBTEST",
subdomain="dbtest",
name="Database Test Store",
description="Testing store model",
contact_email="contact@dbtest.com",
contact_phone="+1234567890",
business_address="123 Test Street",
is_active=True,
is_verified=False,
)
db.add(store)
db.commit()
db.refresh(store)
assert store.id is not None
assert store.store_code == "DBTEST"
assert store.subdomain == "dbtest"
assert store.name == "Database Test Store"
assert store.merchant_id == test_merchant.id
assert store.contact_email == "contact@dbtest.com"
assert store.is_active is True
assert store.is_verified is False
assert store.created_at is not None
def test_store_with_letzshop_urls(self, db, test_merchant):
"""Test Store model with multi-language Letzshop URLs."""
store = Store(
merchant_id=test_merchant.id,
store_code="MULTILANG",
subdomain="multilang",
name="Multi-Language Store",
letzshop_csv_url_fr="https://example.com/feed_fr.csv",
letzshop_csv_url_en="https://example.com/feed_en.csv",
letzshop_csv_url_de="https://example.com/feed_de.csv",
is_active=True,
)
db.add(store)
db.commit()
db.refresh(store)
assert store.letzshop_csv_url_fr == "https://example.com/feed_fr.csv"
assert store.letzshop_csv_url_en == "https://example.com/feed_en.csv"
assert store.letzshop_csv_url_de == "https://example.com/feed_de.csv"
def test_store_code_uniqueness(self, db, test_merchant):
"""Test store_code unique constraint."""
store1 = Store(
merchant_id=test_merchant.id,
store_code="UNIQUE",
subdomain="unique1",
name="Unique Store 1",
)
db.add(store1)
db.commit()
# Duplicate store_code should raise error
with pytest.raises(IntegrityError):
store2 = Store(
merchant_id=test_merchant.id,
store_code="UNIQUE",
subdomain="unique2",
name="Unique Store 2",
)
db.add(store2)
db.commit()
def test_subdomain_uniqueness(self, db, test_merchant):
"""Test subdomain unique constraint."""
store1 = Store(
merchant_id=test_merchant.id,
store_code="STORE1",
subdomain="testsubdomain",
name="Store 1",
)
db.add(store1)
db.commit()
# Duplicate subdomain should raise error
with pytest.raises(IntegrityError):
store2 = Store(
merchant_id=test_merchant.id,
store_code="STORE2",
subdomain="testsubdomain",
name="Store 2",
)
db.add(store2)
db.commit()
def test_store_default_values(self, db, test_merchant):
"""Test Store model default values."""
store = Store(
merchant_id=test_merchant.id,
store_code="DEFAULTS",
subdomain="defaults",
name="Default Store",
)
db.add(store)
db.commit()
db.refresh(store)
assert store.is_active is True # Default
assert store.is_verified is False # Default
def test_store_merchant_relationship(self, db, test_merchant):
"""Test Store-Merchant relationship."""
store = Store(
merchant_id=test_merchant.id,
store_code="RELTEST",
subdomain="reltest",
name="Relationship Test Store",
)
db.add(store)
db.commit()
db.refresh(store)
assert store.merchant is not None
assert store.merchant.id == test_merchant.id
assert store.merchant.name == test_merchant.name

View File

@@ -1,138 +0,0 @@
# tests/unit/models/database/test_team.py
"""Unit tests for StoreUser and Role database models."""
import pytest
from app.modules.tenancy.models import Role, Store, StoreUser
@pytest.mark.unit
@pytest.mark.database
class TestRoleModel:
"""Test Role model."""
def test_role_creation(self, db, test_store):
"""Test Role model creation."""
role = Role(
store_id=test_store.id,
name="Manager",
permissions=["products.create", "orders.view"],
)
db.add(role)
db.commit()
db.refresh(role)
assert role.id is not None
assert role.store_id == test_store.id
assert role.name == "Manager"
assert "products.create" in role.permissions
assert "orders.view" in role.permissions
def test_role_default_permissions(self, db, test_store):
"""Test Role model with default empty permissions."""
role = Role(
store_id=test_store.id,
name="Viewer",
)
db.add(role)
db.commit()
db.refresh(role)
assert role.permissions == [] or role.permissions is None
def test_role_store_relationship(self, db, test_store):
"""Test Role-Store relationship."""
role = Role(
store_id=test_store.id,
name="Admin",
permissions=["*"],
)
db.add(role)
db.commit()
db.refresh(role)
assert role.store is not None
assert role.store.id == test_store.id
@pytest.mark.unit
@pytest.mark.database
class TestStoreUserModel:
"""Test StoreUser model."""
def test_store_user_creation(self, db, test_store, test_user):
"""Test StoreUser model for team management."""
# Create a role
role = Role(
store_id=test_store.id,
name="Manager",
permissions=["products.create", "orders.view"],
)
db.add(role)
db.commit()
# Create store user
store_user = StoreUser(
store_id=test_store.id,
user_id=test_user.id,
role_id=role.id,
is_active=True,
)
db.add(store_user)
db.commit()
db.refresh(store_user)
assert store_user.id is not None
assert store_user.store_id == test_store.id
assert store_user.user_id == test_user.id
assert store_user.role.name == "Manager"
assert "products.create" in store_user.role.permissions
def test_store_user_relationships(self, db, test_store, test_user):
"""Test StoreUser relationships."""
role = Role(
store_id=test_store.id,
name="Staff",
permissions=["orders.view"],
)
db.add(role)
db.commit()
store_user = StoreUser(
store_id=test_store.id,
user_id=test_user.id,
role_id=role.id,
is_active=True,
)
db.add(store_user)
db.commit()
db.refresh(store_user)
assert store_user.store is not None
assert store_user.user is not None
assert store_user.role is not None
assert store_user.store.store_code == test_store.store_code
assert store_user.user.email == test_user.email
def test_store_user_with_active_flag(self, db, test_store, test_user):
"""Test StoreUser is_active field."""
role = Role(
store_id=test_store.id,
name="Default",
permissions=[],
)
db.add(role)
db.commit()
# Create with explicit is_active=True
store_user = StoreUser(
store_id=test_store.id,
user_id=test_user.id,
role_id=role.id,
is_active=True,
)
db.add(store_user)
db.commit()
db.refresh(store_user)
assert store_user.is_active is True

View File

@@ -1,105 +0,0 @@
# tests/unit/models/database/test_user.py
"""Unit tests for User database model."""
import pytest
from sqlalchemy.exc import IntegrityError
from app.modules.tenancy.models import User
@pytest.mark.unit
@pytest.mark.database
class TestUserModel:
"""Test User model."""
def test_user_creation(self, db):
"""Test User model creation and relationships."""
user = User(
email="db_test@example.com",
username="dbtest",
hashed_password="hashed_password_123",
role="user",
is_active=True,
)
db.add(user)
db.commit()
db.refresh(user)
assert user.id is not None
assert user.email == "db_test@example.com"
assert user.username == "dbtest"
assert user.role == "user"
assert user.is_active is True
assert user.created_at is not None
assert user.updated_at is not None
def test_user_email_uniqueness(self, db):
"""Test email unique constraint."""
user1 = User(
email="unique@example.com",
username="user1",
hashed_password="hash1",
)
db.add(user1)
db.commit()
# Duplicate email should raise error
with pytest.raises(IntegrityError):
user2 = User(
email="unique@example.com",
username="user2",
hashed_password="hash2",
)
db.add(user2)
db.commit()
def test_user_username_uniqueness(self, db):
"""Test username unique constraint."""
user1 = User(
email="user1@example.com",
username="sameusername",
hashed_password="hash1",
)
db.add(user1)
db.commit()
# Duplicate username should raise error
with pytest.raises(IntegrityError):
user2 = User(
email="user2@example.com",
username="sameusername",
hashed_password="hash2",
)
db.add(user2)
db.commit()
def test_user_default_values(self, db):
"""Test User model default values."""
user = User(
email="defaults@example.com",
username="defaultuser",
hashed_password="hash",
)
db.add(user)
db.commit()
db.refresh(user)
assert user.is_active is True # Default
assert user.role == "store" # Default (UserRole.STORE)
def test_user_optional_fields(self, db):
"""Test User model with optional fields."""
user = User(
email="optional@example.com",
username="optionaluser",
hashed_password="hash",
first_name="John",
last_name="Doe",
)
db.add(user)
db.commit()
db.refresh(user)
assert user.first_name == "John"
assert user.last_name == "Doe"