Multitenant implementation with custom Domain, theme per vendor

This commit is contained in:
2025-10-26 23:49:29 +01:00
parent c88775134d
commit 1e0cbf5927
24 changed files with 3470 additions and 624 deletions

View File

@@ -0,0 +1,458 @@
# VENDOR THEME EDITOR - COMPLETE IMPLEMENTATION GUIDE
Following Your Frontend Architecture
## 📋 Overview
This implementation follows your **exact Alpine.js architecture pattern** with:
- ✅ Proper logging setup
-`...data()` inheritance
- ✅ Initialization guard pattern
- ✅ Lowercase `apiClient` usage
- ✅ Page-specific logger (`themeLog`)
-`currentPage` identifier
- ✅ Performance tracking
- ✅ Proper error handling
---
## 📦 Files to Install
### 1. JavaScript Component (Alpine.js)
**File:** `vendor-theme-alpine.js`
**Install to:** `static/admin/js/vendor-theme.js`
```bash
cp vendor-theme-alpine.js static/admin/js/vendor-theme.js
```
**Key Features:**
- ✅ Follows `dashboard.js` pattern exactly
- ✅ Uses `adminVendorTheme()` function name
- ✅ Inherits base with `...data()`
- ✅ Sets `currentPage: 'vendor-theme'`
- ✅ Has initialization guard
- ✅ Uses lowercase `apiClient`
- ✅ Has `themeLog` logger
- ✅ Performance tracking with `Date.now()`
---
### 2. API Endpoints (Backend)
**File:** `vendor_themes_api.py`
**Install to:** `app/api/v1/admin/vendor_themes.py`
```bash
cp vendor_themes_api.py app/api/v1/admin/vendor_themes.py
```
**Endpoints:**
```
GET /api/v1/admin/vendor-themes/presets
GET /api/v1/admin/vendor-themes/{vendor_code}
PUT /api/v1/admin/vendor-themes/{vendor_code}
POST /api/v1/admin/vendor-themes/{vendor_code}/preset/{preset_name}
DELETE /api/v1/admin/vendor-themes/{vendor_code}
```
---
### 3. Pydantic Schemas
**File:** `vendor_theme_schemas.py`
**Install to:** `models/schema/vendor_theme.py`
```bash
cp vendor_theme_schemas.py models/schema/vendor_theme.py
```
---
### 4. HTML Template
**File:** `vendor-theme.html`
**Install to:** `app/templates/admin/vendor-theme.html`
```bash
cp vendor-theme.html app/templates/admin/vendor-theme.html
```
**Key Features:**
- ✅ Extends `admin/base.html`
- ✅ Uses `{% block alpine_data %}adminVendorTheme(){% endblock %}`
- ✅ Loads script in `{% block extra_scripts %}`
- ✅ 7 preset buttons
- ✅ 6 color pickers
- ✅ Font and layout selectors
- ✅ Live preview panel
---
### 5. Frontend Router Update
**File:** `pages-updated.py`
**Install to:** `app/api/v1/admin/pages.py`
```bash
cp pages-updated.py app/api/v1/admin/pages.py
```
**Change:**
- Added route: `GET /vendors/{vendor_code}/theme`
- Returns: `admin/vendor-theme.html`
---
### 6. API Router Registration
**File:** `__init__-updated.py`
**Install to:** `app/api/v1/admin/__init__.py`
```bash
cp __init__-updated.py app/api/v1/admin/__init__.py
```
**Changes:**
```python
# Added import
from . import vendor_themes
# Added router registration
router.include_router(vendor_themes.router, tags=["admin-vendor-themes"])
```
---
### 7. Theme Presets (if not already installed)
**File:** `theme_presets.py`
**Install to:** `app/core/theme_presets.py`
```bash
cp theme_presets.py app/core/theme_presets.py
```
---
## 🔧 Installation Steps
### Step 1: Copy All Files
```bash
# 1. JavaScript (Frontend)
cp vendor-theme-alpine.js static/admin/js/vendor-theme.js
# 2. API Endpoints (Backend)
cp vendor_themes_api.py app/api/v1/admin/vendor_themes.py
# 3. Pydantic Schemas
cp vendor_theme_schemas.py models/schema/vendor_theme.py
# 4. HTML Template
cp vendor-theme.html app/templates/admin/vendor-theme.html
# 5. Theme Presets (if not done)
cp theme_presets.py app/core/theme_presets.py
# 6. Update Frontend Router
cp pages-updated.py app/api/v1/admin/pages.py
# 7. Update API Router
cp __init__-updated.py app/api/v1/admin/__init__.py
```
---
### Step 2: Verify Database
Make sure the `vendor_themes` table exists:
```bash
# Check migrations
alembic history
# Run migration if needed
alembic upgrade head
```
---
### Step 3: Restart Server
```bash
# Stop server
pkill -f uvicorn
# Start server
python -m uvicorn app.main:app --reload
```
---
## 🧪 Testing
### 1. Check JavaScript Loading
Open browser console and look for:
```
[THEME INFO] Vendor theme editor module loaded
```
---
### 2. Test Page Load
Navigate to:
```
http://localhost:8000/admin/vendors/VENDOR001/theme
```
Expected console output:
```
[THEME INFO] === VENDOR THEME EDITOR INITIALIZING ===
[THEME INFO] Vendor code: VENDOR001
[THEME INFO] Loading vendor data...
[THEME INFO] Vendor loaded in 45ms: Vendor Name
[THEME INFO] Loading theme...
[THEME INFO] Theme loaded in 23ms: default
[THEME INFO] Loading presets...
[THEME INFO] 7 presets loaded in 12ms
[THEME INFO] === THEME EDITOR INITIALIZATION COMPLETE (80ms) ===
```
---
### 3. Test Preset Application
Click "Modern" preset button.
Expected console output:
```
[THEME INFO] Applying preset: modern
[THEME INFO] Preset applied in 56ms
```
Expected UI: Colors and fonts update instantly
---
### 4. Test Color Changes
1. Click primary color picker
2. Choose a new color
3. Preview should update immediately
---
### 5. Test Save
1. Click "Save Theme" button
2. Expected toast: "Theme saved successfully"
3. Expected console:
```
[THEME INFO] Saving theme: {theme_name: 'modern', ...}
[THEME INFO] Theme saved in 34ms
```
---
### 6. Test API Endpoints
```bash
# Get presets
curl http://localhost:8000/api/v1/admin/vendor-themes/presets \
-H "Authorization: Bearer YOUR_TOKEN"
# Get vendor theme
curl http://localhost:8000/api/v1/admin/vendor-themes/VENDOR001 \
-H "Authorization: Bearer YOUR_TOKEN"
# Apply preset
curl -X POST \
http://localhost:8000/api/v1/admin/vendor-themes/VENDOR001/preset/modern \
-H "Authorization: Bearer YOUR_TOKEN"
# Save theme
curl -X PUT \
http://localhost:8000/api/v1/admin/vendor-themes/VENDOR001 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"colors": {
"primary": "#ff0000"
}
}'
```
---
## 🎯 Architecture Compliance Checklist
### JavaScript File ✅
- [x] Logging setup (`themeLog`)
- [x] Function name: `adminVendorTheme()`
- [x] `...data()` at start
- [x] `currentPage: 'vendor-theme'`
- [x] Initialization guard (`window._vendorThemeInitialized`)
- [x] Uses lowercase `apiClient`
- [x] Uses page-specific logger (`themeLog`)
- [x] Performance tracking (`Date.now()`)
- [x] Module loaded log at end
---
### HTML Template ✅
- [x] Extends `admin/base.html`
- [x] `alpine_data` block uses `adminVendorTheme()`
- [x] `x-show` for loading states
- [x] `x-text` for reactive data
- [x] Loads JS in `extra_scripts` block
---
### API Routes ✅
- [x] RESTful endpoint structure
- [x] Proper error handling
- [x] Admin authentication required
- [x] Pydantic validation
- [x] Logging
---
## 📊 Data Flow
```
1. User visits /admin/vendors/VENDOR001/theme
2. pages.py returns vendor-theme.html
3. Template loads vendor-theme.js
4. Alpine.js calls adminVendorTheme()
5. Component spreads ...data() (gets base UI state)
6. Component adds page-specific state
7. init() runs (with guard)
8. Loads vendor data via apiClient.get()
9. Loads theme data via apiClient.get()
10. Loads presets via apiClient.get()
11. UI updates reactively
12. User clicks preset button
13. applyPreset() sends apiClient.post()
14. API applies preset and saves to DB
15. Response updates themeData
16. UI updates (colors, fonts, preview)
```
---
## 🐛 Troubleshooting
### "adminVendorTheme is not defined"
**Problem:** JavaScript file not loaded or function name mismatch
**Fix:**
1. Check template: `{% block alpine_data %}adminVendorTheme(){% endblock %}`
2. Check JS file has: `function adminVendorTheme() {`
3. Check JS file is loaded in `extra_scripts` block
---
### "apiClient is not defined"
**Problem:** `api-client.js` not loaded before your script
**Fix:** Check `base.html` loads scripts in this order:
1. `log-config.js`
2. `icons.js`
3. `init-alpine.js`
4. `utils.js`
5. `api-client.js`
6. Alpine.js CDN
7. Your page script (vendor-theme.js)
---
### Init runs multiple times
**Problem:** Guard not working
**Expected:** You should see this warning in console:
```
⚠️ [THEME WARN] Theme editor already initialized, skipping...
```
**If not appearing:** Check guard is in `init()`:
```javascript
if (window._vendorThemeInitialized) {
themeLog.warn('Theme editor already initialized, skipping...');
return;
}
window._vendorThemeInitialized = true;
```
---
### No console logs
**Problem:** Log level too low
**Fix:** Set `THEME_LOG_LEVEL = 4` at top of vendor-theme.js for all logs
---
## ✅ Final Checklist
- [ ] All 7 files copied
- [ ] API router registered in `__init__.py`
- [ ] Frontend route added in `pages.py`
- [ ] Database has `vendor_themes` table
- [ ] Server restarted
- [ ] Can access `/admin/vendors/VENDOR001/theme`
- [ ] Console shows module loaded log
- [ ] Console shows initialization logs
- [ ] Presets load and work
- [ ] Color pickers work
- [ ] Save button works
- [ ] No console errors
---
## 🎉 Success Indicators
When everything is working:
1. **Console logs:**
```
[THEME INFO] Vendor theme editor module loaded
[THEME INFO] === VENDOR THEME EDITOR INITIALIZING ===
[THEME INFO] === THEME EDITOR INITIALIZATION COMPLETE (XXms) ===
```
2. **No errors** in browser console
3. **Live preview** updates when you change colors
4. **Preset buttons** change theme instantly
5. **Save button** shows success toast
6. **Changes persist** after page refresh
---
## 📚 Related Documentation
- Frontend Architecture: `FRONTEND_ARCHITECTURE_OVERVIEW.txt`
- Alpine.js Template: `FRONTEND_ALPINE_PAGE_TEMPLATE.md`
- Your working example: `static/admin/js/dashboard.js`
---
**Your theme editor now follows your exact frontend architecture! 🎨**

