Email COnfigs & setup
This commit is contained in:
@@ -18,4 +18,8 @@ __all__ = [
|
||||
# New centralized models
|
||||
'IntegrationProvider',
|
||||
'AISettings',
|
||||
# Email models
|
||||
'EmailSettings',
|
||||
'EmailTemplate',
|
||||
'EmailLog',
|
||||
]
|
||||
|
||||
@@ -696,3 +696,6 @@ class SystemAISettingsAdmin(Igny8ModelAdmin):
|
||||
obj.updated_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
# Import Email Admin (EmailSettings, EmailTemplate, EmailLog)
|
||||
from .email_admin import EmailSettingsAdmin, EmailTemplateAdmin, EmailLogAdmin
|
||||
|
||||
320
backend/igny8_core/modules/system/email_admin.py
Normal file
320
backend/igny8_core/modules/system/email_admin.py
Normal file
@@ -0,0 +1,320 @@
|
||||
"""
|
||||
Email Admin Configuration for IGNY8
|
||||
|
||||
Provides admin interface for managing:
|
||||
- Email Settings (global configuration)
|
||||
- Email Templates (template metadata and testing)
|
||||
- Email Logs (sent email history)
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.urls import path, reverse
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse
|
||||
|
||||
from unfold.admin import ModelAdmin as UnfoldModelAdmin
|
||||
from igny8_core.admin.base import Igny8ModelAdmin
|
||||
from .email_models import EmailSettings, EmailTemplate, EmailLog
|
||||
|
||||
|
||||
@admin.register(EmailSettings)
|
||||
class EmailSettingsAdmin(Igny8ModelAdmin):
|
||||
"""
|
||||
Admin for EmailSettings - Global email configuration (Singleton)
|
||||
"""
|
||||
|
||||
list_display = [
|
||||
'from_email',
|
||||
'from_name',
|
||||
'reply_to_email',
|
||||
'send_welcome_emails',
|
||||
'send_billing_emails',
|
||||
'updated_at',
|
||||
]
|
||||
readonly_fields = ['updated_at']
|
||||
|
||||
fieldsets = (
|
||||
('Sender Configuration', {
|
||||
'fields': ('from_email', 'from_name', 'reply_to_email'),
|
||||
'description': 'Default sender settings. Email address must be verified in Resend.',
|
||||
}),
|
||||
('Company Branding', {
|
||||
'fields': ('company_name', 'company_address', 'logo_url'),
|
||||
'description': 'Company information shown in email templates.',
|
||||
}),
|
||||
('Support Links', {
|
||||
'fields': ('support_email', 'support_url', 'unsubscribe_url'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('Email Types', {
|
||||
'fields': (
|
||||
'send_welcome_emails',
|
||||
'send_billing_emails',
|
||||
'send_subscription_emails',
|
||||
'send_low_credit_warnings',
|
||||
),
|
||||
'description': 'Enable/disable specific email types globally.',
|
||||
}),
|
||||
('Thresholds', {
|
||||
'fields': ('low_credit_threshold', 'renewal_reminder_days'),
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('updated_by', 'updated_at'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Only allow one instance (singleton)"""
|
||||
return not EmailSettings.objects.exists()
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Prevent deletion of singleton"""
|
||||
return False
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""Set updated_by to current user"""
|
||||
obj.updated_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
@admin.register(EmailTemplate)
|
||||
class EmailTemplateAdmin(Igny8ModelAdmin):
|
||||
"""
|
||||
Admin for EmailTemplate - Manage email templates and testing
|
||||
"""
|
||||
|
||||
list_display = [
|
||||
'display_name',
|
||||
'template_type',
|
||||
'template_name',
|
||||
'is_active',
|
||||
'send_count',
|
||||
'last_sent_at',
|
||||
'test_email_button',
|
||||
]
|
||||
list_filter = ['template_type', 'is_active']
|
||||
search_fields = ['display_name', 'template_name', 'description']
|
||||
readonly_fields = ['send_count', 'last_sent_at', 'created_at', 'updated_at']
|
||||
|
||||
fieldsets = (
|
||||
('Template Info', {
|
||||
'fields': ('template_name', 'template_path', 'display_name', 'description'),
|
||||
}),
|
||||
('Email Settings', {
|
||||
'fields': ('template_type', 'default_subject'),
|
||||
}),
|
||||
('Context Configuration', {
|
||||
'fields': ('required_context', 'sample_context'),
|
||||
'description': 'Define required variables and sample data for testing.',
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('Status', {
|
||||
'fields': ('is_active',),
|
||||
}),
|
||||
('Statistics', {
|
||||
'fields': ('send_count', 'last_sent_at'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
def test_email_button(self, obj):
|
||||
"""Add test email button in list view"""
|
||||
url = reverse('admin:system_emailtemplate_test', args=[obj.pk])
|
||||
return format_html(
|
||||
'<a class="button" href="{}" style="padding: 4px 12px; background: #6366f1; color: white; '
|
||||
'border-radius: 4px; text-decoration: none; font-size: 12px;">Test</a>',
|
||||
url
|
||||
)
|
||||
test_email_button.short_description = 'Test'
|
||||
test_email_button.allow_tags = True
|
||||
|
||||
def get_urls(self):
|
||||
"""Add custom URL for test email"""
|
||||
urls = super().get_urls()
|
||||
custom_urls = [
|
||||
path(
|
||||
'<int:template_id>/test/',
|
||||
self.admin_site.admin_view(self.test_email_view),
|
||||
name='system_emailtemplate_test'
|
||||
),
|
||||
path(
|
||||
'<int:template_id>/send-test/',
|
||||
self.admin_site.admin_view(self.send_test_email),
|
||||
name='system_emailtemplate_send_test'
|
||||
),
|
||||
]
|
||||
return custom_urls + urls
|
||||
|
||||
def test_email_view(self, request, template_id):
|
||||
"""Show test email form"""
|
||||
template = EmailTemplate.objects.get(pk=template_id)
|
||||
|
||||
context = {
|
||||
**self.admin_site.each_context(request),
|
||||
'title': f'Test Email: {template.display_name}',
|
||||
'template': template,
|
||||
'opts': self.model._meta,
|
||||
}
|
||||
|
||||
return render(request, 'admin/system/emailtemplate/test_email.html', context)
|
||||
|
||||
def send_test_email(self, request, template_id):
|
||||
"""Send test email"""
|
||||
if request.method != 'POST':
|
||||
return JsonResponse({'error': 'POST required'}, status=405)
|
||||
|
||||
import json
|
||||
from django.utils import timezone
|
||||
from igny8_core.business.billing.services.email_service import get_email_service
|
||||
|
||||
template = EmailTemplate.objects.get(pk=template_id)
|
||||
|
||||
to_email = request.POST.get('to_email', request.user.email)
|
||||
custom_context = request.POST.get('context', '{}')
|
||||
|
||||
try:
|
||||
context = json.loads(custom_context) if custom_context else {}
|
||||
except json.JSONDecodeError:
|
||||
context = template.sample_context or {}
|
||||
|
||||
# Merge sample context with any custom values
|
||||
final_context = {**(template.sample_context or {}), **context}
|
||||
|
||||
# Add default context values
|
||||
final_context.setdefault('user_name', 'Test User')
|
||||
final_context.setdefault('account_name', 'Test Account')
|
||||
final_context.setdefault('frontend_url', 'https://app.igny8.com')
|
||||
|
||||
service = get_email_service()
|
||||
|
||||
try:
|
||||
result = service.send_transactional(
|
||||
to=to_email,
|
||||
subject=f'[TEST] {template.default_subject}',
|
||||
template=template.template_path,
|
||||
context=final_context,
|
||||
tags=['test', template.template_type],
|
||||
)
|
||||
|
||||
if result.get('success'):
|
||||
# Update template stats
|
||||
template.send_count += 1
|
||||
template.last_sent_at = timezone.now()
|
||||
template.save(update_fields=['send_count', 'last_sent_at'])
|
||||
|
||||
# Log the email
|
||||
EmailLog.objects.create(
|
||||
message_id=result.get('id', ''),
|
||||
to_email=to_email,
|
||||
from_email=service.from_email,
|
||||
subject=f'[TEST] {template.default_subject}',
|
||||
template_name=template.template_name,
|
||||
status='sent',
|
||||
provider=result.get('provider', 'resend'),
|
||||
tags=['test', template.template_type],
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f'Test email sent successfully to {to_email}! (ID: {result.get("id", "N/A")})'
|
||||
)
|
||||
else:
|
||||
messages.error(request, f'Failed to send: {result.get("error", "Unknown error")}')
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Error sending test email: {str(e)}')
|
||||
|
||||
return redirect(reverse('admin:system_emailtemplate_changelist'))
|
||||
|
||||
|
||||
@admin.register(EmailLog)
|
||||
class EmailLogAdmin(Igny8ModelAdmin):
|
||||
"""
|
||||
Admin for EmailLog - View sent email history
|
||||
"""
|
||||
|
||||
list_display = [
|
||||
'sent_at',
|
||||
'to_email',
|
||||
'subject_truncated',
|
||||
'template_name',
|
||||
'status_badge',
|
||||
'provider',
|
||||
'message_id_short',
|
||||
]
|
||||
list_filter = ['status', 'provider', 'template_name', 'sent_at']
|
||||
search_fields = ['to_email', 'subject', 'message_id']
|
||||
readonly_fields = [
|
||||
'message_id', 'to_email', 'from_email', 'subject',
|
||||
'template_name', 'status', 'provider', 'error_message',
|
||||
'tags', 'sent_at'
|
||||
]
|
||||
date_hierarchy = 'sent_at'
|
||||
|
||||
fieldsets = (
|
||||
('Email Details', {
|
||||
'fields': ('to_email', 'from_email', 'subject'),
|
||||
}),
|
||||
('Delivery Info', {
|
||||
'fields': ('status', 'provider', 'message_id'),
|
||||
}),
|
||||
('Template', {
|
||||
'fields': ('template_name', 'tags'),
|
||||
}),
|
||||
('Error Info', {
|
||||
'fields': ('error_message',),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('Timestamp', {
|
||||
'fields': ('sent_at',),
|
||||
}),
|
||||
)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Logs are created automatically"""
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Logs are read-only"""
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Allow deletion for cleanup"""
|
||||
return request.user.is_superuser
|
||||
|
||||
def subject_truncated(self, obj):
|
||||
"""Truncate long subjects"""
|
||||
if len(obj.subject) > 50:
|
||||
return f'{obj.subject[:50]}...'
|
||||
return obj.subject
|
||||
subject_truncated.short_description = 'Subject'
|
||||
|
||||
def message_id_short(self, obj):
|
||||
"""Show truncated message ID"""
|
||||
if obj.message_id:
|
||||
return f'{obj.message_id[:20]}...' if len(obj.message_id) > 20 else obj.message_id
|
||||
return '-'
|
||||
message_id_short.short_description = 'Message ID'
|
||||
|
||||
def status_badge(self, obj):
|
||||
"""Show status with color badge"""
|
||||
colors = {
|
||||
'sent': '#3b82f6',
|
||||
'delivered': '#22c55e',
|
||||
'failed': '#ef4444',
|
||||
'bounced': '#f59e0b',
|
||||
}
|
||||
color = colors.get(obj.status, '#6b7280')
|
||||
return format_html(
|
||||
'<span style="background: {}; color: white; padding: 2px 8px; '
|
||||
'border-radius: 4px; font-size: 11px;">{}</span>',
|
||||
color, obj.status.upper()
|
||||
)
|
||||
status_badge.short_description = 'Status'
|
||||
status_badge.allow_tags = True
|
||||
292
backend/igny8_core/modules/system/email_models.py
Normal file
292
backend/igny8_core/modules/system/email_models.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
Email Configuration Models for IGNY8
|
||||
|
||||
Provides database-driven email settings, template management, and send test functionality.
|
||||
Works with the existing EmailService and IntegrationProvider models.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class EmailSettings(models.Model):
|
||||
"""
|
||||
Global email settings - singleton model for email configuration.
|
||||
|
||||
Stores default email settings that can be managed through Django admin.
|
||||
These settings work alongside IntegrationProvider (resend) configuration.
|
||||
"""
|
||||
|
||||
# Default sender settings
|
||||
from_email = models.EmailField(
|
||||
default='noreply@igny8.com',
|
||||
help_text='Default sender email address (must be verified in Resend)'
|
||||
)
|
||||
from_name = models.CharField(
|
||||
max_length=100,
|
||||
default='IGNY8',
|
||||
help_text='Default sender display name'
|
||||
)
|
||||
reply_to_email = models.EmailField(
|
||||
default='support@igny8.com',
|
||||
help_text='Default reply-to email address'
|
||||
)
|
||||
|
||||
# Company branding for emails
|
||||
company_name = models.CharField(
|
||||
max_length=100,
|
||||
default='IGNY8',
|
||||
help_text='Company name shown in emails'
|
||||
)
|
||||
company_address = models.TextField(
|
||||
blank=True,
|
||||
help_text='Company address for email footer (CAN-SPAM compliance)'
|
||||
)
|
||||
logo_url = models.URLField(
|
||||
blank=True,
|
||||
help_text='URL to company logo for emails'
|
||||
)
|
||||
|
||||
# Support links
|
||||
support_email = models.EmailField(
|
||||
default='support@igny8.com',
|
||||
help_text='Support email shown in emails'
|
||||
)
|
||||
support_url = models.URLField(
|
||||
blank=True,
|
||||
help_text='Link to support/help center'
|
||||
)
|
||||
unsubscribe_url = models.URLField(
|
||||
blank=True,
|
||||
help_text='URL for email unsubscribe (for marketing emails)'
|
||||
)
|
||||
|
||||
# Feature flags
|
||||
send_welcome_emails = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Send welcome email on user registration'
|
||||
)
|
||||
send_billing_emails = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Send payment confirmation, invoice emails'
|
||||
)
|
||||
send_subscription_emails = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Send subscription renewal reminders'
|
||||
)
|
||||
send_low_credit_warnings = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Send low credit warning emails'
|
||||
)
|
||||
|
||||
# Credit warning threshold
|
||||
low_credit_threshold = models.IntegerField(
|
||||
default=100,
|
||||
help_text='Send warning when credits fall below this value'
|
||||
)
|
||||
renewal_reminder_days = models.IntegerField(
|
||||
default=7,
|
||||
help_text='Days before subscription renewal to send reminder'
|
||||
)
|
||||
|
||||
# Audit
|
||||
updated_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='email_settings_updates'
|
||||
)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_email_settings'
|
||||
verbose_name = 'Email Settings'
|
||||
verbose_name_plural = 'Email Settings'
|
||||
|
||||
def __str__(self):
|
||||
return f'Email Settings (from: {self.from_email})'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Ensure only one instance exists (singleton)"""
|
||||
self.pk = 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_settings(cls):
|
||||
"""Get singleton settings instance, creating if needed"""
|
||||
obj, _ = cls.objects.get_or_create(pk=1)
|
||||
return obj
|
||||
|
||||
|
||||
class EmailTemplate(models.Model):
|
||||
"""
|
||||
Email template metadata - tracks available email templates
|
||||
and their usage/configuration.
|
||||
|
||||
Templates are stored as Django templates in templates/emails/.
|
||||
This model provides admin visibility and test sending capability.
|
||||
"""
|
||||
|
||||
TEMPLATE_TYPE_CHOICES = [
|
||||
('auth', 'Authentication'),
|
||||
('billing', 'Billing'),
|
||||
('notification', 'Notification'),
|
||||
('marketing', 'Marketing'),
|
||||
]
|
||||
|
||||
# Template identification
|
||||
template_name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True,
|
||||
help_text='Template file name without extension (e.g., "welcome")'
|
||||
)
|
||||
template_path = models.CharField(
|
||||
max_length=200,
|
||||
help_text='Full template path (e.g., "emails/welcome.html")'
|
||||
)
|
||||
|
||||
# Display info
|
||||
display_name = models.CharField(
|
||||
max_length=100,
|
||||
help_text='Human-readable template name'
|
||||
)
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
help_text='Description of when this template is used'
|
||||
)
|
||||
template_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=TEMPLATE_TYPE_CHOICES,
|
||||
default='notification'
|
||||
)
|
||||
|
||||
# Default subject
|
||||
default_subject = models.CharField(
|
||||
max_length=200,
|
||||
help_text='Default email subject line'
|
||||
)
|
||||
|
||||
# Required context variables
|
||||
required_context = models.JSONField(
|
||||
default=list,
|
||||
blank=True,
|
||||
help_text='List of required context variables for this template'
|
||||
)
|
||||
|
||||
# Sample context for testing
|
||||
sample_context = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
help_text='Sample context for test sending (JSON)'
|
||||
)
|
||||
|
||||
# Status
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Whether this template is currently in use'
|
||||
)
|
||||
|
||||
# Stats
|
||||
send_count = models.IntegerField(
|
||||
default=0,
|
||||
help_text='Number of emails sent using this template'
|
||||
)
|
||||
last_sent_at = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text='Last time an email was sent with this template'
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_email_templates'
|
||||
verbose_name = 'Email Template'
|
||||
verbose_name_plural = 'Email Templates'
|
||||
ordering = ['template_type', 'display_name']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.display_name} ({self.template_type})'
|
||||
|
||||
|
||||
class EmailLog(models.Model):
|
||||
"""
|
||||
Log of sent emails for audit and debugging.
|
||||
"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('sent', 'Sent'),
|
||||
('delivered', 'Delivered'),
|
||||
('failed', 'Failed'),
|
||||
('bounced', 'Bounced'),
|
||||
]
|
||||
|
||||
# Email identification
|
||||
message_id = models.CharField(
|
||||
max_length=200,
|
||||
blank=True,
|
||||
help_text='Provider message ID (from Resend)'
|
||||
)
|
||||
|
||||
# Recipients
|
||||
to_email = models.EmailField(
|
||||
help_text='Recipient email'
|
||||
)
|
||||
from_email = models.EmailField(
|
||||
help_text='Sender email'
|
||||
)
|
||||
|
||||
# Content
|
||||
subject = models.CharField(
|
||||
max_length=500,
|
||||
help_text='Email subject'
|
||||
)
|
||||
template_name = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text='Template used (if any)'
|
||||
)
|
||||
|
||||
# Status
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=STATUS_CHOICES,
|
||||
default='sent'
|
||||
)
|
||||
provider = models.CharField(
|
||||
max_length=50,
|
||||
default='resend',
|
||||
help_text='Email provider used'
|
||||
)
|
||||
|
||||
# Error tracking
|
||||
error_message = models.TextField(
|
||||
blank=True,
|
||||
help_text='Error message if failed'
|
||||
)
|
||||
|
||||
# Metadata
|
||||
tags = models.JSONField(
|
||||
default=list,
|
||||
blank=True,
|
||||
help_text='Email tags for categorization'
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
sent_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_email_log'
|
||||
verbose_name = 'Email Log'
|
||||
verbose_name_plural = 'Email Logs'
|
||||
ordering = ['-sent_at']
|
||||
indexes = [
|
||||
models.Index(fields=['to_email', 'sent_at']),
|
||||
models.Index(fields=['status', 'sent_at']),
|
||||
models.Index(fields=['template_name', 'sent_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.subject} → {self.to_email} ({self.status})'
|
||||
@@ -0,0 +1,93 @@
|
||||
# Generated by Django 5.2.10 on 2026-01-08 01:23
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0019_model_schema_update'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EmailTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('template_name', models.CharField(help_text='Template file name without extension (e.g., "welcome")', max_length=100, unique=True)),
|
||||
('template_path', models.CharField(help_text='Full template path (e.g., "emails/welcome.html")', max_length=200)),
|
||||
('display_name', models.CharField(help_text='Human-readable template name', max_length=100)),
|
||||
('description', models.TextField(blank=True, help_text='Description of when this template is used')),
|
||||
('template_type', models.CharField(choices=[('auth', 'Authentication'), ('billing', 'Billing'), ('notification', 'Notification'), ('marketing', 'Marketing')], default='notification', max_length=20)),
|
||||
('default_subject', models.CharField(help_text='Default email subject line', max_length=200)),
|
||||
('required_context', models.JSONField(blank=True, default=list, help_text='List of required context variables for this template')),
|
||||
('sample_context', models.JSONField(blank=True, default=dict, help_text='Sample context for test sending (JSON)')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Whether this template is currently in use')),
|
||||
('send_count', models.IntegerField(default=0, help_text='Number of emails sent using this template')),
|
||||
('last_sent_at', models.DateTimeField(blank=True, help_text='Last time an email was sent with this template', null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Email Template',
|
||||
'verbose_name_plural': 'Email Templates',
|
||||
'db_table': 'igny8_email_templates',
|
||||
'ordering': ['template_type', 'display_name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EmailLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message_id', models.CharField(blank=True, help_text='Provider message ID (from Resend)', max_length=200)),
|
||||
('to_email', models.EmailField(help_text='Recipient email', max_length=254)),
|
||||
('from_email', models.EmailField(help_text='Sender email', max_length=254)),
|
||||
('subject', models.CharField(help_text='Email subject', max_length=500)),
|
||||
('template_name', models.CharField(blank=True, help_text='Template used (if any)', max_length=100)),
|
||||
('status', models.CharField(choices=[('sent', 'Sent'), ('delivered', 'Delivered'), ('failed', 'Failed'), ('bounced', 'Bounced')], default='sent', max_length=20)),
|
||||
('provider', models.CharField(default='resend', help_text='Email provider used', max_length=50)),
|
||||
('error_message', models.TextField(blank=True, help_text='Error message if failed')),
|
||||
('tags', models.JSONField(blank=True, default=list, help_text='Email tags for categorization')),
|
||||
('sent_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Email Log',
|
||||
'verbose_name_plural': 'Email Logs',
|
||||
'db_table': 'igny8_email_log',
|
||||
'ordering': ['-sent_at'],
|
||||
'indexes': [models.Index(fields=['to_email', 'sent_at'], name='igny8_email_to_emai_f0efbd_idx'), models.Index(fields=['status', 'sent_at'], name='igny8_email_status_7107f0_idx'), models.Index(fields=['template_name', 'sent_at'], name='igny8_email_templat_e979b9_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EmailSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('from_email', models.EmailField(default='noreply@igny8.com', help_text='Default sender email address (must be verified in Resend)', max_length=254)),
|
||||
('from_name', models.CharField(default='IGNY8', help_text='Default sender display name', max_length=100)),
|
||||
('reply_to_email', models.EmailField(default='support@igny8.com', help_text='Default reply-to email address', max_length=254)),
|
||||
('company_name', models.CharField(default='IGNY8', help_text='Company name shown in emails', max_length=100)),
|
||||
('company_address', models.TextField(blank=True, help_text='Company address for email footer (CAN-SPAM compliance)')),
|
||||
('logo_url', models.URLField(blank=True, help_text='URL to company logo for emails')),
|
||||
('support_email', models.EmailField(default='support@igny8.com', help_text='Support email shown in emails', max_length=254)),
|
||||
('support_url', models.URLField(blank=True, help_text='Link to support/help center')),
|
||||
('unsubscribe_url', models.URLField(blank=True, help_text='URL for email unsubscribe (for marketing emails)')),
|
||||
('send_welcome_emails', models.BooleanField(default=True, help_text='Send welcome email on user registration')),
|
||||
('send_billing_emails', models.BooleanField(default=True, help_text='Send payment confirmation, invoice emails')),
|
||||
('send_subscription_emails', models.BooleanField(default=True, help_text='Send subscription renewal reminders')),
|
||||
('send_low_credit_warnings', models.BooleanField(default=True, help_text='Send low credit warning emails')),
|
||||
('low_credit_threshold', models.IntegerField(default=100, help_text='Send warning when credits fall below this value')),
|
||||
('renewal_reminder_days', models.IntegerField(default=7, help_text='Days before subscription renewal to send reminder')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='email_settings_updates', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Email Settings',
|
||||
'verbose_name_plural': 'Email Settings',
|
||||
'db_table': 'igny8_email_settings',
|
||||
},
|
||||
),
|
||||
# Note: AccountIntegrationOverride delete removed - table doesn't exist in DB
|
||||
]
|
||||
Reference in New Issue
Block a user