Files
orion/app/modules/messaging/models/store_email_settings.py
Samir Boulahtit e9253fbd84 refactor: rename Wizamart to Orion across entire codebase
Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart
with Orion/orion/ORION across 184 files. This includes database
identifiers, email addresses, domain references, R2 bucket names,
DNS prefixes, encryption salt, Celery app name, config defaults,
Docker configs, CI configs, documentation, seed data, and templates.

Renames homepage-wizamart.html template to homepage-orion.html.
Fixes duplicate file_pattern key in api.yaml architecture rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:46:56 +01:00

259 lines
10 KiB
Python

# app/modules/messaging/models/store_email_settings.py
"""
Store Email Settings model for store-specific email configuration.
This model stores store SMTP/email provider settings, enabling stores 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:
- Stores 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 Orion" 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 StoreEmailSettings(Base, TimestampMixin):
"""
Store email configuration for sending transactional emails.
This is a one-to-one relationship with Store.
Stores must configure this to send emails to their customers.
"""
__tablename__ = "store_email_settings"
id = Column(Integer, primary_key=True, index=True)
store_id = Column(
Integer,
ForeignKey("stores.id", ondelete="CASCADE"),
unique=True,
nullable=False,
index=True,
)
# =========================================================================
# Sender Identity (Required)
# =========================================================================
from_email = Column(String(255), nullable=False) # e.g., orders@storeshop.lu
from_name = Column(String(100), nullable=False) # e.g., "StoreShop"
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
# =========================================================================
store = relationship("Store", back_populates="email_settings")
# =========================================================================
# Indexes
# =========================================================================
__table_args__ = (
Index("idx_store_email_settings_configured", "store_id", "is_configured"),
)
def __repr__(self) -> str:
return f"<StoreEmailSettings(store_id={self.store_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,
"store_id": self.store_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,
}
__all__ = ["EmailProvider", "PREMIUM_EMAIL_PROVIDERS", "StoreEmailSettings"]