refactor: remove all backward compatibility code across 70 files
Some checks failed
Some checks failed
Clean up 28 backward compatibility instances identified in the codebase. The app is not live, so all shims are replaced with the target architecture: - Remove legacy Inventory.location column (use bin_location exclusively) - Remove dashboard _extract_metric_value helper (use flat metrics dict) - Remove legacy stat field duplicates (total_stores, total_imports, etc.) - Remove 13 re-export shims and class aliases across modules - Remove module-enabling JSON fallback (use PlatformModule junction table) - Remove menu_to_legacy_format() conversion (return dataclasses directly) - Remove title/description from MarketplaceProductBase schema - Clean billing convenience method docstrings - Clean test fixtures and backward-compat comments - Add PlatformModule seeding to init_production.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
4
tests/fixtures/auth_fixtures.py
vendored
4
tests/fixtures/auth_fixtures.py
vendored
@@ -49,7 +49,7 @@ def test_admin(db, auth_manager):
|
||||
hashed_password=hashed_password,
|
||||
role="admin",
|
||||
is_active=True,
|
||||
is_super_admin=True, # Default to super admin for backward compatibility
|
||||
is_super_admin=True, # Full platform access
|
||||
)
|
||||
db.add(admin)
|
||||
db.commit()
|
||||
@@ -130,7 +130,7 @@ def another_admin(db, auth_manager):
|
||||
hashed_password=hashed_password,
|
||||
role="admin",
|
||||
is_active=True,
|
||||
is_super_admin=True, # Super admin for backward compatibility
|
||||
is_super_admin=True, # Full platform access
|
||||
)
|
||||
db.add(admin)
|
||||
db.commit()
|
||||
|
||||
2
tests/fixtures/store_fixtures.py
vendored
2
tests/fixtures/store_fixtures.py
vendored
@@ -192,7 +192,6 @@ def test_inventory(db, test_product):
|
||||
store_id=test_product.store_id,
|
||||
warehouse="strassen",
|
||||
bin_location=f"SA-10-{unique_id[:2]}",
|
||||
location=f"WAREHOUSE_A_{unique_id}",
|
||||
quantity=100,
|
||||
reserved_quantity=10,
|
||||
gtin=test_product.marketplace_product.gtin,
|
||||
@@ -213,7 +212,6 @@ def multiple_inventory_entries(db, multiple_products, test_store):
|
||||
gtin=product.gtin,
|
||||
warehouse="strassen",
|
||||
bin_location=f"SA-{i:02d}-01",
|
||||
location=f"LOC_{i}",
|
||||
quantity=10 + (i * 5),
|
||||
reserved_quantity=i,
|
||||
store_id=test_store.id,
|
||||
|
||||
@@ -7,7 +7,7 @@ requests, including:
|
||||
- Merchant domain → platform resolved → store resolved
|
||||
- Store-specific domain overrides merchant domain
|
||||
- Merchant domain resolves to first active store
|
||||
- Existing StoreDomain and subdomain routing still work (backward compatibility)
|
||||
- Existing StoreDomain and subdomain routing still work
|
||||
"""
|
||||
|
||||
import uuid
|
||||
@@ -95,7 +95,7 @@ class TestMerchantDomainFlow:
|
||||
assert data["store_code"] == store.store_code
|
||||
|
||||
def test_subdomain_routing_still_works(self, client, store_with_subdomain):
|
||||
"""Test backward compatibility: subdomain routing still works."""
|
||||
"""Test that subdomain routing still works."""
|
||||
response = client.get(
|
||||
"/middleware-test/subdomain-detection",
|
||||
headers={
|
||||
|
||||
@@ -12,7 +12,7 @@ Tests cover:
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core.frontend_detector import FrontendDetector, get_frontend_type
|
||||
from app.core.frontend_detector import FrontendDetector
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class TestFrontendDetectorAdmin:
|
||||
|
||||
def test_detect_admin_from_subdomain(self):
|
||||
"""Test admin detection from admin subdomain."""
|
||||
result = FrontendDetector.detect(host="admin.oms.lu", path="/dashboard")
|
||||
result = FrontendDetector.detect(host="admin.omsflow.lu", path="/dashboard")
|
||||
assert result == FrontendType.ADMIN
|
||||
|
||||
def test_detect_admin_from_subdomain_with_port(self):
|
||||
@@ -42,7 +42,7 @@ class TestFrontendDetectorAdmin:
|
||||
|
||||
def test_detect_admin_nested_path(self):
|
||||
"""Test admin detection with nested admin path."""
|
||||
result = FrontendDetector.detect(host="oms.lu", path="/admin/stores/123/products")
|
||||
result = FrontendDetector.detect(host="omsflow.lu", path="/admin/stores/123/products")
|
||||
assert result == FrontendType.ADMIN
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class TestFrontendDetectorStore:
|
||||
|
||||
def test_detect_store_nested_path(self):
|
||||
"""Test store detection with nested store path."""
|
||||
result = FrontendDetector.detect(host="oms.lu", path="/store/dashboard/analytics")
|
||||
result = FrontendDetector.detect(host="omsflow.lu", path="/store/dashboard/analytics")
|
||||
assert result == FrontendType.STORE
|
||||
|
||||
def test_stores_plural_not_store_dashboard(self):
|
||||
@@ -92,7 +92,7 @@ class TestFrontendDetectorStorefront:
|
||||
|
||||
def test_detect_storefront_from_store_subdomain(self):
|
||||
"""Test storefront detection from store subdomain."""
|
||||
result = FrontendDetector.detect(host="orion.oms.lu", path="/products")
|
||||
result = FrontendDetector.detect(host="orion.omsflow.lu", path="/products")
|
||||
assert result == FrontendType.STOREFRONT
|
||||
|
||||
def test_detect_storefront_from_store_context(self):
|
||||
@@ -115,7 +115,7 @@ class TestFrontendDetectorPlatform:
|
||||
|
||||
def test_detect_platform_from_marketing_page(self):
|
||||
"""Test platform detection from marketing page."""
|
||||
result = FrontendDetector.detect(host="oms.lu", path="/pricing")
|
||||
result = FrontendDetector.detect(host="omsflow.lu", path="/pricing")
|
||||
assert result == FrontendType.PLATFORM
|
||||
|
||||
def test_detect_platform_from_about(self):
|
||||
@@ -135,7 +135,7 @@ class TestFrontendDetectorPriority:
|
||||
|
||||
def test_admin_subdomain_priority_over_path(self):
|
||||
"""Test that admin subdomain takes priority."""
|
||||
result = FrontendDetector.detect(host="admin.oms.lu", path="/storefront/products")
|
||||
result = FrontendDetector.detect(host="admin.omsflow.lu", path="/storefront/products")
|
||||
assert result == FrontendType.ADMIN
|
||||
|
||||
def test_admin_path_priority_over_store_context(self):
|
||||
@@ -148,7 +148,7 @@ class TestFrontendDetectorPriority:
|
||||
def test_path_priority_over_subdomain(self):
|
||||
"""Test that explicit path takes priority for store/storefront."""
|
||||
# /store/ path on a store subdomain -> STORE (path wins)
|
||||
result = FrontendDetector.detect(host="orion.oms.lu", path="/store/settings")
|
||||
result = FrontendDetector.detect(host="orion.omsflow.lu", path="/store/settings")
|
||||
assert result == FrontendType.STORE
|
||||
|
||||
|
||||
@@ -159,20 +159,20 @@ class TestFrontendDetectorHelpers:
|
||||
def test_strip_port(self):
|
||||
"""Test port stripping from host."""
|
||||
assert FrontendDetector._strip_port("localhost:8000") == "localhost"
|
||||
assert FrontendDetector._strip_port("oms.lu") == "oms.lu"
|
||||
assert FrontendDetector._strip_port("omsflow.lu") == "omsflow.lu"
|
||||
assert FrontendDetector._strip_port("admin.localhost:9999") == "admin.localhost"
|
||||
|
||||
def test_get_subdomain(self):
|
||||
"""Test subdomain extraction."""
|
||||
assert FrontendDetector._get_subdomain("orion.oms.lu") == "orion"
|
||||
assert FrontendDetector._get_subdomain("admin.oms.lu") == "admin"
|
||||
assert FrontendDetector._get_subdomain("oms.lu") is None
|
||||
assert FrontendDetector._get_subdomain("orion.omsflow.lu") == "orion"
|
||||
assert FrontendDetector._get_subdomain("admin.omsflow.lu") == "admin"
|
||||
assert FrontendDetector._get_subdomain("omsflow.lu") is None
|
||||
assert FrontendDetector._get_subdomain("localhost") is None
|
||||
assert FrontendDetector._get_subdomain("127.0.0.1") is None
|
||||
|
||||
def test_is_admin(self):
|
||||
"""Test is_admin convenience method."""
|
||||
assert FrontendDetector.is_admin("admin.oms.lu", "/dashboard") is True
|
||||
assert FrontendDetector.is_admin("admin.omsflow.lu", "/dashboard") is True
|
||||
assert FrontendDetector.is_admin("localhost", "/admin/stores") is True
|
||||
assert FrontendDetector.is_admin("localhost", "/store/settings") is False
|
||||
|
||||
@@ -185,13 +185,13 @@ class TestFrontendDetectorHelpers:
|
||||
def test_is_storefront(self):
|
||||
"""Test is_storefront convenience method."""
|
||||
assert FrontendDetector.is_storefront("localhost", "/storefront/products") is True
|
||||
assert FrontendDetector.is_storefront("orion.oms.lu", "/products") is True
|
||||
assert FrontendDetector.is_storefront("orion.omsflow.lu", "/products") is True
|
||||
assert FrontendDetector.is_storefront("localhost", "/admin/dashboard") is False
|
||||
|
||||
def test_is_platform(self):
|
||||
"""Test is_platform convenience method."""
|
||||
assert FrontendDetector.is_platform("localhost", "/") is True
|
||||
assert FrontendDetector.is_platform("oms.lu", "/pricing") is True
|
||||
assert FrontendDetector.is_platform("omsflow.lu", "/pricing") is True
|
||||
assert FrontendDetector.is_platform("localhost", "/admin/dashboard") is False
|
||||
|
||||
def test_is_api_request(self):
|
||||
@@ -201,46 +201,21 @@ class TestFrontendDetectorHelpers:
|
||||
assert FrontendDetector.is_api_request("/admin/dashboard") is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetFrontendTypeFunction:
|
||||
"""Test suite for get_frontend_type convenience function."""
|
||||
|
||||
def test_get_frontend_type_admin(self):
|
||||
"""Test get_frontend_type returns admin."""
|
||||
result = get_frontend_type("localhost", "/admin/dashboard")
|
||||
assert result == FrontendType.ADMIN
|
||||
|
||||
def test_get_frontend_type_store(self):
|
||||
"""Test get_frontend_type returns store."""
|
||||
result = get_frontend_type("localhost", "/store/settings")
|
||||
assert result == FrontendType.STORE
|
||||
|
||||
def test_get_frontend_type_storefront(self):
|
||||
"""Test get_frontend_type returns storefront."""
|
||||
result = get_frontend_type("localhost", "/storefront/products")
|
||||
assert result == FrontendType.STOREFRONT
|
||||
|
||||
def test_get_frontend_type_platform(self):
|
||||
"""Test get_frontend_type returns platform."""
|
||||
result = get_frontend_type("localhost", "/pricing")
|
||||
assert result == FrontendType.PLATFORM
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestReservedSubdomains:
|
||||
"""Test suite for reserved subdomain handling."""
|
||||
|
||||
def test_www_subdomain_not_storefront(self):
|
||||
"""Test that www subdomain is not treated as store storefront."""
|
||||
result = FrontendDetector.detect(host="www.oms.lu", path="/")
|
||||
result = FrontendDetector.detect(host="www.omsflow.lu", path="/")
|
||||
assert result == FrontendType.PLATFORM
|
||||
|
||||
def test_api_subdomain_not_storefront(self):
|
||||
"""Test that api subdomain is not treated as store storefront."""
|
||||
result = FrontendDetector.detect(host="api.oms.lu", path="/v1/products")
|
||||
result = FrontendDetector.detect(host="api.omsflow.lu", path="/v1/products")
|
||||
assert result == FrontendType.PLATFORM
|
||||
|
||||
def test_portal_subdomain_not_storefront(self):
|
||||
"""Test that portal subdomain is not treated as store storefront."""
|
||||
result = FrontendDetector.detect(host="portal.oms.lu", path="/")
|
||||
result = FrontendDetector.detect(host="portal.omsflow.lu", path="/")
|
||||
assert result == FrontendType.PLATFORM
|
||||
|
||||
@@ -80,7 +80,7 @@ class TestFrontendTypeMiddleware:
|
||||
|
||||
request = Mock(spec=Request)
|
||||
request.url = Mock(path="/products")
|
||||
request.headers = {"host": "orion.oms.lu"}
|
||||
request.headers = {"host": "orion.omsflow.lu"}
|
||||
mock_store = Mock()
|
||||
mock_store.name = "Test Store"
|
||||
request.state = Mock(clean_path="/products", store=mock_store)
|
||||
|
||||
@@ -13,7 +13,7 @@ Tests cover:
|
||||
URL Structure:
|
||||
- Main marketing site: localhost:9999/ (no prefix) -> 'main' platform
|
||||
- Platform sites: localhost:9999/platforms/{code}/ -> specific platform
|
||||
- Production: domain-based (oms.lu, loyalty.lu)
|
||||
- Production: domain-based (omsflow.lu, rewardflow.lu)
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
@@ -42,35 +42,35 @@ class TestPlatformContextManager:
|
||||
# ========================================================================
|
||||
|
||||
def test_detect_domain_based_platform(self):
|
||||
"""Test domain-based platform detection for production (e.g., oms.lu)."""
|
||||
"""Test domain-based platform detection for production (e.g., omsflow.lu)."""
|
||||
request = Mock(spec=Request)
|
||||
request.headers = {"host": "oms.lu"}
|
||||
request.headers = {"host": "omsflow.lu"}
|
||||
request.url = Mock(path="/pricing")
|
||||
|
||||
context = PlatformContextManager.detect_platform_context(request)
|
||||
|
||||
assert context is not None
|
||||
assert context["detection_method"] == "domain"
|
||||
assert context["domain"] == "oms.lu"
|
||||
assert context["host"] == "oms.lu"
|
||||
assert context["domain"] == "omsflow.lu"
|
||||
assert context["host"] == "omsflow.lu"
|
||||
assert context["original_path"] == "/pricing"
|
||||
|
||||
def test_detect_domain_with_port(self):
|
||||
"""Test domain detection with port number."""
|
||||
request = Mock(spec=Request)
|
||||
request.headers = {"host": "loyalty.lu:8443"}
|
||||
request.headers = {"host": "rewardflow.lu:8443"}
|
||||
request.url = Mock(path="/features")
|
||||
|
||||
context = PlatformContextManager.detect_platform_context(request)
|
||||
|
||||
assert context is not None
|
||||
assert context["detection_method"] == "domain"
|
||||
assert context["domain"] == "loyalty.lu"
|
||||
assert context["domain"] == "rewardflow.lu"
|
||||
|
||||
def test_detect_domain_three_level_not_detected(self):
|
||||
"""Test that three-level domains (subdomains) are not detected as platform domains."""
|
||||
request = Mock(spec=Request)
|
||||
request.headers = {"host": "store.oms.lu"}
|
||||
request.headers = {"host": "store.omsflow.lu"}
|
||||
request.url = Mock(path="/shop")
|
||||
|
||||
context = PlatformContextManager.detect_platform_context(request)
|
||||
@@ -295,7 +295,7 @@ class TestPlatformContextManager:
|
||||
|
||||
mock_db.query.return_value.filter.return_value.filter.return_value.first.return_value = mock_platform
|
||||
|
||||
context = {"detection_method": "domain", "domain": "oms.lu"}
|
||||
context = {"detection_method": "domain", "domain": "omsflow.lu"}
|
||||
|
||||
platform = PlatformContextManager.get_platform_from_context(mock_db, context)
|
||||
|
||||
@@ -398,7 +398,7 @@ class TestPlatformContextManager:
|
||||
request = Mock(spec=Request)
|
||||
request.url = Mock(path="/pricing")
|
||||
|
||||
context = {"detection_method": "domain", "domain": "oms.lu"}
|
||||
context = {"detection_method": "domain", "domain": "omsflow.lu"}
|
||||
|
||||
clean_path = PlatformContextManager.extract_clean_path(request, context)
|
||||
|
||||
@@ -598,7 +598,7 @@ class TestPlatformContextMiddleware:
|
||||
scope = {
|
||||
"type": "http",
|
||||
"path": "/pricing",
|
||||
"headers": [(b"host", b"oms.lu")],
|
||||
"headers": [(b"host", b"omsflow.lu")],
|
||||
}
|
||||
|
||||
receive = AsyncMock()
|
||||
@@ -918,7 +918,7 @@ class TestEdgeCases:
|
||||
|
||||
def test_admin_subdomain_with_production_domain(self):
|
||||
"""Test admin subdomain detection for production domains."""
|
||||
assert FrontendDetector.is_admin("admin.oms.lu", "/dashboard") is True
|
||||
assert FrontendDetector.is_admin("admin.omsflow.lu", "/dashboard") is True
|
||||
|
||||
def test_static_file_case_insensitive(self):
|
||||
"""Test static file detection is case-insensitive."""
|
||||
@@ -945,7 +945,7 @@ class TestURLRoutingSummary:
|
||||
- Main marketing: localhost:9999/ -> 'main' platform, path unchanged
|
||||
- OMS platform: localhost:9999/platforms/oms/pricing -> 'oms' platform, path=/pricing
|
||||
- Loyalty platform: localhost:9999/platforms/loyalty/features -> 'loyalty' platform, path=/features
|
||||
- Production OMS: oms.lu/pricing -> 'oms' platform, path=/pricing (no rewrite)
|
||||
- Production OMS: omsflow.lu/pricing -> 'oms' platform, path=/pricing (no rewrite)
|
||||
"""
|
||||
|
||||
def test_main_marketing_site_routing(self):
|
||||
@@ -987,11 +987,11 @@ class TestURLRoutingSummary:
|
||||
def test_production_domain_routing(self):
|
||||
"""Document: Production domains don't rewrite path."""
|
||||
request = Mock(spec=Request)
|
||||
request.headers = {"host": "oms.lu"}
|
||||
request.headers = {"host": "omsflow.lu"}
|
||||
request.url = Mock(path="/pricing")
|
||||
|
||||
context = PlatformContextManager.detect_platform_context(request)
|
||||
|
||||
assert context["detection_method"] == "domain"
|
||||
assert context["domain"] == "oms.lu"
|
||||
assert context["domain"] == "omsflow.lu"
|
||||
# clean_path not set for domain detection - uses original path
|
||||
|
||||
@@ -203,17 +203,17 @@ class TestAdminService:
|
||||
"""Test getting store statistics"""
|
||||
stats = stats_service.get_store_statistics(db)
|
||||
|
||||
assert "total_stores" in stats
|
||||
assert "active_stores" in stats
|
||||
assert "verified_stores" in stats
|
||||
assert "total" in stats
|
||||
assert "verified" in stats
|
||||
assert "pending" in stats
|
||||
assert "inactive" in stats
|
||||
assert "verification_rate" in stats
|
||||
|
||||
assert isinstance(stats["total_stores"], int)
|
||||
assert isinstance(stats["active_stores"], int)
|
||||
assert isinstance(stats["verified_stores"], int)
|
||||
assert isinstance(stats["total"], int)
|
||||
assert isinstance(stats["verified"], int)
|
||||
assert isinstance(stats["verification_rate"], int | float)
|
||||
|
||||
assert stats["total_stores"] >= 1
|
||||
assert stats["total"] >= 1
|
||||
|
||||
# Error Handling Tests
|
||||
def test_get_all_users_database_error(self, db_with_error, test_admin):
|
||||
@@ -259,9 +259,9 @@ class TestAdminService:
|
||||
"""Test store statistics when no stores exist"""
|
||||
stats = stats_service.get_store_statistics(empty_db)
|
||||
|
||||
assert stats["total_stores"] == 0
|
||||
assert stats["active_stores"] == 0
|
||||
assert stats["verified_stores"] == 0
|
||||
assert stats["total"] == 0
|
||||
assert stats["verified"] == 0
|
||||
assert stats["inactive"] == 0
|
||||
assert stats["verification_rate"] == 0
|
||||
|
||||
|
||||
|
||||
@@ -362,21 +362,20 @@ class TestStatsService:
|
||||
"""Test getting store statistics for admin dashboard."""
|
||||
stats = self.service.get_store_statistics(db)
|
||||
|
||||
assert "total_stores" in stats
|
||||
assert "active_stores" in stats
|
||||
assert "inactive_stores" in stats
|
||||
assert "verified_stores" in stats
|
||||
assert "total" in stats
|
||||
assert "verified" in stats
|
||||
assert "pending" in stats
|
||||
assert "inactive" in stats
|
||||
assert "verification_rate" in stats
|
||||
|
||||
assert stats["total_stores"] >= 1
|
||||
assert stats["active_stores"] >= 1
|
||||
assert stats["total"] >= 1
|
||||
|
||||
def test_get_store_statistics_calculates_rates(self, db, test_store):
|
||||
"""Test store statistics calculates rates correctly."""
|
||||
stats = self.service.get_store_statistics(db)
|
||||
|
||||
if stats["total_stores"] > 0:
|
||||
expected_rate = stats["verified_stores"] / stats["total_stores"] * 100
|
||||
if stats["total"] > 0:
|
||||
expected_rate = stats["verified"] / stats["total"] * 100
|
||||
assert abs(stats["verification_rate"] - expected_rate) < 0.01
|
||||
|
||||
def test_get_store_statistics_database_error(self, db):
|
||||
@@ -422,9 +421,9 @@ class TestStatsService:
|
||||
"""Test getting import statistics."""
|
||||
stats = self.service.get_import_statistics(db)
|
||||
|
||||
assert "total_imports" in stats
|
||||
assert "completed_imports" in stats
|
||||
assert "failed_imports" in stats
|
||||
assert "total" in stats
|
||||
assert "completed" in stats
|
||||
assert "failed" in stats
|
||||
assert "success_rate" in stats
|
||||
|
||||
def test_get_import_statistics_with_jobs(
|
||||
@@ -433,15 +432,15 @@ class TestStatsService:
|
||||
"""Test import statistics with existing jobs."""
|
||||
stats = self.service.get_import_statistics(db)
|
||||
|
||||
assert stats["total_imports"] >= 1
|
||||
assert stats["completed_imports"] >= 1 # test job has completed status
|
||||
assert stats["total"] >= 1
|
||||
assert stats["completed"] >= 1 # test job has completed status
|
||||
|
||||
def test_get_import_statistics_calculates_rate(self, db):
|
||||
"""Test import statistics calculates success rate."""
|
||||
stats = self.service.get_import_statistics(db)
|
||||
|
||||
if stats["total_imports"] > 0:
|
||||
expected_rate = stats["completed_imports"] / stats["total_imports"] * 100
|
||||
if stats["total"] > 0:
|
||||
expected_rate = stats["completed"] / stats["total"] * 100
|
||||
assert abs(stats["success_rate"] - expected_rate) < 0.01
|
||||
else:
|
||||
assert stats["success_rate"] == 0
|
||||
@@ -452,9 +451,9 @@ class TestStatsService:
|
||||
stats = self.service.get_import_statistics(db)
|
||||
|
||||
# Should return default values, not raise exception
|
||||
assert stats["total_imports"] == 0
|
||||
assert stats["completed_imports"] == 0
|
||||
assert stats["failed_imports"] == 0
|
||||
assert stats["total"] == 0
|
||||
assert stats["completed"] == 0
|
||||
assert stats["failed"] == 0
|
||||
assert stats["success_rate"] == 0
|
||||
|
||||
# ==================== Private Helper Method Tests ====================
|
||||
@@ -538,7 +537,6 @@ class TestStatsService:
|
||||
gtin=f"123456789{unique_id[:4]}",
|
||||
warehouse="strassen",
|
||||
bin_location=f"ST-{unique_id[:2]}-01",
|
||||
location=f"LOCATION2_{unique_id}",
|
||||
quantity=25,
|
||||
reserved_quantity=5,
|
||||
store_id=test_inventory.store_id,
|
||||
|
||||
Reference in New Issue
Block a user