Files
igny8/docs/PLAN-LIMITS.md
IGNY8 VPS (Salman) 6e2101d019 feat: add Usage Limits Panel component with usage tracking and visual indicators for limits
style: implement custom color schemes and gradients for account section, enhancing visual hierarchy
2025-12-12 13:15:15 +00:00

18 KiB

Plan Limits System

Overview

The Plan Limits System enforces subscription-based usage restrictions in IGNY8. It tracks both hard limits (persistent throughout subscription) and monthly limits (reset on billing cycle).

File: /docs/PLAN-LIMITS.md
Version: 1.0.0
Last Updated: December 12, 2025


Architecture

Limit Types

Hard Limits (Never Reset)

These limits persist for the lifetime of the subscription and represent total capacity:

Limit Type Field Name Description Example Value
Sites max_sites Maximum number of sites per account Starter: 2, Growth: 5, Scale: Unlimited
Team Users max_users Maximum team members Starter: 1, Growth: 3, Scale: 10
Keywords max_keywords Total keywords allowed Starter: 500, Growth: 1000, Scale: Unlimited
Clusters max_clusters Total clusters allowed Starter: 50, Growth: 100, Scale: Unlimited

Monthly Limits (Reset on Billing Cycle)

These limits reset automatically at the start of each billing period:

Limit Type Field Name Description Example Value
Content Ideas max_content_ideas New ideas generated per month Starter: 100, Growth: 300, Scale: 600
Content Words max_content_words Total words generated per month Starter: 100K, Growth: 300K, Scale: 500K
Basic Images max_images_basic Basic AI images per month Starter: 100, Growth: 300, Scale: 500
Premium Images max_images_premium Premium AI images (DALL-E) per month Starter: 20, Growth: 60, Scale: 100
Image Prompts max_image_prompts AI-generated prompts per month Starter: 100, Growth: 300, Scale: 500

Database Schema

Plan Model Extensions

Location: backend/igny8_core/auth/models.py

class Plan(models.Model):
    # ... existing fields ...
    
    # Hard Limits
    max_sites = IntegerField(default=2, validators=[MinValueValidator(1)])
    max_users = IntegerField(default=1, validators=[MinValueValidator(1)])
    max_keywords = IntegerField(default=500, validators=[MinValueValidator(1)])
    max_clusters = IntegerField(default=50, validators=[MinValueValidator(1)])
    
    # Monthly Limits
    max_content_ideas = IntegerField(default=100, validators=[MinValueValidator(1)])
    max_content_words = IntegerField(default=100000, validators=[MinValueValidator(1)])
    max_images_basic = IntegerField(default=100, validators=[MinValueValidator(1)])
    max_images_premium = IntegerField(default=20, validators=[MinValueValidator(1)])
    max_image_prompts = IntegerField(default=100, validators=[MinValueValidator(1)])

PlanLimitUsage Model

Location: backend/igny8_core/business/billing/models.py

Tracks monthly consumption for each limit type:

class PlanLimitUsage(AccountBaseModel):
    LIMIT_TYPE_CHOICES = [
        ('content_ideas', 'Content Ideas'),
        ('content_words', 'Content Words'),
        ('images_basic', 'Basic Images'),
        ('images_premium', 'Premium Images'),
        ('image_prompts', 'Image Prompts'),
    ]
    
    limit_type = CharField(max_length=50, choices=LIMIT_TYPE_CHOICES, db_index=True)
    amount_used = IntegerField(default=0, validators=[MinValueValidator(0)])
    period_start = DateField()
    period_end = DateField()
    metadata = JSONField(default=dict)  # Stores breakdown by site, content_id, etc.
    
    class Meta:
        unique_together = [['account', 'limit_type', 'period_start']]
        indexes = [
            Index(fields=['account', 'period_start']),
            Index(fields=['period_end']),
        ]

Migration: backend/igny8_core/modules/billing/migrations/0015_planlimitusage.py


Service Layer

LimitService

Location: backend/igny8_core/business/billing/services/limit_service.py

Central service for all limit operations.

Key Methods

1. Check Hard Limit
LimitService.check_hard_limit(account, limit_type, additional_count=1)

Purpose: Validate if adding items would exceed hard limit
Raises: HardLimitExceededError if limit exceeded
Example:

try:
    LimitService.check_hard_limit(account, 'sites', additional_count=1)
    # Proceed with site creation
except HardLimitExceededError as e:
    raise PermissionDenied(str(e))
2. Check Monthly Limit
LimitService.check_monthly_limit(account, limit_type, amount)

