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

@@ -0,0 +1,102 @@
# alembic/versions/v0a1b2c3d4e5_add_vendor_email_settings.py
"""Add vendor email settings table.
Revision ID: v0a1b2c3d4e5
Revises: u9c0d1e2f3g4
Create Date: 2026-01-05
Changes:
- Create vendor_email_settings table for vendor SMTP/email provider configuration
- Vendors must configure this to send transactional emails
- Premium providers (SendGrid, Mailgun, SES) are tier-gated (Business+)
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "v0a1b2c3d4e5"
down_revision = "u9c0d1e2f3g4"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create vendor_email_settings table
op.create_table(
"vendor_email_settings",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("vendor_id", sa.Integer(), nullable=False),
# Sender Identity
sa.Column("from_email", sa.String(255), nullable=False),
sa.Column("from_name", sa.String(100), nullable=False),
sa.Column("reply_to_email", sa.String(255), nullable=True),
# Signature/Footer
sa.Column("signature_text", sa.Text(), nullable=True),
sa.Column("signature_html", sa.Text(), nullable=True),
# Provider Configuration
sa.Column("provider", sa.String(20), nullable=False, default="smtp"),
# SMTP Settings
sa.Column("smtp_host", sa.String(255), nullable=True),
sa.Column("smtp_port", sa.Integer(), nullable=True, default=587),
sa.Column("smtp_username", sa.String(255), nullable=True),
sa.Column("smtp_password", sa.String(500), nullable=True),
sa.Column("smtp_use_tls", sa.Boolean(), nullable=False, default=True),
sa.Column("smtp_use_ssl", sa.Boolean(), nullable=False, default=False),
# SendGrid Settings
sa.Column("sendgrid_api_key", sa.String(500), nullable=True),
# Mailgun Settings
sa.Column("mailgun_api_key", sa.String(500), nullable=True),
sa.Column("mailgun_domain", sa.String(255), nullable=True),
# Amazon SES Settings
sa.Column("ses_access_key_id", sa.String(100), nullable=True),
sa.Column("ses_secret_access_key", sa.String(500), nullable=True),
sa.Column("ses_region", sa.String(50), nullable=True, default="eu-west-1"),
# Status & Verification
sa.Column("is_configured", sa.Boolean(), nullable=False, default=False),
sa.Column("is_verified", sa.Boolean(), nullable=False, default=False),
sa.Column("last_verified_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("verification_error", sa.Text(), nullable=True),
# Timestamps
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
# Constraints
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(
["vendor_id"],
["vendors.id"],
name="fk_vendor_email_settings_vendor_id",
ondelete="CASCADE",
),
sa.UniqueConstraint("vendor_id", name="uq_vendor_email_settings_vendor_id"),
)
# Create indexes
op.create_index(
"ix_vendor_email_settings_id",
"vendor_email_settings",
["id"],
unique=False,
)
op.create_index(
"ix_vendor_email_settings_vendor_id",
"vendor_email_settings",
["vendor_id"],
unique=True,
)
op.create_index(
"idx_vendor_email_settings_configured",
"vendor_email_settings",
["vendor_id", "is_configured"],
)
def downgrade() -> None:
# Drop indexes
op.drop_index("idx_vendor_email_settings_configured", table_name="vendor_email_settings")
op.drop_index("ix_vendor_email_settings_vendor_id", table_name="vendor_email_settings")
op.drop_index("ix_vendor_email_settings_id", table_name="vendor_email_settings")
# Drop table
op.drop_table("vendor_email_settings")