feat: add email settings with database overrides for admin and vendor

Platform Email Settings (Admin):
- Add GET/PUT/DELETE /admin/settings/email/* endpoints
- Settings stored in admin_settings table override .env values
- Support all providers: SMTP, SendGrid, Mailgun, Amazon SES
- Edit mode UI with provider-specific configuration forms
- Reset to .env defaults functionality
- Test email to verify configuration

Vendor Email Settings:
- Add VendorEmailSettings model with one-to-one vendor relationship
- Migration: v0a1b2c3d4e5_add_vendor_email_settings.py
- Service: vendor_email_settings_service.py with tier validation
- API endpoints: /vendor/email-settings/* (CRUD, status, verify)
- Email tab in vendor settings page with full configuration
- Warning banner until email is configured (like billing warnings)
- Premium providers (SendGrid, Mailgun, SES) tier-gated to Business+

Email Service Updates:
- get_platform_email_config(db) checks DB first, then .env
- Configurable provider classes accept config dict
- EmailService uses database-aware providers
- Vendor emails use vendor's own SMTP (Wizamart doesn't pay)
- "Powered by Wizamart" footer for Essential/Professional tiers
- White-label (no footer) for Business/Enterprise tiers

Other:
- Add scripts/install.py for first-time platform setup
- Add make install target
- Update init-prod to include email template seeding

🤖 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-05 22:23:47 +01:00
parent ad28a8a9a3
commit 36603178c3
51 changed files with 4959 additions and 1141 deletions

View File

@@ -23,6 +23,7 @@ function vendorMessages(initialConversationId = null) {
loadingMessages: false,
sendingMessage: false,
creatingConversation: false,
error: '',
// Conversations state
conversations: [],
@@ -384,7 +385,8 @@ function vendorMessages(initialConversationId = null) {
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
if (diff < 172800) return 'Yesterday';
return date.toLocaleDateString();
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
return date.toLocaleDateString(locale);
},
formatTime(dateString) {
@@ -392,11 +394,12 @@ function vendorMessages(initialConversationId = null) {
const date = new Date(dateString);
const now = new Date();
const isToday = date.toDateString() === now.toDateString();
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
if (isToday) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
return date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
}
return date.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
return date.toLocaleString(locale, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
};
}