From 8968e7d9cdc04446e0ecf31ab4487a370d049123 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Fri, 13 Feb 2026 21:58:59 +0100 Subject: [PATCH] refactor: remove backward compatibility code for pre-launch baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/core/celery_config.py | 26 +--- app/modules/billing/exceptions.py | 51 ------- .../billing/routes/api/store_features.py | 4 +- app/modules/billing/services/__init__.py | 14 -- .../billing/services/billing_service.py | 74 +++++----- .../tests/unit/test_billing_service.py | 70 +++++----- app/modules/cms/routes/pages/admin.py | 18 +-- app/modules/cms/schemas/content_page.py | 1 - app/modules/cms/schemas/media.py | 13 -- app/modules/cms/services/__init__.py | 2 - .../services/store_email_settings_service.py | 11 -- .../cms/static/shared/js/media-picker.js | 8 +- .../cms/static/store/js/content-pages.js | 2 +- app/modules/messaging/schemas/__init__.py | 2 - app/modules/messaging/schemas/message.py | 4 - app/modules/orders/schemas/__init__.py | 8 -- app/modules/orders/schemas/invoice.py | 6 - app/modules/routes.py | 4 +- app/templates/shared/macros/pagination.html | 86 ------------ docs/architecture/frontend-detection.md | 42 +----- docs/architecture/middleware.md | 25 ++-- ...error-rendering-developer-documentation.md | 129 +++++++---------- docs/features/content-management-system.md | 4 +- docs/features/subscription-billing.md | 10 +- middleware/context.py | 100 ------------- middleware/platform_context.py | 22 --- middleware/store_context.py | 14 +- .../middleware/middleware_test_routes.py | 13 -- tests/unit/middleware/test_context.py | 131 ------------------ .../unit/middleware/test_platform_context.py | 23 +-- tests/unit/middleware/test_store_context.py | 29 +--- 31 files changed, 172 insertions(+), 774 deletions(-) delete mode 100644 middleware/context.py delete mode 100644 tests/unit/middleware/test_context.py diff --git a/app/core/celery_config.py b/app/core/celery_config.py index 956f2768..8db35e9e 100644 --- a/app/core/celery_config.py +++ b/app/core/celery_config.py @@ -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 diff --git a/app/modules/billing/exceptions.py b/app/modules/billing/exceptions.py index 61f78bbd..9dcf2a7f 100644 --- a/app/modules/billing/exceptions.py +++ b/app/modules/billing/exceptions.py @@ -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.""" diff --git a/app/modules/billing/routes/api/store_features.py b/app/modules/billing/routes/api/store_features.py index ef5c9f31..e2d9da22 100644 --- a/app/modules/billing/routes/api/store_features.py +++ b/app/modules/billing/routes/api/store_features.py @@ -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) diff --git a/app/modules/billing/services/__init__.py b/app/modules/billing/services/__init__.py index 281d4fe5..5bf6a6d5 100644 --- a/app/modules/billing/services/__init__.py +++ b/app/modules/billing/services/__init__.py @@ -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", diff --git a/app/modules/billing/services/billing_service.py b/app/modules/billing/services/billing_service.py index f7db848e..51d65e18 100644 --- a/app/modules/billing/services/billing_service.py +++ b/app/modules/billing/services/billing_service.py @@ -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 diff --git a/app/modules/billing/tests/unit/test_billing_service.py b/app/modules/billing/tests/unit/test_billing_service.py index cd9a926a..8aa39ee1 100644 --- a/app/modules/billing/tests/unit/test_billing_service.py +++ b/app/modules/billing/tests/unit/test_billing_service.py @@ -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 ) diff --git a/app/modules/cms/routes/pages/admin.py b/app/modules/cms/routes/pages/admin.py index 37b239a2..ac0d17d6 100644 --- a/app/modules/cms/routes/pages/admin.py +++ b/app/modules/cms/routes/pages/admin.py @@ -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, diff --git a/app/modules/cms/schemas/content_page.py b/app/modules/cms/schemas/content_page.py index a545fc98..6f89fa60 100644 --- a/app/modules/cms/schemas/content_page.py +++ b/app/modules/cms/schemas/content_page.py @@ -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 diff --git a/app/modules/cms/schemas/media.py b/app/modules/cms/schemas/media.py index 91dcd2bc..217970e8 100644 --- a/app/modules/cms/schemas/media.py +++ b/app/modules/cms/schemas/media.py @@ -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): diff --git a/app/modules/cms/services/__init__.py b/app/modules/cms/services/__init__.py index 8acad576..5ccc3f99 100644 --- a/app/modules/cms/services/__init__.py +++ b/app/modules/cms/services/__init__.py @@ -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 ] diff --git a/app/modules/cms/services/store_email_settings_service.py b/app/modules/cms/services/store_email_settings_service.py index e5582800..2db3b303 100644 --- a/app/modules/cms/services/store_email_settings_service.py +++ b/app/modules/cms/services/store_email_settings_service.py @@ -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 diff --git a/app/modules/cms/static/shared/js/media-picker.js b/app/modules/cms/static/shared/js/media-picker.js index 35aa26da..73e8283c 100644 --- a/app/modules/cms/static/shared/js/media-picker.js +++ b/app/modules/cms/static/shared/js/media-picker.js @@ -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)); }, /** diff --git a/app/modules/cms/static/store/js/content-pages.js b/app/modules/cms/static/store/js/content-pages.js index 45d32078..a8d5d717 100644 --- a/app/modules/cms/static/store/js/content-pages.js +++ b/app/modules/cms/static/store/js/content-pages.js @@ -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 || []; diff --git a/app/modules/messaging/schemas/__init__.py b/app/modules/messaging/schemas/__init__.py index 23f8c8e1..3a50436f 100644 --- a/app/modules/messaging/schemas/__init__.py +++ b/app/modules/messaging/schemas/__init__.py @@ -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 diff --git a/app/modules/messaging/schemas/message.py b/app/modules/messaging/schemas/message.py index 03e74ba7..22239322 100644 --- a/app/modules/messaging/schemas/message.py +++ b/app/modules/messaging/schemas/message.py @@ -192,10 +192,6 @@ class ConversationListResponse(BaseModel): limit: int -# Backward compatibility alias -ConversationResponse = ConversationDetailResponse - - # ============================================================================ # Unread Count Schemas # ============================================================================ diff --git a/app/modules/orders/schemas/__init__.py b/app/modules/orders/schemas/__init__.py index aa9970b6..8a442d61 100644 --- a/app/modules/orders/schemas/__init__.py +++ b/app/modules/orders/schemas/__init__.py @@ -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", ] diff --git a/app/modules/orders/schemas/invoice.py b/app/modules/orders/schemas/invoice.py index 9b701bc2..b3c4d83c 100644 --- a/app/modules/orders/schemas/invoice.py +++ b/app/modules/orders/schemas/invoice.py @@ -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 diff --git a/app/modules/routes.py b/app/modules/routes.py index 6c88a5da..bf2934c0 100644 --- a/app/modules/routes.py +++ b/app/modules/routes.py @@ -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 diff --git a/app/templates/shared/macros/pagination.html b/app/templates/shared/macros/pagination.html index 43084850..2a44ae4d 100644 --- a/app/templates/shared/macros/pagination.html +++ b/app/templates/shared/macros/pagination.html @@ -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") %} -
- - Showing - of {{ item_label }} - -
- {# First Page #} - - {# Previous Page #} - - {# Page Numbers #} - - {# Next Page #} - - {# Last Page #} - -
-
-{% endmacro %} - - {% macro pagination_simple(show_condition="true") %}
diff --git a/docs/architecture/frontend-detection.md b/docs/architecture/frontend-detection.md index 037df9b7..fefb821b 100644 --- a/docs/architecture/frontend-detection.md +++ b/docs/architecture/frontend-detection.md @@ -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 diff --git a/docs/architecture/middleware.md b/docs/architecture/middleware.md index 450812eb..ff7f2190 100644 --- a/docs/architecture/middleware.md +++ b/docs/architecture/middleware.md @@ -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. diff --git a/docs/development/error-rendering/error-rendering-developer-documentation.md b/docs/development/error-rendering/error-rendering-developer-documentation.md index 6e016a96..265e7835 100644 --- a/docs/development/error-rendering/error-rendering-developer-documentation.md +++ b/docs/development/error-rendering/error-rendering-developer-documentation.md @@ -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 diff --git a/docs/features/content-management-system.md b/docs/features/content-management-system.md index a16ba3c2..9ec47969 100644 --- a/docs/features/content-management-system.md +++ b/docs/features/content-management-system.md @@ -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 } ] ``` diff --git a/docs/features/subscription-billing.md b/docs/features/subscription-billing.md index 8235d71d..ce442d2e 100644 --- a/docs/features/subscription-billing.md +++ b/docs/features/subscription-billing.md @@ -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 diff --git a/middleware/context.py b/middleware/context.py deleted file mode 100644 index f358e3ad..00000000 --- a/middleware/context.py +++ /dev/null @@ -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 diff --git a/middleware/platform_context.py b/middleware/platform_context.py index d462e50f..a74156ec 100644 --- a/middleware/platform_context.py +++ b/middleware/platform_context.py @@ -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) diff --git a/middleware/store_context.py b/middleware/store_context.py index e94f6897..19a09864 100644 --- a/middleware/store_context.py +++ b/middleware/store_context.py @@ -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"] ): diff --git a/tests/integration/middleware/middleware_test_routes.py b/tests/integration/middleware/middleware_test_routes.py index c12def5c..31013bfc 100644 --- a/tests/integration/middleware/middleware_test_routes.py +++ b/tests/integration/middleware/middleware_test_routes.py @@ -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.""" diff --git a/tests/unit/middleware/test_context.py b/tests/unit/middleware/test_context.py deleted file mode 100644 index 8e9742d7..00000000 --- a/tests/unit/middleware/test_context.py +++ /dev/null @@ -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 diff --git a/tests/unit/middleware/test_platform_context.py b/tests/unit/middleware/test_platform_context.py index 9acd64b8..628e8e57 100644 --- a/tests/unit/middleware/test_platform_context.py +++ b/tests/unit/middleware/test_platform_context.py @@ -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.""" diff --git a/tests/unit/middleware/test_store_context.py b/tests/unit/middleware/test_store_context.py index 8c426da6..7dd8b5bc 100644 --- a/tests/unit/middleware/test_store_context.py +++ b/tests/unit/middleware/test_store_context.py @@ -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 ),