2450 lines
76 KiB
Markdown
2450 lines
76 KiB
Markdown
# Credits & Limits System - Implementation Plan
|
||
|
||
**Date:** January 5, 2026
|
||
**Status:** 🚧 IMPLEMENTATION PLAN - Pending Execution
|
||
**Prepared For:** IGNY8 Platform
|
||
**Purpose:** Comprehensive plan to simplify, optimize, and properly enforce credits and limits system
|
||
|
||
---
|
||
|
||
## Executive Summary
|
||
|
||
This implementation plan addresses the IGNY8 platform's credits and limits system based on complete codebase analysis.
|
||
|
||
### Core Philosophy: SIMPLICITY
|
||
|
||
**Keep ONLY 4 Hard Limits:**
|
||
1. **Sites** - How many sites per account
|
||
2. **Team Users** - How many team members
|
||
3. **Keywords** - Total keywords in workspace
|
||
4. **Ahrefs Queries** - Monthly research queries (NEW)
|
||
|
||
**Everything Else = Credits** - Let users consume credits however they want (content, images, ideas, etc.)
|
||
|
||
### What We're Doing
|
||
|
||
✅ **REMOVE** all unnecessary monthly limits (content_ideas, content_words, images_basic, images_premium, image_prompts)
|
||
✅ **SIMPLIFY** to credit-based system only
|
||
✅ **ENFORCE** the 4 hard limits properly
|
||
✅ **ELIMINATE** data duplication across pages
|
||
✅ **REDESIGN** Usage page with multi-dimensional insights
|
||
✅ **IMPLEMENT** Ahrefs keyword research structure
|
||
✅ **VALIDATE** all credit balance checks
|
||
✅ **USE** user-friendly terminology (no "API", "operations", etc.)
|
||
|
||
---
|
||
|
||
## 1. FINAL SIMPLIFIED MODEL
|
||
|
||
### 1.1 The Only Limits That Matter
|
||
|
||
| Limit | Type | Description | Enforcement |
|
||
|-------|------|-------------|-------------|
|
||
| **Sites** | Hard | Max sites per account | ✅ Keep & Enforce |
|
||
| **Team Users** | Hard | Max team members | ✅ Keep & Enforce |
|
||
| **Keywords** | Hard | Total keywords in workspace | ✅ Keep & Enforce |
|
||
| **Ahrefs Queries** | Monthly | Research queries per month | 🆕 Implement |
|
||
|
||
### 1.2 What Gets REMOVED
|
||
|
||
**❌ REMOVE These Limits (Use Credits Instead):**
|
||
- `max_content_ideas` - Let credits control this
|
||
- `max_content_words` - Let credits control this
|
||
- `max_images_basic` - Let credits control this
|
||
- `max_images_premium` - Let credits control this
|
||
- `max_image_prompts` - Let credits control this
|
||
- `max_clusters` - Let credits control this (or combine with keywords)
|
||
- `usage_content_ideas` - Not needed
|
||
- `usage_content_words` - Not needed
|
||
- `usage_images_basic` - Not needed
|
||
- `usage_images_premium` - Not needed
|
||
- `usage_image_prompts` - Not needed
|
||
|
||
**Why Remove?**
|
||
- Confusing for users ("I have credits but can't generate content?")
|
||
- Double limiting (credits + monthly limits)
|
||
- Maintenance overhead
|
||
- Credit system already provides control
|
||
|
||
### 1.3 Keyword Research Structure (NEW)
|
||
|
||
**Two Ways to Add Keywords:**
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ KEYWORD RESEARCH OPTIONS │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Option 1: IGNY8 Pre-Researched Keywords (FREE) │
|
||
│ ──────────────────────────────────────────── │
|
||
│ • Access global keyword database │
|
||
│ • Filter by industry, sector, country │
|
||
│ • Pre-analyzed for search volume, difficulty │
|
||
│ • Free to browse and add to workspace │
|
||
│ • Limited by: max_keywords (total workspace limit) │
|
||
│ │
|
||
│ Option 2: Ahrefs Live Research (LIMITED) │
|
||
│ ──────────────────────────────────────────── │
|
||
│ • Query Ahrefs API directly │
|
||
│ • Get fresh, custom keyword data │
|
||
│ • Monthly query limit (e.g., 50-500 depending on plan) │
|
||
│ • Limited by: max_ahrefs_queries (monthly limit) │
|
||
│ • Results can be added to workspace (counts toward max_keywords)│
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Implementation:**
|
||
- Rename current "Add Keywords" to "Browse Pre-Researched Keywords"
|
||
- Add new tab/section "Research with Ahrefs" (with monthly limit indicator)
|
||
- Show remaining queries: "You have 42/50 Ahrefs queries remaining this month"
|
||
|
||
---
|
||
|
||
## 2. Current System Analysis (What We Found)
|
||
|
||
### 1.1 Frontend Pages Breakdown
|
||
|
||
#### Plans & Billing Dropdown (`/account/plans`)
|
||
|
||
| Page | Route | Primary Purpose | Current Data Displayed |
|
||
|------|-------|----------------|----------------------|
|
||
| **Current Plan** | `/account/plans` | View active subscription | Plan name, price, renewal date, included credits |
|
||
| **Upgrade Plan** | `/account/plans/upgrade` | Compare and purchase plans | Pricing table with all plans, features, limits |
|
||
| **History** | `/account/plans/history` | Invoices and payments | Invoice list, payment methods, transaction history |
|
||
|
||
**File:** `frontend/src/pages/account/PlansAndBillingPage.tsx` (881 lines)
|
||
|
||
**Tab Structure:**
|
||
```typescript
|
||
type TabType = 'plan' | 'upgrade' | 'invoices';
|
||
```
|
||
|
||
**Key Data Shown:**
|
||
- Current plan details (name, price, billing cycle)
|
||
- Upgrade options (pricing table with all plans)
|
||
- Credit balance (via `getCreditBalance()`)
|
||
- Billing history (invoices, payments)
|
||
- Payment methods management
|
||
|
||
#### Usage Dropdown (`/account/usage`)
|
||
|
||
| Page | Route | Primary Purpose | Current Data Displayed |
|
||
|------|-------|----------------|----------------------|
|
||
| **Limits & Usage** | `/account/usage` | Track plan limits | Hard limits + monthly limits with progress bars |
|
||
| **Credit History** | `/account/usage/credits` | View credit transactions | Transaction log (purchases, deductions, adjustments) |
|
||
| **Activity** | `/account/usage/activity` | Monitor API operations | API call statistics by operation type |
|
||
|
||
**File:** `frontend/src/pages/account/UsageAnalyticsPage.tsx` (266 lines)
|
||
|
||
**Tab Structure:**
|
||
```typescript
|
||
type TabType = 'limits' | 'activity' | 'api';
|
||
```
|
||
|
||
**Key Data Shown:**
|
||
- Usage summary (hard limits: sites, users, keywords, clusters)
|
||
- Monthly limits (content ideas, words, images)
|
||
- Credit balance and monthly usage
|
||
- Transaction history
|
||
- API activity by operation type
|
||
|
||
---
|
||
|
||
### 1.2 Backend Models
|
||
|
||
#### Account Model (`backend/igny8_core/auth/models.py`)
|
||
|
||
**Credits Field:**
|
||
```python
|
||
credits = models.IntegerField(default=0, validators=[MinValueValidator(0)])
|
||
```
|
||
|
||
**Monthly Usage Tracking Fields:**
|
||
```python
|
||
usage_content_ideas = models.IntegerField(default=0) # Monthly limit tracking
|
||
usage_content_words = models.IntegerField(default=0) # Monthly limit tracking
|
||
usage_images_basic = models.IntegerField(default=0) # Monthly limit tracking
|
||
usage_images_premium = models.IntegerField(default=0) # Monthly limit tracking
|
||
usage_image_prompts = models.IntegerField(default=0) # Monthly limit tracking
|
||
usage_period_start = models.DateTimeField(null=True) # Billing period tracking
|
||
usage_period_end = models.DateTimeField(null=True) # Billing period tracking
|
||
```
|
||
|
||
#### Plan Model
|
||
|
||
**Hard Limits (Never Reset):**
|
||
```python
|
||
max_sites = models.IntegerField(default=1) # Sites allowed per account
|
||
max_users = models.IntegerField(default=1) # Team members allowed
|
||
max_keywords = models.IntegerField(default=1000) # Total keywords allowed
|
||
max_clusters = models.IntegerField(default=100) # Total clusters allowed
|
||
```
|
||
|
||
**Monthly Limits (Reset on Billing Cycle):**
|
||
```python
|
||
max_content_ideas = models.IntegerField(default=300) # Ideas per month
|
||
max_content_words = models.IntegerField(default=100000) # Words per month
|
||
max_images_basic = models.IntegerField(default=300) # Basic images per month
|
||
max_images_premium = models.IntegerField(default=60) # Premium images per month
|
||
max_image_prompts = models.IntegerField(default=300) # Image prompts per month
|
||
```
|
||
|
||
**Credits:**
|
||
```python
|
||
included_credits = models.IntegerField(default=0) # Monthly credit allocation
|
||
extra_credit_price = models.DecimalField(default=0.01) # Price per additional credit
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Data Flow Analysis
|
||
|
||
### 2.1 Credit Deduction Flow
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ CREDIT DEDUCTION WORKFLOW │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 1. User triggers AI operation (e.g., generate content) │
|
||
│ ↓ │
|
||
│ 2. Backend service calls CreditService │
|
||
│ ↓ │
|
||
│ 3. Check balance: CreditService.check_credits(account, credits)│
|
||
│ ↓ │
|
||
│ 4. Execute AI operation (OpenAI/Runware/etc.) │
|
||
│ ↓ │
|
||
│ 5. AI returns tokens used (input + output) │
|
||
│ ↓ │
|
||
│ 6. Calculate credits: CreditService.calculate_credits_from_tokens()│
|
||
│ • Lookup AIModelConfig.tokens_per_credit for model │
|
||
│ • credits = ceil(total_tokens / tokens_per_credit) │
|
||
│ ↓ │
|
||
│ 7. Deduct credits: │
|
||
│ • Create CreditTransaction (amount=-credits) │
|
||
│ • Create CreditUsageLog (operation details) │
|
||
│ • Update Account.credits │
|
||
│ ↓ │
|
||
│ 8. Return result to user │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Key Services:**
|
||
- `CreditService.check_credits()` - Pre-flight balance check
|
||
- `CreditService.calculate_credits_from_tokens()` - Token-based calculation
|
||
- `CreditService.deduct_credits_for_operation()` - Deduct and log
|
||
|
||
### 2.2 Limit Enforcement Flow
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ LIMIT ENFORCEMENT WORKFLOW │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ HARD LIMITS (Sites, Users, Keywords, Clusters) │
|
||
│ ─────────────────────────────────────────────── │
|
||
│ 1. User attempts to create new resource │
|
||
│ ↓ │
|
||
│ 2. LimitService.check_hard_limit(account, limit_type, count) │
|
||
│ • Query database for current count │
|
||
│ • Compare: current_count + new_count <= plan.max_XXX │
|
||
│ ↓ │
|
||
│ 3. If exceeded: Raise HardLimitExceededError │
|
||
│ If OK: Allow creation │
|
||
│ │
|
||
│ MONTHLY LIMITS (Ideas, Words, Images, Prompts) │
|
||
│ ───────────────────────────────────────────── │
|
||
│ 1. User attempts AI operation │
|
||
│ ↓ │
|
||
│ 2. LimitService.check_monthly_limit(account, limit_type, amount)│
|
||
│ • Read from Account.usage_XXX field │
|
||
│ • Compare: current_usage + amount <= plan.max_XXX │
|
||
│ ↓ │
|
||
│ 3. If exceeded: Raise MonthlyLimitExceededError │
|
||
│ If OK: Proceed with operation │
|
||
│ ↓ │
|
||
│ 4. After operation: LimitService.increment_usage() │
|
||
│ • Update Account.usage_XXX field │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Key Services:**
|
||
- `LimitService.check_hard_limit()` - Check persistent limits
|
||
- `LimitService.check_monthly_limit()` - Check monthly allowances
|
||
- `LimitService.increment_usage()` - Update monthly usage counters
|
||
- `LimitService.get_usage_summary()` - Get all limits and usage
|
||
|
||
---
|
||
|
||
## 3. Current Limits Configuration
|
||
|
||
### 3.1 Items That ARE Limit-Based (User-Requested)
|
||
|
||
| Item | Type | Plan Field | Enforcement Location | Status |
|
||
|------|------|-----------|---------------------|--------|
|
||
| **Sites** | Hard Limit | `max_sites` | Account creation, Site model | ✅ Enforced |
|
||
| **Team Users** | Hard Limit | `max_users` | User invite, SiteUserAccess model | ✅ Enforced |
|
||
| **Keywords** | Hard Limit | `max_keywords` | Keyword import/creation | ⚠️ PARTIAL (see below) |
|
||
|
||
### 3.2 Items That Should Have Limits (Future)
|
||
|
||
| Item | Type | Plan Field | Status | Notes |
|
||
|------|------|-----------|--------|-------|
|
||
| **Ahrefs Queries** | Monthly Limit | `max_ahrefs_queries` (new) | ❌ Not Implemented | Future feature for keyword research |
|
||
|
||
### 3.3 Items NOT Limit-Based (Credit-Based)
|
||
|
||
| Item | Enforcement | Notes |
|
||
|------|------------|-------|
|
||
| **AI Content Generation** | Credits | Token-based calculation via AIModelConfig |
|
||
| **AI Image Generation** | Credits | Fixed credits per image (1, 5, or 15) |
|
||
| **AI Clustering** | Credits | Token-based calculation |
|
||
| **AI Idea Generation** | Credits | Token-based calculation |
|
||
| **Internal Linking** | Credits | 8 credits per content piece |
|
||
| **SEO Optimization** | Credits | 1 credit per 200 words |
|
||
|
||
**Important:** Monthly limits exist for these (max_content_ideas, max_content_words, max_images_basic, etc.) but they serve as **soft limits for cost control**, not hard enforcement. Users are primarily constrained by credits.
|
||
|
||
---
|
||
|
||
## 4. Data Duplication Issues
|
||
|
||
### 4.1 Problem: Overlapping Data Across Pages
|
||
|
||
**Current Situation:**
|
||
|
||
| Data Type | Plans & Billing | Usage Analytics | Duplication Level |
|
||
|-----------|----------------|-----------------|-------------------|
|
||
| **Credit Balance** | ✅ Shown in Current Plan tab | ✅ Shown in all tabs (top cards) | 🔴 HIGH |
|
||
| **Monthly Usage** | ✅ Credits used this month | ✅ Credits used this month | 🔴 HIGH |
|
||
| **Plan Limits** | ✅ Shown in plan details | ✅ Full limits panel with progress bars | 🟡 MEDIUM |
|
||
| **Credit Transactions** | ✅ Billing History tab | ✅ Credit History tab | 🔴 HIGH |
|
||
|
||
**Example of Duplication:**
|
||
|
||
**Plans & Billing - Current Plan Tab:**
|
||
```tsx
|
||
<Card>
|
||
<h3>Current Plan: {planName}</h3>
|
||
<div>Credits: {creditBalance.credits}</div>
|
||
<div>Used This Month: {creditBalance.credits_used_this_month}</div>
|
||
<div>Monthly Allowance: {creditBalance.plan_credits_per_month}</div>
|
||
</Card>
|
||
```
|
||
|
||
**Usage Analytics - Limits Tab:**
|
||
```tsx
|
||
<Card>
|
||
<div>Credits Left: {creditBalance.credits}</div>
|
||
<div>Credits Used This Month: {creditBalance.credits_used_this_month}</div>
|
||
<div>Your Monthly Limit: {creditBalance.plan_credits_per_month}</div>
|
||
</Card>
|
||
```
|
||
|
||
### 4.2 Problem: Confusing User Journey
|
||
|
||
**Current User Experience Issues:**
|
||
|
||
1. **Credit Balance** appears in BOTH dropdowns
|
||
- Plans & Billing → Current Plan → Shows credit balance
|
||
- Usage → All tabs → Shows credit balance in top cards
|
||
|
||
2. **Transaction History** appears in BOTH dropdowns
|
||
- Plans & Billing → History → Shows invoices and payments
|
||
- Usage → Credit History → Shows credit transactions
|
||
|
||
3. **Plan Limits** appears in BOTH dropdowns
|
||
- Plans & Billing → Current Plan → Shows plan features and limits
|
||
- Usage → Limits & Usage → Shows detailed progress bars for all limits
|
||
|
||
**User Confusion:**
|
||
- "Where do I check my credits?" → Two places
|
||
- "Where do I see my usage?" → Two places
|
||
- "Where do I see my transaction history?" → Two places
|
||
|
||
---
|
||
|
||
## 5. Enforcement Analysis
|
||
|
||
### 5.1 Well-Enforced Limits ✅
|
||
|
||
#### Sites (max_sites)
|
||
**Enforcement Location:** `backend/igny8_core/auth/models.py` (Site model)
|
||
```python
|
||
# When creating a new site
|
||
LimitService.check_hard_limit(account, 'sites', 1)
|
||
```
|
||
|
||
**Status:** ✅ **FULLY ENFORCED**
|
||
**Works:** Users cannot create more sites than their plan allows
|
||
|
||
#### Team Users (max_users)
|
||
**Enforcement Location:** User invite flow
|
||
```python
|
||
# When inviting a new user
|
||
LimitService.check_hard_limit(account, 'users', 1)
|
||
```
|
||
|
||
**Status:** ✅ **FULLY ENFORCED**
|
||
**Works:** Users cannot invite more team members than their plan allows
|
||
|
||
---
|
||
|
||
### 5.2 Partially Enforced Limits ⚠️
|
||
|
||
#### Keywords (max_keywords)
|
||
|
||
**Current State:**
|
||
- ✅ Plan field exists: `Plan.max_keywords`
|
||
- ✅ LimitService has mapping:
|
||
```python
|
||
'keywords': {
|
||
'model': 'planner.Keywords',
|
||
'plan_field': 'max_keywords',
|
||
'display_name': 'Keywords',
|
||
'filter_field': 'account',
|
||
}
|
||
```
|
||
- ⚠️ **ENFORCEMENT INCONSISTENT**
|
||
|
||
**Evidence from Code Search:**
|
||
|
||
Found in `backend/igny8_core/business/planning/models.py`:
|
||
```python
|
||
from igny8_core.business.billing.services.limit_service import LimitService
|
||
|
||
# Only enforced in SOME locations:
|
||
LimitService.increment_usage(
|
||
account=self.account,
|
||
limit_type='keywords',
|
||
amount=1
|
||
)
|
||
```
|
||
|
||
**Problem:**
|
||
- Keyword import from SeedKeywords may not check limit
|
||
- Keyword creation via API may not check limit consistently
|
||
- Manual keyword creation may bypass checks
|
||
|
||
**Impact:**
|
||
- Users may exceed keyword limits without errors
|
||
- Limits shown in UI but not enforced at all entry points
|
||
|
||
**Recommended Fix:**
|
||
1. Add pre-create check to all keyword creation flows:
|
||
```python
|
||
# Before creating keyword
|
||
LimitService.check_hard_limit(account, 'keywords', 1)
|
||
```
|
||
2. Locations to add enforcement:
|
||
- `addSeedKeywordsToWorkflow` API endpoint
|
||
- Keyword bulk import
|
||
- Manual keyword creation form
|
||
- CSV import
|
||
|
||
---
|
||
|
||
### 5.3 Missing Future Limits ❌
|
||
|
||
#### Ahrefs Keyword Queries (Planned)
|
||
|
||
**User Requirement:**
|
||
> "in future soon we will have option for user to query and research keywords from ahrefs, which will also have monthly limit"
|
||
|
||
**Current State:**
|
||
- ❌ No plan field defined
|
||
- ❌ No LimitService mapping
|
||
- ❌ No enforcement
|
||
|
||
**Current Keywords Situation:**
|
||
- **Existing:** Pre-searched keywords from IGNY8's own database (SeedKeywords)
|
||
- **Limited by:** `max_keywords` (how many they can add to their workspace)
|
||
- **Future:** Ahrefs API queries (need new monthly limit)
|
||
|
||
**Recommended Implementation:**
|
||
|
||
1. **Add Plan Field:**
|
||
```python
|
||
# backend/igny8_core/auth/models.py - Plan model
|
||
max_ahrefs_queries = models.IntegerField(
|
||
default=50,
|
||
validators=[MinValueValidator(0)],
|
||
help_text="Maximum Ahrefs keyword queries per month"
|
||
)
|
||
```
|
||
|
||
2. **Add Account Usage Field:**
|
||
```python
|
||
# backend/igny8_core/auth/models.py - Account model
|
||
usage_ahrefs_queries = models.IntegerField(
|
||
default=0,
|
||
validators=[MinValueValidator(0)],
|
||
help_text="Ahrefs queries used this month"
|
||
)
|
||
```
|
||
|
||
3. **Add LimitService Mapping:**
|
||
```python
|
||
# backend/igny8_core/business/billing/services/limit_service.py
|
||
MONTHLY_LIMIT_MAPPINGS = {
|
||
# ... existing mappings ...
|
||
'ahrefs_queries': {
|
||
'plan_field': 'max_ahrefs_queries',
|
||
'usage_field': 'usage_ahrefs_queries',
|
||
'display_name': 'Ahrefs Keyword Queries',
|
||
},
|
||
}
|
||
```
|
||
|
||
4. **Enforce Before Query:**
|
||
```python
|
||
# In Ahrefs query service
|
||
def query_ahrefs_keywords(account, query):
|
||
# Check limit
|
||
LimitService.check_monthly_limit(account, 'ahrefs_queries', 1)
|
||
|
||
# Execute query
|
||
results = ahrefs_api.search_keywords(query)
|
||
|
||
# Increment usage
|
||
LimitService.increment_usage(account, 'ahrefs_queries', 1)
|
||
|
||
return results
|
||
```
|
||
|
||
---
|
||
|
||
## 6. IMPLEMENTATION PLAN
|
||
|
||
### 6.1 Phase 1: Backend Cleanup (Week 1)
|
||
|
||
#### 1.1 Remove Unused Monthly Limits from Database
|
||
|
||
**File:** `backend/igny8_core/auth/models.py`
|
||
|
||
**Remove from Plan Model:**
|
||
```python
|
||
# REMOVE THESE FIELDS:
|
||
max_content_ideas = models.IntegerField(...) # ❌ DELETE
|
||
max_content_words = models.IntegerField(...) # ❌ DELETE
|
||
max_images_basic = models.IntegerField(...) # ❌ DELETE
|
||
max_images_premium = models.IntegerField(...) # ❌ DELETE
|
||
max_image_prompts = models.IntegerField(...) # ❌ DELETE
|
||
max_clusters = models.IntegerField(...) # ❌ DELETE (or merge with keywords)
|
||
```
|
||
|
||
**Remove from Account Model:**
|
||
```python
|
||
# REMOVE THESE FIELDS:
|
||
usage_content_ideas = models.IntegerField(...) # ❌ DELETE
|
||
usage_content_words = models.IntegerField(...) # ❌ DELETE
|
||
usage_images_basic = models.IntegerField(...) # ❌ DELETE
|
||
usage_images_premium = models.IntegerField(...) # ❌ DELETE
|
||
usage_image_prompts = models.IntegerField(...) # ❌ DELETE
|
||
```
|
||
|
||
**KEEP ONLY:**
|
||
```python
|
||
# Plan Model - KEEP THESE:
|
||
max_sites = models.IntegerField(default=1) # ✅ KEEP
|
||
max_users = models.IntegerField(default=1) # ✅ KEEP
|
||
max_keywords = models.IntegerField(default=1000) # ✅ KEEP
|
||
max_ahrefs_queries = models.IntegerField(default=50) # 🆕 ADD
|
||
|
||
# Account Model - KEEP THESE:
|
||
credits = models.IntegerField(default=0) # ✅ KEEP
|
||
usage_ahrefs_queries = models.IntegerField(default=0) # 🆕 ADD
|
||
usage_period_start = models.DateTimeField(...) # ✅ KEEP
|
||
usage_period_end = models.DateTimeField(...) # ✅ KEEP
|
||
```
|
||
|
||
**Migration Script:**
|
||
```python
|
||
# Create migration: 0XXX_remove_unused_limits.py
|
||
operations = [
|
||
# Remove from Plan
|
||
migrations.RemoveField(model_name='plan', name='max_content_ideas'),
|
||
migrations.RemoveField(model_name='plan', name='max_content_words'),
|
||
migrations.RemoveField(model_name='plan', name='max_images_basic'),
|
||
migrations.RemoveField(model_name='plan', name='max_images_premium'),
|
||
migrations.RemoveField(model_name='plan', name='max_image_prompts'),
|
||
migrations.RemoveField(model_name='plan', name='max_clusters'),
|
||
|
||
# Remove from Account
|
||
migrations.RemoveField(model_name='account', name='usage_content_ideas'),
|
||
migrations.RemoveField(model_name='account', name='usage_content_words'),
|
||
migrations.RemoveField(model_name='account', name='usage_images_basic'),
|
||
migrations.RemoveField(model_name='account', name='usage_images_premium'),
|
||
migrations.RemoveField(model_name='account', name='usage_image_prompts'),
|
||
|
||
# Add Ahrefs fields
|
||
migrations.AddField(
|
||
model_name='plan',
|
||
name='max_ahrefs_queries',
|
||
field=models.IntegerField(default=50, validators=[MinValueValidator(0)])
|
||
),
|
||
migrations.AddField(
|
||
model_name='account',
|
||
name='usage_ahrefs_queries',
|
||
field=models.IntegerField(default=0, validators=[MinValueValidator(0)])
|
||
),
|
||
]
|
||
```
|
||
|
||
#### 1.2 Update LimitService
|
||
|
||
**File:** `backend/igny8_core/business/billing/services/limit_service.py`
|
||
|
||
**Remove from MONTHLY_LIMIT_MAPPINGS:**
|
||
```python
|
||
# DELETE THESE:
|
||
'content_ideas': {...}, # ❌ DELETE
|
||
'content_words': {...}, # ❌ DELETE
|
||
'images_basic': {...}, # ❌ DELETE
|
||
'images_premium': {...}, # ❌ DELETE
|
||
'image_prompts': {...}, # ❌ DELETE
|
||
```
|
||
|
||
**Keep/Add:**
|
||
```python
|
||
MONTHLY_LIMIT_MAPPINGS = {
|
||
# ONLY THIS ONE:
|
||
'ahrefs_queries': {
|
||
'plan_field': 'max_ahrefs_queries',
|
||
'usage_field': 'usage_ahrefs_queries',
|
||
'display_name': 'Ahrefs Keyword Research',
|
||
},
|
||
}
|
||
|
||
# Remove from HARD_LIMIT_MAPPINGS:
|
||
'clusters': {...}, # ❌ DELETE (or keep if needed)
|
||
```
|
||
|
||
#### 1.3 Update Serializers
|
||
|
||
**File:** `backend/igny8_core/auth/serializers.py`
|
||
|
||
```python
|
||
class PlanSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
fields = [
|
||
'id', 'name', 'slug', 'price', 'billing_cycle',
|
||
# KEEP ONLY THESE LIMITS:
|
||
'max_sites',
|
||
'max_users',
|
||
'max_keywords',
|
||
'max_ahrefs_queries', # NEW
|
||
# CREDITS:
|
||
'included_credits',
|
||
'extra_credit_price',
|
||
# REMOVE ALL OTHER max_* fields
|
||
]
|
||
```
|
||
|
||
#### 1.4 Remove Limit Checks from AI Operations
|
||
|
||
**Search and Remove:**
|
||
```bash
|
||
# Find all places checking monthly limits
|
||
grep -r "check_monthly_limit.*content_ideas" backend/
|
||
grep -r "check_monthly_limit.*content_words" backend/
|
||
grep -r "check_monthly_limit.*images" backend/
|
||
grep -r "increment_usage.*content_ideas" backend/
|
||
```
|
||
|
||
**Replace with credit checks only:**
|
||
```python
|
||
# OLD (REMOVE):
|
||
LimitService.check_monthly_limit(account, 'content_ideas', 1)
|
||
LimitService.increment_usage(account, 'content_ideas', 1)
|
||
|
||
# NEW (KEEP):
|
||
CreditService.check_credits(account, required_credits)
|
||
# Then after operation:
|
||
CreditService.deduct_credits_for_operation(...)
|
||
```
|
||
|
||
#### 1.5 Add Credit Balance Check to Automation
|
||
|
||
**File:** `backend/igny8_core/business/automation/services.py` (or similar)
|
||
|
||
```python
|
||
from igny8_core.business.billing.services.credit_service import CreditService
|
||
from igny8_core.business.billing.exceptions import InsufficientCreditsError
|
||
|
||
def run_automation(automation_config, account):
|
||
"""Run automation with credit pre-check"""
|
||
|
||
# Estimate credits needed
|
||
estimated_credits = estimate_automation_cost(automation_config)
|
||
|
||
# Check balance BEFORE starting
|
||
try:
|
||
CreditService.check_credits(account, estimated_credits)
|
||
except InsufficientCreditsError:
|
||
raise AutomationError(
|
||
f"Insufficient credits. Need {estimated_credits}, have {account.credits}. "
|
||
f"Please add credits to continue."
|
||
)
|
||
|
||
# Run automation stages
|
||
for stage in automation_config.stages:
|
||
# Each stage checks/deducts its own credits
|
||
run_stage(stage, account)
|
||
```
|
||
|
||
#### 1.6 Enforce Keywords Limit Properly
|
||
|
||
**Files to Update:**
|
||
- `backend/igny8_core/business/planning/views.py` (keyword creation)
|
||
- `backend/igny8_core/api/endpoints/seed_keywords.py` (SeedKeyword import)
|
||
|
||
```python
|
||
from igny8_core.business.billing.services.limit_service import LimitService, HardLimitExceededError
|
||
|
||
# Before creating keywords:
|
||
try:
|
||
LimitService.check_hard_limit(account, 'keywords', num_keywords_to_add)
|
||
except HardLimitExceededError as e:
|
||
return Response({
|
||
'error': 'keyword_limit_exceeded',
|
||
'message': f'You have reached your keyword limit ({plan.max_keywords}). Upgrade your plan to add more.',
|
||
'current_count': current_count,
|
||
'limit': plan.max_keywords,
|
||
'upgrade_url': '/account/plans/upgrade'
|
||
}, status=402)
|
||
|
||
# If check passes, create keywords
|
||
keywords = Keywords.objects.bulk_create([...])
|
||
```
|
||
|
||
---
|
||
|
||
### 6.2 Phase 2: Frontend Cleanup (Week 2)
|
||
|
||
#### 2.1 Remove Duplicate Data from Plans & Billing Page
|
||
|
||
**File:** `frontend/src/pages/account/PlansAndBillingPage.tsx`
|
||
|
||
**Current Plan Tab - Remove:**
|
||
```tsx
|
||
// ❌ DELETE: Credit balance display (move to Usage only)
|
||
// ❌ DELETE: Usage breakdown charts
|
||
// ❌ DELETE: Limit progress bars
|
||
// ❌ DELETE: "Credits used this month" details
|
||
```
|
||
|
||
**Current Plan Tab - Keep:**
|
||
```tsx
|
||
// ✅ KEEP: Plan name, price, billing cycle
|
||
// ✅ KEEP: Renewal date
|
||
// ✅ KEEP: Brief summary text: "50 Pages/Articles • 2 Sites • 2 Users"
|
||
// ✅ KEEP: Upgrade CTA button
|
||
```
|
||
|
||
#### 2.2 Redesign Usage Page with Multi-Dimensional Insights
|
||
|
||
**File:** `frontend/src/pages/account/UsageAnalyticsPage.tsx`
|
||
|
||
**New Structure:**
|
||
|
||
```tsx
|
||
/**
|
||
* Usage & Insights Page - Complete Redesign
|
||
*
|
||
* Tab 1: Overview (NEW)
|
||
* - Credit balance cards
|
||
* - Quick stats (sites, users, keywords count)
|
||
* - Period selector (7, 30, 90 days)
|
||
* - Top metrics
|
||
*
|
||
* Tab 2: Your Limits
|
||
* - Sites, Users, Keywords progress bars
|
||
* - Ahrefs queries remaining
|
||
*
|
||
* Tab 3: Credit Insights (NEW)
|
||
* - Credits by Site
|
||
* - Credits by Content Type (articles, images, etc.)
|
||
* - Credits by Image Quality (basic, quality, premium)
|
||
* - Credits by Automation
|
||
* - Timeline chart (7/30/90 days)
|
||
*
|
||
* Tab 4: Activity Log
|
||
* - Detailed transaction history
|
||
*/
|
||
|
||
type TabType = 'overview' | 'limits' | 'insights' | 'activity';
|
||
```
|
||
|
||
**Tab 1: Overview (NEW)**
|
||
```tsx
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||
{/* Credit Balance Card */}
|
||
<Card>
|
||
<ZapIcon />
|
||
<div>Credits Available</div>
|
||
<div className="text-3xl">{credits.toLocaleString()}</div>
|
||
</Card>
|
||
|
||
{/* Sites Used Card */}
|
||
<Card>
|
||
<GlobeIcon />
|
||
<div>Sites</div>
|
||
<div className="text-3xl">{sitesCount} / {maxSites}</div>
|
||
</Card>
|
||
|
||
{/* Team Members Card */}
|
||
<Card>
|
||
<UsersIcon />
|
||
<div>Team Members</div>
|
||
<div className="text-3xl">{usersCount} / {maxUsers}</div>
|
||
</Card>
|
||
|
||
{/* Keywords Card */}
|
||
<Card>
|
||
<TagIcon />
|
||
<div>Keywords</div>
|
||
<div className="text-3xl">{keywordsCount.toLocaleString()} / {maxKeywords.toLocaleString()}</div>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Period Selector */}
|
||
<div className="flex gap-2">
|
||
<Button onClick={() => setPeriod(7)}>Last 7 Days</Button>
|
||
<Button onClick={() => setPeriod(30)}>Last 30 Days</Button>
|
||
<Button onClick={() => setPeriod(90)}>Last 90 Days</Button>
|
||
</div>
|
||
```
|
||
|
||
**Tab 3: Credit Insights (NEW)**
|
||
```tsx
|
||
{/* Credits by Site */}
|
||
<Card>
|
||
<h3>Credits by Site</h3>
|
||
{sites.map(site => (
|
||
<div key={site.id}>
|
||
<div>{site.name}</div>
|
||
<ProgressBar value={site.credits_used} max={totalCredits} />
|
||
<div>{site.credits_used.toLocaleString()} credits</div>
|
||
</div>
|
||
))}
|
||
</Card>
|
||
|
||
{/* Credits by Content Type */}
|
||
<Card>
|
||
<h3>Credits by Content Type</h3>
|
||
<DonutChart data={[
|
||
{ label: 'Articles', value: contentCredits },
|
||
{ label: 'Images', value: imageCredits },
|
||
{ label: 'Ideas & Planning', value: ideaCredits },
|
||
{ label: 'Other', value: otherCredits },
|
||
]} />
|
||
</Card>
|
||
|
||
{/* Credits by Image Quality */}
|
||
<Card>
|
||
<h3>Image Generation by Quality</h3>
|
||
{imageQualityBreakdown.map(tier => (
|
||
<div key={tier.quality}>
|
||
<div>{tier.quality} Quality</div>
|
||
<div>{tier.count} images • {tier.credits_used} credits</div>
|
||
<div className="text-xs">({tier.credits_per_image} credits each)</div>
|
||
</div>
|
||
))}
|
||
</Card>
|
||
|
||
{/* Credits by Automation */}
|
||
<Card>
|
||
<h3>Automation Runs</h3>
|
||
{automations.map(auto => (
|
||
<div key={auto.id}>
|
||
<div>{auto.name}</div>
|
||
<div>{auto.runs_count} runs • {auto.credits_used} credits</div>
|
||
</div>
|
||
))}
|
||
</Card>
|
||
|
||
{/* Timeline Chart */}
|
||
<Card>
|
||
<h3>Credit Usage Over Time</h3>
|
||
<LineChart
|
||
data={usageTimeline}
|
||
xAxis="date"
|
||
yAxis="credits_used"
|
||
/>
|
||
</Card>
|
||
```
|
||
|
||
#### 2.3 Update API Interfaces
|
||
|
||
**File:** `frontend/src/services/billing.api.ts`
|
||
|
||
```typescript
|
||
// REMOVE from Plan interface:
|
||
export interface Plan {
|
||
id: number;
|
||
name: string;
|
||
price: number;
|
||
|
||
// KEEP ONLY THESE LIMITS:
|
||
max_sites?: number;
|
||
max_users?: number;
|
||
max_keywords?: number;
|
||
max_ahrefs_queries?: number; // NEW
|
||
|
||
included_credits?: number;
|
||
|
||
// REMOVE THESE:
|
||
// max_content_ideas?: number; // ❌ DELETE
|
||
// max_content_words?: number; // ❌ DELETE
|
||
// max_images_basic?: number; // ❌ DELETE
|
||
// max_images_premium?: number; // ❌ DELETE
|
||
// max_image_prompts?: number; // ❌ DELETE
|
||
// max_clusters?: number; // ❌ DELETE
|
||
}
|
||
|
||
// UPDATE UsageSummary interface:
|
||
export interface UsageSummary {
|
||
account_id: number;
|
||
plan_name: string;
|
||
period_start: string;
|
||
period_end: string;
|
||
|
||
hard_limits: {
|
||
sites?: LimitUsage;
|
||
users?: LimitUsage;
|
||
keywords?: LimitUsage;
|
||
// clusters?: LimitUsage; // ❌ DELETE
|
||
};
|
||
|
||
monthly_limits: {
|
||
ahrefs_queries?: LimitUsage; // ONLY THIS ONE
|
||
// content_ideas?: LimitUsage; // ❌ DELETE
|
||
// content_words?: LimitUsage; // ❌ DELETE
|
||
// images_basic?: LimitUsage; // ❌ DELETE
|
||
// images_premium?: LimitUsage; // ❌ DELETE
|
||
// image_prompts?: LimitUsage; // ❌ DELETE
|
||
};
|
||
}
|
||
|
||
// NEW: Multi-dimensional insights
|
||
export interface CreditInsights {
|
||
period_days: number;
|
||
total_credits_used: number;
|
||
|
||
by_site: Array<{
|
||
site_id: number;
|
||
site_name: string;
|
||
credits_used: number;
|
||
percentage: number;
|
||
}>;
|
||
|
||
by_operation: Array<{
|
||
operation_type: string;
|
||
display_name: string;
|
||
credits_used: number;
|
||
count: number;
|
||
percentage: number;
|
||
}>;
|
||
|
||
by_image_quality: Array<{
|
||
quality_tier: 'basic' | 'quality' | 'premium';
|
||
model_name: string;
|
||
credits_per_image: number;
|
||
images_generated: number;
|
||
total_credits: number;
|
||
}>;
|
||
|
||
by_automation: Array<{
|
||
automation_id: number;
|
||
automation_name: string;
|
||
runs_count: number;
|
||
credits_used: number;
|
||
}>;
|
||
|
||
timeline: Array<{
|
||
date: string;
|
||
credits_used: number;
|
||
}>;
|
||
}
|
||
|
||
// NEW API function
|
||
export async function getCreditInsights(days: number = 30): Promise<CreditInsights> {
|
||
return fetchAPI(`/v1/billing/credits/insights/?days=${days}`);
|
||
}
|
||
```
|
||
|
||
#### 2.4 Update UsageLimitsPanel Component
|
||
|
||
**File:** `frontend/src/components/billing/UsageLimitsPanel.tsx`
|
||
|
||
```tsx
|
||
// REMOVE monthly limit configs for deleted fields
|
||
const monthlyLimitConfig = {
|
||
// ONLY THIS ONE:
|
||
ahrefs_queries: {
|
||
icon: <SearchIcon className="w-5 h-5" />,
|
||
color: 'purple' as const,
|
||
description: 'Keyword research queries per month'
|
||
},
|
||
|
||
// DELETE THESE:
|
||
// content_ideas: {...}, // ❌ DELETE
|
||
// content_words: {...}, // ❌ DELETE
|
||
// images_basic: {...}, // ❌ DELETE
|
||
// images_premium: {...}, // ❌ DELETE
|
||
// image_prompts: {...}, // ❌ DELETE
|
||
};
|
||
|
||
// KEEP hard limits
|
||
const hardLimitConfig = {
|
||
sites: { icon: <GlobeIcon />, color: 'success' },
|
||
users: { icon: <UsersIcon />, color: 'info' },
|
||
keywords: { icon: <TagIcon />, color: 'purple' },
|
||
// clusters: {...}, // ❌ DELETE if not needed
|
||
};
|
||
```
|
||
|
||
#### 2.5 Use User-Friendly Terminology
|
||
|
||
**Global Search & Replace:**
|
||
|
||
| Technical Term | User-Friendly Term |
|
||
|---------------|-------------------|
|
||
| "API Activity" | "Activity Log" or "Recent Actions" |
|
||
| "API Operations" | "Actions" or "Activities" |
|
||
| "operation_type" | "Action Type" |
|
||
| "Monthly Limits" | "Monthly Allowances" |
|
||
| "Hard Limits" | "Plan Limits" |
|
||
| "Credits Used" | "Credits Spent" |
|
||
| "Balance" | "Credits Available" |
|
||
|
||
---
|
||
|
||
### 6.3 Phase 3: Keyword Research Implementation (Week 3)
|
||
|
||
#### 3.1 Backend: Ahrefs Service
|
||
|
||
**Create:** `backend/igny8_core/business/keywords/ahrefs_service.py`
|
||
|
||
```python
|
||
from igny8_core.business.billing.services.limit_service import LimitService, MonthlyLimitExceededError
|
||
import requests
|
||
|
||
class AhrefsService:
|
||
"""Service for Ahrefs keyword research with monthly limit enforcement"""
|
||
|
||
@staticmethod
|
||
def query_keywords(account, query_params):
|
||
"""
|
||
Query Ahrefs for keywords with limit enforcement.
|
||
|
||
Args:
|
||
account: Account instance
|
||
query_params: dict with search parameters
|
||
|
||
Returns:
|
||
dict: Ahrefs API response with keyword data
|
||
|
||
Raises:
|
||
MonthlyLimitExceededError: If monthly Ahrefs query limit exceeded
|
||
"""
|
||
# Check monthly limit BEFORE querying
|
||
try:
|
||
LimitService.check_monthly_limit(account, 'ahrefs_queries', 1)
|
||
except MonthlyLimitExceededError as e:
|
||
raise AhrefsQueryLimitExceeded(
|
||
f"You've used all your Ahrefs queries this month. "
|
||
f"Limit: {account.plan.max_ahrefs_queries}. "
|
||
f"Resets on {account.usage_period_end.strftime('%B %d, %Y')}. "
|
||
f"Upgrade your plan for more queries."
|
||
)
|
||
|
||
# Make Ahrefs API call
|
||
try:
|
||
response = requests.post(
|
||
'https://api.ahrefs.com/v3/site-explorer/keywords',
|
||
headers={'Authorization': f'Bearer {settings.AHREFS_API_KEY}'},
|
||
json=query_params
|
||
)
|
||
response.raise_for_status()
|
||
results = response.json()
|
||
|
||
# Increment usage counter
|
||
LimitService.increment_usage(account, 'ahrefs_queries', 1)
|
||
|
||
return results
|
||
|
||
except requests.RequestException as e:
|
||
logger.error(f"Ahrefs API error: {e}")
|
||
raise AhrefsAPIError("Failed to fetch keyword data from Ahrefs")
|
||
```
|
||
|
||
#### 3.2 Frontend: Keyword Research Page
|
||
|
||
**Create:** `frontend/src/pages/Planner/KeywordResearchPage.tsx`
|
||
|
||
```tsx
|
||
/**
|
||
* Keyword Research Page - Two Options
|
||
* 1. Browse IGNY8 pre-researched keywords (free)
|
||
* 2. Research with Ahrefs (monthly limit)
|
||
*/
|
||
|
||
type ResearchTab = 'browse' | 'ahrefs';
|
||
|
||
export default function KeywordResearchPage() {
|
||
const [activeTab, setActiveTab] = useState<ResearchTab>('browse');
|
||
const [ahrefsLimit, setAhrefsLimit] = useState({ used: 0, limit: 50 });
|
||
|
||
return (
|
||
<>
|
||
<PageHeader
|
||
title="Keyword Research"
|
||
description="Find high-opportunity keywords for your content strategy"
|
||
/>
|
||
|
||
{/* Tab Selector */}
|
||
<div className="flex gap-4 mb-6">
|
||
<Button
|
||
variant={activeTab === 'browse' ? 'primary' : 'outline'}
|
||
onClick={() => setActiveTab('browse')}
|
||
>
|
||
<DatabaseIcon className="w-4 h-4" />
|
||
Browse Pre-Researched Keywords
|
||
</Button>
|
||
|
||
<Button
|
||
variant={activeTab === 'ahrefs' ? 'primary' : 'outline'}
|
||
onClick={() => setActiveTab('ahrefs')}
|
||
>
|
||
<SearchIcon className="w-4 h-4" />
|
||
Research with Ahrefs
|
||
<Badge variant="soft" tone="purple">
|
||
{ahrefsLimit.limit - ahrefsLimit.used} remaining
|
||
</Badge>
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Tab Content */}
|
||
{activeTab === 'browse' && (
|
||
<BrowseKeywordsPanel />
|
||
)}
|
||
|
||
{activeTab === 'ahrefs' && (
|
||
<AhrefsResearchPanel
|
||
limit={ahrefsLimit}
|
||
onQuerySuccess={() => {
|
||
setAhrefsLimit(prev => ({
|
||
...prev,
|
||
used: prev.used + 1
|
||
}));
|
||
}}
|
||
/>
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Browse Keywords Panel (Existing SeedKeywords):**
|
||
```tsx
|
||
function BrowseKeywordsPanel() {
|
||
return (
|
||
<Card>
|
||
<h3>Pre-Researched High-Opportunity Keywords</h3>
|
||
<p>Browse thousands of analyzed keywords, ready to use.</p>
|
||
|
||
{/* Filters */}
|
||
<div className="flex gap-4">
|
||
<Select label="Industry" options={industries} />
|
||
<Select label="Sector" options={sectors} />
|
||
<Select label="Country" options={countries} />
|
||
<Input placeholder="Search keywords..." />
|
||
</div>
|
||
|
||
{/* Results Table */}
|
||
<Table>
|
||
<thead>
|
||
<tr>
|
||
<th>Keyword</th>
|
||
<th>Search Volume</th>
|
||
<th>Difficulty</th>
|
||
<th>Opportunity Score</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{keywords.map(kw => (
|
||
<tr key={kw.id}>
|
||
<td>{kw.keyword}</td>
|
||
<td>{kw.search_volume}</td>
|
||
<td><Badge>{kw.difficulty}</Badge></td>
|
||
<td>{kw.opportunity_score}/100</td>
|
||
<td>
|
||
<Button size="sm" onClick={() => addToWorkspace(kw)}>
|
||
Add to Workspace
|
||
</Button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</Table>
|
||
</Card>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Ahrefs Research Panel (NEW):**
|
||
```tsx
|
||
function AhrefsResearchPanel({ limit, onQuerySuccess }) {
|
||
const [query, setQuery] = useState('');
|
||
const [results, setResults] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
const remaining = limit.limit - limit.used;
|
||
const canQuery = remaining > 0;
|
||
|
||
const handleSearch = async () => {
|
||
if (!canQuery) {
|
||
toast.error('You have used all your Ahrefs queries this month.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setLoading(true);
|
||
const data = await fetchAPI('/v1/keywords/ahrefs/search/', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ query })
|
||
});
|
||
setResults(data.keywords);
|
||
onQuerySuccess();
|
||
toast.success('Keywords fetched from Ahrefs!');
|
||
} catch (error) {
|
||
toast.error(error.message);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Card>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3>Live Keyword Research with Ahrefs</h3>
|
||
<Badge variant="soft" tone={remaining > 10 ? 'success' : 'warning'}>
|
||
{remaining} / {limit.limit} queries remaining
|
||
</Badge>
|
||
</div>
|
||
|
||
{!canQuery && (
|
||
<Alert tone="warning">
|
||
<AlertCircleIcon />
|
||
<div>
|
||
<div>No queries remaining</div>
|
||
<div>Your limit resets on {resetDate}. Upgrade for more queries.</div>
|
||
<Button size="sm" onClick={() => navigate('/account/plans/upgrade')}>
|
||
Upgrade Plan
|
||
</Button>
|
||
</div>
|
||
</Alert>
|
||
)}
|
||
|
||
{canQuery && (
|
||
<>
|
||
<div className="flex gap-4 mb-4">
|
||
<Input
|
||
placeholder="Enter seed keyword (e.g., 'digital marketing')"
|
||
value={query}
|
||
onChange={e => setQuery(e.target.value)}
|
||
onKeyPress={e => e.key === 'Enter' && handleSearch()}
|
||
/>
|
||
<Button onClick={handleSearch} loading={loading}>
|
||
<SearchIcon /> Research Keywords
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Results */}
|
||
{results.length > 0 && (
|
||
<Table>
|
||
<thead>
|
||
<tr>
|
||
<th>Keyword</th>
|
||
<th>Volume</th>
|
||
<th>Difficulty</th>
|
||
<th>CPC</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{results.map((kw, idx) => (
|
||
<tr key={idx}>
|
||
<td>{kw.keyword}</td>
|
||
<td>{kw.volume.toLocaleString()}</td>
|
||
<td><Badge>{kw.difficulty}</Badge></td>
|
||
<td>${kw.cpc}</td>
|
||
<td>
|
||
<Button size="sm" onClick={() => addToWorkspace(kw)}>
|
||
Add to Workspace
|
||
</Button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</Table>
|
||
)}
|
||
</>
|
||
)}
|
||
</Card>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 6.4 Phase 4: Validation & Enforcement (Week 4)
|
||
|
||
#### 4.1 Credit Balance Validation Checklist
|
||
|
||
**File:** Search all AI operation services
|
||
|
||
```python
|
||
# REQUIRED PATTERN for all AI operations:
|
||
|
||
# 1. BEFORE operation - estimate and check
|
||
estimated_credits = estimate_credits_needed(operation_params)
|
||
CreditService.check_credits(account, estimated_credits)
|
||
|
||
# 2. EXECUTE operation
|
||
result = ai_service.execute(operation_params)
|
||
|
||
# 3. AFTER operation - calculate actual and deduct
|
||
actual_tokens = result.usage.total_tokens
|
||
actual_credits = CreditService.calculate_credits_from_tokens(
|
||
operation_type='content_generation',
|
||
tokens_input=result.usage.prompt_tokens,
|
||
tokens_output=result.usage.completion_tokens
|
||
)
|
||
CreditService.deduct_credits_for_operation(
|
||
account=account,
|
||
operation_type='content_generation',
|
||
amount=actual_credits,
|
||
model=result.model,
|
||
tokens_in=result.usage.prompt_tokens,
|
||
tokens_out=result.usage.completion_tokens,
|
||
metadata={'content_id': content.id}
|
||
)
|
||
```
|
||
|
||
**Locations to Verify:**
|
||
- ✅ Content generation service
|
||
- ✅ Image generation service
|
||
- ✅ Idea generation service
|
||
- ✅ Clustering service
|
||
- ✅ Internal linking service
|
||
- ✅ Optimization service
|
||
- ✅ Automation runner (PRE-CHECK before starting!)
|
||
|
||
#### 4.2 Automation Credit Pre-Check
|
||
|
||
**File:** `backend/igny8_core/business/automation/services.py`
|
||
|
||
```python
|
||
def estimate_automation_cost(automation_config):
|
||
"""Estimate credits needed for full automation run"""
|
||
estimated = 0
|
||
|
||
for stage in automation_config.stages:
|
||
if stage.type == 'clustering':
|
||
estimated += 10 # Base estimate
|
||
elif stage.type == 'idea_generation':
|
||
estimated += stage.num_ideas * 2
|
||
elif stage.type == 'content_generation':
|
||
estimated += stage.num_articles * 50 # Rough estimate
|
||
elif stage.type == 'image_generation':
|
||
estimated += stage.num_images * stage.credits_per_image
|
||
elif stage.type == 'linking':
|
||
estimated += stage.num_articles * 8
|
||
|
||
# Add 20% buffer
|
||
return int(estimated * 1.2)
|
||
|
||
def run_automation(automation_config, account, trigger='manual'):
|
||
"""
|
||
Run automation with upfront credit validation.
|
||
|
||
Raises:
|
||
InsufficientCreditsError: If not enough credits available
|
||
"""
|
||
# Estimate total cost
|
||
estimated_credits = estimate_automation_cost(automation_config)
|
||
|
||
# CHECK CREDITS BEFORE STARTING
|
||
try:
|
||
CreditService.check_credits(account, estimated_credits)
|
||
except InsufficientCreditsError:
|
||
# Log failed attempt
|
||
AutomationRun.objects.create(
|
||
automation=automation_config,
|
||
account=account,
|
||
status='failed',
|
||
error_message=f'Insufficient credits. Need {estimated_credits}, have {account.credits}.',
|
||
trigger=trigger
|
||
)
|
||
raise
|
||
|
||
# Create run record
|
||
run = AutomationRun.objects.create(
|
||
automation=automation_config,
|
||
account=account,
|
||
status='running',
|
||
estimated_credits=estimated_credits,
|
||
trigger=trigger
|
||
)
|
||
|
||
try:
|
||
# Execute stages (each deducts credits)
|
||
for stage in automation_config.stages:
|
||
execute_stage(stage, account, run)
|
||
|
||
# Mark complete
|
||
run.status = 'completed'
|
||
run.actual_credits = run.credits_used
|
||
run.save()
|
||
|
||
except Exception as e:
|
||
run.status = 'failed'
|
||
run.error_message = str(e)
|
||
run.save()
|
||
raise
|
||
```
|
||
|
||
#### 4.3 Frontend Credit Check Before Actions
|
||
|
||
**Component:** Add to all credit-consuming actions
|
||
|
||
```tsx
|
||
// Hook for credit validation
|
||
function useCreditCheck() {
|
||
const { user } = useAuthStore();
|
||
|
||
const checkCredits = useCallback(async (estimatedCredits: number) => {
|
||
if (user.account.credits < estimatedCredits) {
|
||
const confirmed = await showConfirmDialog({
|
||
title: 'Insufficient Credits',
|
||
message: `This action requires approximately ${estimatedCredits} credits, but you only have ${user.account.credits} available.`,
|
||
confirmText: 'Add Credits',
|
||
cancelText: 'Cancel',
|
||
});
|
||
|
||
if (confirmed) {
|
||
navigate('/account/plans');
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}, [user]);
|
||
|
||
return { checkCredits };
|
||
}
|
||
|
||
// Usage example:
|
||
function GenerateContentButton({ taskId }) {
|
||
const { checkCredits } = useCreditCheck();
|
||
|
||
const handleGenerate = async () => {
|
||
// Estimate credits (rough estimate: 50 for article)
|
||
const canProceed = await checkCredits(50);
|
||
if (!canProceed) return;
|
||
|
||
// Proceed with generation
|
||
await generateContent(taskId);
|
||
};
|
||
|
||
return <Button onClick={handleGenerate}>Generate Content</Button>;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 6.5 Phase 5: Testing & Validation (Week 5)
|
||
|
||
#### Test Cases
|
||
|
||
**Limit Enforcement:**
|
||
- [ ] Try to create site when at `max_sites` → Should fail with upgrade prompt
|
||
- [ ] Try to invite user when at `max_users` → Should fail with upgrade prompt
|
||
- [ ] Try to add 100 keywords when 50 slots remain and limit is 1000 → Should succeed
|
||
- [ ] Try to add 100 keywords when 30 slots remain → Should fail
|
||
- [ ] Try to query Ahrefs when at monthly limit → Should fail with reset date
|
||
- [ ] Query Ahrefs successfully → Counter increments
|
||
- [ ] Monthly reset → Ahrefs counter resets to 0
|
||
|
||
**Credit Validation:**
|
||
- [ ] Try to generate content with 0 credits → Should fail immediately
|
||
- [ ] Try to run automation with insufficient credits → Should fail before starting
|
||
- [ ] Generate content → Credits deducted correctly based on tokens
|
||
- [ ] Generate image (basic) → 1 credit deducted
|
||
- [ ] Generate image (quality) → 5 credits deducted
|
||
- [ ] Generate image (premium) → 15 credits deducted
|
||
- [ ] Run automation → All stages check/deduct credits properly
|
||
|
||
**Page Reorganization:**
|
||
- [ ] Plans & Billing → Current Plan: NO credit usage details
|
||
- [ ] Plans & Billing → Current Plan: Brief summary text only
|
||
- [ ] Usage → Overview: Shows credit balance, sites/users/keywords count
|
||
- [ ] Usage → Limits: Shows only 4 limits (sites, users, keywords, ahrefs)
|
||
- [ ] Usage → Insights: Shows multi-dimensional breakdowns
|
||
- [ ] No duplicate data between Plans & Usage
|
||
|
||
**Terminology:**
|
||
- [ ] No "API" references in user-facing text
|
||
- [ ] "Operations" changed to "Actions" or "Activities"
|
||
- [ ] User-friendly language throughout
|
||
|
||
---
|
||
|
||
## 7. Updated Page Reorganization
|
||
|
||
### 7.1 Page Reorganization (High Priority) 🔥
|
||
|
||
**Problem:** Data duplication creates confusion and maintenance overhead.
|
||
|
||
**Proposed Solution:** Clear separation of concerns between Plans & Billing vs Usage.
|
||
|
||
#### Plans & Billing → Focus on FINANCIAL aspects
|
||
|
||
**Tab 1: Current Plan**
|
||
- Show: Plan name, price, billing cycle, renewal date
|
||
- Show: Brief limits summary (e.g., "50 Pages/Articles, 2 Sites, 2 Users")
|
||
- **Remove:** Detailed limit progress bars (move to Usage)
|
||
- **Remove:** Credits used this month breakdown (move to Usage)
|
||
- Action: "Upgrade Plan" button
|
||
|
||
**Tab 2: Upgrade Plan**
|
||
- Show: Pricing table with plan comparison
|
||
- Show: Plan features and limits (static, for comparison)
|
||
- Action: Purchase/upgrade plan
|
||
|
||
**Tab 3: Billing History**
|
||
- Show: Invoices (with download PDF)
|
||
- Show: Payment methods management
|
||
- Show: Credit package purchases (financial transactions only)
|
||
|
||
#### Usage → Focus on CONSUMPTION tracking
|
||
|
||
**Tab 1: Limits & Usage** _(Keep as is - this is perfect)_
|
||
- Show: All hard limits with progress bars (sites, users, keywords, clusters)
|
||
- Show: All monthly limits with progress bars (ideas, words, images)
|
||
- Show: Days until reset for monthly limits
|
||
- Show: Credit balance and monthly usage
|
||
- Action: "Upgrade for more" CTA when approaching limits
|
||
|
||
**Tab 2: Credit History** _(Keep as is)_
|
||
- Show: Credit transaction log (purchases, deductions, adjustments)
|
||
- Show: Operation details (what consumed credits)
|
||
- Filter: By operation type, date range
|
||
|
||
**Tab 3: Activity Log** _(Keep as is)_
|
||
- Show: API operations by type
|
||
- Show: Total operations count
|
||
- Show: Breakdown by operation type
|
||
|
||
#### Summary of Changes
|
||
|
||
| Page | Current State | Proposed Change |
|
||
|------|--------------|-----------------|
|
||
| **Plans & Billing → Current Plan** | Shows credits, usage, limits | Remove detailed usage, keep financial summary only |
|
||
| **Plans & Billing → History** | Invoices and payments | Keep as is (financial focus) |
|
||
| **Usage → Limits & Usage** | Detailed limits panel | Keep as is (consumption focus) |
|
||
| **Usage → Credit History** | Transaction log | Keep as is (consumption focus) |
|
||
|
||
**Rationale:**
|
||
- **Plans & Billing** = "What am I paying for?" (financial/subscription management)
|
||
- **Usage** = "What am I using?" (consumption monitoring)
|
||
|
||
---
|
||
|
||
### 7.1 Plans & Billing Page (Simplified)
|
||
|
||
**Purpose:** Financial management and subscription control
|
||
|
||
**Tab 1: Current Plan**
|
||
```tsx
|
||
<Card>
|
||
<h2>{plan.name} Plan</h2>
|
||
<div className="text-3xl">${plan.price}/month</div>
|
||
<div>Renews on {renewalDate}</div>
|
||
|
||
{/* Brief Summary - NO detailed limits */}
|
||
<div className="text-sm text-gray-600">
|
||
Your plan includes: {plan.included_credits} credits per month •
|
||
{plan.max_sites} sites • {plan.max_users} team members
|
||
</div>
|
||
|
||
{/* Upgrade CTA */}
|
||
<Button onClick={() => navigate('/account/plans/upgrade')}>
|
||
Upgrade Plan
|
||
</Button>
|
||
</Card>
|
||
|
||
{/* ❌ REMOVE: Credit usage charts */}
|
||
{/* ❌ REMOVE: Limit progress bars */}
|
||
{/* ❌ REMOVE: "Credits used this month" */}
|
||
```
|
||
|
||
**Tab 2: Upgrade Plan** _(No changes needed)_
|
||
|
||
**Tab 3: Billing History** _(No changes needed)_
|
||
|
||
---
|
||
|
||
### 7.2 Usage Page (Multi-Dimensional Insights)
|
||
|
||
**Purpose:** Monitor consumption and optimize usage
|
||
|
||
**Tab 1: Overview (NEW)**
|
||
```tsx
|
||
{/* Quick Stats Cards */}
|
||
<div className="grid grid-cols-4 gap-4">
|
||
<StatCard
|
||
icon={<ZapIcon />}
|
||
label="Credits Available"
|
||
value={credits.toLocaleString()}
|
||
color="brand"
|
||
/>
|
||
<StatCard
|
||
icon={<GlobeIcon />}
|
||
label="Sites"
|
||
value={`${sitesCount} / ${maxSites}`}
|
||
color="success"
|
||
/>
|
||
<StatCard
|
||
icon={<UsersIcon />}
|
||
label="Team Members"
|
||
value={`${usersCount} / ${maxUsers}`}
|
||
color="info"
|
||
/>
|
||
<StatCard
|
||
icon={<TagIcon />}
|
||
label="Keywords"
|
||
value={`${keywordsCount} / ${maxKeywords}`}
|
||
color="purple"
|
||
/>
|
||
</div>
|
||
|
||
{/* Period Selector */}
|
||
<ButtonGroup>
|
||
<Button onClick={() => setPeriod(7)}>Last 7 Days</Button>
|
||
<Button onClick={() => setPeriod(30)}>Last 30 Days</Button>
|
||
<Button onClick={() => setPeriod(90)}>Last 90 Days</Button>
|
||
</ButtonGroup>
|
||
|
||
{/* Top Metrics for Selected Period */}
|
||
<div className="grid grid-cols-3 gap-4">
|
||
<Card>
|
||
<div>Credits Spent</div>
|
||
<div className="text-2xl">{periodCredits.toLocaleString()}</div>
|
||
</Card>
|
||
<Card>
|
||
<div>Articles Created</div>
|
||
<div className="text-2xl">{periodArticles}</div>
|
||
</Card>
|
||
<Card>
|
||
<div>Images Generated</div>
|
||
<div className="text-2xl">{periodImages}</div>
|
||
</Card>
|
||
</div>
|
||
```
|
||
|
||
**Tab 2: Your Limits**
|
||
```tsx
|
||
{/* Only 4 limits total */}
|
||
<LimitCard
|
||
title="Sites"
|
||
icon={<GlobeIcon />}
|
||
current={sitesCount}
|
||
limit={maxSites}
|
||
type="permanent"
|
||
/>
|
||
|
||
<LimitCard
|
||
title="Team Members"
|
||
icon={<UsersIcon />}
|
||
current={usersCount}
|
||
limit={maxUsers}
|
||
type="permanent"
|
||
/>
|
||
|
||
<LimitCard
|
||
title="Keywords in Workspace"
|
||
icon={<TagIcon />}
|
||
current={keywordsCount}
|
||
limit={maxKeywords}
|
||
type="permanent"
|
||
/>
|
||
|
||
<LimitCard
|
||
title="Ahrefs Research Queries"
|
||
icon={<SearchIcon />}
|
||
current={ahrefsUsed}
|
||
limit={ahrefsLimit}
|
||
type="monthly"
|
||
daysUntilReset={daysUntilReset}
|
||
/>
|
||
```
|
||
|
||
**Tab 3: Credit Insights (NEW)**
|
||
```tsx
|
||
{/* Multi-dimensional breakdowns */}
|
||
|
||
{/* By Site */}
|
||
<Card>
|
||
<h3>Credits by Site</h3>
|
||
<div>See which sites consume the most credits</div>
|
||
{insights.by_site.map(site => (
|
||
<div key={site.id}>
|
||
<div className="flex justify-between">
|
||
<span>{site.name}</span>
|
||
<span>{site.credits_used.toLocaleString()} credits ({site.percentage}%)</span>
|
||
</div>
|
||
<ProgressBar value={site.percentage} />
|
||
</div>
|
||
))}
|
||
</Card>
|
||
|
||
{/* By Action Type */}
|
||
<Card>
|
||
<h3>Credits by Action Type</h3>
|
||
<DonutChart data={insights.by_operation} />
|
||
<div>
|
||
{insights.by_operation.map(op => (
|
||
<div key={op.type}>
|
||
<span>{op.display_name}</span>
|
||
<span>{op.credits_used} credits ({op.count} times)</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* By Image Quality */}
|
||
<Card>
|
||
<h3>Image Generation Breakdown</h3>
|
||
<div>Credits vary by quality tier</div>
|
||
{insights.by_image_quality.map(tier => (
|
||
<div key={tier.quality_tier}>
|
||
<div>{tier.quality_tier} Quality</div>
|
||
<div>{tier.images_generated} images × {tier.credits_per_image} credits = {tier.total_credits} credits</div>
|
||
<Badge>{tier.model_name}</Badge>
|
||
</div>
|
||
))}
|
||
</Card>
|
||
|
||
{/* By Automation */}
|
||
<Card>
|
||
<h3>Automation Runs</h3>
|
||
{insights.by_automation.map(auto => (
|
||
<div key={auto.id}>
|
||
<div>{auto.name}</div>
|
||
<div>{auto.runs_count} runs • {auto.credits_used} credits total</div>
|
||
</div>
|
||
))}
|
||
</Card>
|
||
|
||
{/* Timeline Chart */}
|
||
<Card>
|
||
<h3>Credit Usage Over Time</h3>
|
||
<LineChart
|
||
data={insights.timeline}
|
||
xAxis="date"
|
||
yAxis="credits_used"
|
||
/>
|
||
</Card>
|
||
```
|
||
|
||
**Tab 4: Activity Log** _(Keep existing, but rename from "API Activity")_
|
||
|
||
---
|
||
|
||
## 8. Backend Changes Summary
|
||
|
||
### 8.1 Files to Modify
|
||
|
||
| File | Action | Description |
|
||
|------|--------|-------------|
|
||
| `backend/igny8_core/auth/models.py` | Edit | Remove unused limit fields from Plan & Account |
|
||
| `backend/igny8_core/auth/migrations/0XXX_*.py` | Create | Migration to remove fields |
|
||
| `backend/igny8_core/business/billing/services/limit_service.py` | Edit | Remove unused limit mappings |
|
||
| `backend/igny8_core/auth/serializers.py` | Edit | Remove fields from PlanSerializer |
|
||
| `backend/igny8_core/business/keywords/ahrefs_service.py` | Create | New service for Ahrefs integration |
|
||
| `backend/igny8_core/business/automation/services.py` | Edit | Add credit pre-check |
|
||
| `backend/igny8_core/business/planning/views.py` | Edit | Add keyword limit enforcement |
|
||
| `backend/igny8_core/modules/billing/views.py` | Edit | Add credit insights endpoint |
|
||
|
||
### 8.2 New API Endpoints
|
||
|
||
```python
|
||
# Credit Insights (NEW)
|
||
GET /api/v1/billing/credits/insights/?days=30
|
||
Response: CreditInsights object with multi-dimensional breakdowns
|
||
|
||
# Ahrefs Search (NEW)
|
||
POST /api/v1/keywords/ahrefs/search/
|
||
Body: { "query": "digital marketing", "country": "us" }
|
||
Response: { "keywords": [...], "queries_remaining": 42 }
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Frontend Changes Summary
|
||
|
||
### 9.1 Files to Modify
|
||
|
||
| File | Action | Description |
|
||
|------|--------|-------------|
|
||
| `frontend/src/pages/account/PlansAndBillingPage.tsx` | Edit | Remove credit usage details from Current Plan tab |
|
||
| `frontend/src/pages/account/UsageAnalyticsPage.tsx` | Rewrite | Add Overview & Credit Insights tabs |
|
||
| `frontend/src/components/billing/UsageLimitsPanel.tsx` | Edit | Remove unused limits, add Ahrefs |
|
||
| `frontend/src/services/billing.api.ts` | Edit | Remove unused fields, add new interfaces |
|
||
| `frontend/src/pages/Planner/KeywordResearchPage.tsx` | Create | New page for keyword research |
|
||
| `frontend/src/components/keywords/AhrefsResearchPanel.tsx` | Create | Ahrefs search component |
|
||
|
||
### 9.2 New Components
|
||
|
||
```tsx
|
||
// Credit Insights Components
|
||
CreditInsightsDashboard.tsx
|
||
CreditsBySiteWidget.tsx
|
||
CreditsByOperationWidget.tsx
|
||
CreditsByImageQualityWidget.tsx
|
||
CreditsByAutomationWidget.tsx
|
||
CreditTimelineChart.tsx
|
||
|
||
// Ahrefs Research
|
||
KeywordResearchPage.tsx
|
||
AhrefsResearchPanel.tsx
|
||
BrowseKeywordsPanel.tsx
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Testing Checklist
|
||
|
||
**Problem:** Keywords have `max_keywords` limit defined but enforcement is inconsistent.
|
||
|
||
**Required Changes:**
|
||
|
||
1. **Add Pre-Create Checks:**
|
||
|
||
Location: `backend/igny8_core/business/planning/views.py` (or wherever keywords are created)
|
||
```python
|
||
# Before creating keywords
|
||
from igny8_core.business.billing.services.limit_service import LimitService
|
||
|
||
def create_keywords(account, keyword_data):
|
||
# Check if adding keywords would exceed limit
|
||
num_new_keywords = len(keyword_data)
|
||
LimitService.check_hard_limit(account, 'keywords', num_new_keywords)
|
||
|
||
# If check passes, create keywords
|
||
keywords = Keywords.objects.bulk_create([...])
|
||
|
||
return keywords
|
||
```
|
||
|
||
2. **Add Check to SeedKeyword Import:**
|
||
|
||
Location: `backend/igny8_core/api/endpoints/seed_keywords.py` (or similar)
|
||
```python
|
||
# In addSeedKeywordsToWorkflow endpoint
|
||
def add_seed_keywords_to_workflow(seed_keyword_ids, site_id, sector_id):
|
||
account = Site.objects.get(id=site_id).account
|
||
|
||
# Check limit BEFORE importing
|
||
LimitService.check_hard_limit(account, 'keywords', len(seed_keyword_ids))
|
||
|
||
# Import keywords
|
||
for seed_kw in SeedKeyword.objects.filter(id__in=seed_keyword_ids):
|
||
Keywords.objects.create(...)
|
||
```
|
||
|
||
3. **Add Check to Bulk Import:**
|
||
|
||
Ensure CSV/Excel keyword imports also check limits before processing.
|
||
|
||
4. **User-Facing Error Messages:**
|
||
```python
|
||
try:
|
||
LimitService.check_hard_limit(account, 'keywords', 50)
|
||
except HardLimitExceededError as e:
|
||
return Response({
|
||
'error': 'keyword_limit_exceeded',
|
||
'message': 'You have reached your keyword limit. Upgrade your plan to add more keywords.',
|
||
'current': 950,
|
||
'limit': 1000,
|
||
'upgrade_url': '/account/plans/upgrade'
|
||
}, status=402)
|
||
```
|
||
|
||
**Testing:**
|
||
- ✅ Try to import keywords beyond limit → Should fail with clear error
|
||
- ✅ Try to create single keyword at limit → Should fail
|
||
- ✅ Try to bulk import → Should fail if total exceeds
|
||
- ✅ Error message should show current count, limit, and upgrade CTA
|
||
|
||
---
|
||
|
||
### 6.3 Implement Ahrefs Query Limit (Medium Priority)
|
||
|
||
**Problem:** Future feature needs limit definition and enforcement.
|
||
|
||
**Required Changes:**
|
||
|
||
1. **Database Migration:**
|
||
```python
|
||
# Create migration: 0XXX_add_ahrefs_query_limits.py
|
||
|
||
operations = [
|
||
migrations.AddField(
|
||
model_name='plan',
|
||
name='max_ahrefs_queries',
|
||
field=models.IntegerField(default=50, validators=[MinValueValidator(0)]),
|
||
),
|
||
migrations.AddField(
|
||
model_name='account',
|
||
name='usage_ahrefs_queries',
|
||
field=models.IntegerField(default=0, validators=[MinValueValidator(0)]),
|
||
),
|
||
]
|
||
```
|
||
|
||
2. **Update Plan Admin:**
|
||
```python
|
||
# backend/igny8_core/auth/admin.py - PlanAdmin
|
||
fieldsets = (
|
||
# ... existing fieldsets ...
|
||
('Monthly Limits (Reset on Billing Cycle)', {
|
||
'fields': (
|
||
'max_content_ideas', 'max_content_words',
|
||
'max_images_basic', 'max_images_premium', 'max_image_prompts',
|
||
'max_ahrefs_queries', # ADD THIS
|
||
),
|
||
}),
|
||
)
|
||
```
|
||
|
||
3. **Update LimitService:**
|
||
```python
|
||
# backend/igny8_core/business/billing/services/limit_service.py
|
||
MONTHLY_LIMIT_MAPPINGS = {
|
||
# ... existing ...
|
||
'ahrefs_queries': {
|
||
'plan_field': 'max_ahrefs_queries',
|
||
'usage_field': 'usage_ahrefs_queries',
|
||
'display_name': 'Ahrefs Keyword Research Queries',
|
||
},
|
||
}
|
||
```
|
||
|
||
4. **Add to Plan Serializer:**
|
||
```python
|
||
# backend/igny8_core/auth/serializers.py - PlanSerializer
|
||
class PlanSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
fields = [
|
||
# ... existing fields ...
|
||
'max_ahrefs_queries', # ADD THIS
|
||
]
|
||
```
|
||
|
||
5. **Enforce in Ahrefs Service:**
|
||
```python
|
||
# Create new service: backend/igny8_core/business/keywords/ahrefs_service.py
|
||
from igny8_core.business.billing.services.limit_service import LimitService
|
||
|
||
class AhrefsService:
|
||
@staticmethod
|
||
def query_keywords(account, query_params):
|
||
# Check monthly limit
|
||
LimitService.check_monthly_limit(account, 'ahrefs_queries', 1)
|
||
|
||
# Execute Ahrefs API call
|
||
results = ahrefs_api.search_keywords(**query_params)
|
||
|
||
# Increment usage counter
|
||
LimitService.increment_usage(account, 'ahrefs_queries', 1)
|
||
|
||
return results
|
||
```
|
||
|
||
6. **Update Frontend:**
|
||
```tsx
|
||
// frontend/src/components/billing/UsageLimitsPanel.tsx
|
||
const monthlyLimitConfig = {
|
||
// ... existing ...
|
||
ahrefs_queries: {
|
||
icon: <SearchIcon className="w-5 h-5" />,
|
||
color: 'purple' as const
|
||
},
|
||
};
|
||
```
|
||
|
||
**Plan Values (Suggested):**
|
||
|
||
| Plan | max_ahrefs_queries/month |
|
||
|------|-------------------------|
|
||
| Free | 0 (no access) |
|
||
| Starter | 50 queries |
|
||
| Growth | 200 queries |
|
||
| Scale | 500 queries |
|
||
|
||
---
|
||
|
||
### 6.4 Add Cluster Limit Enforcement (Low Priority)
|
||
|
||
**Current State:** Clusters have `max_clusters` limit but may not be consistently enforced.
|
||
|
||
**Recommendation:** Apply same enforcement pattern as keywords:
|
||
1. Check limit before creating clusters
|
||
2. Add to all cluster creation flows (auto-clustering, manual clustering)
|
||
3. User-facing error messages
|
||
|
||
---
|
||
|
||
### 6.5 Monthly Limits Reset Automation (Medium Priority)
|
||
|
||
**Current State:** Monthly limits should reset at billing cycle, but automation may not be in place.
|
||
|
||
**Check Required:**
|
||
- Is there a scheduled task that calls `LimitService.reset_monthly_limits(account)`?
|
||
- When do subscriptions renew?
|
||
- How are usage fields reset?
|
||
|
||
**Recommended:**
|
||
```python
|
||
# backend/igny8_core/business/billing/tasks.py (Celery)
|
||
from celery import shared_task
|
||
from igny8_core.auth.models import Account
|
||
from igny8_core.business.billing.services.limit_service import LimitService
|
||
|
||
@shared_task
|
||
def reset_monthly_limits_for_accounts():
|
||
"""Reset monthly limits for accounts whose billing period has ended"""
|
||
from django.utils import timezone
|
||
|
||
now = timezone.now()
|
||
accounts = Account.objects.filter(
|
||
usage_period_end__lte=now,
|
||
status='active'
|
||
)
|
||
|
||
for account in accounts:
|
||
LimitService.reset_monthly_limits(account)
|
||
logger.info(f"Reset monthly limits for account {account.id}")
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Credit System (Working Well) ✅
|
||
|
||
**No changes needed** - the credit system is well-designed:
|
||
|
||
1. **Token-Based Calculation:** Uses `AIModelConfig.tokens_per_credit` for accurate pricing
|
||
2. **Image Fixed Pricing:** Uses `AIModelConfig.credits_per_image` (1, 5, or 15)
|
||
3. **Proper Logging:** `CreditUsageLog` tracks every operation with metadata
|
||
4. **Transaction Ledger:** `CreditTransaction` maintains audit trail
|
||
5. **Balance Tracking:** Account.credits is source of truth
|
||
|
||
**Already Enforced Properly:**
|
||
- ✅ Pre-flight balance checks before operations
|
||
- ✅ Token-based credit calculation after API calls
|
||
- ✅ Proper transaction logging
|
||
- ✅ Clear error messages (402 Payment Required)
|
||
|
||
---
|
||
|
||
## 8. Summary of Required Backend Changes
|
||
|
||
### 8.1 Database Schema Changes
|
||
|
||
**Add to Plan Model:**
|
||
```python
|
||
max_ahrefs_queries = models.IntegerField(default=50, validators=[MinValueValidator(0)])
|
||
```
|
||
|
||
**Add to Account Model:**
|
||
```python
|
||
usage_ahrefs_queries = models.IntegerField(default=0, validators=[MinValueValidator(0)])
|
||
```
|
||
|
||
### 8.2 Enforcement Additions
|
||
|
||
**Locations Needing Limit Checks:**
|
||
|
||
| Location | Limit Type | Method to Add |
|
||
|----------|-----------|--------------|
|
||
| Keyword creation | Hard | `LimitService.check_hard_limit(account, 'keywords', count)` |
|
||
| SeedKeyword import | Hard | `LimitService.check_hard_limit(account, 'keywords', count)` |
|
||
| Bulk keyword import | Hard | `LimitService.check_hard_limit(account, 'keywords', count)` |
|
||
| Ahrefs query (future) | Monthly | `LimitService.check_monthly_limit(account, 'ahrefs_queries', 1)` |
|
||
| Cluster creation | Hard | `LimitService.check_hard_limit(account, 'clusters', count)` |
|
||
|
||
### 8.3 Service Updates
|
||
|
||
**LimitService Mappings:**
|
||
```python
|
||
# Add to MONTHLY_LIMIT_MAPPINGS
|
||
'ahrefs_queries': {
|
||
'plan_field': 'max_ahrefs_queries',
|
||
'usage_field': 'usage_ahrefs_queries',
|
||
'display_name': 'Ahrefs Keyword Research Queries',
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Summary of Required Frontend Changes
|
||
|
||
### 9.1 Page Content Adjustments
|
||
|
||
**Plans & Billing Page:**
|
||
```tsx
|
||
// Remove from Current Plan tab:
|
||
- ❌ Detailed credit usage breakdown (move to Usage)
|
||
- ❌ Limit progress bars (move to Usage)
|
||
|
||
// Keep in Current Plan tab:
|
||
- ✅ Plan name, price, renewal date
|
||
- ✅ Brief limits summary (text only)
|
||
- ✅ Upgrade CTA
|
||
|
||
// Keep History tab as is
|
||
```
|
||
|
||
**Usage Analytics Page:**
|
||
```tsx
|
||
// Keep all tabs as is - no changes needed
|
||
// This page is perfectly organized
|
||
```
|
||
|
||
### 9.2 Component Updates
|
||
|
||
**UsageLimitsPanel.tsx:**
|
||
```tsx
|
||
// Add Ahrefs queries to monthly limits config
|
||
const monthlyLimitConfig = {
|
||
// ... existing ...
|
||
ahrefs_queries: {
|
||
icon: <SearchIcon className="w-5 h-5" />,
|
||
color: 'purple' as const
|
||
},
|
||
};
|
||
```
|
||
|
||
**billing.api.ts:**
|
||
```typescript
|
||
// Add to Plan interface
|
||
export interface Plan {
|
||
// ... existing fields ...
|
||
max_ahrefs_queries?: number; // ADD THIS
|
||
}
|
||
|
||
// Add to UsageSummary interface
|
||
export interface UsageSummary {
|
||
// ... existing ...
|
||
monthly_limits: {
|
||
// ... existing ...
|
||
ahrefs_queries?: LimitUsage; // ADD THIS
|
||
};
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Implementation Priority
|
||
|
||
### Phase 1: Critical Fixes (Do First) 🔥
|
||
|
||
1. **Enforce Keywords Limit** (Backend)
|
||
- Add checks to keyword creation flows
|
||
- Estimated effort: 4 hours
|
||
- Impact: Prevents users from exceeding limits
|
||
|
||
2. **Page Reorganization** (Frontend)
|
||
- Remove duplicate data from Plans & Billing → Current Plan
|
||
- Estimated effort: 2 hours
|
||
- Impact: Reduces user confusion
|
||
|
||
### Phase 2: Future Features (Do When Implementing Ahrefs)
|
||
|
||
3. **Implement Ahrefs Query Limit** (Backend + Frontend)
|
||
- Database migration
|
||
- LimitService mapping
|
||
- Enforcement in Ahrefs service
|
||
- Frontend display
|
||
- Estimated effort: 6 hours
|
||
- Impact: Ready for Ahrefs integration
|
||
|
||
### Phase 3: Nice-to-Have Improvements
|
||
|
||
4. **Enforce Cluster Limit** (Backend)
|
||
- Similar to keywords enforcement
|
||
- Estimated effort: 2 hours
|
||
|
||
5. **Monthly Limits Reset Automation** (Backend)
|
||
- Celery task for auto-reset
|
||
- Estimated effort: 3 hours
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## 11. Migration Strategy
|
||
|
||
### Week-by-Week Rollout
|
||
|
||
**Week 1: Backend Foundation**
|
||
- [ ] Create database migration to remove unused fields
|
||
- [ ] Update LimitService mappings
|
||
- [ ] Update serializers
|
||
- [ ] Add Ahrefs service skeleton
|
||
- [ ] Deploy to staging
|
||
|
||
**Week 2: Enforcement**
|
||
- [ ] Add keyword limit checks to all entry points
|
||
- [ ] Add automation credit pre-checks
|
||
- [ ] Test all validation flows
|
||
- [ ] Deploy to staging
|
||
|
||
**Week 3: Frontend Cleanup**
|
||
- [ ] Remove duplicate data from Plans & Billing
|
||
- [ ] Update UsageLimitsPanel
|
||
- [ ] Update terminology (remove "API", "operations")
|
||
- [ ] Deploy to staging
|
||
|
||
**Week 4: New Features**
|
||
- [ ] Build Credit Insights tab
|
||
- [ ] Build Keyword Research page
|
||
- [ ] Integrate Ahrefs (when ready)
|
||
- [ ] Add multi-dimensional widgets
|
||
- [ ] Deploy to staging
|
||
|
||
**Week 5: Testing & Production**
|
||
- [ ] Full regression testing
|
||
- [ ] User acceptance testing
|
||
- [ ] Deploy to production
|
||
- [ ] Monitor for issues
|
||
|
||
---
|
||
|
||
## 12. Final Limits Configuration
|
||
|
||
### 12.1 Database Schema (After Cleanup)
|
||
|
||
**Plan Model - FINAL:**
|
||
```python
|
||
class Plan(models.Model):
|
||
# Basic Info
|
||
name = models.CharField(max_length=255)
|
||
slug = models.SlugField(unique=True)
|
||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||
|
||
# ONLY 4 LIMITS:
|
||
max_sites = models.IntegerField(default=1)
|
||
max_users = models.IntegerField(default=1)
|
||
max_keywords = models.IntegerField(default=1000)
|
||
max_ahrefs_queries = models.IntegerField(default=50)
|
||
|
||
# Credits
|
||
included_credits = models.IntegerField(default=0)
|
||
extra_credit_price = models.DecimalField(max_digits=10, decimal_places=2)
|
||
```
|
||
|
||
**Account Model - FINAL:**
|
||
```python
|
||
class Account(models.Model):
|
||
# Credits
|
||
credits = models.IntegerField(default=0)
|
||
|
||
# ONLY 1 Usage Tracker:
|
||
usage_ahrefs_queries = models.IntegerField(default=0)
|
||
|
||
# Billing Period
|
||
usage_period_start = models.DateTimeField(null=True)
|
||
usage_period_end = models.DateTimeField(null=True)
|
||
```
|
||
|
||
### 12.2 Suggested Plan Values
|
||
|
||
| Plan | Price | Included Credits | Sites | Users | Keywords | Ahrefs Queries/mo |
|
||
|------|-------|-----------------|-------|-------|----------|-------------------|
|
||
| **Free** | $0 | 2,000 | 1 | 1 | 100 | 0 |
|
||
| **Starter** | $49 | 10,000 | 2 | 2 | 1,000 | 50 |
|
||
| **Growth** | $149 | 40,000 | 5 | 3 | 5,000 | 200 |
|
||
| **Scale** | $399 | 120,000 | Unlimited | 5 | 20,000 | 500 |
|
||
|
||
---
|
||
|
||
## 13. Success Criteria
|
||
|
||
### 13.1 Technical Success
|
||
|
||
- [ ] All unused limit fields removed from database
|
||
- [ ] Migration runs successfully without data loss
|
||
- [ ] All 4 limits properly enforced
|
||
- [ ] Credit balance checked before ALL operations
|
||
- [ ] Automation pre-checks credit balance
|
||
- [ ] Ahrefs queries counted and limited
|
||
- [ ] No duplicate data across pages
|
||
- [ ] User-friendly terminology throughout
|
||
|
||
### 13.2 User Experience Success
|
||
|
||
- [ ] Users understand the simple 4-limit model
|
||
- [ ] Clear separation: Plans & Billing = financial, Usage = consumption
|
||
- [ ] Multi-dimensional insights provide actionable data
|
||
- [ ] Keyword research flow is intuitive
|
||
- [ ] Credit exhaustion messages are clear and actionable
|
||
- [ ] Upgrade prompts appear at right moments
|
||
|
||
### 13.3 Business Success
|
||
|
||
- [ ] Reduced support questions about limits
|
||
- [ ] Clearer upgrade paths
|
||
- [ ] Better credit consumption visibility drives upgrades
|
||
- [ ] Ahrefs integration ready for launch
|
||
- [ ] System scales without complexity
|
||
|
||
---
|
||
|
||
## 14. Risks & Mitigation
|
||
|
||
### Risk 1: Data Loss During Migration
|
||
**Mitigation:**
|
||
- Backup database before migration
|
||
- Test migration on staging with production data clone
|
||
- Keep removed fields as comments in code for 1 month
|
||
|
||
### Risk 2: Users Confused by Changes
|
||
**Mitigation:**
|
||
- In-app changelog notification
|
||
- Update help documentation
|
||
- Add tooltips to new UI elements
|
||
- Gradual rollout (staging → 10% → 50% → 100%)
|
||
|
||
### Risk 3: Breaking Changes
|
||
**Mitigation:**
|
||
- Maintain backward compatibility in API for 2 weeks
|
||
- Version API endpoints if needed
|
||
- Monitor error logs closely after deployment
|
||
|
||
---
|
||
|
||
## 15. Post-Launch Monitoring
|
||
|
||
### Metrics to Track
|
||
|
||
**Technical:**
|
||
- API error rates (especially 402 Insufficient Credits)
|
||
- Failed automation runs due to credits
|
||
- Keyword limit violations
|
||
- Ahrefs query usage patterns
|
||
|
||
**Business:**
|
||
- Upgrade conversion rate
|
||
- Support tickets about limits/credits
|
||
- Credit package purchase rate
|
||
- User engagement with new Usage insights
|
||
|
||
**User Behavior:**
|
||
- Time spent on Usage page
|
||
- Click-through on upgrade prompts
|
||
- Ahrefs query usage distribution
|
||
- Most-used insights widgets
|
||
|
||
---
|
||
|
||
## 16. Documentation Updates Required
|
||
|
||
- [ ] Update `docs/10-MODULES/BILLING.md`
|
||
- [ ] Update `docs/40-WORKFLOWS/CREDIT-SYSTEM.md`
|
||
- [ ] Create `docs/10-MODULES/KEYWORD-RESEARCH.md`
|
||
- [ ] Update API documentation
|
||
- [ ] Update user help docs
|
||
- [ ] Update admin guides
|
||
|
||
---
|
||
|
||
## 17. Summary
|
||
|
||
### Before (Complex)
|
||
|
||
### Keywords Limit Testing
|
||
|
||
- [ ] Try to create single keyword when at limit → Should fail
|
||
- [ ] Try to import 50 SeedKeywords when 30 slots remain → Should fail
|
||
- [ ] Try to bulk import CSV with 1000 keywords when at limit → Should fail
|
||
- [ ] Error message shows current count, limit, and upgrade link
|
||
- [ ] Upgrade plan → New limit applies immediately
|
||
- [ ] Delete keywords → Can add more up to new total
|
||
|
||
### Ahrefs Limit Testing (Future)
|
||
|
||
- [ ] Query Ahrefs when at monthly limit → Should fail
|
||
- [ ] Error message shows queries used, limit, and reset date
|
||
- [ ] Monthly reset correctly resets counter
|
||
- [ ] Upgrade plan → New monthly allowance applies
|
||
|
||
### Page Reorganization Testing
|
||
|
||
- [ ] Plans & Billing → Current Plan shows only plan info and brief summary
|
||
- [ ] Plans & Billing → Current Plan does NOT show detailed usage breakdown
|
||
- [ ] Usage → Limits & Usage shows all limits with progress bars
|
||
- [ ] Usage → Credit History shows transaction log
|
||
- [ ] No duplicate data between the two sections
|
||
|
||
---
|
||
|
||
## 12. Conclusion
|
||
|
||
### What's Working Well ✅
|
||
|
||
1. **Credit System:** Token-based calculation is accurate and well-implemented
|
||
2. **Usage Tracking:** Monthly limits are properly tracked in Account model
|
||
3. **Sites & Users Limits:** Fully enforced with proper error handling
|
||
4. **Frontend UI:** UsageLimitsPanel component is excellent
|
||
|
||
### What Needs Fixing ⚠️
|
||
|
||
1. **Keywords Enforcement:** Limit exists but not consistently checked
|
||
2. **Data Duplication:** Credits and usage data shown in both Plans & Usage
|
||
3. **Missing Ahrefs Limit:** Future feature needs to be defined
|
||
|
||
### Impact of Changes
|
||
|
||
| Change | User Benefit | Business Benefit |
|
||
|--------|-------------|------------------|
|
||
| Enforce keywords limit | Clear boundaries, upgrade prompts | Prevent unlimited usage, drive upgrades |
|
||
| Remove duplication | Less confusion, faster navigation | Easier maintenance, clearer product positioning |
|
||
| Add Ahrefs limit | Know usage allowance upfront | Control costs, monetize feature |
|
||
|
||
### Next Steps
|
||
|
||
1. **Backend Team:** Implement keyword limit enforcement
|
||
2. **Frontend Team:** Clean up Plans & Billing page
|
||
3. **Product Team:** Define Ahrefs query pricing tiers
|
||
4. **QA Team:** Test all limit scenarios
|
||
|
||
---
|
||
|
||
**End of Report**
|
||
|
||
*For questions or clarification, refer to:*
|
||
- `docs/10-MODULES/BILLING.md` - Billing system documentation
|
||
- `docs/40-WORKFLOWS/CREDIT-SYSTEM.md` - Credit workflows
|
||
- `backend/igny8_core/business/billing/services/limit_service.py` - Limit enforcement code
|