Purpose: Validate if operation would exceed monthly allowance
Raises: MonthlyLimitExceededError if limit exceeded
Example:

try:
    LimitService.check_monthly_limit(account, 'content_words', amount=2500)
    # Proceed with content generation
except MonthlyLimitExceededError as e:
    raise InsufficientCreditsError(str(e))
3. Increment Usage
LimitService.increment_usage(account, limit_type, amount, metadata=None)

Purpose: Record usage after successful operation
Returns: New total usage
Example:

LimitService.increment_usage(
    account=account,
    limit_type='content_words',
    amount=2500,
    metadata={
        'content_id': 123,
        'content_title': 'My Article',
        'site_id': 456
    }
)
4. Get Usage Summary
LimitService.get_usage_summary(account)

Purpose: Comprehensive usage report for all limits
Returns: Dictionary with hard_limits, monthly_limits, period info
Example Response:

{
  "account_id": 1,
  "account_name": "Acme Corp",
  "plan_name": "Growth Plan",
  "period_start": "2025-12-01",
  "period_end": "2025-12-31",
  "days_until_reset": 19,
  "hard_limits": {
    "sites": {
      "display_name": "Sites",
      "current": 3,
      "limit": 5,
      "remaining": 2,
      "percentage_used": 60
    },
    "keywords": {
      "display_name": "Keywords",
      "current": 750,
      "limit": 1000,
      "remaining": 250,
      "percentage_used": 75
    }
  },
  "monthly_limits": {
    "content_words": {
      "display_name": "Content Words",
      "current": 245000,
      "limit": 300000,
      "remaining": 55000,
      "percentage_used": 82
    },
    "images_basic": {
      "display_name": "Basic Images",
      "current": 120,
      "limit": 300,
      "remaining": 180,
      "percentage_used": 40
    }
  }
}
5. Reset Monthly Limits
LimitService.reset_monthly_limits(account)

Purpose: Reset all monthly usage at period end (called by Celery task)
Returns: Dictionary with reset summary
Note: Called automatically by scheduled task, not manually


Enforcement Points

1. Site Creation

File: backend/igny8_core/auth/views.py (SiteViewSet.perform_create)

LimitService.check_hard_limit(account, 'sites', additional_count=1)

2. Content Generation

File: backend/igny8_core/business/content/services/content_generation_service.py

# Check limit before generation
LimitService.check_monthly_limit(account, 'content_words', amount=total_word_count)

# Increment usage after successful generation
LimitService.increment_usage(account, 'content_words', amount=actual_word_count)

3. Content Save Hook

File: backend/igny8_core/business/content/models.py (Content.save)

Automatically increments content_words usage when content_html is saved:

def save(self, *args, **kwargs):
    # Auto-calculate word count
    if self.content_html:
        calculated_count = calculate_word_count(self.content_html)
        self.word_count = calculated_count
    
    super().save(*args, **kwargs)
    
    # Increment usage for newly generated words
    if new_words > 0:
        LimitService.increment_usage(account, 'content_words', amount=new_words)

4. Additional Enforcement Points (To Be Implemented)

Following the same pattern, add checks to:

  • Keyword Import: Check max_keywords before bulk import
  • Clustering: Check max_clusters before creating new clusters
  • Idea Generation: Check max_content_ideas before generating ideas
  • Image Generation: Check max_images_basic/max_images_premium before AI call

Word Counting Utility

Location: backend/igny8_core/utils/word_counter.py

Provides accurate word counting from HTML content.

Functions

calculate_word_count(html_content)

from igny8_core.utils.word_counter import calculate_word_count

word_count = calculate_word_count('<p>Hello <strong>world</strong>!</p>')
# Returns: 2

Method:

  1. Strips HTML tags using BeautifulSoup
  2. Fallback to regex if BeautifulSoup fails
  3. Counts words (sequences of alphanumeric characters)

format_word_count(count)

formatted = format_word_count(1500)  # "1.5K"
formatted = format_word_count(125000)  # "125K"

validate_word_count_limit(html_content, limit)

result = validate_word_count_limit(html, limit=100000)
# Returns: {
#   'allowed': True,
#   'word_count': 2500,
#   'limit': 100000,
#   'remaining': 97500,
#   'would_exceed_by': 0
# }

Scheduled Tasks

Location: backend/igny8_core/tasks/plan_limits.py

1. Reset Monthly Plan Limits

Task Name: reset_monthly_plan_limits
Schedule: Daily at 00:30 UTC
Purpose: Reset monthly usage for accounts at period end

