fix: resolve all architecture validation errors (62 -> 0)

Major refactoring to achieve zero architecture violations:

API Layer:
- vendor/settings.py: Move validation to Pydantic field validators
  (tax rate, delivery method, boost sort, preorder days, languages, locales)
- admin/email_templates.py: Add Pydantic response models
  (TemplateListResponse, CategoriesResponse)
- shop/auth.py: Move password reset logic to CustomerService

Service Layer:
- customer_service.py: Add password reset methods
  (get_customer_for_password_reset, validate_and_reset_password)

Exception Layer:
- customer.py: Add InvalidPasswordResetTokenException,
  PasswordTooShortException

Frontend:
- admin/email-templates.js: Use apiClient, Utils.showToast()
- vendor/email-templates.js: Use apiClient, parent init pattern

Templates:
- admin/email-templates.html: Fix block name to extra_scripts
- shop/base.html: Add language default filter

Tooling:
- validate_architecture.py: Fix LANG-001 false positive for
  SUPPORTED_LANGUAGES and SUPPORTED_LOCALES blocks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-03 18:48:59 +01:00
parent 370d61e8f7
commit 5155ef7445
10 changed files with 391 additions and 377 deletions

View File

@@ -8,8 +8,8 @@ The get_current_vendor_api dependency guarantees token_vendor_id is present.
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api
@@ -39,6 +39,13 @@ SUPPORTED_LOCALES = [
]
# Valid language codes for validation
VALID_LANGUAGE_CODES = {"en", "fr", "de", "lb"}
# Valid locale codes for validation
VALID_LOCALE_CODES = {"fr-LU", "de-LU", "de-DE", "fr-FR", "en-GB"}
class LocalizationSettingsUpdate(BaseModel):
"""Schema for updating localization settings."""
@@ -58,6 +65,29 @@ class LocalizationSettingsUpdate(BaseModel):
None, description="Locale for currency/number formatting"
)
@field_validator("default_language", "dashboard_language", "storefront_language")
@classmethod
def validate_language(cls, v: str | None) -> str | None:
if v is not None and v not in VALID_LANGUAGE_CODES:
raise ValueError(f"Invalid language: {v}. Must be one of: {sorted(VALID_LANGUAGE_CODES)}")
return v
@field_validator("storefront_languages")
@classmethod
def validate_storefront_languages(cls, v: list[str] | None) -> list[str] | None:
if v is not None:
for lang in v:
if lang not in VALID_LANGUAGE_CODES:
raise ValueError(f"Invalid language: {lang}. Must be one of: {sorted(VALID_LANGUAGE_CODES)}")
return v
@field_validator("storefront_locale")
@classmethod
def validate_locale(cls, v: str | None) -> str | None:
if v is not None and v not in VALID_LOCALE_CODES:
raise ValueError(f"Invalid locale: {v}. Must be one of: {sorted(VALID_LOCALE_CODES)}")
return v
class BusinessInfoUpdate(BaseModel):
"""Schema for updating business info (can override company values)."""
@@ -74,6 +104,13 @@ class BusinessInfoUpdate(BaseModel):
)
# Valid Letzshop tax rates
VALID_TAX_RATES = [0, 3, 8, 14, 17]
# Valid delivery methods
VALID_DELIVERY_METHODS = ["nationwide", "package_delivery", "self_collect"]
class LetzshopFeedSettingsUpdate(BaseModel):
"""Schema for updating Letzshop feed settings."""
@@ -83,14 +120,38 @@ class LetzshopFeedSettingsUpdate(BaseModel):
letzshop_default_tax_rate: int | None = Field(None, description="Default VAT rate (0, 3, 8, 14, 17)")
letzshop_boost_sort: str | None = Field(None, description="Sort priority (0.0-10.0)")
letzshop_delivery_method: str | None = Field(None, description="Delivery method")
letzshop_preorder_days: int | None = Field(None, description="Pre-order lead time in days")
letzshop_preorder_days: int | None = Field(None, ge=0, description="Pre-order lead time in days")
@field_validator("letzshop_default_tax_rate")
@classmethod
def validate_tax_rate(cls, v: int | None) -> int | None:
if v is not None and v not in VALID_TAX_RATES:
raise ValueError(f"Invalid tax rate. Must be one of: {VALID_TAX_RATES}")
return v
# Valid Letzshop tax rates
VALID_TAX_RATES = [0, 3, 8, 14, 17]
@field_validator("letzshop_delivery_method")
@classmethod
def validate_delivery_method(cls, v: str | None) -> str | None:
if v is not None:
methods = v.split(",")
for method in methods:
if method.strip() not in VALID_DELIVERY_METHODS:
raise ValueError(f"Invalid delivery method. Must be one of: {VALID_DELIVERY_METHODS}")
return v
# Valid delivery methods
VALID_DELIVERY_METHODS = ["nationwide", "package_delivery", "self_collect"]
@field_validator("letzshop_boost_sort")
@classmethod
def validate_boost_sort(cls, v: str | None) -> str | None:
if v is not None:
try:
boost = float(v)
if boost < 0.0 or boost > 10.0:
raise ValueError("Boost sort must be between 0.0 and 10.0")
except ValueError as e:
if "could not convert" in str(e).lower():
raise ValueError("Boost sort must be a valid number")
raise
return v
@router.get("")
@@ -305,52 +366,14 @@ def update_letzshop_settings(
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Update Letzshop marketplace feed settings."""
"""Update Letzshop marketplace feed settings.
Validation is handled by Pydantic model validators.
"""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
update_data = letzshop_config.model_dump(exclude_unset=True)
# Validate tax rate
if "letzshop_default_tax_rate" in update_data:
if update_data["letzshop_default_tax_rate"] not in VALID_TAX_RATES:
raise HTTPException(
status_code=400,
detail=f"Invalid tax rate. Must be one of: {VALID_TAX_RATES}"
)
# Validate delivery method
if "letzshop_delivery_method" in update_data:
methods = update_data["letzshop_delivery_method"].split(",")
for method in methods:
if method.strip() not in VALID_DELIVERY_METHODS:
raise HTTPException(
status_code=400,
detail=f"Invalid delivery method. Must be one of: {VALID_DELIVERY_METHODS}"
)
# Validate boost_sort (0.0 - 10.0)
if "letzshop_boost_sort" in update_data:
try:
boost = float(update_data["letzshop_boost_sort"])
if boost < 0.0 or boost > 10.0:
raise HTTPException(
status_code=400,
detail="Boost sort must be between 0.0 and 10.0"
)
except ValueError:
raise HTTPException(
status_code=400,
detail="Boost sort must be a valid number"
)
# Validate preorder_days
if "letzshop_preorder_days" in update_data:
if update_data["letzshop_preorder_days"] < 0:
raise HTTPException(
status_code=400,
detail="Preorder days must be non-negative"
)
# Apply updates
# Apply updates (validation already done by Pydantic)
for key, value in update_data.items():
setattr(vendor, key, value)
@@ -386,45 +409,14 @@ def update_localization_settings(
- storefront_language: Default language for customer storefront
- storefront_languages: Enabled languages for storefront selector
- storefront_locale: Locale for currency/number formatting (or null for platform default)
Validation is handled by Pydantic model validators.
"""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
# Update only provided fields
# Update only provided fields (validation already done by Pydantic)
update_data = localization_config.model_dump(exclude_unset=True)
# Validate language codes
valid_language_codes = {lang["code"] for lang in SUPPORTED_LANGUAGES}
valid_locale_codes = {loc["code"] for loc in SUPPORTED_LOCALES}
if "default_language" in update_data and update_data["default_language"]:
if update_data["default_language"] not in valid_language_codes:
raise HTTPException(
status_code=400, detail=f"Invalid language: {update_data['default_language']}"
)
if "dashboard_language" in update_data and update_data["dashboard_language"]:
if update_data["dashboard_language"] not in valid_language_codes:
raise HTTPException(
status_code=400, detail=f"Invalid language: {update_data['dashboard_language']}"
)
if "storefront_language" in update_data and update_data["storefront_language"]:
if update_data["storefront_language"] not in valid_language_codes:
raise HTTPException(
status_code=400, detail=f"Invalid language: {update_data['storefront_language']}"
)
if "storefront_languages" in update_data and update_data["storefront_languages"]:
for lang in update_data["storefront_languages"]:
if lang not in valid_language_codes:
raise HTTPException(status_code=400, detail=f"Invalid language: {lang}")
if "storefront_locale" in update_data and update_data["storefront_locale"]:
if update_data["storefront_locale"] not in valid_locale_codes:
raise HTTPException(
status_code=400, detail=f"Invalid locale: {update_data['storefront_locale']}"
)
# Apply updates
for key, value in update_data.items():
setattr(vendor, key, value)