Currency Locale Configuration: - Add platform-level storefront settings (locale, currency) - Create PlatformSettingsService with resolution chain: vendor → AdminSetting → environment → hardcoded fallback - Add storefront_locale nullable field to Vendor model - Update shop routes to resolve and pass locale to templates - Add window.SHOP_CONFIG for frontend JavaScript access - Centralize formatPrice() in shop-layout.js using SHOP_CONFIG - Remove local formatPrice functions from shop templates Vendor JS Bug Fix: - Fix vendorCode being null on all vendor pages - Root cause: page components overriding init() without calling parent - Add parent init call to 14 vendor JS files - Add JS-013 architecture rule to prevent future regressions - Validator now checks vendor JS files for parent init pattern Files changed: - New: app/services/platform_settings_service.py - New: alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py - Modified: 14 vendor JS files, shop templates, validation scripts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
177 lines
5.8 KiB
Python
177 lines
5.8 KiB
Python
# app/services/platform_settings_service.py
|
|
"""
|
|
Platform Settings Service
|
|
|
|
Provides access to platform-wide settings with a resolution chain:
|
|
1. AdminSetting from database (can be set via admin UI)
|
|
2. Environment variables (from .env/config)
|
|
3. Hardcoded defaults
|
|
|
|
This allows admins to override defaults without code changes,
|
|
while still supporting environment-based configuration.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.config import settings
|
|
from models.database.admin import AdminSetting
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PlatformSettingsService:
|
|
"""
|
|
Service for accessing platform-wide settings.
|
|
|
|
Resolution order:
|
|
1. AdminSetting in database (highest priority)
|
|
2. Environment variable via config
|
|
3. Hardcoded default (lowest priority)
|
|
"""
|
|
|
|
# Mapping of setting keys to their config attribute names and defaults
|
|
SETTINGS_MAP = {
|
|
"default_storefront_locale": {
|
|
"config_attr": "default_storefront_locale",
|
|
"default": "fr-LU",
|
|
"description": "Default locale for currency/number formatting (e.g., fr-LU, de-DE)",
|
|
"category": "storefront",
|
|
},
|
|
"default_currency": {
|
|
"config_attr": "default_currency",
|
|
"default": "EUR",
|
|
"description": "Default currency code for the platform",
|
|
"category": "storefront",
|
|
},
|
|
}
|
|
|
|
def get(self, db: Session, key: str) -> str | None:
|
|
"""
|
|
Get a setting value with full resolution chain.
|
|
|
|
Args:
|
|
db: Database session
|
|
key: Setting key (e.g., 'default_storefront_locale')
|
|
|
|
Returns:
|
|
Setting value or None if not found
|
|
"""
|
|
# 1. Check AdminSetting in database
|
|
admin_setting = db.query(AdminSetting).filter_by(key=key).first()
|
|
if admin_setting and admin_setting.value:
|
|
logger.debug(f"Setting '{key}' resolved from AdminSetting: {admin_setting.value}")
|
|
return admin_setting.value
|
|
|
|
# 2. Check environment/config
|
|
setting_info = self.SETTINGS_MAP.get(key)
|
|
if setting_info:
|
|
config_attr = setting_info.get("config_attr")
|
|
if config_attr and hasattr(settings, config_attr):
|
|
value = getattr(settings, config_attr)
|
|
logger.debug(f"Setting '{key}' resolved from config: {value}")
|
|
return value
|
|
|
|
# 3. Return hardcoded default
|
|
default = setting_info.get("default")
|
|
logger.debug(f"Setting '{key}' resolved from default: {default}")
|
|
return default
|
|
|
|
logger.warning(f"Unknown setting key: {key}")
|
|
return None
|
|
|
|
def get_storefront_locale(self, db: Session) -> str:
|
|
"""Get the default storefront locale."""
|
|
return self.get(db, "default_storefront_locale") or "fr-LU"
|
|
|
|
def get_currency(self, db: Session) -> str:
|
|
"""Get the default currency."""
|
|
return self.get(db, "default_currency") or "EUR"
|
|
|
|
def get_storefront_config(self, db: Session) -> dict[str, str]:
|
|
"""
|
|
Get all storefront-related settings as a dict.
|
|
|
|
Returns:
|
|
Dict with 'locale' and 'currency' keys
|
|
"""
|
|
return {
|
|
"locale": self.get_storefront_locale(db),
|
|
"currency": self.get_currency(db),
|
|
}
|
|
|
|
def set(self, db: Session, key: str, value: str, user_id: int | None = None) -> AdminSetting:
|
|
"""
|
|
Set a platform setting in the database.
|
|
|
|
Args:
|
|
db: Database session
|
|
key: Setting key
|
|
value: Setting value
|
|
user_id: ID of user making the change (for audit)
|
|
|
|
Returns:
|
|
The created/updated AdminSetting
|
|
"""
|
|
setting_info = self.SETTINGS_MAP.get(key, {})
|
|
|
|
admin_setting = db.query(AdminSetting).filter_by(key=key).first()
|
|
if admin_setting:
|
|
admin_setting.value = value
|
|
if user_id:
|
|
admin_setting.last_modified_by_user_id = user_id
|
|
else:
|
|
admin_setting = AdminSetting(
|
|
key=key,
|
|
value=value,
|
|
value_type="string",
|
|
category=setting_info.get("category", "system"),
|
|
description=setting_info.get("description", ""),
|
|
last_modified_by_user_id=user_id,
|
|
)
|
|
db.add(admin_setting)
|
|
|
|
db.commit() # noqa: SVC-006 - Setting change is atomic, commit is intentional
|
|
db.refresh(admin_setting)
|
|
|
|
logger.info(f"Platform setting '{key}' set to '{value}' by user {user_id}")
|
|
return admin_setting
|
|
|
|
def get_all_storefront_settings(self, db: Session) -> dict[str, Any]:
|
|
"""
|
|
Get all storefront settings with their current values and metadata.
|
|
|
|
Useful for admin UI to display current settings.
|
|
|
|
Returns:
|
|
Dict with setting info including current value and source
|
|
"""
|
|
result = {}
|
|
for key, info in self.SETTINGS_MAP.items():
|
|
if info.get("category") == "storefront":
|
|
current_value = self.get(db, key)
|
|
|
|
# Determine source
|
|
admin_setting = db.query(AdminSetting).filter_by(key=key).first()
|
|
if admin_setting and admin_setting.value:
|
|
source = "database"
|
|
elif hasattr(settings, info.get("config_attr", "")):
|
|
source = "environment"
|
|
else:
|
|
source = "default"
|
|
|
|
result[key] = {
|
|
"value": current_value,
|
|
"source": source,
|
|
"description": info.get("description", ""),
|
|
"default": info.get("default"),
|
|
}
|
|
|
|
return result
|
|
|
|
|
|
# Singleton instance
|
|
platform_settings_service = PlatformSettingsService()
|