Files
orion/app/services/platform_settings_service.py
Samir Boulahtit c87bdfa129 feat: add configurable currency locale and fix vendor JS init
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>
2026-01-02 21:26:12 +01:00

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()