# 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`

```python
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:

```python
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
```python
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:**
```python
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
```python
LimitService.check_monthly_limit(account, limit_type, amount)
```

**Purpose:** Validate if operation would exceed monthly allowance  
**Raises:** `MonthlyLimitExceededError` if limit exceeded  
**Example:**
```python
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
```python
LimitService.increment_usage(account, limit_type, amount, metadata=None)
```

**Purpose:** Record usage after successful operation  
**Returns:** New total usage  
**Example:**
```python
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
```python
LimitService.get_usage_summary(account)
```

**Purpose:** Comprehensive usage report for all limits  
**Returns:** Dictionary with hard_limits, monthly_limits, period info  
**Example Response:**
```json
{
  "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
```python
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)

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

### 2. Content Generation
**File:** `backend/igny8_core/business/content/services/content_generation_service.py`

```python
# 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:

```python
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)
```python
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)
```python
formatted = format_word_count(1500)  # "1.5K"
formatted = format_word_count(125000)  # "125K"
```

#### validate_word_count_limit(html_content, limit)
```python
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`

```python
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:**
```bash
curl -H "Authorization: Bearer <token>" \
  /api/v1/billing/usage-summary/
```

**Example Response:**
```json
{
  "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
```python
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
```python
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
```typescript
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
```typescript
// 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
```tsx
<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
```tsx
{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:
     ```python
     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
```python
# 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

---

## Related Files

**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**
