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>
This commit is contained in:
2026-02-13 21:58:59 +01:00
parent 531487f5c9
commit 8968e7d9cd
31 changed files with 172 additions and 774 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">