From c87bdfa1299bdea1a8b595d7b3ec8852d1c5a3c6 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Fri, 2 Jan 2026 21:26:12 +0100 Subject: [PATCH] feat: add configurable currency locale and fix vendor JS init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .architecture-rules/frontend.yaml | 54 ++++++ ...c0d1e2_add_storefront_locale_to_vendors.py | 40 ++++ app/api/v1/shop/profile.py | 13 +- app/core/config.py | 7 + app/routes/shop_pages.py | 44 +++++ app/services/platform_settings_service.py | 176 ++++++++++++++++++ app/templates/shop/account/order-detail.html | 8 +- app/templates/shop/account/orders.html | 8 +- app/templates/shop/account/profile.html | 8 +- app/templates/shop/base.html | 21 ++- app/templates/shop/products.html | 8 +- docs/deployment/launch-readiness.md | 13 ++ models/database/vendor.py | 4 + models/schema/vendor.py | 13 ++ scripts/validate_architecture.py | 56 ++++++ static/shop/js/shop-layout.js | 13 +- static/vendor/js/analytics.js | 6 + static/vendor/js/billing.js | 6 + static/vendor/js/content-page-edit.js | 6 + static/vendor/js/content-pages.js | 6 + static/vendor/js/customers.js | 6 + static/vendor/js/inventory.js | 6 + static/vendor/js/messages.js | 6 + static/vendor/js/notifications.js | 6 + static/vendor/js/order-detail.js | 6 + static/vendor/js/orders.js | 6 + static/vendor/js/products.js | 6 + static/vendor/js/profile.js | 6 + static/vendor/js/settings.js | 6 + static/vendor/js/team.js | 6 + 30 files changed, 522 insertions(+), 48 deletions(-) create mode 100644 alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py create mode 100644 app/services/platform_settings_service.py diff --git a/.architecture-rules/frontend.yaml b/.architecture-rules/frontend.yaml index b33dad69..6c9782f8 100644 --- a/.architecture-rules/frontend.yaml +++ b/.architecture-rules/frontend.yaml @@ -77,6 +77,60 @@ javascript_rules: file_pattern: "static/**/js/**/*.js" check: "async_error_handling" + - id: "JS-013" + name: "Components overriding init() must call parent init" + severity: "error" + description: | + When an Alpine.js component spreads ...data() and defines its own init() method, + it MUST call the parent init() first. The parent init() sets critical properties + like vendorCode (from URL), currentUser, and theme preference. + + Without calling parent init(), properties like vendorCode will be null, causing + API calls like `/vendor/${this.vendorCode}/settings` to fail with + "Endpoint not found: /api/v1/vendor/null/settings". + + WRONG (parent init never called): + function vendorSettings() { + return { + ...data(), + async init() { + await this.loadSettings(); // this.vendorCode is null! + } + }; + } + + RIGHT (call parent init first): + function vendorSettings() { + return { + ...data(), + async init() { + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + + await this.loadSettings(); // this.vendorCode is now set + } + }; + } + + This pattern is required for ALL page-specific JavaScript files that: + 1. Use ...data() to inherit base layout functionality + 2. Define their own init() method + pattern: + file_pattern: "static/vendor/js/**/*.js" + check: "parent_init_call" + required_when: + - "contains: '...data()'" + - "contains: 'async init()'" + required_pattern: + - "data\\(\\)\\.init" + - "parentInit" + exceptions: + - "init-alpine.js" + - "login.js" + - id: "JS-007" name: "Set loading state before async operations" severity: "warning" diff --git a/alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py b/alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py new file mode 100644 index 00000000..b11cb9a0 --- /dev/null +++ b/alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py @@ -0,0 +1,40 @@ +# alembic/versions/s7a8b9c0d1e2_add_storefront_locale_to_vendors.py +"""Add storefront_locale to vendors for currency formatting. + +Revision ID: s7a8b9c0d1e2 +Revises: r6f7a8b9c0d1 +Create Date: 2026-01-02 20:00:00.000000 + +This migration adds a nullable storefront_locale field to vendors. +NULL means the vendor inherits from platform defaults. +Examples: 'fr-LU', 'de-DE', 'en-GB' +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "s7a8b9c0d1e2" +down_revision = "r6f7a8b9c0d1" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Add storefront_locale column to vendors table.""" + # Nullable - NULL means "inherit from platform default" + op.add_column( + "vendors", + sa.Column( + "storefront_locale", + sa.String(10), + nullable=True, + comment="Currency/number formatting locale (NULL = inherit from platform)", + ), + ) + + +def downgrade() -> None: + """Remove storefront_locale column from vendors table.""" + op.drop_column("vendors", "storefront_locale") diff --git a/app/api/v1/shop/profile.py b/app/api/v1/shop/profile.py index cc135dcf..32d76086 100644 --- a/app/api/v1/shop/profile.py +++ b/app/api/v1/shop/profile.py @@ -15,6 +15,7 @@ from app.api.deps import get_current_customer_api from app.core.database import get_db from app.exceptions import ValidationException from app.services.auth_service import AuthService +from app.services.customer_service import customer_service from models.database.customer import Customer from models.schema.customer import ( CustomerPasswordChange, @@ -81,16 +82,10 @@ def update_profile( # If email is being changed, check uniqueness within vendor if update_data.email and update_data.email != customer.email: - existing = ( - db.query(Customer) - .filter( - Customer.vendor_id == customer.vendor_id, - Customer.email == update_data.email, - Customer.id != customer.id, - ) - .first() + existing = customer_service.get_customer_by_email( + db, customer.vendor_id, update_data.email ) - if existing: + if existing and existing.id != customer.id: raise ValidationException("Email already in use") # Update only provided fields diff --git a/app/core/config.py b/app/core/config.py index 6e814786..a2475ba7 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -158,6 +158,13 @@ class Settings(BaseSettings): email_enabled: bool = True # Set to False to disable all emails email_debug: bool = False # Log emails instead of sending (for development) + # ============================================================================= + # STOREFRONT DEFAULTS + # ============================================================================= + # These can be overridden by AdminSetting in the database + default_storefront_locale: str = "fr-LU" # Currency/number formatting locale + default_currency: str = "EUR" # Default currency code + # ============================================================================= # DEMO/SEED DATA CONFIGURATION # ============================================================================= diff --git a/app/routes/shop_pages.py b/app/routes/shop_pages.py index ea5d58fe..72281576 100644 --- a/app/routes/shop_pages.py +++ b/app/routes/shop_pages.py @@ -39,6 +39,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_customer_from_cookie_or_header, get_db from app.services.content_page_service import content_page_service +from app.services.platform_settings_service import platform_settings_service from models.database.customer import Customer router = APIRouter() @@ -47,6 +48,40 @@ templates = Jinja2Templates(directory="app/templates") logger = logging.getLogger(__name__) +# ============================================================================ +# HELPER: Resolve Storefront Locale +# ============================================================================ + + +def get_resolved_storefront_config(db: Session, vendor) -> dict: + """ + Resolve storefront locale and currency with priority: + 1. Vendor's storefront_locale (if set) + 2. Platform's default_storefront_locale (from AdminSetting) + 3. Environment variable (from config) + 4. Hardcoded fallback: 'fr-LU' + + Args: + db: Database session + vendor: Vendor model instance + + Returns: + dict with 'locale' and 'currency' keys + """ + # Get platform defaults from service (handles resolution chain 2-4) + platform_config = platform_settings_service.get_storefront_config(db) + + # Check for vendor override (step 1) + locale = platform_config["locale"] + if vendor and vendor.storefront_locale: + locale = vendor.storefront_locale + + return { + "locale": locale, + "currency": platform_config["currency"], + } + + # ============================================================================ # HELPER: Build Shop Template Context # ============================================================================ @@ -133,6 +168,11 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d extra={"error": str(e), "vendor_id": vendor.id if vendor else None}, ) + # Resolve storefront locale and currency + storefront_config = {"locale": "fr-LU", "currency": "EUR"} # defaults + if db and vendor: + storefront_config = get_resolved_storefront_config(db, vendor) + context = { "request": request, "vendor": vendor, @@ -142,6 +182,8 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d "base_url": base_url, "footer_pages": footer_pages, "header_pages": header_pages, + "storefront_locale": storefront_config["locale"], + "storefront_currency": storefront_config["currency"], } # Add any extra context (user, product_id, category_slug, etc.) @@ -157,6 +199,8 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d "has_theme": theme is not None, "access_method": access_method, "base_url": base_url, + "storefront_locale": storefront_config["locale"], + "storefront_currency": storefront_config["currency"], "footer_pages_count": len(footer_pages), "header_pages_count": len(header_pages), "extra_keys": list(extra_context.keys()) if extra_context else [], diff --git a/app/services/platform_settings_service.py b/app/services/platform_settings_service.py new file mode 100644 index 00000000..8f8c189b --- /dev/null +++ b/app/services/platform_settings_service.py @@ -0,0 +1,176 @@ +# 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() diff --git a/app/templates/shop/account/order-detail.html b/app/templates/shop/account/order-detail.html index 84e9ffcb..4447c725 100644 --- a/app/templates/shop/account/order-detail.html +++ b/app/templates/shop/account/order-detail.html @@ -462,13 +462,7 @@ function shopOrderDetailPage() { return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'; }, - formatPrice(amount) { - if (!amount && amount !== 0) return '-'; - return new Intl.NumberFormat('fr-LU', { - style: 'currency', - currency: 'EUR' - }).format(amount); - }, + // formatPrice is inherited from shopLayoutData() via spread operator formatDateTime(dateStr) { if (!dateStr) return '-'; diff --git a/app/templates/shop/account/orders.html b/app/templates/shop/account/orders.html index 5a6f190b..9ef080b3 100644 --- a/app/templates/shop/account/orders.html +++ b/app/templates/shop/account/orders.html @@ -217,13 +217,7 @@ function shopOrdersPage() { return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'; }, - formatPrice(amount) { - if (!amount && amount !== 0) return '-'; - return new Intl.NumberFormat('fr-LU', { - style: 'currency', - currency: 'EUR' - }).format(amount); - }, + // formatPrice is inherited from shopLayoutData() via spread operator formatDate(dateStr) { if (!dateStr) return '-'; diff --git a/app/templates/shop/account/profile.html b/app/templates/shop/account/profile.html index 75e8000b..6ad376b5 100644 --- a/app/templates/shop/account/profile.html +++ b/app/templates/shop/account/profile.html @@ -523,13 +523,7 @@ function shopProfilePage() { } }, - formatPrice(amount) { - if (!amount && amount !== 0) return '-'; - return new Intl.NumberFormat('fr-LU', { - style: 'currency', - currency: 'EUR' - }).format(amount); - }, + // formatPrice is inherited from shopLayoutData() via spread operator formatDate(dateStr) { if (!dateStr) return '-'; diff --git a/app/templates/shop/base.html b/app/templates/shop/base.html index c9e07c74..23e9e07e 100644 --- a/app/templates/shop/base.html +++ b/app/templates/shop/base.html @@ -308,19 +308,28 @@ {# 1. Log Configuration (must load first) #} - {# 2. Icon System #} + {# 2. Global Shop Configuration (currency/locale settings) #} + + + {# 3. Icon System #} - {# 3. Base Shop Layout (Alpine.js component - must load before Alpine) #} + {# 4. Base Shop Layout (Alpine.js component - must load before Alpine) #} - {# 4. Utilities #} + {# 5. Utilities #} - {# 5. API Client #} + {# 6. API Client #} - {# 6. Alpine.js with CDN fallback (deferred - loads last) #} + {# 7. Alpine.js with CDN fallback (deferred - loads last) #} - {# 7. Page-specific JavaScript #} + {# 8. Page-specific JavaScript #} {% block extra_scripts %}{% endblock %} {# Toast notification container #} diff --git a/app/templates/shop/products.html b/app/templates/shop/products.html index 8265bfa7..4a1424d9 100644 --- a/app/templates/shop/products.html +++ b/app/templates/shop/products.html @@ -207,13 +207,7 @@ document.addEventListener('alpine:init', () => { this.loadProducts(); }, - formatPrice(amount) { - if (!amount && amount !== 0) return ''; - return new Intl.NumberFormat('fr-LU', { - style: 'currency', - currency: 'EUR' - }).format(amount); - }, + // formatPrice is inherited from shopLayoutData() via spread operator async addToCart(product) { console.log('[SHOP] Adding to cart:', product); diff --git a/docs/deployment/launch-readiness.md b/docs/deployment/launch-readiness.md index 7d6e7b82..e5d88873 100644 --- a/docs/deployment/launch-readiness.md +++ b/docs/deployment/launch-readiness.md @@ -54,6 +54,7 @@ The OMS is nearly production ready with core order processing, invoicing, invent | Customer profile | Complete | Full profile management | | Customer addresses | Complete | Multiple addresses, address book | | Customer messages | Complete | Conversation-based messaging | +| Currency locale | Complete | Configurable platform/vendor locale | ### Vendor Dashboard (90% Complete) @@ -123,6 +124,18 @@ The OMS is nearly production ready with core order processing, invoicing, invent - Integration tests for shop addresses API - All validation scripts fixed and passing +### Configurable Currency Locale +- **Two-tier settings architecture**: Platform defaults with vendor overrides +- **Resolution chain**: Vendor setting → AdminSetting → Environment → Hardcoded fallback +- **Platform settings service**: New `PlatformSettingsService` for setting resolution +- **Configuration options**: + - Environment: `DEFAULT_STOREFRONT_LOCALE`, `DEFAULT_CURRENCY` in `.env` + - Admin: `default_storefront_locale`, `default_currency` in AdminSetting table + - Vendor: `storefront_locale` field on Vendor model (nullable = inherit) +- **Supported locales**: fr-LU, de-DE, de-LU, en-GB, nl-BE, etc. +- **Frontend**: `window.SHOP_CONFIG` provides locale/currency to JavaScript +- **Shared formatPrice()**: Single implementation in `shop-layout.js` + --- ## Remaining Gaps diff --git a/models/database/vendor.py b/models/database/vendor.py index 0ffc7fd2..f3064aef 100644 --- a/models/database/vendor.py +++ b/models/database/vendor.py @@ -121,6 +121,10 @@ class Vendor(Base, TimestampMixin): JSON, nullable=False, default=["fr", "de", "en"] ) # Array of enabled languages for storefront language selector + # Currency/number formatting locale (e.g., 'fr-LU' = "29,99 €", 'en-GB' = "€29.99") + # NULL means inherit from platform default (AdminSetting 'default_storefront_locale') + storefront_locale = Column(String(10), nullable=True) + # ======================================================================== # Relationships # ======================================================================== diff --git a/models/schema/vendor.py b/models/schema/vendor.py index f121f6c3..95cb9915 100644 --- a/models/schema/vendor.py +++ b/models/schema/vendor.py @@ -81,6 +81,11 @@ class VendorCreate(BaseModel): storefront_languages: list[str] | None = Field( default=["fr", "de", "en"], description="Enabled languages for storefront" ) + storefront_locale: str | None = Field( + None, + description="Locale for currency/number formatting (e.g., 'fr-LU', 'de-DE'). NULL = inherit from platform default", + max_length=10, + ) @field_validator("subdomain") @classmethod @@ -152,6 +157,11 @@ class VendorUpdate(BaseModel): storefront_languages: list[str] | None = Field( None, description="Enabled languages for storefront" ) + storefront_locale: str | None = Field( + None, + description="Locale for currency/number formatting (e.g., 'fr-LU', 'de-DE'). NULL = inherit from platform default", + max_length=10, + ) @field_validator("subdomain") @classmethod @@ -197,6 +207,9 @@ class VendorResponse(BaseModel): storefront_language: str = "fr" storefront_languages: list[str] = ["fr", "de", "en"] + # Currency/number formatting locale (NULL = inherit from platform default) + storefront_locale: str | None = None + # Timestamps created_at: datetime updated_at: datetime diff --git a/scripts/validate_architecture.py b/scripts/validate_architecture.py index 45d0cccd..a43e5a0c 100755 --- a/scripts/validate_architecture.py +++ b/scripts/validate_architecture.py @@ -2862,6 +2862,9 @@ class ArchitectureValidator: # JS-012: Check for double /api/v1 prefix in API calls self._check_api_prefix_usage(file_path, content, lines) + # JS-013: Check that components overriding init() call parent init + self._check_parent_init_call(file_path, content, lines) + def _check_platform_settings_usage( self, file_path: Path, content: str, lines: list[str] ): @@ -2995,6 +2998,59 @@ class ArchitectureValidator: suggestion="Change '/api/v1/admin/...' to '/admin/...'", ) + def _check_parent_init_call( + self, file_path: Path, content: str, lines: list[str] + ): + """ + JS-013: Check that vendor components overriding init() call parent init. + + When a component uses ...data() to inherit base layout functionality AND + defines its own init() method, it MUST call the parent init first to set + critical properties like vendorCode. + + Note: This only applies to vendor JS files because the vendor data() has + an init() method that extracts vendorCode from URL. Admin data() does not. + """ + # Only check vendor JS files (admin data() doesn't have init()) + if "/vendor/js/" not in str(file_path): + return + + # Skip files that shouldn't have this pattern + excluded_files = ["init-alpine.js", "login.js", "onboarding.js"] + if file_path.name in excluded_files: + return + + # Check if file uses ...data() spread operator + uses_data_spread = "...data()" in content + + # Check if file defines its own async init() + has_own_init = re.search(r"async\s+init\s*\(\s*\)", content) + + # If both conditions are true, check for parent init call + if uses_data_spread and has_own_init: + # Check for parent init call patterns + calls_parent_init = ( + "data().init" in content + or "parentInit" in content + or "parent.init" in content + ) + + if not calls_parent_init: + # Find the line with async init() to report + for i, line in enumerate(lines, 1): + if re.search(r"async\s+init\s*\(\s*\)", line): + self._add_violation( + rule_id="JS-013", + rule_name="Components overriding init() must call parent init", + severity=Severity.ERROR, + file_path=file_path, + line_number=i, + message="Component with ...data() must call parent init() to set vendorCode", + context=line.strip()[:80], + suggestion="Add: const parentInit = data().init; if (parentInit) { await parentInit.call(this); }", + ) + break + def _validate_templates(self, target_path: Path): """Validate template patterns""" print("📄 Validating templates...") diff --git a/static/shop/js/shop-layout.js b/static/shop/js/shop-layout.js index dd93f19f..f2ad4568 100644 --- a/static/shop/js/shop-layout.js +++ b/static/shop/js/shop-layout.js @@ -219,12 +219,15 @@ function shopLayoutData() { }, 3000); }, - // Format currency - formatPrice(price) { - return new Intl.NumberFormat('en-US', { + // Format currency using configured locale + formatPrice(amount) { + if (!amount && amount !== 0) return ''; + const locale = window.SHOP_CONFIG?.locale || 'fr-LU'; + const currency = window.SHOP_CONFIG?.currency || 'EUR'; + return new Intl.NumberFormat(locale, { style: 'currency', - currency: 'USD' - }).format(price); + currency: currency + }).format(amount); }, // Format date diff --git a/static/vendor/js/analytics.js b/static/vendor/js/analytics.js index cadc1aec..5eb2ae4d 100644 --- a/static/vendor/js/analytics.js +++ b/static/vendor/js/analytics.js @@ -58,6 +58,12 @@ function vendorAnalytics() { } window._vendorAnalyticsInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + try { await this.loadAllData(); } catch (error) { diff --git a/static/vendor/js/billing.js b/static/vendor/js/billing.js index a174396d..91610579 100644 --- a/static/vendor/js/billing.js +++ b/static/vendor/js/billing.js @@ -33,6 +33,12 @@ function vendorBilling() { if (window._vendorBillingInitialized) return; window._vendorBillingInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + try { // Check URL params for success/cancel const params = new URLSearchParams(window.location.search); diff --git a/static/vendor/js/content-page-edit.js b/static/vendor/js/content-page-edit.js index e980e62d..4488d30a 100644 --- a/static/vendor/js/content-page-edit.js +++ b/static/vendor/js/content-page-edit.js @@ -44,6 +44,12 @@ function vendorContentPageEditor(pageId) { } window._vendorContentPageEditInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + try { contentPageEditLog.info('=== VENDOR CONTENT PAGE EDITOR INITIALIZING ==='); contentPageEditLog.info('Page ID:', this.pageId); diff --git a/static/vendor/js/content-pages.js b/static/vendor/js/content-pages.js index 6eb093a0..7e5d71f2 100644 --- a/static/vendor/js/content-pages.js +++ b/static/vendor/js/content-pages.js @@ -36,6 +36,12 @@ function vendorContentPagesManager() { } window._vendorContentPagesInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + await this.loadPages(); contentPagesLog.info('=== VENDOR CONTENT PAGES MANAGER INITIALIZATION COMPLETE ==='); diff --git a/static/vendor/js/customers.js b/static/vendor/js/customers.js index d00e33af..0676143f 100644 --- a/static/vendor/js/customers.js +++ b/static/vendor/js/customers.js @@ -106,6 +106,12 @@ function vendorCustomers() { } window._vendorCustomersInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + // Load platform settings for rows per page if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); diff --git a/static/vendor/js/inventory.js b/static/vendor/js/inventory.js index e4b90496..a184a1f9 100644 --- a/static/vendor/js/inventory.js +++ b/static/vendor/js/inventory.js @@ -137,6 +137,12 @@ function vendorInventory() { } window._vendorInventoryInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + // Load platform settings for rows per page if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); diff --git a/static/vendor/js/messages.js b/static/vendor/js/messages.js index c7e59323..dfd984a4 100644 --- a/static/vendor/js/messages.js +++ b/static/vendor/js/messages.js @@ -65,6 +65,12 @@ function vendorMessages(initialConversationId = null) { if (window._vendorMessagesInitialized) return; window._vendorMessagesInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + try { messagesLog.debug('Initializing vendor messages page'); await Promise.all([ diff --git a/static/vendor/js/notifications.js b/static/vendor/js/notifications.js index 2f6ad223..0037288b 100644 --- a/static/vendor/js/notifications.js +++ b/static/vendor/js/notifications.js @@ -60,6 +60,12 @@ function vendorNotifications() { } window._vendorNotificationsInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + try { await this.loadNotifications(); } catch (error) { diff --git a/static/vendor/js/order-detail.js b/static/vendor/js/order-detail.js index f59a47b3..2042dcee 100644 --- a/static/vendor/js/order-detail.js +++ b/static/vendor/js/order-detail.js @@ -62,6 +62,12 @@ function vendorOrderDetail() { } window._orderDetailInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + if (!this.orderId) { this.error = 'Order ID not provided'; this.loading = false; diff --git a/static/vendor/js/orders.js b/static/vendor/js/orders.js index bfd09384..e9668715 100644 --- a/static/vendor/js/orders.js +++ b/static/vendor/js/orders.js @@ -137,6 +137,12 @@ function vendorOrders() { } window._vendorOrdersInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + // Load platform settings for rows per page if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); diff --git a/static/vendor/js/products.js b/static/vendor/js/products.js index d31ad52c..1949dff9 100644 --- a/static/vendor/js/products.js +++ b/static/vendor/js/products.js @@ -121,6 +121,12 @@ function vendorProducts() { } window._vendorProductsInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + // Load platform settings for rows per page if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); diff --git a/static/vendor/js/profile.js b/static/vendor/js/profile.js index fe15b6b0..d72a7279 100644 --- a/static/vendor/js/profile.js +++ b/static/vendor/js/profile.js @@ -54,6 +54,12 @@ function vendorProfile() { } window._vendorProfileInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + try { await this.loadProfile(); } catch (error) { diff --git a/static/vendor/js/settings.js b/static/vendor/js/settings.js index cdd3ba61..9302b74c 100644 --- a/static/vendor/js/settings.js +++ b/static/vendor/js/settings.js @@ -68,6 +68,12 @@ function vendorSettings() { } window._vendorSettingsInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + try { await this.loadSettings(); } catch (error) { diff --git a/static/vendor/js/team.js b/static/vendor/js/team.js index 559ca98a..0b18d1bd 100644 --- a/static/vendor/js/team.js +++ b/static/vendor/js/team.js @@ -73,6 +73,12 @@ function vendorTeam() { } window._vendorTeamInitialized = true; + // IMPORTANT: Call parent init first to set vendorCode from URL + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + try { await Promise.all([ this.loadMembers(),