""" System module models - for global settings and prompts """ from django.db import models from django.conf import settings from igny8_core.auth.models import AccountBaseModel # Import settings models from .settings_models import ( SystemSettings, AccountSettings, UserSettings, ModuleSettings, ModuleEnableSettings, AISettings ) class IntegrationProvider(models.Model): """ Centralized storage for ALL external service API keys. Per final-model-schemas.md: | 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 | | """ PROVIDER_TYPE_CHOICES = [ ('ai', 'AI Provider'), ('payment', 'Payment Gateway'), ('email', 'Email Service'), ('storage', 'Storage Service'), ] # Primary Key provider_id = models.CharField( max_length=50, unique=True, primary_key=True, help_text="Unique identifier (e.g., 'openai', 'stripe', 'resend')" ) # Display name display_name = models.CharField( max_length=100, help_text="Human-readable name" ) # Provider type provider_type = models.CharField( max_length=20, choices=PROVIDER_TYPE_CHOICES, default='ai', db_index=True, help_text="ai / payment / email / storage" ) # Authentication api_key = models.CharField( max_length=500, blank=True, help_text="Primary API key" ) api_secret = models.CharField( max_length=500, blank=True, help_text="Secondary secret (Stripe, PayPal)" ) webhook_secret = models.CharField( max_length=500, blank=True, help_text="Webhook signing secret" ) # Endpoints api_endpoint = models.URLField( blank=True, help_text="Custom endpoint (optional)" ) # Configuration config = models.JSONField( default=dict, blank=True, help_text="Provider-specific config" ) # Status is_active = models.BooleanField( default=True, db_index=True, help_text="Enable/disable provider" ) is_sandbox = models.BooleanField( default=False, help_text="Test mode flag" ) # Audit updated_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='integration_provider_updates', help_text="Audit trail" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'igny8_integration_providers' verbose_name = 'Integration Provider' verbose_name_plural = 'Integration Providers' ordering = ['provider_type', 'display_name'] def __str__(self): status = "Active" if self.is_active else "Inactive" mode = "(Sandbox)" if self.is_sandbox else "" return f"{self.display_name} - {status} {mode}".strip() @classmethod def get_provider(cls, provider_id: str): """Get provider by ID, returns None if not found or inactive""" try: return cls.objects.get(provider_id=provider_id, is_active=True) except cls.DoesNotExist: return None @classmethod def get_api_key(cls, provider_id: str) -> str: """Get API key for a provider""" provider = cls.get_provider(provider_id) return provider.api_key if provider else "" @classmethod def get_providers_by_type(cls, provider_type: str): """Get all active providers of a type""" return cls.objects.filter(provider_type=provider_type, is_active=True) class AIPrompt(AccountBaseModel): """ Account-specific AI Prompt templates. Stores global default in default_prompt, current value in prompt_value. When user saves an override, prompt_value changes but default_prompt stays. Reset copies default_prompt back to prompt_value. """ PROMPT_TYPE_CHOICES = [ ('clustering', 'Clustering'), ('ideas', 'Ideas Generation'), ('content_generation', 'Content Generation'), ('image_prompt_extraction', 'Image Prompt Extraction'), ('image_prompt_template', 'Image Prompt Template'), ('negative_prompt', 'Negative Prompt'), ('site_structure_generation', 'Site Structure Generation'), # Phase 7: Site Builder prompts # Phase 8: Universal Content Types ('product_generation', 'Product Content Generation'), ('service_generation', 'Service Page Generation'), ('taxonomy_generation', 'Taxonomy Generation'), ] prompt_type = models.CharField(max_length=50, choices=PROMPT_TYPE_CHOICES, db_index=True) prompt_value = models.TextField(help_text="Current prompt text (customized or default)") default_prompt = models.TextField( blank=True, help_text="Global default prompt - used for reset to default" ) is_customized = models.BooleanField( default=False, help_text="True if account customized the prompt, False if using global default" ) is_active = models.BooleanField(default=True) updated_at = models.DateTimeField(auto_now=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'igny8_ai_prompts' ordering = ['prompt_type'] unique_together = [['account', 'prompt_type']] # Each account can have one prompt per type indexes = [ models.Index(fields=['prompt_type']), models.Index(fields=['account', 'prompt_type']), models.Index(fields=['is_customized']), ] @classmethod def get_effective_prompt(cls, account, prompt_type): """ Get the effective prompt for an account. Returns account-specific prompt if exists and customized. Otherwise returns global default. """ from .global_settings_models import GlobalAIPrompt # Try to get account-specific prompt try: account_prompt = cls.objects.get(account=account, prompt_type=prompt_type, is_active=True) # If customized, use account's version if account_prompt.is_customized: return account_prompt.prompt_value # If not customized, use default_prompt from account record or global return account_prompt.default_prompt or account_prompt.prompt_value except cls.DoesNotExist: pass # Fallback to global prompt try: global_prompt = GlobalAIPrompt.objects.get(prompt_type=prompt_type, is_active=True) return global_prompt.prompt_value except GlobalAIPrompt.DoesNotExist: return None def reset_to_default(self): """Reset prompt to global default from GlobalAIPrompt""" from .global_settings_models import GlobalAIPrompt try: global_prompt = GlobalAIPrompt.objects.get( prompt_type=self.prompt_type, is_active=True ) self.prompt_value = global_prompt.prompt_value self.default_prompt = global_prompt.prompt_value self.is_customized = False self.save() except GlobalAIPrompt.DoesNotExist: raise ValueError( f"Cannot reset: Global prompt '{self.prompt_type}' not found. " f"Please configure it in Django admin at: /admin/system/globalaiprompt/" ) def __str__(self): status = "Custom" if self.is_customized else "Default" return f"{self.get_prompt_type_display()} ({status})" class IntegrationSettings(AccountBaseModel): """ Per-account integration settings overrides. IMPORTANT: This model stores ONLY model selection and parameter overrides. API keys are NEVER stored here - they come from GlobalIntegrationSettings. Free plan: Cannot create overrides, must use global defaults Starter/Growth/Scale plans: Can override model, temperature, tokens, image settings NULL values in config mean "use global default" """ INTEGRATION_TYPE_CHOICES = [ ('openai', 'OpenAI'), ('dalle', 'DALL-E'), ('anthropic', 'Anthropic'), ('runware', 'Runware'), ] integration_type = models.CharField(max_length=50, choices=INTEGRATION_TYPE_CHOICES, db_index=True) config = models.JSONField( default=dict, help_text=( "Model and parameter overrides only. Fields: model, temperature, max_tokens, " "image_size, image_quality, etc. NULL = use global default. " "NEVER store API keys here." ) ) is_active = models.BooleanField(default=True) updated_at = models.DateTimeField(auto_now=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'igny8_integration_settings' unique_together = [['account', 'integration_type']] ordering = ['integration_type'] indexes = [ models.Index(fields=['integration_type']), models.Index(fields=['account', 'integration_type']), ] def __str__(self): account = getattr(self, 'account', None) return f"{self.get_integration_type_display()} - {account.name if account else 'No Account'}" class AuthorProfile(AccountBaseModel): """ Writing style profiles - tone, language, structure templates. Can be cloned from global templates or created from scratch. Examples: "SaaS B2B Informative", "E-commerce Product Descriptions", etc. """ name = models.CharField(max_length=255, help_text="Profile name (e.g., 'SaaS B2B Informative')") description = models.TextField(blank=True, help_text="Description of the writing style") tone = models.CharField( max_length=100, help_text="Writing tone (e.g., 'Professional', 'Casual', 'Technical', 'Conversational')" ) language = models.CharField(max_length=50, default='en', help_text="Language code (e.g., 'en', 'es', 'fr')") structure_template = models.JSONField( default=dict, help_text="Structure template defining content sections and their order" ) is_custom = models.BooleanField( default=False, help_text="True if created by account, False if cloned from global template" ) cloned_from = models.ForeignKey( 'system.GlobalAuthorProfile', on_delete=models.SET_NULL, null=True, blank=True, related_name='cloned_instances', help_text="Reference to the global template this was cloned from" ) is_active = models.BooleanField(default=True, db_index=True) updated_at = models.DateTimeField(auto_now=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'igny8_author_profiles' ordering = ['name'] verbose_name = 'Author Profile' verbose_name_plural = 'Author Profiles' indexes = [ models.Index(fields=['account', 'is_active']), models.Index(fields=['name']), models.Index(fields=['is_custom']), ] def __str__(self): account = getattr(self, 'account', None) status = "Custom" if self.is_custom else "Template" return f"{self.name} ({status}) - {account.name if account else 'No Account'}" class Strategy(AccountBaseModel): """ Defined content strategies per sector, integrating prompt types, section logic, etc. Can be cloned from global templates or created from scratch. Links together prompts, author profiles, and sector-specific content strategies. """ name = models.CharField(max_length=255, help_text="Strategy name") description = models.TextField(blank=True, help_text="Description of the content strategy") sector = models.ForeignKey( 'igny8_core_auth.Sector', on_delete=models.SET_NULL, null=True, blank=True, related_name='strategies', help_text="Optional: Link strategy to a specific sector" ) prompt_types = models.JSONField( default=list, help_text="List of prompt types to use (e.g., ['clustering', 'ideas', 'content_generation'])" ) section_logic = models.JSONField( default=dict, help_text="Section logic configuration defining content structure and flow" ) is_custom = models.BooleanField( default=False, help_text="True if created by account, False if cloned from global template" ) cloned_from = models.ForeignKey( 'system.GlobalStrategy', on_delete=models.SET_NULL, null=True, blank=True, related_name='cloned_instances', help_text="Reference to the global template this was cloned from" ) is_active = models.BooleanField(default=True, db_index=True) updated_at = models.DateTimeField(auto_now=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'igny8_strategies' ordering = ['name'] verbose_name = 'Strategy' verbose_name_plural = 'Strategies' indexes = [ models.Index(fields=['account', 'is_active']), models.Index(fields=['account', 'sector']), models.Index(fields=['name']), models.Index(fields=['is_custom']), ] def __str__(self): sector_name = self.sector.name if self.sector else 'Global' status = "Custom" if self.is_custom else "Template" return f"{self.name} ({status}) - {sector_name}"