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

@@ -55,22 +55,16 @@ function emailTemplatesPage() {
async loadData() {
this.loading = true;
try {
const [templatesRes, categoriesRes] = await Promise.all([
fetch('/api/v1/admin/email-templates'),
fetch('/api/v1/admin/email-templates/categories')
const [templatesData, categoriesData] = await Promise.all([
apiClient.get('/admin/email-templates'),
apiClient.get('/admin/email-templates/categories')
]);
if (templatesRes.ok) {
this.templates = await templatesRes.json();
}
if (categoriesRes.ok) {
const data = await categoriesRes.json();
this.categories = data.categories || [];
}
this.templates = templatesData.templates || [];
this.categories = categoriesData.categories || [];
} catch (error) {
console.error('Failed to load email templates:', error);
this.showNotification('Failed to load templates', 'error');
Utils.showToast('Failed to load templates', 'error');
} finally {
this.loading = false;
}
@@ -101,20 +95,19 @@ function emailTemplatesPage() {
this.loadingTemplate = true;
try {
const response = await fetch(
`/api/v1/admin/email-templates/${this.editingTemplate.code}/${this.editLanguage}`
const data = await apiClient.get(
`/admin/email-templates/${this.editingTemplate.code}/${this.editLanguage}`
);
if (response.ok) {
const data = await response.json();
this.editForm = {
subject: data.subject || '',
body_html: data.body_html || '',
body_text: data.body_text || '',
variables: data.variables || [],
required_variables: data.required_variables || []
};
} else if (response.status === 404) {
this.editForm = {
subject: data.subject || '',
body_html: data.body_html || '',
body_text: data.body_text || '',
variables: data.variables || [],
required_variables: data.required_variables || []
};
} catch (error) {
if (error.status === 404) {
// Template doesn't exist for this language yet
this.editForm = {
subject: '',
@@ -123,11 +116,11 @@ function emailTemplatesPage() {
variables: [],
required_variables: []
};
this.showNotification(`No template for ${this.editLanguage.toUpperCase()} - create one by saving`, 'info');
Utils.showToast(`No template for ${this.editLanguage.toUpperCase()} - create one by saving`, 'info');
} else {
console.error('Failed to load template:', error);
Utils.showToast('Failed to load template', 'error');
}
} catch (error) {
console.error('Failed to load template:', error);
this.showNotification('Failed to load template', 'error');
} finally {
this.loadingTemplate = false;
}
@@ -150,30 +143,21 @@ function emailTemplatesPage() {
this.saving = true;
try {
const response = await fetch(
`/api/v1/admin/email-templates/${this.editingTemplate.code}/${this.editLanguage}`,
await apiClient.put(
`/admin/email-templates/${this.editingTemplate.code}/${this.editLanguage}`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
subject: this.editForm.subject,
body_html: this.editForm.body_html,
body_text: this.editForm.body_text
})
subject: this.editForm.subject,
body_html: this.editForm.body_html,
body_text: this.editForm.body_text
}
);
if (response.ok) {
this.showNotification('Template saved successfully', 'success');
// Refresh templates list
await this.loadData();
} else {
const error = await response.json();
this.showNotification(error.detail || 'Failed to save template', 'error');
}
Utils.showToast('Template saved successfully', 'success');
// Refresh templates list
await this.loadData();
} catch (error) {
console.error('Failed to save template:', error);
this.showNotification('Failed to save template', 'error');
Utils.showToast(error.detail || 'Failed to save template', 'error');
} finally {
this.saving = false;
}
@@ -185,28 +169,20 @@ function emailTemplatesPage() {
// Use sample variables for preview
const sampleVariables = this.getSampleVariables(template.code);
const response = await fetch(
`/api/v1/admin/email-templates/${template.code}/preview`,
const data = await apiClient.post(
`/admin/email-templates/${template.code}/preview`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
template_code: template.code,
language: 'en',
variables: sampleVariables
})
template_code: template.code,
language: 'en',
variables: sampleVariables
}
);
if (response.ok) {
this.previewData = await response.json();
this.showPreviewModal = true;
} else {
this.showNotification('Failed to load preview', 'error');
}
this.previewData = data;
this.showPreviewModal = true;
} catch (error) {
console.error('Failed to preview template:', error);
this.showNotification('Failed to load preview', 'error');
Utils.showToast('Failed to load preview', 'error');
}
},
@@ -257,47 +233,29 @@ function emailTemplatesPage() {
this.sendingTest = true;
try {
const response = await fetch(
`/api/v1/admin/email-templates/${this.editingTemplate.code}/test`,
const result = await apiClient.post(
`/admin/email-templates/${this.editingTemplate.code}/test`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
template_code: this.editingTemplate.code,
language: this.editLanguage,
to_email: this.testEmailAddress,
variables: this.getSampleVariables(this.editingTemplate.code)
})
template_code: this.editingTemplate.code,
language: this.editLanguage,
to_email: this.testEmailAddress,
variables: this.getSampleVariables(this.editingTemplate.code)
}
);
const result = await response.json();
if (result.success) {
this.showNotification(`Test email sent to ${this.testEmailAddress}`, 'success');
Utils.showToast(`Test email sent to ${this.testEmailAddress}`, 'success');
this.showTestEmailModal = false;
this.testEmailAddress = '';
} else {
this.showNotification(result.message || 'Failed to send test email', 'error');
Utils.showToast(result.message || 'Failed to send test email', 'error');
}
} catch (error) {
console.error('Failed to send test email:', error);
this.showNotification('Failed to send test email', 'error');
Utils.showToast('Failed to send test email', 'error');
} finally {
this.sendingTest = false;
}
},
// Notifications
showNotification(message, type = 'info') {
// Use global notification system if available
if (window.showToast) {
window.showToast(message, type);
} else if (window.Alpine && Alpine.store('notifications')) {
Alpine.store('notifications').add(message, type);
} else {
console.log(`[${type.toUpperCase()}] ${message}`);
}
}
};
}

