fix: resolve JS-005, JS-006, SVC-006 architecture violations

- JS-005: Add initialization guards to email-templates.js (admin/vendor)
- JS-006: Add try/catch error handling to content-pages.js init
- SVC-006: Move db.commit() from services to endpoints for proper
  transaction control in email_template_service and vendor_team_service

🤖 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 20:00:10 +01:00
parent 87056ea65c
commit 6df7167f80
8 changed files with 75 additions and 15 deletions

View File

@@ -180,6 +180,7 @@ def update_template(
body_html=template_data.body_html,
body_text=template_data.body_text,
)
db.commit()
return {"message": "Template updated successfully"}

View File

@@ -126,7 +126,7 @@ def update_template_override(
vendor_id = current_user.token_vendor_id
service = EmailTemplateService(db)
return service.create_or_update_vendor_override(
result = service.create_or_update_vendor_override(
vendor_id=vendor_id,
code=code,
language=language,
@@ -135,6 +135,9 @@ def update_template_override(
body_text=template_data.body_text,
name=template_data.name,
)
db.commit()
return result
@router.delete("/{code}/{language}")
@@ -152,6 +155,7 @@ def delete_template_override(
vendor_id = current_user.token_vendor_id
service = EmailTemplateService(db)
service.delete_vendor_override(vendor_id, code, language)
db.commit()
return {
"message": "Template override deleted - reverted to platform default",

View File

@@ -385,6 +385,7 @@ def list_roles(
vendor = request.state.vendor
roles = vendor_team_service.get_vendor_roles(db=db, vendor_id=vendor.id)
db.commit() # Commit in case default roles were created
return RoleListResponse(roles=roles, total=len(roles))

View File

@@ -220,7 +220,6 @@ class EmailTemplateService:
template.body_html = body_html
template.body_text = body_text
self.db.commit()
logger.info(f"Updated platform template: {code}/{language}")
def preview_template(
@@ -575,8 +574,6 @@ class EmailTemplateService:
name=name,
)
self.db.commit()
logger.info(f"Vendor {vendor_id} updated template override: {code}/{language}")
return {
@@ -614,7 +611,6 @@ class EmailTemplateService:
if not deleted:
raise ResourceNotFoundException("No override found for this template and language")
self.db.commit()
logger.info(f"Vendor {vendor_id} deleted template override: {code}/{language}")
def preview_vendor_template(

View File

@@ -406,20 +406,65 @@ class VendorTeamService:
"id": vu.user.id,
"email": vu.user.email,
"username": vu.user.username,
"first_name": vu.user.first_name,
"last_name": vu.user.last_name,
"full_name": vu.user.full_name,
"user_type": vu.user_type,
"role": vu.role.name if vu.role else "owner",
"role_name": vu.role.name if vu.role else "owner",
"role_id": vu.role.id if vu.role else None,
"permissions": vu.get_all_permissions(),
"is_active": vu.is_active,
"is_owner": vu.is_owner,
"invitation_pending": vu.is_invitation_pending,
"invited_at": vu.invitation_sent_at,
"accepted_at": vu.invitation_accepted_at,
"joined_at": vu.invitation_accepted_at or vu.created_at or vu.user.created_at,
}
)
return members
def get_vendor_roles(self, db: Session, vendor_id: int) -> list[dict[str, Any]]:
"""
Get all roles for a vendor.
Creates default preset roles if none exist.
Args:
db: Database session
vendor_id: Vendor ID
Returns:
List of role info dicts
"""
roles = db.query(Role).filter(Role.vendor_id == vendor_id).all()
# Create default roles if none exist
if not roles:
default_role_names = ["manager", "staff", "support", "viewer", "marketing"]
for role_name in default_role_names:
permissions = list(get_preset_permissions(role_name))
role = Role(
vendor_id=vendor_id,
name=role_name,
permissions=permissions,
)
db.add(role)
db.flush() # Flush to get IDs without committing (endpoint commits)
roles = db.query(Role).filter(Role.vendor_id == vendor_id).all()
return [
{
"id": role.id,
"name": role.name,
"permissions": role.permissions or [],
"vendor_id": role.vendor_id,
"created_at": role.created_at,
"updated_at": role.updated_at,
}
for role in roles
]
# Private helper methods
def _generate_invitation_token(self) -> str:

View File

@@ -51,6 +51,9 @@ function emailTemplatesPage() {
// Lifecycle
async init() {
if (window._adminEmailTemplatesInitialized) return;
window._adminEmailTemplatesInitialized = true;
await this.loadData();
},

View File

@@ -36,15 +36,21 @@ 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);
try {
// 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 ===');
} catch (error) {
contentPagesLog.error('Failed to initialize content pages:', error);
this.error = 'Failed to initialize. Please refresh the page.';
this.loading = false;
}
await this.loadPages();
contentPagesLog.info('=== VENDOR CONTENT PAGES MANAGER INITIALIZATION COMPLETE ===');
},
// Load all pages
@@ -188,7 +194,8 @@ function vendorContentPagesManager() {
formatDate(dateStr) {
if (!dateStr) return '—';
const date = new Date(dateStr);
return date.toLocaleDateString('en-GB', {
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
return date.toLocaleDateString(locale, {
day: '2-digit',
month: 'short',
year: 'numeric'

View File

@@ -54,6 +54,9 @@ function vendorEmailTemplates() {
// Lifecycle
async init() {
if (window._vendorEmailTemplatesInitialized) return;
window._vendorEmailTemplatesInitialized = true;
vendorEmailTemplatesLog.info('Email templates init() called');
// Call parent init to set vendorCode and other base state