Process:

  1. Find all active accounts with subscriptions
  2. Check if current_period_end <= today
  3. Call LimitService.reset_monthly_limits(account)
  4. Update subscription period dates
  5. Log reset summary

2. Check Approaching Limits

Task Name: check_approaching_limits
Schedule: Daily at 09:00 UTC
Purpose: Warn users when usage exceeds 80% threshold

Process:

  1. Find all active accounts
  2. Get usage summary
  3. Check if any limit >= 80%
  4. Log warnings (future: send email notifications)

Celery Beat Configuration:
backend/igny8_core/celery.py

app.conf.beat_schedule = {
    'reset-monthly-plan-limits': {
        'task': 'reset_monthly_plan_limits',
        'schedule': crontab(hour=0, minute=30),
    },
    'check-approaching-limits': {
        'task': 'check_approaching_limits',
        'schedule': crontab(hour=9, minute=0),
    },
}

API Endpoints

Get Usage Summary

Endpoint: GET /api/v1/billing/usage-summary/
Authentication: Required (IsAuthenticatedAndActive)
Response: Usage summary for current account

Example Request:

curl -H "Authorization: Bearer <token>" \
  /api/v1/billing/usage-summary/

Example Response:

{
  "success": true,
  "message": "Usage summary retrieved successfully.",
  "data": {
    "account_id": 1,
    "account_name": "Acme Corp",
    "plan_name": "Growth Plan",
    "period_start": "2025-12-01",
    "period_end": "2025-12-31",
    "days_until_reset": 19,
    "hard_limits": { ... },
    "monthly_limits": { ... }
  }
}

Error Handling

HardLimitExceededError

raise HardLimitExceededError(
    f"Sites limit exceeded. Current: 5, Limit: 5. "
    f"Upgrade your plan to increase this limit."
)

HTTP Status: 403 Forbidden
User Action: Upgrade plan or delete unused resources

MonthlyLimitExceededError

raise MonthlyLimitExceededError(
    f"Content Words limit exceeded. Used: 295000, Requested: 8000, Limit: 300000. "
    f"Resets on December 31, 2025. Upgrade your plan or wait for reset."
)

HTTP Status: 403 Forbidden
User Action: Wait for reset, upgrade plan, or reduce request size


Frontend Integration Guide

TypeScript Types

interface Plan {
  id: number;
  name: string;
  // Hard limits
  max_sites: number;
  max_users: number;
  max_keywords: number;
  max_clusters: number;
  // Monthly limits
  max_content_ideas: number;
  max_content_words: number;
  max_images_basic: number;
  max_images_premium: number;
  max_image_prompts: number;
}

interface UsageSummary {
  account_id: number;
  account_name: string;
  plan_name: string;
  period_start: string;
  period_end: string;
  days_until_reset: number;
  hard_limits: {
    [key: string]: {
      display_name: string;
      current: number;
      limit: number;
      remaining: number;
      percentage_used: number;
    };
  };
  monthly_limits: {
    [key: string]: {
      display_name: string;
      current: number;
      limit: number;
      remaining: number;
      percentage_used: number;
    };
  };
}

API Hook Example

// src/services/api/billing.ts
export const getUsageSummary = async (): Promise<UsageSummary> => {
  const response = await apiClient.get('/billing/usage-summary/');
  return response.data.data;
};

// src/pages/Dashboard.tsx
const { data: usage } = useQuery('usage-summary', getUsageSummary);

UI Components

Usage Widget

<Card>
  <CardHeader>
    <h3>Usage This Month</h3>
    <span>{usage.days_until_reset} days until reset</span>
  </CardHeader>
  <CardBody>
    {Object.entries(usage.monthly_limits).map(([key, data]) => (
      <div key={key}>
        <div>{data.display_name}</div>
        <ProgressBar 
          value={data.percentage_used} 
          variant={data.percentage_used >= 80 ? 'warning' : 'primary'}
        />
        <span>{data.current.toLocaleString()} / {data.limit.toLocaleString()}</span>
      </div>
    ))}
  </CardBody>
</Card>

Limit Warning Alert

{usage.monthly_limits.content_words.percentage_used >= 80 && (
  <Alert variant="warning">
    ⚠️ You've used {usage.monthly_limits.content_words.percentage_used}% of your 
    monthly word limit. Resets in {usage.days_until_reset} days. 
    <Link to="/billing/plans">Upgrade Plan</Link>
  </Alert>
)}

Testing

