Some checks failed
- Add Development URL Quick Reference section to url-routing overview with all login URLs, entry points, and full examples - Replace /shop/ path segments with /storefront/ across 50 docs files - Update file references: shop_pages.py → storefront_pages.py, templates/shop/ → templates/storefront/, api/v1/shop/ → api/v1/storefront/ - Preserve domain references (orion.shop) and /store/ staff dashboard paths - Archive docs left unchanged (historical) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
477 lines
13 KiB
Markdown
477 lines
13 KiB
Markdown
# Architecture Validation Fixes (January 2026)
|
|
|
|
## Overview
|
|
|
|
This document details the architecture validation fixes implemented to achieve **zero architecture errors** in the codebase. The fixes addressed violations across API endpoints, service layer, JavaScript files, and templates.
|
|
|
|
**Before:** 62 errors
|
|
**After:** 0 errors
|
|
|
|
---
|
|
|
|
## Summary of Changes
|
|
|
|
| Category | Files Changed | Violations Fixed |
|
|
|----------|---------------|------------------|
|
|
| API Layer | 3 files | 12 violations |
|
|
| Service Layer | 2 files | Business logic moved |
|
|
| Exceptions | 1 file | 2 new exceptions |
|
|
| JavaScript | 2 files | 8 violations |
|
|
| Templates | 2 files | 2 violations |
|
|
| Tooling | 1 file | 1 false positive fix |
|
|
|
|
---
|
|
|
|
## 1. API Layer Fixes
|
|
|
|
### 1.1 Store Settings API
|
|
|
|
**File:** `app/api/v1/store/settings.py`
|
|
|
|
**Problem:** 10 HTTPException raises directly in endpoint functions (API-003 violations).
|
|
|
|
**Solution:** Moved validation to Pydantic field validators.
|
|
|
|
#### Before (Inline Validation)
|
|
```python
|
|
@router.put("/letzshop")
|
|
def update_letzshop_settings(letzshop_config: LetzshopFeedSettingsUpdate, ...):
|
|
update_data = letzshop_config.model_dump(exclude_unset=True)
|
|
|
|
# Validate tax rate - VIOLATION: HTTPException in endpoint
|
|
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="Invalid tax rate")
|
|
# ... more inline validation
|
|
```
|
|
|
|
#### After (Pydantic Validators)
|
|
```python
|
|
# Constants moved above model
|
|
VALID_TAX_RATES = [0, 3, 8, 14, 17]
|
|
VALID_DELIVERY_METHODS = ["nationwide", "package_delivery", "self_collect"]
|
|
|
|
class LetzshopFeedSettingsUpdate(BaseModel):
|
|
letzshop_default_tax_rate: int | None = Field(None)
|
|
letzshop_boost_sort: str | None = Field(None)
|
|
letzshop_delivery_method: str | None = Field(None)
|
|
letzshop_preorder_days: int | None = Field(None, ge=0) # Built-in validation
|
|
|
|
@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
|
|
|
|
@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")
|
|
return v
|
|
|
|
@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
|
|
|
|
# Endpoint now clean
|
|
@router.put("/letzshop")
|
|
def update_letzshop_settings(letzshop_config: LetzshopFeedSettingsUpdate, ...):
|
|
"""Validation is handled by Pydantic model validators."""
|
|
store = store_service.get_store_by_id(db, current_user.token_store_id)
|
|
update_data = letzshop_config.model_dump(exclude_unset=True)
|
|
|
|
for key, value in update_data.items():
|
|
setattr(store, key, value)
|
|
# ...
|
|
```
|
|
|
|
**Same pattern applied to:**
|
|
- `LocalizationSettingsUpdate` - language and locale validation
|
|
|
|
---
|
|
|
|
### 1.2 Admin Email Templates API
|
|
|
|
**File:** `app/api/v1/admin/email_templates.py`
|
|
|
|
**Problem:** 2 endpoints returning raw dicts instead of Pydantic models (API-001 violations).
|
|
|
|
**Solution:** Added Pydantic response models.
|
|
|
|
#### Before
|
|
```python
|
|
@router.get("")
|
|
def list_templates(...):
|
|
return {"templates": service.list_platform_templates()}
|
|
|
|
@router.get("/categories")
|
|
def get_categories(...):
|
|
return {"categories": service.get_template_categories()}
|
|
```
|
|
|
|
#### After
|
|
```python
|
|
class TemplateListItem(BaseModel):
|
|
code: str
|
|
name: str
|
|
description: str | None = None
|
|
category: str
|
|
languages: list[str] # Matches service output
|
|
is_platform_only: bool = False
|
|
variables: list[str] = []
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
class TemplateListResponse(BaseModel):
|
|
templates: list[TemplateListItem]
|
|
|
|
class CategoriesResponse(BaseModel):
|
|
categories: list[str]
|
|
|
|
@router.get("", response_model=TemplateListResponse)
|
|
def list_templates(...):
|
|
return TemplateListResponse(templates=service.list_platform_templates())
|
|
|
|
@router.get("/categories", response_model=CategoriesResponse)
|
|
def get_categories(...):
|
|
return CategoriesResponse(categories=service.get_template_categories())
|
|
```
|
|
|
|
---
|
|
|
|
### 1.3 Shop Auth API (Password Reset)
|
|
|
|
**File:** `app/api/v1/storefront/auth.py`
|
|
|
|
**Problem:** Business logic in `forgot_password()` and `reset_password()` endpoints.
|
|
|
|
**Solution:** Moved business logic to `CustomerService`.
|
|
|
|
#### Before (Business Logic in Endpoint)
|
|
```python
|
|
@router.post("/auth/reset-password")
|
|
def reset_password(request: Request, reset_token: str, new_password: str, db: Session = Depends(get_db)):
|
|
# Validate password length - BUSINESS LOGIC
|
|
if len(new_password) < 8:
|
|
raise HTTPException(status_code=400, detail="Password too short")
|
|
|
|
# Find valid token - BUSINESS LOGIC
|
|
token_record = PasswordResetToken.find_valid_token(db, reset_token)
|
|
if not token_record:
|
|
raise HTTPException(status_code=400, detail="Invalid token")
|
|
|
|
# ... more business logic
|
|
```
|
|
|
|
#### After (Delegated to Service)
|
|
```python
|
|
@router.post("/auth/reset-password", response_model=PasswordResetResponse)
|
|
def reset_password(request: Request, reset_token: str, new_password: str, db: Session = Depends(get_db)):
|
|
store = getattr(request.state, "store", None)
|
|
if not store:
|
|
raise StoreNotFoundException("context", identifier_type="subdomain")
|
|
|
|
# All business logic in service
|
|
customer = customer_service.validate_and_reset_password(
|
|
db=db,
|
|
store_id=store.id,
|
|
reset_token=reset_token,
|
|
new_password=new_password,
|
|
)
|
|
db.commit()
|
|
|
|
return PasswordResetResponse(message="Password reset successfully.")
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Service Layer Changes
|
|
|
|
### 2.1 CustomerService Enhancements
|
|
|
|
**File:** `app/services/customer_service.py`
|
|
|
|
Added two new methods for password reset:
|
|
|
|
```python
|
|
def get_customer_for_password_reset(
|
|
self, db: Session, store_id: int, email: str
|
|
) -> Customer | None:
|
|
"""Get active customer by email for password reset."""
|
|
return (
|
|
db.query(Customer)
|
|
.filter(
|
|
Customer.store_id == store_id,
|
|
Customer.email == email.lower(),
|
|
Customer.is_active == True,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
def validate_and_reset_password(
|
|
self,
|
|
db: Session,
|
|
store_id: int,
|
|
reset_token: str,
|
|
new_password: str,
|
|
) -> Customer:
|
|
"""Validate reset token and update customer password.
|
|
|
|
Raises:
|
|
PasswordTooShortException: If password too short
|
|
InvalidPasswordResetTokenException: If token invalid/expired
|
|
CustomerNotActiveException: If customer not active
|
|
"""
|
|
if len(new_password) < 8:
|
|
raise PasswordTooShortException(min_length=8)
|
|
|
|
token_record = PasswordResetToken.find_valid_token(db, reset_token)
|
|
if not token_record:
|
|
raise InvalidPasswordResetTokenException()
|
|
|
|
customer = db.query(Customer).filter(Customer.id == token_record.customer_id).first()
|
|
if not customer or customer.store_id != store_id:
|
|
raise InvalidPasswordResetTokenException()
|
|
|
|
if not customer.is_active:
|
|
raise CustomerNotActiveException(customer.email)
|
|
|
|
hashed_password = self.auth_service.hash_password(new_password)
|
|
customer.hashed_password = hashed_password
|
|
token_record.mark_used(db)
|
|
|
|
return customer
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Exception Layer
|
|
|
|
### 3.1 New Customer Exceptions
|
|
|
|
**File:** `app/exceptions/customer.py`
|
|
|
|
Added two new exceptions for password reset:
|
|
|
|
```python
|
|
class InvalidPasswordResetTokenException(ValidationException):
|
|
"""Raised when password reset token is invalid or expired."""
|
|
|
|
def __init__(self):
|
|
super().__init__(
|
|
message="Invalid or expired password reset link. Please request a new one.",
|
|
field="reset_token",
|
|
)
|
|
self.error_code = "INVALID_RESET_TOKEN"
|
|
|
|
|
|
class PasswordTooShortException(ValidationException):
|
|
"""Raised when password doesn't meet minimum length requirement."""
|
|
|
|
def __init__(self, min_length: int = 8):
|
|
super().__init__(
|
|
message=f"Password must be at least {min_length} characters long",
|
|
field="password",
|
|
details={"min_length": min_length},
|
|
)
|
|
self.error_code = "PASSWORD_TOO_SHORT"
|
|
```
|
|
|
|
---
|
|
|
|
## 4. JavaScript Fixes
|
|
|
|
### 4.1 Admin Email Templates
|
|
|
|
**File:** `static/admin/js/email-templates.js`
|
|
|
|
**Problems:**
|
|
- Used raw `fetch()` instead of `apiClient`
|
|
- Used `showNotification()` instead of `Utils.showToast()`
|
|
|
|
#### Before
|
|
```javascript
|
|
async loadData() {
|
|
this.loading = true;
|
|
try {
|
|
const response = await fetch('/api/v1/admin/email-templates');
|
|
const data = await response.json();
|
|
this.templates = data.templates || [];
|
|
} catch (error) {
|
|
showNotification('Failed to load templates', 'error');
|
|
}
|
|
}
|
|
```
|
|
|
|
#### After
|
|
```javascript
|
|
async loadData() {
|
|
this.loading = true;
|
|
try {
|
|
const [templatesData, categoriesData] = await Promise.all([
|
|
apiClient.get('/admin/email-templates'),
|
|
apiClient.get('/admin/email-templates/categories')
|
|
]);
|
|
this.templates = templatesData.templates || [];
|
|
this.categories = categoriesData.categories || [];
|
|
} catch (error) {
|
|
Utils.showToast('Failed to load templates', 'error');
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 4.2 Store Email Templates
|
|
|
|
**File:** `static/store/js/email-templates.js`
|
|
|
|
**Problems:**
|
|
- Used raw `fetch()` instead of `apiClient`
|
|
- Missing parent init call pattern
|
|
- Used `showNotification()` instead of `Utils.showToast()`
|
|
|
|
#### Before
|
|
```javascript
|
|
async init() {
|
|
await this.loadData();
|
|
}
|
|
```
|
|
|
|
#### After
|
|
```javascript
|
|
async init() {
|
|
storeEmailTemplatesLog.info('Email templates init() called');
|
|
|
|
// Call parent init to set storeCode and other base state
|
|
const parentInit = data().init;
|
|
if (parentInit) {
|
|
await parentInit.call(this);
|
|
}
|
|
|
|
await this.loadData();
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Template Fixes
|
|
|
|
### 5.1 Admin Email Templates Template
|
|
|
|
**File:** `app/templates/admin/email-templates.html`
|
|
|
|
**Problem:** Wrong block name (TPL-015 violation).
|
|
|
|
```html
|
|
<!-- Before -->
|
|
{% block scripts %}
|
|
|
|
<!-- After -->
|
|
{% block extra_scripts %}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.2 Shop Base Template
|
|
|
|
**File:** `app/templates/storefront/base.html`
|
|
|
|
**Problem:** `request.state.language` used without default (LANG-009 violation).
|
|
|
|
```html
|
|
<!-- Before -->
|
|
language: '{{ request.state.language }}'
|
|
|
|
<!-- After -->
|
|
language: '{{ request.state.language|default("fr") }}'
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Architecture Validator Fix
|
|
|
|
### 6.1 LANG-001 False Positive
|
|
|
|
**File:** `scripts/validate/validate_architecture.py`
|
|
|
|
**Problem:** Validator flagged "English" as invalid language code in `SUPPORTED_LANGUAGES` dict (where it's a display name, not a code).
|
|
|
|
**Solution:** Extended pattern to skip `SUPPORTED_LANGUAGES` and `SUPPORTED_LOCALES` blocks.
|
|
|
|
```python
|
|
# Before: Only skipped LANGUAGE_NAMES blocks
|
|
if ("LANGUAGE_NAMES" in line or "LANGUAGE_NAMES_EN" in line) and "=" in line:
|
|
in_language_names_dict = True
|
|
|
|
# After: Also skips SUPPORTED_LANGUAGES and SUPPORTED_LOCALES
|
|
if any(
|
|
name in line
|
|
for name in [
|
|
"LANGUAGE_NAMES",
|
|
"LANGUAGE_NAMES_EN",
|
|
"SUPPORTED_LANGUAGES",
|
|
"SUPPORTED_LOCALES",
|
|
]
|
|
) and "=" in line:
|
|
in_language_names_block = True
|
|
if in_language_names_block and stripped in ("}", "]"):
|
|
in_language_names_block = False
|
|
```
|
|
|
|
---
|
|
|
|
## Architecture Rules Reference
|
|
|
|
| Rule ID | Description | Fixed Files |
|
|
|---------|-------------|-------------|
|
|
| API-001 | Endpoint must use Pydantic models | admin/email_templates.py |
|
|
| API-003 | Endpoint must NOT contain business logic | store/settings.py, storefront/auth.py |
|
|
| JS-001 | Must use apiClient for API calls | email-templates.js (admin & store) |
|
|
| JS-002 | Must use Utils.showToast() for notifications | email-templates.js (admin & store) |
|
|
| JS-003 | Must call parent init for Alpine components | store/email-templates.js |
|
|
| TPL-015 | Must use correct block names | admin/email-templates.html |
|
|
| LANG-009 | Must provide language default | storefront/base.html |
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
After these changes:
|
|
|
|
```bash
|
|
$ python scripts/validate/validate_architecture.py
|
|
|
|
Files checked: 439
|
|
Findings: 0 errors, 141 warnings, 2 info
|
|
```
|
|
|
|
The remaining warnings are acceptable patterns (mostly `db.commit()` in services which is allowed for complex operations).
|
|
|
|
---
|
|
|
|
## Commit Reference
|
|
|
|
```
|
|
commit 5155ef7
|
|
fix: resolve all architecture validation errors (62 -> 0)
|
|
|
|
10 files changed, 394 insertions(+), 380 deletions(-)
|
|
```
|