View File

@@ -0,0 +1,360 @@
# THEME PRESETS USAGE GUIDE
## What Changed in Your Presets
### ✅ What You Had Right
- Good preset structure with colors, fonts, layout
- Clean `apply_preset()` function
- Good preset names (modern, classic, minimal, vibrant)
### 🔧 What We Added
1. **Missing color fields:** `background`, `text`, `border`
2. **Missing layout field:** `product_card` style
3. **"default" preset:** Your platform's default theme
4. **Extra presets:** "elegant" and "nature" themes
5. **Helper functions:** `get_preset()`, `get_available_presets()`, `get_preset_preview()`
6. **Custom preset builder:** `create_custom_preset()`
---
## Usage Examples
### 1. Apply Preset to New Vendor
```python
from models.database.vendor_theme import VendorTheme
from app.core.theme_presets import apply_preset
from app.core.database import SessionLocal
# Create theme for vendor
db = SessionLocal()
vendor_id = 1
# Create and apply preset
theme = VendorTheme(vendor_id=vendor_id)
apply_preset(theme, "modern")
db.add(theme)
db.commit()
```
### 2. Change Vendor's Theme
```python
from models.database.vendor_theme import VendorTheme
from app.core.theme_presets import apply_preset
# Get existing theme
theme = db.query(VendorTheme).filter(
VendorTheme.vendor_id == vendor_id
).first()
if theme:
# Update to new preset
apply_preset(theme, "classic")
else:
# Create new theme
theme = VendorTheme(vendor_id=vendor_id)
apply_preset(theme, "classic")
db.add(theme)
db.commit()
```
### 3. Get Available Presets (For UI Dropdown)
```python
from app.core.theme_presets import get_available_presets, get_preset_preview
# Get list of preset names
presets = get_available_presets()
# Returns: ['default', 'modern', 'classic', 'minimal', 'vibrant', 'elegant', 'nature']
# Get preview info for UI
previews = []
for preset_name in presets:
preview = get_preset_preview(preset_name)
previews.append(preview)
# Returns list of dicts with:
# {
# "name": "modern",
# "description": "Contemporary tech-inspired design...",
# "primary_color": "#6366f1",
# "secondary_color": "#8b5cf6",
# ...
# }
```
### 4. API Endpoint to Apply Preset
```python
# In your API route
from fastapi import APIRouter, Depends
from app.core.theme_presets import apply_preset, get_available_presets
@router.put("/theme/preset")
def apply_theme_preset(
preset_name: str,
vendor: Vendor = Depends(require_vendor_context()),
db: Session = Depends(get_db),
):
"""Apply a theme preset to vendor."""
# Validate preset name
if preset_name not in get_available_presets():
raise HTTPException(
status_code=400,
detail=f"Invalid preset. Available: {get_available_presets()}"
)
# Get or create vendor theme
theme = db.query(VendorTheme).filter(
VendorTheme.vendor_id == vendor.id
).first()
if not theme:
theme = VendorTheme(vendor_id=vendor.id)
db.add(theme)
# Apply preset
apply_preset(theme, preset_name)
db.commit()
db.refresh(theme)
return {
"message": f"Theme preset '{preset_name}' applied successfully",
"theme": theme.to_dict()
}
```
### 5. Get All Presets for Theme Selector
```python
@router.get("/theme/presets")
def get_theme_presets():
"""Get all available theme presets with previews."""
from app.core.theme_presets import get_available_presets, get_preset_preview
presets = []
for preset_name in get_available_presets():
preview = get_preset_preview(preset_name)
presets.append(preview)
return {"presets": presets}
# Returns:
# {
# "presets": [
# {
# "name": "default",
# "description": "Clean and professional...",
# "primary_color": "#6366f1",
# "secondary_color": "#8b5cf6",
# "accent_color": "#ec4899",
# "heading_font": "Inter, sans-serif",
# "body_font": "Inter, sans-serif",
# "layout_style": "grid"
# },
# ...
# ]
# }
```
### 6. Create Custom Theme (Not from Preset)
```python
from app.core.theme_presets import create_custom_preset
# User provides custom colors
custom_preset = create_custom_preset(
colors={
"primary": "#ff0000",
"secondary": "#00ff00",
"accent": "#0000ff",
"background": "#ffffff",
"text": "#000000",
"border": "#cccccc"
},
fonts={
"heading": "Arial, sans-serif",
"body": "Verdana, sans-serif"
},
layout={
"style": "grid",
"header": "fixed",
"product_card": "modern"
},
name="my_custom"
)
# Apply to vendor theme
theme = VendorTheme(vendor_id=vendor_id)
theme.theme_name = "custom"
theme.colors = custom_preset["colors"]
theme.font_family_heading = custom_preset["fonts"]["heading"]
theme.font_family_body = custom_preset["fonts"]["body"]
theme.layout_style = custom_preset["layout"]["style"]
theme.header_style = custom_preset["layout"]["header"]
theme.product_card_style = custom_preset["layout"]["product_card"]
theme.is_active = True
db.add(theme)
db.commit()
```
---
## Available Presets
| Preset | Description | Primary Color | Use Case |
|--------|-------------|---------------|----------|
| `default` | Clean & professional | Indigo (#6366f1) | General purpose |
| `modern` | Tech-inspired | Indigo (#6366f1) | Tech products |
| `classic` | Traditional | Dark Blue (#1e40af) | Established brands |
| `minimal` | Ultra-clean B&W | Black (#000000) | Minimalist brands |
| `vibrant` | Bold & energetic | Orange (#f59e0b) | Creative brands |
| `elegant` | Sophisticated | Gray (#6b7280) | Luxury products |
| `nature` | Eco-friendly | Green (#059669) | Organic/eco brands |
---
## Complete Preset Structure
Each preset includes:
```python
{
"colors": {
"primary": "#6366f1", # Main brand color
"secondary": "#8b5cf6", # Supporting color
"accent": "#ec4899", # Call-to-action color
"background": "#ffffff", # Page background
"text": "#1f2937", # Text color
"border": "#e5e7eb" # Border/divider color
},
"fonts": {
"heading": "Inter, sans-serif", # Headings (h1-h6)
"body": "Inter, sans-serif" # Body text
},
"layout": {
"style": "grid", # grid | list | masonry
"header": "fixed", # fixed | static | transparent
"product_card": "modern" # modern | classic | minimal
}
}
```
---
## Integration with Admin Panel
### Theme Editor UI Flow
1. **Preset Selector**
```javascript
// Fetch available presets
fetch('/api/v1/vendor/theme/presets')
.then(r => r.json())
.then(data => {
// Display preset cards with previews
data.presets.forEach(preset => {
showPresetCard(preset.name, preset.primary_color, preset.description)
})
})
```
2. **Apply Preset Button**
```javascript
function applyPreset(presetName) {
fetch('/api/v1/vendor/theme/preset', {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({preset_name: presetName})
})
.then(() => {
alert('Theme updated!')
location.reload() // Refresh to show new theme
})
}
```
3. **Custom Color Picker** (After applying preset)
```javascript
// User can then customize colors
function updateColors(colors) {
fetch('/api/v1/vendor/theme/colors', {
method: 'PUT',
body: JSON.stringify({colors})
})
}
```
---
## Testing Presets
```python
# Test script
from app.core.theme_presets import apply_preset, get_available_presets
from models.database.vendor_theme import VendorTheme
def test_all_presets():
"""Test applying all presets"""
presets = get_available_presets()
for preset_name in presets:
theme = VendorTheme(vendor_id=999) # Test vendor
apply_preset(theme, preset_name)
assert theme.theme_name == preset_name
assert theme.colors is not None
assert theme.font_family_heading is not None
assert theme.is_active == True
print(f"✅ {preset_name} preset OK")
test_all_presets()
```
---
## CSS Variables Generation
Your middleware already handles this via `VendorTheme.to_dict()`, which includes:
```python
"css_variables": {
"--color-primary": "#6366f1",
"--color-secondary": "#8b5cf6",
"--color-accent": "#ec4899",
"--color-background": "#ffffff",
"--color-text": "#1f2937",
"--color-border": "#e5e7eb",
"--font-heading": "Inter, sans-serif",
"--font-body": "Inter, sans-serif",
}
```
Use in templates:
```html
<style>
:root {
{% for key, value in theme.css_variables.items() %}
{{ key }}: {{ value }};
{% endfor %}
}
</style>
```
---
## Next Steps
1. ✅ Copy `theme_presets.py` to `app/core/theme_presets.py`
2. ✅ Create API endpoints for applying presets
3. ✅ Build theme selector UI in admin panel
4. ✅ Test all presets work correctly
5. ✅ Add custom color picker for fine-tuning
Perfect! Your presets are now complete and production-ready! 🎨

View File

@@ -0,0 +1,536 @@
# VENDOR THEME BACKEND - PROPER ARCHITECTURE IMPLEMENTATION
## Overview
This implementation follows your **complete backend architecture** with:
-**Separation of Concerns** - Service layer handles business logic
-**Exception Management** - Custom exceptions for all error cases
-**Proper Layering** - Database → Service → API → Frontend
-**Naming Conventions** - Follows your established patterns
-**No Business Logic in Endpoints** - All logic in service layer
---
## 📦 File Structure
```
app/
├── exceptions/
│ ├── __init__.py # ← UPDATE (add vendor_theme imports)
│ └── vendor_theme.py # ← NEW (custom exceptions)
├── services/
│ └── vendor_theme_service.py # ← NEW (business logic)
└── api/v1/admin/
├── __init__.py # ← UPDATE (register router)
├── pages.py # ← UPDATE (add theme route)
└── vendor_themes.py # ← NEW (endpoints only)
models/
└── schema/
└── vendor_theme.py # ← Already created (Pydantic)
static/admin/js/
└── vendor-theme.js # ← Already created (Alpine.js)
app/templates/admin/
└── vendor-theme.html # ← Already created (HTML)
```
---
## 🏗️ Architecture Layers
### Layer 1: Exceptions (Error Handling)
**File:** `app/exceptions/vendor_theme.py`
```python
# Custom exceptions for domain-specific errors
- VendorThemeNotFoundException
- ThemePresetNotFoundException
- InvalidThemeDataException
- ThemeValidationException
- InvalidColorFormatException
- InvalidFontFamilyException
- ThemeOperationException
```
**Key Points:**
- ✅ Extends base exceptions (`ResourceNotFoundException`, etc.)
- ✅ Provides specific error codes
- ✅ Includes detailed error information
- ✅ No HTTP status codes (that's endpoint layer)
---
### Layer 2: Service (Business Logic)
**File:** `app/services/vendor_theme_service.py`
**Responsibilities:**
- ✅ Get/Create/Update/Delete themes
- ✅ Apply presets
- ✅ Validate theme data
- ✅ Database operations
- ✅ Raise custom exceptions
**Does NOT:**
- ❌ Handle HTTP requests/responses
- ❌ Raise HTTPException
- ❌ Know about FastAPI
**Example Service Method:**
```python
def update_theme(
self,
db: Session,
vendor_code: str,
theme_data: VendorThemeUpdate
) -> VendorTheme:
"""Update theme - returns VendorTheme or raises exception."""
try:
vendor = self._get_vendor_by_code(db, vendor_code)
theme = # ... get or create theme
# Validate
self._validate_theme_data(theme_data)
# Update
self._apply_theme_updates(theme, theme_data)
db.commit()
return theme
except CustomException:
raise # Re-raise custom exceptions
except Exception as e:
db.rollback()
raise ThemeOperationException(...) # Wrap generic errors
```
---
### Layer 3: API Endpoints (HTTP Interface)
**File:** `app/api/v1/admin/vendor_themes.py`
**Responsibilities:**
- ✅ Handle HTTP requests
- ✅ Call service methods
- ✅ Convert exceptions to HTTP responses
- ✅ Return JSON responses
**Does NOT:**
- ❌ Contain business logic
- ❌ Validate data (service does this)
- ❌ Access database directly
**Example Endpoint:**
```python
@router.put("/{vendor_code}")
async def update_vendor_theme(
vendor_code: str,
theme_data: VendorThemeUpdate,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user)
):
"""Endpoint - just calls service and handles errors."""
try:
# Call service (all logic there)
theme = vendor_theme_service.update_theme(db, vendor_code, theme_data)
# Return response
return theme.to_dict()
except VendorNotFoundException:
raise HTTPException(status_code=404, detail="...")
except ThemeValidationException as e:
raise HTTPException(status_code=422, detail=e.message)
except Exception as e:
logger.error(...)
raise HTTPException(status_code=500, detail="...")
```
---
## 📥 Installation
### Step 1: Install Exception Layer
```bash
# 1. Copy exception file
cp vendor_theme_exceptions.py app/exceptions/vendor_theme.py
# 2. Update exceptions __init__.py
cp exceptions__init__-updated.py app/exceptions/__init__.py
```
**Verify:**
```python
from app.exceptions import VendorThemeNotFoundException # Should work
```
---
### Step 2: Install Service Layer
```bash
# Copy service file
cp vendor_theme_service.py app/services/vendor_theme_service.py
```
**Verify:**
```python
from app.services.vendor_theme_service import vendor_theme_service # Should work
```
---
### Step 3: Install API Layer
```bash
# Copy API endpoints
cp vendor_themes_endpoints.py app/api/v1/admin/vendor_themes.py
```
**Verify:**
```python
from app.api.v1.admin import vendor_themes # Should work
```
---
### Step 4: Register Router
```bash
# Update API router
cp __init__-updated.py app/api/v1/admin/__init__.py
```
**Changes:**
```python
# Added:
from . import vendor_themes
router.include_router(vendor_themes.router, tags=["admin-vendor-themes"])
```
---
### Step 5: Update Frontend Router
```bash
# Update pages router
cp pages-updated.py app/api/v1/admin/pages.py
```
**Added Route:**
```python
@router.get("/vendors/{vendor_code}/theme")
async def admin_vendor_theme_page(...):
return templates.TemplateResponse("admin/vendor-theme.html", ...)
```
---
### Step 6: Copy Frontend Files (Already Done)
```bash
# JavaScript
cp vendor-theme-alpine.js static/admin/js/vendor-theme.js
# HTML Template
cp vendor-theme.html app/templates/admin/vendor-theme.html
# Pydantic Schemas
cp vendor_theme_schemas.py models/schema/vendor_theme.py
# Theme Presets
cp theme_presets.py app/core/theme_presets.py
```
---
## 🔄 Data Flow
### Complete Request Flow
```
1. User clicks "Save Theme" button
2. JavaScript (vendor-theme.js)
- apiClient.put('/api/v1/admin/vendor-themes/VENDOR001', data)
3. API Endpoint (vendor_themes.py)
- Receives HTTP PUT request
- Validates admin authentication
- Calls service layer
4. Service Layer (vendor_theme_service.py)
- Validates theme data
- Gets vendor from database
- Creates/updates VendorTheme
- Commits transaction
- Returns VendorTheme object
5. API Endpoint
- Converts VendorTheme to dict
- Returns JSON response
6. JavaScript
- Receives response
- Shows success toast
- Updates UI
```
### Error Flow
```
1. Service detects invalid color
2. Service raises InvalidColorFormatException
3. API endpoint catches exception
4. API converts to HTTPException(422)
5. FastAPI returns JSON error
6. JavaScript catches error
7. Shows error toast to user
```
---
## ✅ Architecture Compliance
### Separation of Concerns ✅
| Layer | Responsibilities | ✅ |
|-------|-----------------|-----|
| **Exceptions** | Define error types | ✅ |
| **Service** | Business logic, validation, DB operations | ✅ |
| **API** | HTTP handling, auth, error conversion | ✅ |
| **Frontend** | User interaction, API calls | ✅ |
### What Goes Where
```python
# ❌ WRONG - Business logic in endpoint
@router.put("/{vendor_code}")
async def update_theme(...):
vendor = db.query(Vendor).filter(...).first() # ❌ Direct DB
if not vendor:
raise HTTPException(404) # ❌ Should be custom exception
theme.colors = theme_data.colors # ❌ Business logic
if not self._is_valid_color(theme.colors['primary']): # ❌ Validation
raise HTTPException(422)
db.commit() # ❌ Transaction management
return theme.to_dict()
# ✅ CORRECT - Clean separation
@router.put("/{vendor_code}")
async def update_theme(...):
try:
# Just call service
theme = vendor_theme_service.update_theme(db, vendor_code, theme_data)
return theme.to_dict()
except VendorNotFoundException:
raise HTTPException(404)
except ThemeValidationException as e:
raise HTTPException(422, detail=e.message)
```
---
## 🧪 Testing
### Unit Tests (Service Layer)
```python
# tests/unit/services/test_vendor_theme_service.py
def test_update_theme_validates_colors(service, db, vendor):
"""Test color validation."""
theme_data = VendorThemeUpdate(
colors={"primary": "not-a-color"} # Invalid
)
with pytest.raises(InvalidColorFormatException):
service.update_theme(db, vendor.vendor_code, theme_data)
def test_apply_preset_invalid_name(service, db, vendor):
"""Test invalid preset name."""
with pytest.raises(ThemePresetNotFoundException) as exc_info:
service.apply_theme_preset(db, vendor.vendor_code, "invalid")
assert "invalid" in str(exc_info.value.message)
```
### Integration Tests (API Layer)
```python
# tests/integration/api/v1/admin/test_vendor_themes.py
def test_update_theme_endpoint(client, admin_headers, vendor):
"""Test theme update endpoint."""
response = client.put(
f"/api/v1/admin/vendor-themes/{vendor.vendor_code}",
json={"colors": {"primary": "#ff0000"}},
headers=admin_headers
)
assert response.status_code == 200
assert response.json()["colors"]["primary"] == "#ff0000"
def test_update_theme_invalid_color(client, admin_headers, vendor):
"""Test validation error response."""
response = client.put(
f"/api/v1/admin/vendor-themes/{vendor.vendor_code}",
json={"colors": {"primary": "invalid"}},
headers=admin_headers
)
assert response.status_code == 422
assert "invalid color" in response.json()["detail"].lower()
```
---
## 📊 Comparison: Before vs After
### Before (Original) ❌
```python
# vendor_themes_api.py (OLD)
@router.put("/{vendor_code}")
async def update_vendor_theme(vendor_code: str, theme_data: dict, ...):
# ❌ Direct database access
vendor = db.query(Vendor).filter(...).first()
# ❌ Business logic in endpoint
if not theme:
theme = VendorTheme(vendor_id=vendor.id)
db.add(theme)
# ❌ Data manipulation in endpoint
if "colors" in theme_data:
theme.colors = theme_data["colors"]
# ❌ Transaction management in endpoint
db.commit()
db.refresh(theme)
# ❌ Generic exception
except Exception as e:
raise HTTPException(500, detail="Failed")
```
### After (Refactored) ✅
```python
# vendor_themes.py (NEW - API Layer)
@router.put("/{vendor_code}")
async def update_vendor_theme(vendor_code: str, theme_data: VendorThemeUpdate, ...):
try:
# ✅ Just call service
theme = vendor_theme_service.update_theme(db, vendor_code, theme_data)
return theme.to_dict()
# ✅ Specific exception handling
except VendorNotFoundException:
raise HTTPException(404, detail="Vendor not found")
except ThemeValidationException as e:
raise HTTPException(422, detail=e.message)
# vendor_theme_service.py (NEW - Service Layer)
class VendorThemeService:
def update_theme(self, db, vendor_code, theme_data):
try:
# ✅ Business logic here
vendor = self._get_vendor_by_code(db, vendor_code)
theme = self._get_or_create_theme(db, vendor)
# ✅ Validation
self._validate_theme_data(theme_data)
# ✅ Data updates
self._apply_theme_updates(theme, theme_data)
# ✅ Transaction management
db.commit()
return theme
# ✅ Custom exceptions
except ValidationError:
raise ThemeValidationException(...)
```
---
## 📚 Files Reference
### Backend Files (7 files)
1. **vendor_theme_exceptions.py**`app/exceptions/vendor_theme.py`
- Custom exception classes
2. **exceptions__init__-updated.py**`app/exceptions/__init__.py`
- Updated with vendor_theme imports
3. **vendor_theme_service.py**`app/services/vendor_theme_service.py`
- Business logic service
4. **vendor_themes_endpoints.py**`app/api/v1/admin/vendor_themes.py`
- API endpoints (thin layer)
5. **__init__-updated.py**`app/api/v1/admin/__init__.py`
- Router registration
6. **pages-updated.py**`app/api/v1/admin/pages.py`
- Frontend route
7. **vendor_theme_schemas.py**`models/schema/vendor_theme.py`
- Pydantic models
### Frontend Files (3 files)
8. **vendor-theme-alpine.js**`static/admin/js/vendor-theme.js`
9. **vendor-theme.html**`app/templates/admin/vendor-theme.html`
10. **theme_presets.py**`app/core/theme_presets.py`
---
## ✅ Final Checklist
### Backend Architecture
- [ ] Exceptions in `app/exceptions/vendor_theme.py`
- [ ] Service in `app/services/vendor_theme_service.py`
- [ ] Endpoints in `app/api/v1/admin/vendor_themes.py`
- [ ] No business logic in endpoints
- [ ] No HTTPException in service
- [ ] Custom exceptions used throughout
### Frontend Architecture
- [ ] Alpine.js component with `...data()`
- [ ] Uses lowercase `apiClient`
- [ ] Has initialization guard
- [ ] Follows dashboard.js pattern
### Integration
- [ ] Router registered in `__init__.py`
- [ ] Frontend route in `pages.py`
- [ ] All exceptions imported
- [ ] Service imported in endpoints
---
**Your theme editor now follows proper backend architecture with complete separation of concerns!** 🎯

View File

@@ -224,11 +224,11 @@ Simplifying to minimal working version:
- Extends base.html
- Alpine.js adminDashboard() component
4. **`app/templates/partials/header.html`** ✅
4. **`app/templates/admin/partials/header.html`** ✅
- Top navigation bar
- Updated logout link to /admin/login
5. **`app/templates/partials/sidebar.html`** ✅
5. **`app/templates/admin/partials/sidebar.html`** ✅
- Side navigation menu
- Updated all links to /admin/* paths