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

678 lines
18 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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**