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>
259 lines
10 KiB
Python
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"]
|