Files
igny8/old-docs/billing/credits-system-audit-and-improvement-plan.md
2025-12-07 16:49:30 +05:00

26 KiB

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:

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:

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:

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

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

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

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(
            '<span style="color: {}; font-weight: bold;">{} credits</span>',
            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(
                '{} <span style="color: {};">({}{})</span>',
                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

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

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:

python manage.py init_credit_costs

🔍 POTENTIAL ISSUES & FIXES

Issue 1: Race Conditions in Credit Deduction

Problem:

# 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

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

# 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

# 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

# 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

# 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

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.