docs: migrate module documentation to single source of truth
Move 39 documentation files from top-level docs/ into each module's docs/ folder, accessible via symlinks from docs/modules/. Create data-model.md files for 10 modules with full schema documentation. Replace originals with redirect stubs. Remove empty guide stubs. Modules migrated: tenancy, billing, loyalty, marketplace, orders, messaging, cms, catalog, inventory, hosting, prospecting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
243
app/modules/messaging/docs/architecture.md
Normal file
243
app/modules/messaging/docs/architecture.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# Messaging System Implementation
|
||||
|
||||
This document describes the messaging system that enables threaded conversations between different platform participants.
|
||||
|
||||
## Overview
|
||||
|
||||
The messaging system supports three communication channels:
|
||||
|
||||
1. **Admin <-> Store**: Platform administrators communicate with store users
|
||||
2. **Store <-> Customer**: Stores communicate with their customers
|
||||
3. **Admin <-> Customer**: Platform administrators communicate with customers
|
||||
|
||||
## Architecture
|
||||
|
||||
### Database Models
|
||||
|
||||
Located in `models/database/message.py`:
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| `Conversation` | Threaded conversation container with subject, type, and status |
|
||||
| `ConversationParticipant` | Links participants to conversations with unread tracking |
|
||||
| `Message` | Individual messages within a conversation |
|
||||
| `MessageAttachment` | File attachments for messages |
|
||||
|
||||
### Enums
|
||||
|
||||
| Enum | Values | Description |
|
||||
|------|--------|-------------|
|
||||
| `ConversationType` | `admin_store`, `store_customer`, `admin_customer` | Defines conversation channel |
|
||||
| `ParticipantType` | `admin`, `store`, `customer` | Type of participant |
|
||||
|
||||
### Polymorphic Participants
|
||||
|
||||
The system uses polymorphic relationships via `participant_type` + `participant_id`:
|
||||
- `admin` and `store` types reference `users.id`
|
||||
- `customer` type references `customers.id`
|
||||
|
||||
### Multi-Tenant Isolation
|
||||
|
||||
Conversations involving customers include a `store_id` to ensure proper data isolation. Store users can only see conversations within their store context.
|
||||
|
||||
## Services
|
||||
|
||||
### MessagingService (`app/services/messaging_service.py`)
|
||||
|
||||
Core business logic for conversations and messages:
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `create_conversation()` | Create a new conversation with participants |
|
||||
| `get_conversation()` | Get conversation with access validation |
|
||||
| `list_conversations()` | Paginated list with filters |
|
||||
| `send_message()` | Send message with automatic unread updates |
|
||||
| `mark_conversation_read()` | Mark all messages read for participant |
|
||||
| `get_unread_count()` | Get total unread count for header badge |
|
||||
| `close_conversation()` | Close a conversation thread |
|
||||
| `reopen_conversation()` | Reopen a closed conversation |
|
||||
|
||||
### MessageAttachmentService (`app/services/message_attachment_service.py`)
|
||||
|
||||
File upload handling:
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `validate_and_store()` | Validate file type/size and store to disk |
|
||||
| `get_max_file_size_bytes()` | Get limit from platform settings |
|
||||
| `delete_attachment()` | Remove files from storage |
|
||||
|
||||
**Allowed file types:**
|
||||
- Images: JPEG, PNG, GIF, WebP
|
||||
- Documents: PDF, Office documents
|
||||
- Archives: ZIP
|
||||
- Text: Plain text, CSV
|
||||
|
||||
**Storage path pattern:** `uploads/messages/YYYY/MM/conversation_id/uuid.ext`
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Admin API (`/api/v1/admin/messages`)
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/messages` | GET | List conversations |
|
||||
| `/messages` | POST | Create conversation |
|
||||
| `/messages/unread-count` | GET | Get unread badge count |
|
||||
| `/messages/recipients` | GET | Get available recipients |
|
||||
| `/messages/{id}` | GET | Get conversation detail |
|
||||
| `/messages/{id}/messages` | POST | Send message (with attachments) |
|
||||
| `/messages/{id}/close` | POST | Close conversation |
|
||||
| `/messages/{id}/reopen` | POST | Reopen conversation |
|
||||
| `/messages/{id}/read` | PUT | Mark as read |
|
||||
| `/messages/{id}/preferences` | PUT | Update notification preferences |
|
||||
|
||||
### Store API (`/api/v1/store/messages`)
|
||||
|
||||
Same structure as admin, but with store context filtering. Stores can only:
|
||||
- See their own store_customer and admin_store conversations
|
||||
- Create store_customer conversations with their customers
|
||||
- Not initiate admin_store conversations (admins initiate those)
|
||||
|
||||
## Frontend
|
||||
|
||||
### Admin Interface
|
||||
|
||||
- **Template:** `app/templates/admin/messages.html`
|
||||
- **JavaScript:** `static/admin/js/messages.js`
|
||||
|
||||
Features:
|
||||
- Split-panel conversation list + message thread
|
||||
- Filters by type (stores/customers) and status (open/closed)
|
||||
- Compose modal for new conversations
|
||||
- File attachment support
|
||||
- 30-second polling for new messages
|
||||
- Header badge with unread count
|
||||
|
||||
### Store Interface
|
||||
|
||||
- **Template:** `app/templates/store/messages.html`
|
||||
- **JavaScript:** `static/store/js/messages.js`
|
||||
|
||||
Similar to admin but with store-specific:
|
||||
- Only store_customer and admin_store channels
|
||||
- Compose modal for customer conversations only
|
||||
|
||||
## Pydantic Schemas
|
||||
|
||||
Located in `models/schema/message.py`:
|
||||
|
||||
- `ConversationCreate` - Create request
|
||||
- `ConversationSummary` - List item with unread count
|
||||
- `ConversationDetailResponse` - Full thread with messages
|
||||
- `ConversationListResponse` - Paginated list
|
||||
- `MessageResponse` - Single message with attachments
|
||||
- `AttachmentResponse` - File metadata with download URL
|
||||
- `UnreadCountResponse` - For header badge
|
||||
|
||||
## Configuration
|
||||
|
||||
### Platform Setting
|
||||
|
||||
The attachment size limit is configurable via platform settings:
|
||||
|
||||
- **Key:** `message_attachment_max_size_mb`
|
||||
- **Default:** 10
|
||||
- **Category:** messaging
|
||||
|
||||
## Storefront (Customer) Interface
|
||||
|
||||
### API Endpoints (`/api/v1/storefront/messages`)
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/messages` | GET | List customer's conversations |
|
||||
| `/messages/unread-count` | GET | Get unread badge count |
|
||||
| `/messages/{id}` | GET | Get conversation detail |
|
||||
| `/messages/{id}/messages` | POST | Send reply message |
|
||||
| `/messages/{id}/read` | PUT | Mark as read |
|
||||
| `/messages/{id}/attachments/{att_id}` | GET | Download attachment |
|
||||
|
||||
### Frontend
|
||||
|
||||
- **Template:** `app/templates/storefront/account/messages.html`
|
||||
- **Page Route:** `/storefront/account/messages` and `/storefront/account/messages/{conversation_id}`
|
||||
|
||||
Features:
|
||||
- Conversation list with unread badges
|
||||
- Filter by status (open/closed)
|
||||
- Thread view with message history
|
||||
- Reply form with file attachments
|
||||
- 30-second polling for new messages
|
||||
- Link from account dashboard with unread count
|
||||
|
||||
### Limitations
|
||||
|
||||
Customers can only:
|
||||
- View their `store_customer` conversations
|
||||
- Reply to existing conversations (cannot initiate)
|
||||
- Cannot close conversations
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Email Notifications (Requires Email Infrastructure)
|
||||
|
||||
The messaging system is designed to support email notifications, but requires email infrastructure to be implemented first:
|
||||
|
||||
**Prerequisites:**
|
||||
- SMTP configuration in settings (host, port, username, password)
|
||||
- Email service (`app/services/email_service.py`)
|
||||
- Email templates (`app/templates/emails/`)
|
||||
- Background task queue for async sending
|
||||
|
||||
**Planned Implementation:**
|
||||
1. **MessageNotificationService** (`app/services/message_notification_service.py`)
|
||||
- `notify_new_message()` - Send email to participants on new message
|
||||
- Respect per-conversation `email_notifications` preference
|
||||
- Include message preview and reply link
|
||||
|
||||
2. **Email Template** (`app/templates/emails/new_message.html`)
|
||||
- Subject: "New message: {conversation_subject}"
|
||||
- Body: Sender name, message preview, link to reply
|
||||
|
||||
3. **Integration Points:**
|
||||
- Call `notify_new_message()` from `messaging_service.send_message()`
|
||||
- Skip notification for sender (only notify other participants)
|
||||
- Rate limit to prevent spam on rapid message exchanges
|
||||
|
||||
**Database Support:**
|
||||
The `email_notifications` field on `ConversationParticipant` is already in place to store per-conversation preferences.
|
||||
|
||||
### WebSocket Support (Optional)
|
||||
|
||||
Real-time message delivery instead of 30-second polling:
|
||||
- Would require WebSocket infrastructure (e.g., FastAPI WebSocket, Redis pub/sub)
|
||||
- Significant infrastructure changes
|
||||
|
||||
## Migration
|
||||
|
||||
The messaging tables are created by migration `e3f4a5b6c7d8_add_messaging_tables.py`:
|
||||
|
||||
```bash
|
||||
# Apply migration
|
||||
alembic upgrade head
|
||||
|
||||
# Rollback
|
||||
alembic downgrade -1
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
### Admin Sidebar
|
||||
Messages is available under "Platform Administration" section.
|
||||
|
||||
### Store Sidebar
|
||||
Messages is available under "Sales" section.
|
||||
|
||||
### Storefront Account Dashboard
|
||||
Messages card is available on the customer account dashboard with unread count badge.
|
||||
|
||||
### Header Badge
|
||||
Both admin and store headers show an unread message count badge next to the messages icon.
|
||||
290
app/modules/messaging/docs/data-model.md
Normal file
290
app/modules/messaging/docs/data-model.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# Messaging Data Model
|
||||
|
||||
Entity relationships and database schema for the messaging module.
|
||||
|
||||
## Entity Relationship Overview
|
||||
|
||||
```
|
||||
Store 1──1 StoreEmailSettings
|
||||
Store 1──* StoreEmailTemplate
|
||||
Store 1──* Conversation 1──* Message 1──* MessageAttachment
|
||||
└──* ConversationParticipant
|
||||
|
||||
EmailTemplate 1──* EmailLog
|
||||
```
|
||||
|
||||
## Models
|
||||
|
||||
### EmailTemplate
|
||||
|
||||
Multi-language email templates stored in database with Jinja2 variable interpolation.
|
||||
|
||||
| Field | Type | Constraints | Description |
|
||||
|-------|------|-------------|-------------|
|
||||
| `id` | Integer | PK | Primary key |
|
||||
| `code` | String(100) | not null, indexed | Template identifier (e.g., "signup_welcome") |
|
||||
| `language` | String(5) | not null, default "en" | Language code |
|
||||
| `name` | String(255) | not null | Human-readable name |
|
||||
| `description` | Text | nullable | Template purpose description |
|
||||
| `category` | String(50) | not null, default "system", indexed | auth, orders, billing, system, marketing |
|
||||
| `subject` | String(500) | not null | Subject line (supports variables) |
|
||||
| `body_html` | Text | not null | HTML body content |
|
||||
| `body_text` | Text | nullable | Plain text fallback |
|
||||
| `variables` | Text | nullable | JSON list of expected variables |
|
||||
| `required_variables` | Text | nullable | JSON list of mandatory variables |
|
||||
| `is_active` | Boolean | not null, default True | Activation status |
|
||||
| `is_platform_only` | Boolean | not null, default False | If True, stores cannot override |
|
||||
| `created_at` | DateTime | tz-aware | Record creation time |
|
||||
| `updated_at` | DateTime | tz-aware | Record update time |
|
||||
|
||||
**Unique Index**: `(code, language)`
|
||||
|
||||
### StoreEmailTemplate
|
||||
|
||||
Store-specific email template overrides. Stores can customize platform templates without modifying defaults.
|
||||
|
||||
| Field | Type | Constraints | Description |
|
||||
|-------|------|-------------|-------------|
|
||||
| `id` | Integer | PK | Primary key |
|
||||
| `store_id` | Integer | FK, not null, indexed | Store owning the override |
|
||||
| `template_code` | String(100) | not null, indexed | References EmailTemplate.code |
|
||||
| `language` | String(5) | not null, default "en" | Language code |
|
||||
| `name` | String(255) | nullable | Custom name (null = use platform) |
|
||||
| `subject` | String(500) | not null | Custom subject line |
|
||||
| `body_html` | Text | not null | Custom HTML body |
|
||||
| `body_text` | Text | nullable | Custom plain text body |
|
||||
| `is_active` | Boolean | not null, default True | Activation status |
|
||||
| `created_at` | DateTime | tz-aware | Record creation time |
|
||||
| `updated_at` | DateTime | tz-aware | Record update time |
|
||||
|
||||
**Unique Constraint**: `(store_id, template_code, language)`
|
||||
|
||||
### EmailLog
|
||||
|
||||
Email sending history and tracking for debugging, analytics, and compliance.
|
||||
|
||||
| Field | Type | Constraints | Description |
|
||||
|-------|------|-------------|-------------|
|
||||
| `id` | Integer | PK | Primary key |
|
||||
| `template_code` | String(100) | nullable, indexed | Reference to template code |
|
||||
| `template_id` | Integer | FK, nullable | Reference to template |
|
||||
| `recipient_email` | String(255) | not null, indexed | Recipient address |
|
||||
| `recipient_name` | String(255) | nullable | Recipient name |
|
||||
| `subject` | String(500) | not null | Email subject line |
|
||||
| `body_html` | Text | nullable | HTML body snapshot |
|
||||
| `body_text` | Text | nullable | Plain text body snapshot |
|
||||
| `from_email` | String(255) | not null | Sender email address |
|
||||
| `from_name` | String(255) | nullable | Sender name |
|
||||
| `reply_to` | String(255) | nullable | Reply-to address |
|
||||
| `status` | String(20) | not null, default "pending", indexed | pending, sent, failed, bounced, delivered, opened, clicked |
|
||||
| `sent_at` | DateTime | nullable | When sent |
|
||||
| `delivered_at` | DateTime | nullable | When delivered |
|
||||
| `opened_at` | DateTime | nullable | When opened |
|
||||
| `clicked_at` | DateTime | nullable | When clicked |
|
||||
| `error_message` | Text | nullable | Error details if failed |
|
||||
| `retry_count` | Integer | not null, default 0 | Retry attempts |
|
||||
| `provider` | String(50) | nullable | smtp, sendgrid, mailgun, ses |
|
||||
| `provider_message_id` | String(255) | nullable, indexed | Provider's message ID |
|
||||
| `store_id` | Integer | FK, nullable, indexed | Associated store |
|
||||
| `user_id` | Integer | FK, nullable, indexed | Associated user |
|
||||
| `related_type` | String(50) | nullable | Related entity type |
|
||||
| `related_id` | Integer | nullable | Related entity ID |
|
||||
| `extra_data` | Text | nullable | JSON additional context |
|
||||
| `created_at` | DateTime | tz-aware | Record creation time |
|
||||
| `updated_at` | DateTime | tz-aware | Record update time |
|
||||
|
||||
### StoreEmailSettings
|
||||
|
||||
Per-store email sending configuration. One-to-one with Store.
|
||||
|
||||
| Field | Type | Constraints | Description |
|
||||
|-------|------|-------------|-------------|
|
||||
| `id` | Integer | PK | Primary key |
|
||||
| `store_id` | Integer | FK, unique, not null | One-to-one with store |
|
||||
| `from_email` | String(255) | not null | Sender email address |
|
||||
| `from_name` | String(100) | not null | Sender display name |
|
||||
| `reply_to_email` | String(255) | nullable | Reply-to address |
|
||||
| `signature_text` | Text | nullable | Plain text signature |
|
||||
| `signature_html` | Text | nullable | HTML signature/footer |
|
||||
| `provider` | String(20) | not null, default "smtp" | smtp, sendgrid, mailgun, ses |
|
||||
| `smtp_host` | String(255) | nullable | SMTP server hostname |
|
||||
| `smtp_port` | Integer | nullable, default 587 | SMTP port |
|
||||
| `smtp_username` | String(255) | nullable | SMTP username |
|
||||
| `smtp_password` | String(500) | nullable | SMTP password (encrypted) |
|
||||
| `smtp_use_tls` | Boolean | not null, default True | Use TLS |
|
||||
| `smtp_use_ssl` | Boolean | not null, default False | Use SSL (port 465) |
|
||||
| `sendgrid_api_key` | String(500) | nullable | SendGrid API key (encrypted) |
|
||||
| `mailgun_api_key` | String(500) | nullable | Mailgun API key (encrypted) |
|
||||
| `mailgun_domain` | String(255) | nullable | Mailgun domain |
|
||||
| `ses_access_key_id` | String(100) | nullable | SES access key ID |
|
||||
| `ses_secret_access_key` | String(500) | nullable | SES secret key (encrypted) |
|
||||
| `ses_region` | String(50) | nullable, default "eu-west-1" | AWS region |
|
||||
| `is_configured` | Boolean | not null, default False | Has complete config |
|
||||
| `is_verified` | Boolean | not null, default False | Test email succeeded |
|
||||
| `last_verified_at` | DateTime | nullable, tz-aware | Last verification |
|
||||
| `verification_error` | Text | nullable | Last error message |
|
||||
| `created_at` | DateTime | tz-aware | Record creation time |
|
||||
| `updated_at` | DateTime | tz-aware | Record update time |
|
||||
|
||||
**Index**: `(store_id, is_configured)`
|
||||
|
||||
### AdminNotification
|
||||
|
||||
Admin-specific notifications for system alerts and warnings.
|
||||
|
||||
| Field | Type | Constraints | Description |
|
||||
|-------|------|-------------|-------------|
|
||||
| `id` | Integer | PK | Primary key |
|
||||
| `type` | String(50) | not null, indexed | system_alert, store_issue, import_failure |
|
||||
| `priority` | String(20) | not null, default "normal", indexed | low, normal, high, critical |
|
||||
| `title` | String(200) | not null | Notification title |
|
||||
| `message` | Text | not null | Notification message |
|
||||
| `is_read` | Boolean | not null, default False, indexed | Read status |
|
||||
| `read_at` | DateTime | nullable | When read |
|
||||
| `read_by_user_id` | Integer | FK, nullable | User who read it |
|
||||
| `action_required` | Boolean | not null, default False, indexed | Action needed |
|
||||
| `action_url` | String(500) | nullable | Link to relevant admin page |
|
||||
| `notification_metadata` | JSON | nullable | Additional context |
|
||||
| `created_at` | DateTime | tz-aware | Record creation time |
|
||||
| `updated_at` | DateTime | tz-aware | Record update time |
|
||||
|
||||
### Conversation
|
||||
|
||||
Threaded conversation between participants with multi-tenant isolation.
|
||||
|
||||
| Field | Type | Constraints | Description |
|
||||
|-------|------|-------------|-------------|
|
||||
| `id` | Integer | PK | Primary key |
|
||||
| `conversation_type` | Enum | not null, indexed | admin_store, store_customer, admin_customer |
|
||||
| `subject` | String(500) | not null | Thread subject line |
|
||||
| `store_id` | Integer | FK, nullable, indexed | Multi-tenant isolation |
|
||||
| `is_closed` | Boolean | not null, default False | Closed status |
|
||||
| `closed_at` | DateTime | nullable | When closed |
|
||||
| `closed_by_type` | Enum | nullable | Type of closer |
|
||||
| `closed_by_id` | Integer | nullable | ID of closer |
|
||||
| `last_message_at` | DateTime | nullable, indexed | Last activity |
|
||||
| `message_count` | Integer | not null, default 0 | Total messages |
|
||||
| `created_at` | DateTime | tz-aware | Record creation time |
|
||||
| `updated_at` | DateTime | tz-aware | Record update time |
|
||||
|
||||
**Composite Index**: `(conversation_type, store_id)`
|
||||
|
||||
### ConversationParticipant
|
||||
|
||||
Links participants (users or customers) to conversations with polymorphic relationships.
|
||||
|
||||
| Field | Type | Constraints | Description |
|
||||
|-------|------|-------------|-------------|
|
||||
| `id` | Integer | PK | Primary key |
|
||||
| `conversation_id` | Integer | FK, not null, indexed | Parent conversation |
|
||||
| `participant_type` | Enum | not null | admin, store, customer |
|
||||
| `participant_id` | Integer | not null, indexed | Polymorphic participant ID |
|
||||
| `store_id` | Integer | FK, nullable | Store context |
|
||||
| `unread_count` | Integer | not null, default 0 | Unread messages |
|
||||
| `last_read_at` | DateTime | nullable | Last read time |
|
||||
| `email_notifications` | Boolean | not null, default True | Email notification pref |
|
||||
| `muted` | Boolean | not null, default False | Muted status |
|
||||
| `created_at` | DateTime | tz-aware | Record creation time |
|
||||
| `updated_at` | DateTime | tz-aware | Record update time |
|
||||
|
||||
**Unique Constraint**: `(conversation_id, participant_type, participant_id)`
|
||||
**Composite Index**: `(participant_type, participant_id)`
|
||||
|
||||
### Message
|
||||
|
||||
Individual message within a conversation thread.
|
||||
|
||||
| Field | Type | Constraints | Description |
|
||||
|-------|------|-------------|-------------|
|
||||
| `id` | Integer | PK | Primary key |
|
||||
| `conversation_id` | Integer | FK, not null, indexed | Parent conversation |
|
||||
| `sender_type` | Enum | not null | admin, store, customer |
|
||||
| `sender_id` | Integer | not null, indexed | Polymorphic sender ID |
|
||||
| `content` | Text | not null | Message body |
|
||||
| `is_system_message` | Boolean | not null, default False | System-generated flag |
|
||||
| `is_deleted` | Boolean | not null, default False | Soft delete flag |
|
||||
| `deleted_at` | DateTime | nullable | When deleted |
|
||||
| `deleted_by_type` | Enum | nullable | Type of deleter |
|
||||
| `deleted_by_id` | Integer | nullable | ID of deleter |
|
||||
| `created_at` | DateTime | tz-aware | Record creation time |
|
||||
| `updated_at` | DateTime | tz-aware | Record update time |
|
||||
|
||||
**Composite Index**: `(conversation_id, created_at)`
|
||||
|
||||
### MessageAttachment
|
||||
|
||||
File attachments for messages.
|
||||
|
||||
| Field | Type | Constraints | Description |
|
||||
|-------|------|-------------|-------------|
|
||||
| `id` | Integer | PK | Primary key |
|
||||
| `message_id` | Integer | FK, not null, indexed | Parent message |
|
||||
| `filename` | String(255) | not null | System filename |
|
||||
| `original_filename` | String(255) | not null | Original upload name |
|
||||
| `file_path` | String(1000) | not null | Storage path |
|
||||
| `file_size` | Integer | not null | File size in bytes |
|
||||
| `mime_type` | String(100) | not null | MIME type |
|
||||
| `is_image` | Boolean | not null, default False | Image flag |
|
||||
| `image_width` | Integer | nullable | Width in pixels |
|
||||
| `image_height` | Integer | nullable | Height in pixels |
|
||||
| `thumbnail_path` | String(1000) | nullable | Thumbnail path |
|
||||
| `created_at` | DateTime | tz-aware | Record creation time |
|
||||
| `updated_at` | DateTime | tz-aware | Record update time |
|
||||
|
||||
## Enums
|
||||
|
||||
### EmailCategory
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `auth` | Signup, password reset, verification |
|
||||
| `orders` | Order confirmations, shipping |
|
||||
| `billing` | Invoices, payment failures |
|
||||
| `system` | Team invites, notifications |
|
||||
| `marketing` | Newsletters, promotions |
|
||||
|
||||
### EmailStatus
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `pending` | Queued for sending |
|
||||
| `sent` | Sent to provider |
|
||||
| `failed` | Send failed |
|
||||
| `bounced` | Bounced back |
|
||||
| `delivered` | Confirmed delivered |
|
||||
| `opened` | Recipient opened |
|
||||
| `clicked` | Link clicked |
|
||||
|
||||
### EmailProvider
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `smtp` | Standard SMTP (all tiers) |
|
||||
| `sendgrid` | SendGrid API (Business+ tier) |
|
||||
| `mailgun` | Mailgun API (Business+ tier) |
|
||||
| `ses` | Amazon SES (Business+ tier) |
|
||||
|
||||
### ConversationType
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `admin_store` | Admin-store conversations |
|
||||
| `store_customer` | Store-customer conversations |
|
||||
| `admin_customer` | Admin-customer conversations |
|
||||
|
||||
### ParticipantType
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `admin` | Platform admin user |
|
||||
| `store` | Store team user |
|
||||
| `customer` | Customer |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
- **Template override system**: Platform templates + per-store overrides with language fallback
|
||||
- **Polymorphic participants**: Conversations support admin, store, and customer participants
|
||||
- **Email tracking**: Full lifecycle tracking (sent → delivered → opened → clicked)
|
||||
- **Provider abstraction**: Multiple email providers with per-store configuration
|
||||
- **Premium tier gating**: SendGrid, Mailgun, SES require Business+ tier
|
||||
- **Soft deletes**: Messages support soft delete with audit trail
|
||||
308
app/modules/messaging/docs/email-settings-impl.md
Normal file
308
app/modules/messaging/docs/email-settings-impl.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# Email Settings Implementation
|
||||
|
||||
This document describes the technical implementation of the email settings system for both store and platform (admin) configurations.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Email System Architecture │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Platform Email │ │ Store Email │ │
|
||||
│ │ (Admin/Billing)│ │ (Customer-facing) │ │
|
||||
│ └────────┬─────────┘ └────────┬─────────┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ get_platform_ │ │ get_store_ │ │
|
||||
│ │ email_config(db) │ │ provider() │ │
|
||||
│ └────────┬─────────┘ └────────┬─────────┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ AdminSettings DB │ │StoreEmailSettings│ │
|
||||
│ │ (.env fallback)│ │ (per store) │ │
|
||||
│ └────────┬─────────┘ └────────┬─────────┘ │
|
||||
│ │ │ │
|
||||
│ └───────────┬───────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ EmailService │ │
|
||||
│ │ send_raw() │ │
|
||||
│ └────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ Email Providers │ │
|
||||
│ │ SMTP/SG/MG/SES │ │
|
||||
│ └──────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Database Models
|
||||
|
||||
### StoreEmailSettings
|
||||
|
||||
```python
|
||||
# models/database/store_email_settings.py
|
||||
|
||||
class StoreEmailSettings(Base):
|
||||
__tablename__ = "store_email_settings"
|
||||
|
||||
id: int
|
||||
store_id: int # FK to stores.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
|
||||
|
||||
### Store Email Settings
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/v1/store/email-settings` | GET | Get current email settings |
|
||||
| `/api/v1/store/email-settings` | PUT | Create/update email settings |
|
||||
| `/api/v1/store/email-settings` | DELETE | Delete email settings |
|
||||
| `/api/v1/store/email-settings/status` | GET | Get configuration status |
|
||||
| `/api/v1/store/email-settings/providers` | GET | Get available providers for tier |
|
||||
| `/api/v1/store/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
|
||||
|
||||
### StoreEmailSettingsService
|
||||
|
||||
Location: `app/services/store_email_settings_service.py`
|
||||
|
||||
Key methods:
|
||||
- `get_settings(store_id)` - Get settings for a store
|
||||
- `create_or_update(store_id, data, current_tier)` - Create/update settings
|
||||
- `delete(store_id)` - Delete settings
|
||||
- `verify_settings(store_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. **Store Config**: `get_store_provider(settings)` creates provider from StoreEmailSettings
|
||||
3. **Provider Selection**: `send_raw()` uses store provider when `store_id` provided and `is_platform_email=False`
|
||||
|
||||
```python
|
||||
# EmailService.send_raw() flow
|
||||
def send_raw(self, to_email, subject, body_html, store_id=None, is_platform_email=False):
|
||||
if store_id and not is_platform_email:
|
||||
# Use store's email provider
|
||||
store_settings = self._get_store_email_settings(store_id)
|
||||
if store_settings and store_settings.is_configured:
|
||||
provider = get_store_provider(store_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, store_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 Orion" footer for non-whitelabel tiers:
|
||||
|
||||
```python
|
||||
WHITELABEL_TIERS = {"business", "enterprise"}
|
||||
|
||||
POWERED_BY_FOOTER_HTML = """
|
||||
<div style="margin-top: 30px; ...">
|
||||
<p>Powered by <a href="https://orion.lu">Orion</a></p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def _inject_powered_by_footer(self, body_html, store_id):
|
||||
tier = self._get_store_tier(store_id)
|
||||
if tier and tier.lower() in WHITELABEL_TIERS:
|
||||
return body_html # No footer for business/enterprise
|
||||
return body_html.replace("</body>", f"{POWERED_BY_FOOTER_HTML}</body>")
|
||||
```
|
||||
|
||||
## Configuration Priority
|
||||
|
||||
### Platform Email
|
||||
|
||||
1. **Database** (admin_settings table) - Highest priority
|
||||
2. **Environment Variables** (.env) - Fallback
|
||||
|
||||
```python
|
||||
def get_platform_email_config(db: Session) -> dict:
|
||||
def get_db_setting(key: str) -> str | None:
|
||||
setting = db.query(AdminSetting).filter(AdminSetting.key == key).first()
|
||||
return setting.value if setting else None
|
||||
|
||||
# Check DB first, fallback to .env
|
||||
db_provider = get_db_setting("email_provider")
|
||||
config["provider"] = db_provider if db_provider else settings.email_provider
|
||||
...
|
||||
```
|
||||
|
||||
### Store Email
|
||||
|
||||
Stores have their own dedicated settings table with no fallback - they must configure their own email.
|
||||
|
||||
## Frontend Components
|
||||
|
||||
### Store Settings Page
|
||||
|
||||
- **Location**: `app/templates/store/settings.html`, `static/store/js/settings.js`
|
||||
- **Alpine.js State**: `emailSettings`, `emailForm`, `hasEmailChanges`
|
||||
- **Methods**: `loadEmailSettings()`, `saveEmailSettings()`, `sendTestEmail()`
|
||||
|
||||
### Admin Settings Page
|
||||
|
||||
- **Location**: `app/templates/admin/settings.html`, `static/admin/js/settings.js`
|
||||
- **Alpine.js State**: `emailSettings`, `emailForm`, `emailEditMode`
|
||||
- **Methods**: `loadEmailSettings()`, `saveEmailSettings()`, `resetEmailSettings()`, `sendTestEmail()`
|
||||
|
||||
### Warning Banner
|
||||
|
||||
Shows until email is configured:
|
||||
|
||||
```html
|
||||
<!-- app/templates/shared/macros/feature_gate.html -->
|
||||
{% macro email_settings_warning() %}
|
||||
<div x-data="emailSettingsWarning()" x-show="showWarning">
|
||||
Configure email settings to send emails to customers.
|
||||
</div>
|
||||
{% endmacro %}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Location: `tests/unit/services/test_store_email_settings_service.py`
|
||||
|
||||
Tests:
|
||||
- Read operations (get_settings, get_status, is_configured)
|
||||
- Write operations (create_or_update, delete)
|
||||
- Tier validation (premium providers)
|
||||
- Verification (mock SMTP)
|
||||
- Provider availability
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Locations:
|
||||
- `tests/integration/api/v1/store/test_email_settings.py`
|
||||
- `tests/integration/api/v1/admin/test_email_settings.py`
|
||||
|
||||
Tests:
|
||||
- CRUD operations via API
|
||||
- Authentication/authorization
|
||||
- Validation errors
|
||||
- Status endpoints
|
||||
|
||||
## Files Modified/Created
|
||||
|
||||
### New Files
|
||||
- `models/database/store_email_settings.py` - Model
|
||||
- `alembic/versions/v0a1b2c3d4e5_add_store_email_settings.py` - Migration
|
||||
- `app/services/store_email_settings_service.py` - Service
|
||||
- `app/api/v1/store/email_settings.py` - API endpoints
|
||||
- `scripts/seed/install.py` - Installation wizard
|
||||
|
||||
### Modified Files
|
||||
- `app/services/email_service.py` - Added platform config, store providers
|
||||
- `app/api/v1/admin/settings.py` - Added email endpoints
|
||||
- `app/templates/admin/settings.html` - Email tab
|
||||
- `app/templates/store/settings.html` - Email tab
|
||||
- `static/admin/js/settings.js` - Email JS
|
||||
- `static/store/js/settings.js` - Email JS
|
||||
- `static/store/js/init-alpine.js` - Warning banner component
|
||||
254
app/modules/messaging/docs/email-settings.md
Normal file
254
app/modules/messaging/docs/email-settings.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# Email Settings Guide
|
||||
|
||||
This guide covers email configuration for both **stores** and **platform administrators**. The Orion platform uses a layered email system where stores manage their own email sending while the platform handles system-level communications.
|
||||
|
||||
## Overview
|
||||
|
||||
The email system has two distinct configurations:
|
||||
|
||||
| Aspect | Platform (Admin) | Store |
|
||||
|--------|-----------------|--------|
|
||||
| Purpose | System emails (billing, admin notifications) | Customer-facing emails (orders, marketing) |
|
||||
| Configuration | Environment variables (.env) + Database overrides | Database (per-store) |
|
||||
| Cost | Platform owner pays | Store pays |
|
||||
| Providers | SMTP, SendGrid, Mailgun, SES | SMTP (all tiers), Premium providers (Business+) |
|
||||
|
||||
---
|
||||
|
||||
## Store Email Settings
|
||||
|
||||
### Getting Started
|
||||
|
||||
As a store, 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 Store 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 store 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 store emails:
|
||||
|
||||
| Tier | Branding |
|
||||
|------|----------|
|
||||
| Essential | "Powered by Orion" footer |
|
||||
| Professional | "Powered by Orion" footer |
|
||||
| Business | No branding (white-label) |
|
||||
| Enterprise | No branding (white-label) |
|
||||
|
||||
Business and Enterprise tier stores get completely white-labeled emails with no Orion 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
|
||||
331
app/modules/messaging/docs/email-system.md
Normal file
331
app/modules/messaging/docs/email-system.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# Email System
|
||||
|
||||
The email system provides multi-provider support with database-stored templates and comprehensive logging for the Orion platform.
|
||||
|
||||
## Overview
|
||||
|
||||
The email system supports:
|
||||
|
||||
- **Multiple Providers**: SMTP, SendGrid, Mailgun, Amazon SES
|
||||
- **Multi-language Templates**: EN, FR, DE, LB (stored in database)
|
||||
- **Jinja2 Templating**: Variable interpolation in subjects and bodies
|
||||
- **Email Logging**: Track all sent emails for debugging and compliance
|
||||
- **Debug Mode**: Log emails instead of sending during development
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Add these settings to your `.env` file:
|
||||
|
||||
```env
|
||||
# Provider: smtp, sendgrid, mailgun, ses
|
||||
EMAIL_PROVIDER=smtp
|
||||
EMAIL_FROM_ADDRESS=noreply@orion.lu
|
||||
EMAIL_FROM_NAME=Orion
|
||||
EMAIL_REPLY_TO=
|
||||
|
||||
# Behavior
|
||||
EMAIL_ENABLED=true
|
||||
EMAIL_DEBUG=false
|
||||
|
||||
# SMTP Settings (when EMAIL_PROVIDER=smtp)
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_USE_TLS=true
|
||||
SMTP_USE_SSL=false
|
||||
|
||||
# SendGrid (when EMAIL_PROVIDER=sendgrid)
|
||||
# SENDGRID_API_KEY=SG.your_api_key_here
|
||||
|
||||
# Mailgun (when EMAIL_PROVIDER=mailgun)
|
||||
# MAILGUN_API_KEY=your_api_key_here
|
||||
# MAILGUN_DOMAIN=mg.yourdomain.com
|
||||
|
||||
# Amazon SES (when EMAIL_PROVIDER=ses)
|
||||
# AWS_ACCESS_KEY_ID=your_access_key
|
||||
# AWS_SECRET_ACCESS_KEY=your_secret_key
|
||||
# AWS_REGION=eu-west-1
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Set `EMAIL_DEBUG=true` to log emails instead of sending them. This is useful during development:
|
||||
|
||||
```env
|
||||
EMAIL_DEBUG=true
|
||||
```
|
||||
|
||||
Emails will be logged to the console with full details (recipient, subject, body preview).
|
||||
|
||||
## Database Models
|
||||
|
||||
### EmailTemplate
|
||||
|
||||
Stores multi-language email templates:
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| id | Integer | Primary key |
|
||||
| code | String(100) | Template identifier (e.g., "signup_welcome") |
|
||||
| language | String(5) | Language code (en, fr, de, lb) |
|
||||
| name | String(255) | Human-readable name |
|
||||
| description | Text | Template purpose |
|
||||
| category | String(50) | AUTH, ORDERS, BILLING, SYSTEM, MARKETING |
|
||||
| subject | String(500) | Email subject (supports Jinja2) |
|
||||
| body_html | Text | HTML body |
|
||||
| body_text | Text | Plain text fallback |
|
||||
| variables | Text | JSON list of expected variables |
|
||||
| is_active | Boolean | Enable/disable template |
|
||||
|
||||
### EmailLog
|
||||
|
||||
Tracks all sent emails:
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| id | Integer | Primary key |
|
||||
| template_code | String(100) | Template used (if any) |
|
||||
| recipient_email | String(255) | Recipient address |
|
||||
| subject | String(500) | Email subject |
|
||||
| status | String(20) | PENDING, SENT, FAILED, DELIVERED, OPENED |
|
||||
| sent_at | DateTime | When email was sent |
|
||||
| error_message | Text | Error details if failed |
|
||||
| provider | String(50) | Provider used (smtp, sendgrid, etc.) |
|
||||
| store_id | Integer | Related store (optional) |
|
||||
| user_id | Integer | Related user (optional) |
|
||||
|
||||
## Usage
|
||||
|
||||
### Using EmailService
|
||||
|
||||
```python
|
||||
from app.services.email_service import EmailService
|
||||
|
||||
def send_welcome_email(db, user, store):
|
||||
email_service = EmailService(db)
|
||||
|
||||
email_service.send_template(
|
||||
template_code="signup_welcome",
|
||||
to_email=user.email,
|
||||
to_name=f"{user.first_name} {user.last_name}",
|
||||
language="fr", # Falls back to "en" if not found
|
||||
variables={
|
||||
"first_name": user.first_name,
|
||||
"merchant_name": store.name,
|
||||
"store_code": store.store_code,
|
||||
"login_url": f"https://orion.lu/store/{store.store_code}/dashboard",
|
||||
"trial_days": 30,
|
||||
"tier_name": "Essential",
|
||||
},
|
||||
store_id=store.id,
|
||||
user_id=user.id,
|
||||
related_type="signup",
|
||||
)
|
||||
```
|
||||
|
||||
### Convenience Function
|
||||
|
||||
```python
|
||||
from app.services.email_service import send_email
|
||||
|
||||
send_email(
|
||||
db=db,
|
||||
template_code="order_confirmation",
|
||||
to_email="customer@example.com",
|
||||
language="en",
|
||||
variables={"order_number": "ORD-001"},
|
||||
)
|
||||
```
|
||||
|
||||
### Sending Raw Emails
|
||||
|
||||
For one-off emails without templates:
|
||||
|
||||
```python
|
||||
email_service = EmailService(db)
|
||||
|
||||
email_service.send_raw(
|
||||
to_email="user@example.com",
|
||||
subject="Custom Subject",
|
||||
body_html="<h1>Hello</h1><p>Custom message</p>",
|
||||
body_text="Hello\n\nCustom message",
|
||||
)
|
||||
```
|
||||
|
||||
## Email Templates
|
||||
|
||||
### Creating Templates
|
||||
|
||||
Templates use Jinja2 syntax for variable interpolation:
|
||||
|
||||
```html
|
||||
<p>Hello {{ first_name }},</p>
|
||||
<p>Welcome to {{ merchant_name }}!</p>
|
||||
```
|
||||
|
||||
### Seeding Templates
|
||||
|
||||
Run the seed script to populate default templates:
|
||||
|
||||
```bash
|
||||
python scripts/seed/seed_email_templates.py
|
||||
```
|
||||
|
||||
This creates templates for:
|
||||
|
||||
- `signup_welcome` (en, fr, de, lb)
|
||||
|
||||
### Available Variables
|
||||
|
||||
For `signup_welcome`:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| first_name | User's first name |
|
||||
| merchant_name | Store merchant name |
|
||||
| email | User's email address |
|
||||
| store_code | Store code for dashboard URL |
|
||||
| login_url | Direct link to dashboard |
|
||||
| trial_days | Number of trial days |
|
||||
| tier_name | Subscription tier name |
|
||||
|
||||
## Provider Setup
|
||||
|
||||
### SMTP
|
||||
|
||||
Standard SMTP configuration:
|
||||
|
||||
```env
|
||||
EMAIL_PROVIDER=smtp
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASSWORD=your-app-password
|
||||
SMTP_USE_TLS=true
|
||||
```
|
||||
|
||||
### SendGrid
|
||||
|
||||
1. Create account at [sendgrid.com](https://sendgrid.com)
|
||||
2. Generate API key in Settings > API Keys
|
||||
3. Configure:
|
||||
|
||||
```env
|
||||
EMAIL_PROVIDER=sendgrid
|
||||
SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
4. Install package: `pip install sendgrid`
|
||||
|
||||
### Mailgun
|
||||
|
||||
1. Create account at [mailgun.com](https://mailgun.com)
|
||||
2. Add and verify your domain
|
||||
3. Get API key from Domain Settings
|
||||
4. Configure:
|
||||
|
||||
```env
|
||||
EMAIL_PROVIDER=mailgun
|
||||
MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxx
|
||||
MAILGUN_DOMAIN=mg.yourdomain.com
|
||||
```
|
||||
|
||||
### Amazon SES
|
||||
|
||||
1. Set up SES in AWS Console
|
||||
2. Verify sender domain/email
|
||||
3. Create IAM user with SES permissions
|
||||
4. Configure:
|
||||
|
||||
```env
|
||||
EMAIL_PROVIDER=ses
|
||||
AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXXXXXX
|
||||
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
AWS_REGION=eu-west-1
|
||||
```
|
||||
|
||||
5. Install package: `pip install boto3`
|
||||
|
||||
## Email Logging
|
||||
|
||||
All emails are logged to the `email_logs` table. Query examples:
|
||||
|
||||
```python
|
||||
# Get failed emails
|
||||
failed = db.query(EmailLog).filter(
|
||||
EmailLog.status == EmailStatus.FAILED.value
|
||||
).all()
|
||||
|
||||
# Get emails for a store
|
||||
store_emails = db.query(EmailLog).filter(
|
||||
EmailLog.store_id == store_id
|
||||
).order_by(EmailLog.created_at.desc()).all()
|
||||
|
||||
# Get recent signup emails
|
||||
signups = db.query(EmailLog).filter(
|
||||
EmailLog.template_code == "signup_welcome",
|
||||
EmailLog.created_at >= datetime.now() - timedelta(days=7)
|
||||
).all()
|
||||
```
|
||||
|
||||
## Language Fallback
|
||||
|
||||
The system automatically falls back to English if a template isn't available in the requested language:
|
||||
|
||||
1. Request template for "de" (German)
|
||||
2. If not found, try "en" (English)
|
||||
3. If still not found, return None (log error)
|
||||
|
||||
## Testing
|
||||
|
||||
Run email service tests:
|
||||
|
||||
```bash
|
||||
pytest tests/unit/services/test_email_service.py -v
|
||||
```
|
||||
|
||||
Test coverage includes:
|
||||
|
||||
- Provider abstraction (Debug, SMTP, etc.)
|
||||
- Template rendering with Jinja2
|
||||
- Language fallback behavior
|
||||
- Email sending success/failure
|
||||
- EmailLog model methods
|
||||
- Template variable handling
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
app/services/email_service.py # Email service with provider abstraction
|
||||
models/database/email.py # EmailTemplate and EmailLog models
|
||||
app/core/config.py # Email configuration settings
|
||||
scripts/seed/seed_email_templates.py # Template seeding script
|
||||
```
|
||||
|
||||
### Provider Abstraction
|
||||
|
||||
The system uses a strategy pattern for email providers:
|
||||
|
||||
```
|
||||
EmailProvider (ABC)
|
||||
├── SMTPProvider
|
||||
├── SendGridProvider
|
||||
├── MailgunProvider
|
||||
├── SESProvider
|
||||
└── DebugProvider
|
||||
```
|
||||
|
||||
Each provider implements the `send()` method with the same signature, making it easy to switch providers via configuration.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned improvements:
|
||||
|
||||
1. **Email Queue**: Background task queue for high-volume sending
|
||||
2. **Webhook Tracking**: Track deliveries, opens, clicks via provider webhooks
|
||||
3. **Template Editor**: Admin UI for editing templates
|
||||
4. **A/B Testing**: Test different email versions
|
||||
5. **Scheduled Emails**: Send emails at specific times
|
||||
63
app/modules/messaging/docs/index.md
Normal file
63
app/modules/messaging/docs/index.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Messaging & Notifications
|
||||
|
||||
Core email and notification system for user registration, password resets, team invitations, and system notifications. Required for basic platform operations.
|
||||
|
||||
## Overview
|
||||
|
||||
| Aspect | Detail |
|
||||
|--------|--------|
|
||||
| Code | `messaging` |
|
||||
| Classification | Core |
|
||||
| Dependencies | None |
|
||||
| Status | Active |
|
||||
|
||||
## Features
|
||||
|
||||
- `customer_messaging` — Customer-facing email communications
|
||||
- `internal_messages` — Internal messaging system
|
||||
- `notification_center` — Admin notification center
|
||||
- `message_attachments` — File attachments for messages
|
||||
- `admin_notifications` — System notifications for admins
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Description |
|
||||
|------------|-------------|
|
||||
| `messaging.view_messages` | View messages |
|
||||
| `messaging.send_messages` | Send messages |
|
||||
| `messaging.manage_templates` | Manage email templates |
|
||||
|
||||
## Data Model
|
||||
|
||||
See [Data Model](data-model.md) for full entity relationships and schema.
|
||||
|
||||
- **EmailTemplate** — Multi-language email templates with Jinja2 variables
|
||||
- **StoreEmailTemplate** — Per-store template overrides
|
||||
- **EmailLog** — Email sending history and delivery tracking
|
||||
- **StoreEmailSettings** — Per-store email provider configuration
|
||||
- **AdminNotification** — System alerts and admin notifications
|
||||
- **Conversation** — Threaded conversations with polymorphic participants
|
||||
- **ConversationParticipant** — Participant links (admin, store, customer)
|
||||
- **Message** — Individual messages with soft delete
|
||||
- **MessageAttachment** — File attachments for messages
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `*` | `/api/v1/admin/messages/*` | Message management |
|
||||
| `*` | `/api/v1/admin/notifications/*` | Notification management |
|
||||
| `*` | `/api/v1/admin/email-templates/*` | Email template CRUD |
|
||||
|
||||
## Configuration
|
||||
|
||||
Email provider settings are configured at the store level via the settings UI.
|
||||
|
||||
## Additional Documentation
|
||||
|
||||
- [Data Model](data-model.md) — Entity relationships and database schema
|
||||
- [Architecture](architecture.md) — Messaging system architecture
|
||||
- [Notifications](notifications.md) — Admin notification system
|
||||
- [Email System](email-system.md) — Email delivery system overview
|
||||
- [Email Settings](email-settings.md) — Provider configuration guide
|
||||
- [Email Settings Implementation](email-settings-impl.md) — Email settings technical details
|
||||
187
app/modules/messaging/docs/notifications.md
Normal file
187
app/modules/messaging/docs/notifications.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Admin Notification System
|
||||
|
||||
## Overview
|
||||
|
||||
The admin notification system provides real-time alerts and notifications to platform administrators for important events, errors, and system status updates.
|
||||
|
||||
## Components
|
||||
|
||||
### Backend
|
||||
|
||||
#### Database Models
|
||||
|
||||
Located in `models/database/admin.py`:
|
||||
|
||||
- **AdminNotification**: Stores individual notifications
|
||||
- `type`: Notification type (import_failure, order_sync_failure, etc.)
|
||||
- `priority`: low, normal, high, critical
|
||||
- `title`, `message`: Content
|
||||
- `is_read`, `read_at`, `read_by_user_id`: Read tracking
|
||||
- `action_required`, `action_url`: Optional action link
|
||||
- `notification_metadata`: JSON for additional context
|
||||
|
||||
- **PlatformAlert**: Stores platform-wide alerts
|
||||
- `alert_type`: security, performance, capacity, integration, etc.
|
||||
- `severity`: info, warning, error, critical
|
||||
- `affected_stores`, `affected_systems`: Scope tracking
|
||||
- `occurrence_count`, `first_occurred_at`, `last_occurred_at`: Deduplication
|
||||
- `is_resolved`, `resolved_at`, `resolution_notes`: Resolution tracking
|
||||
|
||||
#### Service Layer
|
||||
|
||||
Located in `app/services/admin_notification_service.py`:
|
||||
|
||||
```python
|
||||
from app.services.admin_notification_service import (
|
||||
admin_notification_service,
|
||||
platform_alert_service,
|
||||
NotificationType,
|
||||
Priority,
|
||||
AlertType,
|
||||
Severity,
|
||||
)
|
||||
```
|
||||
|
||||
**AdminNotificationService** methods:
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `create_notification()` | Create a new notification |
|
||||
| `get_notifications()` | List notifications with filters |
|
||||
| `get_recent_notifications()` | Get recent unread for header dropdown |
|
||||
| `get_unread_count()` | Count unread notifications |
|
||||
| `mark_as_read()` | Mark single notification read |
|
||||
| `mark_all_as_read()` | Mark all as read |
|
||||
| `delete_notification()` | Delete a notification |
|
||||
|
||||
**Convenience methods** for common scenarios:
|
||||
|
||||
| Method | Use Case |
|
||||
|--------|----------|
|
||||
| `notify_import_failure()` | Product/order import failed |
|
||||
| `notify_order_sync_failure()` | Letzshop sync failed |
|
||||
| `notify_order_exception()` | Order has unmatched products |
|
||||
| `notify_critical_error()` | System critical error |
|
||||
| `notify_store_issue()` | Store-related problem |
|
||||
| `notify_security_alert()` | Security event detected |
|
||||
|
||||
**PlatformAlertService** methods:
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `create_alert()` | Create a new platform alert |
|
||||
| `get_alerts()` | List alerts with filters |
|
||||
| `resolve_alert()` | Mark alert as resolved |
|
||||
| `get_statistics()` | Get alert counts and stats |
|
||||
| `create_or_increment_alert()` | Deduplicate recurring alerts |
|
||||
|
||||
#### API Endpoints
|
||||
|
||||
Located in `app/api/v1/admin/notifications.py`:
|
||||
|
||||
**Notifications:**
|
||||
- `GET /api/v1/admin/notifications` - List with filters
|
||||
- `POST /api/v1/admin/notifications` - Create (manual)
|
||||
- `GET /api/v1/admin/notifications/recent` - For header dropdown
|
||||
- `GET /api/v1/admin/notifications/unread-count` - Badge count
|
||||
- `PUT /api/v1/admin/notifications/{id}/read` - Mark read
|
||||
- `PUT /api/v1/admin/notifications/mark-all-read` - Mark all read
|
||||
- `DELETE /api/v1/admin/notifications/{id}` - Delete
|
||||
|
||||
**Platform Alerts:**
|
||||
- `GET /api/v1/admin/notifications/alerts` - List with filters
|
||||
- `POST /api/v1/admin/notifications/alerts` - Create (manual)
|
||||
- `PUT /api/v1/admin/notifications/alerts/{id}/resolve` - Resolve
|
||||
- `GET /api/v1/admin/notifications/alerts/stats` - Statistics
|
||||
|
||||
### Frontend
|
||||
|
||||
#### Header Dropdown
|
||||
|
||||
Located in `app/templates/admin/partials/header.html`:
|
||||
|
||||
- Real-time notification bell with unread count badge
|
||||
- Polls for new notifications every 60 seconds
|
||||
- Quick actions: mark as read, view all
|
||||
- Priority-based color coding
|
||||
|
||||
#### Notifications Page
|
||||
|
||||
Located in `app/templates/admin/notifications.html` with `static/admin/js/notifications.js`:
|
||||
|
||||
- Full notifications management interface
|
||||
- Two tabs: Notifications and Platform Alerts
|
||||
- Statistics cards (unread, active alerts, critical, resolved today)
|
||||
- Filtering by priority, type, read status
|
||||
- Bulk operations (mark all read)
|
||||
- Alert resolution workflow
|
||||
|
||||
## Automatic Triggers
|
||||
|
||||
Notifications are automatically created in these scenarios:
|
||||
|
||||
### Import Failures
|
||||
|
||||
**Product Import** (`app/tasks/background_tasks.py`):
|
||||
- When a product import job fails completely
|
||||
- When import completes with 5+ errors
|
||||
|
||||
**Historical Order Import** (`app/tasks/letzshop_tasks.py`):
|
||||
- When Letzshop API returns an error
|
||||
- When import fails with an unexpected exception
|
||||
|
||||
### Example Usage
|
||||
|
||||
```python
|
||||
from app.services.admin_notification_service import admin_notification_service
|
||||
|
||||
# In a background task or service
|
||||
admin_notification_service.notify_import_failure(
|
||||
db=db,
|
||||
store_name="Acme Corp",
|
||||
job_id=123,
|
||||
error_message="CSV parsing failed: invalid column format",
|
||||
store_id=5,
|
||||
)
|
||||
db.commit()
|
||||
```
|
||||
|
||||
## Priority Levels
|
||||
|
||||
| Priority | When to Use | Badge Color |
|
||||
|----------|-------------|-------------|
|
||||
| `critical` | System down, data loss risk | Red |
|
||||
| `high` | Import/sync failures, action needed | Orange |
|
||||
| `normal` | Informational alerts | Blue |
|
||||
| `low` | Minor issues, suggestions | Gray |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────────┐
|
||||
│ Background │────▶│ Notification │
|
||||
│ Tasks │ │ Service │
|
||||
└─────────────────┘ └──────────┬───────────┘
|
||||
│
|
||||
┌─────────────────┐ ▼
|
||||
│ API Endpoints │◀───────────────┤
|
||||
└─────────────────┘ │
|
||||
▼
|
||||
┌─────────────────┐ ┌──────────────────────┐
|
||||
│ Header │◀────│ Database │
|
||||
│ Dropdown │ │ (admin_notifications│
|
||||
└─────────────────┘ │ platform_alerts) │
|
||||
│ └──────────────────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Notifications │
|
||||
│ Page │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Email notifications for critical alerts
|
||||
- Webhook integration for external systems
|
||||
- Customizable notification preferences per admin
|
||||
- Scheduled notification digests
|
||||
Reference in New Issue
Block a user