temproary docs uplaoded
This commit is contained in:
@@ -0,0 +1,526 @@
|
||||
# AI Functions Reference
|
||||
|
||||
**Last Verified:** January 20, 2026
|
||||
**Version:** 1.8.4
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
IGNY8's AI engine provides functions for content planning and generation. Located in `backend/igny8_core/ai/`.
|
||||
|
||||
**Providers (v1.4.0+):**
|
||||
- **OpenAI** - GPT-4 for text, DALL-E 3 for images (via `IntegrationProvider`)
|
||||
- **Anthropic** - Claude models for text (via `IntegrationProvider`)
|
||||
- **Runware** - Alternative image generation (via `IntegrationProvider`)
|
||||
- **Bria** - Additional image generation option
|
||||
|
||||
**Key Features:**
|
||||
- Provider API keys stored in `IntegrationProvider` model
|
||||
- Model configurations stored in `AIModelConfig` model
|
||||
- System defaults stored in `SystemAISettings` singleton
|
||||
- Credit-based pricing per model via `AIModelConfig.tokens_per_credit` / `credits_per_image`
|
||||
|
||||
---
|
||||
|
||||
## Architecture (Updated v1.4.0)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AI ENGINE │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ AIEngine │───►│ Function │───►│ Provider │ │
|
||||
│ │ (router) │ │ (logic) │ │ (API) │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │ ▲ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ModelRegistry │───►│AIModelConfig │───►│Integration │ │
|
||||
│ │ (service) │ │ (database) │ │ Provider │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │
|
||||
│ Functions: │
|
||||
│ • AutoClusterKeywords │
|
||||
│ • GenerateContentIdeas │
|
||||
│ • GenerateContent │
|
||||
│ • GenerateImages │
|
||||
│ • GenerateImagePrompts │
|
||||
│ • OptimizeContent (pending) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Model Registry (NEW v1.4.0)
|
||||
|
||||
**Location:** `backend/igny8_core/ai/model_registry.py`
|
||||
|
||||
Central registry for AI model configurations with caching.
|
||||
|
||||
```python
|
||||
from igny8_core.ai.model_registry import ModelRegistry
|
||||
|
||||
# Get model configuration
|
||||
model = ModelRegistry.get_model('gpt-4o-mini')
|
||||
|
||||
# Get pricing rate
|
||||
input_rate = ModelRegistry.get_rate('gpt-4o-mini', 'input')
|
||||
|
||||
# Calculate cost
|
||||
cost = ModelRegistry.calculate_cost('gpt-4o-mini', input_tokens=1000, output_tokens=500)
|
||||
|
||||
# Get API key for provider
|
||||
api_key = ModelRegistry.get_api_key('openai')
|
||||
|
||||
# Get default model
|
||||
default_text = ModelRegistry.get_default_model('text')
|
||||
default_image = ModelRegistry.get_default_model('image')
|
||||
|
||||
# List available models
|
||||
text_models = ModelRegistry.list_models(model_type='text')
|
||||
image_models = ModelRegistry.list_models(model_type='image', provider='runware')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AIEngine
|
||||
|
||||
**Location:** `backend/igny8_core/ai/engine.py`
|
||||
|
||||
Main orchestrator for AI operations.
|
||||
|
||||
```python
|
||||
class AIEngine:
|
||||
def __init__(self, account: Account):
|
||||
self.account = account
|
||||
self.settings = self._load_settings() # Uses SystemAISettings + AccountSettings
|
||||
self.text_provider = self._init_text_provider() # Uses ModelRegistry
|
||||
self.image_provider = self._init_image_provider() # Uses ModelRegistry
|
||||
|
||||
def auto_cluster(self, keywords: List[Keyword]) -> List[Cluster]:
|
||||
"""Cluster keywords by topic"""
|
||||
|
||||
def generate_ideas(self, cluster: Cluster) -> List[ContentIdea]:
|
||||
"""Generate content ideas for cluster"""
|
||||
|
||||
def generate_content(self, task: Task) -> Content:
|
||||
"""Generate full article content"""
|
||||
|
||||
def generate_images(self, content: Content, count: int = 1, quality_tier: str = 'quality') -> List[ContentImage]:
|
||||
"""Generate images for content with quality tier selection (v1.4.0)"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Function: AutoClusterKeywords
|
||||
|
||||
**Purpose:** Group semantically related keywords into clusters
|
||||
|
||||
**Input:**
|
||||
```python
|
||||
{
|
||||
"keywords": [
|
||||
{"id": "...", "keyword": "python tutorial"},
|
||||
{"id": "...", "keyword": "learn python"},
|
||||
{"id": "...", "keyword": "python basics"},
|
||||
...
|
||||
],
|
||||
"site_context": {
|
||||
"name": "Tech Blog",
|
||||
"industry": "Technology"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```python
|
||||
{
|
||||
"clusters": [
|
||||
{
|
||||
"name": "Python Learning Resources",
|
||||
"description": "Tutorials and guides for learning Python",
|
||||
"keywords": ["python tutorial", "learn python", "python basics"]
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Prompt Template:** `auto_cluster`
|
||||
|
||||
**Model:** Default text model (from `AIModelConfig.is_default`)
|
||||
|
||||
**Credit Cost:** Based on `AIModelConfig.tokens_per_credit` for model used
|
||||
|
||||
---
|
||||
|
||||
## Function: GenerateContentIdeas
|
||||
|
||||
**Purpose:** Create content ideas from a keyword cluster
|
||||
|
||||
**Input:**
|
||||
```python
|
||||
{
|
||||
"cluster": {
|
||||
"name": "Python Learning Resources",
|
||||
"description": "...",
|
||||
"keywords": [...]
|
||||
},
|
||||
"site_context": {
|
||||
"name": "Tech Blog",
|
||||
"industry": "Technology"
|
||||
},
|
||||
"count": 5 # Number of ideas to generate
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```python
|
||||
{
|
||||
"ideas": [
|
||||
{
|
||||
"title": "10 Essential Python Tips for Beginners",
|
||||
"description": "A comprehensive guide covering...",
|
||||
"primary_keyword": "python tutorial",
|
||||
"secondary_keywords": ["learn python", "python basics"]
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Prompt Template:** `generate_ideas`
|
||||
|
||||
**Model:** Default text model (from `AIModelConfig.is_default`)
|
||||
|
||||
**Credit Cost:** Based on `AIModelConfig.tokens_per_credit` for model used
|
||||
|
||||
---
|
||||
|
||||
## Function: GenerateContent
|
||||
|
||||
**Purpose:** Create full article from task brief
|
||||
|
||||
**Input:**
|
||||
```python
|
||||
{
|
||||
"task": {
|
||||
"title": "10 Essential Python Tips for Beginners",
|
||||
"brief": "Write a comprehensive guide...",
|
||||
"primary_keyword": "python tutorial",
|
||||
"secondary_keywords": ["learn python", "python basics"]
|
||||
},
|
||||
"site_context": {
|
||||
"name": "Tech Blog",
|
||||
"industry": "Technology",
|
||||
"tone": "Professional but approachable"
|
||||
},
|
||||
"options": {
|
||||
"target_word_count": 2000,
|
||||
"include_headings": True,
|
||||
"include_lists": True,
|
||||
"include_code_blocks": True,
|
||||
"temperature": 0.7, # From SystemAISettings or AccountSettings override
|
||||
"max_tokens": 8192 # From SystemAISettings or AccountSettings override
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```python
|
||||
{
|
||||
"title": "10 Essential Python Tips for Beginners",
|
||||
"body": "<h2>Introduction</h2><p>...</p>...", # Full HTML
|
||||
"excerpt": "Learn the essential Python tips...",
|
||||
"meta_title": "10 Python Tips for Beginners | Tech Blog",
|
||||
"meta_description": "Master Python with these 10 essential tips...",
|
||||
"word_count": 2150
|
||||
}
|
||||
```
|
||||
|
||||
**Prompt Template:** `generate_content`
|
||||
|
||||
**Model:** Default text model (from `AIModelConfig.is_default`)
|
||||
|
||||
**Credit Cost:** Based on `AIModelConfig.tokens_per_credit` for model used
|
||||
|
||||
---
|
||||
|
||||
## Function: GenerateImages (Updated v1.4.0)
|
||||
|
||||
**Purpose:** Create images for article content with quality tier selection
|
||||
|
||||
**Input:**
|
||||
```python
|
||||
{
|
||||
"content": {
|
||||
"title": "10 Essential Python Tips for Beginners",
|
||||
"body": "<html>...</html>",
|
||||
"primary_keyword": "python tutorial"
|
||||
},
|
||||
"options": {
|
||||
"count": 3,
|
||||
"quality_tier": "quality", # basic (1 credit), quality (5 credits), premium (15 credits)
|
||||
"style": "photorealistic", # From SystemAISettings or override
|
||||
"size": "1024x1024" # From SystemAISettings or override
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Quality Tiers (v1.4.0):**
|
||||
|
||||
| Tier | Model Example | credits_per_image | Description |
|
||||
|------|---------------|-------------------|-------------|
|
||||
| basic | runware:97@1 | 1 | Fast generation, simple images |
|
||||
| quality | dall-e-3 | 5 | Balanced quality and speed |
|
||||
| premium | google:4@2 | 15 | Best quality, slower |
|
||||
|
||||
**Process:**
|
||||
1. Get quality tier model from `AIModelConfig.get_image_models_by_tier()`
|
||||
2. Analyze content to identify image opportunities
|
||||
3. Generate prompts for each image (via `GenerateImagePrompts`)
|
||||
4. Call image provider API (using `IntegrationProvider.get_api_key()`)
|
||||
5. Store images and generate thumbnails
|
||||
|
||||
**Output:**
|
||||
```python
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"url": "https://storage.../image1.png",
|
||||
"thumbnail_url": "https://storage.../image1_thumb.png",
|
||||
"alt_text": "Python code example showing...",
|
||||
"caption": "Example of Python list comprehension",
|
||||
"prompt": "A clean code editor showing Python syntax...",
|
||||
"is_featured": True,
|
||||
"model_used": "dall-e-3",
|
||||
"quality_tier": "quality",
|
||||
"credits_used": 5
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Prompt Template:** `generate_image_prompts`
|
||||
|
||||
**Model:** Selected by quality tier from `AIModelConfig`
|
||||
|
||||
**Credit Cost:** `AIModelConfig.credits_per_image` for selected model
|
||||
|
||||
---
|
||||
|
||||
## Function: OptimizeContent (Pending)
|
||||
|
||||
**Status:** ⏸️ Not yet implemented
|
||||
|
||||
**Purpose:** SEO optimize existing content
|
||||
|
||||
**Planned Input:**
|
||||
```python
|
||||
{
|
||||
"content": {
|
||||
"title": "...",
|
||||
"body": "...",
|
||||
"target_keyword": "..."
|
||||
},
|
||||
"optimization_type": "seo" | "readability" | "both"
|
||||
}
|
||||
```
|
||||
|
||||
**Planned Output:**
|
||||
```python
|
||||
{
|
||||
"optimized_title": "...",
|
||||
"optimized_body": "...",
|
||||
"changes": [
|
||||
{"type": "keyword_density", "description": "..."},
|
||||
{"type": "heading_structure", "description": "..."},
|
||||
...
|
||||
],
|
||||
"score_before": 65,
|
||||
"score_after": 85
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prompt Templates
|
||||
|
||||
### System Prompts
|
||||
|
||||
Stored in `PromptTemplate` model or defaults in code.
|
||||
|
||||
| Template | Purpose |
|
||||
|----------|---------|
|
||||
| `auto_cluster` | Keywords → Clusters |
|
||||
| `generate_ideas` | Cluster → Ideas |
|
||||
| `generate_content` | Task → Article |
|
||||
| `generate_image_prompts` | Content → Image prompts |
|
||||
| `optimize_content` | Content → Optimized (pending) |
|
||||
|
||||
### Template Variables
|
||||
|
||||
```python
|
||||
{
|
||||
"site_name": "Tech Blog",
|
||||
"site_industry": "Technology",
|
||||
"site_tone": "Professional",
|
||||
"keyword": "python tutorial",
|
||||
"keywords": ["python", "tutorial", "learn"],
|
||||
"cluster_name": "Python Learning",
|
||||
"task_title": "10 Python Tips",
|
||||
"task_brief": "Write a guide...",
|
||||
"target_word_count": 2000
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Prompts
|
||||
|
||||
Users can customize prompts via Settings → Prompts:
|
||||
|
||||
```python
|
||||
# API
|
||||
GET /api/v1/system/prompts/{type}/
|
||||
PUT /api/v1/system/prompts/{type}/
|
||||
POST /api/v1/system/prompts/{type}/reset/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Provider: OpenAI
|
||||
|
||||
**Location:** `backend/igny8_core/ai/providers/openai_provider.py`
|
||||
|
||||
### Text Generation
|
||||
|
||||
```python
|
||||
class OpenAITextProvider:
|
||||
def complete(self, prompt: str, options: dict) -> str:
|
||||
response = openai.ChatCompletion.create(
|
||||
model=options.get('model', 'gpt-4'),
|
||||
messages=[
|
||||
{"role": "system", "content": self.system_prompt},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=options.get('temperature', 0.7),
|
||||
max_tokens=options.get('max_tokens', 4000)
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
```
|
||||
|
||||
### Image Generation
|
||||
|
||||
```python
|
||||
class OpenAIImageProvider:
|
||||
def generate(self, prompt: str, options: dict) -> str:
|
||||
response = openai.Image.create(
|
||||
model="dall-e-3",
|
||||
prompt=prompt,
|
||||
size=options.get('size', '1024x1024'),
|
||||
quality=options.get('quality', 'standard'),
|
||||
n=1
|
||||
)
|
||||
return response.data[0].url
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Provider: Runware
|
||||
|
||||
**Location:** `backend/igny8_core/ai/providers/runware_provider.py`
|
||||
|
||||
Alternative image generation via Runware API.
|
||||
|
||||
```python
|
||||
class RunwareImageProvider:
|
||||
def generate(self, prompt: str, options: dict) -> str:
|
||||
response = self.client.generate(
|
||||
prompt=prompt,
|
||||
width=options.get('width', 1024),
|
||||
height=options.get('height', 1024),
|
||||
model=options.get('model', 'default')
|
||||
)
|
||||
return response.image_url
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Error | Code | Handling |
|
||||
|-------|------|----------|
|
||||
| Rate limit | `rate_limit_exceeded` | Retry with backoff |
|
||||
| Context too long | `context_length_exceeded` | Truncate input |
|
||||
| Content filter | `content_policy_violation` | Return error to user |
|
||||
| API unavailable | `api_error` | Retry or fail |
|
||||
|
||||
### Retry Strategy
|
||||
|
||||
```python
|
||||
class AIEngine:
|
||||
MAX_RETRIES = 3
|
||||
BASE_DELAY = 1 # seconds
|
||||
|
||||
def _call_with_retry(self, func, *args, **kwargs):
|
||||
for attempt in range(self.MAX_RETRIES):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except RateLimitError:
|
||||
delay = self.BASE_DELAY * (2 ** attempt)
|
||||
time.sleep(delay)
|
||||
except ContentPolicyError:
|
||||
raise # Don't retry policy violations
|
||||
raise MaxRetriesExceeded()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Per-Account Settings
|
||||
|
||||
```python
|
||||
# In AIIntegrationSettings
|
||||
openai_api_key = "sk-..."
|
||||
openai_model = "gpt-4" # or gpt-4-turbo
|
||||
image_provider = "dalle" # or "runware"
|
||||
dalle_api_key = "sk-..."
|
||||
runware_api_key = "..."
|
||||
```
|
||||
|
||||
### Model Options
|
||||
|
||||
| Model | Use Case | Token Limit |
|
||||
|-------|----------|-------------|
|
||||
| `gpt-4` | High quality content | 8,192 |
|
||||
| `gpt-4-turbo` | Faster, larger context | 128,000 |
|
||||
| `gpt-3.5-turbo` | Budget option | 4,096 |
|
||||
|
||||
---
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Logging
|
||||
|
||||
All AI operations logged with:
|
||||
- Input parameters (sanitized)
|
||||
- Output summary
|
||||
- Token usage
|
||||
- Latency
|
||||
- Error details
|
||||
|
||||
### Metrics
|
||||
|
||||
Tracked via internal logging:
|
||||
- Operations per day
|
||||
- Average latency
|
||||
- Error rate
|
||||
- Token consumption
|
||||
- Credit usage
|
||||
@@ -0,0 +1,662 @@
|
||||
# Phase 6: Data Backup & Cleanup Guide
|
||||
|
||||
**Version:** 1.0
|
||||
**Created:** January 9, 2026
|
||||
**Purpose:** Pre-V1.0 Launch Database Preparation
|
||||
|
||||
---
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [What Was Created](#what-was-created)
|
||||
3. [When to Use](#when-to-use)
|
||||
4. [Pre-Execution Checklist](#pre-execution-checklist)
|
||||
5. [Command 1: Export System Config](#command-1-export-system-config)
|
||||
6. [Command 2: Cleanup User Data](#command-2-cleanup-user-data)
|
||||
7. [Complete Workflow](#complete-workflow)
|
||||
8. [Safety Measures](#safety-measures)
|
||||
9. [Rollback Procedures](#rollback-procedures)
|
||||
10. [FAQ](#faq)
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
Phase 6 provides two Django management commands to safely prepare your IGNY8 database for V1.0 production launch:
|
||||
|
||||
1. **Export System Configuration** - Backs up all system settings to JSON files
|
||||
2. **Cleanup User Data** - Removes all test/development user data while preserving system configuration
|
||||
|
||||
### Why These Commands?
|
||||
|
||||
- **Clean Start**: Launch V1.0 with a pristine database
|
||||
- **Configuration Preservation**: Keep all your carefully configured settings
|
||||
- **Safety First**: Multiple safety checks and dry-run options
|
||||
- **Audit Trail**: Complete metadata and logging
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ What Was Created
|
||||
|
||||
### File Locations
|
||||
|
||||
```
|
||||
backend/igny8_core/management/commands/
|
||||
├── export_system_config.py # System configuration backup
|
||||
└── cleanup_user_data.py # User data cleanup
|
||||
```
|
||||
|
||||
### Command 1: `export_system_config.py`
|
||||
|
||||
**Purpose**: Exports all system configuration to JSON files for backup and version control.
|
||||
|
||||
**What it exports:**
|
||||
- ✅ Subscription Plans (Starter, Growth, Scale)
|
||||
- ✅ Credit Cost Configurations
|
||||
- ✅ AI Model Settings (OpenAI, Anthropic, etc.)
|
||||
- ✅ Global Integration Settings
|
||||
- ✅ Industries and Sectors
|
||||
- ✅ Seed Keywords (reference data)
|
||||
- ✅ Author Profiles
|
||||
- ✅ AI Prompts and Variables
|
||||
|
||||
**What it creates:**
|
||||
- Individual JSON files for each data type
|
||||
- `export_metadata.json` with timestamp and statistics
|
||||
- Organized folder structure in `backups/config/`
|
||||
|
||||
### Command 2: `cleanup_user_data.py`
|
||||
|
||||
**Purpose**: Safely removes all user-generated test data before production launch.
|
||||
|
||||
**What it deletes:**
|
||||
- 🗑️ Sites and Site Settings
|
||||
- 🗑️ Keywords, Clusters, Ideas
|
||||
- 🗑️ Tasks, Content, Images
|
||||
- 🗑️ Publishing Records
|
||||
- 🗑️ WordPress Sync Events
|
||||
- 🗑️ Credit Transactions and Usage Logs
|
||||
- 🗑️ Automation Runs
|
||||
- 🗑️ Notifications
|
||||
- 🗑️ Orders
|
||||
|
||||
**What it preserves:**
|
||||
- ✅ User Accounts (admin users)
|
||||
- ✅ System Configuration (all settings from export)
|
||||
- ✅ Plans and Pricing
|
||||
- ✅ AI Models and Prompts
|
||||
- ✅ Industries and Sectors
|
||||
|
||||
---
|
||||
|
||||
## ⏰ When to Use
|
||||
|
||||
### Correct Timing
|
||||
|
||||
✅ **Use these commands when:**
|
||||
- You're preparing for V1.0 production launch
|
||||
- You've completed all testing and configuration
|
||||
- You want to start production with clean data
|
||||
- All system settings (Plans, AI models, prompts) are finalized
|
||||
|
||||
❌ **Do NOT use these commands when:**
|
||||
- You're still in active development
|
||||
- You haven't backed up your configurations
|
||||
- You're unsure about your system settings
|
||||
- You're in production with live users
|
||||
|
||||
### Recommended Timeline
|
||||
|
||||
```
|
||||
Day -7: Final configuration review
|
||||
Day -5: Export system config (first backup)
|
||||
Day -3: Test commands in staging
|
||||
Day -2: Export system config (final backup)
|
||||
Day -1: Cleanup user data in staging
|
||||
Day 0: Launch day - cleanup in production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Pre-Execution Checklist
|
||||
|
||||
Before running ANY Phase 6 command, complete this checklist:
|
||||
|
||||
### Environment Verification
|
||||
|
||||
- [ ] Confirm you're in the correct environment (staging vs production)
|
||||
- [ ] Check `ENVIRONMENT` setting in Django settings
|
||||
- [ ] Verify database connection is correct
|
||||
- [ ] Ensure you have full database backup
|
||||
|
||||
### System State
|
||||
|
||||
- [ ] All Plans configured and tested
|
||||
- [ ] All AI prompts finalized
|
||||
- [ ] All credit costs verified
|
||||
- [ ] All industries/sectors populated
|
||||
- [ ] Seed keywords imported
|
||||
|
||||
### Safety Backups
|
||||
|
||||
- [ ] Full database dump exists
|
||||
- [ ] Previous export exists (if available)
|
||||
- [ ] Media files backed up
|
||||
- [ ] Environment variables documented
|
||||
|
||||
### Access & Permissions
|
||||
|
||||
- [ ] You have Django shell access
|
||||
- [ ] You have database backup access
|
||||
- [ ] You have rollback permissions
|
||||
- [ ] Stakeholders notified
|
||||
|
||||
---
|
||||
|
||||
## 📤 Command 1: Export System Config
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
cd /data/app/igny8/backend
|
||||
python manage.py export_system_config
|
||||
```
|
||||
|
||||
### With Custom Output Directory
|
||||
|
||||
```bash
|
||||
python manage.py export_system_config --output-dir=/path/to/backup
|
||||
```
|
||||
|
||||
### Step-by-Step Execution
|
||||
|
||||
#### Step 1: Navigate to Backend
|
||||
|
||||
```bash
|
||||
cd /data/app/igny8/backend
|
||||
```
|
||||
|
||||
#### Step 2: Run Export
|
||||
|
||||
```bash
|
||||
python manage.py export_system_config --output-dir=../backups/config/$(date +%Y%m%d)
|
||||
```
|
||||
|
||||
#### Step 3: Verify Output
|
||||
|
||||
```bash
|
||||
ls -la ../backups/config/$(date +%Y%m%d)/
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
plans.json # Subscription plans
|
||||
credit_costs.json # Credit cost configurations
|
||||
ai_models.json # AI model settings
|
||||
global_integrations.json # Integration settings
|
||||
industries.json # Industry master data
|
||||
sectors.json # Sector master data
|
||||
seed_keywords.json # Reference keywords
|
||||
author_profiles.json # Writing style profiles
|
||||
prompts.json # AI prompts
|
||||
prompt_variables.json # Prompt variables
|
||||
export_metadata.json # Export timestamp & stats
|
||||
```
|
||||
|
||||
#### Step 4: Verify Data
|
||||
|
||||
Check one of the exports:
|
||||
```bash
|
||||
cat ../backups/config/$(date +%Y%m%d)/plans.json | head -20
|
||||
```
|
||||
|
||||
#### Step 5: Commit to Version Control
|
||||
|
||||
```bash
|
||||
cd /data/app/igny8
|
||||
git add backups/config/
|
||||
git commit -m "Backup: V1.0 system configuration export"
|
||||
git push
|
||||
```
|
||||
|
||||
### What The Output Looks Like
|
||||
|
||||
```
|
||||
Exporting system configuration to: /data/app/igny8/backups/config/20260109
|
||||
|
||||
✓ Exported 3 Subscription Plans → plans.json
|
||||
✓ Exported 12 Credit Cost Configurations → credit_costs.json
|
||||
✓ Exported 4 AI Model Configurations → ai_models.json
|
||||
✓ Exported 1 Global Integration Settings → global_integrations.json
|
||||
✓ Exported 15 Industries → industries.json
|
||||
✓ Exported 47 Sectors → sectors.json
|
||||
✓ Exported 523 Seed Keywords → seed_keywords.json
|
||||
✓ Exported 3 Author Profiles → author_profiles.json
|
||||
✓ Exported 8 AI Prompts → prompts.json
|
||||
✓ Exported 12 Prompt Variables → prompt_variables.json
|
||||
|
||||
✓ Metadata saved to export_metadata.json
|
||||
|
||||
======================================================================
|
||||
System Configuration Export Complete!
|
||||
|
||||
Successful: 10 exports
|
||||
Failed: 0 exports
|
||||
Location: /data/app/igny8/backups/config/20260109
|
||||
======================================================================
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Problem**: "No module named 'django'"
|
||||
```bash
|
||||
# Solution: Activate virtual environment or use Docker
|
||||
docker-compose exec backend python manage.py export_system_config
|
||||
```
|
||||
|
||||
**Problem**: "Permission denied" when writing files
|
||||
```bash
|
||||
# Solution: Check directory permissions
|
||||
mkdir -p ../backups/config
|
||||
chmod 755 ../backups/config
|
||||
```
|
||||
|
||||
**Problem**: Empty JSON files
|
||||
```bash
|
||||
# Solution: Verify data exists in database
|
||||
python manage.py shell
|
||||
>>> from igny8_core.modules.billing.models import Plan
|
||||
>>> Plan.objects.count()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗑️ Command 2: Cleanup User Data
|
||||
|
||||
### ⚠️ CRITICAL WARNING
|
||||
|
||||
**THIS COMMAND PERMANENTLY DELETES DATA**
|
||||
|
||||
- Cannot be undone without database restore
|
||||
- Removes ALL user-generated content
|
||||
- Should ONLY be run before production launch
|
||||
- ALWAYS run `--dry-run` first
|
||||
|
||||
### Safety Features
|
||||
|
||||
1. **Dry-Run Mode**: Preview deletions without actually deleting
|
||||
2. **Confirmation Prompt**: Must type "DELETE ALL DATA" to proceed
|
||||
3. **Production Protection**: Blocked in production environment (unless explicitly allowed)
|
||||
4. **Transaction Safety**: All deletions in atomic transaction
|
||||
5. **Detailed Logging**: Shows exactly what was deleted
|
||||
|
||||
### Usage: Dry Run (Always First!)
|
||||
|
||||
```bash
|
||||
cd /data/app/igny8/backend
|
||||
python manage.py cleanup_user_data --dry-run
|
||||
```
|
||||
|
||||
### Dry Run Output Example
|
||||
|
||||
```
|
||||
======================================================================
|
||||
DRY RUN - No data will be deleted
|
||||
======================================================================
|
||||
|
||||
✓ Would delete 1,234 Notifications
|
||||
✓ Would delete 5,678 Credit Usage Logs
|
||||
✓ Would delete 456 Credit Transactions
|
||||
✓ Would delete 23 Orders
|
||||
✓ Would delete 8,901 WordPress Sync Events
|
||||
✓ Would delete 234 Publishing Records
|
||||
✓ Would delete 45 Automation Runs
|
||||
✓ Would delete 3,456 Images
|
||||
✓ Would delete 2,345 Content
|
||||
✓ Would delete 4,567 Tasks
|
||||
✓ Would delete 5,678 Content Ideas
|
||||
✓ Would delete 1,234 Clusters
|
||||
✓ Would delete 9,876 Keywords
|
||||
✓ Would delete 12 Sites
|
||||
|
||||
→ Keeping 3 Users (not deleted)
|
||||
|
||||
Total records to delete: 43,739
|
||||
|
||||
======================================================================
|
||||
To proceed with actual deletion, run:
|
||||
python manage.py cleanup_user_data --confirm
|
||||
======================================================================
|
||||
```
|
||||
|
||||
### Usage: Actual Cleanup
|
||||
|
||||
```bash
|
||||
python manage.py cleanup_user_data --confirm
|
||||
```
|
||||
|
||||
**You will be prompted:**
|
||||
```
|
||||
======================================================================
|
||||
⚠️ DELETING ALL USER DATA - THIS CANNOT BE UNDONE!
|
||||
======================================================================
|
||||
|
||||
Type "DELETE ALL DATA" to proceed:
|
||||
```
|
||||
|
||||
**Type exactly:** `DELETE ALL DATA`
|
||||
|
||||
### Actual Cleanup Output
|
||||
|
||||
```
|
||||
Proceeding with deletion...
|
||||
|
||||
✓ Deleted 1,234 Notifications
|
||||
✓ Deleted 5,678 Credit Usage Logs
|
||||
✓ Deleted 456 Credit Transactions
|
||||
✓ Deleted 23 Orders
|
||||
✓ Deleted 8,901 WordPress Sync Events
|
||||
✓ Deleted 234 Publishing Records
|
||||
✓ Deleted 45 Automation Runs
|
||||
✓ Deleted 3,456 Images
|
||||
✓ Deleted 2,345 Content
|
||||
✓ Deleted 4,567 Tasks
|
||||
✓ Deleted 5,678 Content Ideas
|
||||
✓ Deleted 1,234 Clusters
|
||||
✓ Deleted 9,876 Keywords
|
||||
✓ Deleted 12 Sites
|
||||
|
||||
======================================================================
|
||||
User Data Cleanup Complete!
|
||||
|
||||
Total records deleted: 43,739
|
||||
Failed deletions: 0
|
||||
======================================================================
|
||||
```
|
||||
|
||||
### Production Environment Protection
|
||||
|
||||
If you try to run cleanup in production:
|
||||
|
||||
```
|
||||
⚠️ BLOCKED: Cannot run cleanup in PRODUCTION environment!
|
||||
|
||||
To allow this, temporarily set ENVIRONMENT to "staging" in settings.
|
||||
```
|
||||
|
||||
To override (ONLY if absolutely necessary):
|
||||
|
||||
```python
|
||||
# In settings.py - TEMPORARY
|
||||
ENVIRONMENT = 'staging' # Change back after cleanup!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Complete Workflow
|
||||
|
||||
### Full Pre-Launch Procedure
|
||||
|
||||
```bash
|
||||
# ========================================
|
||||
# STEP 1: FULL DATABASE BACKUP
|
||||
# ========================================
|
||||
cd /data/app/igny8/backend
|
||||
pg_dump -h localhost -U postgres igny8_db > ../backups/$(date +%Y%m%d)_pre_v1_full_backup.sql
|
||||
|
||||
# Verify backup exists and has content
|
||||
ls -lh ../backups/$(date +%Y%m%d)_pre_v1_full_backup.sql
|
||||
head -50 ../backups/$(date +%Y%m%d)_pre_v1_full_backup.sql
|
||||
|
||||
|
||||
# ========================================
|
||||
# STEP 2: EXPORT SYSTEM CONFIGURATION
|
||||
# ========================================
|
||||
python manage.py export_system_config --output-dir=../backups/config/$(date +%Y%m%d)
|
||||
|
||||
# Verify exports
|
||||
ls -la ../backups/config/$(date +%Y%m%d)/
|
||||
|
||||
# Review critical configs
|
||||
cat ../backups/config/$(date +%Y%m%d)/plans.json | python -m json.tool | head -30
|
||||
cat ../backups/config/$(date +%Y%m%d)/credit_costs.json | python -m json.tool | head -30
|
||||
|
||||
|
||||
# ========================================
|
||||
# STEP 3: COMMIT CONFIGS TO GIT
|
||||
# ========================================
|
||||
cd /data/app/igny8
|
||||
git add backups/config/
|
||||
git commit -m "Pre-V1.0: System configuration backup $(date +%Y%m%d)"
|
||||
git push
|
||||
|
||||
|
||||
# ========================================
|
||||
# STEP 4: BACKUP MEDIA FILES
|
||||
# ========================================
|
||||
cd /data/app/igny8
|
||||
tar -czf backups/$(date +%Y%m%d)_media_backup.tar.gz backend/media/
|
||||
|
||||
|
||||
# ========================================
|
||||
# STEP 5: DRY RUN CLEANUP (REVIEW CAREFULLY)
|
||||
# ========================================
|
||||
cd backend
|
||||
python manage.py cleanup_user_data --dry-run
|
||||
|
||||
# Review the counts - make sure they're expected
|
||||
|
||||
|
||||
# ========================================
|
||||
# STEP 6: ACTUAL CLEANUP (POINT OF NO RETURN)
|
||||
# ========================================
|
||||
python manage.py cleanup_user_data --confirm
|
||||
# Type: DELETE ALL DATA
|
||||
|
||||
|
||||
# ========================================
|
||||
# STEP 7: VERIFY CLEANUP
|
||||
# ========================================
|
||||
python manage.py shell << 'EOF'
|
||||
from igny8_core.auth.models import Site, CustomUser
|
||||
from igny8_core.business.planning.models import Keywords
|
||||
from igny8_core.modules.billing.models import Plan
|
||||
|
||||
print(f"Sites: {Site.objects.count()} (should be 0)")
|
||||
print(f"Keywords: {Keywords.objects.count()} (should be 0)")
|
||||
print(f"Users: {CustomUser.objects.count()} (admins preserved)")
|
||||
print(f"Plans: {Plan.objects.count()} (should have your plans)")
|
||||
EOF
|
||||
|
||||
|
||||
# ========================================
|
||||
# STEP 8: TEST APPLICATION
|
||||
# ========================================
|
||||
python manage.py runserver 0.0.0.0:8000 &
|
||||
# Visit app and verify:
|
||||
# - Can login as admin
|
||||
# - Dashboard loads (empty state)
|
||||
# - Plans visible in settings
|
||||
# - Can create new user account
|
||||
|
||||
|
||||
# ========================================
|
||||
# STEP 9: TAG RELEASE
|
||||
# ========================================
|
||||
cd /data/app/igny8
|
||||
git tag -a v1.0.0-clean -m "V1.0.0 - Clean database ready for launch"
|
||||
git push origin v1.0.0-clean
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Safety Measures
|
||||
|
||||
### Built-in Protections
|
||||
|
||||
1. **Atomic Transactions**: All deletions in single transaction - all or nothing
|
||||
2. **Production Check**: Requires explicit override in production
|
||||
3. **Confirmation Prompt**: Must type exact phrase
|
||||
4. **Dry Run**: See exactly what will be deleted
|
||||
5. **Detailed Logging**: Know what was deleted and any failures
|
||||
|
||||
### Manual Safety Checklist
|
||||
|
||||
Before running cleanup:
|
||||
|
||||
- [ ] **Full database backup** exists and verified
|
||||
- [ ] **System config export** completed and committed to git
|
||||
- [ ] **Media files** backed up
|
||||
- [ ] **Dry run reviewed** and counts are expected
|
||||
- [ ] **Stakeholders notified** of pending cleanup
|
||||
- [ ] **Rollback plan** documented and tested
|
||||
- [ ] **Off-hours execution** scheduled (if production)
|
||||
- [ ] **Monitoring ready** to catch any issues
|
||||
|
||||
### Additional Recommendations
|
||||
|
||||
1. **Staging First**: Always test in staging environment first
|
||||
2. **Screenshot Evidence**: Take screenshots of dry-run output
|
||||
3. **Communication**: Notify team before and after
|
||||
4. **Timing**: Run during low-traffic hours
|
||||
5. **Verification**: Test application immediately after
|
||||
|
||||
---
|
||||
|
||||
## 🔙 Rollback Procedures
|
||||
|
||||
### If Something Goes Wrong
|
||||
|
||||
#### During Cleanup (Transaction Failed)
|
||||
|
||||
No action needed - atomic transaction will automatically rollback.
|
||||
|
||||
#### After Cleanup (Need to Restore)
|
||||
|
||||
```bash
|
||||
# OPTION 1: Restore from PostgreSQL backup
|
||||
cd /data/app/igny8
|
||||
psql -U postgres -d igny8_db < backups/20260109_pre_v1_full_backup.sql
|
||||
|
||||
# OPTION 2: Restore specific tables (if partial restore needed)
|
||||
pg_restore -U postgres -d igny8_db -t "specific_table" backups/20260109_pre_v1_full_backup.sql
|
||||
|
||||
# OPTION 3: Restore from Docker backup (if using Docker)
|
||||
docker-compose exec -T db psql -U postgres igny8_db < backups/20260109_pre_v1_full_backup.sql
|
||||
```
|
||||
|
||||
#### Restore Media Files
|
||||
|
||||
```bash
|
||||
cd /data/app/igny8
|
||||
tar -xzf backups/20260109_media_backup.tar.gz -C backend/
|
||||
```
|
||||
|
||||
#### Verify Restore
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python manage.py shell << 'EOF'
|
||||
from igny8_core.auth.models import Site
|
||||
from igny8_core.business.planning.models import Keywords
|
||||
print(f"Sites restored: {Site.objects.count()}")
|
||||
print(f"Keywords restored: {Keywords.objects.count()}")
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ❓ FAQ
|
||||
|
||||
### Q: Can I run these commands multiple times?
|
||||
|
||||
**A:**
|
||||
- **Export Config**: Yes, safe to run multiple times. Creates timestamped backups.
|
||||
- **Cleanup**: Yes, but after first cleanup there's nothing left to delete (idempotent).
|
||||
|
||||
### Q: What if I only want to delete some data?
|
||||
|
||||
**A:** These commands are all-or-nothing by design for safety. To delete specific data, use Django admin or write a custom management command.
|
||||
|
||||
### Q: Can I restore individual items from the export?
|
||||
|
||||
**A:** Yes! The JSON files use Django's standard serialization format. Use `python manage.py loaddata <file>.json` to restore.
|
||||
|
||||
### Q: Will this affect my development environment?
|
||||
|
||||
**A:** Only if you run it there. These commands work on whatever database your Django settings point to.
|
||||
|
||||
### Q: How long does cleanup take?
|
||||
|
||||
**A:** Depends on data volume. Typical ranges:
|
||||
- Small (< 10k records): 1-5 seconds
|
||||
- Medium (10k-100k): 5-30 seconds
|
||||
- Large (> 100k): 30-120 seconds
|
||||
|
||||
### Q: What if cleanup fails halfway?
|
||||
|
||||
**A:** Can't happen - it's wrapped in an atomic transaction. Either everything deletes or nothing does.
|
||||
|
||||
### Q: Do I need to stop the application?
|
||||
|
||||
**A:** Recommended but not required. Stopping the app prevents race conditions during cleanup.
|
||||
|
||||
### Q: Can I schedule these as cron jobs?
|
||||
|
||||
**A:**
|
||||
- **Export**: Yes, great for automated backups
|
||||
- **Cleanup**: No, should only be run manually with explicit confirmation
|
||||
|
||||
### Q: What about Django migrations?
|
||||
|
||||
**A:** Cleanup only deletes data, not schema. All tables and migrations remain intact.
|
||||
|
||||
### Q: How do I know if my system config is complete?
|
||||
|
||||
**A:** Run the export and review the counts in `export_metadata.json`. Compare with your documentation.
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
### If You Need Help
|
||||
|
||||
1. **Check this guide** thoroughly first
|
||||
2. **Review error messages** carefully
|
||||
3. **Test in staging** before production
|
||||
4. **Contact team** if unsure about any step
|
||||
|
||||
### Emergency Contacts
|
||||
|
||||
- **Database Issues**: DBA team
|
||||
- **Application Issues**: Backend team
|
||||
- **Configuration Questions**: System admin
|
||||
- **Rollback Needed**: All hands on deck!
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Criteria
|
||||
|
||||
After completing Phase 6, you should have:
|
||||
|
||||
- ✅ Multiple timestamped config exports in `backups/config/`
|
||||
- ✅ Full database SQL backup in `backups/`
|
||||
- ✅ Media files backup in `backups/`
|
||||
- ✅ Zero user-generated data in database
|
||||
- ✅ All system configurations intact
|
||||
- ✅ Application starts and loads empty state
|
||||
- ✅ Admin can log in
|
||||
- ✅ New users can sign up
|
||||
- ✅ Plans visible and functional
|
||||
- ✅ Git tag created for v1.0.0-clean
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** January 9, 2026
|
||||
**Next Review:** After V1.0 Launch
|
||||
|
||||
---
|
||||
|
||||
*This guide is part of the IGNY8 Pre-Launch Preparation (Phase 6)*
|
||||
@@ -0,0 +1,114 @@
|
||||
# IGNY8 Billing System Master Document
|
||||
|
||||
**Last Updated:** 2026-01-20
|
||||
|
||||
> **Primary Reference:** For complete billing documentation, see [BILLING-PAYMENTS-COMPLETE.md](../10-MODULES/BILLING-PAYMENTS-COMPLETE.md)
|
||||
|
||||
This document provides a summary of the billing system implementation.
|
||||
|
||||
---
|
||||
|
||||
## 1) Core Principles
|
||||
|
||||
- **Two-Pool Credit System:**
|
||||
- `account.credits` = Plan credits (reset on renewal)
|
||||
- `account.bonus_credits` = Purchased credits (NEVER expire, NEVER reset)
|
||||
- **Credit Usage Priority:** Plan credits used FIRST, bonus credits only when plan credits = 0
|
||||
- **No hardcoded products**: Plans, credit packages, and future add-ons are data-driven.
|
||||
- **Explicit invoice type**: `subscription`, `credit_package`, `addon`, `custom`.
|
||||
- **Correct crediting**:
|
||||
- Subscription: reset plan credits to **full plan amount** on payment (bonus untouched)
|
||||
- Credit package: add to **bonus_credits** only (never plan credits)
|
||||
- **Renewal Grace Period:** 7 days
|
||||
- **Credit Reset on Non-Payment:** 24 hours after renewal (Day +1), plan credits → 0
|
||||
- **Auditability**: Every credit change is recorded in `CreditTransaction`.
|
||||
|
||||
---
|
||||
|
||||
## 2) Two-Pool Credit System
|
||||
|
||||
| Pool | Field | Source | Behavior |
|
||||
|------|-------|--------|----------|
|
||||
| Plan Credits | `account.credits` | Subscription plan | Reset to plan amount on renewal payment, reset to 0 if unpaid after 24h |
|
||||
| Bonus Credits | `account.bonus_credits` | Credit packages | NEVER expire, NEVER reset, only deducted after plan credits = 0 |
|
||||
|
||||
### Credit Deduction Priority
|
||||
1. Deduct from `credits` (plan) first
|
||||
2. Only when `credits = 0`, deduct remainder from `bonus_credits`
|
||||
|
||||
---
|
||||
|
||||
## 3) Renewal Workflow (Simplified)
|
||||
|
||||
### Stripe/PayPal (Auto-Pay) - Industry Standard
|
||||
- **No advance notice** (like Netflix, Spotify)
|
||||
- Day 0: Auto-charge attempt
|
||||
- If success: Receipt email + credits reset to plan amount
|
||||
- If failed: Retry notification, Stripe retries 4x over 7 days
|
||||
- Day +7: Expired if all retries fail
|
||||
|
||||
### Bank Transfer (Manual)
|
||||
- **Day -3:** Invoice created + Email sent
|
||||
- **Day 0:** Renewal day reminder (if unpaid)
|
||||
- **Day +1:** Urgent reminder + credits reset to 0
|
||||
- **Day +7:** Subscription expired
|
||||
|
||||
---
|
||||
|
||||
## 4) Scheduled Tasks (Updated)
|
||||
|
||||
| Task | Purpose | Schedule |
|
||||
|------|---------|----------|
|
||||
| `create_bank_transfer_invoices` | Invoice 3 days before (bank transfer only) | Daily 09:00 |
|
||||
| `process_subscription_renewals` | Auto-pay renewals (Stripe/PayPal) | Daily 00:05 |
|
||||
| `send_renewal_day_reminders` | Day 0 reminder (bank transfer) | Daily 10:00 |
|
||||
| `send_day_after_reminders` | Day +1 urgent reminder + credit reset | Daily 09:15 |
|
||||
| `check_expired_renewals` | Expire after 7-day grace | Daily 00:15 |
|
||||
| `send_credit_invoice_expiry_reminders` | Credit invoice reminder | Daily 09:30 |
|
||||
| `void_expired_credit_invoices` | Auto-void credit invoices (48h) | Daily 00:45 |
|
||||
|
||||
---
|
||||
|
||||
## 5) Invoice Types and Fulfillment
|
||||
|
||||
| Invoice Type | Credits Action | Account Status |
|
||||
|--------------|----------------|----------------|
|
||||
| `subscription` | Reset plan credits to plan amount | Activate account + subscription |
|
||||
| `credit_package` | Add to **bonus_credits** | No status change |
|
||||
| `addon` | Provision entitlement | No status change |
|
||||
| `custom` | As specified | No status change |
|
||||
|
||||
---
|
||||
|
||||
## 6) Key Implementation Rules
|
||||
|
||||
1. **Two pools:** `credits` (plan) + `bonus_credits` (purchased)
|
||||
2. **Deduction order:** Plan credits first, then bonus credits
|
||||
3. **Subscription payment:** Reset plan credits to full amount (bonus untouched)
|
||||
4. **Credit package payment:** Add to bonus_credits only
|
||||
5. **No payment 24h:** Plan credits → 0, bonus credits unchanged
|
||||
6. **Late payment:** Plan credits restored to full amount
|
||||
|
||||
---
|
||||
|
||||
## 7) Quick Reference
|
||||
|
||||
### Payment Method by Country
|
||||
| Country | Stripe | PayPal | Bank Transfer |
|
||||
|---------|--------|--------|---------------|
|
||||
| Pakistan (PK) | ✅ | ❌ | ✅ |
|
||||
| Others | ✅ | ✅ | ❌ |
|
||||
|
||||
### Credit Reset Summary
|
||||
| Event | Plan Credits | Bonus Credits |
|
||||
|-------|--------------|---------------|
|
||||
| Payment success | Reset to plan amount | No change |
|
||||
| No payment (24h) | Reset to 0 | No change |
|
||||
| Late payment | Reset to plan amount | No change |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Complete Documentation: [BILLING-PAYMENTS-COMPLETE.md](../10-MODULES/BILLING-PAYMENTS-COMPLETE.md)
|
||||
- Payment Gateways: [PAYMENT-SYSTEM.md](PAYMENT-SYSTEM.md)
|
||||
@@ -0,0 +1,921 @@
|
||||
# Django Admin Access Guide - Payment & Email Integration Settings
|
||||
|
||||
**Last Updated:** January 20, 2026
|
||||
**Version:** 1.8.4
|
||||
**Purpose:** Guide to configure Stripe, PayPal, and Resend credentials via Django Admin
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Accessing Django Admin](#1-accessing-django-admin)
|
||||
2. [Integration Providers Settings](#2-integration-providers-settings)
|
||||
3. [Stripe Configuration](#3-stripe-configuration)
|
||||
4. [PayPal Configuration](#4-paypal-configuration)
|
||||
5. [Resend Configuration](#5-resend-configuration)
|
||||
6. [Plan & Pricing Configuration](#6-plan--pricing-configuration)
|
||||
7. [Credit Packages Configuration](#7-credit-packages-configuration)
|
||||
8. [Testing Checklist](#8-testing-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 1. Accessing Django Admin
|
||||
|
||||
### 1.1 URL Access
|
||||
|
||||
**Local Development:**
|
||||
```
|
||||
http://localhost:8000/admin/
|
||||
```
|
||||
|
||||
**Staging/Production:**
|
||||
```
|
||||
https://api.igny8.com/admin/
|
||||
```
|
||||
|
||||
### 1.2 Login
|
||||
|
||||
Use your superuser credentials:
|
||||
- **Username:** (your admin username)
|
||||
- **Password:** (your admin password)
|
||||
|
||||
**Create Superuser (if needed):**
|
||||
```bash
|
||||
cd /data/app/igny8/backend
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Integration Providers Settings
|
||||
|
||||
### 2.1 Navigating to Integration Providers
|
||||
|
||||
1. Log in to Django Admin
|
||||
2. Look for **"MODULES"** section (or similar grouping)
|
||||
3. Click on **"System" → "Integration providers"**
|
||||
|
||||
**Direct URL Path:**
|
||||
```
|
||||
/admin/system/integrationprovider/
|
||||
```
|
||||
|
||||
### 2.2 Pre-seeded Providers
|
||||
|
||||
You should see these providers already created:
|
||||
|
||||
| Provider ID | Display Name | Type | Status |
|
||||
|-------------|--------------|------|--------|
|
||||
| `stripe` | Stripe | payment | Active (sandbox) |
|
||||
| `paypal` | PayPal | payment | Active (sandbox) |
|
||||
| `resend` | Resend | email | Active |
|
||||
| `openai` | OpenAI | ai | Active |
|
||||
| `anthropic` | Anthropic | ai | Active |
|
||||
| `google` | Google | ai | Active |
|
||||
| `runware` | Runware | ai | Active |
|
||||
| `cloudflare_r2` | Cloudflare R2 | storage | Active |
|
||||
|
||||
---
|
||||
|
||||
## 3. Stripe Configuration
|
||||
|
||||
### 3.1 Getting Stripe Credentials
|
||||
|
||||
#### Step 1: Login to Stripe Dashboard
|
||||
Go to [dashboard.stripe.com](https://dashboard.stripe.com)
|
||||
|
||||
#### Step 2: Get API Keys
|
||||
|
||||
**Test Mode (Sandbox):**
|
||||
1. Toggle to "Test mode" in top-right
|
||||
2. Go to **Developers → API keys**
|
||||
3. Copy:
|
||||
- **Publishable key** (starts with `pk_test_...`)
|
||||
- **Secret key** (starts with `sk_test_...`)
|
||||
|
||||
**Live Mode (Production):**
|
||||
1. Toggle to "Live mode"
|
||||
2. Go to **Developers → API keys**
|
||||
3. Copy:
|
||||
- **Publishable key** (starts with `pk_live_...`)
|
||||
- **Secret key** (starts with `sk_live_...`)
|
||||
|
||||
#### Step 3: Configure Webhook
|
||||
|
||||
1. Go to **Developers → Webhooks**
|
||||
2. Click **"Add endpoint"**
|
||||
3. Enter endpoint URL:
|
||||
```
|
||||
Test: https://api-staging.igny8.com/api/v1/billing/webhooks/stripe/
|
||||
Live: https://api.igny8.com/api/v1/billing/webhooks/stripe/
|
||||
```
|
||||
4. Select events to listen for:
|
||||
- `checkout.session.completed`
|
||||
- `invoice.paid`
|
||||
- `invoice.payment_failed`
|
||||
- `customer.subscription.updated`
|
||||
- `customer.subscription.deleted`
|
||||
5. Click **"Add endpoint"**
|
||||
6. Copy the **Signing secret** (starts with `whsec_...`)
|
||||
|
||||
#### Step 4: Create Products and Prices
|
||||
|
||||
1. Go to **Products → Add product**
|
||||
2. Create these products:
|
||||
|
||||
**Starter Plan**
|
||||
- Name: `Starter Plan`
|
||||
- Description: `Basic plan for small projects`
|
||||
- Pricing: `$99.00 / month`
|
||||
- Copy the **Price ID** (starts with `price_...`)
|
||||
|
||||
**Growth Plan**
|
||||
- Name: `Growth Plan`
|
||||
- Description: `For growing businesses`
|
||||
- Pricing: `$199.00 / month`
|
||||
- Copy the **Price ID**
|
||||
|
||||
**Scale Plan**
|
||||
- Name: `Scale Plan`
|
||||
- Description: `For large enterprises`
|
||||
- Pricing: `$299.00 / month`
|
||||
- Copy the **Price ID**
|
||||
|
||||
### 3.2 Adding to Django Admin
|
||||
|
||||
1. Go to Django Admin → **System → Integration providers**
|
||||
2. Click on **"stripe"**
|
||||
3. Fill in the fields:
|
||||
|
||||
```
|
||||
Provider ID: stripe (already set)
|
||||
Display name: Stripe (already set)
|
||||
Provider type: payment (already set)
|
||||
|
||||
API key: pk_test_xxxxxxxxxxxxx (or pk_live_ for production)
|
||||
API secret: sk_test_xxxxxxxxxxxxx (or sk_live_ for production)
|
||||
Webhook secret: whsec_xxxxxxxxxxxxx
|
||||
|
||||
API endpoint: [leave empty - uses default]
|
||||
|
||||
Config (JSON):
|
||||
{
|
||||
"currency": "usd",
|
||||
"payment_methods": ["card"],
|
||||
"billing_portal_enabled": true
|
||||
}
|
||||
|
||||
✅ Is active: Checked
|
||||
✅ Is sandbox: Checked (for test mode) / Unchecked (for live mode)
|
||||
```
|
||||
|
||||
4. Click **"Save"**
|
||||
|
||||
### 3.3 Update Plan Models with Stripe Price IDs
|
||||
|
||||
1. Go to Django Admin → **Auth → Plans**
|
||||
2. Edit each plan:
|
||||
|
||||
**Starter Plan:**
|
||||
- Stripe price id: `price_xxxxxxxxxxxxx` (from Stripe dashboard)
|
||||
- Stripe product id: `prod_xxxxxxxxxxxxx` (optional)
|
||||
|
||||
**Growth Plan:**
|
||||
- Stripe price id: `price_xxxxxxxxxxxxx`
|
||||
|
||||
**Scale Plan:**
|
||||
- Stripe price id: `price_xxxxxxxxxxxxx`
|
||||
|
||||
3. Save each plan
|
||||
|
||||
---
|
||||
|
||||
## 4. PayPal Configuration
|
||||
|
||||
### 4.1 Getting PayPal Credentials
|
||||
|
||||
#### Step 1: Login to PayPal Developer Dashboard
|
||||
Go to [developer.paypal.com](https://developer.paypal.com)
|
||||
|
||||
#### Step 2: Create an App
|
||||
|
||||
1. Go to **My Apps & Credentials**
|
||||
2. Select **Sandbox** (for testing) or **Live** (for production)
|
||||
3. Click **"Create App"**
|
||||
4. Enter app name: `IGNY8 Payment Integration`
|
||||
5. Click **"Create App"**
|
||||
6. Copy:
|
||||
- **Client ID** (starts with `AY...` or similar)
|
||||
- **Secret** (click "Show" to reveal)
|
||||
|
||||
#### Step 3: Configure Webhooks
|
||||
|
||||
1. In your app settings, scroll to **"WEBHOOKS"**
|
||||
2. Click **"Add Webhook"**
|
||||
3. Enter webhook URL:
|
||||
```
|
||||
Sandbox: https://api-staging.igny8.com/api/v1/billing/webhooks/paypal/
|
||||
Live: https://api.igny8.com/api/v1/billing/webhooks/paypal/
|
||||
```
|
||||
4. Select event types:
|
||||
- `CHECKOUT.ORDER.APPROVED`
|
||||
- `PAYMENT.CAPTURE.COMPLETED`
|
||||
- `PAYMENT.CAPTURE.DENIED`
|
||||
- `BILLING.SUBSCRIPTION.ACTIVATED`
|
||||
- `BILLING.SUBSCRIPTION.CANCELLED`
|
||||
5. Click **"Save"**
|
||||
6. Copy the **Webhook ID** (starts with `WH-...`)
|
||||
|
||||
#### Step 4: Create Subscription Plans (Required for Subscriptions)
|
||||
|
||||
PayPal subscriptions require creating **Products** and **Plans** in the PayPal dashboard. This is a manual process (not done via API in our implementation).
|
||||
|
||||
##### 4.4.1 Create a PayPal Product
|
||||
|
||||
1. Go to [sandbox.paypal.com/billing/plans](https://www.sandbox.paypal.com/billing/plans) (Sandbox) or [paypal.com/billing/plans](https://www.paypal.com/billing/plans) (Live)
|
||||
2. Click **"Create product"** (or go to Products tab first)
|
||||
3. Fill in:
|
||||
- **Product name**: `IGNY8 Subscription`
|
||||
- **Product type**: `Service`
|
||||
- **Product ID**: `IGNY8-SUB` (or auto-generate)
|
||||
- **Description**: `IGNY8 subscription plans`
|
||||
4. Click **"Create product"**
|
||||
5. Note the **Product ID** (e.g., `PROD-xxxxxxxxxxxxx`)
|
||||
|
||||
##### 4.4.2 Create PayPal Plans (One Per IGNY8 Plan)
|
||||
|
||||
For each plan in your system (Starter, Growth, Scale), create a PayPal plan:
|
||||
|
||||
1. In PayPal dashboard, click **"Create plan"**
|
||||
2. Select the product you just created
|
||||
3. Fill in plan details:
|
||||
|
||||
**Starter Plan:**
|
||||
- **Plan name**: `Starter Plan - Monthly`
|
||||
- **Description**: `Basic plan for small projects`
|
||||
- **Pricing**:
|
||||
- Billing cycle: `Monthly`
|
||||
- Price: `$99.00 USD`
|
||||
- Total cycles: `0` (infinite)
|
||||
- **Setup fee**: `$0.00` (optional)
|
||||
4. Click **"Create plan"**
|
||||
5. **Copy the Plan ID** (starts with `P-...`, e.g., `P-5ML4271244454362WXXX`)
|
||||
|
||||
Repeat for Growth ($199/month) and Scale ($299/month) plans.
|
||||
|
||||
##### 4.4.3 Map PayPal Plan IDs to Django Plans
|
||||
|
||||
1. Go to Django Admin → **Auth → Plans**
|
||||
2. Edit **Starter Plan**:
|
||||
- Scroll to **"PayPal Integration"** section
|
||||
- **Paypal plan id**: Paste `P-xxxxxxxxxxxxx`
|
||||
3. Click **"Save"**
|
||||
4. Repeat for Growth and Scale plans
|
||||
|
||||
**Note:** Without `paypal_plan_id` set, the subscription creation API will return an error.
|
||||
|
||||
### 4.2 Adding to Django Admin
|
||||
|
||||
1. Go to Django Admin → **System → Integration providers**
|
||||
2. Click on **"paypal"**
|
||||
3. Fill in the fields:
|
||||
|
||||
```
|
||||
Provider ID: paypal (already set)
|
||||
Display name: PayPal (already set)
|
||||
Provider type: payment (already set)
|
||||
|
||||
API key: AYxxxxxxxxxxx (Client ID)
|
||||
API secret: ELxxxxxxxxxxx (Secret)
|
||||
Webhook secret: [leave empty - not used by PayPal]
|
||||
|
||||
API endpoint:
|
||||
Sandbox: https://api-m.sandbox.paypal.com
|
||||
Live: https://api-m.paypal.com
|
||||
|
||||
Config (JSON):
|
||||
{
|
||||
"currency": "USD",
|
||||
"webhook_id": "WH-xxxxxxxxxxxxx",
|
||||
"return_url": "https://app.igny8.com/account/plans?paypal=success",
|
||||
"cancel_url": "https://app.igny8.com/account/plans?paypal=cancel"
|
||||
}
|
||||
|
||||
✅ Is active: Checked
|
||||
✅ Is sandbox: Checked (for sandbox) / Unchecked (for live)
|
||||
```
|
||||
|
||||
4. Click **"Save"**
|
||||
|
||||
### 4.3 Live PayPal Payment Enablement (Production)
|
||||
|
||||
Use this section when switching from sandbox to live PayPal payments.
|
||||
|
||||
#### Step 1: Create/Select a Live App
|
||||
1. Go to [developer.paypal.com](https://developer.paypal.com)
|
||||
2. Select **Live** (top toggle)
|
||||
3. Create a new app or select your existing live app
|
||||
4. Copy the **Live Client ID** and **Live Secret**
|
||||
|
||||
#### Step 2: Configure Live Webhook
|
||||
1. In your live app settings, add a webhook:
|
||||
```
|
||||
https://api.igny8.com/api/v1/billing/webhooks/paypal/
|
||||
```
|
||||
2. Select events:
|
||||
- `CHECKOUT.ORDER.APPROVED`
|
||||
- `PAYMENT.CAPTURE.COMPLETED`
|
||||
- `PAYMENT.CAPTURE.DENIED`
|
||||
- `BILLING.SUBSCRIPTION.ACTIVATED`
|
||||
- `BILLING.SUBSCRIPTION.CANCELLED`
|
||||
3. Copy the **Live Webhook ID**
|
||||
|
||||
#### Step 3: Update Django Admin Provider (Live)
|
||||
1. Go to **System → Integration providers → paypal**
|
||||
2. Update fields:
|
||||
- **API key**: Live Client ID
|
||||
- **API secret**: Live Secret
|
||||
- **API endpoint**: `https://api-m.paypal.com`
|
||||
- **Config (JSON)**: set `webhook_id` to the live webhook ID
|
||||
3. Set:
|
||||
- ✅ `is_active` = True
|
||||
- ✅ `is_sandbox` = False
|
||||
4. Click **"Save"**
|
||||
|
||||
#### Step 3.1: Map PayPal Plan IDs in Django
|
||||
PayPal subscription webhooks only work if your plans are mapped.
|
||||
|
||||
1. Go to Django Admin → **Auth → Plans**
|
||||
2. For each plan, set:
|
||||
- **Paypal plan id**: Live PayPal Plan ID (starts with `P-...`)
|
||||
3. Save each plan
|
||||
|
||||
#### Step 4: Validate Live Payment Flow
|
||||
1. Open frontend: `/account/usage`
|
||||
2. Select **PayPal** and complete a real payment
|
||||
3. Confirm:
|
||||
- Order is captured
|
||||
- Credits are added
|
||||
- Payment email is sent
|
||||
|
||||
#### Step 5: Validate PayPal Subscriptions (If Enabled)
|
||||
1. Open frontend: `/account/plans`
|
||||
2. Select **PayPal** and subscribe to a plan
|
||||
3. Confirm:
|
||||
- Subscription is activated
|
||||
- Webhook events are processed
|
||||
- Account plan is updated
|
||||
|
||||
---
|
||||
|
||||
## 5. Resend Configuration
|
||||
|
||||
### 5.1 Getting Resend API Key
|
||||
|
||||
#### Step 1: Login to Resend
|
||||
Go to [resend.com](https://resend.com)
|
||||
|
||||
#### Step 2: Create API Key
|
||||
|
||||
1. Go to **API Keys**
|
||||
2. Click **"Create API Key"**
|
||||
3. Enter name: `IGNY8 Production` (or `IGNY8 Development`)
|
||||
4. Select permission: **"Sending access"**
|
||||
5. Click **"Add"**
|
||||
6. Copy the API key (starts with `re_...`)
|
||||
7. **Save it securely** - you won't see it again!
|
||||
|
||||
#### Step 3: Verify Your Domain
|
||||
|
||||
1. Go to **Domains**
|
||||
2. Click **"Add Domain"**
|
||||
3. Enter your domain: `igny8.com`
|
||||
4. Follow instructions to add DNS records:
|
||||
- **DKIM Record** (TXT)
|
||||
- **SPF Record** (TXT)
|
||||
- **DMARC Record** (TXT)
|
||||
5. Click **"Verify"**
|
||||
6. Wait for verification (can take a few minutes to 24 hours)
|
||||
|
||||
### 5.2 Adding to Django Admin
|
||||
|
||||
1. Go to Django Admin → **System → Integration providers**
|
||||
2. Click on **"resend"**
|
||||
3. Fill in the fields:
|
||||
|
||||
```
|
||||
Provider ID: resend (already set)
|
||||
Display name: Resend (already set)
|
||||
Provider type: email (already set)
|
||||
|
||||
API key: re_xxxxxxxxxxxxx
|
||||
API secret: [leave empty]
|
||||
Webhook secret: [leave empty]
|
||||
API endpoint: [leave empty - uses default]
|
||||
|
||||
Config (JSON):
|
||||
{
|
||||
"from_email": "noreply@igny8.com",
|
||||
"from_name": "IGNY8",
|
||||
"reply_to": "support@igny8.com"
|
||||
}
|
||||
|
||||
✅ Is active: Checked
|
||||
✅ Is sandbox: Unchecked (Resend doesn't have sandbox mode)
|
||||
```
|
||||
|
||||
4. Click **"Save"**
|
||||
|
||||
### 5.3 Email Settings Management
|
||||
|
||||
IGNY8 provides a dedicated **Email Settings** navigation group in Django Admin:
|
||||
|
||||
| Menu Item | URL | Purpose |
|
||||
|-----------|-----|---------|
|
||||
| Email Configuration | `/admin/system/emailsettings/` | Global email defaults (from, reply-to, feature flags) |
|
||||
| Email Templates | `/admin/system/emailtemplate/` | Manage/test email templates |
|
||||
| Email Logs | `/admin/system/emaillog/` | View sent email history |
|
||||
| Resend Provider | `/admin/system/integrationprovider/resend/change/` | API key & config |
|
||||
|
||||
**Email Configuration Settings:**
|
||||
- `from_email` - Default sender (must be verified in Resend)
|
||||
- `from_name` - Display name for sender
|
||||
- `reply_to_email` - Reply-to address
|
||||
- `send_welcome_emails` - Toggle welcome emails on/off
|
||||
- `send_billing_emails` - Toggle payment/invoice emails
|
||||
- `send_subscription_emails` - Toggle renewal reminders
|
||||
- `low_credit_threshold` - Credits level to trigger warning email
|
||||
|
||||
### 5.4 Testing Email Delivery
|
||||
|
||||
**Method 1: Django Admin UI (Recommended)**
|
||||
|
||||
1. Go to **Email Settings → Email Templates**
|
||||
2. Click the **"Test"** button next to any template
|
||||
3. Enter recipient email and customize context JSON
|
||||
4. Click **"Send Test Email"**
|
||||
5. Check **Email Logs** to verify delivery
|
||||
|
||||
**Method 2: Command Line (Docker)**
|
||||
|
||||
```bash
|
||||
docker exec -it igny8_backend python manage.py shell -c "
|
||||
from igny8_core.business.billing.services.email_service import get_email_service
|
||||
|
||||
service = get_email_service()
|
||||
result = service.send_transactional(
|
||||
to='your-email@example.com',
|
||||
subject='Test Email from IGNY8',
|
||||
html='<h1>Test Email</h1><p>If you receive this, Resend is configured correctly!</p>',
|
||||
text='Test Email. If you receive this, Resend is configured correctly!'
|
||||
)
|
||||
print('Result:', result)
|
||||
"
|
||||
```
|
||||
|
||||
**Expected successful response:**
|
||||
```python
|
||||
{'success': True, 'id': '81193754-6f27-4b1a-9c36-d83ae18f6a9a', 'provider': 'resend'}
|
||||
```
|
||||
|
||||
**Method 3: Test with Template**
|
||||
|
||||
```bash
|
||||
docker exec -it igny8_backend python manage.py shell -c "
|
||||
from igny8_core.business.billing.services.email_service import get_email_service
|
||||
|
||||
service = get_email_service()
|
||||
result = service.send_transactional(
|
||||
to='your-email@example.com',
|
||||
subject='Welcome Test',
|
||||
template='emails/welcome.html',
|
||||
context={
|
||||
'user_name': 'Test User',
|
||||
'account_name': 'Test Account',
|
||||
'login_url': 'https://app.igny8.com/login',
|
||||
'frontend_url': 'https://app.igny8.com',
|
||||
},
|
||||
tags=['test']
|
||||
)
|
||||
print('Result:', result)
|
||||
"
|
||||
```
|
||||
|
||||
### 5.5 Available Email Templates
|
||||
|
||||
| Template | Type | Trigger |
|
||||
|----------|------|---------|
|
||||
| `welcome` | Auth | User registration |
|
||||
| `password_reset` | Auth | Password reset request |
|
||||
| `email_verification` | Auth | Email verification |
|
||||
| `payment_confirmation` | Billing | Manual payment submitted |
|
||||
| `payment_approved` | Billing | Payment approved |
|
||||
| `payment_rejected` | Billing | Payment declined |
|
||||
| `payment_failed` | Billing | Auto-payment failed |
|
||||
| `subscription_activated` | Billing | Subscription activated |
|
||||
| `subscription_renewal` | Billing | Renewal reminder |
|
||||
| `refund_notification` | Billing | Refund processed |
|
||||
| `low_credits` | Notification | Credits below threshold |
|
||||
|
||||
### 5.6 Email Service API Reference
|
||||
|
||||
```python
|
||||
send_transactional(
|
||||
to: str | List[str], # Required: recipient email(s)
|
||||
subject: str, # Required: email subject
|
||||
html: str = None, # HTML content
|
||||
text: str = None, # Plain text content
|
||||
template: str = None, # Template path (e.g., 'emails/welcome.html')
|
||||
context: dict = None, # Template context variables
|
||||
from_email: str = None, # Override sender email
|
||||
from_name: str = None, # Override sender name
|
||||
reply_to: str = None, # Reply-to address
|
||||
attachments: List = None, # File attachments
|
||||
tags: List[str] = None # Email tags for tracking
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Plan & Pricing Configuration
|
||||
|
||||
### 6.1 Viewing Plans
|
||||
|
||||
1. Go to Django Admin → **Auth → Plans**
|
||||
2. You should see existing plans:
|
||||
- Free Plan
|
||||
- Starter Plan
|
||||
- Growth Plan
|
||||
- Scale Plan
|
||||
- Enterprise Plan
|
||||
|
||||
### 6.2 Editing Plan Details
|
||||
|
||||
For each plan:
|
||||
|
||||
```
|
||||
Name: Starter Plan
|
||||
Description: Perfect for small projects
|
||||
Price: 99.00
|
||||
Billing period: monthly
|
||||
Included credits: 5000
|
||||
Is active: ✅
|
||||
|
||||
Stripe price id: price_xxxxxxxxxxxxx (from Stripe dashboard)
|
||||
Stripe product id: prod_xxxxxxxxxxxxx (optional)
|
||||
Paypal plan id: P-xxxxxxxxxxxxx (if using PayPal subscriptions)
|
||||
|
||||
Feature limits:
|
||||
Max keywords: 50
|
||||
Max articles per month: 100
|
||||
Max team members: 3
|
||||
Max websites: 1
|
||||
```
|
||||
|
||||
### 6.3 Creating Custom Plans
|
||||
|
||||
1. Click **"Add plan"**
|
||||
2. Fill in all fields
|
||||
3. Make sure to set:
|
||||
- ✅ `is_active` = True (to show to users)
|
||||
- Stripe price ID (from Stripe dashboard)
|
||||
- Included credits (monthly allocation)
|
||||
4. Click **"Save"**
|
||||
|
||||
---
|
||||
|
||||
## 7. Credit Packages Configuration
|
||||
|
||||
### 7.1 Viewing Credit Packages
|
||||
|
||||
1. Go to Django Admin → **Billing → Credit packages**
|
||||
2. You should see existing packages:
|
||||
- Starter: 500 credits @ $9.99
|
||||
- Value: 2,000 credits @ $29.99
|
||||
- Pro: 5,000 credits @ $59.99
|
||||
- Enterprise: 15,000 credits @ $149.99
|
||||
|
||||
### 7.2 Editing Credit Packages
|
||||
|
||||
For each package:
|
||||
|
||||
```
|
||||
Name: Value Package
|
||||
Description: Best value for money
|
||||
Credits: 2000
|
||||
Price: 29.99
|
||||
Display order: 2
|
||||
|
||||
✅ Is active: Checked
|
||||
✅ Is featured: Checked (to highlight on UI)
|
||||
|
||||
Stripe product id: prod_xxxxxxxxxxxxx (optional - for tracking)
|
||||
Paypal product id: (optional)
|
||||
```
|
||||
|
||||
### 7.3 Creating Custom Credit Packages
|
||||
|
||||
1. Click **"Add credit package"**
|
||||
2. Fill in:
|
||||
- Name: e.g., "Black Friday Special"
|
||||
- Credits: e.g., 10000
|
||||
- Price: e.g., 79.99
|
||||
- Description: "Limited time offer!"
|
||||
3. Check ✅ `is_active`
|
||||
4. Check ✅ `is_featured` (optional)
|
||||
5. Click **"Save"**
|
||||
|
||||
---
|
||||
|
||||
## 8. Testing Checklist
|
||||
|
||||
### 8.1 Verify Integration Provider Settings
|
||||
|
||||
Go to Admin → **System → Integration providers**
|
||||
|
||||
- [ ] **Stripe** - API keys added, webhook secret added, is_active=True
|
||||
- [ ] **PayPal** - Client ID/Secret added, webhook ID in config, is_active=True
|
||||
- [ ] **Resend** - API key added, domain verified, is_active=True
|
||||
|
||||
### 8.2 Test Stripe Integration
|
||||
|
||||
1. **Get Config Endpoint:**
|
||||
```bash
|
||||
curl -X GET https://api.igny8.com/api/v1/billing/stripe/config/ \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
Should return publishable key and sandbox status
|
||||
|
||||
2. **Test Checkout Session:**
|
||||
- Go to frontend: `/account/plans`
|
||||
- Select "Stripe" as payment method
|
||||
- Click "Subscribe" on a plan
|
||||
- Should redirect to Stripe Checkout
|
||||
- Complete test payment with card: `4242 4242 4242 4242`
|
||||
- Should receive webhook and activate subscription
|
||||
|
||||
3. **Test Billing Portal:**
|
||||
- Click "Manage Subscription" button
|
||||
- Should redirect to Stripe Billing Portal
|
||||
- Can cancel/update subscription
|
||||
|
||||
### 8.3 Test PayPal Integration
|
||||
|
||||
#### 8.3.1 Verify PayPal Provider Config
|
||||
|
||||
```bash
|
||||
curl -X GET https://api.igny8.com/api/v1/billing/paypal/config/ \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
Should return `client_id` and `sandbox: true`.
|
||||
|
||||
#### 8.3.2 Test One-Time Credit Purchase (Orders API)
|
||||
|
||||
1. Go to frontend: `/account/usage`
|
||||
2. Select **PayPal** as payment method
|
||||
3. Click **"Buy Credits"** on a package
|
||||
4. Should redirect to PayPal sandbox
|
||||
5. Login with sandbox buyer account:
|
||||
- Email: `sb-buyer@personal.example.com` (from PayPal sandbox accounts)
|
||||
- Password: (your sandbox password)
|
||||
6. Complete payment
|
||||
7. **Verify:**
|
||||
- Order is captured (check webhook logs)
|
||||
- Credits are added to account
|
||||
- Payment email is sent
|
||||
|
||||
#### 8.3.3 Test PayPal Subscriptions (Subscriptions API)
|
||||
|
||||
**Prerequisites:**
|
||||
- [ ] PayPal Plans created in dashboard (see Section 4, Step 4)
|
||||
- [ ] `paypal_plan_id` set on each Django Plan (Auth → Plans)
|
||||
|
||||
**Test Steps:**
|
||||
|
||||
1. **Verify Plan Configuration:**
|
||||
```bash
|
||||
# Check if plan has paypal_plan_id
|
||||
docker exec -it igny8_backend python manage.py shell -c "
|
||||
from igny8_core.auth.models import Plan
|
||||
for p in Plan.objects.filter(is_active=True):
|
||||
print(f'{p.name}: paypal_plan_id={p.paypal_plan_id}')
|
||||
"
|
||||
```
|
||||
All paid plans should show a `P-xxxxx` ID.
|
||||
|
||||
2. **Create PayPal Subscription:**
|
||||
- Go to frontend: `/account/plans`
|
||||
- Select **PayPal** as payment method
|
||||
- Click **"Subscribe"** on Starter/Growth/Scale plan
|
||||
- Redirects to PayPal for approval
|
||||
- Login with sandbox buyer account
|
||||
- Approve the subscription
|
||||
|
||||
3. **Verify Subscription Activation:**
|
||||
- Check webhook logs: `BILLING.SUBSCRIPTION.ACTIVATED` should fire
|
||||
- Account plan should be updated
|
||||
- `Subscription` record created with `external_payment_id` = PayPal subscription ID
|
||||
- Credits added based on plan's `included_credits`
|
||||
|
||||
4. **Verify in Django Admin:**
|
||||
- Go to **Auth → Subscriptions**
|
||||
- Find the new subscription
|
||||
- Confirm:
|
||||
- `status` = `active`
|
||||
- `external_payment_id` = `I-xxxxx` (PayPal subscription ID)
|
||||
- `plan` = correct plan
|
||||
|
||||
5. **Test Subscription Cancellation:**
|
||||
- In PayPal sandbox, go to **Pay & Get Paid → Subscriptions**
|
||||
- Cancel the test subscription
|
||||
- `BILLING.SUBSCRIPTION.CANCELLED` webhook should fire
|
||||
- Subscription status should update to `canceled`
|
||||
|
||||
**Sandbox Test Accounts:**
|
||||
|
||||
Create sandbox accounts at [developer.paypal.com/dashboard/accounts](https://developer.paypal.com/dashboard/accounts):
|
||||
- **Business account** - receives payments (seller)
|
||||
- **Personal account** - makes payments (buyer)
|
||||
|
||||
Use the personal account credentials when approving payments/subscriptions.
|
||||
|
||||
### 8.4 Test Email Service
|
||||
|
||||
1. **Test Welcome Email:**
|
||||
```bash
|
||||
cd /data/app/igny8/backend
|
||||
python manage.py shell
|
||||
```
|
||||
```python
|
||||
from igny8_core.auth.models import User, Account
|
||||
from igny8_core.business.billing.services.email_service import send_welcome_email
|
||||
|
||||
user = User.objects.first()
|
||||
account = user.account
|
||||
send_welcome_email(user, account)
|
||||
```
|
||||
Check your inbox for welcome email
|
||||
|
||||
2. **Test Payment Confirmation:**
|
||||
- Complete a test payment (Stripe or PayPal)
|
||||
- Should receive payment confirmation email
|
||||
- Check email content and formatting
|
||||
|
||||
### 8.5 Test Webhooks
|
||||
|
||||
1. **Check Webhook Logs:**
|
||||
```bash
|
||||
cd /data/app/igny8/backend
|
||||
tail -f logs/django.log | grep webhook
|
||||
```
|
||||
|
||||
2. **Trigger Webhook Events:**
|
||||
- **Stripe:** Complete test checkout, then check webhook logs
|
||||
- **PayPal:** Complete test payment, then check webhook logs
|
||||
|
||||
3. **Verify Webhook Processing:**
|
||||
- Subscription should be activated
|
||||
- Credits should be added
|
||||
- Email notifications should be sent
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Admin URLs
|
||||
|
||||
```
|
||||
# Main sections
|
||||
/admin/system/integrationprovider/ # All integration providers
|
||||
/admin/auth/plan/ # Plans and pricing
|
||||
/admin/billing/creditpackage/ # Credit packages
|
||||
/admin/billing/payment/ # Payment history
|
||||
/admin/billing/invoice/ # Invoices
|
||||
/admin/auth/subscription/ # Active subscriptions
|
||||
/admin/billing/credittransaction/ # Credit transaction history
|
||||
|
||||
# Specific provider configs
|
||||
/admin/system/integrationprovider/stripe/change/
|
||||
/admin/system/integrationprovider/paypal/change/
|
||||
/admin/system/integrationprovider/resend/change/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Never Commit API Keys
|
||||
- ❌ Don't add API keys to code
|
||||
- ❌ Don't commit `.env` files
|
||||
- ✅ Use Django Admin to store credentials
|
||||
- ✅ Use IntegrationProvider model (encrypted in DB)
|
||||
|
||||
### Use Environment-Specific Keys
|
||||
- **Development:** Use Stripe test mode, PayPal sandbox
|
||||
- **Staging:** Use separate test credentials
|
||||
- **Production:** Use live credentials ONLY in production
|
||||
|
||||
### Regular Key Rotation
|
||||
- Rotate API keys every 90 days
|
||||
- Rotate webhook secrets if compromised
|
||||
- Keep backup of old keys during rotation
|
||||
|
||||
### Monitor Webhook Security
|
||||
- Verify webhook signatures always
|
||||
- Log all webhook attempts
|
||||
- Alert on failed signature verification
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Stripe Issues
|
||||
|
||||
**"No such customer"**
|
||||
- Check if `stripe_customer_id` is set on Account model
|
||||
- Clear the field and let system recreate customer
|
||||
|
||||
**"Invalid API Key"**
|
||||
- Verify API key in IntegrationProvider
|
||||
- Check if using test key in live mode (or vice versa)
|
||||
|
||||
**Webhook not working**
|
||||
- Check webhook URL in Stripe dashboard
|
||||
- Verify webhook secret in IntegrationProvider
|
||||
- Check server logs for errors
|
||||
|
||||
### PayPal Issues
|
||||
|
||||
**"Invalid client credentials"**
|
||||
- Verify Client ID and Secret in IntegrationProvider
|
||||
- Make sure using sandbox credentials for sandbox mode
|
||||
|
||||
**"Webhook verification failed"**
|
||||
- Check webhook_id in config JSON
|
||||
- Verify webhook URL in PayPal dashboard
|
||||
|
||||
**Order capture fails**
|
||||
- Check order status (must be APPROVED)
|
||||
- Verify order hasn't already been captured
|
||||
|
||||
**"PayPal plan ID not configured for this plan"**
|
||||
- The Plan model is missing `paypal_plan_id`
|
||||
- Go to Django Admin → **Auth → Plans**
|
||||
- Edit the plan and add the PayPal Plan ID (starts with `P-...`)
|
||||
- Create the plan in PayPal dashboard first if not done
|
||||
|
||||
**"No plan found with paypal_plan_id=..."**
|
||||
- Webhook received but no matching plan in Django
|
||||
- Verify the `paypal_plan_id` in Django matches exactly what's in PayPal
|
||||
- Check for typos or extra whitespace
|
||||
|
||||
**Subscription not activating after approval**
|
||||
- Check webhook logs for `BILLING.SUBSCRIPTION.ACTIVATED` event
|
||||
- Verify webhook URL is correctly configured in PayPal app
|
||||
- Check that `webhook_id` in config JSON matches PayPal dashboard
|
||||
- Ensure sandbox/live environment matches between app and PayPal
|
||||
|
||||
**PayPal subscription appears but no credits added**
|
||||
- Check `included_credits` field on the Plan model
|
||||
- Verify subscription webhook handler completed successfully
|
||||
- Look for errors in Django logs during webhook processing
|
||||
|
||||
### Resend Issues
|
||||
|
||||
**"Invalid API key"**
|
||||
- Verify API key starts with `re_`
|
||||
- Create new API key if needed
|
||||
|
||||
**"Domain not verified"**
|
||||
- Check DNS records in domain provider
|
||||
- Wait up to 24 hours for DNS propagation
|
||||
- Use Resend dashboard to verify domain status
|
||||
|
||||
**Emails not delivered**
|
||||
- Check Resend dashboard logs
|
||||
- Verify from_email domain is verified
|
||||
- Check spam folder
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
After configuring all providers:
|
||||
|
||||
1. ✅ Test all payment flows in sandbox mode
|
||||
2. ✅ Test email delivery
|
||||
3. ✅ Verify webhook processing
|
||||
4. ✅ Test frontend payment gateway selection
|
||||
5. ✅ Switch to production credentials when ready to go live
|
||||
|
||||
For production deployment, update:
|
||||
- `is_sandbox` = False for Stripe
|
||||
- `is_sandbox` = False for PayPal
|
||||
- `api_endpoint` = production URLs
|
||||
- Use live API keys for all providers
|
||||
|
||||
---
|
||||
|
||||
**Support:** If you encounter issues, check Django logs:
|
||||
```bash
|
||||
cd /data/app/igny8/backend
|
||||
tail -f logs/django.log
|
||||
```
|
||||
553
v2/Live Docs on Server/igny8-app-docs/90-REFERENCE/FIXES-KB.md
Normal file
553
v2/Live Docs on Server/igny8-app-docs/90-REFERENCE/FIXES-KB.md
Normal file
@@ -0,0 +1,553 @@
|
||||
# Architecture Knowledge Base
|
||||
**Last Updated:** January 20, 2026
|
||||
**Version:** 1.8.4
|
||||
**Purpose:** Critical architectural patterns, common issues, and solutions reference
|
||||
|
||||
---
|
||||
|
||||
## 🔥 CRITICAL FIXES - December 2025
|
||||
|
||||
### PERMANENT FIX: Django Admin Custom Sidebar Not Showing on Subpages
|
||||
**ROOT CAUSE**: Django's `ModelAdmin` view methods (`changelist_view`, `change_view`, etc.) do not call `AdminSite.each_context()`, so custom sidebar logic defined in `site.py` was bypassed on model list/detail/edit pages.
|
||||
|
||||
**SOLUTION IMPLEMENTED**:
|
||||
1. ✅ Created `Igny8ModelAdmin` base class extending `UnfoldModelAdmin`
|
||||
2. ✅ Overrides all view methods to inject `extra_context` with custom sidebar
|
||||
3. ✅ Applied to 46+ admin classes across all modules
|
||||
4. ✅ Sidebar now consistent on homepage, app index, and ALL model pages
|
||||
|
||||
**Files Modified**: `backend/igny8_core/admin/base.py`, all `*/admin.py` files
|
||||
|
||||
### PERMANENT FIX: User Swapping / Random Logout Issue
|
||||
**ROOT CAUSE**: Django's database-backed sessions with in-memory user caching caused cross-request contamination at the process level.
|
||||
|
||||
**SOLUTION IMPLEMENTED**:
|
||||
1. ✅ Redis-backed sessions (`SESSION_ENGINE = 'django.contrib.sessions.backends.cache'`)
|
||||
2. ✅ Custom authentication backend without caching (`NoCacheModelBackend`)
|
||||
3. ✅ Session integrity validation (stores and verifies account_id/user_id on every request)
|
||||
4. ✅ Middleware never mutates `request.user` (uses Django's set value directly)
|
||||
|
||||
**See**: `CRITICAL-BUG-FIXES-DEC-2025.md` for complete details.
|
||||
|
||||
### PERMANENT FIX: useNavigate / useLocation Errors During HMR
|
||||
**ROOT CAUSE**: Individual Suspense boundaries per route lost React Router context during Hot Module Replacement.
|
||||
|
||||
**SOLUTION IMPLEMENTED**:
|
||||
1. ✅ Single top-level Suspense boundary around entire `<Routes>` component
|
||||
2. ✅ Removed 100+ individual Suspense wrappers from route elements
|
||||
3. ✅ Router context now persists through HMR automatically
|
||||
|
||||
**See**: `CRITICAL-BUG-FIXES-DEC-2025.md` for complete details.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
1. [Authentication & Session Management](#authentication--session-management)
|
||||
2. [Site/Sector Architecture](#sitesector-architecture)
|
||||
3. [State Management & Race Conditions](#state-management--race-conditions)
|
||||
4. [Permission System](#permission-system)
|
||||
5. [Frontend Component Dependencies](#frontend-component-dependencies)
|
||||
6. [Common Pitfalls & Solutions](#common-pitfalls--solutions)
|
||||
|
||||
---
|
||||
|
||||
## Authentication & Session Management
|
||||
|
||||
### Token Persistence Architecture
|
||||
|
||||
**Problem Pattern:**
|
||||
- Zustand persist middleware writes to localStorage asynchronously
|
||||
- API calls can happen before tokens are persisted
|
||||
- Results in 403 "Authentication credentials were not provided" errors
|
||||
|
||||
**Solution Implemented:**
|
||||
```typescript
|
||||
// In authStore.ts login/register functions
|
||||
// CRITICAL: Immediately persist tokens synchronously after setting state
|
||||
const authState = {
|
||||
state: { user, token, refreshToken, isAuthenticated: true },
|
||||
version: 0
|
||||
};
|
||||
localStorage.setItem('auth-storage', JSON.stringify(authState));
|
||||
```
|
||||
|
||||
**Key Principle:** Always write tokens to localStorage synchronously in auth actions, don't rely solely on persist middleware.
|
||||
|
||||
---
|
||||
|
||||
### Logout & State Cleanup
|
||||
|
||||
**WRONG APPROACH (causes race conditions):**
|
||||
```typescript
|
||||
logout: () => {
|
||||
localStorage.clear(); // ❌ BREAKS EVERYTHING
|
||||
set({ user: null, token: null });
|
||||
}
|
||||
```
|
||||
|
||||
**CORRECT APPROACH:**
|
||||
```typescript
|
||||
logout: () => {
|
||||
// ✅ Selective removal - only auth-related keys
|
||||
const authKeys = ['auth-storage', 'site-storage', 'sector-storage', 'billing-storage'];
|
||||
authKeys.forEach(key => localStorage.removeItem(key));
|
||||
|
||||
// ✅ Reset dependent stores explicitly
|
||||
useSiteStore.setState({ activeSite: null });
|
||||
useSectorStore.setState({ activeSector: null, sectors: [] });
|
||||
|
||||
set({ user: null, token: null, isAuthenticated: false });
|
||||
}
|
||||
```
|
||||
|
||||
**Key Principle:** Never use `localStorage.clear()` - it breaks Zustand persist middleware initialization. Always selectively remove keys.
|
||||
|
||||
---
|
||||
|
||||
### 403 Error Handling
|
||||
|
||||
**Problem Pattern:**
|
||||
- 403 errors thrown before checking if it's an auth error
|
||||
- Token validation code becomes unreachable
|
||||
- Invalid tokens persist in localStorage
|
||||
|
||||
**WRONG ORDER:**
|
||||
```typescript
|
||||
// In api.ts
|
||||
if (response.status === 403) {
|
||||
throw new Error(response.statusText); // ❌ Thrown immediately
|
||||
}
|
||||
|
||||
// This code NEVER runs (unreachable):
|
||||
if (errorData?.detail?.includes('Authentication credentials')) {
|
||||
logout(); // Never called!
|
||||
}
|
||||
```
|
||||
|
||||
**CORRECT ORDER:**
|
||||
```typescript
|
||||
// Check for auth errors FIRST, then throw
|
||||
if (response.status === 403) {
|
||||
const errorData = JSON.parse(text);
|
||||
|
||||
// ✅ Check authentication BEFORE throwing
|
||||
if (errorData?.detail?.includes('Authentication credentials')) {
|
||||
const authState = useAuthStore.getState();
|
||||
if (authState?.isAuthenticated) {
|
||||
authState.logout();
|
||||
window.location.href = '/signin';
|
||||
}
|
||||
}
|
||||
|
||||
// Now throw the error
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
```
|
||||
|
||||
**Key Principle:** Handle authentication errors before throwing. Order matters in error handling logic.
|
||||
|
||||
---
|
||||
|
||||
## Site/Sector Architecture
|
||||
|
||||
### Data Hierarchy
|
||||
```
|
||||
Account (Tenant)
|
||||
└── Site (e.g., myblog.com)
|
||||
└── Sector (e.g., Technology, Health)
|
||||
└── Keywords
|
||||
└── Clusters
|
||||
└── Ideas
|
||||
└── Content
|
||||
```
|
||||
|
||||
### Where Sectors Are Used (Global Context)
|
||||
|
||||
**USES SECTORS (requires site/sector selection):**
|
||||
- ✅ Planner Module (Keywords, Clusters, Ideas)
|
||||
- ✅ Writer Module (Tasks, Content, Drafts, Published)
|
||||
- ✅ Linker Module (Internal linking)
|
||||
- ✅ Optimizer Module (Content optimization)
|
||||
- ✅ Setup/Add Keywords page
|
||||
- ✅ Seed Keywords reference data
|
||||
|
||||
**DOES NOT USE SECTORS (account-level only):**
|
||||
- ❌ Billing/Plans pages (`/account/*`)
|
||||
- ❌ Account Settings
|
||||
- ❌ Team Management
|
||||
- ❌ User Profile
|
||||
- ❌ Admin Dashboard
|
||||
- ❌ System Settings
|
||||
|
||||
### Sector Loading Pattern
|
||||
|
||||
**Architecture Decision:**
|
||||
- Sectors loaded by **PageHeader component** (not AppLayout)
|
||||
- Only loads when `hideSiteSector={false}` prop is set
|
||||
- Account/billing pages pass `hideSiteSector={true}` to skip loading
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// PageHeader.tsx
|
||||
useEffect(() => {
|
||||
if (hideSiteSector) return; // Skip for account pages
|
||||
|
||||
const currentSiteId = activeSite?.id ?? null;
|
||||
if (currentSiteId && activeSite?.is_active) {
|
||||
loadSectorsForSite(currentSiteId);
|
||||
}
|
||||
}, [activeSite?.id, hideSiteSector]);
|
||||
```
|
||||
|
||||
**Key Principle:** Lazy-load sectors only when components need them. Don't load globally for all pages.
|
||||
|
||||
---
|
||||
|
||||
### Site/Sector Store Persistence
|
||||
|
||||
**Storage Keys:**
|
||||
- `site-storage` - Active site selection
|
||||
- `sector-storage` - Active sector selection
|
||||
|
||||
**Reset Pattern:**
|
||||
```typescript
|
||||
// When site changes, reset sector if it belongs to different site
|
||||
if (currentSector && currentSector.site_id !== newSiteId) {
|
||||
set({ activeSector: null });
|
||||
localStorage.setItem('sector-storage', JSON.stringify({
|
||||
state: { activeSector: null },
|
||||
version: 0
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
**Key Principle:** Sector selection is site-scoped. Always validate sector belongs to active site.
|
||||
|
||||
---
|
||||
|
||||
## State Management & Race Conditions
|
||||
|
||||
### Common Race Condition Patterns
|
||||
|
||||
#### 1. User Switching
|
||||
**Problem:** Rapid logout → login leaves stale state in stores
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
logout: () => {
|
||||
// Reset ALL dependent stores explicitly
|
||||
import('./siteStore').then(({ useSiteStore }) => {
|
||||
useSiteStore.setState({ activeSite: null, loading: false, error: null });
|
||||
});
|
||||
import('./sectorStore').then(({ useSectorStore }) => {
|
||||
useSectorStore.setState({ activeSector: null, sectors: [], loading: false, error: null });
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. API Calls Before Token Persistence
|
||||
**Problem:** API calls happen before Zustand persist writes token
|
||||
|
||||
**Solution:** Synchronous localStorage write immediately after state update (see Authentication section)
|
||||
|
||||
#### 3. Module Loading Failures
|
||||
**Problem:** 404 errors during page navigation cause module loading to fail
|
||||
|
||||
**Solution:** Ensure API endpoints exist before pages try to load them. Use conditional rendering based on route.
|
||||
|
||||
---
|
||||
|
||||
### Zustand Persist Middleware Gotchas
|
||||
|
||||
**Issue 1: Version Mismatch**
|
||||
```typescript
|
||||
// Stored format
|
||||
{ state: { user, token }, version: 0 }
|
||||
|
||||
// If version changes, persist middleware clears state
|
||||
```
|
||||
|
||||
**Issue 2: Async Hydration**
|
||||
- State rehydration from localStorage is async
|
||||
- Can cause brief flash of "no user" state
|
||||
|
||||
**Solution:** Use loading states or check both store AND localStorage:
|
||||
```typescript
|
||||
const getAuthToken = (): string | null => {
|
||||
// Try Zustand store first
|
||||
const authState = useAuthStore.getState();
|
||||
if (authState?.token) return authState.token;
|
||||
|
||||
// Fallback to localStorage
|
||||
const stored = localStorage.getItem('auth-storage');
|
||||
return JSON.parse(stored)?.state?.token || null;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Permission System
|
||||
|
||||
### Superuser/Developer Bypass Pattern
|
||||
|
||||
**Critical Locations for Bypass:**
|
||||
1. Middleware - `auth/middleware.py`
|
||||
2. Permission Classes - `api/permissions.py`
|
||||
3. ViewSet Querysets - `api/base.py`
|
||||
4. Validation Functions - `auth/utils.py`
|
||||
|
||||
**Standard Bypass Check:**
|
||||
```python
|
||||
def check_bypass(user):
|
||||
return (
|
||||
user.is_superuser or
|
||||
user.role == 'developer' or
|
||||
is_system_account_user(user)
|
||||
)
|
||||
```
|
||||
|
||||
**Apply at ALL levels:**
|
||||
- Middleware request validation
|
||||
- DRF permission `has_permission()`
|
||||
- ViewSet `get_queryset()` filtering
|
||||
- Custom validation functions
|
||||
|
||||
**Key Principle:** Bypass checks must be consistent across all permission layers. Missing one layer breaks superuser access.
|
||||
|
||||
---
|
||||
|
||||
### System Account Pattern
|
||||
|
||||
**Reserved Accounts:**
|
||||
- `aws-admin` - System automation account
|
||||
- `default-account` - Default tenant fallback
|
||||
|
||||
**Check Function:**
|
||||
```python
|
||||
def is_system_account_user(user):
|
||||
if not user or not user.account:
|
||||
return False
|
||||
return user.account.slug in ['aws-admin', 'default-account']
|
||||
```
|
||||
|
||||
**Usage:** Always include in bypass checks alongside superuser/developer.
|
||||
|
||||
---
|
||||
|
||||
## Frontend Component Dependencies
|
||||
|
||||
### PageHeader Component
|
||||
**Dependencies:**
|
||||
- `useSiteStore` - Active site
|
||||
- `useSectorStore` - Active sector
|
||||
- `SiteAndSectorSelector` - Dropdown component
|
||||
|
||||
**Props:**
|
||||
- `hideSiteSector: boolean` - Skip site/sector display and loading
|
||||
- `title: string` - Page title
|
||||
- `navigation: ReactNode` - Optional module tabs
|
||||
|
||||
**Used By:**
|
||||
- All Planner pages
|
||||
- All Writer pages
|
||||
- All Optimizer pages
|
||||
- Setup pages
|
||||
- Seed Keywords page
|
||||
|
||||
**NOT Used By:**
|
||||
- Account/billing pages (use plain headers instead)
|
||||
|
||||
---
|
||||
|
||||
### Module Navigation Pattern
|
||||
|
||||
**Component:** `ModuleNavigationTabs.tsx`
|
||||
|
||||
**CRITICAL:** Must be wrapped in `<Router>` context
|
||||
- Uses `useLocation()` and `useNavigate()` hooks
|
||||
- Cannot be used outside `<Routes>` tree
|
||||
|
||||
**Common Error:**
|
||||
```
|
||||
Error: useLocation() may be used only in the context of a <Router> component
|
||||
```
|
||||
|
||||
**Cause:** Component rendered outside React Router context
|
||||
|
||||
**Solution:** Ensure component is within `<Route>` element in App.tsx
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls & Solutions
|
||||
|
||||
### Pitfall 1: Frontend 403 Errors After User Switch
|
||||
|
||||
**Symptoms:**
|
||||
- "Authentication credentials were not provided"
|
||||
- User appears logged in but API calls fail
|
||||
- Manually clearing cache fixes it
|
||||
|
||||
**Root Cause:** Invalid tokens persisting in localStorage after logout
|
||||
|
||||
**Solution:**
|
||||
1. Check 403 handler runs BEFORE throwing error
|
||||
2. Ensure logout clears specific auth keys (not `localStorage.clear()`)
|
||||
3. Add immediate token persistence after login
|
||||
|
||||
**Prevention:** See "Authentication & Session Management" section
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: Sector 404 Errors on Billing Pages
|
||||
|
||||
**Symptoms:**
|
||||
- `GET /v1/auth/sites/{id}/sectors/` returns 404
|
||||
- "Failed to fetch dynamically imported module" error
|
||||
- Billing pages don't load
|
||||
|
||||
**Root Cause:** AppLayout loading sectors for ALL pages globally
|
||||
|
||||
**Solution:** Move sector loading to PageHeader component (lazy loading)
|
||||
|
||||
**Prevention:** Only load data when components that need it are mounted
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: Module Loading Failures After Git Commits
|
||||
|
||||
**Symptoms:**
|
||||
- React Router context errors
|
||||
- "useLocation() may be used only in context of <Router>" errors
|
||||
- Pages work after rebuild but fail after git push
|
||||
|
||||
**Root Cause:** Docker build cache not invalidated properly
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Force clean rebuild
|
||||
docker compose -f docker-compose.app.yml down
|
||||
docker compose -f docker-compose.app.yml build --no-cache igny8_frontend
|
||||
docker compose -f docker-compose.app.yml up -d
|
||||
```
|
||||
|
||||
**Prevention:** Use `--no-cache` flag when rebuilding after major changes
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 4: Plan Selection Issues in Pricing Page
|
||||
|
||||
**Symptoms:**
|
||||
- Monthly/Annual toggle missing
|
||||
- Pre-selected plan not highlighted
|
||||
- Discount calculation wrong
|
||||
|
||||
**Root Cause:**
|
||||
1. PricingTable component missing `showToggle` prop
|
||||
2. Backend missing `is_featured` and `annual_discount_percent` fields
|
||||
3. Frontend not calculating annual price from discount
|
||||
|
||||
**Solution:**
|
||||
1. Add fields to Plan model with migration
|
||||
2. Pass `annualDiscountPercent` to PricingTable
|
||||
3. Calculate: `annualPrice = monthlyPrice * 12 * (1 - discount/100)`
|
||||
|
||||
**Files Modified:**
|
||||
- `backend/igny8_core/auth/models.py`
|
||||
- `backend/igny8_core/auth/serializers.py`
|
||||
- `frontend/src/services/billing.api.ts`
|
||||
- `frontend/src/components/ui/pricing-table/PricingTable.tsx`
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 5: Adjacent JSX Elements Error
|
||||
|
||||
**Symptoms:**
|
||||
- "Adjacent JSX elements must be wrapped in an enclosing tag"
|
||||
- Build fails but line numbers don't help
|
||||
|
||||
**Root Cause:** Mismatched opening/closing tags (usually missing `</div>`)
|
||||
|
||||
**Debugging Strategy:**
|
||||
1. Use TypeScript compiler: `npx tsc --noEmit <file>`
|
||||
2. Count opening vs closing tags: `grep -c "<div" vs grep -c "</div>"`
|
||||
3. Check conditionals have matching closing parens/braces
|
||||
|
||||
**Common Pattern:**
|
||||
```tsx
|
||||
{condition && (
|
||||
<div>
|
||||
{/* Content */}
|
||||
</div>
|
||||
{/* Missing closing parenthesis causes "adjacent elements" error */}
|
||||
}
|
||||
```
|
||||
|
||||
**Solution:** Ensure every opening bracket has matching close bracket
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
### State Management
|
||||
✅ **DO:** Immediately persist auth tokens synchronously
|
||||
✅ **DO:** Selectively remove localStorage keys
|
||||
✅ **DO:** Reset dependent stores on logout
|
||||
❌ **DON'T:** Use `localStorage.clear()`
|
||||
❌ **DON'T:** Rely solely on Zustand persist middleware timing
|
||||
|
||||
### Error Handling
|
||||
✅ **DO:** Check authentication errors BEFORE throwing
|
||||
✅ **DO:** Force logout on invalid tokens
|
||||
✅ **DO:** Redirect to login after logout
|
||||
❌ **DON'T:** Throw errors before checking auth status
|
||||
❌ **DON'T:** Leave invalid tokens in storage
|
||||
|
||||
### Component Architecture
|
||||
✅ **DO:** Lazy-load data at component level
|
||||
✅ **DO:** Skip unnecessary data loading (hideSiteSector pattern)
|
||||
✅ **DO:** Keep components in Router context
|
||||
❌ **DON'T:** Load data globally in AppLayout
|
||||
❌ **DON'T:** Use Router hooks outside Router context
|
||||
|
||||
### Permission System
|
||||
✅ **DO:** Implement bypass at ALL permission layers
|
||||
✅ **DO:** Include system accounts in bypass checks
|
||||
✅ **DO:** Use consistent bypass logic everywhere
|
||||
❌ **DON'T:** Forget middleware layer bypass
|
||||
❌ **DON'T:** Mix permission approaches
|
||||
|
||||
### Docker Builds
|
||||
✅ **DO:** Use `--no-cache` after major changes
|
||||
✅ **DO:** Restart containers after rebuilds
|
||||
✅ **DO:** Check logs for module loading errors
|
||||
❌ **DON'T:** Trust build cache after git commits
|
||||
❌ **DON'T:** Deploy without testing fresh build
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: File Locations
|
||||
|
||||
### Authentication
|
||||
- Token handling: `frontend/src/services/api.ts`
|
||||
- Auth store: `frontend/src/store/authStore.ts`
|
||||
- Middleware: `backend/igny8_core/auth/middleware.py`
|
||||
|
||||
### Permissions
|
||||
- Permission classes: `backend/igny8_core/api/permissions.py`
|
||||
- Base viewsets: `backend/igny8_core/api/base.py`
|
||||
- Validation utils: `backend/igny8_core/auth/utils.py`
|
||||
|
||||
### Site/Sector
|
||||
- Site store: `frontend/src/store/siteStore.ts`
|
||||
- Sector store: `frontend/src/store/sectorStore.ts`
|
||||
- PageHeader: `frontend/src/components/common/PageHeader.tsx`
|
||||
|
||||
### Billing
|
||||
- Billing API: `frontend/src/services/billing.api.ts`
|
||||
- Plans page: `frontend/src/pages/account/PlansAndBillingPage.tsx`
|
||||
- Plan model: `backend/igny8_core/auth/models.py`
|
||||
|
||||
---
|
||||
|
||||
**End of Knowledge Base**
|
||||
*Update this document when architectural patterns change or new common issues are discovered.*
|
||||
@@ -0,0 +1,146 @@
|
||||
# Managed Add-on Plans (Marketing + App)
|
||||
|
||||
**Date:** 2026-01-20
|
||||
|
||||
## Goals
|
||||
- Offer managed services as an optional add-on per site.
|
||||
- Keep core SaaS plans unchanged while enabling add-on selection at pricing, signup, and billing.
|
||||
- Provide clear separation between **Core Plans** and **Managed Add-ons** in backend, frontend, and billing UI.
|
||||
|
||||
---
|
||||
|
||||
## Proposed Managed Add-on Tiers
|
||||
- **Managed Lite** — **$100/site/month**
|
||||
- **Managed Pro** — **$399/site/month**
|
||||
|
||||
### Managed Features (shared)
|
||||
- Onboarding & setup: site integration, automation schedule, content settings.
|
||||
- Monthly SEO content plan: keyword import from library/clustering, topic strategy, content calendar.
|
||||
- Content QA & optimization: review queue checks, SEO meta validation, internal link suggestions.
|
||||
- Publishing ops: scheduled publishing, status monitoring, retry/failure handling.
|
||||
- Reporting: monthly performance + usage summary (credits, content velocity, publishing outcomes).
|
||||
- Support & tuning: strategy optimization/tweaks, automation adjustments, issue triage.
|
||||
|
||||
### Pro extras
|
||||
- Proactive monitoring and escalation.
|
||||
- Priority response.
|
||||
- Expanded strategy iteration (more frequent adjustments).
|
||||
|
||||
---
|
||||
|
||||
## Marketing Site (https://igny8.com/pricing)
|
||||
|
||||
### Layout changes
|
||||
1. **Keep Core Plans section unchanged**.
|
||||
2. Add a **big + icon** directly below the pricing table.
|
||||
3. Add **one single horizontal card** for Managed Add-on:
|
||||
- Visible badge: **COMING SOON**
|
||||
- Card title: “Managed Add-on (Per Site)”
|
||||
- Short summary of major features (1 line)
|
||||
- **Toggle switch** inside the card for **Managed Lite / Managed Pro**
|
||||
- Show price per site for the selected toggle
|
||||
4. No other sections or FAQs added.
|
||||
|
||||
### Suggested UX copy
|
||||
- “Managed Add-on (Per Site) — Coming Soon”
|
||||
- “Choose Lite or Pro”
|
||||
|
||||
---
|
||||
|
||||
## Signup Page (https://app.igny8.com/signup)
|
||||
|
||||
### Layout changes
|
||||
Add **Step 2: Managed Add-ons (Optional)** after plan selection.
|
||||
- Toggle per site: “Add managed services to selected site(s)”
|
||||
- If user selects a plan with multiple sites:
|
||||
- Show checkboxes for each site slot.
|
||||
- Default: none selected.
|
||||
- Inline price calculator:
|
||||
- “Managed Lite x N sites = $X/mo”
|
||||
- “Managed Pro x N sites = $X/mo”
|
||||
|
||||
### UX notes
|
||||
- Keep signup friction low.
|
||||
- If user skips add-on, allow adding later from Billing.
|
||||
|
||||
---
|
||||
|
||||
## App Billing & Plans (Account → Plans & Billing)
|
||||
|
||||
### New UI sections
|
||||
1. **Current Plan** remains unchanged.
|
||||
2. Add **“Managed Add-ons”** section:
|
||||
- Show current add-on tier (if any) and assigned sites.
|
||||
- Show monthly add-on price and next renewal date.
|
||||
3. Add **“Upgrade Add-ons”** tab or sub-panel:
|
||||
- Choose Managed Lite/Pro.
|
||||
- Assign to site(s).
|
||||
- Update monthly total.
|
||||
|
||||
### Existing users
|
||||
- If a user already subscribed to a managed add-on:
|
||||
- Display in **Plan** tab summary.
|
||||
- Include in billing history and invoice breakdown.
|
||||
|
||||
---
|
||||
|
||||
## Backend Model Changes
|
||||
|
||||
### Option A (Minimal changes in Plan model)
|
||||
Add fields to `Plan`:
|
||||
- `plan_type` (choices: `core`, `managed`) — distinguishes SaaS vs add-on.
|
||||
- `per_site` (bool, default false) — marks managed add-ons.
|
||||
- `managed_tier` (optional slug: `lite`, `pro`).
|
||||
|
||||
Add optional relation to `Account` or `Site`:
|
||||
- New model `SiteManagementAddon`:
|
||||
- `site` (FK)
|
||||
- `plan` (FK to Plan where `plan_type=managed`)
|
||||
- `status`, `current_period_start`, `current_period_end`
|
||||
- `external_subscription_id`
|
||||
|
||||
### Option B (Separate ManagedPlan model)
|
||||
Create `ManagedPlan` model (clone of Plan fields needed for pricing + name).
|
||||
Keep `Plan` for core SaaS only.
|
||||
|
||||
**Recommendation:** Option A (fewer tables, uses existing pricing pipeline).
|
||||
|
||||
---
|
||||
|
||||
## Backend Billing Logic
|
||||
- Managed add-ons are **per site**.
|
||||
- Create separate Stripe subscription items per site, or a single subscription with quantity = number of managed sites.
|
||||
- Billing summary should show:
|
||||
- Core plan price
|
||||
- Managed add-on subtotal (N sites x price)
|
||||
- Total monthly
|
||||
|
||||
---
|
||||
|
||||
## Frontend Data Contracts
|
||||
|
||||
### API additions
|
||||
- `GET /api/v1/auth/plans/?type=core` (core plans only)
|
||||
- `GET /api/v1/auth/plans/?type=managed` (managed add-ons)
|
||||
- `GET /api/v1/account/managed-addons/` (current user add-ons + site assignments)
|
||||
- `POST /api/v1/account/managed-addons/` (assign add-on to site(s))
|
||||
- `PUT /api/v1/account/managed-addons/{id}/` (upgrade/downgrade add-on tier)
|
||||
|
||||
---
|
||||
|
||||
## Pricing Copy (Core Plans)
|
||||
Suggested renames to keep consistency:
|
||||
- Starter → **Launch**
|
||||
- Growth → **Growth** (keep)
|
||||
- Scale → **Scale** (keep)
|
||||
|
||||
---
|
||||
|
||||
## Rollout Checklist
|
||||
- Add plan_type + per_site fields + migration.
|
||||
- Add managed add-on seed data (Lite/Pro).
|
||||
- Add managed add-on endpoints + serializer filtering.
|
||||
- Update pricing page layout (marketing).
|
||||
- Update signup flow (managed add-on step).
|
||||
- Update billing page (Managed Add-ons section).
|
||||
- Update invoices to show core + managed breakdown.
|
||||
851
v2/Live Docs on Server/igny8-app-docs/90-REFERENCE/MODELS.md
Normal file
851
v2/Live Docs on Server/igny8-app-docs/90-REFERENCE/MODELS.md
Normal file
@@ -0,0 +1,851 @@
|
||||
# Database Models Reference
|
||||
|
||||
**Last Verified:** January 20, 2026
|
||||
**Version:** 1.8.4
|
||||
**Total Models:** 52+
|
||||
|
||||
---
|
||||
|
||||
## Data Scoping Overview
|
||||
|
||||
| Scope | Models | Base Class | Filter By |
|
||||
|-------|--------|------------|-----------|
|
||||
| **Global** | `IntegrationProvider`, `AIModelConfig`, `SystemAISettings`, `GlobalAIPrompt`, `GlobalAuthorProfile`, `GlobalStrategy`, `GlobalModuleSettings`, `Industry`, `IndustrySector`, `SeedKeyword` | `models.Model` | None (platform-wide) |
|
||||
| **Account** | `Account`, `User`, `Plan`, `Subscription`, `AccountSettings`, `ModuleEnableSettings`, `AISettings`, `AIPrompt`, `AuthorProfile`, `CreditBalance`, `PasswordResetToken` | `AccountBaseModel` | `account` |
|
||||
| **Site** | `Site`, `PublishingSettings`, `AutomationConfig`, `DefaultAutomationConfig`, `AutomationRun`, `SiteIntegration`, `SiteUserAccess` | `AccountBaseModel` | `account`, `site` |
|
||||
| **Site+Sector** | `Keywords`, `Clusters`, `ContentIdeas`, `Tasks`, `Content`, `Images`, `ContentTaxonomyRelation` | `SiteSectorBaseModel` | `site`, `sector` |
|
||||
| **Billing** | `CreditCostConfig`, `BillingConfiguration`, `CreditPackage`, `PaymentMethodConfig`, `WebhookEvent` | `models.Model` | varies |
|
||||
| **System** | `SystemSettings`, `UserSettings`, `EmailSettings`, `EmailTemplate`, `EmailLog` | `models.Model` | varies |
|
||||
| **Plugins** | `Plugin`, `PluginVersion`, `PluginInstallation`, `PluginDownload` | `models.Model` | varies |
|
||||
|
||||
---
|
||||
|
||||
## Model Count by Location
|
||||
|
||||
| Location | Count | Models |
|
||||
|----------|-------|--------|
|
||||
| `auth/models.py` | 10 | Account, User, Plan, Subscription, Industry, IndustrySector, SeedKeyword, Site, SiteUserAccess, PasswordResetToken |
|
||||
| `modules/system/` | 10 | IntegrationProvider, SystemAISettings, SystemSettings, UserSettings, EmailSettings, EmailTemplate, EmailLog, GlobalAIPrompt, GlobalAuthorProfile, GlobalStrategy |
|
||||
| `business/automation/` | 3 | DefaultAutomationConfig, AutomationConfig, AutomationRun |
|
||||
| `business/billing/` | 6 | CreditCostConfig, BillingConfiguration, CreditPackage, PaymentMethodConfig, AIModelConfig, WebhookEvent |
|
||||
| `business/content/` | 1 | ContentTaxonomyRelation |
|
||||
| `plugins/` | 4 | Plugin, PluginVersion, PluginInstallation, PluginDownload |
|
||||
| `modules/planner/` | 3 | Keywords, Clusters, ContentIdeas |
|
||||
| `modules/writer/` | 3 | Tasks, Content, Images |
|
||||
|
||||
---
|
||||
|
||||
## System Models (v1.4.0+) (`igny8_core/modules/system/`)
|
||||
|
||||
**Purpose:** Centralized AI configuration, provider API keys, and system-wide defaults.
|
||||
|
||||
### IntegrationProvider (NEW v1.4.0)
|
||||
|
||||
Centralized storage for ALL external service API keys. Admin-only.
|
||||
|
||||
```python
|
||||
class IntegrationProvider(models.Model):
|
||||
"""Per final-model-schemas.md - Centralized API key storage"""
|
||||
provider_id = CharField(max_length=50, primary_key=True) # openai, runware, stripe, etc.
|
||||
display_name = CharField(max_length=100)
|
||||
provider_type = CharField(max_length=20) # ai, payment, email, storage
|
||||
|
||||
# Authentication
|
||||
api_key = CharField(max_length=500, blank=True)
|
||||
api_secret = CharField(max_length=500, blank=True)
|
||||
webhook_secret = CharField(max_length=500, blank=True)
|
||||
api_endpoint = URLField(blank=True)
|
||||
|
||||
# Configuration
|
||||
config = JSONField(default=dict)
|
||||
is_active = BooleanField(default=True)
|
||||
is_sandbox = BooleanField(default=False)
|
||||
|
||||
# Audit
|
||||
updated_by = ForeignKey(User, null=True)
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
updated_at = DateTimeField(auto_now=True)
|
||||
```
|
||||
|
||||
**Seeded Providers:**
|
||||
- `openai` - AI (text + DALL-E)
|
||||
- `runware` - AI (images)
|
||||
- `anthropic` - AI (future)
|
||||
- `stripe` - Payment
|
||||
- `paypal` - Payment
|
||||
- `resend` - Email
|
||||
|
||||
**Helper Methods:**
|
||||
- `IntegrationProvider.get_provider(provider_id)` - Get active provider
|
||||
- `IntegrationProvider.get_api_key(provider_id)` - Get API key for provider
|
||||
- `IntegrationProvider.get_providers_by_type(type)` - List providers by type
|
||||
|
||||
---
|
||||
|
||||
### AIModelConfig (NEW v1.4.0)
|
||||
|
||||
Single Source of Truth for all AI models with pricing and credit configuration.
|
||||
|
||||
```python
|
||||
class AIModelConfig(models.Model):
|
||||
"""Per final-model-schemas.md - Model definitions + pricing"""
|
||||
model_name = CharField(max_length=100, unique=True) # gpt-4o-mini, dall-e-3, runware:97@1
|
||||
model_type = CharField(max_length=20) # text, image
|
||||
provider = CharField(max_length=50) # Links to IntegrationProvider
|
||||
display_name = CharField(max_length=200)
|
||||
|
||||
is_default = BooleanField(default=False) # One default per type
|
||||
is_active = BooleanField(default=True)
|
||||
|
||||
# Text Model Pricing (per 1K tokens)
|
||||
cost_per_1k_input = DecimalField(max_digits=10, decimal_places=6, null=True)
|
||||
cost_per_1k_output = DecimalField(max_digits=10, decimal_places=6, null=True)
|
||||
tokens_per_credit = IntegerField(null=True) # e.g., 1000, 10000
|
||||
|
||||
# Image Model Pricing
|
||||
credits_per_image = IntegerField(null=True) # e.g., 1, 5, 15
|
||||
quality_tier = CharField(max_length=20, null=True) # basic, quality, premium
|
||||
|
||||
# Model Limits
|
||||
max_tokens = IntegerField(null=True)
|
||||
context_window = IntegerField(null=True)
|
||||
capabilities = JSONField(default=dict) # vision, function_calling, etc.
|
||||
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
updated_at = DateTimeField(auto_now=True)
|
||||
```
|
||||
|
||||
**Credit Configuration Examples:**
|
||||
|
||||
| Model | Type | tokens_per_credit | credits_per_image | quality_tier |
|
||||
|-------|------|-------------------|-------------------|--------------|
|
||||
| gpt-4o | text | 1000 | - | - |
|
||||
| gpt-4o-mini | text | 10000 | - | - |
|
||||
| runware:97@1 | image | - | 1 | basic |
|
||||
| dall-e-3 | image | - | 5 | quality |
|
||||
| google:4@2 | image | - | 15 | premium |
|
||||
|
||||
**Image Model Reference (v1.5.0 Planned):**
|
||||
|
||||
| Model | AIR ID | Tier | Supported Dimensions |
|
||||
|-------|--------|------|---------------------|
|
||||
| Hi Dream Full | `runware:97@1` | Basic | 1024×1024, 1280×768 |
|
||||
| Bria 3.2 | `bria:10@1` | Quality | 1024×1024, 1344×768 |
|
||||
| Nano Banana | `google:4@2` | Premium | 1024×1024, 1376×768 |
|
||||
|
||||
**Helper Methods:**
|
||||
- `AIModelConfig.get_default_text_model()` - Get default text model
|
||||
- `AIModelConfig.get_default_image_model()` - Get default image model
|
||||
- `AIModelConfig.get_image_models_by_tier()` - List image models by quality tier
|
||||
|
||||
---
|
||||
|
||||
### SystemAISettings (NEW v1.4.0)
|
||||
|
||||
System-wide AI defaults. Singleton (pk=1).
|
||||
|
||||
```python
|
||||
class SystemAISettings(models.Model):
|
||||
"""Per final-model-schemas.md - Renamed from GlobalIntegrationSettings"""
|
||||
# AI Parameters
|
||||
temperature = FloatField(default=0.7) # 0.0-2.0
|
||||
max_tokens = IntegerField(default=8192)
|
||||
|
||||
# Image Generation Settings
|
||||
image_style = CharField(max_length=30, default='photorealistic')
|
||||
image_quality = CharField(max_length=20, default='standard') # standard, hd
|
||||
max_images_per_article = IntegerField(default=4) # 1-8
|
||||
image_size = CharField(max_length=20, default='1024x1024')
|
||||
|
||||
# Audit
|
||||
updated_by = ForeignKey(User, null=True)
|
||||
updated_at = DateTimeField(auto_now=True)
|
||||
```
|
||||
|
||||
**Image Style Choices:**
|
||||
- `photorealistic` - Ultra realistic photography
|
||||
- `illustration` - Digital illustration
|
||||
- `3d_render` - Computer generated 3D
|
||||
- `minimal_flat` - Minimal / Flat Design
|
||||
- `artistic` - Artistic / Painterly
|
||||
- `cartoon` - Cartoon / Stylized
|
||||
|
||||
**Image Size Choices:**
|
||||
- `1024x1024` - Square
|
||||
- `1792x1024` - Landscape
|
||||
- `1024x1792` - Portrait
|
||||
|
||||
**Helper Methods:**
|
||||
- `SystemAISettings.get_instance()` - Get singleton instance
|
||||
- `SystemAISettings.get_effective_temperature(account)` - Get with account override
|
||||
- `SystemAISettings.get_effective_image_style(account)` - Get with account override
|
||||
|
||||
---
|
||||
|
||||
### AccountSettings (Per-Account Overrides)
|
||||
|
||||
Generic key-value store for account-specific settings.
|
||||
|
||||
```python
|
||||
class AccountSettings(AccountBaseModel):
|
||||
"""Per final-model-schemas.md - Account overrides"""
|
||||
key = CharField(max_length=100) # Setting key
|
||||
value = JSONField(default=dict) # Setting value
|
||||
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
updated_at = DateTimeField(auto_now=True)
|
||||
```
|
||||
|
||||
**AI-Related Keys** (override SystemAISettings defaults):
|
||||
|
||||
| Key | Type | Example | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `ai.temperature` | float | 0.8 | Override system default |
|
||||
| `ai.max_tokens` | int | 8192 | Override system default |
|
||||
| `ai.image_style` | string | "illustration" | Override system default |
|
||||
| `ai.image_quality` | string | "hd" | Override system default |
|
||||
| `ai.max_images` | int | 6 | Override system default |
|
||||
| `ai.image_quality_tier` | string | "premium" | User's preferred tier |
|
||||
|
||||
---
|
||||
|
||||
### CreditCostConfig (Operation-Level Pricing)
|
||||
|
||||
Fixed credit costs per operation type.
|
||||
|
||||
```python
|
||||
class CreditCostConfig(models.Model):
|
||||
"""Per final-model-schemas.md - Operation pricing"""
|
||||
operation_type = CharField(max_length=50, primary_key=True) # Unique operation ID
|
||||
display_name = CharField(max_length=100)
|
||||
base_credits = IntegerField(default=1) # Fixed credits per operation
|
||||
is_active = BooleanField(default=True)
|
||||
description = TextField(blank=True)
|
||||
```
|
||||
|
||||
**Note:** `tokens_per_credit` moved to AIModelConfig as of v1.4.0.
|
||||
|
||||
---
|
||||
|
||||
## DEPRECATED Models
|
||||
|
||||
### GlobalIntegrationSettings (DEPRECATED in v1.4.0)
|
||||
|
||||
**Replaced by:** `IntegrationProvider` (API keys) + `AIModelConfig` (model configs) + `SystemAISettings` (defaults)
|
||||
|
||||
```python
|
||||
# DEPRECATED - Do not use
|
||||
class GlobalIntegrationSettings(models.Model):
|
||||
# API keys now in IntegrationProvider
|
||||
openai_api_key = CharField(max_length=500) # → IntegrationProvider.get_api_key('openai')
|
||||
runware_api_key = CharField(max_length=500) # → IntegrationProvider.get_api_key('runware')
|
||||
|
||||
# Models now in AIModelConfig
|
||||
openai_model = CharField(default='gpt-4o-mini') # → AIModelConfig.is_default
|
||||
|
||||
# Settings now in SystemAISettings
|
||||
openai_temperature = FloatField(default=0.7) # → SystemAISettings.temperature
|
||||
openai_max_tokens = IntegerField(default=8192) # → SystemAISettings.max_tokens
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GlobalAIPrompt
|
||||
|
||||
```python
|
||||
class GlobalAIPrompt(models.Model):
|
||||
prompt_type = CharField(max_length=100) # clustering, ideas, content_generation
|
||||
prompt_value = TextField()
|
||||
variables = JSONField(default=list)
|
||||
is_active = BooleanField(default=True)
|
||||
```
|
||||
|
||||
### GlobalAuthorProfile
|
||||
|
||||
```python
|
||||
class GlobalAuthorProfile(models.Model):
|
||||
name = CharField(max_length=255)
|
||||
tone = CharField(max_length=50) # professional, casual, technical
|
||||
language = CharField(max_length=10, default='en')
|
||||
is_active = BooleanField(default=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Auth Models (`igny8_core/auth/models/`)
|
||||
|
||||
### User
|
||||
|
||||
```python
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
id = UUIDField(primary_key=True)
|
||||
email = EmailField(unique=True)
|
||||
first_name = CharField(max_length=150)
|
||||
last_name = CharField(max_length=150)
|
||||
|
||||
account = ForeignKey(Account, related_name='users')
|
||||
role = ForeignKey(Group, null=True)
|
||||
|
||||
is_active = BooleanField(default=True)
|
||||
is_staff = BooleanField(default=False)
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
updated_at = DateTimeField(auto_now=True)
|
||||
```
|
||||
|
||||
**Relations:** Account (many-to-one)
|
||||
|
||||
---
|
||||
|
||||
### Account
|
||||
|
||||
```python
|
||||
class Account(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
name = CharField(max_length=255)
|
||||
|
||||
plan = ForeignKey(Plan, null=True)
|
||||
owner = ForeignKey(User, related_name='owned_accounts')
|
||||
|
||||
is_active = BooleanField(default=True)
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
**Relations:** Plan (many-to-one), Users (one-to-many), Sites (one-to-many)
|
||||
|
||||
---
|
||||
|
||||
### Site
|
||||
|
||||
```python
|
||||
class Site(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
name = CharField(max_length=255)
|
||||
domain = CharField(max_length=255, blank=True)
|
||||
|
||||
account = ForeignKey(Account, related_name='sites')
|
||||
industry = ForeignKey(Industry, null=True)
|
||||
|
||||
is_active = BooleanField(default=True)
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
**Relations:** Account (many-to-one), Sectors (one-to-many), Industries (many-to-one)
|
||||
|
||||
---
|
||||
|
||||
### Sector
|
||||
|
||||
```python
|
||||
class Sector(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
name = CharField(max_length=255)
|
||||
description = TextField(blank=True)
|
||||
|
||||
site = ForeignKey(Site, related_name='sectors')
|
||||
|
||||
is_active = BooleanField(default=True)
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
**Relations:** Site (many-to-one)
|
||||
|
||||
---
|
||||
|
||||
### Industry
|
||||
|
||||
```python
|
||||
class Industry(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
name = CharField(max_length=255)
|
||||
description = TextField(blank=True)
|
||||
```
|
||||
|
||||
**Used for:** Default seed keywords, industry-specific prompts
|
||||
|
||||
---
|
||||
|
||||
## Planner Models (`igny8_core/modules/planner/models.py`)
|
||||
|
||||
### Keyword
|
||||
|
||||
```python
|
||||
class Keyword(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
keyword = CharField(max_length=255)
|
||||
|
||||
site = ForeignKey(Site, related_name='keywords')
|
||||
sector = ForeignKey(Sector, null=True, related_name='keywords')
|
||||
cluster = ForeignKey(Cluster, null=True, related_name='keywords')
|
||||
|
||||
search_volume = IntegerField(null=True)
|
||||
difficulty = IntegerField(null=True)
|
||||
cpc = DecimalField(null=True)
|
||||
|
||||
status = CharField(choices=KEYWORD_STATUS) # new, mapped
|
||||
|
||||
created_by = ForeignKey(User)
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
**Status Values:**
|
||||
- `new` - Ready for clustering
|
||||
- `mapped` - Assigned to a cluster
|
||||
|
||||
---
|
||||
|
||||
### Cluster
|
||||
|
||||
```python
|
||||
class Cluster(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
name = CharField(max_length=255)
|
||||
description = TextField(blank=True)
|
||||
|
||||
site = ForeignKey(Site, related_name='clusters')
|
||||
sector = ForeignKey(Sector, null=True, related_name='clusters')
|
||||
|
||||
created_by = ForeignKey(User)
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
**Relations:** Site, Sector, Keywords (one-to-many), ContentIdeas (one-to-many)
|
||||
|
||||
---
|
||||
|
||||
### ContentIdea
|
||||
|
||||
```python
|
||||
class ContentIdea(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
title = CharField(max_length=255)
|
||||
description = TextField(blank=True)
|
||||
|
||||
site = ForeignKey(Site, related_name='ideas')
|
||||
sector = ForeignKey(Sector, null=True)
|
||||
cluster = ForeignKey(Cluster, related_name='ideas')
|
||||
|
||||
primary_keyword = ForeignKey(Keyword, related_name='primary_ideas')
|
||||
secondary_keywords = ManyToManyField(Keyword, related_name='secondary_ideas')
|
||||
|
||||
status = CharField(choices=IDEA_STATUS) # pending, approved, used, archived
|
||||
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Writer Models (`igny8_core/modules/writer/models.py`)
|
||||
|
||||
### Task
|
||||
|
||||
```python
|
||||
class Task(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
title = CharField(max_length=255)
|
||||
brief = TextField(blank=True)
|
||||
|
||||
site = ForeignKey(Site, related_name='tasks')
|
||||
sector = ForeignKey(Sector, null=True)
|
||||
idea = ForeignKey(ContentIdea, null=True, related_name='tasks')
|
||||
|
||||
primary_keyword = CharField(max_length=255)
|
||||
secondary_keywords = JSONField(default=list)
|
||||
|
||||
status = CharField(choices=TASK_STATUS) # queued, completed
|
||||
|
||||
assigned_to = ForeignKey(User, null=True)
|
||||
due_date = DateField(null=True)
|
||||
|
||||
created_by = ForeignKey(User)
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Content
|
||||
|
||||
```python
|
||||
class Content(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
title = CharField(max_length=255)
|
||||
body = TextField() # HTML content
|
||||
excerpt = TextField(blank=True)
|
||||
|
||||
site = ForeignKey(Site, related_name='content')
|
||||
sector = ForeignKey(Sector, null=True)
|
||||
task = ForeignKey(Task, related_name='content')
|
||||
|
||||
meta_title = CharField(max_length=255, blank=True)
|
||||
meta_description = TextField(blank=True)
|
||||
|
||||
# Workflow status
|
||||
status = CharField(choices=CONTENT_STATUS) # draft, review, approved, published
|
||||
|
||||
# Publishing status (v1.3.2)
|
||||
site_status = CharField(choices=SITE_STATUS) # not_published, scheduled, publishing, published, failed
|
||||
scheduled_publish_at = DateTimeField(null=True)
|
||||
site_status_updated_at = DateTimeField(null=True)
|
||||
|
||||
# External site reference
|
||||
external_id = CharField(max_length=255, blank=True) # WordPress post ID
|
||||
external_url = URLField(blank=True) # Published URL
|
||||
|
||||
word_count = IntegerField(default=0)
|
||||
|
||||
created_by = ForeignKey(User)
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
updated_at = DateTimeField(auto_now=True)
|
||||
```
|
||||
|
||||
**Workflow Status Values (status):**
|
||||
- `draft` - Initial state after generation
|
||||
- `review` - Pending human review
|
||||
- `approved` - Ready for publishing (v1.3.2: NEW status)
|
||||
- `published` - Published to WordPress
|
||||
|
||||
**Publishing Status Values (site_status) - v1.3.2:**
|
||||
- `not_published` - Not yet scheduled for external site
|
||||
- `scheduled` - Scheduled for future publishing
|
||||
- `publishing` - Currently being published
|
||||
- `published` - Successfully published to external site
|
||||
- `failed` - Publishing failed
|
||||
|
||||
---
|
||||
|
||||
### ContentImage
|
||||
|
||||
```python
|
||||
class ContentImage(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
content = ForeignKey(Content, related_name='images')
|
||||
|
||||
url = URLField()
|
||||
thumbnail_url = URLField(blank=True)
|
||||
alt_text = CharField(max_length=255)
|
||||
caption = TextField(blank=True)
|
||||
|
||||
is_featured = BooleanField(default=False)
|
||||
position = IntegerField(default=0)
|
||||
|
||||
# AI generation metadata
|
||||
prompt = TextField(blank=True)
|
||||
provider = CharField(max_length=50) # dalle, runware
|
||||
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Models (`igny8_core/business/integration/models.py`)
|
||||
|
||||
### SiteIntegration
|
||||
|
||||
**⚠️ Note:** For WordPress, `Site.wp_api_key` is the **SINGLE source of truth** for API authentication. SiteIntegration is used for sync tracking and future multi-platform support.
|
||||
|
||||
```python
|
||||
class SiteIntegration(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
name = CharField(max_length=255)
|
||||
|
||||
site = ForeignKey(Site, related_name='integrations')
|
||||
platform = CharField(max_length=50) # wordpress, shopify (future)
|
||||
|
||||
# Configuration
|
||||
external_site_url = URLField()
|
||||
config_json = JSONField(default=dict) # Platform-specific settings
|
||||
credentials_json = JSONField(default=dict) # Reserved for future platforms (NOT for WordPress)
|
||||
|
||||
# Sync Tracking
|
||||
sync_enabled = BooleanField(default=True)
|
||||
sync_status = CharField(max_length=50) # pending/syncing/completed/error
|
||||
last_sync_at = DateTimeField(null=True)
|
||||
sync_error = TextField(null=True)
|
||||
|
||||
# Connection
|
||||
connection_status = CharField(max_length=50) # connected/error
|
||||
is_active = BooleanField(default=True)
|
||||
|
||||
# Cached WordPress structure (from initial sync)
|
||||
categories = JSONField(default=list)
|
||||
tags = JSONField(default=list)
|
||||
authors = JSONField(default=list)
|
||||
post_types = JSONField(default=list)
|
||||
structure_updated_at = DateTimeField(null=True)
|
||||
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PublishingSettings (NEW v1.3.2)
|
||||
|
||||
```python
|
||||
class PublishingSettings(AccountBaseModel):
|
||||
"""Site-level publishing configuration. Controls auto-approval, publishing limits, and scheduling."""
|
||||
|
||||
site = OneToOneField(Site, related_name='publishing_settings')
|
||||
|
||||
# Auto-approval settings
|
||||
auto_approval_enabled = BooleanField(default=True)
|
||||
|
||||
# Auto-publish settings
|
||||
auto_publish_enabled = BooleanField(default=True)
|
||||
|
||||
# Publishing limits
|
||||
daily_publish_limit = PositiveIntegerField(default=3)
|
||||
weekly_publish_limit = PositiveIntegerField(default=15)
|
||||
monthly_publish_limit = PositiveIntegerField(default=50)
|
||||
|
||||
# Publishing schedule
|
||||
publish_days = JSONField(default=['mon', 'tue', 'wed', 'thu', 'fri'])
|
||||
publish_time_slots = JSONField(default=['09:00', '14:00', '18:00'])
|
||||
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
updated_at = DateTimeField(auto_now=True)
|
||||
```
|
||||
|
||||
**Default Values:**
|
||||
- `auto_approval_enabled`: True (auto-approve after review)
|
||||
- `auto_publish_enabled`: True (auto-publish approved content)
|
||||
- `daily_publish_limit`: 3 articles per day
|
||||
- `weekly_publish_limit`: 15 articles per week
|
||||
- `monthly_publish_limit`: 50 articles per month
|
||||
- `publish_days`: Monday through Friday
|
||||
- `publish_time_slots`: 9:00 AM, 2:00 PM, 6:00 PM
|
||||
|
||||
**Usage:**
|
||||
```python
|
||||
# Get or create with defaults
|
||||
settings, created = PublishingSettings.get_or_create_for_site(site)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Billing Models (`igny8_core/business/billing/models.py`)
|
||||
|
||||
### Plan
|
||||
|
||||
```python
|
||||
class Plan(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
name = CharField(max_length=100)
|
||||
slug = SlugField(unique=True)
|
||||
|
||||
idea_credits = IntegerField(default=0)
|
||||
content_credits = IntegerField(default=0)
|
||||
image_credits = IntegerField(default=0)
|
||||
optimization_credits = IntegerField(default=0)
|
||||
|
||||
max_sites = IntegerField(default=1)
|
||||
max_users = IntegerField(default=1)
|
||||
|
||||
price_monthly = DecimalField(max_digits=10, decimal_places=2)
|
||||
price_yearly = DecimalField(max_digits=10, decimal_places=2)
|
||||
|
||||
is_active = BooleanField(default=True)
|
||||
is_internal = BooleanField(default=False)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CreditBalance
|
||||
|
||||
```python
|
||||
class CreditBalance(models.Model):
|
||||
account = ForeignKey(Account, related_name='credit_balances')
|
||||
site = ForeignKey(Site, null=True, related_name='credit_balances')
|
||||
|
||||
idea_credits = IntegerField(default=0)
|
||||
content_credits = IntegerField(default=0)
|
||||
image_credits = IntegerField(default=0)
|
||||
optimization_credits = IntegerField(default=0)
|
||||
|
||||
period_start = DateField()
|
||||
period_end = DateField()
|
||||
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
updated_at = DateTimeField(auto_now=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CreditUsage
|
||||
|
||||
```python
|
||||
class CreditUsage(models.Model):
|
||||
account = ForeignKey(Account, related_name='credit_usage')
|
||||
site = ForeignKey(Site, null=True)
|
||||
user = ForeignKey(User)
|
||||
|
||||
credit_type = CharField(max_length=50) # idea, content, image, optimization
|
||||
amount = IntegerField()
|
||||
operation = CharField(max_length=100) # generate_content, etc.
|
||||
|
||||
related_content_type = ForeignKey(ContentType, null=True)
|
||||
related_object_id = UUIDField(null=True)
|
||||
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System Models (`igny8_core/modules/system/`)
|
||||
|
||||
### ModuleEnableSettings
|
||||
|
||||
```python
|
||||
class ModuleEnableSettings(models.Model):
|
||||
account = OneToOneField(Account, primary_key=True)
|
||||
|
||||
planner_enabled = BooleanField(default=True)
|
||||
writer_enabled = BooleanField(default=True)
|
||||
linker_enabled = BooleanField(default=False)
|
||||
optimizer_enabled = BooleanField(default=False)
|
||||
automation_enabled = BooleanField(default=True)
|
||||
integration_enabled = BooleanField(default=True)
|
||||
publisher_enabled = BooleanField(default=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### AIIntegrationSettings
|
||||
|
||||
```python
|
||||
class AIIntegrationSettings(models.Model):
|
||||
account = ForeignKey(Account, related_name='ai_settings')
|
||||
|
||||
# OpenAI
|
||||
openai_api_key = CharField(max_length=255, blank=True)
|
||||
openai_model = CharField(max_length=50, default='gpt-4')
|
||||
|
||||
# Image generation
|
||||
image_provider = CharField(max_length=50, default='dalle') # dalle, runware
|
||||
dalle_api_key = CharField(max_length=255, blank=True)
|
||||
runware_api_key = CharField(max_length=255, blank=True)
|
||||
|
||||
is_validated = BooleanField(default=False)
|
||||
validated_at = DateTimeField(null=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PromptTemplate
|
||||
|
||||
```python
|
||||
class PromptTemplate(models.Model):
|
||||
account = ForeignKey(Account, null=True) # null = system default
|
||||
|
||||
prompt_type = CharField(max_length=100) # auto_cluster, generate_ideas, etc.
|
||||
template = TextField()
|
||||
variables = JSONField(default=list)
|
||||
|
||||
is_active = BooleanField(default=True)
|
||||
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
updated_at = DateTimeField(auto_now=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Publisher Models (`igny8_core/modules/publisher/models.py`)
|
||||
|
||||
### PublishingRecord
|
||||
|
||||
```python
|
||||
class PublishingRecord(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
content = ForeignKey(Content, related_name='publishing_records')
|
||||
integration = ForeignKey(SiteIntegration, related_name='publishing_records')
|
||||
|
||||
external_id = CharField(max_length=255) # WordPress post ID
|
||||
external_url = URLField(blank=True)
|
||||
|
||||
status = CharField(max_length=50) # pending, published, failed
|
||||
|
||||
published_at = DateTimeField(null=True)
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automation Models (`igny8_core/modules/automation/models.py`)
|
||||
|
||||
### AutomationConfig
|
||||
|
||||
```python
|
||||
class AutomationConfig(models.Model):
|
||||
site = ForeignKey(Site, related_name='automation_configs')
|
||||
|
||||
# Stage limits
|
||||
clustering_limit = IntegerField(default=10)
|
||||
ideas_limit = IntegerField(default=10)
|
||||
content_limit = IntegerField(default=5)
|
||||
image_limit = IntegerField(default=10)
|
||||
publish_limit = IntegerField(default=5)
|
||||
|
||||
# Timing
|
||||
delay_between_operations = IntegerField(default=5) # seconds
|
||||
max_runtime = IntegerField(default=3600) # 1 hour
|
||||
|
||||
# Behavior
|
||||
auto_approve = BooleanField(default=False)
|
||||
auto_publish = BooleanField(default=False)
|
||||
stop_on_error = BooleanField(default=True)
|
||||
|
||||
is_active = BooleanField(default=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### AutomationRun
|
||||
|
||||
```python
|
||||
class AutomationRun(models.Model):
|
||||
id = UUIDField(primary_key=True)
|
||||
site = ForeignKey(Site, related_name='automation_runs')
|
||||
config = ForeignKey(AutomationConfig)
|
||||
|
||||
status = CharField(max_length=50) # pending, running, paused, completed, failed, cancelled
|
||||
|
||||
# Progress tracking
|
||||
current_stage = CharField(max_length=50, blank=True)
|
||||
items_processed = IntegerField(default=0)
|
||||
items_total = IntegerField(default=0)
|
||||
|
||||
# Timing
|
||||
started_at = DateTimeField(null=True)
|
||||
completed_at = DateTimeField(null=True)
|
||||
|
||||
# Results
|
||||
error_message = TextField(blank=True)
|
||||
|
||||
started_by = ForeignKey(User)
|
||||
created_at = DateTimeField(auto_now_add=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entity Relationship Overview
|
||||
|
||||
```
|
||||
Account
|
||||
├── Users (many)
|
||||
├── Sites (many)
|
||||
│ ├── Sectors (many)
|
||||
│ ├── Keywords (many)
|
||||
│ ├── Clusters (many)
|
||||
│ ├── ContentIdeas (many)
|
||||
│ ├── Tasks (many)
|
||||
│ ├── Content (many)
|
||||
│ │ └── ContentImages (many)
|
||||
│ ├── SiteIntegrations (many)
|
||||
│ │ └── PublishingRecords (many)
|
||||
│ └── AutomationConfigs (many)
|
||||
│ └── AutomationRuns (many)
|
||||
├── Plan (one)
|
||||
├── CreditBalances (many)
|
||||
├── CreditUsage (many)
|
||||
├── ModuleEnableSettings (one)
|
||||
├── AIIntegrationSettings (many)
|
||||
└── PromptTemplates (many)
|
||||
```
|
||||
@@ -0,0 +1,679 @@
|
||||
# Payment System Documentation
|
||||
|
||||
> **Version:** 2.0.0
|
||||
> **Last Updated:** January 20, 2026
|
||||
> **Status:** Production Ready
|
||||
|
||||
> **Complete Billing Reference:** For comprehensive billing documentation including the two-pool credit system and renewal workflows, see [BILLING-PAYMENTS-COMPLETE.md](../10-MODULES/BILLING-PAYMENTS-COMPLETE.md)
|
||||
|
||||
This document provides payment gateway implementation details for IGNY8.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [System Overview](#system-overview)
|
||||
2. [Payment Entry Points](#payment-entry-points)
|
||||
3. [Backend Architecture](#backend-architecture)
|
||||
4. [Frontend Architecture](#frontend-architecture)
|
||||
5. [Payment Flows](#payment-flows)
|
||||
6. [Country-Based Payment Rules](#country-based-payment-rules)
|
||||
7. [Webhook Processing](#webhook-processing)
|
||||
8. [Models Reference](#models-reference)
|
||||
9. [Security Features](#security-features)
|
||||
|
||||
---
|
||||
|
||||
## System Overview
|
||||
|
||||
### Supported Payment Methods
|
||||
|
||||
| Method | Type | Regions | Use Cases |
|
||||
|--------|------|---------|-----------|
|
||||
| **Stripe** | Credit/Debit Card | Global | Subscriptions, Credit packages |
|
||||
| **PayPal** | PayPal account | Global (except PK) | Subscriptions, Credit packages |
|
||||
| **Bank Transfer** | Manual | Pakistan (PK) | Subscriptions, Credit packages |
|
||||
|
||||
### Payment Method Selection Logic
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ Country-Based Payment Rules │
|
||||
├────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Global Users (non-PK): │
|
||||
│ ✅ Stripe (Credit/Debit Card) │
|
||||
│ ✅ PayPal │
|
||||
│ ❌ Bank Transfer (not available) │
|
||||
│ │
|
||||
│ Pakistan Users (PK): │
|
||||
│ ✅ Stripe (Credit/Debit Card) │
|
||||
│ ❌ PayPal (not available in PK) │
|
||||
│ ✅ Bank Transfer (manual) │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PAYMENT SYSTEM FLOW │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Signup │───────▶ │ /account/ │ │
|
||||
│ │ (no pay) │ │ plans │ │
|
||||
│ └──────────────┘ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────┴─────────────┐ │
|
||||
│ │ │ │
|
||||
│ New User? Existing User? │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ PendingPay- │ │ Plans/Billing│ │
|
||||
│ │ mentView │ │ Dashboard │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ │
|
||||
│ │ │ │
|
||||
│ ┌─────────┼─────────┐ ┌────────┼────────┐ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ ▼ ▼ │
|
||||
│ Stripe PayPal Bank Upgrade Credits Manage │
|
||||
│ Transfer │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Payment Entry Points
|
||||
|
||||
### 1. Signup Flow
|
||||
|
||||
**File:** `frontend/src/components/auth/SignUpFormUnified.tsx`
|
||||
|
||||
**Simplified Signup (No Payment on Signup):**
|
||||
- User selects plan and provides details
|
||||
- Account created with `status='pending_payment'` for paid plans
|
||||
- User redirected to `/account/plans` to complete payment
|
||||
- No payment gateway redirect from signup page
|
||||
|
||||
```typescript
|
||||
// Signup flow creates account only, no checkout
|
||||
const handleSignup = async (data) => {
|
||||
const result = await register({
|
||||
email, password, plan_slug, billing_country
|
||||
});
|
||||
// Redirect to plans page for payment
|
||||
navigate('/account/plans');
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Plans & Billing Page
|
||||
|
||||
**File:** `frontend/src/pages/account/PlansAndBillingPage.tsx`
|
||||
|
||||
Central hub for all payment-related actions:
|
||||
|
||||
**For New Users (pending_payment):**
|
||||
- Shows `PendingPaymentView` component
|
||||
- Full-page payment interface
|
||||
- Invoice details and payment method selection
|
||||
|
||||
**For Existing Users:**
|
||||
- Current plan and subscription status
|
||||
- Credit balance and purchase
|
||||
- Invoice history and downloads
|
||||
- Subscription management
|
||||
|
||||
### 3. PendingPaymentView Component
|
||||
|
||||
**File:** `frontend/src/components/billing/PendingPaymentView.tsx`
|
||||
|
||||
Full-page payment interface for new users:
|
||||
- Displays invoice details and plan info
|
||||
- Payment method selection based on country
|
||||
- Stripe/PayPal redirect or Bank Transfer form
|
||||
- Status checking for bank transfer submissions
|
||||
|
||||
### 4. Bank Transfer Form
|
||||
|
||||
**File:** `frontend/src/components/billing/BankTransferForm.tsx`
|
||||
|
||||
Manual payment submission for Pakistan users:
|
||||
- Bank account details display
|
||||
- Transaction reference input
|
||||
- File upload for payment proof
|
||||
- Submission and status tracking
|
||||
|
||||
---
|
||||
|
||||
## Backend Architecture
|
||||
|
||||
### Service Layer
|
||||
|
||||
#### StripeService
|
||||
|
||||
**File:** `backend/igny8_core/business/billing/services/stripe_service.py`
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `create_checkout_session()` | Create subscription checkout |
|
||||
| `create_credit_checkout_session()` | Create credit package checkout |
|
||||
| `create_billing_portal_session()` | Customer billing portal |
|
||||
| `get_or_create_customer()` | Stripe customer management |
|
||||
| `construct_webhook_event()` | Verify webhook signatures |
|
||||
|
||||
#### PayPalService
|
||||
|
||||
**File:** `backend/igny8_core/business/billing/services/paypal_service.py`
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `create_order()` | Create one-time payment order |
|
||||
| `create_subscription_order()` | Create subscription order |
|
||||
| `capture_order()` | Capture approved payment |
|
||||
| `verify_webhook_signature()` | Webhook verification |
|
||||
|
||||
#### InvoiceService
|
||||
|
||||
**File:** `backend/igny8_core/business/billing/services/invoice_service.py`
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `create_subscription_invoice()` | Invoice for plan subscription |
|
||||
| `create_credit_package_invoice()` | Invoice for credit purchase |
|
||||
| `mark_paid()` | Mark invoice as paid |
|
||||
| `generate_pdf()` | Generate PDF invoice |
|
||||
|
||||
#### PaymentService
|
||||
|
||||
**File:** `backend/igny8_core/business/billing/services/payment_service.py`
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `create_stripe_payment()` | Record Stripe payment |
|
||||
| `create_paypal_payment()` | Record PayPal payment |
|
||||
| `create_manual_payment()` | Record bank transfer |
|
||||
| `approve_manual_payment()` | Admin approval |
|
||||
|
||||
### API Endpoints
|
||||
|
||||
#### Stripe Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/v1/billing/stripe/config/` | GET | Publishable key |
|
||||
| `/v1/billing/stripe/checkout/` | POST | Create checkout session |
|
||||
| `/v1/billing/stripe/credit-checkout/` | POST | Credit package checkout |
|
||||
| `/v1/billing/stripe/billing-portal/` | POST | Billing portal |
|
||||
| `/v1/billing/webhooks/stripe/` | POST | Webhook handler |
|
||||
|
||||
#### PayPal Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/v1/billing/paypal/config/` | GET | Client ID |
|
||||
| `/v1/billing/paypal/create-order/` | POST | Credit package order |
|
||||
| `/v1/billing/paypal/create-subscription-order/` | POST | Subscription order |
|
||||
| `/v1/billing/paypal/capture-order/` | POST | Capture payment |
|
||||
| `/v1/billing/webhooks/paypal/` | POST | Webhook handler |
|
||||
|
||||
#### Invoice Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/v1/billing/invoices/` | GET | List invoices |
|
||||
| `/v1/billing/invoices/{id}/` | GET | Invoice detail |
|
||||
| `/v1/billing/invoices/{id}/download_pdf/` | GET | Download PDF |
|
||||
|
||||
#### Payment Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/v1/billing/payments/` | GET | List payments |
|
||||
| `/v1/billing/payments/manual/` | POST | Submit bank transfer |
|
||||
| `/v1/billing/admin/payments/confirm/` | POST | Admin approve/reject |
|
||||
|
||||
---
|
||||
|
||||
## Frontend Architecture
|
||||
|
||||
### Services
|
||||
|
||||
**File:** `frontend/src/services/billing.api.ts`
|
||||
|
||||
Key functions:
|
||||
|
||||
```typescript
|
||||
// Gateway availability (country-based)
|
||||
getAvailablePaymentGateways(userCountry?: string)
|
||||
|
||||
// Subscription helpers
|
||||
subscribeToPlan(planId, gateway, options)
|
||||
purchaseCredits(packageId, gateway, options)
|
||||
|
||||
// Stripe functions
|
||||
createStripeCheckout(planId, options)
|
||||
createStripeCreditCheckout(packageId, options)
|
||||
openStripeBillingPortal(returnUrl)
|
||||
|
||||
// PayPal functions
|
||||
createPayPalSubscriptionOrder(planId, options)
|
||||
createPayPalCreditOrder(packageId, options)
|
||||
capturePayPalOrder(orderId, metadata)
|
||||
|
||||
// Manual payment
|
||||
submitManualPayment(invoiceId, data)
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
| Component | File | Purpose |
|
||||
|-----------|------|---------|
|
||||
| `PendingPaymentView` | `/components/billing/PendingPaymentView.tsx` | New user payment |
|
||||
| `BankTransferForm` | `/components/billing/BankTransferForm.tsx` | Bank transfer submission |
|
||||
| `PendingPaymentBanner` | `/components/billing/PendingPaymentBanner.tsx` | Alert for pending payments |
|
||||
| `PaymentGatewaySelector` | `/components/billing/PaymentGatewaySelector.tsx` | Gateway selection UI |
|
||||
| `PayInvoiceModal` | `/components/billing/PayInvoiceModal.tsx` | Pay invoice modal |
|
||||
|
||||
---
|
||||
|
||||
## Payment Flows
|
||||
|
||||
### Flow 1: New User Signup with Stripe
|
||||
|
||||
```
|
||||
1. User submits signup form with plan
|
||||
2. Backend creates:
|
||||
- User account
|
||||
- Account (status='pending_payment')
|
||||
- Subscription (status='pending_payment')
|
||||
- Invoice (status='pending')
|
||||
3. User redirected to /account/plans
|
||||
4. PendingPaymentView displays
|
||||
5. User selects Stripe, clicks Pay
|
||||
6. Redirect to Stripe Checkout
|
||||
7. User completes payment
|
||||
8. Stripe webhook received:
|
||||
- Payment recorded
|
||||
- Invoice marked paid
|
||||
- Account activated
|
||||
- Credits added
|
||||
9. User redirected back to /account/plans
|
||||
10. Success message, dashboard displays
|
||||
```
|
||||
|
||||
### Flow 2: New User with PayPal (Non-PK)
|
||||
|
||||
```
|
||||
1. Same as Stripe steps 1-4
|
||||
5. User selects PayPal, clicks Pay
|
||||
6. PayPal order created
|
||||
7. Redirect to PayPal approval
|
||||
8. User approves on PayPal
|
||||
9. Redirect back with order_id
|
||||
10. Frontend calls capture-order
|
||||
11. Backend processes:
|
||||
- Payment captured
|
||||
- Payment recorded
|
||||
- Invoice marked paid
|
||||
- Account activated
|
||||
- Credits added
|
||||
12. Success displayed
|
||||
```
|
||||
|
||||
### Flow 3: Pakistan User with Bank Transfer
|
||||
|
||||
```
|
||||
1. Same as signup steps 1-4
|
||||
5. User sees Stripe + Bank Transfer options
|
||||
6. User selects Bank Transfer
|
||||
7. BankTransferForm displays:
|
||||
- Bank details (SCB Pakistan)
|
||||
- Reference input
|
||||
- Proof upload option
|
||||
8. User makes transfer, submits form
|
||||
9. Backend creates:
|
||||
- Payment (status='pending_approval')
|
||||
10. User sees "Awaiting Approval" status
|
||||
11. Admin reviews in Django Admin
|
||||
12. Admin approves:
|
||||
- Payment marked succeeded
|
||||
- Invoice marked paid
|
||||
- Account activated
|
||||
- Credits added
|
||||
13. User receives email confirmation
|
||||
```
|
||||
|
||||
### Flow 4: Existing User Buys Credits
|
||||
|
||||
```
|
||||
1. User on /account/plans clicks "Buy Credits"
|
||||
2. Credit package selection modal
|
||||
3. User selects package and gateway
|
||||
4. For Stripe/PayPal: redirect flow
|
||||
5. For Bank Transfer: form submission
|
||||
6. On success: credits added to account
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Country-Based Payment Rules
|
||||
|
||||
### Implementation in Frontend
|
||||
|
||||
```typescript
|
||||
// billing.api.ts
|
||||
export async function getAvailablePaymentGateways(userCountry?: string) {
|
||||
const [stripeAvailable, paypalAvailable] = await Promise.all([
|
||||
isStripeConfigured(),
|
||||
isPayPalConfigured(),
|
||||
]);
|
||||
|
||||
const isPakistan = userCountry?.toUpperCase() === 'PK';
|
||||
|
||||
return {
|
||||
stripe: stripeAvailable,
|
||||
// PayPal: NOT available for Pakistan
|
||||
paypal: !isPakistan && paypalAvailable,
|
||||
// Bank Transfer: ONLY for Pakistan
|
||||
manual: isPakistan,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in Components
|
||||
|
||||
```typescript
|
||||
// PendingPaymentView.tsx
|
||||
const isPakistan = userCountry === 'PK';
|
||||
|
||||
// Load gateways with country filter
|
||||
const gateways = await getAvailablePaymentGateways(userCountry);
|
||||
|
||||
// Show appropriate options
|
||||
const paymentOptions = [
|
||||
{ type: 'stripe', ... }, // Always shown if configured
|
||||
isPakistan ? { type: 'manual', ... } : { type: 'paypal', ... },
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Webhook Processing
|
||||
|
||||
### Stripe Webhooks
|
||||
|
||||
**Endpoint:** `POST /v1/billing/webhooks/stripe/`
|
||||
|
||||
| Event | Handler Action |
|
||||
|-------|----------------|
|
||||
| `checkout.session.completed` | Activate subscription, add credits |
|
||||
| `invoice.paid` | Add renewal credits |
|
||||
| `invoice.payment_failed` | Send notification |
|
||||
| `customer.subscription.updated` | Sync changes |
|
||||
| `customer.subscription.deleted` | Cancel subscription |
|
||||
|
||||
**Idempotency:** Checks `WebhookEvent` model before processing:
|
||||
```python
|
||||
# Check if already processed
|
||||
if WebhookEvent.objects.filter(
|
||||
provider='stripe',
|
||||
event_id=event_id,
|
||||
status='processed'
|
||||
).exists():
|
||||
return # Already handled
|
||||
```
|
||||
|
||||
### PayPal Webhooks
|
||||
|
||||
**Endpoint:** `POST /v1/billing/webhooks/paypal/`
|
||||
|
||||
| Event | Handler Action |
|
||||
|-------|----------------|
|
||||
| `CHECKOUT.ORDER.APPROVED` | Auto-capture if configured |
|
||||
| `PAYMENT.CAPTURE.COMPLETED` | Mark succeeded, add credits |
|
||||
| `PAYMENT.CAPTURE.DENIED` | Mark failed |
|
||||
| `BILLING.SUBSCRIPTION.ACTIVATED` | Activate subscription |
|
||||
|
||||
**Signature Verification:** Enabled and enforced:
|
||||
```python
|
||||
is_valid = service.verify_webhook_signature(...)
|
||||
if not is_valid:
|
||||
return Response({'error': 'Invalid signature'}, status=400)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Models Reference
|
||||
|
||||
### Invoice Model
|
||||
|
||||
```python
|
||||
class Invoice(AccountBaseModel):
|
||||
STATUS_CHOICES = [
|
||||
('draft', 'Draft'),
|
||||
('pending', 'Pending'),
|
||||
('paid', 'Paid'),
|
||||
('void', 'Void'),
|
||||
('uncollectible', 'Uncollectible'),
|
||||
]
|
||||
|
||||
invoice_number = models.CharField(max_length=50, unique=True)
|
||||
subscription = models.ForeignKey('auth.Subscription', ...)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
|
||||
subtotal = models.DecimalField(...)
|
||||
tax = models.DecimalField(...)
|
||||
total = models.DecimalField(...)
|
||||
currency = models.CharField(max_length=3, default='USD')
|
||||
due_date = models.DateField()
|
||||
line_items = models.JSONField(default=list)
|
||||
```
|
||||
|
||||
### Payment Model
|
||||
|
||||
```python
|
||||
class Payment(AccountBaseModel):
|
||||
STATUS_CHOICES = [
|
||||
('pending', 'Pending'),
|
||||
('pending_approval', 'Pending Approval'),
|
||||
('succeeded', 'Succeeded'),
|
||||
('failed', 'Failed'),
|
||||
('refunded', 'Refunded'),
|
||||
]
|
||||
|
||||
PAYMENT_METHOD_CHOICES = [
|
||||
('stripe', 'Stripe'),
|
||||
('paypal', 'PayPal'),
|
||||
('bank_transfer', 'Bank Transfer'),
|
||||
('manual', 'Manual'),
|
||||
]
|
||||
|
||||
invoice = models.ForeignKey('Invoice', ...)
|
||||
amount = models.DecimalField(...)
|
||||
currency = models.CharField(max_length=3)
|
||||
payment_method = models.CharField(choices=PAYMENT_METHOD_CHOICES)
|
||||
status = models.CharField(choices=STATUS_CHOICES)
|
||||
stripe_payment_intent_id = models.CharField(...)
|
||||
paypal_order_id = models.CharField(...)
|
||||
manual_reference = models.CharField(..., unique=True)
|
||||
```
|
||||
|
||||
### WebhookEvent Model
|
||||
|
||||
```python
|
||||
class WebhookEvent(models.Model):
|
||||
"""Audit trail for webhook processing"""
|
||||
PROVIDER_CHOICES = [
|
||||
('stripe', 'Stripe'),
|
||||
('paypal', 'PayPal'),
|
||||
]
|
||||
|
||||
provider = models.CharField(choices=PROVIDER_CHOICES)
|
||||
event_id = models.CharField(max_length=255)
|
||||
event_type = models.CharField(max_length=100)
|
||||
payload = models.JSONField()
|
||||
status = models.CharField() # 'pending', 'processed', 'failed'
|
||||
processed_at = models.DateTimeField(null=True)
|
||||
error_message = models.TextField(blank=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Features
|
||||
|
||||
### Implemented Security Measures
|
||||
|
||||
1. **Webhook Signature Verification**
|
||||
- Stripe: `stripe.Webhook.construct_event()` with signing secret
|
||||
- PayPal: `verify_webhook_signature()` API call
|
||||
|
||||
2. **Idempotency**
|
||||
- `WebhookEvent` model tracks processed events
|
||||
- Duplicate detection before processing
|
||||
|
||||
3. **Amount Validation**
|
||||
- PayPal capture validates amount matches expected
|
||||
- Prevents manipulation attacks
|
||||
|
||||
4. **Manual Reference Uniqueness**
|
||||
- Database constraint prevents duplicate bank transfer references
|
||||
- Prevents double submission
|
||||
|
||||
5. **CSRF Protection**
|
||||
- Webhook endpoints exempt (external callers)
|
||||
- All other endpoints protected
|
||||
|
||||
6. **Authentication**
|
||||
- Payment endpoints require `IsAuthenticatedAndActive`
|
||||
- Config endpoints allow `AllowAny` (public keys only)
|
||||
|
||||
---
|
||||
|
||||
## Admin Operations
|
||||
|
||||
### Django Admin Features
|
||||
|
||||
**Location:** Django Admin > Billing
|
||||
|
||||
- **Invoices:** View, filter, download PDF
|
||||
- **Payments:** View, approve/reject manual payments
|
||||
- **Credit Transactions:** Audit trail
|
||||
- **Credit Packages:** Manage packages
|
||||
|
||||
### Manual Payment Approval
|
||||
|
||||
```
|
||||
1. Admin navigates to Payments in Django Admin
|
||||
2. Filter by status='pending_approval'
|
||||
3. Review payment details and proof
|
||||
4. Click "Approve" or "Reject" action
|
||||
5. System automatically:
|
||||
- Updates payment status
|
||||
- Marks invoice paid (if approved)
|
||||
- Activates account (if approved)
|
||||
- Adds credits (if approved)
|
||||
- Sends email notification
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Stripe
|
||||
STRIPE_SECRET_KEY=sk_...
|
||||
STRIPE_PUBLISHABLE_KEY=pk_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
|
||||
# PayPal
|
||||
PAYPAL_CLIENT_ID=...
|
||||
PAYPAL_CLIENT_SECRET=...
|
||||
PAYPAL_WEBHOOK_ID=...
|
||||
PAYPAL_MODE=sandbox|live
|
||||
|
||||
# General
|
||||
DEFAULT_CURRENCY=USD
|
||||
```
|
||||
|
||||
### IntegrationProvider Setup
|
||||
|
||||
Payment gateways configured via `IntegrationProvider` model in Django Admin:
|
||||
|
||||
1. **Stripe Provider:**
|
||||
- Name: "Stripe"
|
||||
- Provider Type: "stripe"
|
||||
- Credentials: `{"secret_key": "...", "publishable_key": "...", "webhook_secret": "..."}`
|
||||
|
||||
2. **PayPal Provider:**
|
||||
- Name: "PayPal"
|
||||
- Provider Type: "paypal"
|
||||
- Credentials: `{"client_id": "...", "client_secret": "...", "webhook_id": "..."}`
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Issue | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| "Stripe not configured" | Missing IntegrationProvider | Add Stripe provider in admin |
|
||||
| "PayPal not configured" | Missing IntegrationProvider | Add PayPal provider in admin |
|
||||
| PayPal shown for PK users | Country not passed correctly | Ensure `billing_country` saved on account |
|
||||
| Duplicate payments | Webhook retry without idempotency | Check `WebhookEvent` for duplicates |
|
||||
| PDF download fails | Missing `reportlab` | Run `pip install reportlab` |
|
||||
|
||||
### Debug Logging
|
||||
|
||||
Enable billing debug logs:
|
||||
```python
|
||||
# settings.py
|
||||
LOGGING = {
|
||||
'loggers': {
|
||||
'igny8_core.business.billing': {
|
||||
'level': 'DEBUG',
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Reference
|
||||
|
||||
### Backend Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `billing/views/stripe_views.py` | Stripe API endpoints |
|
||||
| `billing/views/paypal_views.py` | PayPal API endpoints |
|
||||
| `billing/views/refund_views.py` | Refund processing |
|
||||
| `billing/services/stripe_service.py` | Stripe service layer |
|
||||
| `billing/services/paypal_service.py` | PayPal service layer |
|
||||
| `billing/services/invoice_service.py` | Invoice operations |
|
||||
| `billing/services/payment_service.py` | Payment operations |
|
||||
| `billing/services/pdf_service.py` | PDF generation |
|
||||
| `billing/services/email_service.py` | Email notifications |
|
||||
| `billing/models.py` | Billing models |
|
||||
| `billing/urls.py` | URL routing |
|
||||
|
||||
### Frontend Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `services/billing.api.ts` | API client functions |
|
||||
| `pages/account/PlansAndBillingPage.tsx` | Main billing page |
|
||||
| `components/billing/PendingPaymentView.tsx` | New user payment |
|
||||
| `components/billing/BankTransferForm.tsx` | Bank transfer form |
|
||||
| `components/billing/PayInvoiceModal.tsx` | Invoice payment modal |
|
||||
| `components/billing/PaymentGatewaySelector.tsx` | Gateway selection |
|
||||
| `components/billing/PendingPaymentBanner.tsx` | Payment alert banner |
|
||||
|
||||
---
|
||||
|
||||
*Document generated from production codebase - January 8, 2026*
|
||||
@@ -0,0 +1,198 @@
|
||||
# Global Keywords Database (SeedKeyword) - Import Guide
|
||||
|
||||
**Last Updated:** January 20, 2026
|
||||
**Version:** 1.8.4
|
||||
|
||||
## Overview
|
||||
|
||||
The Global Keywords Database stores canonical keyword suggestions that can be imported into account-specific keywords. These are organized by Industry and Sector.
|
||||
|
||||
**Admin URL:** `https://api.igny8.com/admin/igny8_core_auth/seedkeyword/`
|
||||
|
||||
---
|
||||
|
||||
## Import Functionality
|
||||
|
||||
### CSV Format
|
||||
|
||||
The import expects a CSV file with the following columns:
|
||||
|
||||
| Column | Type | Required | Description | Example |
|
||||
|--------|------|----------|-------------|---------|
|
||||
| `keyword` | String | **Yes** | The keyword phrase | "best massage chairs" |
|
||||
| `industry` | String | **Yes** | Industry name (must exist) | "Health & Wellness" |
|
||||
| `sector` | String | **Yes** | Sector name (must exist) | "Massage Products" |
|
||||
| `volume` | Integer | No | Monthly search volume | 5400 |
|
||||
| `difficulty` | Integer | No | Keyword difficulty (0-100) | 45 |
|
||||
| `country` | String | No | Country code (US, CA, GB, etc.) | "US" |
|
||||
| `is_active` | Boolean | No | Active status | True |
|
||||
|
||||
### Sample CSV
|
||||
|
||||
```csv
|
||||
keyword,industry,sector,volume,difficulty,country,is_active
|
||||
best massage chairs,Health & Wellness,Massage Products,5400,45,US,True
|
||||
deep tissue massage chair,Health & Wellness,Massage Products,720,52,US,True
|
||||
shiatsu massage chair,Health & Wellness,Massage Products,1200,48,US,True
|
||||
```
|
||||
|
||||
**Template file available:** `/data/app/igny8/backend/seed_keywords_import_template.csv`
|
||||
|
||||
---
|
||||
|
||||
## How to Import
|
||||
|
||||
### Step 1: Prepare Your CSV File
|
||||
|
||||
1. Download the template: `seed_keywords_import_template.csv`
|
||||
2. Add your keywords (one per row)
|
||||
3. Ensure Industry and Sector names **exactly match** existing records
|
||||
4. Save as CSV (UTF-8 encoding)
|
||||
|
||||
### Step 2: Import via Django Admin
|
||||
|
||||
1. Go to: `https://api.igny8.com/admin/igny8_core_auth/seedkeyword/`
|
||||
2. Click **"Import"** button (top right)
|
||||
3. Click **"Choose File"** and select your CSV
|
||||
4. Click **"Submit"**
|
||||
5. Review the preview:
|
||||
- ✅ Green = New records to be created
|
||||
- 🔵 Blue = Existing records to be updated
|
||||
- ❌ Red = Errors (fix and re-import)
|
||||
6. If preview looks good, click **"Confirm import"**
|
||||
|
||||
### Step 3: Verify Import
|
||||
|
||||
- Check the list to see your imported keywords
|
||||
- Use filters to find specific industries/sectors
|
||||
- Edit any records if needed
|
||||
|
||||
---
|
||||
|
||||
## Data Validation
|
||||
|
||||
The import process automatically:
|
||||
|
||||
✅ **Validates volume:** Ensures it's a positive integer (defaults to 0 if invalid)
|
||||
✅ **Validates difficulty:** Clamps to 0-100 range
|
||||
✅ **Validates country:** Must be one of: US, CA, GB, AE, AU, IN, PK (defaults to US)
|
||||
✅ **Handles duplicates:** Uses `(keyword, industry, sector)` as unique key
|
||||
✅ **Skip unchanged:** If keyword already exists with same data, it's skipped
|
||||
|
||||
---
|
||||
|
||||
## Bulk Delete
|
||||
|
||||
### How to Delete Keywords
|
||||
|
||||
1. Select keywords using checkboxes (or "Select all")
|
||||
2. Choose **"Delete selected keywords"** from the action dropdown
|
||||
3. Click **"Go"**
|
||||
4. Review the confirmation page showing all related objects
|
||||
5. Click **"Yes, I'm sure"** to confirm deletion
|
||||
|
||||
**Note:** Only superusers and developers can delete seed keywords.
|
||||
|
||||
---
|
||||
|
||||
## Export Functionality
|
||||
|
||||
### Export to CSV/Excel
|
||||
|
||||
1. Go to: `https://api.igny8.com/admin/igny8_core_auth/seedkeyword/`
|
||||
2. (Optional) Use filters to narrow down results
|
||||
3. Click **"Export"** button (top right)
|
||||
4. Choose format: CSV, Excel, JSON, etc.
|
||||
5. File downloads with all selected fields
|
||||
|
||||
**Export includes:**
|
||||
- All keyword data
|
||||
- Related industry/sector names
|
||||
- SEO metrics (volume, difficulty)
|
||||
- Metadata (created date, active status)
|
||||
|
||||
---
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Issue: "Industry not found" error during import
|
||||
|
||||
**Solution:**
|
||||
- Ensure the industry name in your CSV **exactly matches** an existing Industry record
|
||||
- Check spelling, capitalization, and spacing
|
||||
- View existing industries: `/admin/igny8_core_auth/industry/`
|
||||
|
||||
### Issue: "Sector not found" error during import
|
||||
|
||||
**Solution:**
|
||||
- Ensure the sector name in your CSV **exactly matches** an existing IndustrySector record
|
||||
- The sector must belong to the specified industry
|
||||
- View existing sectors: `/admin/igny8_core_auth/industrysector/`
|
||||
|
||||
### Issue: Import shows errors for all rows
|
||||
|
||||
**Solution:**
|
||||
- Check CSV encoding (must be UTF-8)
|
||||
- Ensure column headers match exactly: `keyword,industry,sector,volume,difficulty,country,is_active`
|
||||
- Remove any extra columns or spaces in headers
|
||||
- Verify there are no special characters causing parsing issues
|
||||
|
||||
### Issue: Duplicate keyword error
|
||||
|
||||
**Solution:**
|
||||
- Keywords are unique per `(keyword, industry, sector)` combination
|
||||
- If importing a keyword that already exists, it will be updated (not duplicated)
|
||||
- Use `skip_unchanged = True` to avoid unnecessary updates
|
||||
|
||||
### Issue: Delete confirmation page has no "Delete" button
|
||||
|
||||
**Solution:** ✅ **FIXED** - Custom bulk delete action now includes proper delete button on confirmation page
|
||||
|
||||
---
|
||||
|
||||
## Permissions
|
||||
|
||||
| Action | Permission Required |
|
||||
|--------|-------------------|
|
||||
| View | Staff users |
|
||||
| Add | Superuser |
|
||||
| Edit | Superuser |
|
||||
| Delete | Superuser or Developer |
|
||||
| Import | Superuser |
|
||||
| Export | Staff users |
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Model Location
|
||||
- **Model:** `backend/igny8_core/auth/models.py` - `SeedKeyword`
|
||||
- **Admin:** `backend/igny8_core/auth/admin.py` - `SeedKeywordAdmin`
|
||||
- **Resource:** `backend/igny8_core/auth/admin.py` - `SeedKeywordResource`
|
||||
|
||||
### Database Table
|
||||
- **Table name:** `igny8_seed_keywords`
|
||||
- **Unique constraint:** `(keyword, industry, sector)`
|
||||
- **Indexes:**
|
||||
- `keyword`
|
||||
- `industry, sector`
|
||||
- `industry, sector, is_active`
|
||||
- `country`
|
||||
|
||||
### API Access (Read-Only)
|
||||
- **Endpoint:** `/api/v1/auth/seed-keywords/`
|
||||
- **ViewSet:** `SeedKeywordViewSet` (ReadOnlyModelViewSet)
|
||||
- **Filters:** industry, sector, country, is_active
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Django Admin Guide](../../docs/90-REFERENCE/DJANGO-ADMIN-ACCESS-GUIDE.md)
|
||||
- [Models Reference](../../docs/90-REFERENCE/MODELS.md)
|
||||
- [Planner Module](../../docs/10-MODULES/PLANNER.md)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** January 11, 2026
|
||||
**Maintainer:** IGNY8 Team
|
||||
@@ -0,0 +1,314 @@
|
||||
# Final Model Schemas - Clean State
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines the simplified, clean architecture for AI configuration and billing models.
|
||||
|
||||
**Total Models**: 5 (down from 7)
|
||||
- IntegrationProvider (API keys)
|
||||
- AIModelConfig (model definitions + pricing)
|
||||
- AISettings (system defaults + account overrides) - *renamed from GlobalIntegrationSettings*
|
||||
- AccountSettings (generic key-value store)
|
||||
- CreditCostConfig (operation-level pricing)
|
||||
|
||||
**Models Removed**:
|
||||
- IntegrationSettings (merged into AISettings/AccountSettings)
|
||||
- CreditCostConfig.tokens_per_credit (moved to AIModelConfig)
|
||||
|
||||
---
|
||||
|
||||
## 1. IntegrationProvider (API Keys Only)
|
||||
|
||||
Centralized storage for ALL external service API keys.
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `provider_id` | CharField(50) PK | Yes | openai, runware, stripe, paypal, resend |
|
||||
| `display_name` | CharField(100) | Yes | Human-readable name |
|
||||
| `provider_type` | CharField(20) | Yes | ai / payment / email / storage |
|
||||
| `api_key` | CharField(500) | No | Primary API key |
|
||||
| `api_secret` | CharField(500) | No | Secondary secret (Stripe, PayPal) |
|
||||
| `webhook_secret` | CharField(500) | No | Webhook signing secret |
|
||||
| `api_endpoint` | URLField | No | Custom endpoint (optional) |
|
||||
| `config` | JSONField | No | Provider-specific config |
|
||||
| `is_active` | BooleanField | Yes | Enable/disable provider |
|
||||
| `is_sandbox` | BooleanField | Yes | Test mode flag |
|
||||
| `updated_by` | FK(User) | No | Audit trail |
|
||||
| `created_at` | DateTime | Auto | |
|
||||
| `updated_at` | DateTime | Auto | |
|
||||
|
||||
**Seeded Providers**:
|
||||
- `openai` - AI (text + DALL-E)
|
||||
- `runware` - AI (images)
|
||||
- `anthropic` - AI (future)
|
||||
- `stripe` - Payment
|
||||
- `paypal` - Payment
|
||||
- `resend` - Email
|
||||
|
||||
---
|
||||
|
||||
## 2. AIModelConfig (Single Source of Truth for Models)
|
||||
|
||||
All AI models (text + image) with pricing and credit configuration.
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `id` | AutoField PK | Auto | |
|
||||
| `model_name` | CharField(100) | Yes | gpt-5.1, dall-e-3, runware:97@1 |
|
||||
| `model_type` | CharField(20) | Yes | text / image |
|
||||
| `provider` | CharField(50) | Yes | Links to IntegrationProvider |
|
||||
| `display_name` | CharField(200) | Yes | Human-readable |
|
||||
| `is_default` | BooleanField | Yes | One default per type |
|
||||
| `is_active` | BooleanField | Yes | Enable/disable |
|
||||
| `cost_per_1k_input` | DecimalField | No | Provider cost (USD) - text models |
|
||||
| `cost_per_1k_output` | DecimalField | No | Provider cost (USD) - text models |
|
||||
| `tokens_per_credit` | IntegerField | No | Text: tokens per 1 credit (e.g., 1000) |
|
||||
| `credits_per_image` | IntegerField | No | Image: credits per image (e.g., 1, 5, 15) |
|
||||
| `quality_tier` | CharField(20) | No | basic / quality / premium |
|
||||
| `max_tokens` | IntegerField | No | Model token limit |
|
||||
| `context_window` | IntegerField | No | Model context size |
|
||||
| `capabilities` | JSONField | No | vision, function_calling, etc. |
|
||||
| `created_at` | DateTime | Auto | |
|
||||
| `updated_at` | DateTime | Auto | |
|
||||
|
||||
**Credit Configuration Examples**:
|
||||
|
||||
| Model | Type | tokens_per_credit | credits_per_image | quality_tier |
|
||||
|-------|------|-------------------|-------------------|--------------|
|
||||
| gpt-5.1 | text | 1000 | - | - |
|
||||
| gpt-4o-mini | text | 10000 | - | - |
|
||||
| runware:97@1 | image | - | 1 | basic |
|
||||
| dall-e-3 | image | - | 5 | quality |
|
||||
| google:4@2 | image | - | 15 | premium |
|
||||
|
||||
---
|
||||
|
||||
## 3. AISettings (Renamed from GlobalIntegrationSettings)
|
||||
|
||||
System-wide AI defaults. Singleton (pk=1).
|
||||
|
||||
| Field | Type | Required | Default | Notes |
|
||||
|-------|------|----------|---------|-------|
|
||||
| `id` | AutoField PK | Auto | 1 | Singleton |
|
||||
| `temperature` | FloatField | Yes | 0.7 | AI temperature (0.0-2.0) |
|
||||
| `max_tokens` | IntegerField | Yes | 8192 | Max response tokens |
|
||||
| `image_style` | CharField(30) | Yes | photorealistic | Default image style |
|
||||
| `image_quality` | CharField(20) | Yes | standard | standard / hd |
|
||||
| `max_images_per_article` | IntegerField | Yes | 4 | Max in-article images |
|
||||
| `image_size` | CharField(20) | Yes | 1024x1024 | Default image dimensions |
|
||||
| `updated_by` | FK(User) | No | | Audit trail |
|
||||
| `updated_at` | DateTime | Auto | | |
|
||||
|
||||
**Removed Fields** (now elsewhere):
|
||||
- All `*_api_key` fields → IntegrationProvider
|
||||
- All `*_model` fields → AIModelConfig.is_default
|
||||
- `default_text_provider` → AIModelConfig.is_default where model_type='text'
|
||||
- `default_image_service` → AIModelConfig.is_default where model_type='image'
|
||||
|
||||
**Image Style Choices**:
|
||||
- `photorealistic` - Ultra realistic photography
|
||||
- `illustration` - Digital illustration
|
||||
- `3d_render` - Computer generated 3D
|
||||
- `minimal_flat` - Minimal / Flat Design
|
||||
- `artistic` - Artistic / Painterly
|
||||
- `cartoon` - Cartoon / Stylized
|
||||
|
||||
---
|
||||
|
||||
## 4. AccountSettings (Per-Account Overrides)
|
||||
|
||||
Generic key-value store for account-specific settings.
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `id` | AutoField PK | Auto | |
|
||||
| `account` | FK(Account) | Yes | |
|
||||
| `key` | CharField(100) | Yes | Setting key |
|
||||
| `value` | JSONField | Yes | Setting value |
|
||||
| `created_at` | DateTime | Auto | |
|
||||
| `updated_at` | DateTime | Auto | |
|
||||
|
||||
**Unique Constraint**: `(account, key)`
|
||||
|
||||
**AI-Related Keys** (override AISettings defaults):
|
||||
|
||||
| Key | Type | Example | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `ai.temperature` | float | 0.7 | Override system default |
|
||||
| `ai.max_tokens` | int | 8192 | Override system default |
|
||||
| `ai.image_style` | string | "photorealistic" | Override system default |
|
||||
| `ai.image_quality` | string | "hd" | Override system default |
|
||||
| `ai.max_images` | int | 6 | Override system default |
|
||||
| `ai.image_quality_tier` | string | "quality" | User's preferred tier |
|
||||
|
||||
---
|
||||
|
||||
## 5. CreditCostConfig (Operation-Level Pricing)
|
||||
|
||||
Fixed credit costs per operation type.
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `operation_type` | CharField(50) PK | Yes | Unique operation ID |
|
||||
| `display_name` | CharField(100) | Yes | Human-readable |
|
||||
| `base_credits` | IntegerField | Yes | Fixed credits per operation |
|
||||
| `is_active` | BooleanField | Yes | Enable/disable |
|
||||
| `description` | TextField | No | Admin notes |
|
||||
|
||||
**Removed**: `tokens_per_credit` (now in AIModelConfig)
|
||||
|
||||
---
|
||||
|
||||
## Frontend Settings Structure
|
||||
|
||||
### Content Generation Settings Tab
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Content Generation Settings │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ AI Parameters │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Temperature [=====○====] 0.7 │ │
|
||||
│ │ More focused ←→ More creative │ │
|
||||
│ │ │ │
|
||||
│ │ Max Tokens [8192 ▼] │ │
|
||||
│ │ Response length limit │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Image Generation │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Image Quality │ │
|
||||
│ │ ○ Basic (1 credit/image) - Fast, simple │ │
|
||||
│ │ ● Quality (5 credits/image) - Balanced │ │
|
||||
│ │ ○ Premium (15 credits/image) - Best quality │ │
|
||||
│ │ │ │
|
||||
│ │ Image Style [Photorealistic ▼] │ │
|
||||
│ │ │ │
|
||||
│ │ Images per Article [4 ▼] (max 8) │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Save Settings] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Frontend API Response
|
||||
|
||||
```
|
||||
GET /api/v1/accounts/settings/ai/
|
||||
|
||||
Response:
|
||||
{
|
||||
"content_generation": {
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 8192
|
||||
},
|
||||
"image_generation": {
|
||||
"quality_tiers": [
|
||||
{"tier": "basic", "credits": 1, "label": "Basic", "description": "Fast, simple images"},
|
||||
{"tier": "quality", "credits": 5, "label": "Quality", "description": "Balanced quality"},
|
||||
{"tier": "premium", "credits": 15, "label": "Premium", "description": "Best quality"}
|
||||
],
|
||||
"selected_tier": "quality",
|
||||
"styles": [
|
||||
{"value": "photorealistic", "label": "Photorealistic"},
|
||||
{"value": "illustration", "label": "Illustration"},
|
||||
{"value": "3d_render", "label": "3D Render"},
|
||||
{"value": "minimal_flat", "label": "Minimal / Flat"},
|
||||
{"value": "artistic", "label": "Artistic"},
|
||||
{"value": "cartoon", "label": "Cartoon"}
|
||||
],
|
||||
"selected_style": "photorealistic",
|
||||
"max_images": 4,
|
||||
"max_allowed": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Save Settings Request
|
||||
|
||||
```
|
||||
PUT /api/v1/accounts/settings/ai/
|
||||
|
||||
Request:
|
||||
{
|
||||
"temperature": 0.8,
|
||||
"max_tokens": 4096,
|
||||
"image_quality_tier": "premium",
|
||||
"image_style": "illustration",
|
||||
"max_images": 6
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ IntegrationProv. │ │ AIModelConfig │ │ AISettings │
|
||||
│ │ │ │ │ (Singleton) │
|
||||
│ - API keys │◄────│ - Model list │ │ │
|
||||
│ - Provider info │ │ - Pricing │ │ - Defaults │
|
||||
└──────────────────┘ │ - Credits config │ │ - temperature │
|
||||
│ - quality_tier │ │ - max_tokens │
|
||||
└────────┬─────────┘ │ - image_style │
|
||||
│ └────────┬─────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────────────────────────┐
|
||||
│ AccountSettings │
|
||||
│ │
|
||||
│ Account-specific overrides: │
|
||||
│ - ai.temperature = 0.8 │
|
||||
│ - ai.image_quality_tier = "premium" │
|
||||
│ - ai.image_style = "illustration" │
|
||||
└──────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Frontend Settings UI │
|
||||
│ │
|
||||
│ GET /api/v1/accounts/settings/ai/ │
|
||||
│ - Merges AISettings + AccountSettings │
|
||||
│ - Returns effective values for account │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Summary
|
||||
|
||||
| From | To | Action |
|
||||
|------|----|--------|
|
||||
| GlobalIntegrationSettings.*_api_key | IntegrationProvider | Already done |
|
||||
| GlobalIntegrationSettings.*_model | AIModelConfig.is_default | Already done |
|
||||
| GlobalIntegrationSettings (remaining) | AISettings (rename) | Phase 3 |
|
||||
| IntegrationSettings | AccountSettings | Phase 3 - delete model |
|
||||
| CreditCostConfig.tokens_per_credit | AIModelConfig.tokens_per_credit | Already done |
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 Implementation Steps
|
||||
|
||||
1. **Rename Model**: GlobalIntegrationSettings → AISettings
|
||||
2. **Remove Fields**: All `*_api_key`, `*_model` fields from AISettings
|
||||
3. **Create API Endpoint**: `/api/v1/accounts/settings/ai/`
|
||||
4. **Update Frontend**: Load from new endpoint, use quality_tier picker
|
||||
5. **Delete Model**: IntegrationSettings (after migrating any data)
|
||||
6. **Cleanup**: Remove deprecated choices/constants
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify (Phase 3)
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `modules/system/global_settings_models.py` | Rename class, remove deprecated fields |
|
||||
| `modules/system/admin.py` | Update admin for AISettings |
|
||||
| `modules/system/views.py` | New AI settings API endpoint |
|
||||
| `modules/system/serializers.py` | AISettingsSerializer |
|
||||
| `settings.py` | Update admin sidebar |
|
||||
| `ai/ai_core.py` | Use AISettings instead of GlobalIntegrationSettings |
|
||||
| Frontend | New settings component with quality tier picker |
|
||||
Reference in New Issue
Block a user