Compare commits

...

2 Commits

Author SHA1 Message Date
77b76afb3f chore(deps): add pre-commit to dev requirements
Some checks failed
CI / ruff (push) Successful in 10s
CI / dependency-scanning (push) Has been cancelled
CI / audit (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / architecture (push) Has been cancelled
CI / pytest (push) Has started running
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 22:00:13 +01:00
8968e7d9cd refactor: remove backward compatibility code for pre-launch baseline
Clean up accumulated backward-compat shims, deprecated wrappers, unused
aliases, and legacy code across the codebase. Since the platform is not
live yet, this establishes a clean baseline.

Changes:
- Delete deprecated middleware/context.py (RequestContext, get_request_context)
- Remove unused factory get_store_email_settings_service()
- Remove deprecated pagination_full macro, /admin/platform-homepage route
- Remove ConversationResponse, InvoiceSettings* unprefixed aliases
- Simplify celery_config.py (remove empty LEGACY_TASK_MODULES)
- Standardize billing exceptions: *Error aliases → *Exception names
- Consolidate duplicate TierNotFoundError/FeatureNotFoundError classes
- Remove deprecated is_admin_request() from Store/PlatformContextManager
- Remove is_platform_default field, MediaUploadResponse legacy flat fields
- Remove MediaItemResponse.url alias, update JS to use file_url
- Update all affected tests and documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 21:58:59 +01:00
32 changed files with 174 additions and 779 deletions

View File

@@ -9,13 +9,6 @@ It includes:
- Task retry policies
- Sentry integration for error tracking
- Module-based task discovery (discovers tasks from app/modules/*/tasks/)
Task Discovery:
- Legacy tasks: Explicitly listed in the 'include' parameter
- Module tasks: Auto-discovered via discover_module_tasks()
As modules are migrated, their tasks will move from the legacy include list
to automatic discovery from the module's tasks/ directory.
"""
import logging
@@ -48,38 +41,27 @@ if SENTRY_DSN:
# =============================================================================
# TASK DISCOVERY
# =============================================================================
# Legacy tasks (will be migrated to modules over time)
# MIGRATION STATUS:
# - subscription: MIGRATED to billing module (kept for capture_capacity_snapshot -> monitoring)
# - marketplace, letzshop, export: MIGRATED to marketplace module
# - code_quality, test_runner: Will migrate to dev-tools module
LEGACY_TASK_MODULES: list[str] = [
# All legacy tasks have been migrated to their respective modules.
# Task discovery now happens via app.modules.tasks.discover_module_tasks()
]
def get_all_task_modules() -> list[str]:
"""
Get all task modules (legacy + module-based).
Get all task modules via module-based discovery.
Returns:
Combined list of legacy task modules and discovered module tasks
List of discovered module task packages
"""
all_modules = list(LEGACY_TASK_MODULES)
try:
from app.modules.tasks import discover_module_tasks
module_tasks = discover_module_tasks()
all_modules.extend(module_tasks)
logger.info(f"Discovered {len(module_tasks)} module task packages")
return module_tasks
except ImportError as e:
logger.warning(f"Could not import module task discovery: {e}")
except Exception as e:
logger.error(f"Error discovering module tasks: {e}")
return all_modules
return []
# Create Celery application

View File

@@ -19,24 +19,18 @@ from app.exceptions.base import (
__all__ = [
# Base billing exception
"BillingException",
"BillingServiceError", # Alias for backwards compatibility
# Subscription exceptions
"SubscriptionNotFoundException",
"NoActiveSubscriptionException",
"NoActiveSubscriptionError", # Alias for backwards compatibility
"SubscriptionNotCancelledException",
"SubscriptionNotCancelledError", # Alias for backwards compatibility
"SubscriptionAlreadyCancelledException",
# Tier exceptions
"TierNotFoundException",
"TierNotFoundError",
"TierLimitExceededException",
# Payment exceptions
"PaymentSystemNotConfiguredException",
"PaymentSystemNotConfiguredError", # Alias for backwards compatibility
"StripeNotConfiguredException",
"StripePriceNotConfiguredException",
"StripePriceNotConfiguredError", # Alias for backwards compatibility
"PaymentFailedException",
# Webhook exceptions
"InvalidWebhookSignatureException",
@@ -44,7 +38,6 @@ __all__ = [
"WebhookVerificationException",
# Feature exceptions
"FeatureNotFoundException",
"FeatureNotFoundError",
"FeatureNotAvailableException",
"InvalidFeatureCodesError",
]
@@ -62,10 +55,6 @@ class BillingException(BusinessLogicException):
super().__init__(message=message, error_code=error_code, details=details)
# Alias for backwards compatibility with billing_service.py
BillingServiceError = BillingException
# =============================================================================
# Subscription Exceptions
# =============================================================================
@@ -92,10 +81,6 @@ class NoActiveSubscriptionException(BusinessLogicException):
)
# Alias for backwards compatibility with billing_service.py
NoActiveSubscriptionError = NoActiveSubscriptionException
class SubscriptionNotCancelledException(BusinessLogicException):
"""Raised when trying to reactivate a subscription that is not cancelled."""
@@ -106,10 +91,6 @@ class SubscriptionNotCancelledException(BusinessLogicException):
)
# Alias for backwards compatibility with billing_service.py
SubscriptionNotCancelledError = SubscriptionNotCancelledException
class SubscriptionAlreadyCancelledException(BusinessLogicException):
"""Raised when trying to cancel an already cancelled subscription."""
@@ -138,18 +119,6 @@ class TierNotFoundException(ResourceNotFoundException):
self.tier_code = tier_code
class TierNotFoundError(ResourceNotFoundException):
"""Subscription tier not found (alternate naming)."""
def __init__(self, tier_code: str):
super().__init__(
resource_type="SubscriptionTier",
identifier=tier_code,
message=f"Tier '{tier_code}' not found",
)
self.tier_code = tier_code
class TierLimitExceededException(BillingException):
"""Raised when a tier limit is exceeded."""
@@ -180,10 +149,6 @@ class PaymentSystemNotConfiguredException(ServiceUnavailableException):
super().__init__(message="Payment system not configured")
# Alias for backwards compatibility with billing_service.py
PaymentSystemNotConfiguredError = PaymentSystemNotConfiguredException
class StripeNotConfiguredException(BillingException):
"""Raised when Stripe is not configured."""
@@ -206,10 +171,6 @@ class StripePriceNotConfiguredException(BusinessLogicException):
self.tier_code = tier_code
# Alias for backwards compatibility with billing_service.py
StripePriceNotConfiguredError = StripePriceNotConfiguredException
class PaymentFailedException(BillingException):
"""Raised when a payment fails."""
@@ -277,18 +238,6 @@ class FeatureNotFoundException(ResourceNotFoundException):
self.feature_code = feature_code
class FeatureNotFoundError(ResourceNotFoundException):
"""Feature not found (alternate naming)."""
def __init__(self, feature_code: str):
super().__init__(
resource_type="Feature",
identifier=feature_code,
message=f"Feature '{feature_code}' not found",
)
self.feature_code = feature_code
class FeatureNotAvailableException(BillingException):
"""Raised when a feature is not available in current tier."""

View File

@@ -24,7 +24,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_store_api, require_module_access
from app.core.database import get_db
from app.modules.billing.exceptions import FeatureNotFoundError
from app.modules.billing.exceptions import FeatureNotFoundException
from app.modules.billing.schemas.billing import (
CategoryListResponse,
FeatureCodeListResponse,
@@ -275,7 +275,7 @@ def get_feature_detail(
# Get feature declaration
decl = feature_aggregator.get_declaration(feature_code)
if not decl:
raise FeatureNotFoundError(feature_code)
raise FeatureNotFoundException(feature_code)
# Check availability
is_available = feature_service.has_feature(db, merchant_id, platform_id, feature_code)

View File

@@ -5,14 +5,6 @@ Billing module services.
Provides subscription management, Stripe integration, and admin operations.
"""
from app.modules.billing.exceptions import (
BillingServiceError,
NoActiveSubscriptionError,
PaymentSystemNotConfiguredError,
StripePriceNotConfiguredError,
SubscriptionNotCancelledError,
TierNotFoundError,
)
from app.modules.billing.services.admin_subscription_service import (
AdminSubscriptionService,
admin_subscription_service,
@@ -56,12 +48,6 @@ __all__ = [
"admin_subscription_service",
"BillingService",
"billing_service",
"BillingServiceError",
"PaymentSystemNotConfiguredError",
"TierNotFoundError",
"StripePriceNotConfiguredError",
"NoActiveSubscriptionError",
"SubscriptionNotCancelledError",
"FeatureService",
"feature_service",
"PlatformPricingService",

View File

@@ -16,12 +16,12 @@ from datetime import datetime
from sqlalchemy.orm import Session
from app.modules.billing.exceptions import (
BillingServiceError,
NoActiveSubscriptionError,
PaymentSystemNotConfiguredError,
StripePriceNotConfiguredError,
SubscriptionNotCancelledError,
TierNotFoundError,
BillingException,
NoActiveSubscriptionException,
PaymentSystemNotConfiguredException,
StripePriceNotConfiguredException,
SubscriptionNotCancelledException,
TierNotFoundException,
)
from app.modules.billing.models import (
AddOnProduct,
@@ -93,7 +93,7 @@ class BillingService:
Get a tier by its code.
Raises:
TierNotFoundError: If tier doesn't exist
TierNotFoundException: If tier doesn't exist
"""
tier = (
db.query(SubscriptionTier)
@@ -105,7 +105,7 @@ class BillingService:
)
if not tier:
raise TierNotFoundError(tier_code)
raise TierNotFoundException(tier_code)
return tier
@@ -126,12 +126,12 @@ class BillingService:
Dict with checkout_url and session_id
Raises:
PaymentSystemNotConfiguredError: If Stripe not configured
TierNotFoundError: If tier doesn't exist
StripePriceNotConfiguredError: If price not configured
PaymentSystemNotConfiguredException: If Stripe not configured
TierNotFoundException: If tier doesn't exist
StripePriceNotConfiguredException: If price not configured
"""
if not stripe_service.is_configured:
raise PaymentSystemNotConfiguredError()
raise PaymentSystemNotConfiguredException()
tier = self.get_tier_by_code(db, tier_code)
@@ -142,7 +142,7 @@ class BillingService:
)
if not price_id:
raise StripePriceNotConfiguredError(tier_code)
raise StripePriceNotConfiguredException(tier_code)
# Check if this is a new subscription (for trial)
existing_sub = subscription_service.get_merchant_subscription(
@@ -188,18 +188,18 @@ class BillingService:
Dict with portal_url
Raises:
PaymentSystemNotConfiguredError: If Stripe not configured
NoActiveSubscriptionError: If no subscription with customer ID
PaymentSystemNotConfiguredException: If Stripe not configured
NoActiveSubscriptionException: If no subscription with customer ID
"""
if not stripe_service.is_configured:
raise PaymentSystemNotConfiguredError()
raise PaymentSystemNotConfiguredException()
subscription = subscription_service.get_merchant_subscription(
db, merchant_id, platform_id
)
if not subscription or not subscription.stripe_customer_id:
raise NoActiveSubscriptionError()
raise NoActiveSubscriptionException()
session = stripe_service.create_portal_session(
customer_id=subscription.stripe_customer_id,
@@ -266,14 +266,14 @@ class BillingService:
Dict with message and effective_date
Raises:
NoActiveSubscriptionError: If no subscription to cancel
NoActiveSubscriptionException: If no subscription to cancel
"""
subscription = subscription_service.get_merchant_subscription(
db, merchant_id, platform_id
)
if not subscription or not subscription.stripe_subscription_id:
raise NoActiveSubscriptionError()
raise NoActiveSubscriptionException()
if stripe_service.is_configured:
stripe_service.cancel_subscription(
@@ -308,18 +308,18 @@ class BillingService:
Dict with success message
Raises:
NoActiveSubscriptionError: If no subscription
SubscriptionNotCancelledError: If not cancelled
NoActiveSubscriptionException: If no subscription
SubscriptionNotCancelledException: If not cancelled
"""
subscription = subscription_service.get_merchant_subscription(
db, merchant_id, platform_id
)
if not subscription or not subscription.stripe_subscription_id:
raise NoActiveSubscriptionError()
raise NoActiveSubscriptionException()
if not subscription.cancelled_at:
raise SubscriptionNotCancelledError()
raise SubscriptionNotCancelledException()
if stripe_service.is_configured:
stripe_service.reactivate_subscription(subscription.stripe_subscription_id)
@@ -339,14 +339,14 @@ class BillingService:
Dict with amount_due_cents, currency, next_payment_date, line_items
Raises:
NoActiveSubscriptionError: If no subscription with customer ID
NoActiveSubscriptionException: If no subscription with customer ID
"""
subscription = subscription_service.get_merchant_subscription(
db, merchant_id, platform_id
)
if not subscription or not subscription.stripe_customer_id:
raise NoActiveSubscriptionError()
raise NoActiveSubscriptionException()
if not stripe_service.is_configured:
return {
@@ -399,16 +399,16 @@ class BillingService:
Dict with message, new_tier, effective_immediately
Raises:
TierNotFoundError: If tier doesn't exist
NoActiveSubscriptionError: If no subscription
StripePriceNotConfiguredError: If price not configured
TierNotFoundException: If tier doesn't exist
NoActiveSubscriptionException: If no subscription
StripePriceNotConfiguredException: If price not configured
"""
subscription = subscription_service.get_merchant_subscription(
db, merchant_id, platform_id
)
if not subscription or not subscription.stripe_subscription_id:
raise NoActiveSubscriptionError()
raise NoActiveSubscriptionException()
tier = self.get_tier_by_code(db, new_tier_code)
@@ -419,7 +419,7 @@ class BillingService:
)
if not price_id:
raise StripePriceNotConfiguredError(new_tier_code)
raise StripePriceNotConfiguredException(new_tier_code)
# Update in Stripe
if stripe_service.is_configured:
@@ -472,11 +472,11 @@ class BillingService:
Dict with checkout_url and session_id
Raises:
PaymentSystemNotConfiguredError: If Stripe not configured
BillingServiceError: If addon doesn't exist
PaymentSystemNotConfiguredException: If Stripe not configured
BillingException: If addon doesn't exist
"""
if not stripe_service.is_configured:
raise PaymentSystemNotConfiguredError()
raise PaymentSystemNotConfiguredException()
addon = (
db.query(AddOnProduct)
@@ -488,10 +488,10 @@ class BillingService:
)
if not addon:
raise BillingServiceError(f"Add-on '{addon_code}' not found")
raise BillingException(f"Add-on '{addon_code}' not found")
if not addon.stripe_price_id:
raise BillingServiceError(f"Stripe price not configured for add-on '{addon_code}'")
raise BillingException(f"Stripe price not configured for add-on '{addon_code}'")
from app.modules.tenancy.models import Store
store = db.query(Store).filter(Store.id == store_id).first()
@@ -522,7 +522,7 @@ class BillingService:
Dict with message and addon_code
Raises:
BillingServiceError: If addon not found or not owned by store
BillingException: If addon not found or not owned by store
"""
store_addon = (
db.query(StoreAddOn)
@@ -534,7 +534,7 @@ class BillingService:
)
if not store_addon:
raise BillingServiceError("Add-on not found")
raise BillingException("Add-on not found")
addon_code = store_addon.addon_product.code

View File

@@ -6,6 +6,13 @@ from unittest.mock import MagicMock, patch
import pytest
from app.modules.billing.exceptions import (
NoActiveSubscriptionException,
PaymentSystemNotConfiguredException,
StripePriceNotConfiguredException,
SubscriptionNotCancelledException,
TierNotFoundException,
)
from app.modules.billing.models import (
AddOnProduct,
BillingHistory,
@@ -13,14 +20,7 @@ from app.modules.billing.models import (
SubscriptionStatus,
SubscriptionTier,
)
from app.modules.billing.services.billing_service import (
BillingService,
NoActiveSubscriptionError,
PaymentSystemNotConfiguredError,
StripePriceNotConfiguredError,
SubscriptionNotCancelledError,
TierNotFoundError,
)
from app.modules.billing.services.billing_service import BillingService
# ============================================================================
# Tier Lookup
@@ -41,17 +41,17 @@ class TestBillingServiceTiers:
assert tier.code == "essential"
def test_get_tier_by_code_not_found(self, db):
"""Nonexistent tier raises TierNotFoundError."""
with pytest.raises(TierNotFoundError) as exc_info:
"""Nonexistent tier raises TierNotFoundException."""
with pytest.raises(TierNotFoundException) as exc_info:
self.service.get_tier_by_code(db, "nonexistent")
assert exc_info.value.tier_code == "nonexistent"
def test_get_tier_by_code_inactive_not_returned(self, db, bs_tier_essential):
"""Inactive tier raises TierNotFoundError (only active tiers returned)."""
"""Inactive tier raises TierNotFoundException (only active tiers returned)."""
bs_tier_essential.is_active = False
db.flush()
with pytest.raises(TierNotFoundError):
with pytest.raises(TierNotFoundException):
self.service.get_tier_by_code(db, "essential")
@@ -249,8 +249,8 @@ class TestBillingServiceChangeTier:
self.service = BillingService()
def test_change_tier_no_subscription_raises(self, db, bs_tiers):
"""Raises NoActiveSubscriptionError when no subscription exists."""
with pytest.raises(NoActiveSubscriptionError):
"""Raises NoActiveSubscriptionException when no subscription exists."""
with pytest.raises(NoActiveSubscriptionException):
self.service.change_tier(db, 99999, 99999, "professional", False)
def test_change_tier_no_stripe_subscription_raises(
@@ -258,7 +258,7 @@ class TestBillingServiceChangeTier:
):
"""Raises when subscription has no stripe_subscription_id."""
# bs_subscription has no Stripe IDs
with pytest.raises(NoActiveSubscriptionError):
with pytest.raises(NoActiveSubscriptionException):
self.service.change_tier(
db,
bs_subscription.merchant_id,
@@ -270,8 +270,8 @@ class TestBillingServiceChangeTier:
def test_change_tier_nonexistent_tier_raises(
self, db, bs_stripe_subscription
):
"""Raises TierNotFoundError for nonexistent tier."""
with pytest.raises(TierNotFoundError):
"""Raises TierNotFoundException for nonexistent tier."""
with pytest.raises(TierNotFoundException):
self.service.change_tier(
db,
bs_stripe_subscription.merchant_id,
@@ -283,9 +283,9 @@ class TestBillingServiceChangeTier:
def test_change_tier_no_price_id_raises(
self, db, bs_stripe_subscription, bs_tiers
):
"""Raises StripePriceNotConfiguredError when tier has no Stripe price."""
"""Raises StripePriceNotConfiguredException when tier has no Stripe price."""
# bs_tiers have no stripe_price_* set
with pytest.raises(StripePriceNotConfiguredError):
with pytest.raises(StripePriceNotConfiguredException):
self.service.change_tier(
db,
bs_stripe_subscription.merchant_id,
@@ -382,12 +382,12 @@ class TestBillingServiceCancel:
def test_cancel_no_subscription_raises(self, db):
"""Raises when no subscription found."""
with pytest.raises(NoActiveSubscriptionError):
with pytest.raises(NoActiveSubscriptionException):
self.service.cancel_subscription(db, 99999, 99999, None, False)
def test_cancel_no_stripe_id_raises(self, db, bs_subscription):
"""Raises when subscription has no stripe_subscription_id."""
with pytest.raises(NoActiveSubscriptionError):
with pytest.raises(NoActiveSubscriptionException):
self.service.cancel_subscription(
db,
bs_subscription.merchant_id,
@@ -431,12 +431,12 @@ class TestBillingServiceReactivate:
def test_reactivate_no_subscription_raises(self, db):
"""Raises when no subscription found."""
with pytest.raises(NoActiveSubscriptionError):
with pytest.raises(NoActiveSubscriptionException):
self.service.reactivate_subscription(db, 99999, 99999)
def test_reactivate_not_cancelled_raises(self, db, bs_stripe_subscription):
"""Raises SubscriptionNotCancelledError when not cancelled."""
with pytest.raises(SubscriptionNotCancelledError):
"""Raises SubscriptionNotCancelledException when not cancelled."""
with pytest.raises(SubscriptionNotCancelledException):
self.service.reactivate_subscription(
db,
bs_stripe_subscription.merchant_id,
@@ -480,25 +480,25 @@ class TestBillingServiceCheckout:
self.service = BillingService()
def test_checkout_stripe_not_configured_raises(self, db, bs_tiers_with_stripe):
"""Raises PaymentSystemNotConfiguredError when Stripe is off."""
"""Raises PaymentSystemNotConfiguredException when Stripe is off."""
with patch(
"app.modules.billing.services.billing_service.stripe_service"
) as mock_stripe:
mock_stripe.is_configured = False
with pytest.raises(PaymentSystemNotConfiguredError):
with pytest.raises(PaymentSystemNotConfiguredException):
self.service.create_checkout_session(
db, 1, 1, "essential", False, "http://ok", "http://cancel"
)
def test_checkout_nonexistent_tier_raises(self, db):
"""Raises TierNotFoundError for nonexistent tier."""
"""Raises TierNotFoundException for nonexistent tier."""
with patch(
"app.modules.billing.services.billing_service.stripe_service"
) as mock_stripe:
mock_stripe.is_configured = True
with pytest.raises(TierNotFoundError):
with pytest.raises(TierNotFoundException):
self.service.create_checkout_session(
db, 1, 1, "nonexistent", False, "http://ok", "http://cancel"
)
@@ -518,23 +518,23 @@ class TestBillingServicePortal:
self.service = BillingService()
def test_portal_stripe_not_configured_raises(self, db):
"""Raises PaymentSystemNotConfiguredError when Stripe is off."""
"""Raises PaymentSystemNotConfiguredException when Stripe is off."""
with patch(
"app.modules.billing.services.billing_service.stripe_service"
) as mock_stripe:
mock_stripe.is_configured = False
with pytest.raises(PaymentSystemNotConfiguredError):
with pytest.raises(PaymentSystemNotConfiguredException):
self.service.create_portal_session(db, 1, 1, "http://return")
def test_portal_no_subscription_raises(self, db):
"""Raises NoActiveSubscriptionError when no subscription found."""
"""Raises NoActiveSubscriptionException when no subscription found."""
with patch(
"app.modules.billing.services.billing_service.stripe_service"
) as mock_stripe:
mock_stripe.is_configured = True
with pytest.raises(NoActiveSubscriptionError):
with pytest.raises(NoActiveSubscriptionException):
self.service.create_portal_session(db, 99999, 99999, "http://return")
def test_portal_no_customer_id_raises(self, db, bs_subscription):
@@ -544,7 +544,7 @@ class TestBillingServicePortal:
) as mock_stripe:
mock_stripe.is_configured = True
with pytest.raises(NoActiveSubscriptionError):
with pytest.raises(NoActiveSubscriptionException):
self.service.create_portal_session(
db,
bs_subscription.merchant_id,
@@ -568,12 +568,12 @@ class TestBillingServiceUpcomingInvoice:
def test_upcoming_invoice_no_subscription_raises(self, db):
"""Raises when no subscription exists."""
with pytest.raises(NoActiveSubscriptionError):
with pytest.raises(NoActiveSubscriptionException):
self.service.get_upcoming_invoice(db, 99999, 99999)
def test_upcoming_invoice_no_customer_id_raises(self, db, bs_subscription):
"""Raises when subscription has no stripe_customer_id."""
with pytest.raises(NoActiveSubscriptionError):
with pytest.raises(NoActiveSubscriptionException):
self.service.get_upcoming_invoice(
db, bs_subscription.merchant_id, bs_subscription.platform_id
)

View File

@@ -6,7 +6,7 @@ Admin pages for managing platform and store content pages.
"""
from fastapi import APIRouter, Depends, Path, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.api.deps import get_db, require_menu_access
@@ -22,22 +22,6 @@ router = APIRouter()
# ============================================================================
@router.get("/platform-homepage", include_in_schema=False)
async def admin_platform_homepage_manager(
request: Request,
current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)),
db: Session = Depends(get_db),
):
"""
Deprecated: Redirects to platforms page.
Platform homepages are now managed via:
- /admin/platforms → Select platform → Homepage button
- Or directly: /admin/content-pages?platform_code={code}&slug=home
"""
return RedirectResponse(url="/admin/platforms", status_code=302)
@router.get("/content-pages", response_class=HTMLResponse, include_in_schema=False)
async def admin_content_pages_list(
request: Request,

View File

@@ -90,7 +90,6 @@ class ContentPageResponse(BaseModel):
show_in_header: bool
show_in_legal: bool
is_platform_page: bool = False
is_platform_default: bool = False # Deprecated: use is_platform_page
is_store_default: bool = False
is_store_override: bool = False
page_tier: str | None = None

View File

@@ -37,7 +37,6 @@ class MediaItemResponse(BaseModel):
filename: str
original_filename: str | None = None
file_url: str
url: str | None = None # Alias for file_url for JS compatibility
thumbnail_url: str | None = None
media_type: str # image, video, document
mime_type: str | None = None
@@ -53,11 +52,6 @@ class MediaItemResponse(BaseModel):
model_config = {"from_attributes": True}
def model_post_init(self, __context: Any) -> None:
"""Set url from file_url if not provided."""
if self.url is None:
object.__setattr__(self, "url", self.file_url)
class MediaListResponse(BaseModel):
"""Paginated list of media items."""
@@ -80,13 +74,6 @@ class MediaUploadResponse(BaseModel):
success: bool = True
message: str | None = None
media: MediaItemResponse | None = None
# Legacy fields for backwards compatibility
id: int | None = None
file_url: str | None = None
thumbnail_url: str | None = None
filename: str | None = None
file_size: int | None = None
media_type: str | None = None
class UploadedFileInfo(BaseModel):

View File

@@ -15,7 +15,6 @@ from app.modules.cms.services.media_service import (
)
from app.modules.cms.services.store_email_settings_service import (
StoreEmailSettingsService,
get_store_email_settings_service, # Deprecated: use store_email_settings_service
store_email_settings_service,
)
from app.modules.cms.services.store_theme_service import (
@@ -32,5 +31,4 @@ __all__ = [
"store_theme_service",
"StoreEmailSettingsService",
"store_email_settings_service",
"get_store_email_settings_service", # Deprecated
]

View File

@@ -481,14 +481,3 @@ class StoreEmailSettingsService:
# Module-level service instance (singleton pattern)
store_email_settings_service = StoreEmailSettingsService()
# Deprecated: Factory function for backwards compatibility
def get_store_email_settings_service(db: Session) -> StoreEmailSettingsService:
"""
Factory function to get a StoreEmailSettingsService instance.
Deprecated: Use the singleton `store_email_settings_service` instead and pass
`db` to individual methods.
"""
return store_email_settings_service

View File

@@ -262,9 +262,9 @@ function mediaPickerMixin(storeIdGetter, multiSelect = false) {
*/
setMainImage(media) {
if (this.form) {
this.form.primary_image_url = media.url;
this.form.primary_image_url = media.file_url;
}
mediaPickerLog.info('Main image set:', media.url);
mediaPickerLog.info('Main image set:', media.file_url);
},
/**
@@ -272,13 +272,13 @@ function mediaPickerMixin(storeIdGetter, multiSelect = false) {
*/
addAdditionalImages(mediaList) {
if (this.form && Array.isArray(this.form.additional_images)) {
const newUrls = mediaList.map(m => m.url);
const newUrls = mediaList.map(m => m.file_url);
this.form.additional_images = [
...this.form.additional_images,
...newUrls
];
}
mediaPickerLog.info('Additional images added:', mediaList.map(m => m.url));
mediaPickerLog.info('Additional images added:', mediaList.map(m => m.file_url));
},
/**

View File

@@ -73,7 +73,7 @@ function storeContentPagesManager() {
// Platform pages - filter to only show actual platform defaults
const allPages = platformResponse.data || platformResponse || [];
this.platformPages = allPages.filter(p => p.is_platform_default);
this.platformPages = allPages.filter(p => p.is_platform_page);
// Store's custom pages (includes overrides)
this.customPages = storeResponse.data || storeResponse || [];

View File

@@ -34,7 +34,6 @@ from app.modules.messaging.schemas.message import (
ConversationCreate,
ConversationDetailResponse,
ConversationListResponse,
ConversationResponse,
ConversationSummary,
MarkReadResponse,
# Message schemas
@@ -90,7 +89,6 @@ __all__ = [
"ConversationSummary",
"ConversationDetailResponse",
"ConversationListResponse",
"ConversationResponse",
# Unread count
"UnreadCountResponse",
# Notification preferences

View File

@@ -192,10 +192,6 @@ class ConversationListResponse(BaseModel):
limit: int
# Backward compatibility alias
ConversationResponse = ConversationDetailResponse
# ============================================================================
# Unread Count Schemas
# ============================================================================

View File

@@ -21,10 +21,6 @@ from app.modules.orders.schemas.invoice import (
InvoiceResponse,
# Address schemas
InvoiceSellerDetails,
# Backward compatibility
InvoiceSettingsCreate,
InvoiceSettingsResponse,
InvoiceSettingsUpdate,
InvoiceStatsResponse,
InvoiceStatusUpdate,
# Invoice settings schemas
@@ -148,8 +144,4 @@ __all__ = [
# PDF
"InvoicePDFGeneratedResponse",
"InvoiceStatsResponse",
# Backward compatibility
"InvoiceSettingsCreate",
"InvoiceSettingsUpdate",
"InvoiceSettingsResponse",
]

View File

@@ -308,9 +308,3 @@ class InvoiceStatsResponse(BaseModel):
@property
def total_revenue(self) -> float:
return self.total_revenue_cents / 100
# Backward compatibility re-exports
InvoiceSettingsCreate = StoreInvoiceSettingsCreate
InvoiceSettingsUpdate = StoreInvoiceSettingsUpdate
InvoiceSettingsResponse = StoreInvoiceSettingsResponse

View File

@@ -61,7 +61,7 @@ class RouteInfo:
include_in_schema: bool = True
module_code: str = ""
route_type: str = "" # "api" or "pages"
frontend: str = "" # "admin", "store", "shop"
frontend: str = "" # "admin", "store", "storefront"
priority: int = 0 # Higher = registered later (for catch-all routes)
custom_prefix: str = "" # Custom prefix from ROUTE_CONFIG
@@ -76,7 +76,7 @@ def discover_module_routes() -> list[RouteInfo]:
Route discovery looks for:
- routes/api/admin.py -> admin API routes
- routes/api/store.py -> store API routes
- routes/api/shop.py -> shop API routes
- routes/api/storefront.py -> storefront API routes
- routes/pages/admin.py -> admin page routes
- routes/pages/store.py -> store page routes

View File

@@ -93,92 +93,6 @@
{{ pagination_simple() }}
#}
{#
Pagination Full Macro (First/Prev/Numbers/Next/Last)
=====================================================
⚠️ DEPRECATED: Use the standard 'pagination' macro instead.
This macro expects flat variables (total, skip, page, limit) but our Alpine.js
components use nested pagination objects (pagination.total, pagination.page, etc.).
Use:
{% from 'shared/macros/pagination.html' import pagination %}
{{ pagination(show_condition="!loading && pagination.total > 0") }}
---
Legacy documentation (for reference only):
Required Alpine.js data properties:
- page: Current page number
- total: Total number of items
- limit: Items per page
- skip: Current skip value (page - 1) * limit
- totalPages: Computed total pages (Math.ceil(total / limit))
Required Alpine.js methods:
- getPageNumbers(): Returns array of page numbers to display
- goToPage(pageNum): Go to specific page
- loadData(): Function to reload data (called internally as loadFn parameter)
#}
{% macro pagination_full(show_condition="total > limit", load_fn="loadData()", item_label="items") %}
<div x-show="{{ show_condition }}" class="flex flex-col sm:flex-row items-center justify-between gap-4 px-4 py-3 border-t dark:border-gray-700">
<span class="text-sm text-gray-600 dark:text-gray-400">
Showing <span x-text="skip + 1"></span>-<span x-text="Math.min(skip + limit, total)"></span> of <span x-text="total"></span> {{ item_label }}
</span>
<div class="flex items-center gap-1">
{# First Page #}
<button
@click="page = 1; {{ load_fn }}"
:disabled="page <= 1"
class="px-2 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="First page"
>
<span x-html="$icon('chevron-double-left', 'w-4 h-4')"></span>
</button>
{# Previous Page #}
<button
@click="page--; {{ load_fn }}"
:disabled="page <= 1"
class="px-2 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="Previous page"
>
<span x-html="$icon('chevron-left', 'w-4 h-4')"></span>
</button>
{# Page Numbers #}
<template x-for="p in getPageNumbers()" :key="p">
<button
@click="goToPage(p)"
class="px-3 py-1 text-sm font-medium rounded-md border transition-colors"
:class="p === page
? 'bg-purple-600 text-white border-purple-600 dark:bg-purple-500 dark:border-purple-500'
: 'text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600'"
x-text="p"
></button>
</template>
{# Next Page #}
<button
@click="page++; {{ load_fn }}"
:disabled="page >= totalPages"
class="px-2 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="Next page"
>
<span x-html="$icon('chevron-right', 'w-4 h-4')"></span>
</button>
{# Last Page #}
<button
@click="page = totalPages; {{ load_fn }}"
:disabled="page >= totalPages"
class="px-2 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="Last page"
>
<span x-html="$icon('chevron-double-right', 'w-4 h-4')"></span>
</button>
</div>
</div>
{% endmacro %}
{% macro pagination_simple(show_condition="true") %}
<div x-show="{{ show_condition }}" class="flex items-center justify-between px-4 py-3 bg-white border-t border-gray-200 dark:border-gray-700 dark:bg-gray-800">
<div class="flex items-center">

View File

@@ -172,41 +172,6 @@ if FrontendDetector.is_storefront(host, path, has_store_context=True):
| Custom domain | mybakery.lu | /products | STOREFRONT |
| Platform root | oms.lu | /pricing | PLATFORM |
## Migration from RequestContext
The previous `RequestContext` enum is deprecated. Here's the mapping:
| Old (RequestContext) | New (FrontendType) |
|---------------------|-------------------|
| `API` | Use `FrontendDetector.is_api_request()` + FrontendType |
| `ADMIN` | `FrontendType.ADMIN` |
| `STORE_DASHBOARD` | `FrontendType.STORE` |
| `SHOP` | `FrontendType.STOREFRONT` |
| `FALLBACK` | `FrontendType.PLATFORM` |
### Code Migration
**Before (deprecated):**
```python
from middleware.context import RequestContext, get_request_context
context = get_request_context(request)
if context == RequestContext.SHOP:
# Storefront logic
pass
```
**After:**
```python
from middleware.frontend_type import get_frontend_type
from app.modules.enums import FrontendType
frontend_type = get_frontend_type(request)
if frontend_type == FrontendType.STOREFRONT:
# Storefront logic
pass
```
## Request State
After `FrontendTypeMiddleware` runs, the following is available:
@@ -255,10 +220,9 @@ pytest tests/unit/core/test_frontend_detector.py tests/unit/middleware/test_fron
### DON'T
1. **Don't use `RequestContext`** - it's deprecated
2. **Don't duplicate path detection logic** - use FrontendDetector
3. **Don't hardcode path patterns** in middleware - they're centralized in FrontendDetector
4. **Don't check `request.state.context_type`** - use `request.state.frontend_type`
1. **Don't duplicate path detection logic** - use FrontendDetector
2. **Don't hardcode path patterns** in middleware - they're centralized in FrontendDetector
3. **Don't check `request.state.context_type`** - use `request.state.frontend_type`
## Architecture Rules

View File

@@ -186,19 +186,19 @@ All middleware components follow a consistent naming pattern for maintainability
```
✅ Good:
middleware/logging.py
middleware/context.py
middleware/store_context.py
middleware/auth.py
❌ Avoid:
middleware/logging_middleware.py
middleware/context_middleware.py
middleware/store_context_middleware.py
middleware/auth_middleware.py
```
**Rationale**:
- Keeps names concise and consistent
- Follows Django, Flask, and FastAPI conventions
- Makes imports cleaner: `from middleware.logging import LoggingMiddleware`
- Makes imports cleaner: `from middleware.store_context import StoreContextMiddleware`
- Reduces redundancy (the `middleware/` directory already indicates the purpose)
#### Test File Naming: Mirror the Source File
@@ -206,10 +206,9 @@ middleware/auth_middleware.py
Test files directly mirror the middleware filename with a `test_` prefix:
```
middleware/logging.py → tests/unit/middleware/test_logging.py
middleware/context.py → tests/unit/middleware/test_context.py
middleware/auth.py → tests/unit/middleware/test_auth.py
middleware/logging.py → tests/unit/middleware/test_logging.py
middleware/store_context.py → tests/unit/middleware/test_store_context.py
middleware/auth.py → tests/unit/middleware/test_auth.py
```
#### One Component Per File
@@ -229,19 +228,15 @@ class FrontendTypeMiddleware: # ASGI wrapper for frontend detection
class AuthManager: # Authentication logic
```
> **Note**: The old `middleware/context.py` with `ContextMiddleware` and `RequestContext` is deprecated.
> Use `FrontendTypeMiddleware` and `FrontendType` enum instead.
> See [Frontend Detection Architecture](frontend-detection.md) for migration guide.
#### One Test File Per Component
Follow the Single Responsibility Principle - each test file tests exactly one component:
```
✅ Good:
tests/unit/middleware/test_logging.py # Tests only LoggingMiddleware
tests/unit/middleware/test_context.py # Tests only ContextManager/Middleware
tests/unit/middleware/test_decorators.py # Tests only rate_limit decorator
tests/unit/middleware/test_logging.py # Tests only LoggingMiddleware
tests/unit/middleware/test_store_context.py # Tests only StoreContextManager/Middleware
tests/unit/middleware/test_decorators.py # Tests only rate_limit decorator
❌ Avoid:
tests/unit/middleware/test_all_middleware.py # Tests multiple components
@@ -261,12 +256,12 @@ When importing middleware components, use explicit imports:
```python
# ✅ Preferred - Explicit and clear
from middleware.logging import LoggingMiddleware
from middleware.context import ContextManager, RequestContext
from middleware.store_context import StoreContextManager
from middleware.auth import AuthManager
# ❌ Avoid - Less clear
from middleware import logging_middleware
from middleware import context_middleware
from middleware import store_context_middleware
```
**See**: [Complete Naming Conventions Guide](../development/naming-conventions.md) for project-wide standards.

View File

@@ -1,7 +1,7 @@
# Error Handling System - Developer Documentation
**Version:** 1.0.0
**Last Updated:** 2025
**Version:** 1.0.0
**Last Updated:** 2025
**Status:** Phase 1 Complete (Admin), Phase 2-3 Pending (Store, Shop)
---
@@ -120,12 +120,13 @@ class RequestContext(str, Enum):
**Usage:**
```python
from middleware.context_middleware import get_request_context, RequestContext
from middleware.frontend_type import get_frontend_type
from app.modules.enums import FrontendType
def my_handler(request: Request):
context = get_request_context(request)
if context == RequestContext.ADMIN:
frontend_type = get_frontend_type(request)
if frontend_type == FrontendType.ADMIN:
# Handle admin-specific logic
pass
```
@@ -285,7 +286,7 @@ API endpoints MUST always return JSON, even if the client sends `Accept: text/ht
### 2. HTML Error Pages (HTML Page Requests)
**When:**
**When:**
- NOT an API request
- GET request
- Accept header includes `text/html`
@@ -309,7 +310,7 @@ API endpoints MUST always return JSON, even if the client sends `Accept: text/ht
### 3. Login Redirects (401 Unauthorized)
**When:**
**When:**
- HTML page request
- 401 status code
@@ -628,7 +629,7 @@ message="SELECT * FROM products WHERE id=123 returned 0 rows"
error_code="PRODUCT_OUT_OF_STOCK"
error_code="PAYMENT_PROCESSING_FAILED"
# Bad
# Bad
error_code="error1"
error_code="ProductOutOfStockException"
```
@@ -789,25 +790,18 @@ raise ServiceUnavailableException("Maintenance in progress")
**Test Context Detection:**
```python
# tests/test_context_middleware.py
from middleware.context_middleware import ContextManager, RequestContext
from fastapi import Request
# tests/test_frontend_detector.py
from app.core.frontend_detector import FrontendDetector
from app.modules.enums import FrontendType
def test_api_context_detection():
request = MockRequest(path="/api/v1/stores")
context = ContextManager.detect_context(request)
assert context == RequestContext.API
def test_admin_detection():
assert FrontendDetector.is_admin("localhost", "/admin/dashboard") is True
def test_admin_context_detection():
request = MockRequest(path="/admin/dashboard")
context = ContextManager.detect_context(request)
assert context == RequestContext.ADMIN
def test_shop_context_detection():
request = MockRequest(path="/products")
request.state.store = MockStore(id=1, name="Test Store")
context = ContextManager.detect_context(request)
assert context == RequestContext.SHOP
def test_storefront_detection():
frontend_type = FrontendDetector.detect(
host="localhost", path="/storefront/products", has_store_context=True
)
assert frontend_type == FrontendType.STOREFRONT
```
**Test Error Renderer:**
@@ -966,7 +960,7 @@ def test_error_page_performance(client: TestClient):
)
assert response.status_code == 404
duration = time.time() - start
# Should complete in reasonable time (< 5 seconds)
assert duration < 5.0
print(f"100 error pages rendered in {duration:.2f}s")
@@ -1196,45 +1190,26 @@ if context_type == RequestContext.SHOP:
## API Reference
### Context Detection
### Frontend Type Detection
#### `ContextManager.detect_context(request: Request) -> RequestContext`
#### `get_frontend_type(request: Request) -> FrontendType`
Detects the request context type.
Gets the frontend type for the current request.
**Parameters:**
- `request` (Request): FastAPI request object
**Returns:**
- RequestContext enum value
- FrontendType enum value
**Example:**
```python
from middleware.context_middleware import ContextManager, RequestContext
context = ContextManager.detect_context(request)
if context == RequestContext.ADMIN:
# Handle admin-specific logic
pass
```
#### `get_request_context(request: Request) -> RequestContext`
Helper function to get current request context from request state.
**Parameters:**
- `request` (Request): FastAPI request object
**Returns:**
- RequestContext enum value (defaults to FALLBACK if not set)
**Example:**
```python
from middleware.context_middleware import get_request_context, RequestContext
from middleware.frontend_type import get_frontend_type
from app.modules.enums import FrontendType
def my_handler(request: Request):
context = get_request_context(request)
return {"context": context.value}
frontend_type = get_frontend_type(request)
return {"frontend_type": frontend_type.value}
```
### Error Rendering
@@ -1398,28 +1373,28 @@ STATUS_CODE_MESSAGES = {
## Best Practices Summary
### DO:
✅ Use specific exception classes for different error scenarios
✅ Provide clear, user-friendly error messages
✅ Include relevant details in the `details` dictionary
✅ Use consistent error codes across the application
✅ Test both API and HTML responses
✅ Keep error templates simple and accessible
✅ Use debug mode responsibly (admin only)
✅ Follow the template inheritance pattern
✅ Document any new exception types
✅ Test error pages manually in browsers
✅ Use specific exception classes for different error scenarios
✅ Provide clear, user-friendly error messages
✅ Include relevant details in the `details` dictionary
✅ Use consistent error codes across the application
✅ Test both API and HTML responses
✅ Keep error templates simple and accessible
✅ Use debug mode responsibly (admin only)
✅ Follow the template inheritance pattern
✅ Document any new exception types
✅ Test error pages manually in browsers
### DON'T:
❌ Expose sensitive information in error messages
❌ Use generic exceptions for domain-specific errors
❌ Return HTML for API endpoints
❌ Skip the Accept header check
❌ Hardcode HTML in exception handlers
❌ Forget to add fallback templates
❌ Show technical details to customers
❌ Use complex JavaScript in error pages
❌ Forget to test 401 redirects
❌ Mix API and page response logic
❌ Expose sensitive information in error messages
❌ Use generic exceptions for domain-specific errors
❌ Return HTML for API endpoints
❌ Skip the Accept header check
❌ Hardcode HTML in exception handlers
❌ Forget to add fallback templates
❌ Show technical details to customers
❌ Use complex JavaScript in error pages
❌ Forget to test 401 redirects
❌ Mix API and page response logic
---
@@ -1435,7 +1410,7 @@ For questions or issues with the error handling system:
---
**Document Version:** 1.0.0
**Last Updated:** 2025
**Maintained By:** [Your team name]
**Document Version:** 1.0.0
**Last Updated:** 2025
**Maintained By:** [Your team name]
**Next Review:** After Phase 2 & 3 completion

View File

@@ -160,14 +160,14 @@ Response:
"slug": "about",
"title": "About Wizamart", // Store override
"is_store_override": true,
"is_platform_default": false
"is_platform_page": false
},
{
"id": 2,
"slug": "shipping",
"title": "Shipping Information", // Platform default
"is_store_override": false,
"is_platform_default": true
"is_platform_page": true
}
]
```

View File

@@ -582,11 +582,11 @@ Custom exceptions for billing operations (`app/modules/billing/exceptions.py`):
| Exception | HTTP Status | Description |
|-----------|-------------|-------------|
| `PaymentSystemNotConfiguredError` | 503 | Stripe not configured |
| `TierNotFoundError` | 404 | Invalid tier code |
| `StripePriceNotConfiguredError` | 400 | No Stripe price for tier |
| `NoActiveSubscriptionError` | 400 | Operation requires subscription |
| `SubscriptionNotCancelledError` | 400 | Cannot reactivate active subscription |
| `PaymentSystemNotConfiguredException` | 503 | Stripe not configured |
| `TierNotFoundException` | 404 | Invalid tier code |
| `StripePriceNotConfiguredException` | 400 | No Stripe price for tier |
| `NoActiveSubscriptionException` | 400 | Operation requires subscription |
| `SubscriptionNotCancelledException` | 400 | Cannot reactivate active subscription |
## Testing

View File

@@ -1,100 +0,0 @@
# middleware/context.py
"""
DEPRECATED: This module is deprecated in favor of middleware/frontend_type.py
The RequestContext enum and ContextMiddleware have been replaced by:
- FrontendType enum (app/modules/enums.py)
- FrontendTypeMiddleware (middleware/frontend_type.py)
- FrontendDetector (app/core/frontend_detector.py)
This file is kept for backwards compatibility during the migration period.
All new code should use FrontendType and FrontendTypeMiddleware instead.
Migration guide:
- RequestContext.API -> Check with FrontendDetector.is_api_request()
- RequestContext.ADMIN -> FrontendType.ADMIN
- RequestContext.STORE_DASHBOARD -> FrontendType.STORE
- RequestContext.STOREFRONT -> FrontendType.STOREFRONT
- RequestContext.FALLBACK -> FrontendType.PLATFORM (or handle API separately)
- get_request_context(request) -> get_frontend_type(request)
- request.state.context_type -> request.state.frontend_type
"""
import logging
import warnings
from enum import Enum
from fastapi import Request
from app.modules.enums import FrontendType
from middleware.frontend_type import get_frontend_type
logger = logging.getLogger(__name__)
class RequestContext(str, Enum):
"""
DEPRECATED: Use FrontendType enum instead.
Request context types for the application.
This enum is kept for backwards compatibility.
Migration:
- API -> Use FrontendDetector.is_api_request() + FrontendType
- ADMIN -> FrontendType.ADMIN
- STORE_DASHBOARD -> FrontendType.STORE
- STOREFRONT -> FrontendType.STOREFRONT
- FALLBACK -> FrontendType.PLATFORM
"""
API = "api"
ADMIN = "admin"
STORE_DASHBOARD = "store"
STOREFRONT = "storefront"
FALLBACK = "fallback"
def get_request_context(request: Request) -> RequestContext:
"""
DEPRECATED: Use get_frontend_type() from middleware.frontend_type instead.
Helper function to get current request context.
This function maps FrontendType to RequestContext for backwards compatibility.
Args:
request: FastAPI request object
Returns:
RequestContext enum value (defaults to FALLBACK if not set)
"""
warnings.warn(
"get_request_context() is deprecated. Use get_frontend_type() from "
"middleware.frontend_type instead.",
DeprecationWarning,
stacklevel=2,
)
# Get the new frontend_type
frontend_type = get_frontend_type(request)
# Map FrontendType to RequestContext for backwards compatibility
mapping = {
FrontendType.ADMIN: RequestContext.ADMIN,
FrontendType.STORE: RequestContext.STORE_DASHBOARD,
FrontendType.STOREFRONT: RequestContext.STOREFRONT,
FrontendType.PLATFORM: RequestContext.FALLBACK,
}
# Check if it's an API request
if request.url.path.startswith("/api/"):
return RequestContext.API
return mapping.get(frontend_type, RequestContext.FALLBACK)
# ContextManager and ContextMiddleware are removed.
# They have been replaced by FrontendDetector and FrontendTypeMiddleware.
# Import from the new locations:
# from app.core.frontend_detector import FrontendDetector
# from middleware.frontend_type import FrontendTypeMiddleware, get_frontend_type

View File

@@ -257,18 +257,6 @@ class PlatformContextManager:
# For domain-based or default, path remains unchanged
return request.url.path
@staticmethod
def is_admin_request(request: Request) -> bool:
"""
Check if request is for admin interface.
DEPRECATED: Use FrontendDetector.is_admin() instead.
Kept for backwards compatibility.
"""
host = request.headers.get("host", "")
path = request.url.path
return FrontendDetector.is_admin(host, path)
@staticmethod
def is_static_file_request(request: Request) -> bool:
"""Check if request is for static files."""
@@ -478,16 +466,6 @@ class PlatformContextMiddleware:
return True
return "favicon.ico" in path_lower
def _is_admin_request(self, path: str, host: str) -> bool:
"""
Check if request is for admin interface.
DEPRECATED: Use FrontendDetector.is_admin() instead.
Kept for backwards compatibility.
"""
return FrontendDetector.is_admin(host, path)
def get_current_platform(request: Request) -> Platform | None:
"""Helper function to get current platform from request state."""
return getattr(request.state, "platform", None)

View File

@@ -222,18 +222,6 @@ class StoreContextManager:
return request.url.path
@staticmethod
def is_admin_request(request: Request) -> bool:
"""
Check if request is for admin interface.
DEPRECATED: Use FrontendDetector.is_admin() instead.
Kept for backwards compatibility.
"""
host = request.headers.get("host", "")
path = request.url.path
return FrontendDetector.is_admin(host, path)
@staticmethod
def is_api_request(request: Request) -> bool:
"""Check if request is for API endpoints."""
@@ -430,7 +418,7 @@ class StoreContextMiddleware(BaseHTTPMiddleware):
"""
# Skip store detection for admin, static files, and system requests
if (
StoreContextManager.is_admin_request(request)
FrontendDetector.is_admin(request.headers.get("host", ""), request.url.path)
or StoreContextManager.is_static_file_request(request)
or request.url.path in ["/", "/health", "/docs", "/redoc", "/openapi.json"]
):

View File

@@ -8,8 +8,5 @@ isort>=5.13.2
ruff==0.8.4 # Pinned — must match pre-commit hook and local version
mypy>=1.13.0
# Optional: More advanced linting (if you prefer flake8 over ruff)
# Note: flake8 itself works with 3.13, but some plugins may not
# flake8>=7.1.0
# flake8-docstrings>=1.7.0
# flake8-import-order>=0.18.2
# Pre-commit hooks
pre-commit>=3.6.0

View File

@@ -377,19 +377,6 @@ async def test_clean_path_context(request: Request):
}
@api_router.get("/enum")
async def test_api_enum(request: Request):
"""Test context enum instance."""
from middleware.context import RequestContext
context = getattr(request.state, "context_type", None)
return {
"is_enum": isinstance(context, RequestContext) if context else False,
"enum_name": context.name if context else None,
"enum_value": context.value if context else None,
}
@api_router.get("/theme")
async def test_api_theme(request: Request):
"""Test theme in API context."""

View File

@@ -1,131 +0,0 @@
# tests/unit/middleware/test_context.py
"""
DEPRECATED: Tests for backward compatibility of middleware.context module.
The ContextMiddleware and ContextManager classes have been replaced by:
- FrontendTypeMiddleware (middleware/frontend_type.py)
- FrontendDetector (app/core/frontend_detector.py)
These tests verify the backward compatibility layer still works for code
that uses the deprecated RequestContext enum and get_request_context() function.
For new tests, see:
- tests/unit/core/test_frontend_detector.py
- tests/unit/middleware/test_frontend_type.py
"""
import warnings
from unittest.mock import Mock
import pytest
from fastapi import Request
from middleware.context import RequestContext, get_request_context
@pytest.mark.unit
class TestRequestContextEnumBackwardCompatibility:
"""Test suite for deprecated RequestContext enum."""
def test_request_context_values(self):
"""Test RequestContext enum has correct values."""
assert RequestContext.API.value == "api"
assert RequestContext.ADMIN.value == "admin"
assert RequestContext.STORE_DASHBOARD.value == "store"
assert RequestContext.STOREFRONT.value == "storefront"
assert RequestContext.FALLBACK.value == "fallback"
def test_request_context_types(self):
"""Test RequestContext enum values are strings."""
for context in RequestContext:
assert isinstance(context.value, str)
@pytest.mark.unit
class TestGetRequestContextBackwardCompatibility:
"""Test suite for deprecated get_request_context() function."""
def test_get_request_context_returns_api_for_api_paths(self):
"""Test get_request_context returns API for /api/ paths."""
request = Mock(spec=Request)
request.url = Mock(path="/api/v1/stores")
request.state = Mock()
request.state.frontend_type = None
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
context = get_request_context(request)
assert context == RequestContext.API
def test_get_request_context_deprecation_warning(self):
"""Test get_request_context raises DeprecationWarning."""
from app.modules.enums import FrontendType
request = Mock(spec=Request)
request.url = Mock(path="/admin/dashboard")
request.state = Mock()
request.state.frontend_type = FrontendType.ADMIN
with pytest.warns(DeprecationWarning, match="get_request_context.*deprecated"):
get_request_context(request)
def test_get_request_context_maps_admin(self):
"""Test get_request_context maps FrontendType.ADMIN to RequestContext.ADMIN."""
from app.modules.enums import FrontendType
request = Mock(spec=Request)
request.url = Mock(path="/admin/dashboard")
request.state = Mock()
request.state.frontend_type = FrontendType.ADMIN
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
context = get_request_context(request)
assert context == RequestContext.ADMIN
def test_get_request_context_maps_store(self):
"""Test get_request_context maps FrontendType.STORE to RequestContext.STORE_DASHBOARD."""
from app.modules.enums import FrontendType
request = Mock(spec=Request)
request.url = Mock(path="/store/settings")
request.state = Mock()
request.state.frontend_type = FrontendType.STORE
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
context = get_request_context(request)
assert context == RequestContext.STORE_DASHBOARD
def test_get_request_context_maps_storefront(self):
"""Test get_request_context maps FrontendType.STOREFRONT to RequestContext.STOREFRONT."""
from app.modules.enums import FrontendType
request = Mock(spec=Request)
request.url = Mock(path="/storefront/products")
request.state = Mock()
request.state.frontend_type = FrontendType.STOREFRONT
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
context = get_request_context(request)
assert context == RequestContext.STOREFRONT
def test_get_request_context_maps_platform_to_fallback(self):
"""Test get_request_context maps FrontendType.PLATFORM to RequestContext.FALLBACK."""
from app.modules.enums import FrontendType
request = Mock(spec=Request)
request.url = Mock(path="/pricing")
request.state = Mock()
request.state.frontend_type = FrontendType.PLATFORM
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
context = get_request_context(request)
assert context == RequestContext.FALLBACK

View File

@@ -22,6 +22,7 @@ import pytest
from fastapi import Request
from sqlalchemy.orm import Session
from app.core.frontend_detector import FrontendDetector
from middleware.platform_context import (
DEFAULT_PLATFORM_CODE,
PlatformContextManager,
@@ -210,7 +211,7 @@ class TestPlatformContextManager:
request.headers = {"host": "admin.localhost"}
request.url = Mock(path="/dashboard")
assert PlatformContextManager.is_admin_request(request) is True
assert FrontendDetector.is_admin("admin.localhost", "/dashboard") is True
context = PlatformContextManager.detect_platform_context(request)
assert context is None
@@ -221,26 +222,18 @@ class TestPlatformContextManager:
request.headers = {"host": "localhost"}
request.url = Mock(path="/admin/stores")
assert PlatformContextManager.is_admin_request(request) is True
assert FrontendDetector.is_admin("localhost", "/admin/stores") is True
context = PlatformContextManager.detect_platform_context(request)
assert context is None
def test_skip_admin_path_with_port(self):
"""Test admin detection with port in host."""
request = Mock(spec=Request)
request.headers = {"host": "admin.localhost:9999"}
request.url = Mock(path="/dashboard")
assert PlatformContextManager.is_admin_request(request) is True
assert FrontendDetector.is_admin("admin.localhost:9999", "/dashboard") is True
def test_not_admin_regular_path(self):
"""Test non-admin path is not detected as admin."""
request = Mock(spec=Request)
request.headers = {"host": "localhost"}
request.url = Mock(path="/shop/products")
assert PlatformContextManager.is_admin_request(request) is False
assert FrontendDetector.is_admin("localhost", "/shop/products") is False
# ========================================================================
# Static File Detection Tests
@@ -925,11 +918,7 @@ class TestEdgeCases:
def test_admin_subdomain_with_production_domain(self):
"""Test admin subdomain detection for production domains."""
request = Mock(spec=Request)
request.headers = {"host": "admin.oms.lu"}
request.url = Mock(path="/dashboard")
assert PlatformContextManager.is_admin_request(request) is True
assert FrontendDetector.is_admin("admin.oms.lu", "/dashboard") is True
def test_static_file_case_insensitive(self):
"""Test static file detection is case-insensitive."""

View File

@@ -17,6 +17,7 @@ import pytest
from fastapi import Request
from sqlalchemy.orm import Session
from app.core.frontend_detector import FrontendDetector
from app.modules.tenancy.exceptions import StoreNotFoundException
from middleware.store_context import (
StoreContextManager,
@@ -372,35 +373,19 @@ class TestStoreContextManager:
def test_is_admin_request_admin_subdomain(self):
"""Test admin request detection from subdomain."""
request = Mock(spec=Request)
request.headers = {"host": "admin.platform.com"}
request.url = Mock(path="/dashboard")
assert StoreContextManager.is_admin_request(request) is True
assert FrontendDetector.is_admin("admin.platform.com", "/dashboard") is True
def test_is_admin_request_admin_path(self):
"""Test admin request detection from path."""
request = Mock(spec=Request)
request.headers = {"host": "localhost"}
request.url = Mock(path="/admin/dashboard")
assert StoreContextManager.is_admin_request(request) is True
assert FrontendDetector.is_admin("localhost", "/admin/dashboard") is True
def test_is_admin_request_with_port(self):
"""Test admin request detection with port number."""
request = Mock(spec=Request)
request.headers = {"host": "admin.localhost:8000"}
request.url = Mock(path="/dashboard")
assert StoreContextManager.is_admin_request(request) is True
assert FrontendDetector.is_admin("admin.localhost:8000", "/dashboard") is True
def test_is_not_admin_request(self):
"""Test non-admin request."""
request = Mock(spec=Request)
request.headers = {"host": "store1.platform.com"}
request.url = Mock(path="/storefront")
assert StoreContextManager.is_admin_request(request) is False
assert FrontendDetector.is_admin("store1.platform.com", "/storefront") is False
def test_is_api_request(self):
"""Test API request detection."""
@@ -599,7 +584,7 @@ class TestStoreContextMiddleware:
call_next = AsyncMock(return_value=Mock())
with patch.object(StoreContextManager, "is_admin_request", return_value=True):
with patch.object(FrontendDetector, "is_admin", return_value=True):
await middleware.dispatch(request, call_next)
assert request.state.store is None
@@ -775,7 +760,7 @@ class TestStoreContextMiddleware:
call_next = AsyncMock(return_value=Mock())
with (
patch.object(StoreContextManager, "is_admin_request", return_value=False),
patch.object(FrontendDetector, "is_admin", return_value=False),
patch.object(
StoreContextManager, "is_static_file_request", return_value=False
),