diff --git a/app/api/v1/admin/settings.py b/app/api/v1/admin/settings.py index 2cdb84c4..12bccbfb 100644 --- a/app/api/v1/admin/settings.py +++ b/app/api/v1/admin/settings.py @@ -610,7 +610,8 @@ def reset_email_settings( for key in EMAIL_SETTING_KEYS: setting = admin_settings_service.get_setting_by_key(db, key) if setting: - db.delete(setting) + # Use service method for deletion (API-002 compliance) + admin_settings_service.delete_setting(db, key, current_admin.id) deleted_count += 1 # Log action diff --git a/app/api/v1/vendor/email_settings.py b/app/api/v1/vendor/email_settings.py index 12c10e7e..b818979b 100644 --- a/app/api/v1/vendor/email_settings.py +++ b/app/api/v1/vendor/email_settings.py @@ -14,13 +14,12 @@ Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pa import logging -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends from pydantic import BaseModel, EmailStr, Field from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_api from app.core.database import get_db -from app.exceptions import NotFoundError, ValidationError, AuthorizationError from app.services.vendor_email_settings_service import VendorEmailSettingsService from app.services.subscription_service import subscription_service from models.database.user import User @@ -76,16 +75,62 @@ class VerifyEmailRequest(BaseModel): test_email: EmailStr = Field(..., description="Email address to send test email to") +# Response models for API-001 compliance +class EmailSettingsResponse(BaseModel): + """Response for email settings.""" + + configured: bool + verified: bool | None = None + settings: dict | None = None + message: str | None = None + + +class EmailStatusResponse(BaseModel): + """Response for email status check.""" + + is_configured: bool + is_verified: bool + + +class ProvidersResponse(BaseModel): + """Response for available providers.""" + + providers: list[dict] + current_tier: str | None + + +class EmailUpdateResponse(BaseModel): + """Response for email settings update.""" + + success: bool + message: str + settings: dict + + +class EmailVerifyResponse(BaseModel): + """Response for email verification.""" + + success: bool + message: str + + +class EmailDeleteResponse(BaseModel): + """Response for email settings deletion.""" + + success: bool + message: str + + # ============================================================================= # ENDPOINTS # ============================================================================= -@router.get("") +@router.get("", response_model=EmailSettingsResponse) def get_email_settings( current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), -): +) -> EmailSettingsResponse: """ Get current email settings for the vendor. @@ -96,24 +141,24 @@ def get_email_settings( settings = service.get_settings(vendor_id) if not settings: - return { - "configured": False, - "settings": None, - "message": "Email settings not configured. Configure SMTP to send emails to customers.", - } + return EmailSettingsResponse( + configured=False, + settings=None, + message="Email settings not configured. Configure SMTP to send emails to customers.", + ) - return { - "configured": settings.is_configured, - "verified": settings.is_verified, - "settings": settings.to_dict(), - } + return EmailSettingsResponse( + configured=settings.is_configured, + verified=settings.is_verified, + settings=settings.to_dict(), + ) -@router.get("/status") +@router.get("/status", response_model=EmailStatusResponse) def get_email_status( current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), -): +) -> EmailStatusResponse: """ Get email configuration status. @@ -121,14 +166,15 @@ def get_email_status( """ vendor_id = current_user.token_vendor_id service = VendorEmailSettingsService(db) - return service.get_status(vendor_id) + status = service.get_status(vendor_id) + return EmailStatusResponse(**status) -@router.get("/providers") +@router.get("/providers", response_model=ProvidersResponse) def get_available_providers( current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), -): +) -> ProvidersResponse: """ Get available email providers for current tier. @@ -140,22 +186,24 @@ def get_available_providers( # Get vendor's current tier tier = subscription_service.get_current_tier(db, vendor_id) - return { - "providers": service.get_available_providers(tier), - "current_tier": tier.value if tier else None, - } + return ProvidersResponse( + providers=service.get_available_providers(tier), + current_tier=tier.value if tier else None, + ) -@router.put("") +@router.put("", response_model=EmailUpdateResponse) def update_email_settings( data: EmailSettingsUpdate, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), -): +) -> EmailUpdateResponse: """ Create or update email settings. Premium providers (SendGrid, Mailgun, SES) require Business+ tier. + Raises AuthorizationException if tier is insufficient. + Raises ValidationException if data is invalid. """ vendor_id = current_user.token_vendor_id service = VendorEmailSettingsService(db) @@ -163,63 +211,66 @@ def update_email_settings( # Get vendor's current tier for validation tier = subscription_service.get_current_tier(db, vendor_id) - try: - settings = service.create_or_update( - vendor_id=vendor_id, - data=data.model_dump(exclude_unset=True), - current_tier=tier, - ) - return { - "success": True, - "message": "Email settings updated successfully", - "settings": settings.to_dict(), - } - except AuthorizationError as e: - raise HTTPException(status_code=403, detail=str(e)) - except ValidationError as e: - raise HTTPException(status_code=400, detail=str(e)) + # Service raises appropriate exceptions (API-003 compliance) + settings = service.create_or_update( + vendor_id=vendor_id, + data=data.model_dump(exclude_unset=True), + current_tier=tier, + ) + db.commit() + + return EmailUpdateResponse( + success=True, + message="Email settings updated successfully", + settings=settings.to_dict(), + ) -@router.post("/verify") +@router.post("/verify", response_model=EmailVerifyResponse) def verify_email_settings( data: VerifyEmailRequest, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), -): +) -> EmailVerifyResponse: """ Verify email settings by sending a test email. Sends a test email to the provided address and updates verification status. + Raises ResourceNotFoundException if settings not configured. + Raises ValidationException if verification fails. """ vendor_id = current_user.token_vendor_id service = VendorEmailSettingsService(db) - try: - result = service.verify_settings(vendor_id, data.test_email) - if result["success"]: - return result - else: - raise HTTPException(status_code=400, detail=result["message"]) - except NotFoundError as e: - raise HTTPException(status_code=404, detail=str(e)) - except ValidationError as e: - raise HTTPException(status_code=400, detail=str(e)) + # Service raises appropriate exceptions (API-003 compliance) + result = service.verify_settings(vendor_id, data.test_email) + db.commit() + + return EmailVerifyResponse( + success=result["success"], + message=result["message"], + ) -@router.delete("") +@router.delete("", response_model=EmailDeleteResponse) def delete_email_settings( current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), -): +) -> EmailDeleteResponse: """ Delete email settings. Warning: This will disable email sending for the vendor. + Raises ResourceNotFoundException if settings not found. """ vendor_id = current_user.token_vendor_id service = VendorEmailSettingsService(db) - if service.delete(vendor_id): - return {"success": True, "message": "Email settings deleted"} - else: - raise HTTPException(status_code=404, detail="Email settings not found") + # Service raises ResourceNotFoundException if not found (API-003 compliance) + service.delete(vendor_id) + db.commit() + + return EmailDeleteResponse( + success=True, + message="Email settings deleted", + ) diff --git a/app/services/vendor_email_settings_service.py b/app/services/vendor_email_settings_service.py index d8afabe6..a62119a3 100644 --- a/app/services/vendor_email_settings_service.py +++ b/app/services/vendor_email_settings_service.py @@ -18,7 +18,12 @@ from email.mime.text import MIMEText from sqlalchemy.orm import Session -from app.exceptions import NotFoundError, ValidationError, AuthorizationError +from app.exceptions import ( + AuthorizationException, + ResourceNotFoundException, + ValidationException, + ExternalServiceException, +) from models.database import ( Vendor, VendorEmailSettings, @@ -57,9 +62,9 @@ class VendorEmailSettingsService: """Get email settings or raise 404.""" settings = self.get_settings(vendor_id) if not settings: - raise NotFoundError( - f"Email settings not found for vendor {vendor_id}. " - "Configure email settings to send emails." + raise ResourceNotFoundException( + resource_type="vendor_email_settings", + identifier=str(vendor_id), ) return settings @@ -125,14 +130,18 @@ class VendorEmailSettingsService: Returns: Updated VendorEmailSettings + + Raises: + AuthorizationException: If trying to use premium provider without required tier """ # Validate premium provider access provider = data.get("provider", "smtp") if provider in [p.value for p in PREMIUM_EMAIL_PROVIDERS]: if current_tier not in PREMIUM_TIERS: - raise AuthorizationError( - f"Provider '{provider}' requires Business or Enterprise tier. " - "Upgrade your plan to use advanced email providers." + raise AuthorizationException( + message=f"Provider '{provider}' requires Business or Enterprise tier. " + "Upgrade your plan to use advanced email providers.", + required_permission="business_tier", ) settings = self.get_settings(vendor_id) @@ -182,21 +191,26 @@ class VendorEmailSettingsService: settings.is_verified = False settings.verification_error = None - self.db.commit() - self.db.refresh(settings) - + self.db.flush() logger.info(f"Updated email settings for vendor {vendor_id}: provider={settings.provider}") return settings - def delete(self, vendor_id: int) -> bool: - """Delete email settings for a vendor.""" + def delete(self, vendor_id: int) -> None: + """ + Delete email settings for a vendor. + + Raises: + ResourceNotFoundException: If settings not found + """ settings = self.get_settings(vendor_id) - if settings: - self.db.delete(settings) - self.db.commit() - logger.info(f"Deleted email settings for vendor {vendor_id}") - return True - return False + if not settings: + raise ResourceNotFoundException( + resource_type="vendor_email_settings", + identifier=str(vendor_id), + ) + self.db.delete(settings) + self.db.flush() + logger.info(f"Deleted email settings for vendor {vendor_id}") # ========================================================================= # VERIFICATION @@ -212,11 +226,18 @@ class VendorEmailSettingsService: Returns: dict with success status and message + + Raises: + ResourceNotFoundException: If settings not found + ValidationException: If settings incomplete """ settings = self.get_settings_or_404(vendor_id) if not settings.is_fully_configured(): - raise ValidationError("Email settings incomplete. Configure all required fields first.") + raise ValidationException( + message="Email settings incomplete. Configure all required fields first.", + field="settings", + ) try: # Send test email based on provider @@ -229,11 +250,14 @@ class VendorEmailSettingsService: elif settings.provider == EmailProvider.SES.value: self._send_ses_test(settings, test_email) else: - raise ValidationError(f"Unknown provider: {settings.provider}") + raise ValidationException( + message=f"Unknown provider: {settings.provider}", + field="provider", + ) # Mark as verified settings.mark_verified() - self.db.commit() + self.db.flush() logger.info(f"Email settings verified for vendor {vendor_id}") return { @@ -241,12 +265,15 @@ class VendorEmailSettingsService: "message": f"Test email sent successfully to {test_email}", } + except (ValidationException, ExternalServiceException): + raise # Re-raise domain exceptions except Exception as e: error_msg = str(e) settings.mark_verification_failed(error_msg) - self.db.commit() + self.db.flush() logger.warning(f"Email verification failed for vendor {vendor_id}: {error_msg}") + # Return error dict instead of raising - verification failure is not a server error return { "success": False, "message": f"Failed to send test email: {error_msg}", @@ -304,7 +331,10 @@ class VendorEmailSettingsService: from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Mail except ImportError: - raise ValidationError("SendGrid library not installed. Contact support.") + raise ExternalServiceException( + service_name="SendGrid", + message="SendGrid library not installed. Contact support.", + ) message = Mail( from_email=(settings.from_email, settings.from_name), @@ -327,7 +357,10 @@ class VendorEmailSettingsService: response = sg.send(message) if response.status_code >= 400: - raise Exception(f"SendGrid error: {response.status_code}") + raise ExternalServiceException( + service_name="SendGrid", + message=f"SendGrid error: HTTP {response.status_code}", + ) def _send_mailgun_test(self, settings: VendorEmailSettings, to_email: str) -> None: """Send test email via Mailgun.""" @@ -356,14 +389,20 @@ class VendorEmailSettingsService: ) if response.status_code >= 400: - raise Exception(f"Mailgun error: {response.text}") + raise ExternalServiceException( + service_name="Mailgun", + message=f"Mailgun error: {response.text}", + ) def _send_ses_test(self, settings: VendorEmailSettings, to_email: str) -> None: """Send test email via Amazon SES.""" try: import boto3 except ImportError: - raise ValidationError("boto3 library not installed. Contact support.") + raise ExternalServiceException( + service_name="Amazon SES", + message="boto3 library not installed. Contact support.", + ) client = boto3.client( "ses", diff --git a/docs/guides/email-settings.md b/docs/guides/email-settings.md new file mode 100644 index 00000000..d1f99ef9 --- /dev/null +++ b/docs/guides/email-settings.md @@ -0,0 +1,254 @@ +# Email Settings Guide + +This guide covers email configuration for both **vendors** and **platform administrators**. The Wizamart platform uses a layered email system where vendors manage their own email sending while the platform handles system-level communications. + +## Overview + +The email system has two distinct configurations: + +| Aspect | Platform (Admin) | Vendor | +|--------|-----------------|--------| +| Purpose | System emails (billing, admin notifications) | Customer-facing emails (orders, marketing) | +| Configuration | Environment variables (.env) + Database overrides | Database (per-vendor) | +| Cost | Platform owner pays | Vendor pays | +| Providers | SMTP, SendGrid, Mailgun, SES | SMTP (all tiers), Premium providers (Business+) | + +--- + +## Vendor Email Settings + +### Getting Started + +As a vendor, you need to configure email settings to send emails to your customers. This includes order confirmations, shipping updates, and marketing emails. + +#### Accessing Email Settings + +1. Log in to your Vendor Dashboard +2. Navigate to **Settings** from the sidebar +3. Click on the **Email** tab + +### Available Providers + +| Provider | Tier Required | Best For | +|----------|---------------|----------| +| SMTP | All tiers | Standard email servers, most common | +| SendGrid | Business+ | High-volume transactional emails | +| Mailgun | Business+ | Developer-friendly API | +| Amazon SES | Business+ | AWS ecosystem, cost-effective | + +### Configuring SMTP + +SMTP is available for all subscription tiers. Common SMTP providers include: +- Gmail (smtp.gmail.com:587) +- Microsoft 365 (smtp.office365.com:587) +- Your hosting provider's SMTP server + +**Required Fields:** +- **From Email**: The sender email address (e.g., orders@yourstore.com) +- **From Name**: The sender display name (e.g., "Your Store") +- **SMTP Host**: Your SMTP server address +- **SMTP Port**: Usually 587 (TLS) or 465 (SSL) +- **SMTP Username**: Your login username +- **SMTP Password**: Your login password +- **Use TLS**: Enable for port 587 (recommended) +- **Use SSL**: Enable for port 465 + +### Configuring Premium Providers (Business+) + +If you have a Business or Enterprise subscription, you can use premium email providers: + +#### SendGrid +1. Create a SendGrid account at [sendgrid.com](https://sendgrid.com) +2. Generate an API key +3. Enter the API key in your vendor settings + +#### Mailgun +1. Create a Mailgun account at [mailgun.com](https://mailgun.com) +2. Add and verify your domain +3. Get your API key from the dashboard +4. Enter the API key and domain in your settings + +#### Amazon SES +1. Set up SES in your AWS account +2. Verify your sender domain/email +3. Create IAM credentials with SES permissions +4. Enter the access key, secret key, and region + +### Verifying Your Configuration + +After configuring your email settings: + +1. Click **Save Settings** +2. Enter a test email address in the **Test Email** field +3. Click **Send Test** +4. Check your inbox for the test email + +If the test fails, check: +- Your credentials are correct +- Your IP/domain is not blocked +- For Gmail: Allow "less secure apps" or use an app password + +### Email Warning Banner + +Until you configure and verify your email settings, you'll see a warning banner at the top of your dashboard. This ensures you don't forget to set up email before your store goes live. + +--- + +## Platform Admin Email Settings + +### Overview + +Platform administrators can configure system-wide email settings for platform communications like: +- Subscription billing notifications +- Admin alerts +- Platform-wide announcements + +### Configuration Sources + +Admin email settings support two configuration sources: + +1. **Environment Variables (.env)** - Default configuration +2. **Database Overrides** - Override .env via the admin UI + +Database settings take priority over .env values. + +### Accessing Admin Email Settings + +1. Log in to the Admin Panel +2. Navigate to **Settings** +3. Click on the **Email** tab + +### Viewing Current Configuration + +The Email tab shows: +- **Provider**: Current email provider (SMTP, SendGrid, etc.) +- **From Email**: Sender email address +- **From Name**: Sender display name +- **Status**: Whether email is configured and enabled +- **DB Overrides**: Whether database overrides are active + +### Editing Settings + +Click **Edit Settings** to modify the email configuration: + +1. Select the email provider +2. Enter the required credentials +3. Configure enabled/debug flags +4. Click **Save Email Settings** + +### Resetting to .env Defaults + +If you've made database overrides and want to revert to .env configuration: + +1. Click **Reset to .env Defaults** +2. Confirm the action + +This removes all email settings from the database, reverting to .env values. + +### Testing Configuration + +1. Enter a test email address +2. Click **Send Test** +3. Check your inbox + +--- + +## Environment Variables Reference + +For platform configuration via .env: + +```env +# Provider: smtp, sendgrid, mailgun, ses +EMAIL_PROVIDER=smtp + +# Sender identity +EMAIL_FROM_ADDRESS=noreply@yourplatform.com +EMAIL_FROM_NAME=Your Platform +EMAIL_REPLY_TO=support@yourplatform.com + +# Behavior +EMAIL_ENABLED=true +EMAIL_DEBUG=false + +# SMTP Configuration +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USER=your-username +SMTP_PASSWORD=your-password +SMTP_USE_TLS=true +SMTP_USE_SSL=false + +# SendGrid +SENDGRID_API_KEY=your-api-key + +# Mailgun +MAILGUN_API_KEY=your-api-key +MAILGUN_DOMAIN=mg.yourdomain.com + +# Amazon SES +AWS_ACCESS_KEY_ID=your-access-key +AWS_SECRET_ACCESS_KEY=your-secret-key +AWS_REGION=eu-west-1 +``` + +--- + +## Tier-Based Branding + +The email system includes tier-based branding for vendor emails: + +| Tier | Branding | +|------|----------| +| Essential | "Powered by Wizamart" footer | +| Professional | "Powered by Wizamart" footer | +| Business | No branding (white-label) | +| Enterprise | No branding (white-label) | + +Business and Enterprise tier vendors get completely white-labeled emails with no Wizamart branding. + +--- + +## Troubleshooting + +### Common Issues + +**"Email sending is disabled"** +- Check that `EMAIL_ENABLED=true` in .env +- Or enable it in the admin settings + +**"Connection refused" on SMTP** +- Verify SMTP host and port +- Check firewall rules +- Ensure TLS/SSL settings match your server + +**"Authentication failed"** +- Double-check username/password +- For Gmail, use an App Password +- For Microsoft 365, check MFA requirements + +**"SendGrid error: 403"** +- Verify your API key has Mail Send permissions +- Check sender identity is verified + +**Premium provider not available** +- Upgrade to Business or Enterprise tier +- Contact support if you have the right tier but can't access + +### Debug Mode + +Enable debug mode to log emails instead of sending them: +- Set `EMAIL_DEBUG=true` in .env +- Or enable "Debug mode" in admin settings + +Debug mode logs the email content to the server logs without actually sending. + +--- + +## Security Best Practices + +1. **Never share API keys or passwords** in logs or frontend +2. **Use environment variables** for sensitive credentials +3. **Enable TLS** for SMTP connections +4. **Verify sender domains** with your email provider +5. **Monitor email logs** for delivery issues +6. **Rotate credentials** periodically diff --git a/docs/implementation/email-settings.md b/docs/implementation/email-settings.md new file mode 100644 index 00000000..9c871cd7 --- /dev/null +++ b/docs/implementation/email-settings.md @@ -0,0 +1,308 @@ +# Email Settings Implementation + +This document describes the technical implementation of the email settings system for both vendor and platform (admin) configurations. + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Email System Architecture │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ Platform Email │ │ Vendor Email │ │ +│ │ (Admin/Billing)│ │ (Customer-facing) │ │ +│ └────────┬─────────┘ └────────┬─────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ get_platform_ │ │ get_vendor_ │ │ +│ │ email_config(db) │ │ provider() │ │ +│ └────────┬─────────┘ └────────┬─────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ AdminSettings DB │ │VendorEmailSettings│ │ +│ │ (.env fallback)│ │ (per vendor) │ │ +│ └────────┬─────────┘ └────────┬─────────┘ │ +│ │ │ │ +│ └───────────┬───────────────┘ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ EmailService │ │ +│ │ send_raw() │ │ +│ └────────┬─────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ Email Providers │ │ +│ │ SMTP/SG/MG/SES │ │ +│ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Database Models + +### VendorEmailSettings + +```python +# models/database/vendor_email_settings.py + +class VendorEmailSettings(Base): + __tablename__ = "vendor_email_settings" + + id: int + vendor_id: int # FK to vendors.id (one-to-one) + + # Sender Identity + from_email: str + from_name: str + reply_to_email: str | None + + # Signature + signature_text: str | None + signature_html: str | None + + # Provider + provider: str = "smtp" # smtp, sendgrid, mailgun, ses + + # SMTP Settings + smtp_host: str | None + smtp_port: int = 587 + smtp_username: str | None + smtp_password: str | None + smtp_use_tls: bool = True + smtp_use_ssl: bool = False + + # SendGrid + sendgrid_api_key: str | None + + # Mailgun + mailgun_api_key: str | None + mailgun_domain: str | None + + # SES + ses_access_key_id: str | None + ses_secret_access_key: str | None + ses_region: str = "eu-west-1" + + # Status + is_configured: bool = False + is_verified: bool = False + last_verified_at: datetime | None + verification_error: str | None +``` + +### Admin Settings (Platform Email) + +Platform email settings are stored in the generic `admin_settings` table with category="email": + +```python +# Keys stored in admin_settings table +EMAIL_SETTING_KEYS = { + "email_provider", + "email_from_address", + "email_from_name", + "email_reply_to", + "smtp_host", + "smtp_port", + "smtp_user", + "smtp_password", + "smtp_use_tls", + "smtp_use_ssl", + "sendgrid_api_key", + "mailgun_api_key", + "mailgun_domain", + "aws_access_key_id", + "aws_secret_access_key", + "aws_region", + "email_enabled", + "email_debug", +} +``` + +## API Endpoints + +### Vendor Email Settings + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/vendor/email-settings` | GET | Get current email settings | +| `/api/v1/vendor/email-settings` | PUT | Create/update email settings | +| `/api/v1/vendor/email-settings` | DELETE | Delete email settings | +| `/api/v1/vendor/email-settings/status` | GET | Get configuration status | +| `/api/v1/vendor/email-settings/providers` | GET | Get available providers for tier | +| `/api/v1/vendor/email-settings/verify` | POST | Send test email | + +### Admin Email Settings + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/admin/settings/email/status` | GET | Get effective email config | +| `/api/v1/admin/settings/email/settings` | PUT | Update email settings in DB | +| `/api/v1/admin/settings/email/settings` | DELETE | Reset to .env defaults | +| `/api/v1/admin/settings/email/test` | POST | Send test email | + +## Services + +### VendorEmailSettingsService + +Location: `app/services/vendor_email_settings_service.py` + +Key methods: +- `get_settings(vendor_id)` - Get settings for a vendor +- `create_or_update(vendor_id, data, current_tier)` - Create/update settings +- `delete(vendor_id)` - Delete settings +- `verify_settings(vendor_id, test_email)` - Send test email +- `get_available_providers(tier)` - Get providers for subscription tier + +### EmailService Integration + +The EmailService (`app/services/email_service.py`) uses: + +1. **Platform Config**: `get_platform_email_config(db)` checks database first, then .env +2. **Vendor Config**: `get_vendor_provider(settings)` creates provider from VendorEmailSettings +3. **Provider Selection**: `send_raw()` uses vendor provider when `vendor_id` provided and `is_platform_email=False` + +```python +# EmailService.send_raw() flow +def send_raw(self, to_email, subject, body_html, vendor_id=None, is_platform_email=False): + if vendor_id and not is_platform_email: + # Use vendor's email provider + vendor_settings = self._get_vendor_email_settings(vendor_id) + if vendor_settings and vendor_settings.is_configured: + provider = get_vendor_provider(vendor_settings) + else: + # Use platform provider (DB config > .env) + provider = self.provider # Set in __init__ via get_platform_provider(db) +``` + +## Tier-Based Features + +### Premium Provider Gating + +Premium providers (SendGrid, Mailgun, SES) are gated to Business+ tiers: + +```python +PREMIUM_EMAIL_PROVIDERS = {EmailProvider.SENDGRID, EmailProvider.MAILGUN, EmailProvider.SES} +PREMIUM_TIERS = {TierCode.BUSINESS, TierCode.ENTERPRISE} + +def create_or_update(self, vendor_id, data, current_tier): + provider = data.get("provider", "smtp") + if provider in [p.value for p in PREMIUM_EMAIL_PROVIDERS]: + if current_tier not in PREMIUM_TIERS: + raise AuthorizationException(...) +``` + +### White-Label Branding + +Emails include "Powered by Wizamart" footer for non-whitelabel tiers: + +```python +WHITELABEL_TIERS = {"business", "enterprise"} + +POWERED_BY_FOOTER_HTML = """ +
Powered by Wizamart
+