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:
@@ -21,6 +21,7 @@ from .customer import Customer, CustomerAddress
|
||||
from .password_reset_token import PasswordResetToken
|
||||
from .email import EmailCategory, EmailLog, EmailStatus, EmailTemplate
|
||||
from .vendor_email_template import VendorEmailTemplate
|
||||
from .vendor_email_settings import EmailProvider, VendorEmailSettings, PREMIUM_EMAIL_PROVIDERS
|
||||
from .feature import Feature, FeatureCategory, FeatureCode, FeatureUILocation
|
||||
from .inventory import Inventory
|
||||
from .inventory_transaction import InventoryTransaction, TransactionType
|
||||
@@ -114,6 +115,9 @@ __all__ = [
|
||||
"EmailStatus",
|
||||
"EmailTemplate",
|
||||
"VendorEmailTemplate",
|
||||
"VendorEmailSettings",
|
||||
"EmailProvider",
|
||||
"PREMIUM_EMAIL_PROVIDERS",
|
||||
# Features
|
||||
"Feature",
|
||||
"FeatureCategory",
|
||||
|
||||
@@ -177,6 +177,14 @@ class Vendor(Base, TimestampMixin):
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Email settings (one-to-one) - vendor SMTP/provider configuration
|
||||
email_settings = relationship(
|
||||
"VendorEmailSettings",
|
||||
back_populates="vendor",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Subscription (one-to-one)
|
||||
subscription = relationship(
|
||||
"VendorSubscription",
|
||||
|
||||
255
models/database/vendor_email_settings.py
Normal file
255
models/database/vendor_email_settings.py
Normal file
@@ -0,0 +1,255 @@
|
||||
# models/database/vendor_email_settings.py
|
||||
"""
|
||||
Vendor Email Settings model for vendor-specific email configuration.
|
||||
|
||||
This model stores vendor SMTP/email provider settings, enabling vendors to:
|
||||
- Send emails from their own domain/email address
|
||||
- Use their own SMTP server or email provider (tier-gated)
|
||||
- Customize sender name, reply-to address, and signature
|
||||
|
||||
Architecture:
|
||||
- Vendors MUST configure email settings to send transactional emails
|
||||
- Platform emails (billing, subscription) still use platform settings
|
||||
- Advanced providers (SendGrid, Mailgun, SES) are tier-gated (Business+)
|
||||
- "Powered by Wizamart" footer is added for Essential/Professional tiers
|
||||
"""
|
||||
|
||||
import enum
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
|
||||
|
||||
class EmailProvider(str, enum.Enum):
|
||||
"""Supported email providers."""
|
||||
|
||||
SMTP = "smtp" # Standard SMTP (all tiers)
|
||||
SENDGRID = "sendgrid" # SendGrid API (Business+ tier)
|
||||
MAILGUN = "mailgun" # Mailgun API (Business+ tier)
|
||||
SES = "ses" # Amazon SES (Business+ tier)
|
||||
|
||||
|
||||
# Providers that require Business+ tier
|
||||
PREMIUM_EMAIL_PROVIDERS = {
|
||||
EmailProvider.SENDGRID,
|
||||
EmailProvider.MAILGUN,
|
||||
EmailProvider.SES,
|
||||
}
|
||||
|
||||
|
||||
class VendorEmailSettings(Base, TimestampMixin):
|
||||
"""
|
||||
Vendor email configuration for sending transactional emails.
|
||||
|
||||
This is a one-to-one relationship with Vendor.
|
||||
Vendors must configure this to send emails to their customers.
|
||||
"""
|
||||
|
||||
__tablename__ = "vendor_email_settings"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(
|
||||
Integer,
|
||||
ForeignKey("vendors.id", ondelete="CASCADE"),
|
||||
unique=True,
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Sender Identity (Required)
|
||||
# =========================================================================
|
||||
from_email = Column(String(255), nullable=False) # e.g., orders@vendorshop.lu
|
||||
from_name = Column(String(100), nullable=False) # e.g., "VendorShop"
|
||||
reply_to_email = Column(String(255), nullable=True) # Optional reply-to address
|
||||
|
||||
# =========================================================================
|
||||
# Email Signature/Footer (Optional)
|
||||
# =========================================================================
|
||||
signature_text = Column(Text, nullable=True) # Plain text signature
|
||||
signature_html = Column(Text, nullable=True) # HTML signature (footer)
|
||||
|
||||
# =========================================================================
|
||||
# Provider Configuration
|
||||
# =========================================================================
|
||||
provider = Column(
|
||||
String(20),
|
||||
default=EmailProvider.SMTP.value,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# SMTP Settings (used when provider=smtp)
|
||||
# =========================================================================
|
||||
smtp_host = Column(String(255), nullable=True)
|
||||
smtp_port = Column(Integer, nullable=True, default=587)
|
||||
smtp_username = Column(String(255), nullable=True)
|
||||
smtp_password = Column(String(500), nullable=True) # Encrypted at rest
|
||||
smtp_use_tls = Column(Boolean, default=True, nullable=False)
|
||||
smtp_use_ssl = Column(Boolean, default=False, nullable=False) # For port 465
|
||||
|
||||
# =========================================================================
|
||||
# SendGrid Settings (used when provider=sendgrid, Business+ tier)
|
||||
# =========================================================================
|
||||
sendgrid_api_key = Column(String(500), nullable=True) # Encrypted at rest
|
||||
|
||||
# =========================================================================
|
||||
# Mailgun Settings (used when provider=mailgun, Business+ tier)
|
||||
# =========================================================================
|
||||
mailgun_api_key = Column(String(500), nullable=True) # Encrypted at rest
|
||||
mailgun_domain = Column(String(255), nullable=True)
|
||||
|
||||
# =========================================================================
|
||||
# Amazon SES Settings (used when provider=ses, Business+ tier)
|
||||
# =========================================================================
|
||||
ses_access_key_id = Column(String(100), nullable=True)
|
||||
ses_secret_access_key = Column(String(500), nullable=True) # Encrypted at rest
|
||||
ses_region = Column(String(50), nullable=True, default="eu-west-1")
|
||||
|
||||
# =========================================================================
|
||||
# Status & Verification
|
||||
# =========================================================================
|
||||
is_configured = Column(Boolean, default=False, nullable=False) # Has complete config
|
||||
is_verified = Column(Boolean, default=False, nullable=False) # Test email succeeded
|
||||
last_verified_at = Column(DateTime(timezone=True), nullable=True)
|
||||
verification_error = Column(Text, nullable=True) # Last verification error message
|
||||
|
||||
# =========================================================================
|
||||
# Relationship
|
||||
# =========================================================================
|
||||
vendor = relationship("Vendor", back_populates="email_settings")
|
||||
|
||||
# =========================================================================
|
||||
# Indexes
|
||||
# =========================================================================
|
||||
__table_args__ = (
|
||||
Index("idx_vendor_email_settings_configured", "vendor_id", "is_configured"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<VendorEmailSettings(vendor_id={self.vendor_id}, provider='{self.provider}', from='{self.from_email}')>"
|
||||
|
||||
# =========================================================================
|
||||
# Helper Methods
|
||||
# =========================================================================
|
||||
|
||||
def is_smtp_configured(self) -> bool:
|
||||
"""Check if SMTP settings are complete."""
|
||||
if self.provider != EmailProvider.SMTP.value:
|
||||
return False
|
||||
return bool(
|
||||
self.smtp_host
|
||||
and self.smtp_port
|
||||
and self.smtp_username
|
||||
and self.smtp_password
|
||||
)
|
||||
|
||||
def is_sendgrid_configured(self) -> bool:
|
||||
"""Check if SendGrid settings are complete."""
|
||||
if self.provider != EmailProvider.SENDGRID.value:
|
||||
return False
|
||||
return bool(self.sendgrid_api_key)
|
||||
|
||||
def is_mailgun_configured(self) -> bool:
|
||||
"""Check if Mailgun settings are complete."""
|
||||
if self.provider != EmailProvider.MAILGUN.value:
|
||||
return False
|
||||
return bool(self.mailgun_api_key and self.mailgun_domain)
|
||||
|
||||
def is_ses_configured(self) -> bool:
|
||||
"""Check if Amazon SES settings are complete."""
|
||||
if self.provider != EmailProvider.SES.value:
|
||||
return False
|
||||
return bool(
|
||||
self.ses_access_key_id
|
||||
and self.ses_secret_access_key
|
||||
and self.ses_region
|
||||
)
|
||||
|
||||
def is_provider_configured(self) -> bool:
|
||||
"""Check if the current provider is fully configured."""
|
||||
provider_checks = {
|
||||
EmailProvider.SMTP.value: self.is_smtp_configured,
|
||||
EmailProvider.SENDGRID.value: self.is_sendgrid_configured,
|
||||
EmailProvider.MAILGUN.value: self.is_mailgun_configured,
|
||||
EmailProvider.SES.value: self.is_ses_configured,
|
||||
}
|
||||
check_fn = provider_checks.get(self.provider)
|
||||
return check_fn() if check_fn else False
|
||||
|
||||
def is_fully_configured(self) -> bool:
|
||||
"""Check if email settings are fully configured (identity + provider)."""
|
||||
return bool(
|
||||
self.from_email
|
||||
and self.from_name
|
||||
and self.is_provider_configured()
|
||||
)
|
||||
|
||||
def update_configuration_status(self) -> None:
|
||||
"""Update the is_configured flag based on current settings."""
|
||||
self.is_configured = self.is_fully_configured()
|
||||
|
||||
def mark_verified(self) -> None:
|
||||
"""Mark settings as verified (test email succeeded)."""
|
||||
self.is_verified = True
|
||||
self.last_verified_at = datetime.now(UTC)
|
||||
self.verification_error = None
|
||||
|
||||
def mark_verification_failed(self, error: str) -> None:
|
||||
"""Mark settings as verification failed."""
|
||||
self.is_verified = False
|
||||
self.verification_error = error
|
||||
|
||||
def requires_premium_tier(self) -> bool:
|
||||
"""Check if current provider requires Business+ tier."""
|
||||
return self.provider in [p.value for p in PREMIUM_EMAIL_PROVIDERS]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary for API responses (excludes sensitive data)."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"vendor_id": self.vendor_id,
|
||||
"from_email": self.from_email,
|
||||
"from_name": self.from_name,
|
||||
"reply_to_email": self.reply_to_email,
|
||||
"signature_text": self.signature_text,
|
||||
"signature_html": self.signature_html,
|
||||
"provider": self.provider,
|
||||
# SMTP (mask password)
|
||||
"smtp_host": self.smtp_host,
|
||||
"smtp_port": self.smtp_port,
|
||||
"smtp_username": self.smtp_username,
|
||||
"smtp_password_set": bool(self.smtp_password),
|
||||
"smtp_use_tls": self.smtp_use_tls,
|
||||
"smtp_use_ssl": self.smtp_use_ssl,
|
||||
# SendGrid (mask API key)
|
||||
"sendgrid_api_key_set": bool(self.sendgrid_api_key),
|
||||
# Mailgun (mask API key)
|
||||
"mailgun_api_key_set": bool(self.mailgun_api_key),
|
||||
"mailgun_domain": self.mailgun_domain,
|
||||
# SES (mask credentials)
|
||||
"ses_access_key_id_set": bool(self.ses_access_key_id),
|
||||
"ses_region": self.ses_region,
|
||||
# Status
|
||||
"is_configured": self.is_configured,
|
||||
"is_verified": self.is_verified,
|
||||
"last_verified_at": self.last_verified_at.isoformat() if self.last_verified_at else None,
|
||||
"verification_error": self.verification_error,
|
||||
"requires_premium_tier": self.requires_premium_tier(),
|
||||
# Timestamps
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
Reference in New Issue
Block a user