Phase 0: Foundation & Credit System - Initial implementation
- Updated CREDIT_COSTS constants to Phase 0 format with new operations - Enhanced CreditService with get_credit_cost() method and operation_type support - Created AccountModuleSettings model for module enable/disable functionality - Added AccountModuleSettingsSerializer and ViewSet - Registered module settings API endpoint: /api/v1/system/settings/account-modules/ - Maintained backward compatibility with existing credit system
This commit is contained in:
@@ -1,22 +1,25 @@
|
|||||||
"""
|
"""
|
||||||
Credit Cost Constants
|
Credit Cost Constants - Phase 0: Credit-Only System
|
||||||
|
All features are unlimited. Only credits restrict usage.
|
||||||
"""
|
"""
|
||||||
CREDIT_COSTS = {
|
CREDIT_COSTS = {
|
||||||
'clustering': {
|
# Existing operations
|
||||||
'base': 1, # 1 credit per 30 keywords
|
'clustering': 10, # Per clustering request
|
||||||
'per_keyword': 1 / 30,
|
'idea_generation': 15, # Per cluster → ideas request
|
||||||
},
|
'content_generation': 1, # Per 100 words
|
||||||
'ideas': {
|
'image_prompt_extraction': 2, # Per content piece
|
||||||
'base': 1, # 1 credit per idea
|
'image_generation': 5, # Per image
|
||||||
},
|
|
||||||
'content': {
|
# Legacy operation names (for backward compatibility)
|
||||||
'base': 3, # 3 credits per full blog post
|
'ideas': 15, # Alias for idea_generation
|
||||||
},
|
'content': 1, # Alias for content_generation (per 100 words)
|
||||||
'images': {
|
'images': 5, # Alias for image_generation
|
||||||
'base': 1, # 1 credit per image
|
'reparse': 2, # Alias for image_prompt_extraction
|
||||||
},
|
|
||||||
'reparse': {
|
# NEW: Phase 2+ operations
|
||||||
'base': 1, # 1 credit per reparse
|
'linking': 8, # Per content piece (NEW)
|
||||||
},
|
'optimization': 1, # Per 200 words (NEW)
|
||||||
|
'site_structure_generation': 50, # Per site blueprint (NEW)
|
||||||
|
'site_page_generation': 20, # Per page (NEW)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,17 +13,49 @@ class CreditService:
|
|||||||
"""Service for managing credits"""
|
"""Service for managing credits"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_credits(account, required_credits):
|
def get_credit_cost(operation_type, amount=None):
|
||||||
|
"""
|
||||||
|
Get credit cost for operation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
operation_type: Type of operation (from CREDIT_COSTS)
|
||||||
|
amount: Optional amount (word count, etc.) for variable costs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Number of credits required
|
||||||
|
"""
|
||||||
|
base_cost = CREDIT_COSTS.get(operation_type, 0)
|
||||||
|
|
||||||
|
# Variable costs based on amount
|
||||||
|
if operation_type == 'content_generation' and amount:
|
||||||
|
# Per 100 words
|
||||||
|
return max(1, int(base_cost * (amount / 100)))
|
||||||
|
elif operation_type == 'optimization' and amount:
|
||||||
|
# Per 200 words
|
||||||
|
return max(1, int(base_cost * (amount / 200)))
|
||||||
|
|
||||||
|
return base_cost
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_credits(account, required_credits=None, operation_type=None, amount=None):
|
||||||
"""
|
"""
|
||||||
Check if account has enough credits.
|
Check if account has enough credits.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
account: Account instance
|
account: Account instance
|
||||||
required_credits: Number of credits required
|
required_credits: Number of credits required (legacy parameter)
|
||||||
|
operation_type: Type of operation (new parameter)
|
||||||
|
amount: Optional amount for variable costs (new parameter)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
InsufficientCreditsError: If account doesn't have enough credits
|
InsufficientCreditsError: If account doesn't have enough credits
|
||||||
"""
|
"""
|
||||||
|
# Support both old and new API
|
||||||
|
if operation_type:
|
||||||
|
required_credits = CreditService.get_credit_cost(operation_type, amount)
|
||||||
|
elif required_credits is None:
|
||||||
|
raise ValueError("Either required_credits or operation_type must be provided")
|
||||||
|
|
||||||
if account.credits < required_credits:
|
if account.credits < required_credits:
|
||||||
raise InsufficientCreditsError(
|
raise InsufficientCreditsError(
|
||||||
f"Insufficient credits. Required: {required_credits}, Available: {account.credits}"
|
f"Insufficient credits. Required: {required_credits}, Available: {account.credits}"
|
||||||
@@ -121,6 +153,9 @@ class CreditService:
|
|||||||
"""
|
"""
|
||||||
Calculate credits needed for an operation.
|
Calculate credits needed for an operation.
|
||||||
|
|
||||||
|
DEPRECATED: Use get_credit_cost() instead.
|
||||||
|
Kept for backward compatibility.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
operation_type: Type of operation
|
operation_type: Type of operation
|
||||||
**kwargs: Operation-specific parameters
|
**kwargs: Operation-specific parameters
|
||||||
@@ -131,31 +166,31 @@ class CreditService:
|
|||||||
Raises:
|
Raises:
|
||||||
CreditCalculationError: If calculation fails
|
CreditCalculationError: If calculation fails
|
||||||
"""
|
"""
|
||||||
if operation_type not in CREDIT_COSTS:
|
# Map old operation types to new ones
|
||||||
raise CreditCalculationError(f"Unknown operation type: {operation_type}")
|
operation_mapping = {
|
||||||
|
'ideas': 'idea_generation',
|
||||||
|
'content': 'content_generation',
|
||||||
|
'images': 'image_generation',
|
||||||
|
'reparse': 'image_prompt_extraction',
|
||||||
|
}
|
||||||
|
|
||||||
cost_config = CREDIT_COSTS[operation_type]
|
mapped_type = operation_mapping.get(operation_type, operation_type)
|
||||||
|
|
||||||
if operation_type == 'clustering':
|
# Handle variable costs
|
||||||
# 1 credit per 30 keywords
|
if mapped_type == 'content_generation':
|
||||||
|
word_count = kwargs.get('word_count') or kwargs.get('content_count', 1000) * 100
|
||||||
|
return CreditService.get_credit_cost(mapped_type, word_count)
|
||||||
|
elif mapped_type == 'clustering':
|
||||||
keyword_count = kwargs.get('keyword_count', 0)
|
keyword_count = kwargs.get('keyword_count', 0)
|
||||||
credits = max(1, int(keyword_count * cost_config['per_keyword']))
|
# Clustering is fixed cost per request
|
||||||
return credits
|
return CreditService.get_credit_cost(mapped_type)
|
||||||
elif operation_type == 'ideas':
|
elif mapped_type == 'idea_generation':
|
||||||
# 1 credit per idea
|
|
||||||
idea_count = kwargs.get('idea_count', 1)
|
idea_count = kwargs.get('idea_count', 1)
|
||||||
return cost_config['base'] * idea_count
|
# Fixed cost per request
|
||||||
elif operation_type == 'content':
|
return CreditService.get_credit_cost(mapped_type)
|
||||||
# 3 credits per content piece
|
elif mapped_type == 'image_generation':
|
||||||
content_count = kwargs.get('content_count', 1)
|
|
||||||
return cost_config['base'] * content_count
|
|
||||||
elif operation_type == 'images':
|
|
||||||
# 1 credit per image
|
|
||||||
image_count = kwargs.get('image_count', 1)
|
image_count = kwargs.get('image_count', 1)
|
||||||
return cost_config['base'] * image_count
|
return CreditService.get_credit_cost(mapped_type) * image_count
|
||||||
elif operation_type == 'reparse':
|
|
||||||
# 1 credit per reparse
|
|
||||||
return cost_config['base']
|
|
||||||
|
|
||||||
return cost_config['base']
|
return CreditService.get_credit_cost(mapped_type)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from igny8_core.auth.models import AccountBaseModel
|
|||||||
|
|
||||||
# Import settings models
|
# Import settings models
|
||||||
from .settings_models import (
|
from .settings_models import (
|
||||||
SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
|
SystemSettings, AccountSettings, UserSettings, ModuleSettings, AccountModuleSettings, AISettings
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,61 @@ class ModuleSettings(BaseSettings):
|
|||||||
return f"ModuleSetting: {self.module_name} - {self.key}"
|
return f"ModuleSetting: {self.module_name} - {self.key}"
|
||||||
|
|
||||||
|
|
||||||
|
class AccountModuleSettings(AccountBaseModel):
|
||||||
|
"""
|
||||||
|
Account-level module enable/disable settings.
|
||||||
|
Phase 0: Credit System - Module Settings
|
||||||
|
"""
|
||||||
|
# Module enable/disable flags
|
||||||
|
planner_enabled = models.BooleanField(default=True, help_text="Enable Planner module")
|
||||||
|
writer_enabled = models.BooleanField(default=True, help_text="Enable Writer module")
|
||||||
|
thinker_enabled = models.BooleanField(default=True, help_text="Enable Thinker module")
|
||||||
|
automation_enabled = models.BooleanField(default=True, help_text="Enable Automation module")
|
||||||
|
site_builder_enabled = models.BooleanField(default=True, help_text="Enable Site Builder module")
|
||||||
|
linker_enabled = models.BooleanField(default=True, help_text="Enable Linker module")
|
||||||
|
optimizer_enabled = models.BooleanField(default=True, help_text="Enable Optimizer module")
|
||||||
|
publisher_enabled = models.BooleanField(default=True, help_text="Enable Publisher module")
|
||||||
|
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'igny8_account_module_settings'
|
||||||
|
verbose_name = 'Account Module Settings'
|
||||||
|
verbose_name_plural = 'Account Module Settings'
|
||||||
|
# One settings record per account
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['account'], name='unique_account_module_settings')
|
||||||
|
]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['account']),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
account = getattr(self, 'account', None)
|
||||||
|
return f"ModuleSettings: {account.name if account else 'No Account'}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_create_for_account(cls, account):
|
||||||
|
"""Get or create module settings for an account"""
|
||||||
|
settings, created = cls.objects.get_or_create(account=account)
|
||||||
|
return settings
|
||||||
|
|
||||||
|
def is_module_enabled(self, module_name):
|
||||||
|
"""Check if a module is enabled"""
|
||||||
|
module_map = {
|
||||||
|
'planner': self.planner_enabled,
|
||||||
|
'writer': self.writer_enabled,
|
||||||
|
'thinker': self.thinker_enabled,
|
||||||
|
'automation': self.automation_enabled,
|
||||||
|
'site_builder': self.site_builder_enabled,
|
||||||
|
'linker': self.linker_enabled,
|
||||||
|
'optimizer': self.optimizer_enabled,
|
||||||
|
'publisher': self.publisher_enabled,
|
||||||
|
}
|
||||||
|
return module_map.get(module_name, True) # Default to enabled if module not found
|
||||||
|
|
||||||
|
|
||||||
# AISettings extends IntegrationSettings (which already exists)
|
# AISettings extends IntegrationSettings (which already exists)
|
||||||
# We'll create it as a separate model that can reference IntegrationSettings
|
# We'll create it as a separate model that can reference IntegrationSettings
|
||||||
class AISettings(AccountBaseModel):
|
class AISettings(AccountBaseModel):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Serializers for Settings Models
|
Serializers for Settings Models
|
||||||
"""
|
"""
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
|
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AccountModuleSettings, AISettings
|
||||||
from .validators import validate_settings_schema
|
from .validators import validate_settings_schema
|
||||||
|
|
||||||
|
|
||||||
@@ -58,6 +58,18 @@ class ModuleSettingsSerializer(serializers.ModelSerializer):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class AccountModuleSettingsSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for Account Module Settings (Phase 0)"""
|
||||||
|
class Meta:
|
||||||
|
model = AccountModuleSettings
|
||||||
|
fields = [
|
||||||
|
'id', 'planner_enabled', 'writer_enabled', 'thinker_enabled',
|
||||||
|
'automation_enabled', 'site_builder_enabled', 'linker_enabled',
|
||||||
|
'optimizer_enabled', 'publisher_enabled', 'created_at', 'updated_at'
|
||||||
|
]
|
||||||
|
read_only_fields = ['created_at', 'updated_at', 'account']
|
||||||
|
|
||||||
|
|
||||||
class AISettingsSerializer(serializers.ModelSerializer):
|
class AISettingsSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AISettings
|
model = AISettings
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAu
|
|||||||
from igny8_core.api.pagination import CustomPageNumberPagination
|
from igny8_core.api.pagination import CustomPageNumberPagination
|
||||||
from igny8_core.api.throttles import DebugScopedRateThrottle
|
from igny8_core.api.throttles import DebugScopedRateThrottle
|
||||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
|
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
|
||||||
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
|
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AccountModuleSettings, AISettings
|
||||||
from .settings_serializers import (
|
from .settings_serializers import (
|
||||||
SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer,
|
SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer,
|
||||||
ModuleSettingsSerializer, AISettingsSerializer
|
ModuleSettingsSerializer, AccountModuleSettingsSerializer, AISettingsSerializer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -276,6 +276,75 @@ class ModuleSettingsViewSet(AccountModelViewSet):
|
|||||||
serializer.save(account=account)
|
serializer.save(account=account)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_view(
|
||||||
|
list=extend_schema(tags=['System']),
|
||||||
|
retrieve=extend_schema(tags=['System']),
|
||||||
|
update=extend_schema(tags=['System']),
|
||||||
|
partial_update=extend_schema(tags=['System']),
|
||||||
|
)
|
||||||
|
class AccountModuleSettingsViewSet(AccountModelViewSet):
|
||||||
|
"""
|
||||||
|
ViewSet for managing account module enable/disable settings.
|
||||||
|
Phase 0: Credit System - Module Settings
|
||||||
|
One settings record per account (get_or_create pattern)
|
||||||
|
"""
|
||||||
|
queryset = AccountModuleSettings.objects.all()
|
||||||
|
serializer_class = AccountModuleSettingsSerializer
|
||||||
|
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||||
|
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
||||||
|
pagination_class = CustomPageNumberPagination
|
||||||
|
throttle_scope = 'system'
|
||||||
|
throttle_classes = [DebugScopedRateThrottle]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Get module settings for current account"""
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
return queryset.filter(account=self.request.account)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
"""Get or create module settings for account"""
|
||||||
|
account = request.account
|
||||||
|
settings = AccountModuleSettings.get_or_create_for_account(account)
|
||||||
|
serializer = self.get_serializer(settings)
|
||||||
|
return success_response(data=serializer.data, request=request)
|
||||||
|
|
||||||
|
def retrieve(self, request, pk=None):
|
||||||
|
"""Get module settings for account"""
|
||||||
|
account = request.account
|
||||||
|
try:
|
||||||
|
settings = AccountModuleSettings.objects.get(account=account, pk=pk)
|
||||||
|
except AccountModuleSettings.DoesNotExist:
|
||||||
|
# Create if doesn't exist
|
||||||
|
settings = AccountModuleSettings.get_or_create_for_account(account)
|
||||||
|
serializer = self.get_serializer(settings)
|
||||||
|
return success_response(data=serializer.data, request=request)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
"""Set account automatically"""
|
||||||
|
account = getattr(self.request, 'account', None)
|
||||||
|
if not account:
|
||||||
|
user = getattr(self.request, 'user', None)
|
||||||
|
if user:
|
||||||
|
account = getattr(user, 'account', None)
|
||||||
|
|
||||||
|
if not account:
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
raise ValidationError("Account is required")
|
||||||
|
|
||||||
|
serializer.save(account=account)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], url_path='check/(?P<module_name>[^/.]+)', url_name='check_module')
|
||||||
|
def check_module(self, request, module_name=None):
|
||||||
|
"""Check if a specific module is enabled"""
|
||||||
|
account = request.account
|
||||||
|
settings = AccountModuleSettings.get_or_create_for_account(account)
|
||||||
|
is_enabled = settings.is_module_enabled(module_name)
|
||||||
|
return success_response(
|
||||||
|
data={'module_name': module_name, 'enabled': is_enabled},
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_view(
|
@extend_schema_view(
|
||||||
list=extend_schema(tags=['System']),
|
list=extend_schema(tags=['System']),
|
||||||
create=extend_schema(tags=['System']),
|
create=extend_schema(tags=['System']),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from .views import AIPromptViewSet, AuthorProfileViewSet, StrategyViewSet, syste
|
|||||||
from .integration_views import IntegrationSettingsViewSet
|
from .integration_views import IntegrationSettingsViewSet
|
||||||
from .settings_views import (
|
from .settings_views import (
|
||||||
SystemSettingsViewSet, AccountSettingsViewSet, UserSettingsViewSet,
|
SystemSettingsViewSet, AccountSettingsViewSet, UserSettingsViewSet,
|
||||||
ModuleSettingsViewSet, AISettingsViewSet
|
ModuleSettingsViewSet, AccountModuleSettingsViewSet, AISettingsViewSet
|
||||||
)
|
)
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'prompts', AIPromptViewSet, basename='prompts')
|
router.register(r'prompts', AIPromptViewSet, basename='prompts')
|
||||||
@@ -17,6 +17,7 @@ router.register(r'settings/system', SystemSettingsViewSet, basename='system-sett
|
|||||||
router.register(r'settings/account', AccountSettingsViewSet, basename='account-settings')
|
router.register(r'settings/account', AccountSettingsViewSet, basename='account-settings')
|
||||||
router.register(r'settings/user', UserSettingsViewSet, basename='user-settings')
|
router.register(r'settings/user', UserSettingsViewSet, basename='user-settings')
|
||||||
router.register(r'settings/modules', ModuleSettingsViewSet, basename='module-settings')
|
router.register(r'settings/modules', ModuleSettingsViewSet, basename='module-settings')
|
||||||
|
router.register(r'settings/account-modules', AccountModuleSettingsViewSet, basename='account-module-settings')
|
||||||
router.register(r'settings/ai', AISettingsViewSet, basename='ai-settings')
|
router.register(r'settings/ai', AISettingsViewSet, basename='ai-settings')
|
||||||
|
|
||||||
# Custom URL patterns for integration settings - matching reference plugin structure
|
# Custom URL patterns for integration settings - matching reference plugin structure
|
||||||
|
|||||||
Reference in New Issue
Block a user