style: implement custom color schemes and gradients for account section, enhancing visual hierarchy
678 lines
18 KiB
Markdown
678 lines
18 KiB
Markdown
# 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**
|