View File

@@ -54,6 +54,14 @@ function vendorEmailTemplates() {
// Lifecycle
async init() {
vendorEmailTemplatesLog.info('Email templates init() called');
// Call parent init to set vendorCode and other base state
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
await this.loadData();
},
@@ -63,37 +71,17 @@ function vendorEmailTemplates() {
this.error = '';
try {
const response = await fetch('/api/v1/vendor/email-templates', {
headers: {
'Authorization': `Bearer ${this.getAuthToken()}`
}
});
if (response.ok) {
const data = await response.json();
this.templates = data.templates || [];
this.supportedLanguages = data.supported_languages || ['en', 'fr', 'de', 'lb'];
} else {
const error = await response.json();
this.error = error.detail || 'Failed to load templates';
}
const response = await apiClient.get('/vendor/email-templates');
this.templates = response.templates || [];
this.supportedLanguages = response.supported_languages || ['en', 'fr', 'de', 'lb'];
} catch (error) {
vendorEmailTemplatesLog.error('Failed to load templates:', error);
this.error = 'Failed to load templates';
this.error = error.detail || 'Failed to load templates';
} finally {
this.loading = false;
}
},
// Auth token helper
getAuthToken() {
// Get from cookie or localStorage depending on your auth setup
return document.cookie
.split('; ')
.find(row => row.startsWith('vendor_token='))
?.split('=')[1] || '';
},
// Category styling
getCategoryClass(category) {
const classes = {
@@ -121,24 +109,18 @@ function vendorEmailTemplates() {
this.loadingTemplate = true;
try {
const response = await fetch(
`/api/v1/vendor/email-templates/${this.editingTemplate.code}/${this.editLanguage}`,
{
headers: {
'Authorization': `Bearer ${this.getAuthToken()}`
}
}
const data = await apiClient.get(
`/vendor/email-templates/${this.editingTemplate.code}/${this.editLanguage}`
);
if (response.ok) {
const data = await response.json();
this.templateSource = data.source;
this.editForm = {
subject: data.subject || '',
body_html: data.body_html || '',
body_text: data.body_text || ''
};
} else if (response.status === 404) {
this.templateSource = data.source;
this.editForm = {
subject: data.subject || '',
body_html: data.body_html || '',
body_text: data.body_text || ''
};
} catch (error) {
if (error.status === 404) {
// No template for this language
this.templateSource = 'none';
this.editForm = {
@@ -146,11 +128,11 @@ function vendorEmailTemplates() {
body_html: '',
body_text: ''
};
this.showNotification(`No template available for ${this.editLanguage.toUpperCase()}`, 'info');
Utils.showToast(`No template available for ${this.editLanguage.toUpperCase()}`, 'info');
} else {
vendorEmailTemplatesLog.error('Failed to load template:', error);
Utils.showToast('Failed to load template', 'error');
}
} catch (error) {
vendorEmailTemplatesLog.error('Failed to load template:', error);
this.showNotification('Failed to load template', 'error');
} finally {
this.loadingTemplate = false;
}
@@ -172,34 +154,22 @@ function vendorEmailTemplates() {
this.saving = true;
try {
const response = await fetch(
`/api/v1/vendor/email-templates/${this.editingTemplate.code}/${this.editLanguage}`,
await apiClient.put(
`/vendor/email-templates/${this.editingTemplate.code}/${this.editLanguage}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getAuthToken()}`
},
body: JSON.stringify({
subject: this.editForm.subject,
body_html: this.editForm.body_html,
body_text: this.editForm.body_text || null
})
subject: this.editForm.subject,
body_html: this.editForm.body_html,
body_text: this.editForm.body_text || null
}
);
if (response.ok) {
this.showNotification('Template saved successfully', 'success');
this.templateSource = 'vendor_override';
// Refresh list to show updated status
await this.loadData();
} else {
const error = await response.json();
this.showNotification(error.detail || 'Failed to save template', 'error');
}
Utils.showToast('Template saved successfully', 'success');
this.templateSource = 'vendor_override';
// Refresh list to show updated status
await this.loadData();
} catch (error) {
vendorEmailTemplatesLog.error('Failed to save template:', error);
this.showNotification('Failed to save template', 'error');
Utils.showToast(error.detail || 'Failed to save template', 'error');
} finally {
this.saving = false;
}
@@ -215,29 +185,18 @@ function vendorEmailTemplates() {
this.reverting = true;
try {
const response = await fetch(
`/api/v1/vendor/email-templates/${this.editingTemplate.code}/${this.editLanguage}`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${this.getAuthToken()}`
}
}
await apiClient.delete(
`/vendor/email-templates/${this.editingTemplate.code}/${this.editLanguage}`
);
if (response.ok) {
this.showNotification('Reverted to platform default', 'success');
// Reload the template to show platform version
await this.loadTemplateLanguage();
// Refresh list
await this.loadData();
} else {
const error = await response.json();
this.showNotification(error.detail || 'Failed to revert', 'error');
}
Utils.showToast('Reverted to platform default', 'success');
// Reload the template to show platform version
await this.loadTemplateLanguage();
// Refresh list
await this.loadData();
} catch (error) {
vendorEmailTemplatesLog.error('Failed to revert template:', error);
this.showNotification('Failed to revert', 'error');
Utils.showToast(error.detail || 'Failed to revert', 'error');
} finally {
this.reverting = false;
}
@@ -248,30 +207,19 @@ function vendorEmailTemplates() {
if (!this.editingTemplate) return;
try {
const response = await fetch(
`/api/v1/vendor/email-templates/${this.editingTemplate.code}/preview`,
const data = await apiClient.post(
`/vendor/email-templates/${this.editingTemplate.code}/preview`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getAuthToken()}`
},
body: JSON.stringify({
language: this.editLanguage,
variables: {}
})
language: this.editLanguage,
variables: {}
}
);
if (response.ok) {
this.previewData = await response.json();
this.showPreviewModal = true;
} else {
this.showNotification('Failed to load preview', 'error');
}
this.previewData = data;
this.showPreviewModal = true;
} catch (error) {
vendorEmailTemplatesLog.error('Failed to preview template:', error);
this.showNotification('Failed to load preview', 'error');
Utils.showToast('Failed to load preview', 'error');
}
},
@@ -286,49 +234,28 @@ function vendorEmailTemplates() {
this.sendingTest = true;
try {
const response = await fetch(
`/api/v1/vendor/email-templates/${this.editingTemplate.code}/test`,
const result = await apiClient.post(
`/vendor/email-templates/${this.editingTemplate.code}/test`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getAuthToken()}`
},
body: JSON.stringify({
to_email: this.testEmailAddress,
language: this.editLanguage,
variables: {}
})
to_email: this.testEmailAddress,
language: this.editLanguage,
variables: {}
}
);
const result = await response.json();
if (result.success) {
this.showNotification(`Test email sent to ${this.testEmailAddress}`, 'success');
Utils.showToast(`Test email sent to ${this.testEmailAddress}`, 'success');
this.showTestEmailModal = false;
this.testEmailAddress = '';
} else {
this.showNotification(result.message || 'Failed to send test email', 'error');
Utils.showToast(result.message || 'Failed to send test email', 'error');
}
} catch (error) {
vendorEmailTemplatesLog.error('Failed to send test email:', error);
this.showNotification('Failed to send test email', 'error');
Utils.showToast('Failed to send test email', 'error');
} finally {
this.sendingTest = false;
}
},
// Notifications
showNotification(message, type = 'info') {
// Use global notification system if available
if (window.showToast) {
window.showToast(message, type);
} else if (window.Alpine && Alpine.store('notifications')) {
Alpine.store('notifications').add(message, type);
} else {
console.log(`[${type.toUpperCase()}] ${message}`);
}
}
};
}