Manual Testing Checklist

  1. Hard Limit - Sites:

    • Set plan max_sites = 2
    • Create 2 sites successfully
    • Attempt to create 3rd site → should fail with error
  2. Monthly Limit - Words:

    • Set plan max_content_words = 5000
    • Generate content with 3000 words
    • Generate content with 2500 words → should fail
    • Check usage API shows 3000/5000
  3. Usage Increment:

    • Generate content
    • Verify PlanLimitUsage.amount_used increments correctly
    • Check metadata contains content_id
  4. Monthly Reset:

    • Manually run: docker exec igny8_backend python manage.py shell
    • Execute:
      from igny8_core.tasks.plan_limits import reset_monthly_plan_limits
      reset_monthly_plan_limits()
      
    • Verify usage resets to 0
    • Verify new period records created
  5. Usage Summary API:

    • Call GET /api/v1/billing/usage-summary/
    • Verify all limits present
    • Verify percentages calculated correctly

Unit Test Example

# tests/test_limit_service.py
def test_check_hard_limit_exceeded():
    account = create_test_account(plan_max_sites=2)
    create_test_sites(account, count=2)
    
    with pytest.raises(HardLimitExceededError):
        LimitService.check_hard_limit(account, 'sites', additional_count=1)

def test_increment_monthly_usage():
    account = create_test_account()
    
    LimitService.increment_usage(account, 'content_words', amount=1000)
    
    usage = PlanLimitUsage.objects.get(account=account, limit_type='content_words')
    assert usage.amount_used == 1000

Monitoring & Logs

Key Log Messages

Successful limit check:

INFO Hard limit check: sites - Current: 2, Requested: 1, Limit: 5
INFO Monthly limit check: content_words - Current: 50000, Requested: 2500, Limit: 100000

Limit exceeded:

WARNING Hard limit exceeded: sites - Current: 5, Requested: 1, Limit: 5
WARNING Monthly limit exceeded: content_words - Used: 98000, Requested: 5000, Limit: 100000

Usage increment:

INFO Incremented content_words usage by 2500. New total: 52500

Monthly reset:

INFO Resetting limits for account 123 (Acme Corp) - period ended 2025-12-31
INFO Reset complete for account 123: New period 2026-01-01 to 2026-01-31
INFO Monthly plan limits reset task complete: 45 accounts reset, 0 errors

Troubleshooting

Issue: Limits not enforcing

Check:

  1. Verify Plan has non-zero limit values: Plan.objects.get(id=X)
  2. Check if service calling LimitService methods
  3. Review logs for exceptions being caught

Issue: Usage not incrementing

Check:

  1. Verify Content.save() executing successfully
  2. Check for exceptions in logs during increment_usage
  3. Query PlanLimitUsage table directly

Issue: Reset task not running

Check:

  1. Celery Beat is running: docker exec igny8_backend celery -A igny8_core inspect active
  2. Check Celery Beat schedule: docker exec igny8_backend celery -A igny8_core inspect scheduled
  3. Review Celery logs: docker logs igny8_celery_beat

Future Enhancements

  1. Email Notifications:

    • Send warning emails at 80%, 90%, 100% thresholds
    • Weekly usage summary reports
    • Monthly reset confirmations
  2. Additional Enforcement:

    • Keyword bulk import limit check
    • Cluster creation limit check
    • Idea generation limit check
    • Image generation limit checks
  3. Usage Analytics:

    • Historical usage trends
    • Projection of limit exhaustion date
    • Recommendations for plan upgrades
  4. Soft Limits:

    • Allow slight overages with warnings
    • Grace period before hard enforcement
  5. Admin Tools:

    • Override limits for specific accounts
    • One-time usage bonuses
    • Custom limit adjustments

Models:

  • backend/igny8_core/auth/models.py - Plan model
  • backend/igny8_core/business/billing/models.py - PlanLimitUsage model

Services:

  • backend/igny8_core/business/billing/services/limit_service.py - LimitService
  • backend/igny8_core/utils/word_counter.py - Word counting utility

Views:

  • backend/igny8_core/auth/views.py - Site creation enforcement
  • backend/igny8_core/business/billing/views.py - Usage summary API
  • backend/igny8_core/business/content/services/content_generation_service.py - Content generation enforcement

Tasks:

  • backend/igny8_core/tasks/plan_limits.py - Reset and warning tasks
  • backend/igny8_core/celery.py - Celery Beat schedule

Migrations:

  • backend/igny8_core/auth/migrations/0013_plan_max_clusters_plan_max_content_ideas_and_more.py
  • backend/igny8_core/modules/billing/migrations/0015_planlimitusage.py

Documentation:

  • CHANGELOG.md - Version history with plan limits feature
  • .cursorrules - Development standards and versioning rules

End of Document