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:
160
app/api/v1/vendor/settings.py
vendored
160
app/api/v1/vendor/settings.py
vendored
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user