feat: multi-module improvements across merchant, store, i18n, and customer systems
All checks were successful
CI / ruff (push) Successful in 12s
CI / pytest (push) Successful in 50m57s
CI / validate (push) Successful in 24s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Successful in 40s
CI / deploy (push) Successful in 51s

- Fix platform-grouped merchant sidebar menu with core items at root level
- Add merchant store management (detail page, create store, team page)
- Fix store settings 500 error by removing dead stripe/API tab
- Move onboarding translations to module-owned locale files
- Fix onboarding banner i18n with server-side rendering + context inheritance
- Refactor login language selectors to use languageSelector() function (LANG-002)
- Move HTTPException handling to global exception handler in merchant routes (API-003)
- Add language selector to all login pages and portal headers
- Fix customer module: drop order stats from customer model, add to orders module
- Fix admin menu config visibility for super admin platform context
- Fix storefront auth and layout issues
- Add missing i18n translations for onboarding steps (en/fr/de/lb)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 23:48:25 +01:00
parent f141cc4e6a
commit a77a8a3a98
113 changed files with 3741 additions and 2923 deletions

View File

@@ -0,0 +1,255 @@
# tests/unit/test_merchant_store_service.py
"""Unit tests for MerchantStoreService."""
import uuid
import pytest
from app.modules.tenancy.exceptions import (
MerchantNotFoundException,
StoreAlreadyExistsException,
StoreNotFoundException,
StoreValidationException,
)
from app.modules.tenancy.models import Merchant, Store
from app.modules.tenancy.services.merchant_store_service import MerchantStoreService
@pytest.fixture
def merchant_owner(db, test_user):
"""Create a merchant owned by test_user."""
unique_id = str(uuid.uuid4())[:8]
merchant = Merchant(
name=f"Test Merchant {unique_id}",
owner_user_id=test_user.id,
contact_email=f"merchant{unique_id}@test.com",
is_active=True,
is_verified=True,
)
db.add(merchant)
db.commit()
db.refresh(merchant)
return merchant
@pytest.fixture
def other_merchant_owner(db):
"""Create a separate merchant for ownership isolation tests."""
from app.modules.tenancy.models.user import User
unique_id = str(uuid.uuid4())[:8]
user = User(
email=f"other{unique_id}@test.com",
username=f"other_{unique_id}",
hashed_password="fakehash", # noqa: SEC001
role="merchant_owner",
is_active=True,
)
db.add(user)
db.flush()
merchant = Merchant(
name=f"Other Merchant {unique_id}",
owner_user_id=user.id,
contact_email=f"otherm{unique_id}@test.com",
is_active=True,
is_verified=True,
)
db.add(merchant)
db.commit()
db.refresh(merchant)
return merchant
@pytest.fixture
def merchant_store(db, merchant_owner):
"""Create a store under the merchant."""
unique_id = str(uuid.uuid4())[:8]
store = Store(
merchant_id=merchant_owner.id,
store_code=f"MS_{unique_id}".upper(),
subdomain=f"ms-{unique_id}".lower(),
name=f"Merchant Store {unique_id}",
description="A test store",
is_active=True,
is_verified=False,
)
db.add(store)
db.commit()
db.refresh(store)
return store
@pytest.mark.unit
@pytest.mark.tenancy
class TestMerchantStoreServiceGetDetail:
"""Tests for get_store_detail."""
def setup_method(self):
self.service = MerchantStoreService()
def test_get_store_detail_success(self, db, merchant_owner, merchant_store):
"""Test getting store detail with valid ownership."""
result = self.service.get_store_detail(db, merchant_owner.id, merchant_store.id)
assert result["id"] == merchant_store.id
assert result["store_code"] == merchant_store.store_code
assert result["name"] == merchant_store.name
assert result["is_active"] is True
assert "platforms" in result
def test_get_store_detail_wrong_merchant(
self, db, other_merchant_owner, merchant_store
):
"""Test that accessing another merchant's store raises not found."""
with pytest.raises(StoreNotFoundException):
self.service.get_store_detail(
db, other_merchant_owner.id, merchant_store.id
)
def test_get_store_detail_nonexistent(self, db, merchant_owner):
"""Test getting a non-existent store."""
with pytest.raises(StoreNotFoundException):
self.service.get_store_detail(db, merchant_owner.id, 99999)
@pytest.mark.unit
@pytest.mark.tenancy
class TestMerchantStoreServiceUpdate:
"""Tests for update_store."""
def setup_method(self):
self.service = MerchantStoreService()
def test_update_store_success(self, db, merchant_owner, merchant_store):
"""Test updating store fields."""
result = self.service.update_store(
db,
merchant_owner.id,
merchant_store.id,
{"name": "Updated Name", "contact_email": "new@test.com"},
)
db.commit()
assert result["name"] == "Updated Name"
assert result["contact_email"] == "new@test.com"
def test_update_store_ignores_disallowed_fields(
self, db, merchant_owner, merchant_store
):
"""Test that admin-only fields are ignored."""
result = self.service.update_store(
db,
merchant_owner.id,
merchant_store.id,
{"is_active": False, "is_verified": True, "name": "OK Name"},
)
db.commit()
# is_active and is_verified should NOT change
assert result["is_active"] is True
assert result["is_verified"] is False
assert result["name"] == "OK Name"
def test_update_store_wrong_merchant(
self, db, other_merchant_owner, merchant_store
):
"""Test that updating another merchant's store raises not found."""
with pytest.raises(StoreNotFoundException):
self.service.update_store(
db,
other_merchant_owner.id,
merchant_store.id,
{"name": "Hacked"},
)
@pytest.mark.unit
@pytest.mark.tenancy
class TestMerchantStoreServiceCreate:
"""Tests for create_store."""
def setup_method(self):
self.service = MerchantStoreService()
def test_create_store_success(self, db, merchant_owner):
"""Test successful store creation."""
unique_id = str(uuid.uuid4())[:8]
store_data = {
"name": f"New Store {unique_id}",
"store_code": f"NEW_{unique_id}",
"subdomain": f"new-{unique_id}".lower(),
"description": "Test description",
"platform_ids": [],
}
result = self.service.create_store(db, merchant_owner.id, store_data)
db.commit()
assert result["name"] == store_data["name"]
assert result["store_code"] == store_data["store_code"].upper()
assert result["is_active"] is True
assert result["is_verified"] is False
def test_create_store_duplicate_code(self, db, merchant_owner, merchant_store):
"""Test creating store with duplicate store code."""
store_data = {
"name": "Another Store",
"store_code": merchant_store.store_code,
"subdomain": f"another-{uuid.uuid4().hex[:8]}",
"description": None,
"platform_ids": [],
}
with pytest.raises(StoreAlreadyExistsException):
self.service.create_store(db, merchant_owner.id, store_data)
def test_create_store_duplicate_subdomain(self, db, merchant_owner, merchant_store):
"""Test creating store with duplicate subdomain."""
store_data = {
"name": "Another Store",
"store_code": f"UNIQUE_{uuid.uuid4().hex[:8]}",
"subdomain": merchant_store.subdomain,
"description": None,
"platform_ids": [],
}
with pytest.raises(StoreValidationException):
self.service.create_store(db, merchant_owner.id, store_data)
def test_create_store_nonexistent_merchant(self, db):
"""Test creating store for non-existent merchant."""
store_data = {
"name": "No Merchant Store",
"store_code": f"NM_{uuid.uuid4().hex[:8]}",
"subdomain": f"nm-{uuid.uuid4().hex[:8]}",
"description": None,
"platform_ids": [],
}
with pytest.raises(MerchantNotFoundException):
self.service.create_store(db, 99999, store_data)
def test_create_store_creates_default_roles(self, db, merchant_owner):
"""Test that default roles are created for new store."""
from app.modules.tenancy.models.store import Role
unique_id = str(uuid.uuid4())[:8]
store_data = {
"name": f"Roles Store {unique_id}",
"store_code": f"ROLE_{unique_id}",
"subdomain": f"role-{unique_id}".lower(),
"description": None,
"platform_ids": [],
}
result = self.service.create_store(db, merchant_owner.id, store_data)
db.commit()
roles = db.query(Role).filter(Role.store_id == result["id"]).all()
role_names = {r.name for r in roles}
assert "Owner" in role_names
assert "Manager" in role_names
assert "Editor" in role_names
assert "Viewer" in role_names