style: implement custom color schemes and gradients for account section, enhancing visual hierarchy
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_keywordsbefore bulk import - Clustering: Check
max_clustersbefore creating new clusters - Idea Generation: Check
max_content_ideasbefore generating ideas - Image Generation: Check
max_images_basic/max_images_premiumbefore 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:
- Strips HTML tags using BeautifulSoup
- Fallback to regex if BeautifulSoup fails
- 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:
- Find all active accounts with subscriptions
- Check if
current_period_end<= today - Call
LimitService.reset_monthly_limits(account) - Update subscription period dates
- 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:
- Find all active accounts
- Get usage summary
- Check if any limit >= 80%
- 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
-
Hard Limit - Sites:
- Set plan
max_sites = 2 - Create 2 sites successfully
- Attempt to create 3rd site → should fail with error
- Set plan
-
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
- Set plan
-
Usage Increment:
- Generate content
- Verify
PlanLimitUsage.amount_usedincrements correctly - Check metadata contains content_id
-
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
- Manually run:
-
Usage Summary API:
- Call GET
/api/v1/billing/usage-summary/ - Verify all limits present
- Verify percentages calculated correctly
- Call GET
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:
- Verify Plan has non-zero limit values:
Plan.objects.get(id=X) - Check if service calling LimitService methods
- Review logs for exceptions being caught
Issue: Usage not incrementing
Check:
- Verify Content.save() executing successfully
- Check for exceptions in logs during increment_usage
- Query
PlanLimitUsagetable directly
Issue: Reset task not running
Check:
- Celery Beat is running:
docker exec igny8_backend celery -A igny8_core inspect active - Check Celery Beat schedule:
docker exec igny8_backend celery -A igny8_core inspect scheduled - Review Celery logs:
docker logs igny8_celery_beat
Future Enhancements
-
Email Notifications:
- Send warning emails at 80%, 90%, 100% thresholds
- Weekly usage summary reports
- Monthly reset confirmations
-
Additional Enforcement:
- Keyword bulk import limit check
- Cluster creation limit check
- Idea generation limit check
- Image generation limit checks
-
Usage Analytics:
- Historical usage trends
- Projection of limit exhaustion date
- Recommendations for plan upgrades
-
Soft Limits:
- Allow slight overages with warnings
- Grace period before hard enforcement
-
Admin Tools:
- Override limits for specific accounts
- One-time usage bonuses
- Custom limit adjustments
Related Files
Models:
backend/igny8_core/auth/models.py- Plan modelbackend/igny8_core/business/billing/models.py- PlanLimitUsage model
Services:
backend/igny8_core/business/billing/services/limit_service.py- LimitServicebackend/igny8_core/utils/word_counter.py- Word counting utility
Views:
backend/igny8_core/auth/views.py- Site creation enforcementbackend/igny8_core/business/billing/views.py- Usage summary APIbackend/igny8_core/business/content/services/content_generation_service.py- Content generation enforcement
Tasks:
backend/igny8_core/tasks/plan_limits.py- Reset and warning tasksbackend/igny8_core/celery.py- Celery Beat schedule
Migrations:
backend/igny8_core/auth/migrations/0013_plan_max_clusters_plan_max_content_ideas_and_more.pybackend/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