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:
@@ -1,414 +1 @@
|
||||
# CMS Implementation Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
This guide shows you how to implement the Content Management System for static pages.
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
✅ **Database Model**: `models/database/content_page.py`
|
||||
✅ **Service Layer**: `app/services/content_page_service.py`
|
||||
✅ **Admin API**: `app/api/v1/admin/content_pages.py`
|
||||
✅ **Store API**: `app/api/v1/store/content_pages.py`
|
||||
✅ **Storefront API**: `app/api/v1/storefront/content_pages.py`
|
||||
✅ **Documentation**: Full CMS documentation in `docs/features/content-management-system.md`
|
||||
|
||||
## Next Steps to Activate
|
||||
|
||||
### 1. Create Database Migration
|
||||
|
||||
```bash
|
||||
# Create Alembic migration
|
||||
alembic revision --autogenerate -m "Add content_pages table"
|
||||
|
||||
# Review the generated migration in alembic/versions/
|
||||
|
||||
# Run migration
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
### 2. Add Relationship to Store Model
|
||||
|
||||
Edit `models/database/store.py` and add this relationship:
|
||||
|
||||
```python
|
||||
# Add this import
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
# Add this relationship to Store class
|
||||
content_pages = relationship("ContentPage", back_populates="store", cascade="all, delete-orphan")
|
||||
```
|
||||
|
||||
### 3. Register API Routers
|
||||
|
||||
Edit the appropriate router files to include the new endpoints:
|
||||
|
||||
**Admin Router** (`app/api/v1/admin/__init__.py`):
|
||||
```python
|
||||
from app.api.v1.admin import content_pages
|
||||
|
||||
api_router.include_router(
|
||||
content_pages.router,
|
||||
prefix="/content-pages",
|
||||
tags=["admin-content-pages"]
|
||||
)
|
||||
```
|
||||
|
||||
**Store Router** (`app/api/v1/store/__init__.py`):
|
||||
```python
|
||||
from app.api.v1.store import content_pages
|
||||
|
||||
api_router.include_router(
|
||||
content_pages.router,
|
||||
prefix="/{store_code}/content-pages",
|
||||
tags=["store-content-pages"]
|
||||
)
|
||||
```
|
||||
|
||||
**Storefront Router** (`app/api/v1/storefront/__init__.py` or create if doesn't exist):
|
||||
```python
|
||||
from app.api.v1.storefront import content_pages
|
||||
|
||||
api_router.include_router(
|
||||
content_pages.router,
|
||||
prefix="/content-pages",
|
||||
tags=["storefront-content-pages"]
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Update Storefront Routes to Use CMS
|
||||
|
||||
Edit `app/routes/storefront_pages.py` to add a generic content page handler:
|
||||
|
||||
```python
|
||||
from app.services.content_page_service import content_page_service
|
||||
|
||||
@router.get("/{slug}", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def generic_content_page(
|
||||
slug: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Generic content page handler.
|
||||
Handles: /about, /faq, /contact, /shipping, /returns, /privacy, /terms, etc.
|
||||
"""
|
||||
store = getattr(request.state, 'store', None)
|
||||
store_id = store.id if store else None
|
||||
|
||||
page = content_page_service.get_page_for_store(
|
||||
db,
|
||||
slug=slug,
|
||||
store_id=store_id,
|
||||
include_unpublished=False
|
||||
)
|
||||
|
||||
if not page:
|
||||
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"storefront/content-page.html",
|
||||
get_storefront_context(request, page=page)
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Create Generic Content Page Template
|
||||
|
||||
Create `app/templates/storefront/content-page.html`:
|
||||
|
||||
```jinja2
|
||||
{# app/templates/storefront/content-page.html #}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{% block title %}{{ page.title }}{% endblock %}
|
||||
|
||||
{% block meta_description %}
|
||||
{{ page.meta_description or page.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
|
||||
{# Breadcrumbs #}
|
||||
<nav class="mb-6 text-sm">
|
||||
<a href="{{ base_url }}" class="text-primary hover:underline">Home</a>
|
||||
<span class="mx-2 text-gray-400">/</span>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ page.title }}</span>
|
||||
</nav>
|
||||
|
||||
{# Page Title #}
|
||||
<h1 class="text-4xl font-bold text-gray-900 dark:text-gray-100 mb-8">
|
||||
{{ page.title }}
|
||||
</h1>
|
||||
|
||||
{# Content #}
|
||||
<div class="prose prose-lg dark:prose-invert max-w-none">
|
||||
{{ page.content | safe }}
|
||||
</div>
|
||||
|
||||
{# Last updated #}
|
||||
{% if page.updated_at %}
|
||||
<div class="mt-12 pt-6 border-t border-gray-200 dark:border-gray-700 text-sm text-gray-500 dark:text-gray-400">
|
||||
Last updated: {{ page.updated_at.strftime('%B %d, %Y') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### 6. Update Footer to Load Navigation Dynamically
|
||||
|
||||
Edit `app/templates/storefront/base.html` to load navigation from database.
|
||||
|
||||
First, update the context helper to include footer pages:
|
||||
|
||||
```python
|
||||
# app/routes/storefront_pages.py
|
||||
|
||||
def get_storefront_context(request: Request, **extra_context) -> dict:
|
||||
# ... existing code ...
|
||||
|
||||
# Load footer navigation pages
|
||||
db = next(get_db())
|
||||
try:
|
||||
footer_pages = content_page_service.list_pages_for_store(
|
||||
db,
|
||||
store_id=store.id if store else None,
|
||||
include_unpublished=False,
|
||||
footer_only=True
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"store": store,
|
||||
"theme": theme,
|
||||
"clean_path": clean_path,
|
||||
"access_method": access_method,
|
||||
"base_url": base_url,
|
||||
"footer_pages": footer_pages, # Add this
|
||||
**extra_context
|
||||
}
|
||||
|
||||
return context
|
||||
```
|
||||
|
||||
Then update the footer template:
|
||||
|
||||
```jinja2
|
||||
{# app/templates/storefront/base.html - Footer section #}
|
||||
|
||||
<div>
|
||||
<h4 class="font-semibold mb-4">Quick Links</h4>
|
||||
<ul class="space-y-2">
|
||||
{% for page in footer_pages %}
|
||||
<li>
|
||||
<a href="{{ base_url }}{{ page.slug }}"
|
||||
class="text-gray-600 hover:text-primary dark:text-gray-400">
|
||||
{{ page.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 7. Create Default Platform Pages (Script)
|
||||
|
||||
Create `scripts/seed/create_default_content_pages.py`:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Create default platform content pages."""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import SessionLocal
|
||||
from app.services.content_page_service import content_page_service
|
||||
|
||||
def create_defaults():
|
||||
db: Session = SessionLocal()
|
||||
|
||||
try:
|
||||
# About Us
|
||||
content_page_service.create_page(
|
||||
db,
|
||||
slug="about",
|
||||
title="About Us",
|
||||
content="""
|
||||
<h2>Welcome to Our Marketplace</h2>
|
||||
<p>We connect quality stores with customers worldwide.</p>
|
||||
<p>Our mission is to provide a seamless shopping experience...</p>
|
||||
""",
|
||||
is_published=True,
|
||||
show_in_footer=True,
|
||||
display_order=1
|
||||
)
|
||||
|
||||
# Shipping Information
|
||||
content_page_service.create_page(
|
||||
db,
|
||||
slug="shipping",
|
||||
title="Shipping Information",
|
||||
content="""
|
||||
<h2>Shipping Policy</h2>
|
||||
<p>We offer fast and reliable shipping...</p>
|
||||
""",
|
||||
is_published=True,
|
||||
show_in_footer=True,
|
||||
display_order=2
|
||||
)
|
||||
|
||||
# Returns
|
||||
content_page_service.create_page(
|
||||
db,
|
||||
slug="returns",
|
||||
title="Returns & Refunds",
|
||||
content="""
|
||||
<h2>Return Policy</h2>
|
||||
<p>30-day return policy on all items...</p>
|
||||
""",
|
||||
is_published=True,
|
||||
show_in_footer=True,
|
||||
display_order=3
|
||||
)
|
||||
|
||||
# Privacy Policy
|
||||
content_page_service.create_page(
|
||||
db,
|
||||
slug="privacy",
|
||||
title="Privacy Policy",
|
||||
content="""
|
||||
<h2>Privacy Policy</h2>
|
||||
<p>Your privacy is important to us...</p>
|
||||
""",
|
||||
is_published=True,
|
||||
show_in_footer=True,
|
||||
display_order=4
|
||||
)
|
||||
|
||||
# Terms of Service
|
||||
content_page_service.create_page(
|
||||
db,
|
||||
slug="terms",
|
||||
title="Terms of Service",
|
||||
content="""
|
||||
<h2>Terms of Service</h2>
|
||||
<p>By using our platform, you agree to...</p>
|
||||
""",
|
||||
is_published=True,
|
||||
show_in_footer=True,
|
||||
display_order=5
|
||||
)
|
||||
|
||||
# Contact
|
||||
content_page_service.create_page(
|
||||
db,
|
||||
slug="contact",
|
||||
title="Contact Us",
|
||||
content="""
|
||||
<h2>Get in Touch</h2>
|
||||
<p>Have questions? We'd love to hear from you!</p>
|
||||
<p>Email: support@example.com</p>
|
||||
""",
|
||||
is_published=True,
|
||||
show_in_footer=True,
|
||||
display_order=6
|
||||
)
|
||||
|
||||
# FAQ
|
||||
content_page_service.create_page(
|
||||
db,
|
||||
slug="faq",
|
||||
title="Frequently Asked Questions",
|
||||
content="""
|
||||
<h2>FAQ</h2>
|
||||
<h3>How do I place an order?</h3>
|
||||
<p>Simply browse our products...</p>
|
||||
""",
|
||||
is_published=True,
|
||||
show_in_footer=True,
|
||||
display_order=7
|
||||
)
|
||||
|
||||
print("✅ Created default content pages successfully")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_defaults()
|
||||
```
|
||||
|
||||
Run it:
|
||||
```bash
|
||||
python scripts/seed/create_default_content_pages.py
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### 1. Test Platform Defaults
|
||||
|
||||
```bash
|
||||
# Create platform default
|
||||
curl -X POST http://localhost:8000/api/v1/admin/content-pages/platform \
|
||||
-H "Authorization: Bearer <admin_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"slug": "about",
|
||||
"title": "About Our Marketplace",
|
||||
"content": "<h1>About</h1><p>Platform default content</p>",
|
||||
"is_published": true,
|
||||
"show_in_footer": true
|
||||
}'
|
||||
|
||||
# View in storefront
|
||||
curl http://localhost:8000/store/orion/about
|
||||
```
|
||||
|
||||
### 2. Test Store Override
|
||||
|
||||
```bash
|
||||
# Create store override
|
||||
curl -X POST http://localhost:8000/api/v1/store/orion/content-pages/ \
|
||||
-H "Authorization: Bearer <store_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"slug": "about",
|
||||
"title": "About Orion",
|
||||
"content": "<h1>About Orion</h1><p>Custom store content</p>",
|
||||
"is_published": true
|
||||
}'
|
||||
|
||||
# View in storefront (should show store content)
|
||||
curl http://localhost:8000/store/orion/about
|
||||
```
|
||||
|
||||
### 3. Test Fallback
|
||||
|
||||
```bash
|
||||
# Delete store override
|
||||
curl -X DELETE http://localhost:8000/api/v1/store/orion/content-pages/{id} \
|
||||
-H "Authorization: Bearer <store_token>"
|
||||
|
||||
# View in storefront (should fall back to platform default)
|
||||
curl http://localhost:8000/store/orion/about
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
You now have a complete CMS system that allows:
|
||||
|
||||
1. **Platform admins** to create default content for all stores
|
||||
2. **Stores** to override specific pages with custom content
|
||||
3. **Automatic fallback** to platform defaults when store hasn't customized
|
||||
4. **Dynamic navigation** loading from database
|
||||
5. **SEO optimization** with meta tags
|
||||
6. **Draft/Published workflow** for content management
|
||||
|
||||
All pages are accessible via their slug: `/about`, `/faq`, `/contact`, etc. with proper store context and routing support!
|
||||
This document has moved to the CMS module docs: [Implementation](../modules/cms/implementation.md)
|
||||
|
||||
@@ -1,604 +1 @@
|
||||
# Content Management System (CMS)
|
||||
|
||||
## Overview
|
||||
|
||||
The Content Management System allows platform administrators and stores to manage static content pages like About, FAQ, Contact, Shipping, Returns, Privacy Policy, Terms of Service, etc.
|
||||
|
||||
**Key Features:**
|
||||
- ✅ Platform-level default content
|
||||
- ✅ Store-specific overrides
|
||||
- ✅ Fallback system (store → platform default)
|
||||
- ✅ Rich text content (HTML/Markdown)
|
||||
- ✅ SEO metadata
|
||||
- ✅ Published/Draft status
|
||||
- ✅ Navigation management (footer/header)
|
||||
- ✅ Display order control
|
||||
|
||||
## Architecture
|
||||
|
||||
### Two-Tier Content System
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CONTENT LOOKUP FLOW │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Request: /about
|
||||
|
||||
1. Check for store-specific override
|
||||
↓
|
||||
SELECT * FROM content_pages
|
||||
WHERE store_id = 123 AND slug = 'about' AND is_published = true
|
||||
↓
|
||||
Found? ✅ Return store content
|
||||
❌ Continue to step 2
|
||||
|
||||
2. Check for platform default
|
||||
↓
|
||||
SELECT * FROM content_pages
|
||||
WHERE store_id IS NULL AND slug = 'about' AND is_published = true
|
||||
↓
|
||||
Found? ✅ Return platform content
|
||||
❌ Return 404 or default template
|
||||
```
|
||||
|
||||
### Database Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE content_pages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
-- Store association (NULL = platform default)
|
||||
store_id INTEGER REFERENCES stores(id) ON DELETE CASCADE,
|
||||
|
||||
-- Page identification
|
||||
slug VARCHAR(100) NOT NULL, -- about, faq, contact, shipping, returns
|
||||
title VARCHAR(200) NOT NULL,
|
||||
|
||||
-- Content
|
||||
content TEXT NOT NULL, -- HTML or Markdown
|
||||
content_format VARCHAR(20) DEFAULT 'html', -- html, markdown
|
||||
|
||||
-- SEO
|
||||
meta_description VARCHAR(300),
|
||||
meta_keywords VARCHAR(300),
|
||||
|
||||
-- Publishing
|
||||
is_published BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
published_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- Navigation placement
|
||||
display_order INTEGER DEFAULT 0,
|
||||
show_in_footer BOOLEAN DEFAULT TRUE, -- Quick Links column
|
||||
show_in_header BOOLEAN DEFAULT FALSE, -- Top navigation
|
||||
show_in_legal BOOLEAN DEFAULT FALSE, -- Bottom bar with copyright
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
|
||||
-- Author tracking
|
||||
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
updated_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT uq_store_slug UNIQUE (store_id, slug)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_store_published ON content_pages (store_id, is_published);
|
||||
CREATE INDEX idx_slug_published ON content_pages (slug, is_published);
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Platform Administrator Workflow
|
||||
|
||||
**1. Create Platform Default Pages**
|
||||
|
||||
Platform admins create default content that all stores inherit:
|
||||
|
||||
```bash
|
||||
POST /api/v1/admin/content-pages/platform
|
||||
{
|
||||
"slug": "about",
|
||||
"title": "About Us",
|
||||
"content": "<h1>About Us</h1><p>We are a marketplace...</p>",
|
||||
"content_format": "html",
|
||||
"meta_description": "Learn more about our marketplace",
|
||||
"is_published": true,
|
||||
"show_in_header": true,
|
||||
"show_in_footer": true,
|
||||
"show_in_legal": false,
|
||||
"display_order": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Common Platform Defaults:**
|
||||
- `about` - About Us
|
||||
- `contact` - Contact Us
|
||||
- `faq` - Frequently Asked Questions
|
||||
- `shipping` - Shipping Information
|
||||
- `returns` - Return Policy
|
||||
- `privacy` - Privacy Policy
|
||||
- `terms` - Terms of Service
|
||||
- `help` - Help Center
|
||||
|
||||
**2. View All Content Pages**
|
||||
|
||||
```bash
|
||||
GET /api/v1/admin/content-pages/
|
||||
GET /api/v1/admin/content-pages/?store_id=123 # Filter by store
|
||||
GET /api/v1/admin/content-pages/platform # Only platform defaults
|
||||
```
|
||||
|
||||
**3. Update Platform Default**
|
||||
|
||||
```bash
|
||||
PUT /api/v1/admin/content-pages/1
|
||||
{
|
||||
"title": "Updated About Us",
|
||||
"content": "<h1>About Our Platform</h1>...",
|
||||
"is_published": true
|
||||
}
|
||||
```
|
||||
|
||||
### Store Workflow
|
||||
|
||||
**1. View Available Pages**
|
||||
|
||||
Stores see their overrides + platform defaults:
|
||||
|
||||
```bash
|
||||
GET /api/v1/store/{code}/content-pages/
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 15,
|
||||
"slug": "about",
|
||||
"title": "About Orion", // Store override
|
||||
"is_store_override": true,
|
||||
"is_platform_page": false
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"slug": "shipping",
|
||||
"title": "Shipping Information", // Platform default
|
||||
"is_store_override": false,
|
||||
"is_platform_page": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**2. Create Store Override**
|
||||
|
||||
Store creates custom "About" page:
|
||||
|
||||
```bash
|
||||
POST /api/v1/store/{code}/content-pages/
|
||||
{
|
||||
"slug": "about",
|
||||
"title": "About Orion",
|
||||
"content": "<h1>About Orion</h1><p>We specialize in...</p>",
|
||||
"is_published": true
|
||||
}
|
||||
```
|
||||
|
||||
This overrides the platform default for this store only.
|
||||
|
||||
**3. View Only Store Overrides**
|
||||
|
||||
```bash
|
||||
GET /api/v1/store/{code}/content-pages/overrides
|
||||
```
|
||||
|
||||
Shows what the store has customized (excludes platform defaults).
|
||||
|
||||
**4. Delete Override (Revert to Platform Default)**
|
||||
|
||||
```bash
|
||||
DELETE /api/v1/store/{code}/content-pages/15
|
||||
```
|
||||
|
||||
After deletion, platform default will be shown again.
|
||||
|
||||
### Storefront (Public)
|
||||
|
||||
**1. Get Page Content**
|
||||
|
||||
```bash
|
||||
GET /api/v1/storefront/content-pages/about
|
||||
```
|
||||
|
||||
Automatically uses store context from middleware:
|
||||
- Returns store override if exists
|
||||
- Falls back to platform default
|
||||
- Returns 404 if neither exists
|
||||
|
||||
**2. Get Navigation Links**
|
||||
|
||||
```bash
|
||||
# Get all navigation pages
|
||||
GET /api/v1/storefront/content-pages/navigation
|
||||
|
||||
# Filter by placement
|
||||
GET /api/v1/storefront/content-pages/navigation?header_only=true
|
||||
GET /api/v1/storefront/content-pages/navigation?footer_only=true
|
||||
GET /api/v1/storefront/content-pages/navigation?legal_only=true
|
||||
```
|
||||
|
||||
Returns published pages filtered by navigation placement.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── models/database/
|
||||
│ └── content_page.py ← Database model
|
||||
│
|
||||
├── services/
|
||||
│ └── content_page_service.py ← Business logic
|
||||
│
|
||||
├── api/v1/
|
||||
│ ├── admin/
|
||||
│ │ └── content_pages.py ← Admin API endpoints
|
||||
│ ├── store/
|
||||
│ │ └── content_pages.py ← Store API endpoints
|
||||
│ └── storefront/
|
||||
│ └── content_pages.py ← Public API endpoints
|
||||
│
|
||||
└── templates/storefront/
|
||||
├── about.html ← Content page template
|
||||
├── faq.html
|
||||
├── contact.html
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Template Integration
|
||||
|
||||
### Generic Content Page Template
|
||||
|
||||
Create a reusable template for all content pages:
|
||||
|
||||
```jinja2
|
||||
{# app/templates/storefront/content-page.html #}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{% block title %}{{ page.title }}{% endblock %}
|
||||
|
||||
{% block meta_description %}
|
||||
{{ page.meta_description or page.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
|
||||
{# Breadcrumbs #}
|
||||
<nav class="mb-6">
|
||||
<a href="{{ base_url }}" class="text-primary hover:underline">Home</a>
|
||||
<span class="mx-2">/</span>
|
||||
<span class="text-gray-600">{{ page.title }}</span>
|
||||
</nav>
|
||||
|
||||
{# Page Title #}
|
||||
<h1 class="text-4xl font-bold text-gray-900 dark:text-gray-100 mb-8">
|
||||
{{ page.title }}
|
||||
</h1>
|
||||
|
||||
{# Content #}
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
{% if page.content_format == 'markdown' %}
|
||||
{{ page.content | markdown }}
|
||||
{% else %}
|
||||
{{ page.content | safe }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Last updated #}
|
||||
{% if page.updated_at %}
|
||||
<div class="mt-12 pt-6 border-t text-sm text-gray-500">
|
||||
Last updated: {{ page.updated_at }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Route Handler
|
||||
|
||||
```python
|
||||
# app/routes/storefront_pages.py
|
||||
|
||||
from app.services.content_page_service import content_page_service
|
||||
|
||||
@router.get("/{slug}", response_class=HTMLResponse)
|
||||
async def content_page(
|
||||
slug: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Generic content page handler.
|
||||
|
||||
Loads content from database with store override support.
|
||||
"""
|
||||
store = getattr(request.state, 'store', None)
|
||||
store_id = store.id if store else None
|
||||
|
||||
page = content_page_service.get_page_for_store(
|
||||
db,
|
||||
slug=slug,
|
||||
store_id=store_id,
|
||||
include_unpublished=False
|
||||
)
|
||||
|
||||
if not page:
|
||||
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"storefront/content-page.html",
|
||||
get_storefront_context(request, page=page)
|
||||
)
|
||||
```
|
||||
|
||||
### Dynamic Footer Navigation
|
||||
|
||||
Update footer to load links from database:
|
||||
|
||||
```jinja2
|
||||
{# app/templates/storefront/base.html #}
|
||||
|
||||
<footer>
|
||||
<div class="grid grid-cols-3">
|
||||
|
||||
<div>
|
||||
<h4>Quick Links</h4>
|
||||
<ul>
|
||||
{% for page in footer_pages %}
|
||||
<li>
|
||||
<a href="{{ base_url }}{{ page.slug }}">
|
||||
{{ page.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Content Formatting
|
||||
|
||||
**HTML Content:**
|
||||
```html
|
||||
<h1>About Us</h1>
|
||||
<p>We are a <strong>leading marketplace</strong> for...</p>
|
||||
<ul>
|
||||
<li>Quality products</li>
|
||||
<li>Fast shipping</li>
|
||||
<li>Great support</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
**Markdown Content:**
|
||||
```markdown
|
||||
# About Us
|
||||
|
||||
We are a **leading marketplace** for...
|
||||
|
||||
- Quality products
|
||||
- Fast shipping
|
||||
- Great support
|
||||
```
|
||||
|
||||
### 2. SEO Optimization
|
||||
|
||||
Always provide meta descriptions:
|
||||
|
||||
```json
|
||||
{
|
||||
"meta_description": "Learn about our marketplace, mission, and values. We connect stores with customers worldwide.",
|
||||
"meta_keywords": "about us, marketplace, e-commerce, mission"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Draft → Published Workflow
|
||||
|
||||
1. Create page with `is_published: false`
|
||||
2. Preview using `include_unpublished=true` parameter
|
||||
3. Review and edit
|
||||
4. Publish with `is_published: true`
|
||||
|
||||
### 4. Navigation Management
|
||||
|
||||
The CMS supports three navigation placement categories:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ HEADER (show_in_header=true) │
|
||||
│ [Logo] About Us Contact [Login] [Sign Up] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ PAGE CONTENT │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ FOOTER (show_in_footer=true) │
|
||||
│ ┌──────────────┬──────────────┬────────────┬──────────────┐ │
|
||||
│ │ Quick Links │ Platform │ Contact │ Social │ │
|
||||
│ │ • About │ • Admin │ • Email │ • Twitter │ │
|
||||
│ │ • FAQ │ • Store │ • Phone │ • LinkedIn │ │
|
||||
│ │ • Contact │ │ │ │ │
|
||||
│ │ • Shipping │ │ │ │ │
|
||||
│ └──────────────┴──────────────┴────────────┴──────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ LEGAL BAR (show_in_legal=true) │
|
||||
│ © 2025 Orion Privacy Policy │ Terms │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Navigation Categories:**
|
||||
|
||||
| Category | Field | Location | Typical Pages |
|
||||
|----------|-------|----------|---------------|
|
||||
| Header | `show_in_header` | Top navigation bar | About, Contact |
|
||||
| Footer | `show_in_footer` | Quick Links column | FAQ, Shipping, Returns |
|
||||
| Legal | `show_in_legal` | Bottom bar with © | Privacy, Terms |
|
||||
|
||||
**Use `display_order` to control link ordering within each category:**
|
||||
|
||||
```python
|
||||
# Platform defaults with navigation placement
|
||||
"about": display_order=1, show_in_header=True, show_in_footer=True
|
||||
"contact": display_order=2, show_in_header=True, show_in_footer=True
|
||||
"faq": display_order=3, show_in_footer=True
|
||||
"shipping": display_order=4, show_in_footer=True
|
||||
"returns": display_order=5, show_in_footer=True
|
||||
"privacy": display_order=6, show_in_legal=True
|
||||
"terms": display_order=7, show_in_legal=True
|
||||
```
|
||||
|
||||
### 5. Content Reversion
|
||||
|
||||
To revert store override back to platform default:
|
||||
|
||||
```bash
|
||||
# Store deletes their custom page
|
||||
DELETE /api/v1/store/{code}/content-pages/15
|
||||
|
||||
# Platform default will now be shown automatically
|
||||
```
|
||||
|
||||
## Common Page Slugs
|
||||
|
||||
Standard slugs to implement:
|
||||
|
||||
| Slug | Title | Header | Footer | Legal | Order |
|
||||
|------|-------|--------|--------|-------|-------|
|
||||
| `about` | About Us | ✅ | ✅ | ❌ | 1 |
|
||||
| `contact` | Contact Us | ✅ | ✅ | ❌ | 2 |
|
||||
| `faq` | FAQ | ❌ | ✅ | ❌ | 3 |
|
||||
| `shipping` | Shipping Info | ❌ | ✅ | ❌ | 4 |
|
||||
| `returns` | Returns | ❌ | ✅ | ❌ | 5 |
|
||||
| `privacy` | Privacy Policy | ❌ | ❌ | ✅ | 6 |
|
||||
| `terms` | Terms of Service | ❌ | ❌ | ✅ | 7 |
|
||||
| `help` | Help Center | ❌ | ✅ | ❌ | 8 |
|
||||
| `size-guide` | Size Guide | ❌ | ❌ | ❌ | - |
|
||||
| `careers` | Careers | ❌ | ❌ | ❌ | - |
|
||||
| `cookies` | Cookie Policy | ❌ | ❌ | ✅ | 8 |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **HTML Sanitization**: If using HTML format, sanitize user input to prevent XSS
|
||||
2. **Authorization**: Stores can only edit their own pages
|
||||
3. **Published Status**: Only published pages visible to public
|
||||
4. **Store Isolation**: Stores cannot see/edit other store's content
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. **Create Platform Defaults**:
|
||||
```bash
|
||||
python scripts/seed/create_default_content_pages.py
|
||||
```
|
||||
|
||||
2. **Migrate Existing Static Templates**:
|
||||
- Convert existing HTML templates to database content
|
||||
- Preserve existing URLs and SEO
|
||||
|
||||
3. **Update Routes**:
|
||||
- Add generic content page route handler
|
||||
- Remove individual route handlers for each page
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Possible improvements:
|
||||
|
||||
- **Version History**: Track content changes over time
|
||||
- **Rich Text Editor**: WYSIWYG editor in admin/store panel
|
||||
- **Image Management**: Upload and insert images
|
||||
- **Templates**: Pre-built page templates for common pages
|
||||
- **Localization**: Multi-language content support
|
||||
- **Scheduled Publishing**: Publish pages at specific times
|
||||
- **Content Approval**: Admin review before store pages go live
|
||||
|
||||
## API Reference Summary
|
||||
|
||||
### Admin Endpoints
|
||||
|
||||
```
|
||||
GET /api/v1/admin/content-pages/ # List all pages
|
||||
GET /api/v1/admin/content-pages/platform # List platform defaults
|
||||
POST /api/v1/admin/content-pages/platform # Create platform default
|
||||
GET /api/v1/admin/content-pages/{id} # Get specific page
|
||||
PUT /api/v1/admin/content-pages/{id} # Update page
|
||||
DELETE /api/v1/admin/content-pages/{id} # Delete page
|
||||
```
|
||||
|
||||
### Store Endpoints
|
||||
|
||||
```
|
||||
GET /api/v1/store/{code}/content-pages/ # List all (store + platform)
|
||||
GET /api/v1/store/{code}/content-pages/overrides # List store overrides only
|
||||
GET /api/v1/store/{code}/content-pages/{slug} # Get specific page
|
||||
POST /api/v1/store/{code}/content-pages/ # Create store override
|
||||
PUT /api/v1/store/{code}/content-pages/{id} # Update store page
|
||||
DELETE /api/v1/store/{code}/content-pages/{id} # Delete store page
|
||||
```
|
||||
|
||||
### Storefront (Public) Endpoints
|
||||
|
||||
```
|
||||
GET /api/v1/storefront/content-pages/navigation # Get navigation links
|
||||
GET /api/v1/storefront/content-pages/{slug} # Get page content
|
||||
```
|
||||
|
||||
## Example: Complete Workflow
|
||||
|
||||
**1. Platform Admin Creates Defaults:**
|
||||
```bash
|
||||
# Create "About" page
|
||||
curl -X POST /api/v1/admin/content-pages/platform \
|
||||
-H "Authorization: Bearer <admin_token>" \
|
||||
-d '{
|
||||
"slug": "about",
|
||||
"title": "About Our Marketplace",
|
||||
"content": "<h1>About</h1><p>Default content...</p>",
|
||||
"is_published": true
|
||||
}'
|
||||
```
|
||||
|
||||
**2. All Stores See Platform Default:**
|
||||
- Store A visits: `store-a.com/about` → Shows platform default
|
||||
- Store B visits: `store-b.com/about` → Shows platform default
|
||||
|
||||
**3. Store A Creates Override:**
|
||||
```bash
|
||||
curl -X POST /api/v1/store/store-a/content-pages/ \
|
||||
-H "Authorization: Bearer <store_token>" \
|
||||
-d '{
|
||||
"slug": "about",
|
||||
"title": "About Store A",
|
||||
"content": "<h1>About Store A</h1><p>Custom content...</p>",
|
||||
"is_published": true
|
||||
}'
|
||||
```
|
||||
|
||||
**4. Now:**
|
||||
- Store A visits: `store-a.com/about` → Shows Store A custom content
|
||||
- Store B visits: `store-b.com/about` → Still shows platform default
|
||||
|
||||
**5. Store A Reverts to Default:**
|
||||
```bash
|
||||
curl -X DELETE /api/v1/store/store-a/content-pages/15 \
|
||||
-H "Authorization: Bearer <store_token>"
|
||||
```
|
||||
|
||||
**6. Result:**
|
||||
- Store A visits: `store-a.com/about` → Shows platform default again
|
||||
This document has moved to the CMS module docs: [CMS Architecture](../modules/cms/architecture.md)
|
||||
|
||||
@@ -1,331 +1 @@
|
||||
# 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
|
||||
This document has moved to the messaging module docs: [Email System](../modules/messaging/email-system.md)
|
||||
|
||||
@@ -1,191 +1,3 @@
|
||||
# Store Onboarding System
|
||||
|
||||
The store onboarding system is a mandatory 4-step wizard that guides new stores through the initial setup process after signup. Dashboard access is blocked until onboarding is completed.
|
||||
|
||||
## Overview
|
||||
|
||||
The onboarding wizard consists of four sequential steps:
|
||||
|
||||
1. **Merchant Profile Setup** - Basic merchant and contact information
|
||||
2. **Letzshop API Configuration** - Connect to Letzshop marketplace
|
||||
3. **Product & Order Import Configuration** - Set up CSV feed URLs
|
||||
4. **Order Sync** - Import historical orders with progress tracking
|
||||
|
||||
## User Flow
|
||||
|
||||
```
|
||||
Signup Complete
|
||||
↓
|
||||
Redirect to /store/{code}/onboarding
|
||||
↓
|
||||
Step 1: Merchant Profile
|
||||
↓
|
||||
Step 2: Letzshop API (with connection test)
|
||||
↓
|
||||
Step 3: Product Import Config
|
||||
↓
|
||||
Step 4: Order Sync (with progress bar)
|
||||
↓
|
||||
Onboarding Complete
|
||||
↓
|
||||
Redirect to Dashboard
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### Mandatory Completion
|
||||
- Dashboard and other protected routes redirect to onboarding if not completed
|
||||
- Admin can skip onboarding for support cases (via admin API)
|
||||
|
||||
### Step Validation
|
||||
- Steps must be completed in order (no skipping ahead)
|
||||
- Each step validates required fields before proceeding
|
||||
|
||||
### Progress Persistence
|
||||
- Onboarding progress is saved in the database
|
||||
- Users can resume from where they left off
|
||||
- Page reload doesn't lose progress
|
||||
|
||||
### Connection Testing
|
||||
- Step 2 includes real-time Letzshop API connection testing
|
||||
- Shows success/failure status before saving credentials
|
||||
|
||||
### Historical Import
|
||||
- Step 4 triggers a background job for order import
|
||||
- Real-time progress bar with polling (2-second intervals)
|
||||
- Shows order count as import progresses
|
||||
|
||||
## Database Model
|
||||
|
||||
### StoreOnboarding Table
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| id | Integer | Primary key |
|
||||
| store_id | Integer | Foreign key to stores (unique) |
|
||||
| status | String(20) | not_started, in_progress, completed, skipped |
|
||||
| current_step | String(30) | Current step identifier |
|
||||
| step_*_completed | Boolean | Completion flag per step |
|
||||
| step_*_completed_at | DateTime | Completion timestamp per step |
|
||||
| skipped_by_admin | Boolean | Admin override flag |
|
||||
| skipped_reason | Text | Reason for skip (admin) |
|
||||
|
||||
### Onboarding Steps Enum
|
||||
|
||||
```python
|
||||
class OnboardingStep(str, enum.Enum):
|
||||
MERCHANT_PROFILE = "merchant_profile"
|
||||
LETZSHOP_API = "letzshop_api"
|
||||
PRODUCT_IMPORT = "product_import"
|
||||
ORDER_SYNC = "order_sync"
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
All endpoints are under `/api/v1/store/onboarding/`:
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/status` | Get full onboarding status |
|
||||
| GET | `/step/merchant-profile` | Get merchant profile data |
|
||||
| POST | `/step/merchant-profile` | Save merchant profile |
|
||||
| POST | `/step/letzshop-api/test` | Test API connection |
|
||||
| POST | `/step/letzshop-api` | Save API credentials |
|
||||
| GET | `/step/product-import` | Get import config |
|
||||
| POST | `/step/product-import` | Save import config |
|
||||
| POST | `/step/order-sync/trigger` | Start historical import |
|
||||
| GET | `/step/order-sync/progress/{job_id}` | Get import progress |
|
||||
| POST | `/step/order-sync/complete` | Complete onboarding |
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Signup Flow
|
||||
|
||||
When a store is created during signup, an onboarding record is automatically created:
|
||||
|
||||
```python
|
||||
# In platform_signup_service.py
|
||||
onboarding_service = OnboardingService(db)
|
||||
onboarding_service.create_onboarding(store.id)
|
||||
```
|
||||
|
||||
### Route Protection
|
||||
|
||||
Protected routes check onboarding status and redirect if not completed:
|
||||
|
||||
```python
|
||||
# In store_pages.py
|
||||
onboarding_service = OnboardingService(db)
|
||||
if not onboarding_service.is_completed(current_user.token_store_id):
|
||||
return RedirectResponse(f"/store/{store_code}/onboarding")
|
||||
```
|
||||
|
||||
### Historical Import
|
||||
|
||||
Step 4 uses the existing `LetzshopHistoricalImportJob` infrastructure:
|
||||
|
||||
```python
|
||||
order_service = LetzshopOrderService(db)
|
||||
job = order_service.create_historical_import_job(store_id, user_id)
|
||||
```
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### Template
|
||||
|
||||
`app/templates/store/onboarding.html`:
|
||||
- Standalone page (doesn't use store base template)
|
||||
- Progress indicator with step circles
|
||||
- Animated transitions between steps
|
||||
- Real-time sync progress bar
|
||||
|
||||
### JavaScript
|
||||
|
||||
`static/store/js/onboarding.js`:
|
||||
- Alpine.js component
|
||||
- API calls for each step
|
||||
- Connection test functionality
|
||||
- Progress polling for order sync
|
||||
|
||||
## Admin Skip Capability
|
||||
|
||||
For support cases, admins can skip onboarding:
|
||||
|
||||
```python
|
||||
onboarding_service.skip_onboarding(
|
||||
store_id=store_id,
|
||||
admin_user_id=admin_user_id,
|
||||
reason="Manual setup required for migration"
|
||||
)
|
||||
```
|
||||
|
||||
This sets `skipped_by_admin=True` and allows dashboard access without completing all steps.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `models/database/onboarding.py` | Database model and enums |
|
||||
| `models/schema/onboarding.py` | Pydantic schemas |
|
||||
| `app/services/onboarding_service.py` | Business logic |
|
||||
| `app/api/v1/store/onboarding.py` | API endpoints |
|
||||
| `app/routes/store_pages.py` | Page routes and redirects |
|
||||
| `app/templates/store/onboarding.html` | Frontend template |
|
||||
| `static/store/js/onboarding.js` | Alpine.js component |
|
||||
| `alembic/versions/m1b2c3d4e5f6_add_store_onboarding_table.py` | Migration |
|
||||
|
||||
## Testing
|
||||
|
||||
Run the onboarding tests:
|
||||
|
||||
```bash
|
||||
pytest tests/integration/api/v1/store/test_onboarding.py -v
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
No additional configuration is required. The onboarding system uses existing configurations:
|
||||
|
||||
- Letzshop API: Uses `LetzshopCredentialsService`
|
||||
- Order Import: Uses `LetzshopOrderService`
|
||||
- Email: Uses `EmailService` for welcome email (sent after signup)
|
||||
This document has moved to the tenancy module docs: [Store Onboarding](../modules/tenancy/onboarding.md)
|
||||
|
||||
@@ -1,631 +1,3 @@
|
||||
# Subscription & Billing System
|
||||
|
||||
The platform provides a comprehensive subscription and billing system for managing merchant subscriptions, feature-based usage limits, and payments through Stripe.
|
||||
|
||||
## Overview
|
||||
|
||||
The billing system enables:
|
||||
|
||||
- **Subscription Tiers**: Database-driven tier definitions with configurable feature limits
|
||||
- **Feature Provider Pattern**: Modules declare features and usage via `FeatureProviderProtocol`, aggregated by `FeatureAggregatorService`
|
||||
- **Dynamic Usage Tracking**: Quantitative features (orders, products, team members) tracked per merchant with dynamic limits from `TierFeatureLimit`
|
||||
- **Binary Feature Gating**: Toggle-based features (analytics, API access, white-label) controlled per tier
|
||||
- **Merchant-Level Billing**: Subscriptions are per merchant+platform, not per store
|
||||
- **Stripe Integration**: Checkout sessions, customer portal, and webhook handling
|
||||
- **Add-ons**: Optional purchasable items (domains, SSL, email packages)
|
||||
- **Capacity Forecasting**: Growth trends and scaling recommendations
|
||||
- **Background Jobs**: Automated subscription lifecycle management
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Concepts
|
||||
|
||||
The billing system uses a **feature provider pattern** where:
|
||||
|
||||
1. **`TierFeatureLimit`** replaces hardcoded tier columns (`orders_per_month`, `products_limit`, `team_members`). Each feature limit is a row linking a tier to a feature code with a `limit_value`.
|
||||
2. **`MerchantFeatureOverride`** provides per-merchant exceptions to tier defaults.
|
||||
3. **Module feature providers** implement `FeatureProviderProtocol` to supply current usage data.
|
||||
4. **`FeatureAggregatorService`** collects usage from all providers and combines it with tier limits to produce `FeatureSummary` records.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Frontend Page Request │
|
||||
│ (Store Billing, Admin Subscriptions, Admin Store Detail) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ FeatureAggregatorService │
|
||||
│ (app/modules/billing/services/feature_service.py) │
|
||||
│ │
|
||||
│ • Collects feature providers from all enabled modules │
|
||||
│ • Queries TierFeatureLimit for limit values │
|
||||
│ • Queries MerchantFeatureOverride for per-merchant limits │
|
||||
│ • Calls provider.get_current_usage() for live counts │
|
||||
│ • Returns FeatureSummary[] with current/limit/percentage │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
|
||||
│ catalog module │ │ orders module │ │ tenancy module │
|
||||
│ products count │ │ orders count │ │ team members │
|
||||
└────────────────┘ └────────────────┘ └────────────────┘
|
||||
```
|
||||
|
||||
### Database Models
|
||||
|
||||
All subscription models are in `app/modules/billing/models/`:
|
||||
|
||||
| Model | Purpose |
|
||||
|-------|---------|
|
||||
| `SubscriptionTier` | Tier definitions with Stripe price IDs and feature codes |
|
||||
| `TierFeatureLimit` | Per-tier feature limits (feature_code + limit_value) |
|
||||
| `MerchantSubscription` | Per-merchant+platform subscription status |
|
||||
| `MerchantFeatureOverride` | Per-merchant feature limit overrides |
|
||||
| `AddOnProduct` | Purchasable add-ons (domains, SSL, email) |
|
||||
| `StoreAddOn` | Add-ons purchased by each store |
|
||||
| `StripeWebhookEvent` | Idempotency tracking for webhooks |
|
||||
| `BillingHistory` | Invoice and payment history |
|
||||
| `CapacitySnapshot` | Daily platform capacity metrics for forecasting |
|
||||
|
||||
### Feature Types
|
||||
|
||||
Features come in two types:
|
||||
|
||||
| Type | Description | Example |
|
||||
|------|-------------|---------|
|
||||
| **Quantitative** | Has a numeric limit with usage tracking | `max_products` (limit: 200, current: 150) |
|
||||
| **Binary** | Toggle-based, either enabled or disabled | `analytics_dashboard` (enabled/disabled) |
|
||||
|
||||
### FeatureSummary Dataclass
|
||||
|
||||
The core data structure returned by the feature system:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class FeatureSummary:
|
||||
code: str # e.g., "max_products"
|
||||
name_key: str # i18n key for display name
|
||||
limit: int | None # None = unlimited
|
||||
current: int # Current usage count
|
||||
remaining: int # Remaining before limit
|
||||
percent_used: float # 0.0 to 100.0
|
||||
feature_type: str # "quantitative" or "binary"
|
||||
scope: str # "tier" or "merchant_override"
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
| Service | Location | Purpose |
|
||||
|---------|----------|---------|
|
||||
| `FeatureAggregatorService` | `app/modules/billing/services/feature_service.py` | Aggregates usage from module providers, resolves tier limits + overrides |
|
||||
| `BillingService` | `app/modules/billing/services/billing_service.py` | Subscription operations, checkout, portal |
|
||||
| `SubscriptionService` | `app/modules/billing/services/subscription_service.py` | Subscription CRUD, tier lookups |
|
||||
| `AdminSubscriptionService` | `app/modules/billing/services/admin_subscription_service.py` | Admin subscription management |
|
||||
| `StripeService` | `app/modules/billing/services/stripe_service.py` | Core Stripe API operations |
|
||||
| `CapacityForecastService` | `app/modules/billing/services/capacity_forecast_service.py` | Growth trends, projections |
|
||||
|
||||
### Background Tasks
|
||||
|
||||
| Task | Location | Schedule | Purpose |
|
||||
|------|----------|----------|---------|
|
||||
| `reset_period_counters` | `app/modules/billing/tasks/subscription.py` | Daily | Reset order counters at period end |
|
||||
| `check_trial_expirations` | `app/modules/billing/tasks/subscription.py` | Daily | Expire trials without payment method |
|
||||
| `sync_stripe_status` | `app/modules/billing/tasks/subscription.py` | Hourly | Sync status with Stripe |
|
||||
| `cleanup_stale_subscriptions` | `app/modules/billing/tasks/subscription.py` | Weekly | Clean up old cancelled subscriptions |
|
||||
| `capture_capacity_snapshot` | `app/modules/billing/tasks/subscription.py` | Daily | Capture capacity metrics snapshot |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Store Billing API
|
||||
|
||||
Base: `/api/v1/store/billing`
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/billing/subscription` | GET | Current subscription status |
|
||||
| `/billing/tiers` | GET | Available tiers for upgrade |
|
||||
| `/billing/usage` | GET | Dynamic usage metrics (from feature providers) |
|
||||
| `/billing/checkout` | POST | Create Stripe checkout session |
|
||||
| `/billing/portal` | POST | Create Stripe customer portal session |
|
||||
| `/billing/invoices` | GET | Invoice history |
|
||||
| `/billing/upcoming-invoice` | GET | Preview next invoice |
|
||||
| `/billing/change-tier` | POST | Upgrade/downgrade tier |
|
||||
| `/billing/addons` | GET | Available add-on products |
|
||||
| `/billing/my-addons` | GET | Store's purchased add-ons |
|
||||
| `/billing/addons/purchase` | POST | Purchase an add-on |
|
||||
| `/billing/addons/{id}` | DELETE | Cancel an add-on |
|
||||
| `/billing/cancel` | POST | Cancel subscription |
|
||||
| `/billing/reactivate` | POST | Reactivate cancelled subscription |
|
||||
|
||||
The `/billing/usage` endpoint returns `UsageMetric[]`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Products",
|
||||
"current": 150,
|
||||
"limit": 200,
|
||||
"percentage": 75.0,
|
||||
"is_unlimited": false,
|
||||
"is_at_limit": false,
|
||||
"is_approaching_limit": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Admin Subscription API
|
||||
|
||||
Base: `/api/v1/admin/subscriptions`
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/tiers` | GET | List all subscription tiers |
|
||||
| `/tiers` | POST | Create a new tier |
|
||||
| `/tiers/{code}` | PATCH | Update a tier |
|
||||
| `/tiers/{code}` | DELETE | Delete a tier |
|
||||
| `/stats` | GET | Subscription statistics |
|
||||
| `/merchants/{id}/platforms/{pid}` | GET | Get merchant subscription |
|
||||
| `/merchants/{id}/platforms/{pid}` | PUT | Update merchant subscription |
|
||||
| `/store/{store_id}` | GET | Convenience: get subscription + usage for a store |
|
||||
|
||||
### Admin Feature Management API
|
||||
|
||||
Base: `/api/v1/admin/subscriptions/features`
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/catalog` | GET | Feature catalog grouped by category |
|
||||
| `/tiers/{code}/limits` | GET | Get feature limits for a tier |
|
||||
| `/tiers/{code}/limits` | PUT | Upsert feature limits for a tier |
|
||||
| `/merchants/{id}/overrides` | GET | Get merchant feature overrides |
|
||||
| `/merchants/{id}/overrides` | PUT | Upsert merchant feature overrides |
|
||||
|
||||
The **feature catalog** returns features grouped by category:
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"analytics": [
|
||||
{"code": "basic_analytics", "name": "Basic Analytics", "feature_type": "binary", "category": "analytics"},
|
||||
{"code": "analytics_dashboard", "name": "Analytics Dashboard", "feature_type": "binary", "category": "analytics"}
|
||||
],
|
||||
"limits": [
|
||||
{"code": "max_products", "name": "Product Limit", "feature_type": "quantitative", "category": "limits"},
|
||||
{"code": "max_orders_per_month", "name": "Orders per Month", "feature_type": "quantitative", "category": "limits"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Tier feature limits** use `TierFeatureLimitEntry[]` format:
|
||||
|
||||
```json
|
||||
[
|
||||
{"feature_code": "max_products", "limit_value": 200, "enabled": true},
|
||||
{"feature_code": "max_orders_per_month", "limit_value": 100, "enabled": true},
|
||||
{"feature_code": "analytics_dashboard", "limit_value": null, "enabled": true}
|
||||
]
|
||||
```
|
||||
|
||||
### Admin Store Convenience Endpoint
|
||||
|
||||
`GET /api/v1/admin/subscriptions/store/{store_id}` resolves a store to its merchant and returns subscription + usage in one call:
|
||||
|
||||
```json
|
||||
{
|
||||
"subscription": {
|
||||
"tier": "professional",
|
||||
"status": "active",
|
||||
"period_start": "2026-01-01T00:00:00Z",
|
||||
"period_end": "2026-02-01T00:00:00Z"
|
||||
},
|
||||
"tier": {
|
||||
"code": "professional",
|
||||
"name": "Professional",
|
||||
"price_monthly_cents": 9900
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"name": "Products",
|
||||
"current": 150,
|
||||
"limit": null,
|
||||
"percentage": 0,
|
||||
"is_unlimited": true,
|
||||
"is_at_limit": false,
|
||||
"is_approaching_limit": false
|
||||
},
|
||||
{
|
||||
"name": "Orders per Month",
|
||||
"current": 320,
|
||||
"limit": 500,
|
||||
"percentage": 64.0,
|
||||
"is_unlimited": false,
|
||||
"is_at_limit": false,
|
||||
"is_approaching_limit": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Platform Health API
|
||||
|
||||
Capacity endpoints under `/api/v1/admin/platform-health`:
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/platform-health/health` | GET | Full platform health report |
|
||||
| `/platform-health/capacity` | GET | Capacity-focused metrics |
|
||||
| `/platform-health/subscription-capacity` | GET | Subscription-based capacity vs usage |
|
||||
| `/platform-health/trends` | GET | Growth trends over time |
|
||||
| `/platform-health/recommendations` | GET | Scaling recommendations |
|
||||
| `/platform-health/snapshot` | POST | Manually capture capacity snapshot |
|
||||
|
||||
## Subscription Tiers
|
||||
|
||||
### Tier Structure
|
||||
|
||||
Tiers are stored in the `subscription_tiers` table. Feature limits are stored separately in `tier_feature_limits`:
|
||||
|
||||
```
|
||||
SubscriptionTier (essential)
|
||||
├── TierFeatureLimit: max_products = 200
|
||||
├── TierFeatureLimit: max_orders_per_month = 100
|
||||
├── TierFeatureLimit: max_team_members = 1
|
||||
├── TierFeatureLimit: basic_support (binary, enabled)
|
||||
└── TierFeatureLimit: basic_analytics (binary, enabled)
|
||||
|
||||
SubscriptionTier (professional)
|
||||
├── TierFeatureLimit: max_products = NULL (unlimited)
|
||||
├── TierFeatureLimit: max_orders_per_month = 500
|
||||
├── TierFeatureLimit: max_team_members = 3
|
||||
├── TierFeatureLimit: priority_support (binary, enabled)
|
||||
├── TierFeatureLimit: analytics_dashboard (binary, enabled)
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Admin Tier Management
|
||||
|
||||
Administrators manage tiers at `/admin/subscription-tiers`:
|
||||
|
||||
**Capabilities:**
|
||||
|
||||
- View all tiers with stats (total, active, public, MRR)
|
||||
- Create/edit tiers with pricing and Stripe IDs
|
||||
- Activate/deactivate tiers
|
||||
- Assign features to tiers via slide-over panel with:
|
||||
- Binary features: checkbox toggles grouped by category
|
||||
- Quantitative features: checkbox + numeric limit input
|
||||
- Select all / Deselect all per category
|
||||
|
||||
**Feature Panel API Flow:**
|
||||
|
||||
1. Open panel: `GET /admin/subscriptions/features/catalog` (all available features)
|
||||
2. Open panel: `GET /admin/subscriptions/features/tiers/{code}/limits` (current tier limits)
|
||||
3. Save: `PUT /admin/subscriptions/features/tiers/{code}/limits` (upsert limits)
|
||||
|
||||
### Per-Merchant Overrides
|
||||
|
||||
Admins can override tier limits for individual merchants via the subscription edit modal:
|
||||
|
||||
1. Open edit modal for a subscription
|
||||
2. Fetches feature catalog + current merchant overrides
|
||||
3. Shows each quantitative feature with override input (or "Tier default" placeholder)
|
||||
4. Save sends `PUT /admin/subscriptions/features/merchants/{id}/overrides`
|
||||
|
||||
## Frontend Pages
|
||||
|
||||
### Store Billing Page
|
||||
|
||||
**Location:** `/store/{store_code}/billing`
|
||||
|
||||
**Template:** `app/modules/billing/templates/billing/store/billing.html`
|
||||
**JS:** `app/modules/billing/static/store/js/billing.js`
|
||||
|
||||
The billing page fetches usage metrics dynamically from `GET /store/billing/usage` and renders them with Alpine.js:
|
||||
|
||||
```html
|
||||
<template x-for="metric in usageMetrics" :key="metric.name">
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span x-text="metric.name"></span>
|
||||
<span x-text="metric.is_unlimited ? metric.current + ' (Unlimited)' : metric.current + ' / ' + metric.limit"></span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="h-2 rounded-full"
|
||||
:class="metric.percentage >= 90 ? 'bg-red-600' : metric.percentage >= 70 ? 'bg-yellow-600' : 'bg-purple-600'"
|
||||
:style="`width: ${Math.min(100, metric.percentage || 0)}%`"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Page sections:**
|
||||
|
||||
1. **Current Plan**: Tier name, status, next billing date
|
||||
2. **Usage Meters**: Dynamic usage bars from feature providers
|
||||
3. **Change Plan**: Tier cards showing `feature_codes` list
|
||||
4. **Payment Method**: Link to Stripe portal
|
||||
5. **Invoice History**: Recent invoices with PDF links
|
||||
6. **Add-ons**: Available and purchased add-ons
|
||||
|
||||
### Admin Subscriptions Page
|
||||
|
||||
**Location:** `/admin/subscriptions`
|
||||
|
||||
**Template:** `app/modules/billing/templates/billing/admin/subscriptions.html`
|
||||
**JS:** `app/modules/billing/static/admin/js/subscriptions.js`
|
||||
|
||||
Lists all merchant subscriptions with:
|
||||
|
||||
- Tier, status, merchant info, period dates
|
||||
- Features count column (from `feature_codes.length`)
|
||||
- Edit modal with dynamic feature override editor
|
||||
|
||||
### Admin Subscription Tiers Page
|
||||
|
||||
**Location:** `/admin/subscription-tiers`
|
||||
|
||||
**Template:** `app/modules/billing/templates/billing/admin/subscription-tiers.html`
|
||||
**JS:** `app/modules/billing/static/admin/js/subscription-tiers.js`
|
||||
|
||||
Manages tier definitions with:
|
||||
|
||||
- Stats cards (total, active, public, MRR)
|
||||
- Tier table (code, name, pricing, features count, status)
|
||||
- Create/edit modal (pricing, Stripe IDs, description, toggles)
|
||||
- Feature assignment slide-over panel (binary toggles + quantitative limit inputs)
|
||||
|
||||
### Merchant Subscription Detail Page
|
||||
|
||||
**Template:** `app/modules/billing/templates/billing/merchant/subscription-detail.html`
|
||||
|
||||
Shows subscription details with plan limits rendered dynamically from `tier.feature_limits`:
|
||||
|
||||
```html
|
||||
<template x-for="fl in (subscription?.tier?.feature_limits || [])" :key="fl.feature_code">
|
||||
<div class="p-4 bg-gray-50 rounded-lg">
|
||||
<p class="text-sm text-gray-500" x-text="fl.feature_code.replace(/_/g, ' ')"></p>
|
||||
<p class="text-xl font-bold" x-text="fl.limit_value || 'Unlimited'"></p>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Admin Store Detail Page
|
||||
|
||||
**Template:** `app/modules/tenancy/templates/tenancy/admin/store-detail.html`
|
||||
**JS:** `app/modules/tenancy/static/admin/js/store-detail.js`
|
||||
|
||||
The subscription section uses the convenience endpoint `GET /admin/subscriptions/store/{store_id}` to load subscription + usage metrics in one call, rendering dynamic usage bars.
|
||||
|
||||
## Stripe Integration
|
||||
|
||||
### Configuration
|
||||
|
||||
Required environment variables:
|
||||
|
||||
```bash
|
||||
STRIPE_SECRET_KEY=sk_test_...
|
||||
STRIPE_PUBLISHABLE_KEY=pk_test_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
STRIPE_TRIAL_DAYS=30 # Optional, default trial period
|
||||
```
|
||||
|
||||
### Setup Guide
|
||||
|
||||
#### Step 1: Get API Keys
|
||||
|
||||
1. Go to [Stripe Dashboard](https://dashboard.stripe.com/apikeys)
|
||||
2. Copy your **Publishable key** (`pk_test_...` or `pk_live_...`)
|
||||
3. Copy your **Secret key** (`sk_test_...` or `sk_live_...`)
|
||||
|
||||
#### Step 2: Create Webhook Endpoint
|
||||
|
||||
1. Go to [Stripe Webhooks](https://dashboard.stripe.com/webhooks)
|
||||
2. Click **Add endpoint**
|
||||
3. Enter your endpoint URL: `https://yourdomain.com/api/v1/webhooks/stripe`
|
||||
4. Select events to listen to:
|
||||
- `checkout.session.completed`
|
||||
- `customer.subscription.created`
|
||||
- `customer.subscription.updated`
|
||||
- `customer.subscription.deleted`
|
||||
- `invoice.paid`
|
||||
- `invoice.payment_failed`
|
||||
5. Click **Add endpoint**
|
||||
6. Copy the **Signing secret** (`whsec_...`) - this is your `STRIPE_WEBHOOK_SECRET`
|
||||
|
||||
#### Step 3: Local Development with Stripe CLI
|
||||
|
||||
For local testing, use the [Stripe CLI](https://stripe.com/docs/stripe-cli):
|
||||
|
||||
```bash
|
||||
# Install Stripe CLI
|
||||
brew install stripe/stripe-cli/stripe # macOS
|
||||
# or download from https://github.com/stripe/stripe-cli/releases
|
||||
|
||||
# Login to Stripe
|
||||
stripe login
|
||||
|
||||
# Forward webhooks to your local server
|
||||
stripe listen --forward-to localhost:8000/api/v1/webhooks/stripe
|
||||
|
||||
# The CLI will display a webhook signing secret (whsec_...)
|
||||
# Use this as STRIPE_WEBHOOK_SECRET for local development
|
||||
```
|
||||
|
||||
#### Step 4: Create Products & Prices in Stripe
|
||||
|
||||
Create subscription products for each tier:
|
||||
|
||||
1. Go to [Stripe Products](https://dashboard.stripe.com/products)
|
||||
2. Create products for each tier (Essential, Professional, Business, Enterprise)
|
||||
3. Add monthly and annual prices for each
|
||||
4. Copy the Price IDs (`price_...`) and update your tier configuration
|
||||
|
||||
### Webhook Events
|
||||
|
||||
The system handles these Stripe events:
|
||||
|
||||
| Event | Handler |
|
||||
|-------|---------|
|
||||
| `checkout.session.completed` | Activates subscription, links customer |
|
||||
| `customer.subscription.updated` | Updates tier, status, period |
|
||||
| `customer.subscription.deleted` | Marks subscription cancelled |
|
||||
| `invoice.paid` | Records payment in billing history |
|
||||
| `invoice.payment_failed` | Marks past due, increments retry count |
|
||||
|
||||
### Webhook Endpoint
|
||||
|
||||
Webhooks are received at `/api/v1/webhooks/stripe`:
|
||||
|
||||
```python
|
||||
# Uses signature verification for security
|
||||
event = stripe_service.construct_event(payload, stripe_signature)
|
||||
handler = StripeWebhookHandler()
|
||||
result = handler.handle_event(db, event)
|
||||
```
|
||||
|
||||
The handler implements **idempotency** via `StripeWebhookEvent` records. Duplicate events (same `event_id`) are skipped.
|
||||
|
||||
## Add-ons
|
||||
|
||||
### Available Add-ons
|
||||
|
||||
| Code | Name | Category | Price |
|
||||
|------|------|----------|-------|
|
||||
| `domain` | Custom Domain | domain | €15/year |
|
||||
| `ssl_premium` | Premium SSL | ssl | €49/year |
|
||||
| `email_5` | 5 Email Addresses | email | €5/month |
|
||||
| `email_10` | 10 Email Addresses | email | €9/month |
|
||||
| `email_25` | 25 Email Addresses | email | €19/month |
|
||||
|
||||
### Purchase Flow
|
||||
|
||||
1. Store selects add-on on billing page
|
||||
2. For domains: enter domain name, validate availability
|
||||
3. Create Stripe checkout session with add-on price
|
||||
4. On webhook success: create `StoreAddOn` record
|
||||
|
||||
## Capacity Forecasting
|
||||
|
||||
### Subscription-Based Capacity
|
||||
|
||||
Track theoretical vs actual capacity:
|
||||
|
||||
```python
|
||||
capacity = platform_health_service.get_subscription_capacity(db)
|
||||
|
||||
# Returns:
|
||||
{
|
||||
"total_subscriptions": 150,
|
||||
"tier_distribution": {
|
||||
"essential": 80,
|
||||
"professional": 50,
|
||||
"business": 18,
|
||||
"enterprise": 2
|
||||
},
|
||||
"products": {
|
||||
"actual": 125000,
|
||||
"theoretical_limit": 500000,
|
||||
"utilization_percent": 25.0,
|
||||
"headroom": 375000
|
||||
},
|
||||
"orders_monthly": {
|
||||
"actual": 45000,
|
||||
"theoretical_limit": 300000,
|
||||
"utilization_percent": 15.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Growth Trends
|
||||
|
||||
Analyze growth over time:
|
||||
|
||||
```python
|
||||
trends = capacity_forecast_service.get_growth_trends(db, days=30)
|
||||
|
||||
# Returns growth rates, daily projections, monthly projections
|
||||
```
|
||||
|
||||
### Scaling Recommendations
|
||||
|
||||
```python
|
||||
recommendations = capacity_forecast_service.get_scaling_recommendations(db)
|
||||
|
||||
# Returns:
|
||||
[
|
||||
{
|
||||
"category": "capacity",
|
||||
"severity": "warning",
|
||||
"title": "Product capacity approaching limit",
|
||||
"description": "Currently at 85% of theoretical product capacity",
|
||||
"action": "Consider upgrading store tiers or adding capacity"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Infrastructure Scaling Reference
|
||||
|
||||
| Clients | vCPU | RAM | Storage | Database | Monthly Cost |
|
||||
|---------|------|-----|---------|----------|--------------|
|
||||
| 1-50 | 2 | 4GB | 100GB | SQLite | €30 |
|
||||
| 50-100 | 4 | 8GB | 250GB | PostgreSQL | €80 |
|
||||
| 100-300 | 4 | 16GB | 500GB | PostgreSQL | €150 |
|
||||
| 300-500 | 8 | 32GB | 1TB | PostgreSQL + Redis | €350 |
|
||||
| 500-1000 | 16 | 64GB | 2TB | PostgreSQL + Redis | €700 |
|
||||
| 1000+ | 32+ | 128GB+ | 4TB+ | PostgreSQL cluster | €1,500+ |
|
||||
|
||||
## Exception Handling
|
||||
|
||||
Custom exceptions for billing operations (`app/modules/billing/exceptions.py`):
|
||||
|
||||
| Exception | HTTP Status | Description |
|
||||
|-----------|-------------|-------------|
|
||||
| `PaymentSystemNotConfiguredException` | 503 | Stripe not configured |
|
||||
| `TierNotFoundException` | 404 | Invalid tier code |
|
||||
| `StripePriceNotConfiguredException` | 400 | No Stripe price for tier |
|
||||
| `NoActiveSubscriptionException` | 400 | Operation requires subscription |
|
||||
| `SubscriptionNotCancelledException` | 400 | Cannot reactivate active subscription |
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests for the billing system:
|
||||
|
||||
```bash
|
||||
# Run billing service tests
|
||||
pytest tests/unit/services/test_billing_service.py -v
|
||||
|
||||
# Run webhook handler tests
|
||||
pytest tests/unit/services/test_stripe_webhook_handler.py -v
|
||||
|
||||
# Run feature service tests
|
||||
pytest tests/unit/services/test_feature_service.py -v
|
||||
|
||||
# Run usage service tests
|
||||
pytest tests/unit/services/test_usage_service.py -v
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- `BillingService`: Tier queries, invoices, add-ons
|
||||
- `StripeWebhookHandler`: Event idempotency, checkout completion, status mapping
|
||||
- `FeatureService`: Feature aggregation, tier limits, merchant overrides
|
||||
- `UsageService`: Usage tracking, limit checks
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Webhook signatures verified before processing
|
||||
- Idempotency keys prevent duplicate event processing
|
||||
- Customer portal links are session-based and expire
|
||||
- Stripe API key stored securely in environment variables
|
||||
- Background tasks run with database session isolation
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Feature Gating System](../implementation/feature-gating-system.md) - Feature access control and UI integration
|
||||
- [Metrics Provider Pattern](../architecture/metrics-provider-pattern.md) - Protocol-based metrics from modules
|
||||
- [Capacity Monitoring](../operations/capacity-monitoring.md) - Detailed monitoring guide
|
||||
- [Capacity Planning](../architecture/capacity-planning.md) - Infrastructure sizing
|
||||
- [Stripe Integration](../deployment/stripe-integration.md) - Payment setup
|
||||
- [Subscription Tier Management](../guides/subscription-tier-management.md) - User guide for tier management
|
||||
This document has moved to the billing module docs: [Subscription System](../modules/billing/subscription-system.md)
|
||||
|
||||
@@ -1,502 +1 @@
|
||||
# Hosting Module - User Journeys
|
||||
|
||||
## Personas
|
||||
|
||||
| # | Persona | Role / Auth | Description |
|
||||
|---|---------|-------------|-------------|
|
||||
| 1 | **Platform Admin** | `admin` role | Manages the POC → live website pipeline, tracks client services, monitors renewals |
|
||||
| 2 | **Prospect** | No auth (receives proposal link) | Views their POC website preview via a shared link |
|
||||
|
||||
!!! note "Admin-only module"
|
||||
The hosting module is primarily an admin-only module. The only non-admin page is the
|
||||
**POC Viewer** — a public preview page that shows the prospect's POC website with a
|
||||
HostWizard banner. Prospects do not have accounts until their proposal is accepted, at
|
||||
which point a Merchant account is created for them.
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle Overview
|
||||
|
||||
The hosting module manages the complete POC → live website pipeline:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Prospect identified] --> B[Create Hosted Site]
|
||||
B --> C[Status: DRAFT]
|
||||
C --> D[Build POC website via CMS]
|
||||
D --> E[Mark POC Ready]
|
||||
E --> F[Status: POC_READY]
|
||||
F --> G[Send Proposal to prospect]
|
||||
G --> H[Status: PROPOSAL_SENT]
|
||||
H --> I{Prospect accepts?}
|
||||
I -->|Yes| J[Accept Proposal]
|
||||
J --> K[Status: ACCEPTED]
|
||||
K --> L[Merchant account created]
|
||||
L --> M[Go Live with domain]
|
||||
M --> N[Status: LIVE]
|
||||
I -->|No| O[Cancel]
|
||||
O --> P[Status: CANCELLED]
|
||||
N --> Q{Issues?}
|
||||
Q -->|Payment issues| R[Suspend]
|
||||
R --> S[Status: SUSPENDED]
|
||||
S --> T[Reactivate → LIVE]
|
||||
Q -->|Client leaves| O
|
||||
```
|
||||
|
||||
### Status Transitions
|
||||
|
||||
| From | Allowed Targets |
|
||||
|------|----------------|
|
||||
| `draft` | `poc_ready`, `cancelled` |
|
||||
| `poc_ready` | `proposal_sent`, `cancelled` |
|
||||
| `proposal_sent` | `accepted`, `cancelled` |
|
||||
| `accepted` | `live`, `cancelled` |
|
||||
| `live` | `suspended`, `cancelled` |
|
||||
| `suspended` | `live`, `cancelled` |
|
||||
| `cancelled` | _(terminal)_ |
|
||||
|
||||
---
|
||||
|
||||
## Dev URLs (localhost:9999)
|
||||
|
||||
The dev server uses path-based platform routing: `http://localhost:9999/platforms/hosting/...`
|
||||
|
||||
### 1. Admin Pages
|
||||
|
||||
Login as: `admin@orion.lu` or `samir.boulahtit@gmail.com`
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Dashboard | `http://localhost:9999/platforms/hosting/admin/hosting` |
|
||||
| Sites List | `http://localhost:9999/platforms/hosting/admin/hosting/sites` |
|
||||
| New Site | `http://localhost:9999/platforms/hosting/admin/hosting/sites/new` |
|
||||
| Site Detail | `http://localhost:9999/platforms/hosting/admin/hosting/sites/{site_id}` |
|
||||
| Client Services | `http://localhost:9999/platforms/hosting/admin/hosting/clients` |
|
||||
|
||||
### 2. Public Pages
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| POC Viewer | `http://localhost:9999/platforms/hosting/hosting/sites/{site_id}/preview` |
|
||||
|
||||
### 3. Admin API Endpoints
|
||||
|
||||
**Sites** (prefix: `/platforms/hosting/api/admin/hosting/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | list sites | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites` |
|
||||
| GET | site detail | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}` |
|
||||
| POST | create site | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites` |
|
||||
| POST | create from prospect | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/from-prospect/{prospect_id}` |
|
||||
| PUT | update site | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}` |
|
||||
| DELETE | delete site | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}` |
|
||||
|
||||
**Lifecycle** (prefix: `/platforms/hosting/api/admin/hosting/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| POST | mark POC ready | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}/mark-poc-ready` |
|
||||
| POST | send proposal | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}/send-proposal` |
|
||||
| POST | accept proposal | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}/accept` |
|
||||
| POST | go live | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}/go-live` |
|
||||
| POST | suspend | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}/suspend` |
|
||||
| POST | cancel | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}/cancel` |
|
||||
|
||||
**Client Services** (prefix: `/platforms/hosting/api/admin/hosting/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | list services | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{site_id}/services` |
|
||||
| POST | create service | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{site_id}/services` |
|
||||
| PUT | update service | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{site_id}/services/{id}` |
|
||||
| DELETE | delete service | `http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{site_id}/services/{id}` |
|
||||
|
||||
**Stats** (prefix: `/platforms/hosting/api/admin/hosting/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | dashboard stats | `http://localhost:9999/platforms/hosting/api/admin/hosting/stats/dashboard` |
|
||||
|
||||
---
|
||||
|
||||
## Production URLs (hostwizard.lu)
|
||||
|
||||
In production, the platform uses **domain-based routing**.
|
||||
|
||||
### Admin Pages & API
|
||||
|
||||
| Page / Endpoint | Production URL |
|
||||
|-----------------|----------------|
|
||||
| Dashboard | `https://hostwizard.lu/admin/hosting` |
|
||||
| Sites | `https://hostwizard.lu/admin/hosting/sites` |
|
||||
| New Site | `https://hostwizard.lu/admin/hosting/sites/new` |
|
||||
| Site Detail | `https://hostwizard.lu/admin/hosting/sites/{id}` |
|
||||
| Client Services | `https://hostwizard.lu/admin/hosting/clients` |
|
||||
| API - Sites | `GET https://hostwizard.lu/api/admin/hosting/sites` |
|
||||
| API - Stats | `GET https://hostwizard.lu/api/admin/hosting/stats/dashboard` |
|
||||
|
||||
### Public Pages
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| POC Viewer | `https://hostwizard.lu/hosting/sites/{site_id}/preview` |
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### Hosted Site
|
||||
|
||||
```
|
||||
HostedSite
|
||||
├── id (PK)
|
||||
├── store_id (FK → stores.id, unique) # The CMS-powered website
|
||||
├── prospect_id (FK → prospects.id, nullable) # Origin prospect
|
||||
├── status: draft | poc_ready | proposal_sent | accepted | live | suspended | cancelled
|
||||
├── business_name (str)
|
||||
├── contact_name, contact_email, contact_phone
|
||||
├── proposal_sent_at, proposal_accepted_at, went_live_at (datetime)
|
||||
├── proposal_notes (text)
|
||||
├── live_domain (str, unique)
|
||||
├── internal_notes (text)
|
||||
├── created_at, updated_at
|
||||
└── Relationships: store, prospect, client_services
|
||||
```
|
||||
|
||||
### Client Service
|
||||
|
||||
```
|
||||
ClientService
|
||||
├── id (PK)
|
||||
├── hosted_site_id (FK → hosted_sites.id, CASCADE)
|
||||
├── service_type: domain | email | ssl | hosting | website_maintenance
|
||||
├── name (str) # e.g., "acme.lu domain", "5 mailboxes"
|
||||
├── status: pending | active | suspended | expired | cancelled
|
||||
├── billing_period: monthly | annual | one_time
|
||||
├── price_cents (int), currency (str, default EUR)
|
||||
├── addon_product_id (FK, nullable) # Link to billing product
|
||||
├── domain_name, registrar # Domain-specific
|
||||
├── mailbox_count # Email-specific
|
||||
├── expires_at, period_start, period_end, auto_renew
|
||||
├── notes (text)
|
||||
└── created_at, updated_at
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Journeys
|
||||
|
||||
### Journey 1: Create Hosted Site from Prospect
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Convert a qualified prospect into a hosted site with a POC website
|
||||
|
||||
**Prerequisite:** A prospect exists in the prospecting module (see [Prospecting Journeys](prospecting.md))
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[View prospect in prospecting module] --> B[Click 'Create Hosted Site from Prospect']
|
||||
B --> C[HostedSite created with status DRAFT]
|
||||
C --> D[Store auto-created on hosting platform]
|
||||
D --> E[Contact info pre-filled from prospect]
|
||||
E --> F[Navigate to site detail]
|
||||
F --> G[Build POC website via CMS editor]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Create hosted site from prospect:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/hosting/sites/from-prospect/{prospect_id}`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/hosting/sites/from-prospect/{prospect_id}`
|
||||
2. This automatically:
|
||||
- Creates a Store on the hosting platform
|
||||
- Creates a HostedSite record linked to the Store and Prospect
|
||||
- Pre-fills business_name, contact_name, contact_email, contact_phone from prospect data
|
||||
3. View the new site:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/hosting/sites/{site_id}`
|
||||
- Prod: `https://hostwizard.lu/admin/hosting/sites/{site_id}`
|
||||
4. Click the Store link to open the CMS editor and build the POC website
|
||||
|
||||
---
|
||||
|
||||
### Journey 2: Create Hosted Site Manually
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Create a hosted site without an existing prospect (e.g., direct referral)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Navigate to New Site page] --> B[Fill in business details]
|
||||
B --> C[Submit form]
|
||||
C --> D[HostedSite + Store created]
|
||||
D --> E[Navigate to site detail]
|
||||
E --> F[Build POC website]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Navigate to New Site form:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/hosting/sites/new`
|
||||
- Prod: `https://hostwizard.lu/admin/hosting/sites/new`
|
||||
2. Create the site:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/hosting/sites`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/hosting/sites`
|
||||
- Body: `{ "business_name": "Boulangerie du Parc", "contact_name": "Jean Müller", "contact_email": "jean@boulangerie-parc.lu", "contact_phone": "+352 26 123 456" }`
|
||||
3. A Store is auto-created with subdomain `boulangerie-du-parc` on the hosting platform
|
||||
|
||||
---
|
||||
|
||||
### Journey 3: POC → Proposal Flow
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Build a POC website, mark it ready, and send a proposal to the prospect
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Site is DRAFT] --> B[Build POC website via CMS]
|
||||
B --> C[Mark POC Ready]
|
||||
C --> D[Site is POC_READY]
|
||||
D --> E[Preview the POC site]
|
||||
E --> F[Send Proposal with notes]
|
||||
F --> G[Site is PROPOSAL_SENT]
|
||||
G --> H[Share preview link with prospect]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Build the POC website using the Store's CMS editor (linked from site detail page)
|
||||
2. When the POC is ready, mark it:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}/mark-poc-ready`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/hosting/sites/{id}/mark-poc-ready`
|
||||
3. Preview the POC site (public link, no auth needed):
|
||||
- Dev: `http://localhost:9999/platforms/hosting/hosting/sites/{id}/preview`
|
||||
- Prod: `https://hostwizard.lu/hosting/sites/{id}/preview`
|
||||
4. Send proposal to the prospect:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}/send-proposal`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/hosting/sites/{id}/send-proposal`
|
||||
- Body: `{ "notes": "Custom website with 5 pages, domain registration included" }`
|
||||
5. Share the preview link with the prospect via email
|
||||
|
||||
!!! info "POC Viewer"
|
||||
The POC Viewer page renders the Store's storefront in an iframe with a teal
|
||||
HostWizard banner at the top. It only works for sites with status `poc_ready`
|
||||
or `proposal_sent`. Once the site goes live, the preview is disabled.
|
||||
|
||||
---
|
||||
|
||||
### Journey 4: Accept Proposal & Create Merchant
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** When a prospect accepts, create their merchant account and subscription
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Prospect accepts proposal] --> B{Existing merchant?}
|
||||
B -->|Yes| C[Link to existing merchant]
|
||||
B -->|No| D[Auto-create merchant + owner account]
|
||||
C --> E[Accept Proposal]
|
||||
D --> E
|
||||
E --> F[Site is ACCEPTED]
|
||||
F --> G[Store reassigned to merchant]
|
||||
G --> H[Subscription created on hosting platform]
|
||||
H --> I[Prospect marked as CONVERTED]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Accept the proposal (auto-creates merchant if no merchant_id provided):
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}/accept`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/hosting/sites/{id}/accept`
|
||||
- Body: `{}` (auto-create merchant) or `{ "merchant_id": 5 }` (link to existing)
|
||||
2. This automatically:
|
||||
- Creates a new Merchant from contact info (name, email, phone)
|
||||
- Creates a store owner account with a temporary password
|
||||
- Reassigns the Store from the system merchant to the new merchant
|
||||
- Creates a MerchantSubscription on the hosting platform (essential tier)
|
||||
- Marks the linked prospect as CONVERTED (if prospect_id is set)
|
||||
3. View the updated site detail:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/hosting/sites/{id}`
|
||||
- Prod: `https://hostwizard.lu/admin/hosting/sites/{id}`
|
||||
|
||||
!!! warning "Merchant account credentials"
|
||||
When accepting without an existing `merchant_id`, a new merchant owner account is
|
||||
created with a temporary password. The admin should communicate these credentials
|
||||
to the client so they can log in and self-edit their website via the CMS.
|
||||
|
||||
---
|
||||
|
||||
### Journey 5: Go Live with Custom Domain
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Assign a production domain to the website and make it live
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Site is ACCEPTED] --> B[Configure DNS for client domain]
|
||||
B --> C[Go Live with domain]
|
||||
C --> D[Site is LIVE]
|
||||
D --> E[StoreDomain created]
|
||||
E --> F[Website accessible at client domain]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Ensure DNS is configured for the client's domain (A/AAAA records pointing to the server)
|
||||
2. Go live:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}/go-live`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/hosting/sites/{id}/go-live`
|
||||
- Body: `{ "domain": "boulangerie-parc.lu" }`
|
||||
3. This automatically:
|
||||
- Sets `went_live_at` timestamp
|
||||
- Creates a StoreDomain record (primary) for the domain
|
||||
- Sets `live_domain` on the hosted site
|
||||
4. The website is now accessible at `https://boulangerie-parc.lu`
|
||||
|
||||
---
|
||||
|
||||
### Journey 6: Add Client Services
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Track operational services (domains, email, SSL, hosting) for a client
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Open site detail] --> B[Go to Services tab]
|
||||
B --> C[Add domain service]
|
||||
C --> D[Add email service]
|
||||
D --> E[Add SSL service]
|
||||
E --> F[Add hosting service]
|
||||
F --> G[Services tracked with expiry dates]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Navigate to site detail, Services tab:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/hosting/sites/{site_id}`
|
||||
- Prod: `https://hostwizard.lu/admin/hosting/sites/{site_id}`
|
||||
2. Add a domain service:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{site_id}/services`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/hosting/sites/{site_id}/services`
|
||||
- Body: `{ "service_type": "domain", "name": "boulangerie-parc.lu domain", "domain_name": "boulangerie-parc.lu", "registrar": "Namecheap", "billing_period": "annual", "price_cents": 1500, "expires_at": "2027-03-01T00:00:00", "auto_renew": true }`
|
||||
3. Add an email service:
|
||||
- Body: `{ "service_type": "email", "name": "5 mailboxes", "mailbox_count": 5, "billing_period": "monthly", "price_cents": 999 }`
|
||||
4. Add an SSL service:
|
||||
- Body: `{ "service_type": "ssl", "name": "SSL certificate", "billing_period": "annual", "price_cents": 0, "expires_at": "2027-03-01T00:00:00" }`
|
||||
5. View all services for a site:
|
||||
- API Dev: `GET http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{site_id}/services`
|
||||
- API Prod: `GET https://hostwizard.lu/api/admin/hosting/sites/{site_id}/services`
|
||||
|
||||
---
|
||||
|
||||
### Journey 7: Dashboard & Renewal Monitoring
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Monitor business KPIs and upcoming service renewals
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Navigate to Dashboard] --> B[View KPIs]
|
||||
B --> C[Total sites, live sites, POC sites]
|
||||
C --> D[Monthly revenue]
|
||||
D --> E[Active services count]
|
||||
E --> F[Upcoming renewals in 30 days]
|
||||
F --> G[Navigate to Client Services]
|
||||
G --> H[Filter by expiring soon]
|
||||
H --> I[Renew or update services]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Navigate to Dashboard:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/hosting`
|
||||
- Prod: `https://hostwizard.lu/admin/hosting`
|
||||
2. View dashboard stats:
|
||||
- API Dev: `GET http://localhost:9999/platforms/hosting/api/admin/hosting/stats/dashboard`
|
||||
- API Prod: `GET https://hostwizard.lu/api/admin/hosting/stats/dashboard`
|
||||
- Returns: `total_sites`, `live_sites`, `poc_sites`, `sites_by_status`, `active_services`, `monthly_revenue_cents`, `upcoming_renewals`, `services_by_type`
|
||||
3. Navigate to Client Services for detailed view:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/hosting/clients`
|
||||
- Prod: `https://hostwizard.lu/admin/hosting/clients`
|
||||
4. Filter by type (domain, email, ssl, hosting) or status
|
||||
5. Toggle "Expiring Soon" to see services expiring within 30 days
|
||||
|
||||
---
|
||||
|
||||
### Journey 8: Suspend & Reactivate
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Handle suspension (e.g., unpaid invoices) and reactivation
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Suspend a site:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}/suspend`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/hosting/sites/{id}/suspend`
|
||||
2. Site status changes to `suspended`
|
||||
3. Once payment is resolved, reactivate by transitioning back to live:
|
||||
- The `suspended → live` transition is allowed
|
||||
4. To permanently close a site:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/hosting/sites/{id}/cancel`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/hosting/sites/{id}/cancel`
|
||||
5. `cancelled` is a terminal state — no further transitions allowed
|
||||
|
||||
---
|
||||
|
||||
### Journey 9: Complete Pipeline (Prospect → Live Site)
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Walk the complete pipeline from prospect to live website
|
||||
|
||||
This journey combines the prospecting and hosting modules end-to-end:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Import domain / capture lead] --> B[Enrich & score prospect]
|
||||
B --> C[Create hosted site from prospect]
|
||||
C --> D[Build POC website via CMS]
|
||||
D --> E[Mark POC ready]
|
||||
E --> F[Send proposal + share preview link]
|
||||
F --> G{Prospect accepts?}
|
||||
G -->|Yes| H[Accept → Merchant created]
|
||||
H --> I[Add client services]
|
||||
I --> J[Go live with domain]
|
||||
J --> K[Website live at client domain]
|
||||
K --> L[Monitor renewals & services]
|
||||
G -->|No| M[Cancel or follow up later]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. **Prospecting phase** (see [Prospecting Journeys](prospecting.md)):
|
||||
- Import domain or capture lead offline
|
||||
- Run enrichment pipeline
|
||||
- Score and qualify the prospect
|
||||
2. **Create hosted site**: `POST /api/admin/hosting/sites/from-prospect/{prospect_id}`
|
||||
3. **Build POC**: Edit the auto-created Store via CMS
|
||||
4. **Mark POC ready**: `POST /api/admin/hosting/sites/{id}/mark-poc-ready`
|
||||
5. **Send proposal**: `POST /api/admin/hosting/sites/{id}/send-proposal`
|
||||
6. **Share preview**: Send `https://hostwizard.lu/hosting/sites/{id}/preview` to prospect
|
||||
7. **Accept proposal**: `POST /api/admin/hosting/sites/{id}/accept`
|
||||
8. **Add services**: `POST /api/admin/hosting/sites/{id}/services` (domain, email, SSL, hosting)
|
||||
9. **Go live**: `POST /api/admin/hosting/sites/{id}/go-live` with domain
|
||||
10. **Monitor**: Dashboard at `https://hostwizard.lu/admin/hosting`
|
||||
|
||||
---
|
||||
|
||||
## Recommended Test Order
|
||||
|
||||
1. **Journey 2** - Create a site manually first (simplest path, no prospect dependency)
|
||||
2. **Journey 3** - Walk the POC → proposal flow
|
||||
3. **Journey 4** - Accept proposal and verify merchant creation
|
||||
4. **Journey 5** - Go live with a test domain
|
||||
5. **Journey 6** - Add client services
|
||||
6. **Journey 7** - Check dashboard stats
|
||||
7. **Journey 1** - Test the prospect → hosted site conversion (requires prospecting data)
|
||||
8. **Journey 8** - Test suspend/reactivate/cancel
|
||||
9. **Journey 9** - Walk the complete end-to-end pipeline
|
||||
|
||||
!!! tip "Test Journey 2 before Journey 1"
|
||||
Journey 2 (manual creation) doesn't require any prospecting data and is the fastest
|
||||
way to verify the hosting module works. Journey 1 (from prospect) requires running
|
||||
the prospecting module first.
|
||||
This document has moved to the hosting module docs: [User Journeys](../../modules/hosting/user-journeys.md)
|
||||
|
||||
@@ -1,794 +1,3 @@
|
||||
# Loyalty Module - User Journeys
|
||||
|
||||
## Personas
|
||||
|
||||
| # | Persona | Role / Auth | Description |
|
||||
|---|---------|-------------|-------------|
|
||||
| 1 | **Platform Admin** | `admin` role | Oversees all merchants' loyalty programs, views platform-wide stats, manages merchant settings |
|
||||
| 2 | **Merchant Owner** | `store` role + owns merchant | Manages their merchant-wide loyalty program via the store interface. There is **no separate merchant owner UI** - loyalty programs are merchant-scoped but managed through any of the merchant's stores |
|
||||
| 3 | **Store Staff / Team Member** | `store` role + store membership | Operates the POS terminal - scans cards, adds stamps/points, redeems rewards |
|
||||
| 4 | **Customer (authenticated)** | Customer login | Views their loyalty card, balance, and transaction history |
|
||||
| 5 | **Customer (anonymous)** | No auth | Browses program info, self-enrolls, downloads wallet passes |
|
||||
|
||||
!!! note "Merchant Owner vs Store Staff"
|
||||
The loyalty module does **not** have a dedicated merchant owner interface. The merchant owner
|
||||
accesses loyalty through the **store interface** (`/store/{store_code}/loyalty/...`). Since the
|
||||
loyalty program is scoped at the merchant level (one program shared by all stores), the owner
|
||||
can manage it from any of their stores. The difference is only in **permissions** - owners have
|
||||
full access, team members have role-based access.
|
||||
|
||||
---
|
||||
|
||||
## Current Dev Database State
|
||||
|
||||
### Merchants & Stores
|
||||
|
||||
| Merchant | Owner | Stores |
|
||||
|----------|-------|--------|
|
||||
| WizaCorp Ltd. (id=1) | john.owner@wizacorp.com | ORION, WIZAGADGETS, WIZAHOME |
|
||||
| Fashion Group S.A. (id=2) | jane.owner@fashiongroup.com | FASHIONHUB, FASHIONOUTLET |
|
||||
| BookWorld Publishing (id=3) | bob.owner@bookworld.com | BOOKSTORE, BOOKDIGITAL |
|
||||
|
||||
### Users
|
||||
|
||||
| Email | Role | Type |
|
||||
|-------|------|------|
|
||||
| admin@orion.lu | admin | Platform admin |
|
||||
| samir.boulahtit@gmail.com | admin | Platform admin |
|
||||
| john.owner@wizacorp.com | store | Owner of WizaCorp (merchant 1) |
|
||||
| jane.owner@fashiongroup.com | store | Owner of Fashion Group (merchant 2) |
|
||||
| bob.owner@bookworld.com | store | Owner of BookWorld (merchant 3) |
|
||||
| alice.manager@wizacorp.com | store | Team member (stores 1, 2) |
|
||||
| charlie.staff@wizacorp.com | store | Team member (store 3) |
|
||||
| diana.stylist@fashiongroup.com | store | Team member (stores 4, 5) |
|
||||
| eric.sales@fashiongroup.com | store | Team member (store 5) |
|
||||
| fiona.editor@bookworld.com | store | Team member (stores 6, 7) |
|
||||
|
||||
### Loyalty Data Status
|
||||
|
||||
| Table | Rows |
|
||||
|-------|------|
|
||||
| loyalty_programs | 0 |
|
||||
| loyalty_cards | 0 |
|
||||
| loyalty_transactions | 0 |
|
||||
| merchant_loyalty_settings | 0 |
|
||||
| staff_pins | 0 |
|
||||
| merchant_subscriptions | 0 |
|
||||
|
||||
!!! warning "No loyalty programs exist yet"
|
||||
All loyalty tables are empty. The first step in testing is to create a loyalty program
|
||||
via the store interface. There are also **no subscriptions** set up, which may gate access
|
||||
to the loyalty module depending on feature-gating configuration.
|
||||
|
||||
---
|
||||
|
||||
## Dev URLs (localhost:9999)
|
||||
|
||||
The dev server uses path-based platform routing: `http://localhost:9999/platforms/loyalty/...`
|
||||
|
||||
### 1. Platform Admin Pages
|
||||
|
||||
Login as: `admin@orion.lu` or `samir.boulahtit@gmail.com`
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Programs Dashboard | `http://localhost:9999/platforms/loyalty/admin/loyalty/programs` |
|
||||
| Analytics | `http://localhost:9999/platforms/loyalty/admin/loyalty/analytics` |
|
||||
| WizaCorp Detail | `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/1` |
|
||||
| WizaCorp Settings | `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/1/settings` |
|
||||
| Fashion Group Detail | `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/2` |
|
||||
| Fashion Group Settings | `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/2/settings` |
|
||||
| BookWorld Detail | `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/3` |
|
||||
| BookWorld Settings | `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/3/settings` |
|
||||
|
||||
### 2. Merchant Owner / Store Pages
|
||||
|
||||
Login as the store owner, then navigate to any of their stores.
|
||||
|
||||
**WizaCorp (john.owner@wizacorp.com):**
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Terminal | `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/terminal` |
|
||||
| Cards | `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/cards` |
|
||||
| Settings | `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/settings` |
|
||||
| Stats | `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/stats` |
|
||||
| Enroll Customer | `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/enroll` |
|
||||
|
||||
**Fashion Group (jane.owner@fashiongroup.com):**
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Terminal | `http://localhost:9999/platforms/loyalty/store/FASHIONHUB/loyalty/terminal` |
|
||||
| Cards | `http://localhost:9999/platforms/loyalty/store/FASHIONHUB/loyalty/cards` |
|
||||
| Settings | `http://localhost:9999/platforms/loyalty/store/FASHIONHUB/loyalty/settings` |
|
||||
| Stats | `http://localhost:9999/platforms/loyalty/store/FASHIONHUB/loyalty/stats` |
|
||||
| Enroll Customer | `http://localhost:9999/platforms/loyalty/store/FASHIONHUB/loyalty/enroll` |
|
||||
|
||||
**BookWorld (bob.owner@bookworld.com):**
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Terminal | `http://localhost:9999/platforms/loyalty/store/BOOKSTORE/loyalty/terminal` |
|
||||
| Cards | `http://localhost:9999/platforms/loyalty/store/BOOKSTORE/loyalty/cards` |
|
||||
| Settings | `http://localhost:9999/platforms/loyalty/store/BOOKSTORE/loyalty/settings` |
|
||||
| Stats | `http://localhost:9999/platforms/loyalty/store/BOOKSTORE/loyalty/stats` |
|
||||
| Enroll Customer | `http://localhost:9999/platforms/loyalty/store/BOOKSTORE/loyalty/enroll` |
|
||||
|
||||
### 3. Customer Storefront Pages
|
||||
|
||||
Login as a customer (e.g., `customer1@orion.example.com`).
|
||||
|
||||
!!! note "Store domain required"
|
||||
Storefront pages require a store domain context. Only ORION (`orion.shop`)
|
||||
and FASHIONHUB (`fashionhub.store`) have domains configured. In dev, storefront
|
||||
routes may need to be accessed through the store's domain or platform path.
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Loyalty Dashboard | `http://localhost:9999/platforms/loyalty/account/loyalty` |
|
||||
| Transaction History | `http://localhost:9999/platforms/loyalty/account/loyalty/history` |
|
||||
|
||||
### 4. Public Pages (No Auth)
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Self-Enrollment | `http://localhost:9999/platforms/loyalty/loyalty/join` |
|
||||
| Enrollment Success | `http://localhost:9999/platforms/loyalty/loyalty/join/success` |
|
||||
|
||||
### 5. API Endpoints
|
||||
|
||||
**Admin API** (prefix: `/platforms/loyalty/api/admin/loyalty/`):
|
||||
|
||||
| Method | Dev URL |
|
||||
|--------|---------|
|
||||
| GET | `http://localhost:9999/platforms/loyalty/api/admin/loyalty/programs` |
|
||||
| GET | `http://localhost:9999/platforms/loyalty/api/admin/loyalty/stats` |
|
||||
|
||||
**Store API** (prefix: `/platforms/loyalty/api/store/loyalty/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | program | `http://localhost:9999/platforms/loyalty/api/store/loyalty/program` |
|
||||
| POST | program | `http://localhost:9999/platforms/loyalty/api/store/loyalty/program` |
|
||||
| POST | stamp | `http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp` |
|
||||
| POST | points | `http://localhost:9999/platforms/loyalty/api/store/loyalty/points` |
|
||||
| POST | enroll | `http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/enroll` |
|
||||
| POST | lookup | `http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/lookup` |
|
||||
|
||||
**Storefront API** (prefix: `/platforms/loyalty/api/storefront/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | program | `http://localhost:9999/platforms/loyalty/api/storefront/loyalty/program` |
|
||||
| POST | enroll | `http://localhost:9999/platforms/loyalty/api/storefront/loyalty/enroll` |
|
||||
| GET | card | `http://localhost:9999/platforms/loyalty/api/storefront/loyalty/card` |
|
||||
| GET | transactions | `http://localhost:9999/platforms/loyalty/api/storefront/loyalty/transactions` |
|
||||
|
||||
**Public API** (prefix: `/platforms/loyalty/api/loyalty/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | program | `http://localhost:9999/platforms/loyalty/api/loyalty/programs/ORION` |
|
||||
|
||||
---
|
||||
|
||||
## Production URLs (rewardflow.lu)
|
||||
|
||||
In production, the platform uses **domain-based routing** instead of the `/platforms/loyalty/` path prefix.
|
||||
Store context is detected via **custom domains** (registered in `store_domains` table)
|
||||
or **subdomains** of `rewardflow.lu` (from `Store.subdomain`).
|
||||
|
||||
### URL Routing Summary
|
||||
|
||||
| Routing mode | Priority | Pattern | Example |
|
||||
|-------------|----------|---------|---------|
|
||||
| Platform domain | — | `rewardflow.lu/...` | Admin pages, public API |
|
||||
| Store custom domain | 1 (highest) | `{custom_domain}/...` | Store with its own domain (overrides merchant domain) |
|
||||
| Merchant domain | 2 | `{merchant_domain}/...` | All stores inherit merchant's domain |
|
||||
| Store subdomain | 3 (fallback) | `{store_code}.rewardflow.lu/...` | Default when no custom/merchant domain |
|
||||
|
||||
!!! info "Domain Resolution Priority"
|
||||
When a request arrives, the middleware resolves the store in this order:
|
||||
|
||||
1. **Store custom domain** (`store_domains` table) — highest priority, store-specific override
|
||||
2. **Merchant domain** (`merchant_domains` table) — inherited by all merchant's stores
|
||||
3. **Store subdomain** (`Store.subdomain` + platform domain) — fallback
|
||||
|
||||
### Case 1: Store with custom domain (e.g., `orion.shop`)
|
||||
|
||||
The store has a verified entry in the `store_domains` table. **All** store URLs
|
||||
(storefront, store backend, store APIs) are served from the custom domain.
|
||||
|
||||
**Storefront (customer-facing):**
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| Loyalty Dashboard | `https://orion.shop/account/loyalty` |
|
||||
| Transaction History | `https://orion.shop/account/loyalty/history` |
|
||||
| Self-Enrollment | `https://orion.shop/loyalty/join` |
|
||||
| Enrollment Success | `https://orion.shop/loyalty/join/success` |
|
||||
|
||||
**Storefront API:**
|
||||
|
||||
| Method | Production URL |
|
||||
|--------|----------------|
|
||||
| GET card | `https://orion.shop/api/storefront/loyalty/card` |
|
||||
| GET transactions | `https://orion.shop/api/storefront/loyalty/transactions` |
|
||||
| POST enroll | `https://orion.shop/api/storefront/loyalty/enroll` |
|
||||
| GET program | `https://orion.shop/api/storefront/loyalty/program` |
|
||||
|
||||
**Store backend (staff/owner):**
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| Store Login | `https://orion.shop/store/ORION/login` |
|
||||
| Terminal | `https://orion.shop/store/ORION/loyalty/terminal` |
|
||||
| Cards | `https://orion.shop/store/ORION/loyalty/cards` |
|
||||
| Card Detail | `https://orion.shop/store/ORION/loyalty/cards/{card_id}` |
|
||||
| Settings | `https://orion.shop/store/ORION/loyalty/settings` |
|
||||
| Stats | `https://orion.shop/store/ORION/loyalty/stats` |
|
||||
| Enroll Customer | `https://orion.shop/store/ORION/loyalty/enroll` |
|
||||
|
||||
**Store API:**
|
||||
|
||||
| Method | Production URL |
|
||||
|--------|----------------|
|
||||
| GET program | `https://orion.shop/api/store/loyalty/program` |
|
||||
| POST program | `https://orion.shop/api/store/loyalty/program` |
|
||||
| POST stamp | `https://orion.shop/api/store/loyalty/stamp` |
|
||||
| POST points | `https://orion.shop/api/store/loyalty/points` |
|
||||
| POST enroll | `https://orion.shop/api/store/loyalty/cards/enroll` |
|
||||
| POST lookup | `https://orion.shop/api/store/loyalty/cards/lookup` |
|
||||
|
||||
### Case 2: Store with merchant domain (e.g., `myloyaltyprogram.lu`)
|
||||
|
||||
The merchant has registered a domain in the `merchant_domains` table. Stores without
|
||||
their own custom domain inherit the merchant domain. The middleware resolves the
|
||||
merchant domain to the merchant's first active store by default, or to a specific
|
||||
store when the URL includes `/store/{store_code}/...`.
|
||||
|
||||
**Storefront (customer-facing):**
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| Loyalty Dashboard | `https://myloyaltyprogram.lu/account/loyalty` |
|
||||
| Transaction History | `https://myloyaltyprogram.lu/account/loyalty/history` |
|
||||
| Self-Enrollment | `https://myloyaltyprogram.lu/loyalty/join` |
|
||||
| Enrollment Success | `https://myloyaltyprogram.lu/loyalty/join/success` |
|
||||
|
||||
**Storefront API:**
|
||||
|
||||
| Method | Production URL |
|
||||
|--------|----------------|
|
||||
| GET card | `https://myloyaltyprogram.lu/api/storefront/loyalty/card` |
|
||||
| GET transactions | `https://myloyaltyprogram.lu/api/storefront/loyalty/transactions` |
|
||||
| POST enroll | `https://myloyaltyprogram.lu/api/storefront/loyalty/enroll` |
|
||||
| GET program | `https://myloyaltyprogram.lu/api/storefront/loyalty/program` |
|
||||
|
||||
**Store backend (staff/owner):**
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| Store Login | `https://myloyaltyprogram.lu/store/WIZAGADGETS/login` |
|
||||
| Terminal | `https://myloyaltyprogram.lu/store/WIZAGADGETS/loyalty/terminal` |
|
||||
| Cards | `https://myloyaltyprogram.lu/store/WIZAGADGETS/loyalty/cards` |
|
||||
| Settings | `https://myloyaltyprogram.lu/store/WIZAGADGETS/loyalty/settings` |
|
||||
| Stats | `https://myloyaltyprogram.lu/store/WIZAGADGETS/loyalty/stats` |
|
||||
|
||||
**Store API:**
|
||||
|
||||
| Method | Production URL |
|
||||
|--------|----------------|
|
||||
| GET program | `https://myloyaltyprogram.lu/api/store/loyalty/program` |
|
||||
| POST stamp | `https://myloyaltyprogram.lu/api/store/loyalty/stamp` |
|
||||
| POST points | `https://myloyaltyprogram.lu/api/store/loyalty/points` |
|
||||
| POST enroll | `https://myloyaltyprogram.lu/api/store/loyalty/cards/enroll` |
|
||||
| POST lookup | `https://myloyaltyprogram.lu/api/store/loyalty/cards/lookup` |
|
||||
|
||||
!!! note "Merchant domain resolves to first active store"
|
||||
When a customer visits `myloyaltyprogram.lu` without a `/store/{code}/...` path,
|
||||
the middleware resolves to the merchant's **first active store** (ordered by ID).
|
||||
This is ideal for storefront pages like `/loyalty/join` where the customer doesn't
|
||||
need to know which specific store they're interacting with.
|
||||
|
||||
### Case 3: Store without custom domain (uses platform subdomain)
|
||||
|
||||
The store has no entry in `store_domains` and the merchant has no registered domain.
|
||||
**All** store URLs are served via a subdomain of the platform domain: `{store_code}.rewardflow.lu`.
|
||||
|
||||
**Storefront (customer-facing):**
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| Loyalty Dashboard | `https://bookstore.rewardflow.lu/account/loyalty` |
|
||||
| Transaction History | `https://bookstore.rewardflow.lu/account/loyalty/history` |
|
||||
| Self-Enrollment | `https://bookstore.rewardflow.lu/loyalty/join` |
|
||||
| Enrollment Success | `https://bookstore.rewardflow.lu/loyalty/join/success` |
|
||||
|
||||
**Storefront API:**
|
||||
|
||||
| Method | Production URL |
|
||||
|--------|----------------|
|
||||
| GET card | `https://bookstore.rewardflow.lu/api/storefront/loyalty/card` |
|
||||
| GET transactions | `https://bookstore.rewardflow.lu/api/storefront/loyalty/transactions` |
|
||||
| POST enroll | `https://bookstore.rewardflow.lu/api/storefront/loyalty/enroll` |
|
||||
| GET program | `https://bookstore.rewardflow.lu/api/storefront/loyalty/program` |
|
||||
|
||||
**Store backend (staff/owner):**
|
||||
|
||||
| Page | Production URL |
|
||||
|------|----------------|
|
||||
| Store Login | `https://bookstore.rewardflow.lu/store/BOOKSTORE/login` |
|
||||
| Terminal | `https://bookstore.rewardflow.lu/store/BOOKSTORE/loyalty/terminal` |
|
||||
| Cards | `https://bookstore.rewardflow.lu/store/BOOKSTORE/loyalty/cards` |
|
||||
| Settings | `https://bookstore.rewardflow.lu/store/BOOKSTORE/loyalty/settings` |
|
||||
| Stats | `https://bookstore.rewardflow.lu/store/BOOKSTORE/loyalty/stats` |
|
||||
|
||||
**Store API:**
|
||||
|
||||
| Method | Production URL |
|
||||
|--------|----------------|
|
||||
| GET program | `https://bookstore.rewardflow.lu/api/store/loyalty/program` |
|
||||
| POST stamp | `https://bookstore.rewardflow.lu/api/store/loyalty/stamp` |
|
||||
| POST points | `https://bookstore.rewardflow.lu/api/store/loyalty/points` |
|
||||
| POST enroll | `https://bookstore.rewardflow.lu/api/store/loyalty/cards/enroll` |
|
||||
| POST lookup | `https://bookstore.rewardflow.lu/api/store/loyalty/cards/lookup` |
|
||||
|
||||
### Platform Admin & Public API (always on platform domain)
|
||||
|
||||
| Page / Endpoint | Production URL |
|
||||
|-----------------|----------------|
|
||||
| Admin Programs | `https://rewardflow.lu/admin/loyalty/programs` |
|
||||
| Admin Analytics | `https://rewardflow.lu/admin/loyalty/analytics` |
|
||||
| Admin Merchant Detail | `https://rewardflow.lu/admin/loyalty/merchants/{id}` |
|
||||
| Admin Merchant Settings | `https://rewardflow.lu/admin/loyalty/merchants/{id}/settings` |
|
||||
| Admin API - Programs | `GET https://rewardflow.lu/api/admin/loyalty/programs` |
|
||||
| Admin API - Stats | `GET https://rewardflow.lu/api/admin/loyalty/stats` |
|
||||
| Public API - Program | `GET https://rewardflow.lu/api/loyalty/programs/ORION` |
|
||||
| Apple Wallet Pass | `GET https://rewardflow.lu/api/loyalty/passes/apple/{serial}.pkpass` |
|
||||
|
||||
### Domain configuration per store (current DB state)
|
||||
|
||||
**Merchant domains** (`merchant_domains` table):
|
||||
|
||||
| Merchant | Merchant Domain | Status |
|
||||
|----------|-----------------|--------|
|
||||
| WizaCorp Ltd. | _(none yet)_ | — |
|
||||
| Fashion Group S.A. | _(none yet)_ | — |
|
||||
| BookWorld Publishing | _(none yet)_ | — |
|
||||
|
||||
**Store domains** (`store_domains` table) and effective resolution:
|
||||
|
||||
| Store | Merchant | Store Custom Domain | Effective Domain |
|
||||
|-------|----------|---------------------|------------------|
|
||||
| ORION | WizaCorp | `orion.shop` | `orion.shop` (store override) |
|
||||
| FASHIONHUB | Fashion Group | `fashionhub.store` | `fashionhub.store` (store override) |
|
||||
| WIZAGADGETS | WizaCorp | _(none)_ | `wizagadgets.rewardflow.lu` (subdomain fallback) |
|
||||
| WIZAHOME | WizaCorp | _(none)_ | `wizahome.rewardflow.lu` (subdomain fallback) |
|
||||
| FASHIONOUTLET | Fashion Group | _(none)_ | `fashionoutlet.rewardflow.lu` (subdomain fallback) |
|
||||
| BOOKSTORE | BookWorld | _(none)_ | `bookstore.rewardflow.lu` (subdomain fallback) |
|
||||
| BOOKDIGITAL | BookWorld | _(none)_ | `bookdigital.rewardflow.lu` (subdomain fallback) |
|
||||
|
||||
!!! example "After merchant domain registration"
|
||||
If WizaCorp registers `myloyaltyprogram.lu` as their merchant domain, the table becomes:
|
||||
|
||||
| Store | Effective Domain | Reason |
|
||||
|-------|------------------|--------|
|
||||
| ORION | `orion.shop` | Store custom domain takes priority |
|
||||
| WIZAGADGETS | `myloyaltyprogram.lu` | Inherits merchant domain |
|
||||
| WIZAHOME | `myloyaltyprogram.lu` | Inherits merchant domain |
|
||||
|
||||
!!! info "`{store_domain}` in journey URLs"
|
||||
In the journeys below, `{store_domain}` refers to the store's **effective domain**, resolved in priority order:
|
||||
|
||||
1. **Store custom domain**: `orion.shop` (from `store_domains` table) — highest priority
|
||||
2. **Merchant domain**: `myloyaltyprogram.lu` (from `merchant_domains` table) — inherited default
|
||||
3. **Subdomain fallback**: `orion.rewardflow.lu` (from `Store.subdomain` + platform domain)
|
||||
|
||||
---
|
||||
|
||||
## User Journeys
|
||||
|
||||
### Journey 0: Merchant Subscription & Domain Setup
|
||||
|
||||
**Persona:** Merchant Owner (e.g., john.owner@wizacorp.com) + Platform Admin
|
||||
**Goal:** Subscribe to the loyalty platform, register a merchant domain, and optionally configure store domain overrides
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Merchant owner logs in] --> B[Navigate to billing page]
|
||||
B --> C[Choose subscription tier]
|
||||
C --> D[Complete Stripe checkout]
|
||||
D --> E[Subscription active]
|
||||
E --> F{Register merchant domain?}
|
||||
F -->|Yes| G[Admin registers merchant domain]
|
||||
G --> H[Verify DNS ownership]
|
||||
H --> I[Activate merchant domain]
|
||||
I --> J{Store-specific override?}
|
||||
J -->|Yes| K[Register store custom domain]
|
||||
K --> L[Verify & activate store domain]
|
||||
J -->|No| M[All stores inherit merchant domain]
|
||||
F -->|No| N[Stores use subdomain fallback]
|
||||
L --> O[Domain setup complete]
|
||||
M --> O
|
||||
N --> O
|
||||
```
|
||||
|
||||
**Step 1: Subscribe to the platform**
|
||||
|
||||
1. Login as `john.owner@wizacorp.com` and navigate to billing:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/billing`
|
||||
- Prod (custom domain): `https://orion.shop/store/ORION/billing`
|
||||
- Prod (subdomain): `https://orion.rewardflow.lu/store/ORION/billing`
|
||||
2. View available subscription tiers:
|
||||
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/v1/store/billing/tiers`
|
||||
- API Prod: `GET https://{store_domain}/api/v1/store/billing/tiers`
|
||||
3. Select a tier and initiate Stripe checkout:
|
||||
- API Dev: `POST http://localhost:9999/platforms/loyalty/api/v1/store/billing/checkout`
|
||||
- API Prod: `POST https://{store_domain}/api/v1/store/billing/checkout`
|
||||
4. Complete payment on Stripe checkout page
|
||||
5. Webhook `checkout.session.completed` activates the subscription
|
||||
6. Verify subscription is active:
|
||||
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/v1/store/billing/subscription`
|
||||
- API Prod: `GET https://{store_domain}/api/v1/store/billing/subscription`
|
||||
|
||||
**Step 2: Register merchant domain (admin action)**
|
||||
|
||||
!!! note "Admin-only operation"
|
||||
Merchant domain registration is currently an admin operation. The platform admin
|
||||
registers the domain on behalf of the merchant via the admin API.
|
||||
|
||||
1. Platform admin registers a merchant domain:
|
||||
- API Dev: `POST http://localhost:9999/platforms/loyalty/api/v1/admin/merchants/{merchant_id}/domains`
|
||||
- API Prod: `POST https://rewardflow.lu/api/v1/admin/merchants/{merchant_id}/domains`
|
||||
- Body: `{"domain": "myloyaltyprogram.lu", "is_primary": true}`
|
||||
2. The API returns a `verification_token` for DNS verification
|
||||
3. Get DNS verification instructions:
|
||||
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/v1/admin/merchants/domains/merchant/{domain_id}/verification-instructions`
|
||||
- API Prod: `GET https://rewardflow.lu/api/v1/admin/merchants/domains/merchant/{domain_id}/verification-instructions`
|
||||
4. Merchant adds a DNS TXT record: `_orion-verify.myloyaltyprogram.lu TXT {verification_token}`
|
||||
5. Verify the domain:
|
||||
- API Dev: `POST http://localhost:9999/platforms/loyalty/api/v1/admin/merchants/domains/merchant/{domain_id}/verify`
|
||||
- API Prod: `POST https://rewardflow.lu/api/v1/admin/merchants/domains/merchant/{domain_id}/verify`
|
||||
6. Activate the domain:
|
||||
- API Dev: `PUT http://localhost:9999/platforms/loyalty/api/v1/admin/merchants/domains/merchant/{domain_id}`
|
||||
- API Prod: `PUT https://rewardflow.lu/api/v1/admin/merchants/domains/merchant/{domain_id}`
|
||||
- Body: `{"is_active": true}`
|
||||
7. All merchant stores now inherit `myloyaltyprogram.lu` as their effective domain
|
||||
|
||||
**Step 3: (Optional) Register store-specific domain override**
|
||||
|
||||
If a store needs its own domain (e.g., ORION is a major brand and wants `mysuperloyaltyprogram.lu`):
|
||||
|
||||
1. Platform admin registers a store domain:
|
||||
- API Dev: `POST http://localhost:9999/platforms/loyalty/api/v1/admin/stores/{store_id}/domains`
|
||||
- API Prod: `POST https://rewardflow.lu/api/v1/admin/stores/{store_id}/domains`
|
||||
- Body: `{"domain": "mysuperloyaltyprogram.lu", "is_primary": true}`
|
||||
2. Follow the same DNS verification and activation flow as merchant domains
|
||||
3. Once active, this store's effective domain becomes `mysuperloyaltyprogram.lu` (overrides merchant domain)
|
||||
4. Other stores (WIZAGADGETS, WIZAHOME) continue to use `myloyaltyprogram.lu`
|
||||
|
||||
**Result after domain setup for WizaCorp:**
|
||||
|
||||
| Store | Effective Domain | Source |
|
||||
|-------|------------------|--------|
|
||||
| ORION | `mysuperloyaltyprogram.lu` | Store custom domain (override) |
|
||||
| WIZAGADGETS | `myloyaltyprogram.lu` | Merchant domain (inherited) |
|
||||
| WIZAHOME | `myloyaltyprogram.lu` | Merchant domain (inherited) |
|
||||
|
||||
**Expected blockers in current state:**
|
||||
|
||||
- No subscriptions exist yet - create one first via billing page or admin API
|
||||
- No merchant domains registered - admin must register via API
|
||||
- DNS verification requires actual DNS records (mock in tests)
|
||||
|
||||
---
|
||||
|
||||
### Journey 1: Merchant Owner - First-Time Setup
|
||||
|
||||
**Persona:** Merchant Owner (e.g., john.owner@wizacorp.com)
|
||||
**Goal:** Set up a loyalty program for their merchant
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Login as store owner] --> B[Navigate to store loyalty settings]
|
||||
B --> C{Program exists?}
|
||||
C -->|No| D[Create loyalty program]
|
||||
D --> E[Choose type: stamps / points / hybrid]
|
||||
E --> F[Configure program settings]
|
||||
F --> G[Set branding - colors, logo]
|
||||
G --> H[Configure anti-fraud settings]
|
||||
H --> I[Create staff PINs]
|
||||
I --> J[Program is live]
|
||||
C -->|Yes| K[View/edit existing program]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Login as `john.owner@wizacorp.com` at:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/login`
|
||||
- Prod (custom domain): `https://orion.shop/store/ORION/login`
|
||||
- Prod (subdomain): `https://orion.rewardflow.lu/store/ORION/login`
|
||||
2. Navigate to loyalty settings:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/settings`
|
||||
- Prod (custom domain): `https://orion.shop/store/ORION/loyalty/settings`
|
||||
- Prod (subdomain): `https://orion.rewardflow.lu/store/ORION/loyalty/settings`
|
||||
3. Create a new loyalty program:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/program`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/program`
|
||||
4. Choose loyalty type (stamps, points, or hybrid)
|
||||
5. Configure program parameters (stamp target, points-per-euro, rewards)
|
||||
6. Set branding (card color, logo, hero image)
|
||||
7. Configure anti-fraud (cooldown, daily limits, PIN requirements)
|
||||
8. Create staff PINs:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/pins`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/pins`
|
||||
9. Verify program is live - check from another store (same merchant):
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/WIZAGADGETS/loyalty/settings`
|
||||
- Prod (subdomain): `https://wizagadgets.rewardflow.lu/store/WIZAGADGETS/loyalty/settings`
|
||||
|
||||
**Expected blockers in current state:**
|
||||
|
||||
- No loyalty programs exist - this is the first journey to test
|
||||
|
||||
!!! note "Subscription is not required for program creation"
|
||||
The loyalty module currently has **no feature gating** — program creation works
|
||||
without an active subscription. Journey 0 (subscription & domain setup) is
|
||||
independent and can be done before or after program creation. However, in production
|
||||
you would typically subscribe first to get a custom domain for your loyalty URLs.
|
||||
|
||||
---
|
||||
|
||||
### Journey 2: Store Staff - Daily Operations (Stamps)
|
||||
|
||||
**Persona:** Store Staff (e.g., alice.manager@wizacorp.com)
|
||||
**Goal:** Process customer loyalty stamp transactions
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Open terminal] --> B[Customer presents card/QR]
|
||||
B --> C[Scan/lookup card]
|
||||
C --> D[Enter staff PIN]
|
||||
D --> E[Add stamp]
|
||||
E --> F{Target reached?}
|
||||
F -->|Yes| G[Prompt: Redeem reward?]
|
||||
G -->|Yes| H[Redeem stamps for reward]
|
||||
G -->|No| I[Save for later]
|
||||
F -->|No| J[Done - show updated count]
|
||||
H --> J
|
||||
I --> J
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Login as `alice.manager@wizacorp.com` and open the terminal:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/terminal`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/terminal`
|
||||
2. Scan customer QR code or enter card number:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/lookup`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/cards/lookup`
|
||||
3. Enter staff PIN for verification
|
||||
4. Add stamp:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/stamp`
|
||||
5. If target reached, redeem reward:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp/redeem`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/stamp/redeem`
|
||||
6. View updated card:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/cards/{card_id}`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/cards/{card_id}`
|
||||
7. Browse all cards:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/cards`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/cards`
|
||||
|
||||
**Anti-fraud scenarios to test:**
|
||||
|
||||
- Cooldown rejection (stamp within 15 min)
|
||||
- Daily limit hit (max 5 stamps/day)
|
||||
- PIN lockout (5 failed attempts)
|
||||
|
||||
---
|
||||
|
||||
### Journey 3: Store Staff - Daily Operations (Points)
|
||||
|
||||
**Persona:** Store Staff (e.g., alice.manager@wizacorp.com)
|
||||
**Goal:** Process customer loyalty points from purchase
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Open terminal] --> B[Customer presents card]
|
||||
B --> C[Scan/lookup card]
|
||||
C --> D[Enter purchase amount]
|
||||
D --> E[Enter staff PIN]
|
||||
E --> F[Points calculated & added]
|
||||
F --> G{Enough for reward?}
|
||||
G -->|Yes| H[Offer redemption]
|
||||
G -->|No| I[Done - show balance]
|
||||
H --> I
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Open the terminal:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/terminal`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/terminal`
|
||||
2. Lookup card:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/lookup`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/cards/lookup`
|
||||
3. Enter purchase amount (e.g., 25.00 EUR)
|
||||
4. Earn points (auto-calculated at 10 pts/EUR = 250 points):
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/points`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/points`
|
||||
5. If enough balance, redeem points for reward:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/points/redeem`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/points/redeem`
|
||||
6. Check store-level stats:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/stats`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/stats`
|
||||
|
||||
---
|
||||
|
||||
### Journey 4: Customer Self-Enrollment
|
||||
|
||||
**Persona:** Anonymous Customer
|
||||
**Goal:** Join a merchant's loyalty program
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[See QR code at store counter] --> B[Scan QR / visit enrollment page]
|
||||
B --> C[Fill in details - email, name]
|
||||
C --> D[Submit enrollment]
|
||||
D --> E[Receive card number]
|
||||
E --> F[Optional: Add to Apple/Google Wallet]
|
||||
F --> G[Start collecting stamps/points]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Visit the public enrollment page:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/loyalty/join`
|
||||
- Prod (custom domain): `https://orion.shop/loyalty/join`
|
||||
- Prod (subdomain): `https://bookstore.rewardflow.lu/loyalty/join`
|
||||
2. Fill in enrollment form (email, name)
|
||||
3. Submit enrollment:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/storefront/loyalty/enroll`
|
||||
- Prod (custom domain): `POST https://orion.shop/api/storefront/loyalty/enroll`
|
||||
- Prod (subdomain): `POST https://bookstore.rewardflow.lu/api/storefront/loyalty/enroll`
|
||||
4. Redirected to success page:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/loyalty/join/success?card=XXXX-XXXX-XXXX`
|
||||
- Prod (custom domain): `https://orion.shop/loyalty/join/success?card=XXXX-XXXX-XXXX`
|
||||
- Prod (subdomain): `https://bookstore.rewardflow.lu/loyalty/join/success?card=XXXX-XXXX-XXXX`
|
||||
5. Optionally download Apple Wallet pass:
|
||||
- Dev: `GET http://localhost:9999/platforms/loyalty/api/loyalty/passes/apple/{serial_number}.pkpass`
|
||||
- Prod: `GET https://rewardflow.lu/api/loyalty/passes/apple/{serial_number}.pkpass`
|
||||
|
||||
---
|
||||
|
||||
### Journey 5: Customer - View Loyalty Status
|
||||
|
||||
**Persona:** Authenticated Customer (e.g., `customer1@orion.example.com`)
|
||||
**Goal:** Check loyalty balance and history
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Login as customer at the storefront
|
||||
2. View loyalty dashboard (card balance, available rewards):
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/account/loyalty`
|
||||
- Prod (custom domain): `https://orion.shop/account/loyalty`
|
||||
- Prod (subdomain): `https://bookstore.rewardflow.lu/account/loyalty`
|
||||
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/storefront/loyalty/card`
|
||||
- API Prod: `GET https://orion.shop/api/storefront/loyalty/card`
|
||||
3. View full transaction history:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/account/loyalty/history`
|
||||
- Prod (custom domain): `https://orion.shop/account/loyalty/history`
|
||||
- Prod (subdomain): `https://bookstore.rewardflow.lu/account/loyalty/history`
|
||||
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/storefront/loyalty/transactions`
|
||||
- API Prod: `GET https://orion.shop/api/storefront/loyalty/transactions`
|
||||
|
||||
---
|
||||
|
||||
### Journey 6: Platform Admin - Oversight
|
||||
|
||||
**Persona:** Platform Admin (`admin@orion.lu` or `samir.boulahtit@gmail.com`)
|
||||
**Goal:** Monitor all loyalty programs across merchants
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Login as admin
|
||||
2. View all programs:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/programs`
|
||||
- Prod: `https://rewardflow.lu/admin/loyalty/programs`
|
||||
3. View platform-wide analytics:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/analytics`
|
||||
- Prod: `https://rewardflow.lu/admin/loyalty/analytics`
|
||||
4. Drill into WizaCorp's program:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/1`
|
||||
- Prod: `https://rewardflow.lu/admin/loyalty/merchants/1`
|
||||
5. Manage WizaCorp's merchant-level settings:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/1/settings`
|
||||
- Prod: `https://rewardflow.lu/admin/loyalty/merchants/1/settings`
|
||||
- API Dev: `PATCH http://localhost:9999/platforms/loyalty/api/admin/loyalty/merchants/1/settings`
|
||||
- API Prod: `PATCH https://rewardflow.lu/api/admin/loyalty/merchants/1/settings`
|
||||
6. Adjust settings: PIN policy, self-enrollment toggle, void permissions
|
||||
7. Check other merchants:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/2`
|
||||
- Prod: `https://rewardflow.lu/admin/loyalty/merchants/2`
|
||||
|
||||
---
|
||||
|
||||
### Journey 7: Void / Return Flow
|
||||
|
||||
**Persona:** Store Staff (e.g., alice.manager@wizacorp.com)
|
||||
**Goal:** Reverse a loyalty transaction (customer return)
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Open terminal and lookup card:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/terminal`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/terminal`
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/lookup`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/cards/lookup`
|
||||
2. View the card's transaction history to find the transaction to void:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/cards/{card_id}`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/cards/{card_id}`
|
||||
- API Dev: `GET http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/{card_id}/transactions`
|
||||
- API Prod: `GET https://{store_domain}/api/store/loyalty/cards/{card_id}/transactions`
|
||||
3. Void a stamp transaction:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp/void`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/stamp/void`
|
||||
4. Or void a points transaction:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/points/void`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/points/void`
|
||||
5. Verify: original and void transactions are linked in the audit log
|
||||
|
||||
---
|
||||
|
||||
### Journey 8: Cross-Store Redemption
|
||||
|
||||
**Persona:** Customer + Store Staff at two different stores
|
||||
**Goal:** Customer earns at Store A, redeems at Store B (same merchant)
|
||||
|
||||
**Precondition:** Cross-location redemption must be enabled in merchant settings:
|
||||
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/admin/loyalty/merchants/1/settings`
|
||||
- Prod: `https://rewardflow.lu/admin/loyalty/merchants/1/settings`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Staff at ORION adds stamps to customer's card:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/ORION/loyalty/terminal`
|
||||
- Prod: `https://{store_domain}/store/ORION/loyalty/terminal`
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/stamp`
|
||||
2. Customer visits WIZAGADGETS
|
||||
3. Staff at WIZAGADGETS looks up the same card:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/WIZAGADGETS/loyalty/terminal`
|
||||
- Prod: `https://{store_domain}/store/WIZAGADGETS/loyalty/terminal`
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/cards/lookup`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/cards/lookup`
|
||||
4. Card is found (same merchant) with accumulated stamps
|
||||
5. Staff at WIZAGADGETS redeems the reward:
|
||||
- Dev: `POST http://localhost:9999/platforms/loyalty/api/store/loyalty/stamp/redeem`
|
||||
- Prod: `POST https://{store_domain}/api/store/loyalty/stamp/redeem`
|
||||
6. Verify transaction history shows both stores:
|
||||
- Dev: `http://localhost:9999/platforms/loyalty/store/WIZAGADGETS/loyalty/cards/{card_id}`
|
||||
- Prod: `https://{store_domain}/store/WIZAGADGETS/loyalty/cards/{card_id}`
|
||||
|
||||
---
|
||||
|
||||
## Recommended Test Order
|
||||
|
||||
1. **Journey 1** - Create a program first (nothing else works without this)
|
||||
2. **Journey 0** - Subscribe and set up domains (independent, but needed for custom domain URLs)
|
||||
3. **Journey 4** - Enroll a test customer
|
||||
4. **Journey 2 or 3** - Process stamps/points
|
||||
5. **Journey 5** - Verify customer can see their data
|
||||
6. **Journey 7** - Test void/return
|
||||
7. **Journey 8** - Test cross-store (enroll via ORION, redeem via WIZAGADGETS)
|
||||
8. **Journey 6** - Admin overview (verify data appears correctly)
|
||||
|
||||
!!! tip "Journey 0 and Journey 1 are independent"
|
||||
There is no feature gating on loyalty program creation — you can test them in
|
||||
either order. Journey 0 is listed second because domain setup is about URL
|
||||
presentation, not a functional prerequisite for the loyalty module.
|
||||
This document has moved to the loyalty module docs: [User Journeys](../../modules/loyalty/user-journeys.md)
|
||||
|
||||
@@ -1,435 +1 @@
|
||||
# Prospecting Module - User Journeys
|
||||
|
||||
## Personas
|
||||
|
||||
| # | Persona | Role / Auth | Description |
|
||||
|---|---------|-------------|-------------|
|
||||
| 1 | **Platform Admin** | `admin` role | Manages prospects, runs enrichment scans, sends campaigns, exports leads |
|
||||
|
||||
!!! note "Admin-only module"
|
||||
The prospecting module is exclusively for platform admins. There are no store-level
|
||||
or customer-facing pages. All access requires admin authentication.
|
||||
|
||||
---
|
||||
|
||||
## Platforms Using Prospecting
|
||||
|
||||
The prospecting module is enabled on multiple platforms:
|
||||
|
||||
| Platform | Domain | Path Prefix (dev) |
|
||||
|----------|--------|--------------------|
|
||||
| HostWizard | hostwizard.lu | `/platforms/hosting/` |
|
||||
|
||||
---
|
||||
|
||||
## Dev URLs (localhost:9999)
|
||||
|
||||
The dev server uses path-based platform routing: `http://localhost:9999/platforms/hosting/...`
|
||||
|
||||
### 1. Admin Pages
|
||||
|
||||
Login as: `admin@orion.lu` or `samir.boulahtit@gmail.com`
|
||||
|
||||
| Page | Dev URL |
|
||||
|------|---------|
|
||||
| Dashboard | `http://localhost:9999/platforms/hosting/admin/prospecting` |
|
||||
| Prospects List | `http://localhost:9999/platforms/hosting/admin/prospecting/prospects` |
|
||||
| Prospect Detail | `http://localhost:9999/platforms/hosting/admin/prospecting/prospects/{prospect_id}` |
|
||||
| Leads List | `http://localhost:9999/platforms/hosting/admin/prospecting/leads` |
|
||||
| Quick Capture | `http://localhost:9999/platforms/hosting/admin/prospecting/capture` |
|
||||
| Scan Jobs | `http://localhost:9999/platforms/hosting/admin/prospecting/scan-jobs` |
|
||||
| Campaigns | `http://localhost:9999/platforms/hosting/admin/prospecting/campaigns` |
|
||||
|
||||
### 2. Admin API Endpoints
|
||||
|
||||
**Prospects** (prefix: `/platforms/hosting/api/admin/prospecting/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | prospects | `http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects` |
|
||||
| GET | prospect detail | `http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects/{id}` |
|
||||
| POST | create prospect | `http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects` |
|
||||
| PUT | update prospect | `http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects/{id}` |
|
||||
| DELETE | delete prospect | `http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects/{id}` |
|
||||
| POST | import CSV | `http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects/import` |
|
||||
|
||||
**Leads** (prefix: `/platforms/hosting/api/admin/prospecting/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | leads (filtered) | `http://localhost:9999/platforms/hosting/api/admin/prospecting/leads` |
|
||||
| GET | top priority | `http://localhost:9999/platforms/hosting/api/admin/prospecting/leads/top-priority` |
|
||||
| GET | quick wins | `http://localhost:9999/platforms/hosting/api/admin/prospecting/leads/quick-wins` |
|
||||
| GET | export CSV | `http://localhost:9999/platforms/hosting/api/admin/prospecting/leads/export/csv` |
|
||||
|
||||
**Enrichment** (prefix: `/platforms/hosting/api/admin/prospecting/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| POST | HTTP check (single) | `http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/http-check/{id}` |
|
||||
| POST | HTTP check (batch) | `http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/http-check/batch` |
|
||||
| POST | tech scan (single) | `http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/tech-scan/{id}` |
|
||||
| POST | tech scan (batch) | `http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/tech-scan/batch` |
|
||||
| POST | performance (single) | `http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/performance/{id}` |
|
||||
| POST | performance (batch) | `http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/performance/batch` |
|
||||
| POST | contacts (single) | `http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/contacts/{id}` |
|
||||
| POST | full enrichment | `http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/full/{id}` |
|
||||
| POST | score compute | `http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/score-compute/batch` |
|
||||
|
||||
**Interactions** (prefix: `/platforms/hosting/api/admin/prospecting/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | prospect interactions | `http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects/{id}/interactions` |
|
||||
| POST | log interaction | `http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects/{id}/interactions` |
|
||||
| GET | upcoming follow-ups | `http://localhost:9999/platforms/hosting/api/admin/prospecting/interactions/upcoming` |
|
||||
|
||||
**Campaigns** (prefix: `/platforms/hosting/api/admin/prospecting/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | list templates | `http://localhost:9999/platforms/hosting/api/admin/prospecting/campaigns/templates` |
|
||||
| POST | create template | `http://localhost:9999/platforms/hosting/api/admin/prospecting/campaigns/templates` |
|
||||
| PUT | update template | `http://localhost:9999/platforms/hosting/api/admin/prospecting/campaigns/templates/{id}` |
|
||||
| DELETE | delete template | `http://localhost:9999/platforms/hosting/api/admin/prospecting/campaigns/templates/{id}` |
|
||||
| POST | preview campaign | `http://localhost:9999/platforms/hosting/api/admin/prospecting/campaigns/preview` |
|
||||
| POST | send campaign | `http://localhost:9999/platforms/hosting/api/admin/prospecting/campaigns/send` |
|
||||
| GET | list sends | `http://localhost:9999/platforms/hosting/api/admin/prospecting/campaigns/sends` |
|
||||
|
||||
**Stats** (prefix: `/platforms/hosting/api/admin/prospecting/`):
|
||||
|
||||
| Method | Endpoint | Dev URL |
|
||||
|--------|----------|---------|
|
||||
| GET | dashboard stats | `http://localhost:9999/platforms/hosting/api/admin/prospecting/stats` |
|
||||
| GET | scan jobs | `http://localhost:9999/platforms/hosting/api/admin/prospecting/stats/jobs` |
|
||||
|
||||
---
|
||||
|
||||
## Production URLs (hostwizard.lu)
|
||||
|
||||
In production, the platform uses **domain-based routing**.
|
||||
|
||||
### Admin Pages & API
|
||||
|
||||
| Page / Endpoint | Production URL |
|
||||
|-----------------|----------------|
|
||||
| Dashboard | `https://hostwizard.lu/admin/prospecting` |
|
||||
| Prospects | `https://hostwizard.lu/admin/prospecting/prospects` |
|
||||
| Prospect Detail | `https://hostwizard.lu/admin/prospecting/prospects/{id}` |
|
||||
| Leads | `https://hostwizard.lu/admin/prospecting/leads` |
|
||||
| Quick Capture | `https://hostwizard.lu/admin/prospecting/capture` |
|
||||
| Scan Jobs | `https://hostwizard.lu/admin/prospecting/scan-jobs` |
|
||||
| Campaigns | `https://hostwizard.lu/admin/prospecting/campaigns` |
|
||||
| API - Prospects | `GET https://hostwizard.lu/api/admin/prospecting/prospects` |
|
||||
| API - Leads | `GET https://hostwizard.lu/api/admin/prospecting/leads` |
|
||||
| API - Stats | `GET https://hostwizard.lu/api/admin/prospecting/stats` |
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### Prospect
|
||||
|
||||
```
|
||||
Prospect
|
||||
├── id (PK)
|
||||
├── channel: DIGITAL | OFFLINE
|
||||
├── business_name (str)
|
||||
├── domain_name (str, unique)
|
||||
├── status: PENDING | ACTIVE | INACTIVE | PARKED | ERROR | CONTACTED | CONVERTED
|
||||
├── source (str)
|
||||
├── Digital fields: has_website, uses_https, http_status_code, redirect_url, scan timestamps
|
||||
├── Offline fields: address, city, postal_code, country, location_lat/lng, captured_by_user_id
|
||||
├── notes, tags (JSON)
|
||||
├── created_at, updated_at
|
||||
└── Relationships: tech_profile, performance_profile, score, contacts, interactions
|
||||
```
|
||||
|
||||
### Prospect Score (0-100)
|
||||
|
||||
```
|
||||
ProspectScore
|
||||
├── score (0-100, overall)
|
||||
├── Components: technical_health (max 40), modernity (max 25), business_value (max 25), engagement (max 10)
|
||||
├── reason_flags (JSON array)
|
||||
├── score_breakdown (JSON dict)
|
||||
└── lead_tier: top_priority | quick_win | strategic | low_priority
|
||||
```
|
||||
|
||||
### Status Flow
|
||||
|
||||
```
|
||||
PENDING
|
||||
↓ (HTTP check determines website status)
|
||||
ACTIVE (has website) or PARKED (no website / parked domain)
|
||||
↓ (contact attempt)
|
||||
CONTACTED
|
||||
↓ (outcome)
|
||||
CONVERTED (sale) or INACTIVE (not interested)
|
||||
|
||||
Alternative: PENDING → ERROR (invalid domain, technical issues)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Journeys
|
||||
|
||||
### Journey 1: Digital Lead Discovery (Domain Scanning)
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Import `.lu` domains, enrich them, and identify sales opportunities
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Import CSV of .lu domains] --> B[Prospects created with status PENDING]
|
||||
B --> C[Run HTTP check batch]
|
||||
C --> D[Run tech scan batch]
|
||||
D --> E[Run performance scan batch]
|
||||
E --> F[Run contact scrape]
|
||||
F --> G[Compute scores batch]
|
||||
G --> H[View scored leads]
|
||||
H --> I{Score tier?}
|
||||
I -->|>= 70: Top Priority| J[Export & contact immediately]
|
||||
I -->|50-69: Quick Win| K[Queue for campaign]
|
||||
I -->|30-49: Strategic| L[Monitor & nurture]
|
||||
I -->|< 30: Low Priority| M[Deprioritize]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Navigate to Dashboard:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/prospecting`
|
||||
- Prod: `https://hostwizard.lu/admin/prospecting`
|
||||
2. Import domains via CSV:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects/import`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/prospects/import`
|
||||
3. Run HTTP batch check:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/http-check/batch`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/enrichment/http-check/batch`
|
||||
4. Run tech scan batch:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/tech-scan/batch`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/enrichment/tech-scan/batch`
|
||||
5. Run performance scan batch:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/performance/batch`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/enrichment/performance/batch`
|
||||
6. Compute scores:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/score-compute/batch`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/enrichment/score-compute/batch`
|
||||
7. Monitor scan jobs:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/prospecting/scan-jobs`
|
||||
- Prod: `https://hostwizard.lu/admin/prospecting/scan-jobs`
|
||||
8. View scored leads:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/prospecting/leads`
|
||||
- Prod: `https://hostwizard.lu/admin/prospecting/leads`
|
||||
9. Export top priority leads:
|
||||
- API Dev: `GET http://localhost:9999/platforms/hosting/api/admin/prospecting/leads/export/csv?min_score=70`
|
||||
- API Prod: `GET https://hostwizard.lu/api/admin/prospecting/leads/export/csv?min_score=70`
|
||||
|
||||
---
|
||||
|
||||
### Journey 2: Offline Lead Capture
|
||||
|
||||
**Persona:** Platform Admin (out in the field)
|
||||
**Goal:** Capture business details from in-person encounters using the mobile-friendly capture form
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Meet business owner in-person] --> B[Open Quick Capture on mobile]
|
||||
B --> C[Enter business name, address, contact info]
|
||||
C --> D[Prospect created with channel=OFFLINE]
|
||||
D --> E{Has website?}
|
||||
E -->|Yes| F[Run full enrichment]
|
||||
E -->|No| G[Score based on business value only]
|
||||
F --> H[Prospect fully enriched with score]
|
||||
G --> H
|
||||
H --> I[Log interaction: VISIT]
|
||||
I --> J[Set follow-up date]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Open Quick Capture (mobile-friendly):
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/prospecting/capture`
|
||||
- Prod: `https://hostwizard.lu/admin/prospecting/capture`
|
||||
2. Fill in business details and submit:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/prospects`
|
||||
- Body includes: `channel: "offline"`, `business_name`, `address`, `city`, `postal_code`
|
||||
3. Optionally run full enrichment (if domain known):
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/full/{id}`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/enrichment/full/{id}`
|
||||
4. Log the interaction:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects/{id}/interactions`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/prospects/{id}/interactions`
|
||||
- Body: `{ "interaction_type": "visit", "notes": "Met at trade fair", "next_action": "Send proposal", "next_action_date": "2026-03-10" }`
|
||||
|
||||
---
|
||||
|
||||
### Journey 3: Lead Qualification & Export
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Filter enriched prospects by score tier and export qualified leads for outreach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Navigate to Leads page] --> B[Filter by score tier]
|
||||
B --> C{View preset lists}
|
||||
C -->|Top Priority| D[Score >= 70]
|
||||
C -->|Quick Wins| E[Score 50-69]
|
||||
C -->|Custom filter| F[Set min/max score, channel, contact type]
|
||||
D --> G[Review leads]
|
||||
E --> G
|
||||
F --> G
|
||||
G --> H[Export as CSV]
|
||||
H --> I[Use in campaigns or CRM]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Navigate to Leads:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/prospecting/leads`
|
||||
- Prod: `https://hostwizard.lu/admin/prospecting/leads`
|
||||
2. View top priority leads:
|
||||
- API Dev: `GET http://localhost:9999/platforms/hosting/api/admin/prospecting/leads/top-priority`
|
||||
- API Prod: `GET https://hostwizard.lu/api/admin/prospecting/leads/top-priority`
|
||||
3. View quick wins:
|
||||
- API Dev: `GET http://localhost:9999/platforms/hosting/api/admin/prospecting/leads/quick-wins`
|
||||
- API Prod: `GET https://hostwizard.lu/api/admin/prospecting/leads/quick-wins`
|
||||
4. Filter with custom parameters:
|
||||
- API Dev: `GET http://localhost:9999/platforms/hosting/api/admin/prospecting/leads?min_score=60&has_email=true&channel=digital`
|
||||
- API Prod: `GET https://hostwizard.lu/api/admin/prospecting/leads?min_score=60&has_email=true&channel=digital`
|
||||
5. Export filtered leads as CSV:
|
||||
- API Dev: `GET http://localhost:9999/platforms/hosting/api/admin/prospecting/leads/export/csv?min_score=50&lead_tier=quick_win`
|
||||
- API Prod: `GET https://hostwizard.lu/api/admin/prospecting/leads/export/csv?min_score=50&lead_tier=quick_win`
|
||||
|
||||
---
|
||||
|
||||
### Journey 4: Campaign Creation & Outreach
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Create email campaign templates and send targeted outreach to qualified prospects
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Navigate to Campaigns] --> B[Create campaign template]
|
||||
B --> C[Choose lead type: no_website, bad_website, etc.]
|
||||
C --> D[Write email template with variables]
|
||||
D --> E[Preview rendered for specific prospect]
|
||||
E --> F{Looks good?}
|
||||
F -->|Yes| G[Select qualifying leads]
|
||||
G --> H[Send campaign]
|
||||
H --> I[Monitor send status]
|
||||
F -->|No| J[Edit template]
|
||||
J --> E
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Navigate to Campaigns:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/prospecting/campaigns`
|
||||
- Prod: `https://hostwizard.lu/admin/prospecting/campaigns`
|
||||
2. Create a campaign template:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/campaigns/templates`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/campaigns/templates`
|
||||
- Body: `{ "name": "No Website Outreach", "lead_type": "no_website", "channel": "email", "language": "fr", "subject_template": "Votre presence en ligne", "body_template": "Bonjour {{business_name}}..." }`
|
||||
3. Preview the template for a specific prospect:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/campaigns/preview`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/campaigns/preview`
|
||||
- Body: `{ "template_id": 1, "prospect_id": 42 }`
|
||||
4. Send campaign to selected prospects:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/campaigns/send`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/campaigns/send`
|
||||
- Body: `{ "template_id": 1, "prospect_ids": [42, 43, 44] }`
|
||||
5. Monitor campaign sends:
|
||||
- API Dev: `GET http://localhost:9999/platforms/hosting/api/admin/prospecting/campaigns/sends`
|
||||
- API Prod: `GET https://hostwizard.lu/api/admin/prospecting/campaigns/sends`
|
||||
|
||||
---
|
||||
|
||||
### Journey 5: Interaction Tracking & Follow-ups
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Log interactions with prospects and track follow-up actions
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Open prospect detail] --> B[View interaction history]
|
||||
B --> C[Log new interaction]
|
||||
C --> D[Set next action & date]
|
||||
D --> E[View upcoming follow-ups]
|
||||
E --> F[Complete follow-up]
|
||||
F --> G{Positive outcome?}
|
||||
G -->|Yes| H[Mark as CONTACTED → CONVERTED]
|
||||
G -->|No| I[Schedule next follow-up or mark INACTIVE]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. View prospect detail:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/prospecting/prospects/{id}`
|
||||
- Prod: `https://hostwizard.lu/admin/prospecting/prospects/{id}`
|
||||
2. View interactions:
|
||||
- API Dev: `GET http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects/{id}/interactions`
|
||||
- API Prod: `GET https://hostwizard.lu/api/admin/prospecting/prospects/{id}/interactions`
|
||||
3. Log a new interaction:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/prospects/{id}/interactions`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/prospects/{id}/interactions`
|
||||
- Body: `{ "interaction_type": "call", "subject": "Follow-up call", "outcome": "positive", "next_action": "Send proposal", "next_action_date": "2026-03-15" }`
|
||||
4. View upcoming follow-ups across all prospects:
|
||||
- API Dev: `GET http://localhost:9999/platforms/hosting/api/admin/prospecting/interactions/upcoming`
|
||||
- API Prod: `GET https://hostwizard.lu/api/admin/prospecting/interactions/upcoming`
|
||||
|
||||
---
|
||||
|
||||
### Journey 6: Full Enrichment Pipeline (Single Prospect)
|
||||
|
||||
**Persona:** Platform Admin
|
||||
**Goal:** Run the complete enrichment pipeline for a single prospect to get all data at once
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Open prospect detail] --> B[Click 'Full Enrichment']
|
||||
B --> C[Step 1: HTTP check]
|
||||
C --> D{Has website?}
|
||||
D -->|Yes| E[Step 2: Tech scan]
|
||||
D -->|No| H[Step 5: Compute score]
|
||||
E --> F[Step 3: Performance audit]
|
||||
F --> G[Step 4: Contact scrape]
|
||||
G --> H
|
||||
H --> I[Prospect fully enriched]
|
||||
I --> J[View score & breakdown]
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Run full enrichment for a prospect:
|
||||
- API Dev: `POST http://localhost:9999/platforms/hosting/api/admin/prospecting/enrichment/full/{prospect_id}`
|
||||
- API Prod: `POST https://hostwizard.lu/api/admin/prospecting/enrichment/full/{prospect_id}`
|
||||
2. View the enriched prospect detail:
|
||||
- Dev: `http://localhost:9999/platforms/hosting/admin/prospecting/prospects/{prospect_id}`
|
||||
- Prod: `https://hostwizard.lu/admin/prospecting/prospects/{prospect_id}`
|
||||
|
||||
The full enrichment runs 5 sequential steps:
|
||||
|
||||
1. **HTTP check** — Verifies domain connectivity, checks HTTPS, records redirects
|
||||
2. **Tech scan** — Detects CMS, server, hosting provider, JS framework, SSL cert details
|
||||
3. **Performance audit** — Runs PageSpeed analysis, records load times and scores
|
||||
4. **Contact scrape** — Extracts emails, phones, addresses, social links from the website
|
||||
5. **Score compute** — Calculates 0-100 opportunity score with component breakdown
|
||||
|
||||
---
|
||||
|
||||
## Recommended Test Order
|
||||
|
||||
1. **Journey 1** (steps 1-3) - Import domains and run HTTP checks first
|
||||
2. **Journey 6** - Run full enrichment on a single prospect to test the complete pipeline
|
||||
3. **Journey 1** (steps 4-9) - Run batch scans and view scored leads
|
||||
4. **Journey 2** - Test offline capture on mobile
|
||||
5. **Journey 3** - Filter and export leads
|
||||
6. **Journey 4** - Create campaign templates and send to prospects
|
||||
7. **Journey 5** - Log interactions and check follow-ups
|
||||
|
||||
!!! tip "Enrichment order matters"
|
||||
The enrichment pipeline must run in order: HTTP check first (determines if website exists),
|
||||
then tech scan, performance, and contacts (all require a live website). Score computation
|
||||
should run last as it uses data from all other steps.
|
||||
This document has moved to the prospecting module docs: [User Journeys](../../modules/prospecting/user-journeys.md)
|
||||
|
||||
Reference in New Issue
Block a user