# Credits System - Complete Audit and Improvement Plan **Date:** December 4, 2025 **Status:** Audit Complete - Awaiting Implementation **Priority:** HIGH --- ## ๐Ÿ“‹ EXECUTIVE SUMMARY This document provides a comprehensive audit of the IGNY8 credits system, identifies gaps and potential issues, and proposes improvements including backend admin configuration for credit costs per function. **Current State:** โœ… Working **Areas for Improvement:** Configuration Management, Billing Integration, Admin UI, Reporting --- ## ๐Ÿ” SYSTEM ARCHITECTURE AUDIT ### Current Credit System Components ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ CREDITS SYSTEM โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ 1. Account Model (credits field) โ”‚ โ”‚ 2. CreditTransaction Model (history) โ”‚ โ”‚ 3. CreditUsageLog Model (detailed usage) โ”‚ โ”‚ 4. CreditService (business logic) โ”‚ โ”‚ 5. CREDIT_COSTS (hardcoded constants) โ”‚ โ”‚ 6. Credit API Endpoints โ”‚ โ”‚ 7. Frontend Dashboard โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` --- ## ๐Ÿ—„๏ธ DATABASE MODELS AUDIT ### 1. Account Model (`auth.Account`) **Location:** `backend/igny8_core/auth/models.py` **Credits Field:** ```python credits = models.IntegerField(default=0, help_text="Current credit balance") ``` **Status:** โœ… Working **Findings:** - Simple integer field for balance - No soft delete or archive mechanism - No credit expiration tracking - No overdraft protection **Recommendations:** - โœ… Keep simple design (no changes needed) - Add `credits_expires_at` field for subscription credits - Add `bonus_credits` field separate from subscription credits --- ### 2. CreditTransaction Model **Location:** `backend/igny8_core/business/billing/models.py` **Current Structure:** ```python class CreditTransaction(AccountBaseModel): TRANSACTION_TYPE_CHOICES = [ ('purchase', 'Purchase'), ('subscription', 'Subscription Renewal'), ('refund', 'Refund'), ('deduction', 'Usage Deduction'), ('adjustment', 'Manual Adjustment'), ] transaction_type = models.CharField(max_length=20, choices=TRANSACTION_TYPE_CHOICES) amount = models.IntegerField() # + for add, - for deduct balance_after = models.IntegerField() description = models.CharField(max_length=255) metadata = models.JSONField(default=dict) created_at = models.DateTimeField(auto_now_add=True) ``` **Status:** โœ… Working **Findings:** - Comprehensive transaction types - Good metadata for context - Proper indexing on account/type/date - Missing: invoice_id FK, payment_method **Recommendations:** - Add `invoice_id` FK (for billing integration) - Add `payment_method` field ('stripe', 'manual', 'free') - Add `status` field ('pending', 'completed', 'failed', 'refunded') - Add `external_transaction_id` for Stripe payment IDs --- ### 3. CreditUsageLog Model **Location:** `backend/igny8_core/business/billing/models.py` **Current Structure:** ```python class CreditUsageLog(AccountBaseModel): OPERATION_TYPE_CHOICES = [ ('clustering', 'Clustering'), ('idea_generation', 'Idea Generation'), ('content_generation', 'Content Generation'), ('image_prompt_extraction', 'Image Prompt Extraction'), ('image_generation', 'Image Generation'), ('linking', 'Content Linking'), ('optimization', 'Content Optimization'), ('site_structure_generation', 'Site Structure Generation'), ('site_page_generation', 'Site Page Generation'), ] operation_type = models.CharField(max_length=50, choices=OPERATION_TYPE_CHOICES) credits_used = models.IntegerField() cost_usd = models.DecimalField(max_digits=10, decimal_places=2, null=True) model_used = models.CharField(max_length=100, blank=True) tokens_input = models.IntegerField(null=True) tokens_output = models.IntegerField(null=True) related_object_type = models.CharField(max_length=50, blank=True) related_object_id = models.IntegerField(null=True) metadata = models.JSONField(default=dict) created_at = models.DateTimeField(auto_now_add=True) ``` **Status:** โœ… Working **Findings:** - Excellent detail tracking (model, tokens, cost) - Good related object tracking - Proper operation type choices - Missing: site/sector isolation, duration tracking **Recommendations:** - Add `site` FK for multi-tenant filtering - Add `sector` FK for isolation - Add `duration_seconds` field (API call time) - Add `success` boolean field (track failures) - Add `error_message` field for failed operations --- ## ๐Ÿ’ณ CREDIT COST CONFIGURATION AUDIT ### Current Implementation: Hardcoded Constants **Location:** `backend/igny8_core/business/billing/constants.py` ```python CREDIT_COSTS = { 'clustering': 10, # Per clustering request 'idea_generation': 15, # Per cluster โ†’ ideas request 'content_generation': 1, # Per 100 words 'image_prompt_extraction': 2, # Per content piece 'image_generation': 5, # Per image 'linking': 8, # Per content piece 'optimization': 1, # Per 200 words 'site_structure_generation': 50, # Per site blueprint 'site_page_generation': 20, # Per page } ``` **Status:** โš ๏ธ Working but NOT configurable **Problems:** 1. โŒ **Hardcoded values** - Requires code deployment to change 2. โŒ **No admin UI** - Cannot adjust costs without developer 3. โŒ **No versioning** - Cannot track cost changes over time 4. โŒ **No A/B testing** - Cannot test different pricing 5. โŒ **No per-account pricing** - All accounts same cost 6. โŒ **No promotional pricing** - Cannot offer discounts --- ## ๐Ÿ’ฐ BILLING & INVOICING GAPS ### Current State **โœ… Working:** - Credit deduction on AI operations - Credit transaction logging - Credit balance API - Credit usage API - Monthly credit replenishment (Celery task) **โŒ Missing (NOT Implemented):** 1. **Invoice Generation** - No Invoice model or PDF generation 2. **Payment Processing** - No Stripe/PayPal integration 3. **Subscription Management** - No recurring billing 4. **Purchase Credits** - No one-time credit purchase flow 5. **Refund Processing** - No refund workflow 6. **Payment History** - No payment records 7. **Tax Calculation** - No tax/VAT handling 8. **Billing Address** - No billing info storage --- ## ๐ŸŽฏ PROPOSED SOLUTION: Backend Admin Configuration ### New Model: CreditCostConfig **Purpose:** Make credit costs configurable from Django Admin **Location:** `backend/igny8_core/modules/billing/models.py` ```python class CreditCostConfig(models.Model): """ Configurable credit costs per AI function Admin-editable alternative to hardcoded constants """ # Operation identification operation_type = models.CharField( max_length=50, unique=True, choices=CreditUsageLog.OPERATION_TYPE_CHOICES, help_text="AI operation type" ) # Cost configuration credits_cost = models.IntegerField( validators=[MinValueValidator(0)], help_text="Credits required for this operation" ) # Unit of measurement unit = models.CharField( max_length=50, default='per_request', choices=[ ('per_request', 'Per Request'), ('per_100_words', 'Per 100 Words'), ('per_200_words', 'Per 200 Words'), ('per_item', 'Per Item'), ('per_image', 'Per Image'), ], help_text="What the cost applies to" ) # Metadata display_name = models.CharField(max_length=100, help_text="Human-readable name") description = models.TextField(blank=True, help_text="What this operation does") # Status is_active = models.BooleanField(default=True, help_text="Enable/disable this operation") # Audit fields created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) updated_by = models.ForeignKey( 'auth.User', null=True, blank=True, on_delete=models.SET_NULL, help_text="Admin who last updated" ) # Change tracking previous_cost = models.IntegerField( null=True, blank=True, help_text="Cost before last update (for audit trail)" ) class Meta: db_table = 'igny8_credit_cost_config' verbose_name = 'Credit Cost Configuration' verbose_name_plural = 'Credit Cost Configurations' ordering = ['operation_type'] def __str__(self): return f"{self.display_name} - {self.credits_cost} credits {self.unit}" def save(self, *args, **kwargs): # Track cost changes if self.pk: old = CreditCostConfig.objects.get(pk=self.pk) if old.credits_cost != self.credits_cost: self.previous_cost = old.credits_cost super().save(*args, **kwargs) ``` --- ### Django Admin Configuration **Location:** `backend/igny8_core/modules/billing/admin.py` ```python from django.contrib import admin from django.utils.html import format_html from .models import CreditCostConfig @admin.register(CreditCostConfig) class CreditCostConfigAdmin(admin.ModelAdmin): list_display = [ 'operation_type', 'display_name', 'credits_cost_display', 'unit', 'is_active', 'cost_change_indicator', 'updated_at', 'updated_by' ] list_filter = ['is_active', 'unit', 'updated_at'] search_fields = ['operation_type', 'display_name', 'description'] fieldsets = ( ('Operation', { 'fields': ('operation_type', 'display_name', 'description') }), ('Cost Configuration', { 'fields': ('credits_cost', 'unit', 'is_active') }), ('Audit Trail', { 'fields': ('previous_cost', 'updated_by', 'created_at', 'updated_at'), 'classes': ('collapse',) }), ) readonly_fields = ['created_at', 'updated_at', 'previous_cost'] def credits_cost_display(self, obj): """Show cost with color coding""" if obj.credits_cost >= 20: color = 'red' elif obj.credits_cost >= 10: color = 'orange' else: color = 'green' return format_html( '{} credits', color, obj.credits_cost ) credits_cost_display.short_description = 'Cost' def cost_change_indicator(self, obj): """Show if cost changed recently""" if obj.previous_cost is not None: if obj.credits_cost > obj.previous_cost: icon = '๐Ÿ“ˆ' # Increased color = 'red' elif obj.credits_cost < obj.previous_cost: icon = '๐Ÿ“‰' # Decreased color = 'green' else: icon = 'โžก๏ธ' # Same color = 'gray' return format_html( '{} ({} โ†’ {})', icon, color, obj.previous_cost, obj.credits_cost ) return 'โ€”' cost_change_indicator.short_description = 'Recent Change' def save_model(self, request, obj, form, change): """Track who made the change""" obj.updated_by = request.user super().save_model(request, obj, form, change) ``` --- ### Updated CreditService to Use Database **Location:** `backend/igny8_core/business/billing/services/credit_service.py` ```python class CreditService: """Service for managing credits""" @staticmethod def get_credit_cost(operation_type, amount=None): """ Get credit cost for an operation. Now checks database config first, falls back to constants. Args: operation_type: Type of operation amount: Optional amount (word count, image count, etc.) Returns: int: Credit cost """ # Try to get from database config first try: from igny8_core.modules.billing.models import CreditCostConfig config = CreditCostConfig.objects.filter( operation_type=operation_type, is_active=True ).first() if config: base_cost = config.credits_cost # Apply unit-based calculation if config.unit == 'per_100_words' and amount: return max(1, (amount // 100)) * base_cost elif config.unit == 'per_200_words' and amount: return max(1, (amount // 200)) * base_cost elif config.unit in ['per_item', 'per_image'] and amount: return amount * base_cost else: return base_cost except Exception as e: logger.warning(f"Failed to get cost from database, using constants: {e}") # Fallback to hardcoded constants from igny8_core.business.billing.constants import CREDIT_COSTS base_cost = CREDIT_COSTS.get(operation_type, 1) # Apply multipliers for word-based operations if operation_type == 'content_generation' and amount: return max(1, (amount // 100)) # 1 credit per 100 words elif operation_type == 'optimization' and amount: return max(1, (amount // 200)) # 1 credit per 200 words elif operation_type in ['image_generation'] and amount: return amount * base_cost else: return base_cost ``` --- ## ๐Ÿ“Š ADMIN UI - CREDIT COST CONFIGURATION ### Management Command: Initialize Credit Costs **Location:** `backend/igny8_core/modules/billing/management/commands/init_credit_costs.py` ```python from django.core.management.base import BaseCommand from igny8_core.modules.billing.models import CreditCostConfig from igny8_core.business.billing.constants import CREDIT_COSTS class Command(BaseCommand): help = 'Initialize credit cost configurations from constants' def handle(self, *args, **options): """Migrate hardcoded costs to database""" operation_metadata = { 'clustering': { 'display_name': 'Auto Clustering', 'description': 'Group keywords into semantic clusters using AI', 'unit': 'per_request' }, 'idea_generation': { 'display_name': 'Idea Generation', 'description': 'Generate content ideas from keyword clusters', 'unit': 'per_request' }, 'content_generation': { 'display_name': 'Content Generation', 'description': 'Generate article content using AI', 'unit': 'per_100_words' }, 'image_prompt_extraction': { 'display_name': 'Image Prompt Extraction', 'description': 'Extract image prompts from content', 'unit': 'per_request' }, 'image_generation': { 'display_name': 'Image Generation', 'description': 'Generate images using AI (DALL-E, Runware)', 'unit': 'per_image' }, 'linking': { 'display_name': 'Content Linking', 'description': 'Generate internal links between content', 'unit': 'per_request' }, 'optimization': { 'display_name': 'Content Optimization', 'description': 'Optimize content for SEO', 'unit': 'per_200_words' }, 'site_structure_generation': { 'display_name': 'Site Structure Generation', 'description': 'Generate complete site blueprint', 'unit': 'per_request' }, 'site_page_generation': { 'display_name': 'Site Page Generation', 'description': 'Generate site pages from blueprint', 'unit': 'per_item' }, } created_count = 0 updated_count = 0 for operation_type, cost in CREDIT_COSTS.items(): # Skip legacy aliases if operation_type in ['ideas', 'content', 'images', 'reparse']: continue metadata = operation_metadata.get(operation_type, {}) config, created = CreditCostConfig.objects.get_or_create( operation_type=operation_type, defaults={ 'credits_cost': cost, 'display_name': metadata.get('display_name', operation_type.replace('_', ' ').title()), 'description': metadata.get('description', ''), 'unit': metadata.get('unit', 'per_request'), 'is_active': True } ) if created: created_count += 1 self.stdout.write( self.style.SUCCESS(f'โœ… Created: {config.display_name} - {cost} credits') ) else: updated_count += 1 self.stdout.write( self.style.WARNING(f'โš ๏ธ Already exists: {config.display_name}') ) self.stdout.write( self.style.SUCCESS(f'\nโœ… Complete: {created_count} created, {updated_count} already existed') ) ``` **Run command:** ```bash python manage.py init_credit_costs ``` --- ## ๐Ÿ” POTENTIAL ISSUES & FIXES ### Issue 1: Race Conditions in Credit Deduction **Problem:** ```python # Current code (simplified) if account.credits < required: raise InsufficientCreditsError() account.credits -= required # Race condition here! account.save() ``` **Risk:** Two requests could both check balance and deduct simultaneously **Fix:** Use database-level atomic update ```python from django.db.models import F @transaction.atomic def deduct_credits(account, amount): # Atomic update with check updated = Account.objects.filter( id=account.id, credits__gte=amount # Check in database ).update( credits=F('credits') - amount ) if updated == 0: raise InsufficientCreditsError() account.refresh_from_db() return account.credits ``` --- ### Issue 2: Negative Credit Balance **Problem:** No hard constraint preventing negative credits **Fix 1:** Database constraint ```python # Migration operations = [ migrations.AddConstraint( model_name='account', constraint=models.CheckConstraint( check=models.Q(credits__gte=0), name='credits_non_negative' ), ), ] ``` **Fix 2:** Service layer validation (current approach - OK) --- ### Issue 3: Missing Credit Expiration **Problem:** Subscription credits never expire **Fix:** Add expiration tracking ```python # Account model credits_expires_at = models.DateTimeField(null=True, blank=True) # Celery task (daily) @shared_task def expire_credits(): """Expire old credits""" expired = Account.objects.filter( credits_expires_at__lt=timezone.now(), credits__gt=0 ) for account in expired: # Transfer to expired_credits field or log account.credits = 0 account.save() ``` --- ### Issue 4: No Usage Analytics **Problem:** Hard to analyze which functions cost most credits **Fix:** Add aggregation views ```python # Backend @action(detail=False, methods=['get']) def cost_breakdown(self, request): """Get credit cost breakdown by operation""" from django.db.models import Sum breakdown = CreditUsageLog.objects.filter( account=request.account ).values('operation_type').annotate( total_credits=Sum('credits_used'), count=Count('id') ).order_by('-total_credits') return Response(breakdown) ``` --- ### Issue 5: No Budget Alerts **Problem:** Users can run out of credits unexpectedly **Fix:** Add threshold alerts ```python # After each deduction def check_low_balance(account): if account.credits < 100: # Configurable threshold send_low_balance_email(account) if account.credits < 50: send_critical_balance_email(account) ``` --- ## ๐Ÿ“ˆ FUTURE ENHANCEMENTS ### Phase 1: Billing Integration (Priority: HIGH) **Models to Add:** 1. `Invoice` - Store invoice records 2. `Payment` - Track payments 3. `Subscription` - Recurring billing 4. `CreditPackage` - One-time credit purchases **Integrations:** - Stripe for payments - PDF generation for invoices - Email notifications --- ### Phase 2: Advanced Pricing (Priority: MEDIUM) **Features:** 1. **Volume Discounts** - 1000+ credits/month: 10% discount - 5000+ credits/month: 20% discount 2. **Per-Account Pricing** - Enterprise accounts: Custom pricing - Trial accounts: Limited operations 3. **Promotional Codes** - Discount codes - Free credit grants 4. **Credit Bundles** - Starter: 500 credits - Pro: 2000 credits - Enterprise: 10000 credits --- ### Phase 3: Usage Analytics Dashboard (Priority: MEDIUM) **Features:** 1. **Cost Breakdown Charts** - By operation type - By time period - By site/sector 2. **Trend Analysis** - Daily/weekly/monthly usage - Forecasting - Budget alerts 3. **Comparison Reports** - Compare accounts - Compare time periods - Benchmark against averages --- ## ๐Ÿงช TESTING CHECKLIST ### Unit Tests Required - [ ] Test CreditCostConfig model creation - [ ] Test CreditService with database config - [ ] Test fallback to constants - [ ] Test atomic credit deduction - [ ] Test negative balance prevention - [ ] Test cost calculation with units ### Integration Tests Required - [ ] Test full credit deduction flow - [ ] Test monthly replenishment - [ ] Test admin UI operations - [ ] Test concurrent deductions (race conditions) - [ ] Test cost changes propagate correctly ### Manual Testing Required - [ ] Create credit config in Django Admin - [ ] Update cost and verify in logs - [ ] Deactivate operation and verify rejection - [ ] Test with different units (per 100 words, per image) - [ ] Verify audit trail (previous_cost, updated_by) --- ## ๐Ÿ“‹ IMPLEMENTATION ROADMAP ### Week 1: Database Configuration - [ ] Create `CreditCostConfig` model - [ ] Create migration - [ ] Create Django Admin - [ ] Create init_credit_costs command - [ ] Update CreditService to use database ### Week 2: Testing & Refinement - [ ] Write unit tests - [ ] Write integration tests - [ ] Manual QA testing - [ ] Fix race conditions - [ ] Add constraints ### Week 3: Documentation & Deployment - [ ] Update API documentation - [ ] Create admin user guide - [ ] Deploy to staging - [ ] User acceptance testing - [ ] Deploy to production ### Week 4: Monitoring & Optimization - [ ] Monitor cost changes - [ ] Analyze usage patterns - [ ] Optimize slow queries - [ ] Plan Phase 2 features --- ## ๐ŸŽฏ SUCCESS CRITERIA โœ… **Backend Admin:** Credits configurable via Django Admin โœ… **No Code Deploys:** Cost changes don't require deployment โœ… **Audit Trail:** Track who changed costs and when โœ… **Backward Compatible:** Existing code continues to work โœ… **Performance:** No regression in credit deduction speed โœ… **Data Integrity:** No race conditions or negative balances โœ… **Testing:** 100% test coverage for critical paths --- ## ๐Ÿ“Š CREDITS SYSTEM FLOWCHART ```mermaid graph TD A[AI Operation Request] --> B{Check CreditCostConfig} B -->|Found| C[Get Cost from Database] B -->|Not Found| D[Get Cost from Constants] C --> E[Calculate Total Cost] D --> E E --> F{Sufficient Credits?} F -->|Yes| G[Atomic Deduct Credits] F -->|No| H[Raise InsufficientCreditsError] G --> I[Create CreditTransaction] I --> J[Create CreditUsageLog] J --> K[Return Success] H --> L[Return Error] ``` --- ## ๐Ÿ” SECURITY CONSIDERATIONS ### Credit Manipulation Prevention 1. **No Client-Side Credit Calculation** - All calculations server-side - Credits never exposed in API responses 2. **Atomic Transactions** - Use database transactions - Prevent race conditions 3. **Audit Logging** - Log all credit changes - Track who/when/why 4. **Rate Limiting** - Prevent credit abuse - Throttle expensive operations 5. **Admin Permissions** - Only superusers can modify costs - Track all admin changes --- ## END OF AUDIT This comprehensive audit identifies all aspects of the credits system, proposes a database-driven configuration approach, and provides a clear roadmap for implementation. The system is currently working well but lacks flexibility for cost adjustments without code deployments. **Recommendation:** Implement the CreditCostConfig model in Phase 1 to enable admin-configurable costs.