35 KiB
Safe Migration & Testing Plan
Overview
This document ensures the refactor is done safely with zero breakage. All existing functionality must work identically before and after migration.
Current Working System Components
1. AI Functions (Core - Used by ALL paths)
All AI operations go through these functions in backend/igny8_core/ai/functions/:
| Function | File | Operation Type | Credits | Called Via |
|---|---|---|---|---|
| Auto Cluster | auto_cluster.py |
clustering |
Token-based | AIEngine |
| Generate Ideas | generate_ideas.py |
idea_generation |
Token-based | AIEngine |
| Generate Content | generate_content.py |
content_generation |
Token-based | AIEngine |
| Generate Image Prompts | generate_image_prompts.py |
image_prompt_extraction |
Token-based | AIEngine |
| Generate Images | generate_images.py |
image_generation |
Per-image | AICore direct |
| Optimize Content | optimize_content.py |
content_optimization |
Token-based | OptimizerService |
Operation Type Mapping (in engine.py):
mapping = {
'auto_cluster': 'clustering',
'generate_ideas': 'idea_generation',
'generate_content': 'content_generation',
'generate_image_prompts': 'image_prompt_extraction',
'generate_images': 'image_generation',
'generate_site_structure': 'site_structure_generation',
}
1.1 Non-AIEngine Credit Deduction Points
| Service | Operation Type | Direct Credit Deduction |
|---|---|---|
| LinkerService | internal_linking |
✅ Uses CreditService directly |
| OptimizerService | content_optimization |
✅ Uses CreditService directly |
2. Execution Paths (All use same AI functions)
┌─────────────────────────────────────────────────────────────────────────┐
│ ALL EXECUTION PATHS │
└─────────────────────────────────────────────────────────────────────────┘
PATH 1: Manual Module Page Buttons (via run_ai_task Celery task)
├── Planner Page → "Cluster Keywords" button → api/planner.py → run_ai_task.delay('auto_cluster')
├── Planner Page → "Generate Ideas" button → api/planner.py → run_ai_task.delay('generate_ideas')
├── Writer Page → "Generate Content" button → api/writer.py → run_ai_task.delay('generate_content')
├── Writer Page → "Extract Prompts" button → api/writer.py → run_ai_task.delay('generate_image_prompts')
├── Writer Page → "Generate Images" button → api/writer.py → process_image_generation_queue.delay()
└── All go through: Celery task → AIEngine → AI Function → AICore → API → CreditService
PATH 2: Automation Page Manual Run
├── Automation Page → "Run Now" button
├── automation/tasks.py → run_automation_task
├── AutomationService.run_automation()
│ Stage 1: _run_clustering() → AIEngine.execute('auto_cluster')
│ Stage 2: _run_idea_generation() → AIEngine.execute('generate_ideas')
│ Stage 3: _run_task_creation() → Database only (no AI)
│ Stage 4: _run_content_generation() → AIEngine.execute('generate_content')
│ Stage 5: _run_image_prompt_extraction() → AIEngine.execute('generate_image_prompts')
│ Stage 6: _run_image_generation() → process_image_generation_queue.delay()
│ Stage 7: _run_publishing() → WordPress API (no AI)
└── Same credit deduction path (stages 1,2,4,5 via AIEngine, stage 6 via AICore)
PATH 3: Scheduled Automation Run
├── Celery Beat scheduler → check_scheduled_automations task (hourly)
├── automation_tasks.py → run_automation_task.delay()
├── AutomationService.run_automation()
├── Calls same AI functions through AIEngine
└── Same credit deduction path
PATH 4: Direct Service Operations (Bypass AIEngine)
├── Linker Module → LinkerService → CreditService.deduct_credits_for_operation('internal_linking')
├── Optimizer Module → OptimizerService → CreditService.deduct_credits_for_operation('content_optimization')
└── These use direct credit deduction, not via AIEngine
3. Credit Flow (Current)
┌─────────────────────────────────────────────────────────────────────────┐
│ CURRENT CREDIT FLOW │
└─────────────────────────────────────────────────────────────────────────┘
For TEXT operations (clustering, ideas, content) - VIA AIEngine:
1. API endpoint receives request
2. Celery task run_ai_task triggered
3. AIEngine.execute(function_name, payload)
a. Step 2.5: CreditService.check_credits() - PRE-CHECK
b. Step 3: AICore.run_ai_request() - makes OpenAI API call
c. Response includes token usage (input_tokens, output_tokens)
d. Step 5.5: CreditService.deduct_credits_for_operation() - DEDUCTION
4. CreditService.calculate_credits_from_tokens() uses CreditCostConfig
5. Creates CreditTransaction (balance history)
6. Creates CreditUsageLog (detailed tracking)
7. Updates Account.credits
For IMAGE operations - VIA process_image_generation_queue:
1. API endpoint receives request
2. Celery task process_image_generation_queue triggered
3. For each image: AICore.generate_image()
a. Makes Runware/OpenAI API call
b. Calculates cost via ModelRegistry.calculate_cost()
c. Returns image URL
4. ⚠️ Credit deduction handled AFTER all images generated
5. Uses CreditCostConfig.image_generation entry (min_credits, tokens_per_credit)
6. Creates CreditUsageLog
For DIRECT SERVICE OPERATIONS (Linker, Optimizer):
1. Service method called (e.g., LinkerService.generate_links())
2. AI call made directly to AICore or external service
3. CreditService.deduct_credits_for_operation() called directly
4. Uses operation-specific CreditCostConfig entry
3.1 Celery Task Definitions
| Task File | Task Name | Entry Point For |
|---|---|---|
ai/tasks.py |
run_ai_task |
All manual AI buttons (universal task) |
ai/tasks.py |
process_image_generation_queue |
Image generation queue |
automation/tasks.py |
check_scheduled_automations |
Hourly scheduler check |
automation/tasks.py |
run_automation_task |
Full automation pipeline |
automation/tasks.py |
resume_paused_automation |
Resume after pause |
4. Key Files Currently Working
| File | Purpose | Must Keep Working |
|---|---|---|
ai/engine.py |
Orchestrates AI functions | ✅ |
ai/ai_core.py |
API calls, key loading | ✅ |
ai/model_registry.py |
Model config lookup | ✅ |
ai/settings.py |
get_model_config() | ✅ |
ai/tasks.py |
Celery tasks | ✅ |
ai/functions/*.py |
All AI functions | ✅ |
billing/services/credit_service.py |
Credit calculation | ✅ |
billing/models.py |
CreditUsageLog, CreditCostConfig | ✅ |
automation/services/automation_service.py |
Automation runner | ✅ |
modules/system/global_settings_models.py |
API keys, defaults | ✅ |
Potential Issues to Verify Before Migration
⚠️ Issue 1: Image Credit Deduction May Be Incomplete
The process_image_generation_queue task calculates cost via ModelRegistry.calculate_cost() but credit deduction needs verification:
- Does it call
CreditService.deduct_credits_for_operation()? - Or does it just log cost without deducting credits?
Verification Query:
docker exec igny8_backend python manage.py shell -c "
from igny8_core.business.billing.models import CreditUsageLog
logs = CreditUsageLog.objects.filter(operation_type='image_generation').order_by('-created_at')[:5]
for log in logs:
print(f'{log.operation_type}: {log.credits_used} credits, cost=${log.cost_usd}, model={log.model_used}')
if not logs:
print('⚠️ NO image_generation credit logs found - may not be tracking credits!')
"
⚠️ Issue 2: GlobalIntegrationSettings.runware_model Points to Non-Existent Model
Current value: bria:10@1 - but this model doesn't exist in AIModelConfig.
This may cause fallback issues.
⚠️ Issue 3: Multiple Credit Calculation Paths
Credits are calculated in different ways depending on the path:
- Via AIEngine → uses
CreditCostConfigtoken-based calculation - Via direct service → uses
CreditCostConfigwith operation-specific rates - Via image generation → may use different calculation
Need to unify after migration.
Pre-Migration Baseline Capture
Step 1: Database State Snapshot
Run this BEFORE any changes to establish baseline:
docker exec igny8_backend python manage.py shell << 'EOF'
import json
from datetime import datetime
print("=" * 80)
print(f"BASELINE SNAPSHOT - {datetime.now().isoformat()}")
print("=" * 80)
# 1. AIModelConfig
print("\n=== AIModelConfig ===")
from igny8_core.business.billing.models import AIModelConfig
for m in AIModelConfig.objects.all().order_by('model_type', 'sort_order'):
print(json.dumps({
'model_name': m.model_name,
'model_type': m.model_type,
'provider': m.provider,
'is_active': m.is_active,
'is_default': m.is_default,
'cost_per_image': str(m.cost_per_image) if m.cost_per_image else None,
'input_cost_per_1m': str(m.input_cost_per_1m) if m.input_cost_per_1m else None,
'output_cost_per_1m': str(m.output_cost_per_1m) if m.output_cost_per_1m else None,
}))
# 2. CreditCostConfig
print("\n=== CreditCostConfig ===")
from igny8_core.business.billing.models import CreditCostConfig
for c in CreditCostConfig.objects.filter(is_active=True):
print(json.dumps({
'operation_type': c.operation_type,
'tokens_per_credit': c.tokens_per_credit,
'min_credits': c.min_credits,
'price_per_credit_usd': str(c.price_per_credit_usd),
}))
# 3. GlobalIntegrationSettings
print("\n=== GlobalIntegrationSettings ===")
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
g = GlobalIntegrationSettings.get_instance()
print(json.dumps({
'openai_model': g.openai_model,
'openai_temperature': g.openai_temperature,
'openai_max_tokens': g.openai_max_tokens,
'default_text_provider': g.default_text_provider,
'dalle_model': g.dalle_model,
'runware_model': g.runware_model,
'default_image_service': g.default_image_service,
'image_style': g.image_style,
'max_in_article_images': g.max_in_article_images,
'has_openai_key': bool(g.openai_api_key),
'has_runware_key': bool(g.runware_api_key),
}))
# 4. Recent Credit Usage (last 20)
print("\n=== Recent CreditUsageLog (last 20) ===")
from igny8_core.business.billing.models import CreditUsageLog
for log in CreditUsageLog.objects.all().order_by('-created_at')[:20]:
print(json.dumps({
'operation_type': log.operation_type,
'credits_used': log.credits_used,
'model_used': log.model_used,
'tokens_input': log.tokens_input,
'tokens_output': log.tokens_output,
'cost_usd': str(log.cost_usd) if log.cost_usd else None,
}))
# 5. Account Credits
print("\n=== Account Credits ===")
from igny8_core.auth.models import Account
for acc in Account.objects.all()[:5]:
print(json.dumps({
'account_id': acc.id,
'name': acc.name,
'credits': acc.credits,
}))
print("\n" + "=" * 80)
print("BASELINE CAPTURE COMPLETE")
print("=" * 80)
EOF
Save this output to: 4th-jan-refactor/baseline-snapshot.json
Integration Tests (Must Pass Before AND After)
Test File: backend/igny8_core/tests/test_ai_system_integration.py
"""
AI System Integration Tests
===========================
These tests verify the entire AI pipeline works end-to-end.
Run BEFORE migration to establish baseline.
Run AFTER each phase to verify nothing broke.
Usage:
docker exec igny8_backend python manage.py test igny8_core.tests.test_ai_system_integration
"""
from decimal import Decimal
from django.test import TestCase, TransactionTestCase
from django.db import transaction
from unittest.mock import patch, MagicMock
import json
class AIModelConfigTests(TestCase):
"""Test AIModelConfig loading and access"""
def test_text_models_exist(self):
"""All required text models exist and are configured"""
from igny8_core.business.billing.models import AIModelConfig
required_text_models = ['gpt-5.1', 'gpt-4o-mini']
for model_name in required_text_models:
model = AIModelConfig.objects.filter(model_name=model_name).first()
self.assertIsNotNone(model, f"Text model {model_name} not found")
self.assertEqual(model.model_type, 'text')
def test_image_models_exist(self):
"""All required image models exist and are configured"""
from igny8_core.business.billing.models import AIModelConfig
required_image_models = ['runware:97@1', 'dall-e-3', 'google:4@2']
for model_name in required_image_models:
model = AIModelConfig.objects.filter(model_name=model_name).first()
self.assertIsNotNone(model, f"Image model {model_name} not found")
self.assertEqual(model.model_type, 'image')
self.assertIsNotNone(model.cost_per_image, f"{model_name} missing cost_per_image")
def test_default_text_model_exists(self):
"""Exactly one default text model is set"""
from igny8_core.business.billing.models import AIModelConfig
defaults = AIModelConfig.objects.filter(model_type='text', is_default=True, is_active=True)
self.assertEqual(defaults.count(), 1, "Should have exactly 1 default text model")
self.assertEqual(defaults.first().model_name, 'gpt-5.1')
def test_default_image_model_exists(self):
"""Exactly one default image model is set"""
from igny8_core.business.billing.models import AIModelConfig
defaults = AIModelConfig.objects.filter(model_type='image', is_default=True, is_active=True)
self.assertEqual(defaults.count(), 1, "Should have exactly 1 default image model")
class ModelRegistryTests(TestCase):
"""Test ModelRegistry functionality"""
def test_get_model_from_db(self):
"""ModelRegistry.get_model() returns model from database"""
from igny8_core.ai.model_registry import ModelRegistry
model = ModelRegistry.get_model('gpt-5.1')
self.assertIsNotNone(model)
# Should be AIModelConfig instance or dict with model_name
if hasattr(model, 'model_name'):
self.assertEqual(model.model_name, 'gpt-5.1')
else:
self.assertEqual(model.get('model_name'), 'gpt-5.1')
def test_get_image_model(self):
"""ModelRegistry returns image models correctly"""
from igny8_core.ai.model_registry import ModelRegistry
for model_name in ['runware:97@1', 'dall-e-3', 'google:4@2']:
model = ModelRegistry.get_model(model_name)
self.assertIsNotNone(model, f"Model {model_name} not found")
def test_calculate_cost_text(self):
"""Cost calculation for text models works"""
from igny8_core.ai.model_registry import ModelRegistry
cost = ModelRegistry.calculate_cost('gpt-5.1', input_tokens=1000, output_tokens=500)
self.assertIsInstance(cost, (int, float, Decimal))
self.assertGreater(cost, 0)
def test_calculate_cost_image(self):
"""Cost calculation for image models works"""
from igny8_core.ai.model_registry import ModelRegistry
cost = ModelRegistry.calculate_cost('dall-e-3', num_images=1)
self.assertIsInstance(cost, (int, float, Decimal))
self.assertGreater(cost, 0)
class APIKeyLoadingTests(TestCase):
"""Test API key loading from GlobalIntegrationSettings"""
def test_openai_key_loads(self):
"""OpenAI API key loads from GlobalIntegrationSettings"""
from igny8_core.ai.ai_core import AICore
ai_core = AICore()
key = ai_core.get_api_key('openai')
self.assertIsNotNone(key, "OpenAI API key not configured")
self.assertTrue(len(key) > 10, "OpenAI API key too short")
def test_runware_key_loads(self):
"""Runware API key loads from GlobalIntegrationSettings"""
from igny8_core.ai.ai_core import AICore
ai_core = AICore()
key = ai_core.get_api_key('runware')
self.assertIsNotNone(key, "Runware API key not configured")
class CreditServiceTests(TestCase):
"""Test credit calculation and deduction"""
def test_calculate_credits_text_operation(self):
"""Credit calculation for text operations works"""
from igny8_core.business.billing.services.credit_service import CreditService
# Test content generation
credits = CreditService.calculate_credits_from_tokens(
'content_generation',
tokens_input=1000,
tokens_output=2000
)
self.assertIsInstance(credits, int)
self.assertGreater(credits, 0)
def test_calculate_credits_image_operation(self):
"""Credit calculation for image operations works"""
from igny8_core.business.billing.services.credit_service import CreditService
# Image generation uses min_credits from CreditCostConfig
credits = CreditService.calculate_credits_from_tokens(
'image_generation',
tokens_input=0,
tokens_output=0
)
self.assertIsInstance(credits, int)
self.assertGreaterEqual(credits, 1)
def test_credit_cost_config_exists(self):
"""All required CreditCostConfig entries exist"""
from igny8_core.business.billing.models import CreditCostConfig
required_ops = ['clustering', 'idea_generation', 'content_generation',
'image_generation', 'image_prompt_extraction']
for op in required_ops:
config = CreditCostConfig.objects.filter(operation_type=op, is_active=True).first()
self.assertIsNotNone(config, f"CreditCostConfig for {op} not found")
class AISettingsTests(TestCase):
"""Test get_model_config() function"""
def test_get_model_config_returns_config(self):
"""get_model_config() returns valid configuration"""
from igny8_core.ai.settings import get_model_config
from igny8_core.auth.models import Account
account = Account.objects.first()
if account:
config = get_model_config('content_generation', account)
self.assertIn('model', config)
self.assertIn('max_tokens', config)
self.assertIn('temperature', config)
self.assertIsNotNone(config['model'])
class AIFunctionValidationTests(TestCase):
"""Test AI function classes load correctly"""
def test_auto_cluster_function_loads(self):
"""AutoClusterFunction loads and validates"""
from igny8_core.ai.functions.auto_cluster import AutoClusterFunction
fn = AutoClusterFunction()
self.assertEqual(fn.get_name(), 'auto_cluster')
self.assertIsNotNone(fn.get_metadata())
def test_generate_ideas_function_loads(self):
"""GenerateIdeasFunction loads and validates"""
from igny8_core.ai.functions.generate_ideas import GenerateIdeasFunction
fn = GenerateIdeasFunction()
self.assertEqual(fn.get_name(), 'generate_ideas')
def test_generate_content_function_loads(self):
"""GenerateContentFunction loads and validates"""
from igny8_core.ai.functions.generate_content import GenerateContentFunction
fn = GenerateContentFunction()
self.assertEqual(fn.get_name(), 'generate_content')
def test_generate_images_function_loads(self):
"""GenerateImagesFunction loads and validates"""
from igny8_core.ai.functions.generate_images import GenerateImagesFunction
fn = GenerateImagesFunction()
self.assertEqual(fn.get_name(), 'generate_images')
class GlobalIntegrationSettingsTests(TestCase):
"""Test GlobalIntegrationSettings singleton"""
def test_singleton_loads(self):
"""GlobalIntegrationSettings singleton loads"""
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
settings = GlobalIntegrationSettings.get_instance()
self.assertIsNotNone(settings)
self.assertEqual(settings.pk, 1)
def test_image_settings_exist(self):
"""Image settings are configured"""
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
settings = GlobalIntegrationSettings.get_instance()
self.assertIsNotNone(settings.image_style)
self.assertIsNotNone(settings.max_in_article_images)
self.assertGreater(settings.max_in_article_images, 0)
class AutomationServiceTests(TestCase):
"""Test AutomationService uses same AI functions"""
def test_automation_service_imports(self):
"""AutomationService can import AI functions"""
from igny8_core.business.automation.services.automation_service import AutomationService
# Just verify the import works and class exists
self.assertIsNotNone(AutomationService)
Running Tests
# Run all integration tests
docker exec igny8_backend python manage.py test igny8_core.tests.test_ai_system_integration -v 2
# Run specific test class
docker exec igny8_backend python manage.py test igny8_core.tests.test_ai_system_integration.AIModelConfigTests -v 2
# Run with coverage
docker exec igny8_backend coverage run manage.py test igny8_core.tests.test_ai_system_integration
docker exec igny8_backend coverage report
Phased Migration with Verification
Phase 1: Add New Models (NO BREAKING CHANGES)
Changes:
- Add
IntegrationProvidermodel - Add new fields to
AIModelConfig:credits_per_image,tokens_per_credit,quality_tier - Create migrations
- Populate new data
Verification:
# 1. Run migrations
docker exec igny8_backend python manage.py makemigrations
docker exec igny8_backend python manage.py migrate
# 2. Run ALL tests - must pass
docker exec igny8_backend python manage.py test igny8_core.tests.test_ai_system_integration -v 2
# 3. Manual verification - existing features still work
# - Go to Writer page, generate content (check credits deducted)
# - Go to Writer page, generate images (check credits deducted)
# - Run automation manually (check it completes)
Rollback: Delete new tables/fields (no existing data affected)
Phase 2: Add Parallel Code Paths (OLD KEEPS WORKING)
Changes:
- Add new methods to
ModelRegistry(keep old ones) - Add new methods to
CreditService(keep old ones) - Add comparison logging
Pattern:
# model_registry.py
@classmethod
def get_default_model_NEW(cls, model_type: str):
"""NEW: Get default model from AIModelConfig.is_default"""
from igny8_core.business.billing.models import AIModelConfig
return AIModelConfig.objects.filter(
model_type=model_type,
is_default=True,
is_active=True
).first()
@classmethod
def get_model(cls, model_id: str):
"""EXISTING: Keep working exactly as before"""
# ... existing code unchanged ...
# credit_service.py
@staticmethod
def calculate_credits_for_image_NEW(model_name: str, num_images: int) -> int:
"""NEW: Calculate from AIModelConfig.credits_per_image"""
from igny8_core.business.billing.models import AIModelConfig
try:
model = AIModelConfig.objects.get(model_name=model_name, is_active=True)
if model.credits_per_image:
return model.credits_per_image * num_images
except AIModelConfig.DoesNotExist:
pass
return None # Signal to use old method
@staticmethod
def calculate_credits_from_tokens(operation_type, tokens_input, tokens_output):
"""EXISTING: Keep working, add comparison logging"""
# Calculate old way
old_result = cls._calculate_old(operation_type, tokens_input, tokens_output)
# Calculate new way (for comparison only)
new_result = cls._calculate_new(operation_type, tokens_input, tokens_output)
# Log if different
if old_result != new_result:
logger.warning(f"[MIGRATION] Credit calc mismatch: {operation_type} old={old_result} new={new_result}")
# Return OLD (safe) for now
return old_result
Verification:
# 1. Run tests
docker exec igny8_backend python manage.py test igny8_core.tests.test_ai_system_integration -v 2
# 2. Check logs for any mismatch warnings
docker logs igny8_backend 2>&1 | grep "MIGRATION"
# 3. Manual test all paths
Rollback: Remove new methods only
Phase 3: Add IntegrationProvider for API Keys (OLD FALLBACK)
Changes:
AICore._load_account_settings()triesIntegrationProviderfirst, falls back toGlobalIntegrationSettings
Pattern:
# ai_core.py
def _load_account_settings(self):
"""Load API keys - try new IntegrationProvider, fallback to old"""
# Try NEW way first
try:
from igny8_core.business.billing.models import IntegrationProvider
openai_provider = IntegrationProvider.objects.filter(
provider_id='openai', is_active=True
).first()
if openai_provider and openai_provider.api_key:
self._openai_api_key = openai_provider.api_key
logger.info("[MIGRATION] Loaded OpenAI key from IntegrationProvider")
else:
raise Exception("Fallback to old method")
except Exception:
# FALLBACK to old GlobalIntegrationSettings
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
global_settings = GlobalIntegrationSettings.get_instance()
self._openai_api_key = global_settings.openai_api_key
logger.info("[MIGRATION] Loaded OpenAI key from GlobalIntegrationSettings (fallback)")
# Same pattern for runware...
Verification:
# 1. Run tests
docker exec igny8_backend python manage.py test igny8_core.tests.test_ai_system_integration -v 2
# 2. Verify API calls work
# - Generate content (uses OpenAI)
# - Generate images with Runware model
# - Generate images with DALL-E model
# 3. Check logs
docker logs igny8_backend 2>&1 | grep "MIGRATION"
Rollback: Revert _load_account_settings() to old version
Phase 4: Switch to New Credit Calculation
Changes:
- Use
AIModelConfig.credits_per_imagefor image operations - Use
AIModelConfig.tokens_per_creditfor text operations - Keep
CreditCostConfigas fallback
Verification:
# 1. Before switching, compare calculations for 1 week
# Log both old and new results, verify they match
# 2. Run tests
docker exec igny8_backend python manage.py test igny8_core.tests.test_ai_system_integration -v 2
# 3. Verify credit logs show correct values
docker exec igny8_backend python manage.py shell -c "
from igny8_core.business.billing.models import CreditUsageLog
for log in CreditUsageLog.objects.order_by('-created_at')[:10]:
print(f'{log.operation_type}: {log.credits_used} credits, model={log.model_used}')
"
Phase 5: Create API Endpoint
Changes:
- Add
/api/v1/system/ai-models/endpoint - Returns models from database
Verification:
# 1. Test endpoint
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/v1/system/ai-models/
# 2. Verify response structure
# Should have text_models, image_models, image_settings
Phase 6: Update Frontend
Changes:
- Remove hardcoded model choices from
Settings.tsx - Load from API
Verification:
# 1. Frontend tests
cd frontend && npm run test
# 2. Manual verification
# - Open Sites > Settings > Image Settings
# - Verify dropdown shows models from API
# - Change model, save, verify it persists
Phase 7: Cleanup (After 1 Week Stable)
Remove from GlobalIntegrationSettings:
# API key fields (moved to IntegrationProvider)
- openai_api_key
- anthropic_api_key # unused
- bria_api_key # unused
- runware_api_key
# Model selection fields (now from AIModelConfig.is_default)
- openai_model
- dalle_model
- runware_model
- default_text_provider
- default_image_service
# Hardcoded CHOICES
- OPENAI_MODEL_CHOICES
- DALLE_MODEL_CHOICES
- RUNWARE_MODEL_CHOICES
- TEXT_PROVIDER_CHOICES
- IMAGE_SERVICE_CHOICES
Remove from constants.py:
- MODEL_RATES = {...}
- IMAGE_MODEL_RATES = {...}
Remove from model_registry.py:
- Fallback to constants.py (all lookups must use DB)
- Hardcoded default model names
Remove from ai_core.py:
- IMAGE_MODEL_RATES import and usage
- Direct GlobalIntegrationSettings key access (use IntegrationProvider)
Remove CreditCostConfig entries (optional - use AIModelConfig):
- image_generation entry (use AIModelConfig.credits_per_image instead)
Remove from frontend Settings.tsx:
- QUALITY_TO_CONFIG hardcoded mapping
- RUNWARE_MODEL_CHOICES hardcoded array
- DALLE_MODEL_CHOICES hardcoded array
- MODEL_LANDSCAPE_SIZES hardcoded mapping
- Any hardcoded model names
Verification:
# 1. Run full test suite
docker exec igny8_backend python manage.py test
# 2. Run integration tests
docker exec igny8_backend python manage.py test igny8_core.tests.test_ai_system_integration -v 2
# 3. Full manual test of all paths
Complete Test Checklist
Manual Test Checklist (Run After Each Phase)
Path 1: Manual Module Page Actions
| Test | Page | Button/Action | Expected Result | Verify Credits | Pass? |
|---|---|---|---|---|---|
| Cluster Keywords | Planner | "Cluster" button | Creates clusters | Check CreditUsageLog for clustering |
☐ |
| Generate Ideas | Planner | "Generate Ideas" button | Creates ideas | Check CreditUsageLog for idea_generation |
☐ |
| Generate Content | Writer | "Generate" button | Creates content | Check CreditUsageLog for content_generation |
☐ |
| Generate Product Content | Writer | "Generate Product" button | Creates product content | Check CreditUsageLog for content_generation |
☐ |
| Generate Service Content | Writer | "Generate Service" button | Creates service content | Check CreditUsageLog for content_generation |
☐ |
| Extract Image Prompts | Writer | "Extract Prompts" button | Creates prompts | Check CreditUsageLog for image_prompt_extraction |
☐ |
| Generate Images (Basic) | Writer | Image gen w/ Runware basic | Creates images | Check CreditUsageLog: 1 credit/image | ☐ |
| Generate Images (Quality) | Writer | Image gen w/ DALL-E | Creates images | Check CreditUsageLog: 5 credits/image | ☐ |
| Generate Images (Premium) | Writer | Image gen w/ Google | Creates images | Check CreditUsageLog: 15 credits/image | ☐ |
| Internal Linking | Linker | "Generate Links" button | Creates links | Check CreditUsageLog for internal_linking |
☐ |
| Optimize Content | Optimizer | "Optimize" button | Optimizes content | Check CreditUsageLog for content_optimization |
☐ |
Path 2: Automation Manual Run
| Test | Page | Action | Expected Result | Verify Credits | Pass? |
|---|---|---|---|---|---|
| Run Full Automation | Automation | "Run Now" button | All stages complete | Credits for each stage | ☐ |
| Run Stage 1 Only | Automation | Run clustering stage | Clusters created | Check clustering credits |
☐ |
| Run Stage 2 Only | Automation | Run idea stage | Ideas created | Check idea_generation credits |
☐ |
| Run Stage 4 Only | Automation | Run content stage | Content created | Check content_generation credits |
☐ |
| Run Stage 5 Only | Automation | Run prompts stage | Prompts created | Check image_prompt_extraction credits |
☐ |
| Run Stage 6 Only | Automation | Run image stage | Images created | Check image_generation credits |
☐ |
| Pause/Resume | Automation | Pause then resume | Continues correctly | No double-charging | ☐ |
Path 3: Scheduled Automation
| Test | Setup | Expected | Verify | Pass? |
|---|---|---|---|---|
| Schedule Triggers | Set schedule for automation | Runs on schedule | Check logs at scheduled time | ☐ |
| Credits Deducted | After scheduled run | Credits reduced | Check account balance | ☐ |
| Multiple Automations | Multiple scheduled | All run | Each deducts credits | ☐ |
Credit Verification Queries
# Check recent credit usage logs
docker exec igny8_backend python manage.py shell -c "
from igny8_core.business.billing.models import CreditUsageLog
for log in CreditUsageLog.objects.order_by('-created_at')[:20]:
print(f'{log.created_at}: {log.operation_type} | {log.credits_used} credits | model={log.model_used} | tokens={log.tokens_input}+{log.tokens_output}')
"
# Check account balance
docker exec igny8_backend python manage.py shell -c "
from igny8_core.auth.models import Account
for acc in Account.objects.all()[:5]:
print(f'{acc.name}: {acc.credits} credits')
"
# Check credit transactions
docker exec igny8_backend python manage.py shell -c "
from igny8_core.business.billing.models import CreditTransaction
for txn in CreditTransaction.objects.order_by('-created_at')[:10]:
print(f'{txn.created_at}: {txn.transaction_type} | {txn.credits_amount} | balance={txn.balance_after}')
"
Frontend Verification
| Test | Page | Action | Expected | Pass? |
|---|---|---|---|---|
| Settings Load | Sites > Settings | Open page | Image settings dropdown populated | ☐ |
| Model Selection | Sites > Settings | Change image model | Shows Basic/Quality/Premium | ☐ |
| Model Persists | Sites > Settings | Save and reload | Selected model persists | ☐ |
| Credits Display | Any page | Header/sidebar | Shows correct credit balance | ☐ |
| Usage Analytics | Analytics | View usage | Shows correct breakdown | ☐ |
Rollback Procedures
If Phase 1 Fails
# Remove new migrations
docker exec igny8_backend python manage.py migrate billing <previous_migration>
docker exec igny8_backend python manage.py migrate system <previous_migration>
If Phase 2-6 Fails
# Revert code changes via git
git checkout -- backend/igny8_core/ai/
git checkout -- backend/igny8_core/business/billing/
# Restart containers
docker-compose restart backend
If Phase 7 Fails (Cleanup)
# This is why we wait 1 week before cleanup
# Restore from backup or revert git
Success Criteria
Migration is COMPLETE when:
- ✅ All integration tests pass
- ✅ All manual tests pass
- ✅ No errors in logs for 1 week
- ✅ Credit calculations match expected values
- ✅ All execution paths work (manual, automation, scheduled)
- ✅ Frontend loads models from API
- ✅ No legacy code remains:
- No
MODEL_RATES/IMAGE_MODEL_RATESin constants.py - No API keys in
GlobalIntegrationSettings - No hardcoded models in frontend
- No fallback to constants in
ModelRegistry
- No