From 9ca048fb9d4323d7a7cdbd027cf4a4faacd5cfef Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Tue, 6 Jan 2026 21:28:13 +0000 Subject: [PATCH] Phase 3 - credts, usage, plans app pages #Migrations --- backend/igny8_core/ai/tasks.py | 28 + backend/igny8_core/api/account_views.py | 10 + backend/igny8_core/auth/admin.py | 10 +- .../commands/create_aws_admin_tenant.py | 13 +- .../0019_simplify_credits_limits.py | 100 + .../migrations/0020_fix_historical_account.py | 39 + backend/igny8_core/auth/models.py | 37 +- backend/igny8_core/auth/serializers.py | 4 +- .../management/commands/backfill_usage.py | 54 +- .../billing/services/limit_service.py | 61 +- .../integration/services/defaults_service.py | 4 + .../management/commands/get_account_limits.py | 19 +- docs/10-MODULES/BILLING.md | 32 +- docs/40-WORKFLOWS/CREDIT-SYSTEM.md | 529 ++-- docs/plans/CREDITS-LIMITS-AUDIT-REPORT.md | 2449 ++++++++++++++++ .../CREDITS-LIMITS-IMPLEMENTATION-PLAN.md | 2578 +++++++++++++++++ docs/plans/IMPLEMENTATION-SUMMARY.md | 274 ++ docs/plans/SYSTEM-ARCHITECTURE-DIAGRAM.md | 356 +++ .../phase-3-plan/FINAL-CREDITS-LIMITS-PLAN.md | 653 +++++ frontend/src/App.tsx | 17 +- .../src/components/auth/ProtectedRoute.tsx | 1 - .../src/components/auth/SignUpFormUnified.tsx | 5 +- .../billing/CreditInsightsCharts.tsx | 450 +++ .../billing/InsufficientCreditsModal.tsx | 168 ++ .../components/billing/UsageLimitsPanel.tsx | 8 +- .../src/components/ui/pricing-table/index.tsx | 30 +- frontend/src/layout/AppSidebar.tsx | 11 +- frontend/src/pages/AuthPages/SignUp.tsx | 10 +- frontend/src/pages/Reference/SeedKeywords.tsx | 39 +- frontend/src/pages/Settings/Plans.tsx | 10 +- .../src/pages/account/PlansAndBillingPage.tsx | 1244 ++++---- .../src/pages/account/UsageAnalyticsPage.tsx | 21 +- .../src/pages/account/UsageDashboardPage.tsx | 679 +++++ frontend/src/pages/account/UsageLogsPage.tsx | 419 +++ frontend/src/services/billing.api.ts | 25 +- frontend/src/utils/creditCheck.ts | 79 + frontend/src/utils/pricingHelpers.ts | 11 +- 37 files changed, 9328 insertions(+), 1149 deletions(-) create mode 100644 backend/igny8_core/auth/migrations/0019_simplify_credits_limits.py create mode 100644 backend/igny8_core/auth/migrations/0020_fix_historical_account.py create mode 100644 docs/plans/CREDITS-LIMITS-AUDIT-REPORT.md create mode 100644 docs/plans/CREDITS-LIMITS-IMPLEMENTATION-PLAN.md create mode 100644 docs/plans/IMPLEMENTATION-SUMMARY.md create mode 100644 docs/plans/SYSTEM-ARCHITECTURE-DIAGRAM.md create mode 100644 docs/plans/phase-3-plan/FINAL-CREDITS-LIMITS-PLAN.md create mode 100644 frontend/src/components/billing/CreditInsightsCharts.tsx create mode 100644 frontend/src/components/billing/InsufficientCreditsModal.tsx create mode 100644 frontend/src/pages/account/UsageDashboardPage.tsx create mode 100644 frontend/src/pages/account/UsageLogsPage.tsx create mode 100644 frontend/src/utils/creditCheck.ts diff --git a/backend/igny8_core/ai/tasks.py b/backend/igny8_core/ai/tasks.py index 549698eb..5b7698e2 100644 --- a/backend/igny8_core/ai/tasks.py +++ b/backend/igny8_core/ai/tasks.py @@ -157,6 +157,7 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None from igny8_core.modules.system.models import IntegrationSettings from igny8_core.ai.ai_core import AICore from igny8_core.ai.prompts import PromptRegistry + from igny8_core.business.billing.services.credit_service import CreditService logger.info("=" * 80) logger.info(f"process_image_generation_queue STARTED") @@ -709,6 +710,33 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None }) failed += 1 else: + # Deduct credits for successful image generation + credits_deducted = 0 + cost_usd = result.get('cost_usd', 0) + if account: + try: + credits_deducted = CreditService.deduct_credits_for_image( + account=account, + model_name=model, + num_images=1, + description=f"Image generation: {content.title[:50] if content else 'Image'}" if content else f"Image {image_id}", + metadata={ + 'image_id': image_id, + 'content_id': content_id, + 'provider': provider, + 'model': model, + 'image_type': image.image_type if image else 'unknown', + 'size': image_size, + }, + cost_usd=cost_usd, + related_object_type='image', + related_object_id=image_id + ) + logger.info(f"[process_image_generation_queue] Credits deducted for image {image_id}: account balance now {credits_deducted}") + except Exception as credit_error: + logger.error(f"[process_image_generation_queue] Failed to deduct credits for image {image_id}: {credit_error}") + # Don't fail the image generation if credit deduction fails + # Update progress: Complete (100%) self.update_state( state='PROGRESS', diff --git a/backend/igny8_core/api/account_views.py b/backend/igny8_core/api/account_views.py index 2edfe71d..2f73649d 100644 --- a/backend/igny8_core/api/account_views.py +++ b/backend/igny8_core/api/account_views.py @@ -132,6 +132,16 @@ class TeamManagementViewSet(viewsets.ViewSet): status=status.HTTP_400_BAD_REQUEST ) + # Check hard limit for users BEFORE creating + from igny8_core.business.billing.services.limit_service import LimitService, HardLimitExceededError + try: + LimitService.check_hard_limit(account, 'users', additional_count=1) + except HardLimitExceededError as e: + return Response( + {'error': str(e)}, + status=status.HTTP_400_BAD_REQUEST + ) + # Create user (simplified - in production, send invitation email) user = User.objects.create_user( email=email, diff --git a/backend/igny8_core/auth/admin.py b/backend/igny8_core/auth/admin.py index a3f78822..be023f40 100644 --- a/backend/igny8_core/auth/admin.py +++ b/backend/igny8_core/auth/admin.py @@ -117,7 +117,7 @@ class PlanResource(resources.ModelResource): class Meta: model = Plan fields = ('id', 'name', 'slug', 'price', 'billing_cycle', 'max_sites', 'max_users', - 'max_keywords', 'max_content_words', 'included_credits', 'is_active', 'is_featured') + 'max_keywords', 'max_ahrefs_queries', 'included_credits', 'is_active', 'is_featured') export_order = fields import_id_fields = ('id',) skip_unchanged = True @@ -127,7 +127,7 @@ class PlanResource(resources.ModelResource): class PlanAdmin(ImportExportMixin, Igny8ModelAdmin): resource_class = PlanResource """Plan admin - Global, no account filtering needed""" - list_display = ['name', 'slug', 'price', 'billing_cycle', 'max_sites', 'max_users', 'max_keywords', 'max_content_words', 'included_credits', 'is_active', 'is_featured'] + list_display = ['name', 'slug', 'price', 'billing_cycle', 'max_sites', 'max_users', 'max_keywords', 'max_ahrefs_queries', 'included_credits', 'is_active', 'is_featured'] list_filter = ['is_active', 'billing_cycle', 'is_internal', 'is_featured'] search_fields = ['name', 'slug'] readonly_fields = ['created_at'] @@ -147,12 +147,12 @@ class PlanAdmin(ImportExportMixin, Igny8ModelAdmin): 'description': 'Persistent limits for account-level resources' }), ('Hard Limits (Persistent)', { - 'fields': ('max_keywords', 'max_clusters'), + 'fields': ('max_keywords',), 'description': 'Total allowed - never reset' }), ('Monthly Limits (Reset on Billing Cycle)', { - 'fields': ('max_content_ideas', 'max_content_words', 'max_images_basic', 'max_images_premium', 'max_image_prompts'), - 'description': 'Monthly allowances - reset at billing cycle' + 'fields': ('max_ahrefs_queries',), + 'description': 'Monthly Ahrefs keyword research queries (0 = disabled)' }), ('Billing & Credits', { 'fields': ('included_credits', 'extra_credit_price', 'allow_credit_topup', 'auto_credit_topup_threshold', 'auto_credit_topup_amount', 'credits_per_month') diff --git a/backend/igny8_core/auth/management/commands/create_aws_admin_tenant.py b/backend/igny8_core/auth/management/commands/create_aws_admin_tenant.py index 164db5af..7d2e6ee2 100644 --- a/backend/igny8_core/auth/management/commands/create_aws_admin_tenant.py +++ b/backend/igny8_core/auth/management/commands/create_aws_admin_tenant.py @@ -25,18 +25,7 @@ class Command(BaseCommand): 'max_users': 999999, 'max_sites': 999999, 'max_keywords': 999999, - 'max_clusters': 999999, - 'max_content_ideas': 999999, - 'monthly_word_count_limit': 999999999, - 'daily_content_tasks': 999999, - 'daily_ai_requests': 999999, - 'daily_ai_request_limit': 999999, - 'monthly_ai_credit_limit': 999999, - 'monthly_image_count': 999999, - 'daily_image_generation_limit': 999999, - 'monthly_cluster_ai_credits': 999999, - 'monthly_content_ai_credits': 999999, - 'monthly_image_ai_credits': 999999, + 'max_ahrefs_queries': 999999, 'included_credits': 999999, 'is_active': True, 'features': ['ai_writer', 'image_gen', 'auto_publish', 'custom_prompts', 'unlimited'], diff --git a/backend/igny8_core/auth/migrations/0019_simplify_credits_limits.py b/backend/igny8_core/auth/migrations/0019_simplify_credits_limits.py new file mode 100644 index 00000000..1f42a1e4 --- /dev/null +++ b/backend/igny8_core/auth/migrations/0019_simplify_credits_limits.py @@ -0,0 +1,100 @@ +# Generated by IGNY8 Phase 1: Simplify Credits & Limits +# Migration: Remove unused limit fields, add Ahrefs query tracking +# Date: January 5, 2026 + +from django.db import migrations, models +import django.core.validators + + +class Migration(migrations.Migration): + """ + Simplify the credits and limits system: + + PLAN MODEL: + - REMOVE: max_clusters, max_content_ideas, max_content_words, + max_images_basic, max_images_premium, max_image_prompts + - ADD: max_ahrefs_queries (monthly keyword research queries) + + ACCOUNT MODEL: + - REMOVE: usage_content_ideas, usage_content_words, usage_images_basic, + usage_images_premium, usage_image_prompts + - ADD: usage_ahrefs_queries + + RATIONALE: + All consumption is now controlled by credits only. The only non-credit + limits are: sites, users, keywords (hard limits) and ahrefs_queries (monthly). + """ + + dependencies = [ + ('igny8_core_auth', '0018_add_country_remove_intent_seedkeyword'), + ] + + operations = [ + # STEP 1: Add new Ahrefs fields FIRST (before removing old ones) + migrations.AddField( + model_name='plan', + name='max_ahrefs_queries', + field=models.IntegerField( + default=0, + validators=[django.core.validators.MinValueValidator(0)], + help_text='Monthly Ahrefs keyword research queries (0 = disabled)' + ), + ), + migrations.AddField( + model_name='account', + name='usage_ahrefs_queries', + field=models.IntegerField( + default=0, + validators=[django.core.validators.MinValueValidator(0)], + help_text='Ahrefs queries used this month' + ), + ), + + # STEP 2: Remove unused Plan fields + migrations.RemoveField( + model_name='plan', + name='max_clusters', + ), + 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', + ), + + # STEP 3: Remove unused Account fields + 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', + ), + ] diff --git a/backend/igny8_core/auth/migrations/0020_fix_historical_account.py b/backend/igny8_core/auth/migrations/0020_fix_historical_account.py new file mode 100644 index 00000000..186aae42 --- /dev/null +++ b/backend/igny8_core/auth/migrations/0020_fix_historical_account.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.9 on 2026-01-06 00:11 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('igny8_core_auth', '0019_simplify_credits_limits'), + ] + + operations = [ + migrations.RemoveField( + model_name='historicalaccount', + name='usage_content_ideas', + ), + migrations.RemoveField( + model_name='historicalaccount', + name='usage_content_words', + ), + migrations.RemoveField( + model_name='historicalaccount', + name='usage_image_prompts', + ), + migrations.RemoveField( + model_name='historicalaccount', + name='usage_images_basic', + ), + migrations.RemoveField( + model_name='historicalaccount', + name='usage_images_premium', + ), + migrations.AddField( + model_name='historicalaccount', + name='usage_ahrefs_queries', + field=models.IntegerField(default=0, help_text='Ahrefs queries used this month', validators=[django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/backend/igny8_core/auth/models.py b/backend/igny8_core/auth/models.py index 00ec7b30..80a7a675 100644 --- a/backend/igny8_core/auth/models.py +++ b/backend/igny8_core/auth/models.py @@ -108,11 +108,7 @@ class Account(SoftDeletableModel): tax_id = models.CharField(max_length=100, blank=True, help_text="VAT/Tax ID number") # Monthly usage tracking (reset on billing cycle) - usage_content_ideas = models.IntegerField(default=0, validators=[MinValueValidator(0)], help_text="Content ideas generated this month") - usage_content_words = models.IntegerField(default=0, validators=[MinValueValidator(0)], help_text="Content words generated this month") - usage_images_basic = models.IntegerField(default=0, validators=[MinValueValidator(0)], help_text="Basic AI images this month") - usage_images_premium = models.IntegerField(default=0, validators=[MinValueValidator(0)], help_text="Premium AI images this month") - usage_image_prompts = models.IntegerField(default=0, validators=[MinValueValidator(0)], help_text="Image prompts this month") + usage_ahrefs_queries = models.IntegerField(default=0, validators=[MinValueValidator(0)], help_text="Ahrefs queries used this month") usage_period_start = models.DateTimeField(null=True, blank=True, help_text="Current billing period start") usage_period_end = models.DateTimeField(null=True, blank=True, help_text="Current billing period end") @@ -216,37 +212,12 @@ class Plan(models.Model): validators=[MinValueValidator(1)], help_text="Maximum total keywords allowed (hard limit)" ) - max_clusters = models.IntegerField( - default=100, - validators=[MinValueValidator(1)], - help_text="Maximum AI keyword clusters allowed (hard limit)" - ) # Monthly Limits (Reset on billing cycle) - max_content_ideas = models.IntegerField( - default=300, - validators=[MinValueValidator(1)], - help_text="Maximum AI content ideas per month" - ) - max_content_words = models.IntegerField( - default=100000, - validators=[MinValueValidator(1)], - help_text="Maximum content words per month (e.g., 100000 = 100K words)" - ) - max_images_basic = models.IntegerField( - default=300, + max_ahrefs_queries = models.IntegerField( + default=0, validators=[MinValueValidator(0)], - help_text="Maximum basic AI images per month" - ) - max_images_premium = models.IntegerField( - default=60, - validators=[MinValueValidator(0)], - help_text="Maximum premium AI images per month (DALL-E)" - ) - max_image_prompts = models.IntegerField( - default=300, - validators=[MinValueValidator(0)], - help_text="Maximum image prompts per month" + help_text="Monthly Ahrefs keyword research queries (0 = disabled)" ) # Billing & Credits (Phase 0: Credit-only system) diff --git a/backend/igny8_core/auth/serializers.py b/backend/igny8_core/auth/serializers.py index 37946ed5..aa14465d 100644 --- a/backend/igny8_core/auth/serializers.py +++ b/backend/igny8_core/auth/serializers.py @@ -13,9 +13,7 @@ class PlanSerializer(serializers.ModelSerializer): 'id', 'name', 'slug', 'price', 'original_price', 'billing_cycle', 'annual_discount_percent', 'is_featured', 'features', 'is_active', 'max_users', 'max_sites', 'max_industries', 'max_author_profiles', - 'max_keywords', 'max_clusters', - 'max_content_ideas', 'max_content_words', - 'max_images_basic', 'max_images_premium', 'max_image_prompts', + 'max_keywords', 'max_ahrefs_queries', 'included_credits', 'extra_credit_price', 'allow_credit_topup', 'auto_credit_topup_threshold', 'auto_credit_topup_amount', 'stripe_product_id', 'stripe_price_id', 'credits_per_month' diff --git a/backend/igny8_core/business/billing/management/commands/backfill_usage.py b/backend/igny8_core/business/billing/management/commands/backfill_usage.py index 53d6dab0..0512a6a8 100644 --- a/backend/igny8_core/business/billing/management/commands/backfill_usage.py +++ b/backend/igny8_core/business/billing/management/commands/backfill_usage.py @@ -1,6 +1,9 @@ """ Management command to backfill usage tracking for existing content. Usage: python manage.py backfill_usage [account_id] + +NOTE: Since the simplification of limits (Jan 2026), this command only +tracks Ahrefs queries. All other usage is tracked via CreditUsageLog. """ from django.core.management.base import BaseCommand from django.apps import apps @@ -9,7 +12,7 @@ from igny8_core.auth.models import Account class Command(BaseCommand): - help = 'Backfill usage tracking for existing content' + help = 'Backfill usage tracking for existing content (Ahrefs queries only)' def add_arguments(self, parser): parser.add_argument( @@ -30,10 +33,6 @@ class Command(BaseCommand): else: accounts = Account.objects.filter(plan__isnull=False).select_related('plan') - ContentIdeas = apps.get_model('planner', 'ContentIdeas') - Content = apps.get_model('writer', 'Content') - Images = apps.get_model('writer', 'Images') - total_accounts = accounts.count() self.stdout.write(f'Processing {total_accounts} account(s)...\n') @@ -43,45 +42,14 @@ class Command(BaseCommand): self.stdout.write(f'Plan: {account.plan.name if account.plan else "No Plan"}') self.stdout.write('=' * 60) - # Count content ideas - ideas_count = ContentIdeas.objects.filter(account=account).count() - self.stdout.write(f'Content Ideas: {ideas_count}') + # Ahrefs queries are tracked in CreditUsageLog with operation_type='ahrefs_query' + # We don't backfill these as they should be tracked in real-time going forward + # This command is primarily for verification - # Count content words - from django.db.models import Sum - total_words = Content.objects.filter(account=account).aggregate( - total=Sum('word_count') - )['total'] or 0 - self.stdout.write(f'Content Words: {total_words}') - - # Count images - total_images = Images.objects.filter(account=account).count() - images_with_prompts = Images.objects.filter( - account=account, prompt__isnull=False - ).exclude(prompt='').count() - self.stdout.write(f'Total Images: {total_images}') - self.stdout.write(f'Images with Prompts: {images_with_prompts}') - - # Update account usage fields - with transaction.atomic(): - account.usage_content_ideas = ideas_count - account.usage_content_words = total_words - account.usage_images_basic = total_images - account.usage_images_premium = 0 # Premium not implemented yet - account.usage_image_prompts = images_with_prompts - account.save(update_fields=[ - 'usage_content_ideas', 'usage_content_words', - 'usage_images_basic', 'usage_images_premium', 'usage_image_prompts', - 'updated_at' - ]) - - self.stdout.write(self.style.SUCCESS('\n✅ Updated usage tracking:')) - self.stdout.write(f' usage_content_ideas: {account.usage_content_ideas}') - self.stdout.write(f' usage_content_words: {account.usage_content_words}') - self.stdout.write(f' usage_images_basic: {account.usage_images_basic}') - self.stdout.write(f' usage_images_premium: {account.usage_images_premium}') - self.stdout.write(f' usage_image_prompts: {account.usage_image_prompts}\n') + self.stdout.write(f'Ahrefs queries used this month: {account.usage_ahrefs_queries}') + self.stdout.write(self.style.SUCCESS('\n✅ Verified usage tracking')) + self.stdout.write(f' usage_ahrefs_queries: {account.usage_ahrefs_queries}\n') self.stdout.write('=' * 60) - self.stdout.write(self.style.SUCCESS('✅ Backfill complete!')) + self.stdout.write(self.style.SUCCESS('✅ Verification complete!')) self.stdout.write('=' * 60) diff --git a/backend/igny8_core/business/billing/services/limit_service.py b/backend/igny8_core/business/billing/services/limit_service.py index 9c244f43..754164a2 100644 --- a/backend/igny8_core/business/billing/services/limit_service.py +++ b/backend/igny8_core/business/billing/services/limit_service.py @@ -1,6 +1,6 @@ """ Limit Service for Plan Limit Enforcement -Manages hard limits (sites, users, keywords, clusters) and monthly limits (ideas, words, images, prompts) +Manages hard limits (sites, users, keywords) and monthly limits (ahrefs_queries) """ from django.db import transaction from django.utils import timezone @@ -18,12 +18,12 @@ class LimitExceededError(Exception): class HardLimitExceededError(LimitExceededError): - """Raised when a hard limit (sites, users, keywords, clusters) is exceeded""" + """Raised when a hard limit (sites, users, keywords) is exceeded""" pass class MonthlyLimitExceededError(LimitExceededError): - """Raised when a monthly limit (ideas, words, images, prompts) is exceeded""" + """Raised when a monthly limit (ahrefs_queries) is exceeded""" pass @@ -31,6 +31,7 @@ class LimitService: """Service for managing and enforcing plan limits""" # Map limit types to model/field names + # Simplified to only 3 hard limits: sites, users, keywords HARD_LIMIT_MAPPINGS = { 'sites': { 'model': 'igny8_core_auth.Site', @@ -39,10 +40,10 @@ class LimitService: 'filter_field': 'account', }, 'users': { - 'model': 'igny8_core_auth.SiteUserAccess', + 'model': 'igny8_core_auth.User', 'plan_field': 'max_users', - 'display_name': 'Team Users', - 'filter_field': 'site__account', + 'display_name': 'Team Members', + 'filter_field': 'account', }, 'keywords': { 'model': 'planner.Keywords', @@ -50,39 +51,15 @@ class LimitService: 'display_name': 'Keywords', 'filter_field': 'account', }, - 'clusters': { - 'model': 'planner.Clusters', - 'plan_field': 'max_clusters', - 'display_name': 'Clusters', - 'filter_field': 'account', - }, } + # Simplified to only 1 monthly limit: ahrefs_queries + # All other consumption is controlled by credits only MONTHLY_LIMIT_MAPPINGS = { - 'content_ideas': { - 'plan_field': 'max_content_ideas', - 'usage_field': 'usage_content_ideas', - 'display_name': 'Content Ideas', - }, - 'content_words': { - 'plan_field': 'max_content_words', - 'usage_field': 'usage_content_words', - 'display_name': 'Content Words', - }, - 'images_basic': { - 'plan_field': 'max_images_basic', - 'usage_field': 'usage_images_basic', - 'display_name': 'Basic Images', - }, - 'images_premium': { - 'plan_field': 'max_images_premium', - 'usage_field': 'usage_images_premium', - 'display_name': 'Premium Images', - }, - 'image_prompts': { - 'plan_field': 'max_image_prompts', - 'usage_field': 'usage_image_prompts', - 'display_name': 'Image Prompts', + 'ahrefs_queries': { + 'plan_field': 'max_ahrefs_queries', + 'usage_field': 'usage_ahrefs_queries', + 'display_name': 'Keyword Research Queries', }, } @@ -318,11 +295,8 @@ class LimitService: Returns: dict: Summary of reset operation """ - account.usage_content_ideas = 0 - account.usage_content_words = 0 - account.usage_images_basic = 0 - account.usage_images_premium = 0 - account.usage_image_prompts = 0 + # Reset only ahrefs_queries (the only monthly limit now) + account.usage_ahrefs_queries = 0 old_period_end = account.usage_period_end @@ -341,8 +315,7 @@ class LimitService: account.usage_period_end = new_period_end account.save(update_fields=[ - 'usage_content_ideas', 'usage_content_words', - 'usage_images_basic', 'usage_images_premium', 'usage_image_prompts', + 'usage_ahrefs_queries', 'usage_period_start', 'usage_period_end', 'updated_at' ]) @@ -353,5 +326,5 @@ class LimitService: 'old_period_end': old_period_end.isoformat() if old_period_end else None, 'new_period_start': new_period_start.isoformat(), 'new_period_end': new_period_end.isoformat(), - 'limits_reset': 5, + 'limits_reset': 1, } diff --git a/backend/igny8_core/business/integration/services/defaults_service.py b/backend/igny8_core/business/integration/services/defaults_service.py index c5179ecf..3d807d7a 100644 --- a/backend/igny8_core/business/integration/services/defaults_service.py +++ b/backend/igny8_core/business/integration/services/defaults_service.py @@ -68,6 +68,10 @@ class DefaultsService: Returns: Tuple of (Site, PublishingSettings, AutomationConfig) """ + # Check hard limit for sites BEFORE creating + from igny8_core.business.billing.services.limit_service import LimitService, HardLimitExceededError + LimitService.check_hard_limit(self.account, 'sites', additional_count=1) + # Create the site site = Site.objects.create( account=self.account, diff --git a/backend/igny8_core/modules/billing/management/commands/get_account_limits.py b/backend/igny8_core/modules/billing/management/commands/get_account_limits.py index 99f51d55..9d2d0bcc 100644 --- a/backend/igny8_core/modules/billing/management/commands/get_account_limits.py +++ b/backend/igny8_core/modules/billing/management/commands/get_account_limits.py @@ -29,23 +29,10 @@ class Command(BaseCommand): ], 'Planner': [ ('max_keywords', 'Max Keywords'), - ('max_clusters', 'Max Clusters'), - ('max_content_ideas', 'Max Content Ideas'), - ('daily_cluster_limit', 'Daily Cluster Limit'), + ('max_ahrefs_queries', 'Max Ahrefs Queries'), ], - 'Writer': [ - ('monthly_word_count_limit', 'Monthly Word Count Limit'), - ('daily_content_tasks', 'Daily Content Tasks'), - ], - 'Images': [ - ('monthly_image_count', 'Monthly Image Count'), - ('daily_image_generation_limit', 'Daily Image Generation Limit'), - ], - 'AI Credits': [ - ('monthly_ai_credit_limit', 'Monthly AI Credit Limit'), - ('monthly_cluster_ai_credits', 'Monthly Cluster AI Credits'), - ('monthly_content_ai_credits', 'Monthly Content AI Credits'), - ('monthly_image_ai_credits', 'Monthly Image AI Credits'), + 'Credits': [ + ('included_credits', 'Included Credits'), ], } diff --git a/docs/10-MODULES/BILLING.md b/docs/10-MODULES/BILLING.md index 2d041517..cb1706b5 100644 --- a/docs/10-MODULES/BILLING.md +++ b/docs/10-MODULES/BILLING.md @@ -1,7 +1,7 @@ # Billing Module **Last Verified:** January 5, 2026 -**Status:** ✅ Active +**Status:** ✅ Active (Simplified January 2026) **Backend Path:** `backend/igny8_core/modules/billing/` + `backend/igny8_core/business/billing/` **Frontend Path:** `frontend/src/pages/Billing/` + `frontend/src/pages/Account/` @@ -13,6 +13,7 @@ |------|------|-----------| | Models | `business/billing/models.py` | `CreditTransaction`, `CreditUsageLog`, `CreditCostConfig`, `AIModelConfig` | | Service | `business/billing/services/credit_service.py` | `CreditService` | +| Limit Service | `business/billing/services/limit_service.py` | `LimitService` (4 limits only) | | Views | `modules/billing/views.py` | `CreditBalanceViewSet`, `CreditUsageViewSet` | | Frontend | `pages/Account/PlansAndBillingPage.tsx` | Plans, credits, billing history | | Store | `store/billingStore.ts` | `useBillingStore` | @@ -24,7 +25,7 @@ The Billing module manages: - Credit balance and transactions - AI model pricing and credit configuration (v1.4.0) -- Usage tracking and limits +- Usage tracking with 4 simplified limits (v1.5.0) - Plan enforcement - Payment processing @@ -205,17 +206,14 @@ CreditService.add_credits( | Sites | `max_sites` | Maximum sites per account | | Users | `max_users` | Maximum team members | | Keywords | `max_keywords` | Total keywords allowed | -| Clusters | `max_clusters` | Total clusters allowed | ### Monthly Limits (Reset on Billing Cycle) | Limit | Field | Description | |-------|-------|-------------| -| Content Ideas | `max_content_ideas` | Ideas per month | -| Content Words | `max_content_words` | Words generated per month | -| Basic Images | `max_images_basic` | Basic AI images per month | -| Premium Images | `max_images_premium` | Premium AI images per month | -| Image Prompts | `max_image_prompts` | Prompts per month | +| Ahrefs Queries | `max_ahrefs_queries` | Live Ahrefs API queries per month | + +**Note:** As of January 2026, the limit system was simplified from 10+ limits to just 4. Credits handle all AI operation costs (content generation, image generation, clustering, etc.) instead of separate per-operation limits. --- @@ -224,9 +222,9 @@ CreditService.add_credits( **Component:** `UsageLimitsPanel.tsx` Displays: -- Progress bars for each limit +- Progress bars for 4 limits only (Sites, Users, Keywords, Ahrefs Queries) - Color coding: blue (safe), yellow (warning), red (critical) -- Days until reset for monthly limits +- Days until reset for monthly limits (Ahrefs Queries) - Upgrade CTA when approaching limits --- @@ -264,17 +262,17 @@ Displays: ### Plans & Billing (`/account/plans`) **Tabs:** -1. **Current Plan** - Active plan, upgrade options -2. **Credits Overview** - Balance, usage chart, cost breakdown -3. **Purchase Credits** - Credit packages -4. **Billing History** - Invoices and transactions +1. **Current Plan** - Active plan details, renewal date, "View Usage" link +2. **Upgrade Plan** - Pricing table with plan comparison +3. **Billing History** - Invoices and payment history ### Usage Analytics (`/account/usage`) **Tabs:** -1. **Limits & Usage** - Plan limits with progress bars -2. **Activity** - Credit transaction history -3. **API Usage** - API call statistics +1. **Limits & Usage** - Plan limits with progress bars (4 limits only) +2. **Credit History** - Credit transaction history +3. **Credit Insights** - Charts: credits by type, daily timeline, operations breakdown +4. **Activity Log** - API call statistics and operation details --- diff --git a/docs/40-WORKFLOWS/CREDIT-SYSTEM.md b/docs/40-WORKFLOWS/CREDIT-SYSTEM.md index 4d389ced..8c94b8c3 100644 --- a/docs/40-WORKFLOWS/CREDIT-SYSTEM.md +++ b/docs/40-WORKFLOWS/CREDIT-SYSTEM.md @@ -1,169 +1,164 @@ -# Usage & Content System +# Credit System -**Last Verified:** December 25, 2025 +**Last Verified:** January 5, 2026 +**Status:** ✅ Simplified (v1.5.0) --- ## Overview -IGNY8 uses a content-based allowance system. Users see "Content Pieces" while the backend tracks detailed credit consumption for internal cost monitoring. - -**User View:** `47/50 Content Pieces Remaining` -**Backend Tracks:** Idea credits, content credits, image credits (for cost analysis) +IGNY8 uses a unified credit system where all AI operations consume credits from a single balance. Plan limits are simplified to 4 hard/monthly limits only. --- -## How It Works - -### User-Facing (Simple) - -| What Users See | Description | -|----------------|-------------| -| **Content Pieces** | Monthly allowance of pages/articles | -| **X/Y Remaining** | Used vs total for the month | -| **Upgrade Plan** | Get more content pieces | - -### Backend (Detailed - Internal Only) - -| Credit Type | Used For | Tracked For | -|-------------|----------|-------------| -| Idea Credits | Clustering, idea generation | Cost analysis | -| Content Credits | Article generation | Usage limits | -| Image Credits | Image generation | Cost analysis | -| Optimization Credits | SEO optimization (future) | Cost analysis | - ---- - -## Plan Allowances - -| Plan | Content Pieces/Month | Sites | Users | -|------|---------------------|-------|-------| -| Starter | 50 | 2 | 2 | -| Growth | 200 | 5 | 3 | -| Scale | 500 | Unlimited | 5 | - -**Included with every content piece:** -- AI keyword clustering -- AI idea generation -- AI content writing (1000-2000 words) -- 3 images (1 featured + 2 in-article) -- Internal linking -- SEO optimization -- WordPress publishing - ---- - -## Backend Soft Limits (Hidden from Users) - -To prevent abuse, the backend enforces hidden limits: - -| Limit | Starter | Growth | Scale | -|-------|---------|--------|-------| -| Keyword imports/mo | 500 | 2,000 | 5,000 | -| Clustering operations | 100 | 400 | 1,000 | -| Idea generations | 150 | 600 | 1,500 | -| Images generated | 200 | 800 | 2,000 | - -If users hit these limits, they see: "You've reached your preparation limit for this month." - ---- - -## Content Deduction Flow +## Credit Flow (Verified Architecture) ``` ┌─────────────────────────────────────────────────────────────────┐ -│ CONTENT CREATION FLOW │ +│ CREDIT FLOW │ ├─────────────────────────────────────────────────────────────────┤ │ │ -│ User clicks Check Generate │ -│ "Generate" ──────► Allowance ──────► Content │ -│ │ │ │ -│ │ Limit │ │ -│ │ Reached ▼ │ -│ ▼ Deduct 1 │ -│ Show Upgrade Content │ -│ Modal Piece │ +│ Plan.included_credits = Monthly allocation (e.g., 10,000) │ +│ ↓ (Added on subscription renewal/approval) │ +│ Account.credits = Current balance (real-time, decremented) │ +│ ↓ (Decremented on each AI operation) │ +│ CreditTransaction = Log of all credit changes │ +│ CreditUsageLog = Detailed operation tracking │ +│ │ +│ THESE ARE NOT PARALLEL - They serve different purposes: │ +│ • Plan.included_credits = "How many credits per month" │ +│ • Account.credits = "How many credits you have RIGHT NOW" │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` +**Where credits are added to Account.credits:** +1. `billing/views.py` - When manual payment is approved +2. `payment_service.py` - When credit package purchased +3. `credit_service.py` - Generic `add_credits()` method + --- -## Operations & Credit Costs +## Simplified Limits (v1.5.0) -### Planner Operations +### Hard Limits (Never Reset) -| Operation | Credits | Type | -|-----------|---------|------| -| Add keyword | 0 | Free | -| Auto-cluster keywords | 1 | Idea | -| Generate content ideas | 1 per idea | Idea | +| Limit | Plan Field | Account Field | Description | +|-------|------------|---------------|-------------| +| Sites | `max_sites` | (count of Site objects) | Maximum sites per account | +| Users | `max_users` | (count of User objects) | Maximum team members | +| Keywords | `max_keywords` | (count of Keyword objects) | Total keywords allowed | -### Writer Operations +### Monthly Limits (Reset on Billing Cycle) -| Operation | Credits | Type | -|-----------|---------|------| -| Create task | 0 | Free | -| Generate content | 1 | Content | -| Regenerate content | 1 | Content | -| Generate images | 1 per image | Image | -| Regenerate image | 1 | Image | -| Edit content | 0 | Free | +| Limit | Plan Field | Account Field | Description | +|-------|------------|---------------|-------------| +| Ahrefs Queries | `max_ahrefs_queries` | `usage_ahrefs_queries` | Live Ahrefs API queries per month | -### Automation Operations +### Removed Limits (Now Credit-Based) -| Operation | Credits | Type | -|-----------|---------|------| -| Run automation | Sum of operations | Mixed | -| Pause/resume | 0 | Free | +The following limits were removed in v1.5.0 - credits handle these: +- ~~max_clusters~~ → Credits +- ~~max_content_ideas~~ → Credits +- ~~max_content_words~~ → Credits +- ~~max_images_basic~~ → Credits +- ~~max_images_premium~~ → Credits +- ~~max_image_prompts~~ → Credits -### Publisher Operations +--- -| Operation | Credits | Type | -|-----------|---------|------| -| Publish to WordPress | 0 | Free | -| Sync from WordPress | 0 | Free | +## Plan Tiers -### Optimizer Operations (Future) +| Plan | Credits/Month | Sites | Users | Keywords | Ahrefs Queries | +|------|---------------|-------|-------|----------|----------------| +| Free | 500 | 1 | 1 | 100 | 0 | +| Starter | 5,000 | 3 | 2 | 500 | 50 | +| Growth | 15,000 | 10 | 5 | 2,000 | 200 | +| Scale | 50,000 | Unlimited | 10 | 10,000 | 500 | -| Operation | Credits | Type | -|-----------|---------|------| -| Optimize content | 1 | Optimization | -| Batch optimize | 1 per item | Optimization | +--- + +## Credit Operations + +### Token-Based Operations (Text AI) + +Credits calculated from actual token usage: +- `credits = ceil(total_tokens / tokens_per_credit)` +- `tokens_per_credit` defined per model in `AIModelConfig` + +| Operation | Model Example | tokens_per_credit | +|-----------|---------------|-------------------| +| Keyword Clustering | gpt-4o-mini | 10,000 | +| Idea Generation | gpt-4o-mini | 10,000 | +| Content Generation | gpt-4o | 1,000 | +| Content Optimization | gpt-4o-mini | 10,000 | + +### Fixed-Cost Operations (Image AI) + +Credits per image based on quality tier: + +| Quality Tier | Model Example | Credits/Image | +|--------------|---------------|---------------| +| Basic | runware:97@1 | 1 | +| Quality | dall-e-3 | 5 | +| Premium | google:4@2 | 15 | + +### Free Operations + +| Operation | Cost | +|-----------|------| +| Add keyword (manual) | 0 | +| Create content task | 0 | +| Edit content | 0 | +| Publish to WordPress | 0 | +| Sync from WordPress | 0 | --- ## Database Models -### CreditBalance +### Account (Credit Balance) ```python -class CreditBalance(models.Model): - account = models.ForeignKey(Account) - site = models.ForeignKey(Site, null=True) - - idea_credits = models.IntegerField(default=0) - content_credits = models.IntegerField(default=0) - image_credits = models.IntegerField(default=0) - optimization_credits = models.IntegerField(default=0) - - period_start = models.DateField() - period_end = models.DateField() +class Account(models.Model): + credits = models.IntegerField(default=0) # Current balance + usage_ahrefs_queries = models.IntegerField(default=0) # Monthly Ahrefs usage ``` -### CreditUsage +### Plan (Allocations) ```python -class CreditUsage(models.Model): +class Plan(models.Model): + included_credits = models.IntegerField(default=0) # Monthly allocation + max_sites = models.IntegerField(default=1) + max_users = models.IntegerField(default=1) + max_keywords = models.IntegerField(default=100) + max_ahrefs_queries = models.IntegerField(default=0) # Monthly Ahrefs limit +``` + +### CreditTransaction (Ledger) + +```python +class CreditTransaction(models.Model): account = models.ForeignKey(Account) - site = models.ForeignKey(Site, null=True) - user = models.ForeignKey(User) - - credit_type = models.CharField() # idea/content/image/optimization - amount = models.IntegerField() - operation = models.CharField() # generate_content, etc. - - created_at = models.DateTimeField(auto_now_add=True) + transaction_type = models.CharField() # purchase/subscription/refund/deduction + amount = models.DecimalField() # Positive (add) or negative (deduct) + balance_after = models.DecimalField() + description = models.CharField() + created_at = models.DateTimeField() +``` + +### CreditUsageLog (Analytics) + +```python +class CreditUsageLog(models.Model): + account = models.ForeignKey(Account) + operation_type = models.CharField() # clustering/content_generation/image_generation + credits_used = models.DecimalField() + model_used = models.CharField() + tokens_input = models.IntegerField() + tokens_output = models.IntegerField() + created_at = models.DateTimeField() ``` --- @@ -172,26 +167,58 @@ class CreditUsage(models.Model): ### CreditService -Location: `backend/igny8_core/business/billing/services.py` +Location: `backend/igny8_core/business/billing/services/credit_service.py` **Key Methods:** ```python class CreditService: - def check_balance(account, site, credit_type, amount) -> bool: - """Check if sufficient credits available""" + @staticmethod + def check_credits(account, required_credits): + """Check if sufficient credits available, raises InsufficientCreditsError if not""" - def deduct_credits(account, site, user, credit_type, amount, operation) -> bool: - """Deduct credits and log usage""" + @staticmethod + def deduct_credits_for_operation(account, operation_type, model, tokens_in, tokens_out, metadata=None): + """Deduct credits and log usage after AI operation""" - def get_balance(account, site) -> CreditBalance: - """Get current balance""" + @staticmethod + def add_credits(account, amount, transaction_type, description): + """Add credits (admin/purchase/subscription)""" - def reset_monthly_credits(account) -> None: - """Reset credits at period start""" + @staticmethod + def calculate_credits_from_tokens(operation_type, tokens_in, tokens_out, model=None): + """Calculate credits based on token usage and model""" +``` + +### LimitService + +Location: `backend/igny8_core/business/billing/services/limit_service.py` + +**Key Methods:** + +```python +class LimitService: + HARD_LIMIT_MAPPINGS = { + 'sites': {...}, + 'users': {...}, + 'keywords': {...}, + } + + MONTHLY_LIMIT_MAPPINGS = { + 'ahrefs_queries': {...}, + } + + @classmethod + def check_hard_limit(cls, account, limit_name, additional_count=1): + """Check if adding items would exceed hard limit""" - def add_credits(account, credit_type, amount, reason) -> None: - """Add credits (admin/purchase)""" + @classmethod + def check_monthly_limit(cls, account, limit_name, additional_count=1): + """Check if operation would exceed monthly limit""" + + @classmethod + def increment_monthly_usage(cls, account, limit_name, count=1): + """Increment monthly usage counter""" ``` ### Usage in AI Operations @@ -199,26 +226,23 @@ class CreditService: ```python # In content generation service def generate_content(task, user): - # 1. Check balance - if not credit_service.check_balance( - account=task.site.account, - site=task.site, - credit_type='content', - amount=1 - ): - raise InsufficientCreditsError() + account = task.site.account + + # 1. Pre-check credits (estimated) + estimated_credits = 50 # Estimate for content generation + CreditService.check_credits(account, estimated_credits) # 2. Execute AI function - content = ai_engine.generate_content(task) + content, usage = ai_engine.generate_content(task) - # 3. Deduct credits - credit_service.deduct_credits( - account=task.site.account, - site=task.site, - user=user, - credit_type='content', - amount=1, - operation='generate_content' + # 3. Deduct actual credits based on token usage + CreditService.deduct_credits_for_operation( + account=account, + operation_type='content_generation', + model=usage.model, + tokens_in=usage.input_tokens, + tokens_out=usage.output_tokens, + metadata={'content_id': content.id} ) return content @@ -228,19 +252,14 @@ def generate_content(task, user): ## API Responses -### Successful Deduction +### Successful Operation ```json { "success": true, "data": { ... }, - "credits_used": { - "type": "content", - "amount": 1 - }, - "balance": { - "content_credits": 49 - } + "credits_used": 15, + "balance": 9985 } ``` @@ -251,10 +270,25 @@ HTTP 402 Payment Required { "success": false, - "error": "Insufficient content credits", + "error": "Insufficient credits", "code": "INSUFFICIENT_CREDITS", - "required": 1, - "available": 0 + "required": 50, + "available": 25 +} +``` + +### Limit Exceeded + +```json +HTTP 402 Payment Required + +{ + "success": false, + "error": "Keyword limit reached", + "code": "HARD_LIMIT_EXCEEDED", + "limit": "keywords", + "current": 500, + "max": 500 } ``` @@ -262,113 +296,125 @@ HTTP 402 Payment Required ## Frontend Handling -### Balance Display +### Credit Balance Display -- Header shows credit balances +- Header shows current credit balance - Updates after each operation - Warning at low balance (< 10%) -### Error Handling +### Pre-Operation Check ```typescript -// In writer store -async generateContent(taskId: string) { - try { - const response = await api.generateContent(taskId); - // Update billing store - billingStore.fetchBalance(); - return response; - } catch (error) { - if (error.code === 'INSUFFICIENT_CREDITS') { - // Show upgrade modal - uiStore.showUpgradeModal(); +import { checkCreditsBeforeOperation } from '@/utils/creditCheck'; +import { useInsufficientCreditsModal } from '@/components/billing/InsufficientCreditsModal'; + +function ContentGenerator() { + const { showModal } = useInsufficientCreditsModal(); + + const handleGenerate = async () => { + // Check credits before operation + const check = await checkCreditsBeforeOperation(50); // estimated cost + + if (!check.hasEnoughCredits) { + showModal({ + requiredCredits: check.requiredCredits, + availableCredits: check.availableCredits, + }); + return; } - throw error; - } + + // Proceed with generation + await generateContent(); + }; } ``` --- -## Usage Tracking +## API Endpoints -### Usage Summary Endpoint +### Credit Balance ``` -GET /api/v1/billing/usage/summary/?period=month +GET /api/v1/billing/balance/ ``` Response: ```json { - "period": "2025-01", - "usage": { - "idea_credits": 45, - "content_credits": 23, - "image_credits": 67, - "optimization_credits": 0 - }, - "by_operation": { - "auto_cluster": 12, - "generate_ideas": 33, - "generate_content": 23, - "generate_images": 67 - } + "credits": 9500, + "plan_credits_per_month": 10000, + "credits_used_this_month": 500, + "credits_remaining": 9500 } ``` ---- - -## Automation Credit Estimation - -Before running automation: +### Usage Limits ``` -GET /api/v1/automation/estimate/?site_id=... +GET /api/v1/billing/usage/limits/ ``` Response: ```json { - "estimated_credits": { - "idea_credits": 25, - "content_credits": 10, - "image_credits": 30 + "limits": { + "sites": { "current": 2, "limit": 5, "type": "hard" }, + "users": { "current": 2, "limit": 3, "type": "hard" }, + "keywords": { "current": 847, "limit": 1000, "type": "hard" }, + "ahrefs_queries": { "current": 23, "limit": 50, "type": "monthly" } }, - "stages": { - "clustering": 5, - "ideas": 20, - "content": 10, - "images": 30 - }, - "has_sufficient_credits": true + "days_until_reset": 18 +} +``` + +### Usage Analytics + +``` +GET /api/v1/account/usage/analytics/?days=30 +``` + +Response: +```json +{ + "period_days": 30, + "start_date": "2025-12-06", + "end_date": "2026-01-05", + "current_balance": 9500, + "total_usage": 500, + "total_purchases": 0, + "usage_by_type": [ + { "transaction_type": "content_generation", "total": -350, "count": 15 }, + { "transaction_type": "image_generation", "total": -100, "count": 20 }, + { "transaction_type": "clustering", "total": -50, "count": 10 } + ], + "daily_usage": [ + { "date": "2026-01-05", "usage": 25, "purchases": 0, "net": -25 } + ] } ``` --- -## Credit Reset +## Credit Allocation -Credits reset monthly based on billing cycle: +Credits are added to `Account.credits` when: -1. **Monthly Reset Job** runs at period end -2. **Unused credits** do not roll over -3. **Purchased credits** may have different expiry +1. **Subscription Renewal** - `Plan.included_credits` added monthly +2. **Payment Approval** - Manual payments approved by admin +3. **Credit Purchase** - Credit packages bought by user +4. **Admin Adjustment** - Manual credit grants/adjustments -### Celery Task +### Monthly Reset + +Monthly limits (Ahrefs queries) reset on billing cycle: ```python -@celery.task -def reset_monthly_credits(): - """ - Run daily, resets credits for accounts - whose period_end is today - """ - today = date.today() - balances = CreditBalance.objects.filter(period_end=today) - - for balance in balances: - credit_service.reset_monthly_credits(balance.account) +# In Account model +def reset_monthly_usage(self): + """Reset monthly usage counters (called on billing cycle renewal)""" + self.usage_ahrefs_queries = 0 + self.save(update_fields=['usage_ahrefs_queries']) ``` --- @@ -380,20 +426,29 @@ def reset_monthly_credits(): Via Django Admin or API: ```python +from igny8_core.business.billing.services.credit_service import CreditService + # Add credits -credit_service.add_credits( +CreditService.add_credits( account=account, - credit_type='content', - amount=100, - reason='Customer support adjustment' + amount=1000, + transaction_type='adjustment', + description='Customer support adjustment' ) ``` ### Usage Audit -All credit changes logged in `CreditUsage` with: +All credit changes logged in `CreditTransaction` with: - Timestamp -- User who triggered +- Transaction type +- Amount (positive or negative) +- Balance after transaction +- Description + +All AI operations logged in `CreditUsageLog` with: - Operation type -- Amount deducted -- Related object ID +- Credits used +- Model used +- Token counts +- Related object metadata diff --git a/docs/plans/CREDITS-LIMITS-AUDIT-REPORT.md b/docs/plans/CREDITS-LIMITS-AUDIT-REPORT.md new file mode 100644 index 00000000..238497d9 --- /dev/null +++ b/docs/plans/CREDITS-LIMITS-AUDIT-REPORT.md @@ -0,0 +1,2449 @@ +# 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 + +

Current Plan: {planName}

+
Credits: {creditBalance.credits}
+
Used This Month: {creditBalance.credits_used_this_month}
+
Monthly Allowance: {creditBalance.plan_credits_per_month}
+
+``` + +**Usage Analytics - Limits Tab:** +```tsx + +
Credits Left: {creditBalance.credits}
+
Credits Used This Month: {creditBalance.credits_used_this_month}
+
Your Monthly Limit: {creditBalance.plan_credits_per_month}
+
+``` + +### 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 +
+ {/* Credit Balance Card */} + + +
Credits Available
+
{credits.toLocaleString()}
+
+ + {/* Sites Used Card */} + + +
Sites
+
{sitesCount} / {maxSites}
+
+ + {/* Team Members Card */} + + +
Team Members
+
{usersCount} / {maxUsers}
+
+ + {/* Keywords Card */} + + +
Keywords
+
{keywordsCount.toLocaleString()} / {maxKeywords.toLocaleString()}
+
+
+ +{/* Period Selector */} +
+ + + +
+``` + +**Tab 3: Credit Insights (NEW)** +```tsx +{/* Credits by Site */} + +

Credits by Site

+ {sites.map(site => ( +
+
{site.name}
+ +
{site.credits_used.toLocaleString()} credits
+
+ ))} +
+ +{/* Credits by Content Type */} + +

Credits by Content Type

+ +
+ +{/* Credits by Image Quality */} + +

Image Generation by Quality

+ {imageQualityBreakdown.map(tier => ( +
+
{tier.quality} Quality
+
{tier.count} images • {tier.credits_used} credits
+
({tier.credits_per_image} credits each)
+
+ ))} +
+ +{/* Credits by Automation */} + +

Automation Runs

+ {automations.map(auto => ( +
+
{auto.name}
+
{auto.runs_count} runs • {auto.credits_used} credits
+
+ ))} +
+ +{/* Timeline Chart */} + +

Credit Usage Over Time

+ +
+``` + +#### 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 { + 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: , + 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: , color: 'success' }, + users: { icon: , color: 'info' }, + keywords: { icon: , 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('browse'); + const [ahrefsLimit, setAhrefsLimit] = useState({ used: 0, limit: 50 }); + + return ( + <> + + + {/* Tab Selector */} +
+ + + +
+ + {/* Tab Content */} + {activeTab === 'browse' && ( + + )} + + {activeTab === 'ahrefs' && ( + { + setAhrefsLimit(prev => ({ + ...prev, + used: prev.used + 1 + })); + }} + /> + )} + + ); +} +``` + +**Browse Keywords Panel (Existing SeedKeywords):** +```tsx +function BrowseKeywordsPanel() { + return ( + +

Pre-Researched High-Opportunity Keywords

+

Browse thousands of analyzed keywords, ready to use.

+ + {/* Filters */} +
+ + +
+ + {/* Results Table */} + + + + + + + + + + + + {keywords.map(kw => ( + + + + + + + + ))} + +
KeywordSearch VolumeDifficultyOpportunity Score
{kw.keyword}{kw.search_volume}{kw.difficulty}{kw.opportunity_score}/100 + +
+
+ ); +} +``` + +**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 ( + +
+

Live Keyword Research with Ahrefs

+ 10 ? 'success' : 'warning'}> + {remaining} / {limit.limit} queries remaining + +
+ + {!canQuery && ( + + +
+
No queries remaining
+
Your limit resets on {resetDate}. Upgrade for more queries.
+ +
+
+ )} + + {canQuery && ( + <> +
+ setQuery(e.target.value)} + onKeyPress={e => e.key === 'Enter' && handleSearch()} + /> + +
+ + {/* Results */} + {results.length > 0 && ( + + + + + + + + + + + + {results.map((kw, idx) => ( + + + + + + + + ))} + +
KeywordVolumeDifficultyCPC
{kw.keyword}{kw.volume.toLocaleString()}{kw.difficulty}${kw.cpc} + +
+ )} + + )} +
+ ); +} +``` + +--- + +### 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 ; +} +``` + +--- + +### 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 + +

{plan.name} Plan

+
${plan.price}/month
+
Renews on {renewalDate}
+ + {/* Brief Summary - NO detailed limits */} +
+ Your plan includes: {plan.included_credits} credits per month • + {plan.max_sites} sites • {plan.max_users} team members +
+ + {/* Upgrade CTA */} + +
+ +{/* ❌ 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 */} +
+ } + label="Credits Available" + value={credits.toLocaleString()} + color="brand" + /> + } + label="Sites" + value={`${sitesCount} / ${maxSites}`} + color="success" + /> + } + label="Team Members" + value={`${usersCount} / ${maxUsers}`} + color="info" + /> + } + label="Keywords" + value={`${keywordsCount} / ${maxKeywords}`} + color="purple" + /> +
+ +{/* Period Selector */} + + + + + + +{/* Top Metrics for Selected Period */} +
+ +
Credits Spent
+
{periodCredits.toLocaleString()}
+
+ +
Articles Created
+
{periodArticles}
+
+ +
Images Generated
+
{periodImages}
+
+
+``` + +**Tab 2: Your Limits** +```tsx +{/* Only 4 limits total */} +} + current={sitesCount} + limit={maxSites} + type="permanent" +/> + +} + current={usersCount} + limit={maxUsers} + type="permanent" +/> + +} + current={keywordsCount} + limit={maxKeywords} + type="permanent" +/> + +} + current={ahrefsUsed} + limit={ahrefsLimit} + type="monthly" + daysUntilReset={daysUntilReset} +/> +``` + +**Tab 3: Credit Insights (NEW)** +```tsx +{/* Multi-dimensional breakdowns */} + +{/* By Site */} + +

Credits by Site

+
See which sites consume the most credits
+ {insights.by_site.map(site => ( +
+
+ {site.name} + {site.credits_used.toLocaleString()} credits ({site.percentage}%) +
+ +
+ ))} +
+ +{/* By Action Type */} + +

Credits by Action Type

+ +
+ {insights.by_operation.map(op => ( +
+ {op.display_name} + {op.credits_used} credits ({op.count} times) +
+ ))} +
+
+ +{/* By Image Quality */} + +

Image Generation Breakdown

+
Credits vary by quality tier
+ {insights.by_image_quality.map(tier => ( +
+
{tier.quality_tier} Quality
+
{tier.images_generated} images × {tier.credits_per_image} credits = {tier.total_credits} credits
+ {tier.model_name} +
+ ))} +
+ +{/* By Automation */} + +

Automation Runs

+ {insights.by_automation.map(auto => ( +
+
{auto.name}
+
{auto.runs_count} runs • {auto.credits_used} credits total
+
+ ))} +
+ +{/* Timeline Chart */} + +

Credit Usage Over Time

+ +
+``` + +**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: , + 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: , + 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 diff --git a/docs/plans/CREDITS-LIMITS-IMPLEMENTATION-PLAN.md b/docs/plans/CREDITS-LIMITS-IMPLEMENTATION-PLAN.md new file mode 100644 index 00000000..ae9904b0 --- /dev/null +++ b/docs/plans/CREDITS-LIMITS-IMPLEMENTATION-PLAN.md @@ -0,0 +1,2578 @@ +# 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 + +

Current Plan: {planName}

+
Credits: {creditBalance.credits}
+
Used This Month: {creditBalance.credits_used_this_month}
+
Monthly Allowance: {creditBalance.plan_credits_per_month}
+
+``` + +**Usage Analytics - Limits Tab:** +```tsx + +
Credits Left: {creditBalance.credits}
+
Credits Used This Month: {creditBalance.credits_used_this_month}
+
Your Monthly Limit: {creditBalance.plan_credits_per_month}
+
+``` + +### 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 +
+ {/* Credit Balance Card */} + + +
Credits Available
+
{credits.toLocaleString()}
+
+ + {/* Sites Used Card */} + + +
Sites
+
{sitesCount} / {maxSites}
+
+ + {/* Team Members Card */} + + +
Team Members
+
{usersCount} / {maxUsers}
+
+ + {/* Keywords Card */} + + +
Keywords
+
{keywordsCount.toLocaleString()} / {maxKeywords.toLocaleString()}
+
+
+ +{/* Period Selector */} +
+ + + +
+``` + +**Tab 3: Credit Insights (NEW)** +```tsx +{/* Credits by Site */} + +

Credits by Site

+ {sites.map(site => ( +
+
{site.name}
+ +
{site.credits_used.toLocaleString()} credits
+
+ ))} +
+ +{/* Credits by Content Type */} + +

Credits by Content Type

+ +
+ +{/* Credits by Image Quality */} + +

Image Generation by Quality

+ {imageQualityBreakdown.map(tier => ( +
+
{tier.quality} Quality
+
{tier.count} images • {tier.credits_used} credits
+
({tier.credits_per_image} credits each)
+
+ ))} +
+ +{/* Credits by Automation */} + +

Automation Runs

+ {automations.map(auto => ( +
+
{auto.name}
+
{auto.runs_count} runs • {auto.credits_used} credits
+
+ ))} +
+ +{/* Timeline Chart */} + +

Credit Usage Over Time

+ +
+``` + +#### 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 { + 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: , + 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: , color: 'success' }, + users: { icon: , color: 'info' }, + keywords: { icon: , 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('browse'); + const [ahrefsLimit, setAhrefsLimit] = useState({ used: 0, limit: 50 }); + + return ( + <> + + + {/* Tab Selector */} +
+ + + +
+ + {/* Tab Content */} + {activeTab === 'browse' && ( + + )} + + {activeTab === 'ahrefs' && ( + { + setAhrefsLimit(prev => ({ + ...prev, + used: prev.used + 1 + })); + }} + /> + )} + + ); +} +``` + +**Browse Keywords Panel (Existing SeedKeywords):** +```tsx +function BrowseKeywordsPanel() { + return ( + +

Pre-Researched High-Opportunity Keywords

+

Browse thousands of analyzed keywords, ready to use.

+ + {/* Filters */} +
+ + +
+ + {/* Results Table */} + + + + + + + + + + + + {keywords.map(kw => ( + + + + + + + + ))} + +
KeywordSearch VolumeDifficultyOpportunity Score
{kw.keyword}{kw.search_volume}{kw.difficulty}{kw.opportunity_score}/100 + +
+
+ ); +} +``` + +**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 ( + +
+

Live Keyword Research with Ahrefs

+ 10 ? 'success' : 'warning'}> + {remaining} / {limit.limit} queries remaining + +
+ + {!canQuery && ( + + +
+
No queries remaining
+
Your limit resets on {resetDate}. Upgrade for more queries.
+ +
+
+ )} + + {canQuery && ( + <> +
+ setQuery(e.target.value)} + onKeyPress={e => e.key === 'Enter' && handleSearch()} + /> + +
+ + {/* Results */} + {results.length > 0 && ( + + + + + + + + + + + + {results.map((kw, idx) => ( + + + + + + + + ))} + +
KeywordVolumeDifficultyCPC
{kw.keyword}{kw.volume.toLocaleString()}{kw.difficulty}${kw.cpc} + +
+ )} + + )} +
+ ); +} +``` + +--- + +### 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 ; +} +``` + +--- + +### 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 + +

{plan.name} Plan

+
${plan.price}/month
+
Renews on {renewalDate}
+ + {/* Brief Summary - NO detailed limits */} +
+ Your plan includes: {plan.included_credits} credits per month • + {plan.max_sites} sites • {plan.max_users} team members +
+ + {/* Upgrade CTA */} + +
+ +{/* ❌ 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 */} +
+ } + label="Credits Available" + value={credits.toLocaleString()} + color="brand" + /> + } + label="Sites" + value={`${sitesCount} / ${maxSites}`} + color="success" + /> + } + label="Team Members" + value={`${usersCount} / ${maxUsers}`} + color="info" + /> + } + label="Keywords" + value={`${keywordsCount} / ${maxKeywords}`} + color="purple" + /> +
+ +{/* Period Selector */} + + + + + + +{/* Top Metrics for Selected Period */} +
+ +
Credits Spent
+
{periodCredits.toLocaleString()}
+
+ +
Articles Created
+
{periodArticles}
+
+ +
Images Generated
+
{periodImages}
+
+
+``` + +**Tab 2: Your Limits** +```tsx +{/* Only 4 limits total */} +} + current={sitesCount} + limit={maxSites} + type="permanent" +/> + +} + current={usersCount} + limit={maxUsers} + type="permanent" +/> + +} + current={keywordsCount} + limit={maxKeywords} + type="permanent" +/> + +} + current={ahrefsUsed} + limit={ahrefsLimit} + type="monthly" + daysUntilReset={daysUntilReset} +/> +``` + +**Tab 3: Credit Insights (NEW)** +```tsx +{/* Multi-dimensional breakdowns */} + +{/* By Site */} + +

Credits by Site

+
See which sites consume the most credits
+ {insights.by_site.map(site => ( +
+
+ {site.name} + {site.credits_used.toLocaleString()} credits ({site.percentage}%) +
+ +
+ ))} +
+ +{/* By Action Type */} + +

Credits by Action Type

+ +
+ {insights.by_operation.map(op => ( +
+ {op.display_name} + {op.credits_used} credits ({op.count} times) +
+ ))} +
+
+ +{/* By Image Quality */} + +

Image Generation Breakdown

+
Credits vary by quality tier
+ {insights.by_image_quality.map(tier => ( +
+
{tier.quality_tier} Quality
+
{tier.images_generated} images × {tier.credits_per_image} credits = {tier.total_credits} credits
+ {tier.model_name} +
+ ))} +
+ +{/* By Automation */} + +

Automation Runs

+ {insights.by_automation.map(auto => ( +
+
{auto.name}
+
{auto.runs_count} runs • {auto.credits_used} credits total
+
+ ))} +
+ +{/* Timeline Chart */} + +

Credit Usage Over Time

+ +
+``` + +**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: , + 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: , + 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 + +--- + +### Before (Complex & Confusing) +- 10+ limit fields (max_content_ideas, max_content_words, max_images_basic, max_images_premium, etc.) +- Duplicate data across Plans & Billing and Usage pages +- Inconsistent enforcement +- Technical terminology ("API Activity") +- Double limiting (credits + monthly limits) +- User confusion: "I have credits but can't generate content?" + +### After (Simple & Clear) +- **4 limits only:** Sites, Users, Keywords, Ahrefs Queries +- **Everything else = Credits:** Let users consume how they want +- Clear page separation: Plans & Billing (financial) vs Usage (consumption) +- Multi-dimensional insights for optimization +- User-friendly language throughout +- Proper enforcement everywhere +- Simple upgrade paths + +### What Changes + +| Area | Before | After | +|------|--------|-------| +| **Limits** | 10+ different limits | 4 limits only | +| **Plan Model** | 12 limit fields | 4 limit fields | +| **Account Model** | 5 usage tracking fields | 1 usage tracking field | +| **Pages** | Duplicate data everywhere | Clean separation | +| **Terminology** | "API", "operations" | "Actions", "activities" | +| **Enforcement** | Inconsistent | Consistent everywhere | +| **Insights** | Basic usage log | Multi-dimensional analysis | +| **Keyword Research** | One way (SeedKeywords) | Two ways (Browse + Ahrefs) | + +--- + +## 18. Next Steps for Development Team + +### Backend Developer Tasks + +1. **Week 1:** + - Review this implementation plan + - Create database migration script + - Test migration on local/staging with production data clone + - Update models (Plan, Account) + - Update LimitService + - Update serializers + - Verify all tests pass + +2. **Week 2:** + - Add keyword limit enforcement to all entry points + - Update automation service with credit pre-check + - Search codebase for monthly limit checks and remove + - Add Ahrefs service skeleton + - Create credit insights API endpoint + - Write tests for new validations + +3. **Week 3:** + - Code review + - Deploy to staging + - Integration testing + - Performance testing + +### Frontend Developer Tasks + +1. **Week 1:** + - Review this implementation plan + - Update Plans & Billing page (remove duplicates) + - Update Usage page structure + - Remove unused limit displays + - Update terminology throughout + +2. **Week 2:** + - Build Credit Insights tab with widgets + - Build Keyword Research page + - Add Ahrefs research panel + - Update TypeScript interfaces + - Add credit pre-check hooks + +3. **Week 3:** + - Testing & bug fixes + - Deploy to staging + - User acceptance testing + +### QA Tasks + +- Execute full testing checklist (see Section 10) +- Regression testing on existing features +- Performance testing (page load times, API response times) +- Edge case testing (limits, credits, errors) +- Cross-browser testing +- Mobile responsiveness testing + +### DevOps Tasks + +- Database backup strategy +- Migration rollback plan +- Staging deployment +- Production deployment plan +- Monitoring setup +- Alerting configuration + +--- + +## 19. Key Implementation Principles + +### Principle 1: Simplicity First +**Remove complexity wherever possible. If a limit isn't absolutely necessary, remove it.** + +### Principle 2: Credit-Based Philosophy +**Credits are the primary control mechanism. Hard limits only for true boundaries (sites, users, keywords, Ahrefs).** + +### Principle 3: Clear Separation +**Plans & Billing = "What am I paying for?" Usage = "What am I using?"** + +### Principle 4: User-Friendly Language +**No technical jargon. Write for business users, not developers.** + +### Principle 5: Proper Enforcement +**Every limit must be checked at every entry point. No exceptions.** + +### Principle 6: Actionable Insights +**Don't just show numbers. Show breakdowns that help users optimize their usage.** + +### Principle 7: Clear Upgrade Paths +**When users hit limits, show clear value proposition for upgrading.** + +--- + +## 20. Conclusion + +This implementation plan transforms the IGNY8 credits and limits system from complex and confusing to simple and powerful. + +**Core Changes:** +- **4 limits only** (sites, users, keywords, Ahrefs queries) +- **Remove 10+ unused limit fields** from database +- **Eliminate data duplication** across pages +- **Add multi-dimensional insights** for better optimization +- **Implement Ahrefs research** with monthly query limits +- **Enforce all limits properly** at every entry point +- **Validate credit balance** before all operations +- **Use user-friendly terminology** throughout + +**Expected Outcomes:** +- Reduced user confusion +- Fewer support tickets +- Higher upgrade conversion +- Better credit management +- Clearer value proposition +- Scalable system architecture + +**Timeline:** 5 weeks from start to production deployment + +**Status:** ✅ **READY FOR IMPLEMENTATION** + +--- + +**Document prepared by:** GitHub Copilot (Claude Sonnet 4.5) +**Date:** January 5, 2026 +**Next Review:** After Week 1 completion +**Approval Required:** Backend Lead, Frontend Lead, Product Manager + +--- + +**End of Implementation Plan** + +*For questions or clarification during implementation:* +- See `docs/10-MODULES/BILLING.md` for current system +- See `docs/40-WORKFLOWS/CREDIT-SYSTEM.md` for credit workflows +- Review `backend/igny8_core/business/billing/services/` for service architecture +- Check this document's commit history for rationale behind decisions diff --git a/docs/plans/IMPLEMENTATION-SUMMARY.md b/docs/plans/IMPLEMENTATION-SUMMARY.md new file mode 100644 index 00000000..b890a789 --- /dev/null +++ b/docs/plans/IMPLEMENTATION-SUMMARY.md @@ -0,0 +1,274 @@ +# Credits & Limits Implementation - Quick Summary + +**Status:** 🚧 READY TO IMPLEMENT +**Timeline:** 5 weeks +**Priority:** HIGH + +--- + +## The Big Picture + +### What We're Doing +Simplifying the IGNY8 credits and limits system from complex (10+ limits) to simple (4 limits only). + +### Core Philosophy +**Keep only 4 hard limits. Everything else = credits.** + +--- + +## The 4 Limits (FINAL) + +| Limit | Type | What It Controls | +|-------|------|-----------------| +| **Sites** | Hard | Max sites per account (e.g., 1, 2, 5, unlimited) | +| **Team Users** | Hard | Max team members (e.g., 1, 2, 3, 5) | +| **Keywords** | Hard | Total keywords in workspace (e.g., 100, 1K, 5K, 20K) | +| **Ahrefs Queries** | Monthly | Live keyword research per month (e.g., 0, 50, 200, 500) | + +**Everything else (content, images, ideas, etc.) = credits only.** + +--- + +## What Gets REMOVED + +### Database Fields to Delete + +**From Plan Model:** +- ❌ `max_content_ideas` +- ❌ `max_content_words` +- ❌ `max_images_basic` +- ❌ `max_images_premium` +- ❌ `max_image_prompts` +- ❌ `max_clusters` (consider merging with keywords) + +**From Account Model:** +- ❌ `usage_content_ideas` +- ❌ `usage_content_words` +- ❌ `usage_images_basic` +- ❌ `usage_images_premium` +- ❌ `usage_image_prompts` + +### Why? +- Confusing for users (double limiting) +- Maintenance overhead +- Credit system already provides control + +--- + +## What Gets ADDED + +### New Fields + +**Plan Model:** +```python +max_ahrefs_queries = models.IntegerField(default=50) +``` + +**Account Model:** +```python +usage_ahrefs_queries = models.IntegerField(default=0) +``` + +### New Feature: Keyword Research + +**Two ways to add keywords:** + +1. **Browse Pre-Researched Keywords** (FREE) + - IGNY8's global keyword database + - Pre-analyzed, ready to use + - Limited by: `max_keywords` (workspace limit) + +2. **Research with Ahrefs** (LIMITED) + - Live Ahrefs API queries + - Fresh, custom keyword data + - Limited by: `max_ahrefs_queries` (monthly limit) + +--- + +## Page Changes + +### Plans & Billing Page (Simplified) + +**Current Plan Tab - BEFORE:** +- ❌ Credit balance display +- ❌ Usage charts +- ❌ Limit progress bars +- ❌ "Credits used this month" breakdown + +**Current Plan Tab - AFTER:** +- ✅ Plan name, price, renewal date +- ✅ Brief summary: "50 articles • 2 sites • 2 users" +- ✅ Upgrade CTA +- ❌ NO detailed usage (moved to Usage page) + +### Usage Page (Enhanced) + +**NEW Tab Structure:** + +1. **Overview** (NEW) + - Quick stats cards (credits, sites, users, keywords) + - Period selector (7, 30, 90 days) + - Top metrics + +2. **Your Limits** + - Only 4 limits with progress bars + - Sites, Users, Keywords, Ahrefs Queries + +3. **Credit Insights** (NEW) + - Credits by Site + - Credits by Action Type + - Credits by Image Quality (basic/quality/premium) + - Credits by Automation + - Timeline chart + +4. **Activity Log** + - Detailed transaction history + - (Renamed from "API Activity") + +--- + +## Key Implementation Tasks + +### Backend (Week 1-2) + +1. **Remove unused fields** + - Create migration to drop fields + - Update models, serializers + - Remove from LimitService mappings + +2. **Add Ahrefs fields** + - Add to Plan and Account models + - Add to LimitService mappings + - Create Ahrefs service + +3. **Enforce limits properly** + - Add keyword limit checks to ALL entry points + - Add automation credit pre-check + - Validate before all operations + +### Frontend (Week 2-3) + +1. **Clean up Plans & Billing** + - Remove duplicate credit/usage data + - Keep only financial info + +2. **Enhance Usage page** + - Add Overview tab + - Add Credit Insights tab with widgets + - Multi-dimensional breakdowns + +3. **Build Keyword Research** + - Browse panel (existing SeedKeywords) + - Ahrefs panel (new) + - Query limit indicator + +4. **Update terminology** + - Remove "API", "operations" + - Use "actions", "activities" + +--- + +## Validation Requirements + +### Must Check BEFORE Every Operation + +**All AI Operations:** +```python +# 1. Check credits +CreditService.check_credits(account, estimated_credits) + +# 2. Execute +result = ai_service.execute() + +# 3. Deduct +CreditService.deduct_credits_for_operation(...) +``` + +**Keyword Creation:** +```python +# Check limit +LimitService.check_hard_limit(account, 'keywords', count) + +# Then create +Keywords.objects.bulk_create([...]) +``` + +**Automation Runs:** +```python +# Estimate total cost +estimated = estimate_automation_cost(config) + +# Check BEFORE starting +CreditService.check_credits(account, estimated) + +# Then run +execute_automation(config) +``` + +--- + +## Success Criteria + +### Technical +- [ ] All unused fields removed +- [ ] 4 limits properly enforced +- [ ] Credit checks before ALL operations +- [ ] Automation pre-checks credits +- [ ] No duplicate data across pages + +### User Experience +- [ ] Simple 4-limit model is clear +- [ ] Multi-dimensional insights are actionable +- [ ] Keyword research flow is intuitive +- [ ] Error messages are user-friendly +- [ ] Upgrade prompts at right moments + +### Business +- [ ] Reduced support questions +- [ ] Higher upgrade conversion +- [ ] Better credit visibility +- [ ] System scales cleanly + +--- + +## Suggested Plan Values + +| Plan | Price | Credits/mo | Sites | Users | Keywords | Ahrefs/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 | ∞ | 5 | 20,000 | 500 | + +--- + +## Timeline + +**Week 1:** Backend cleanup (remove fields, add Ahrefs) +**Week 2:** Enforcement (keyword limits, automation checks) +**Week 3:** Frontend cleanup (remove duplicates, update UI) +**Week 4:** New features (Credit Insights, Keyword Research) +**Week 5:** Testing & Production deployment + +--- + +## Files to Review + +**Full Implementation Plan:** +- `docs/plans/CREDITS-LIMITS-IMPLEMENTATION-PLAN.md` + +**Current System Docs:** +- `docs/10-MODULES/BILLING.md` +- `docs/40-WORKFLOWS/CREDIT-SYSTEM.md` + +**Code:** +- Backend: `backend/igny8_core/auth/models.py` +- Backend: `backend/igny8_core/business/billing/services/limit_service.py` +- Frontend: `frontend/src/pages/account/PlansAndBillingPage.tsx` +- Frontend: `frontend/src/pages/account/UsageAnalyticsPage.tsx` + +--- + +**Status:** ✅ READY FOR TEAM REVIEW AND IMPLEMENTATION + +**Next Step:** Schedule implementation kickoff meeting with backend, frontend, and QA leads. diff --git a/docs/plans/SYSTEM-ARCHITECTURE-DIAGRAM.md b/docs/plans/SYSTEM-ARCHITECTURE-DIAGRAM.md new file mode 100644 index 00000000..aec36b58 --- /dev/null +++ b/docs/plans/SYSTEM-ARCHITECTURE-DIAGRAM.md @@ -0,0 +1,356 @@ +# System Architecture - Before vs After + +## BEFORE (Complex & Confusing) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ PLAN MODEL (BEFORE) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Hard Limits (Never Reset): │ +│ ├─ max_sites ✅ Keep │ +│ ├─ max_users ✅ Keep │ +│ ├─ max_keywords ✅ Keep │ +│ └─ max_clusters ❌ Remove │ +│ │ +│ Monthly Limits (Reset Every Month): │ +│ ├─ max_content_ideas ❌ Remove │ +│ ├─ max_content_words ❌ Remove │ +│ ├─ max_images_basic ❌ Remove │ +│ ├─ max_images_premium ❌ Remove │ +│ └─ max_image_prompts ❌ Remove │ +│ │ +│ Credits: │ +│ └─ included_credits ✅ Keep │ +│ │ +│ TOTAL LIMITS: 10 fields ❌ TOO COMPLEX │ +│ │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ ACCOUNT MODEL (BEFORE) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Credits: │ +│ └─ credits ✅ Keep │ +│ │ +│ Monthly Usage Tracking: │ +│ ├─ usage_content_ideas ❌ Remove │ +│ ├─ usage_content_words ❌ Remove │ +│ ├─ usage_images_basic ❌ Remove │ +│ ├─ usage_images_premium ❌ Remove │ +│ └─ usage_image_prompts ❌ Remove │ +│ │ +│ Period Tracking: │ +│ ├─ usage_period_start ✅ Keep │ +│ └─ usage_period_end ✅ Keep │ +│ │ +│ TOTAL USAGE FIELDS: 5 ❌ UNNECESSARY │ +│ │ +└─────────────────────────────────────────────────────────────────┘ + +USER CONFUSION: +"I have 5000 credits but can't generate content because I hit my +monthly word limit? This makes no sense!" +``` + +--- + +## AFTER (Simple & Clear) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ PLAN MODEL (AFTER) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ THE ONLY 4 LIMITS: │ +│ ├─ max_sites (e.g., 1, 2, 5, unlimited) │ +│ ├─ max_users (e.g., 1, 2, 3, 5) │ +│ ├─ max_keywords (e.g., 100, 1K, 5K, 20K) │ +│ └─ max_ahrefs_queries (e.g., 0, 50, 200, 500) [NEW] │ +│ │ +│ Credits: │ +│ └─ included_credits (e.g., 2K, 10K, 40K, 120K) │ +│ │ +│ TOTAL LIMITS: 4 fields ✅ SIMPLE & CLEAR │ +│ │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ ACCOUNT MODEL (AFTER) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Credits: │ +│ └─ credits (current balance) │ +│ │ +│ Monthly Usage Tracking: │ +│ └─ usage_ahrefs_queries (only 1 tracker needed) [NEW] │ +│ │ +│ Period Tracking: │ +│ ├─ usage_period_start │ +│ └─ usage_period_end │ +│ │ +│ TOTAL USAGE FIELDS: 1 ✅ CLEAN │ +│ │ +└─────────────────────────────────────────────────────────────────┘ + +USER CLARITY: +"I have 5000 credits. I can use them for whatever I need - +articles, images, ideas. Simple!" +``` + +--- + +## Page Structure - Before vs After + +### BEFORE (Duplicate Data Everywhere) + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ PLANS & BILLING PAGE │ +├───────────────────────────────────────────────────────────────────┤ +│ Tab 1: Current Plan │ +│ ├─ Plan name, price ✅ │ +│ ├─ Credit balance ⚠️ DUPLICATE (also in Usage) │ +│ ├─ Credits used ⚠️ DUPLICATE (also in Usage) │ +│ ├─ Usage charts ⚠️ DUPLICATE (also in Usage) │ +│ └─ Limit bars ⚠️ DUPLICATE (also in Usage) │ +│ │ +│ Tab 2: Upgrade │ +│ └─ Pricing table ✅ │ +│ │ +│ Tab 3: History │ +│ ├─ Invoices ✅ │ +│ └─ Transactions ⚠️ PARTIAL DUPLICATE (also in Usage) │ +└───────────────────────────────────────────────────────────────────┘ + +┌───────────────────────────────────────────────────────────────────┐ +│ USAGE PAGE │ +├───────────────────────────────────────────────────────────────────┤ +│ Tab 1: Limits & Usage │ +│ ├─ Credit balance ⚠️ DUPLICATE (also in Plans & Billing) │ +│ ├─ Credits used ⚠️ DUPLICATE (also in Plans & Billing) │ +│ ├─ Limit bars ⚠️ DUPLICATE (also in Plans & Billing) │ +│ └─ 10+ limit types ❌ TOO MANY │ +│ │ +│ Tab 2: Credit History │ +│ └─ Transactions ⚠️ PARTIAL DUPLICATE (also in Plans & Billing)│ +│ │ +│ Tab 3: API Activity ❌ TECHNICAL TERMINOLOGY │ +│ └─ Operation logs │ +└───────────────────────────────────────────────────────────────────┘ +``` + +### AFTER (Clear Separation) + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ PLANS & BILLING PAGE (Financial Focus) │ +├───────────────────────────────────────────────────────────────────┤ +│ Tab 1: Current Plan │ +│ ├─ Plan name, price, renewal date ✅ │ +│ ├─ Brief summary: "50 articles • 2 sites • 2 users" ✅ │ +│ ├─ Upgrade CTA ✅ │ +│ └─ ❌ NO usage details (moved to Usage page) │ +│ │ +│ Tab 2: Upgrade Plan │ +│ └─ Pricing table ✅ │ +│ │ +│ Tab 3: Billing History │ +│ ├─ Invoices ✅ │ +│ ├─ Payment methods ✅ │ +│ └─ Credit purchases ✅ (financial transactions only) │ +└───────────────────────────────────────────────────────────────────┘ + +┌───────────────────────────────────────────────────────────────────┐ +│ USAGE PAGE (Consumption Tracking) │ +├───────────────────────────────────────────────────────────────────┤ +│ Tab 1: Overview [NEW] │ +│ ├─ Quick stats: Credits, Sites, Users, Keywords ✅ │ +│ ├─ Period selector: 7/30/90 days ✅ │ +│ └─ Top metrics for selected period ✅ │ +│ │ +│ Tab 2: Your Limits │ +│ └─ ONLY 4 limits with progress bars ✅ │ +│ ├─ Sites (e.g., 2 / 5) │ +│ ├─ Users (e.g., 2 / 3) │ +│ ├─ Keywords (e.g., 847 / 1,000) │ +│ └─ Ahrefs Queries (e.g., 23 / 50 this month) │ +│ │ +│ Tab 3: Credit Insights [NEW] │ +│ ├─ Credits by Site 📊 │ +│ ├─ Credits by Action Type 📊 │ +│ ├─ Credits by Image Quality 📊 │ +│ ├─ Credits by Automation 📊 │ +│ └─ Timeline chart 📈 │ +│ │ +│ Tab 4: Activity Log (renamed from "API Activity") │ +│ └─ Detailed transaction history ✅ │ +└───────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Credit Flow - Before vs After + +### BEFORE (Double Limiting) + +``` +User wants to generate content + ↓ +Check 1: Do they have credits? ✅ Yes, 5000 credits + ↓ +Check 2: Have they hit monthly word limit? ❌ YES, 100K/100K + ↓ +BLOCKED! "You've reached your monthly word limit" + ↓ +User: "But I have 5000 credits left! 😤" +``` + +### AFTER (Simple Credit-Based) + +``` +User wants to generate content + ↓ +Check: Do they have credits? ✅ Yes, 5000 credits + ↓ +Generate content (costs ~50 credits based on tokens) + ↓ +Deduct credits: 5000 - 50 = 4950 remaining + ↓ +User: "Simple! I can use my credits however I want 😊" +``` + +--- + +## Keyword Research - NEW Structure + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ KEYWORD RESEARCH PAGE [NEW] │ +├───────────────────────────────────────────────────────────────────┤ +│ │ +│ Tab Selector: │ +│ [Browse Pre-Researched] [Research with Ahrefs - 42/50 left] │ +│ │ +├───────────────────────────────────────────────────────────────────┤ +│ Option 1: Browse Pre-Researched Keywords (FREE) │ +│ ─────────────────────────────────────────────── │ +│ • Global IGNY8 keyword database │ +│ • Filter by: Industry, Sector, Country │ +│ • Pre-analyzed metrics: Volume, Difficulty, Opportunity │ +│ • Free to browse and add │ +│ • Adds to workspace (counts toward max_keywords) │ +│ │ +│ [Industry ▼] [Sector ▼] [Country ▼] [Search...] │ +│ │ +│ Results: │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Keyword │ Volume │ Diff │ Score │ [Add] │ │ +│ ├─────────────────────────────────────────────────────────┤ │ +│ │ digital marketing │ 45K │ 65 │ 88/100 │ [Add] │ │ +│ │ content strategy │ 12K │ 42 │ 92/100 │ [Add] │ │ +│ │ seo optimization │ 33K │ 58 │ 85/100 │ [Add] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +├───────────────────────────────────────────────────────────────────┤ +│ Option 2: Research with Ahrefs (LIMITED) │ +│ ─────────────────────────────────────────── │ +│ • Live Ahrefs API queries │ +│ • Fresh, custom keyword data │ +│ • Monthly limit: 42 / 50 queries remaining ⚠️ │ +│ • Resets: February 1, 2026 │ +│ │ +│ [Enter seed keyword...] [Research Keywords] │ +│ │ +│ ⚠️ Warning: Each search uses 1 query from your monthly limit │ +│ │ +│ Results (if searched): │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Keyword │ Volume │ Diff │ CPC │ [Add] │ │ +│ ├─────────────────────────────────────────────────────────┤ │ +│ │ ai content tools │ 8.9K │ 48 │ $4.50│ [Add] │ │ +│ │ automated writing │ 3.2K │ 35 │ $3.80│ [Add] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +└───────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Enforcement Points + +### Backend Validation (REQUIRED at these locations) + +``` +1. Keyword Creation + ├─ Manual keyword creation form + ├─ SeedKeyword import (from Browse tab) + ├─ Ahrefs import (from Research tab) + ├─ CSV/Excel bulk import + └─ API endpoint: POST /api/v1/keywords/ + + ✅ Must check: LimitService.check_hard_limit(account, 'keywords', count) + +2. Site Creation + └─ API endpoint: POST /api/v1/sites/ + + ✅ Must check: LimitService.check_hard_limit(account, 'sites', 1) + +3. User Invitation + └─ API endpoint: POST /api/v1/users/invite/ + + ✅ Must check: LimitService.check_hard_limit(account, 'users', 1) + +4. Ahrefs Query + └─ API endpoint: POST /api/v1/keywords/ahrefs/search/ + + ✅ Must check: LimitService.check_monthly_limit(account, 'ahrefs_queries', 1) + ✅ Must increment: LimitService.increment_usage(account, 'ahrefs_queries', 1) + +5. All AI Operations (Content, Images, Ideas, etc.) + + ✅ Must check: CreditService.check_credits(account, estimated_credits) + ✅ Must deduct: CreditService.deduct_credits_for_operation(...) + +6. Automation Runs + + ✅ Must pre-check: CreditService.check_credits(account, estimated_total) + ✅ Each stage deducts: CreditService.deduct_credits_for_operation(...) +``` + +--- + +## Error Messages (User-Friendly) + +### OLD (Technical) +``` +HTTP 402 - HardLimitExceededError: +max_keywords limit reached (1000/1000) +``` + +### NEW (User-Friendly) +``` +┌─────────────────────────────────────────────────────────────┐ +│ ⚠️ Keyword Limit Reached │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ You've reached your keyword limit of 1,000 keywords. │ +│ │ +│ Current workspace: 1,000 keywords │ +│ Your plan limit: 1,000 keywords │ +│ │ +│ To add more keywords, you can: │ +│ • Delete unused keywords to free up space │ +│ • Upgrade to Growth plan (5,000 keywords) │ +│ │ +│ [Delete Keywords] [Upgrade Plan] [Cancel] │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +**End of Visual Reference** + +See `CREDITS-LIMITS-IMPLEMENTATION-PLAN.md` for complete details. diff --git a/docs/plans/phase-3-plan/FINAL-CREDITS-LIMITS-PLAN.md b/docs/plans/phase-3-plan/FINAL-CREDITS-LIMITS-PLAN.md new file mode 100644 index 00000000..662dfaad --- /dev/null +++ b/docs/plans/phase-3-plan/FINAL-CREDITS-LIMITS-PLAN.md @@ -0,0 +1,653 @@ +# Final Credits & Limits Implementation Plan + +**Created:** January 5, 2026 +**Status:** ✅ COMPLETE (Pending UAT & Production Deployment) +**Last Updated:** January 5, 2026 +**Verified Against:** Backend codebase, Frontend codebase, Current implementation + +--- + +## Executive Summary + +After comprehensive codebase review, this document presents the verified and refined implementation plan for simplifying the credits and limits system. + +### Key Finding: Credit Flow is CORRECT ✅ + +The current credit architecture is **sound and logical**: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ CREDIT FLOW (VERIFIED) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Plan.included_credits = Monthly allocation (e.g., 10,000) │ +│ ↓ (Added on subscription renewal/approval) │ +│ Account.credits = Current balance (real-time, decremented) │ +│ ↓ (Decremented on each AI operation) │ +│ CreditTransaction = Log of all credit changes │ +│ CreditUsageLog = Detailed operation tracking │ +│ │ +│ THESE ARE NOT PARALLEL - They serve different purposes: │ +│ • Plan.included_credits = "How many credits per month" │ +│ • Account.credits = "How many credits you have RIGHT NOW" │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Where credits are added to Account.credits:** +1. `billing/views.py:445-465` - When manual payment is approved +2. `payment_service.py:219-249` - When credit package purchased +3. `credit_service.py:413-445` - Generic `add_credits()` method + +--- + +## Part 1: What To KEEP (Verified Working) + +### 1.1 Credit System (NO CHANGES NEEDED) + +| Component | Location | Status | +|-----------|----------|--------| +| `Account.credits` | `auth/models.py:83` | ✅ Keep - Real-time balance | +| `Plan.included_credits` | `auth/models.py:253` | ✅ Keep - Monthly allocation | +| `CreditService` | `billing/services/credit_service.py` | ✅ Keep - All methods working | +| `CreditTransaction` | `billing/models.py:26-48` | ✅ Keep - Audit trail | +| `CreditUsageLog` | `billing/models.py:90-130` | ✅ Keep - Operation tracking | + +### 1.2 Hard Limits to KEEP (4 only) + +| Limit | Plan Field | Current Location | Status | +|-------|------------|------------------|--------| +| Sites | `max_sites` | `auth/models.py:207` | ✅ Keep | +| Users | `max_users` | `auth/models.py:204` | ✅ Keep | +| Keywords | `max_keywords` | `auth/models.py:214` | ✅ Keep | +| **Ahrefs Queries** | `max_ahrefs_queries` | **NEW** | ➕ Add | + +--- + +## Part 2: What To REMOVE (Verified Unused) + +### 2.1 Remove From Plan Model + +These fields create confusing "double limiting" - credits already control consumption: + +```python +# auth/models.py - REMOVE these fields (lines 220-251) +max_clusters = ... # Remove - no real use +max_content_ideas = ... # Remove - credits handle this +max_content_words = ... # Remove - credits handle this +max_images_basic = ... # Remove - credits handle this +max_images_premium = ... # Remove - credits handle this +max_image_prompts = ... # Remove - credits handle this +``` + +**Why remove?** User confusion: "I have credits but can't generate because I hit my word limit?" + +### 2.2 Remove From Account Model + +```python +# auth/models.py - REMOVE these fields (lines 108-114) +usage_content_ideas = ... # Remove - tracked in CreditUsageLog +usage_content_words = ... # Remove - tracked in CreditUsageLog +usage_images_basic = ... # Remove - tracked in CreditUsageLog +usage_images_premium = ... # Remove - tracked in CreditUsageLog +usage_image_prompts = ... # Remove - tracked in CreditUsageLog +``` + +### 2.3 Update LimitService + +```python +# limit_service.py - Update HARD_LIMIT_MAPPINGS +HARD_LIMIT_MAPPINGS = { + 'sites': {...}, # Keep + 'users': {...}, # Keep + 'keywords': {...}, # Keep + # 'clusters': {...}, # REMOVE +} + +# limit_service.py - Update MONTHLY_LIMIT_MAPPINGS +MONTHLY_LIMIT_MAPPINGS = { + 'ahrefs_queries': { # NEW - only monthly limit + 'plan_field': 'max_ahrefs_queries', + 'usage_field': 'usage_ahrefs_queries', + 'display_name': 'Keyword Research Queries', + }, + # REMOVE all others (content_ideas, content_words, images_*, image_prompts) +} +``` + +--- + +## Part 3: Database Migration + +### Migration Script + +```python +# migrations/0XXX_simplify_credits_limits.py + +from django.db import migrations, models + +class Migration(migrations.Migration): + dependencies = [ + ('igny8_core_auth', '0XXX_previous'), # Update with actual + ] + + operations = [ + # STEP 1: Add new Ahrefs fields + migrations.AddField( + model_name='plan', + name='max_ahrefs_queries', + field=models.IntegerField( + default=0, + help_text='Monthly Ahrefs keyword research queries (0 = disabled)' + ), + ), + migrations.AddField( + model_name='account', + name='usage_ahrefs_queries', + field=models.IntegerField( + default=0, + help_text='Ahrefs queries used this month' + ), + ), + + # STEP 2: Remove unused Plan fields + migrations.RemoveField(model_name='plan', name='max_clusters'), + 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'), + + # STEP 3: Remove unused Account fields + 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'), + ] +``` + +--- + +## Part 4: Frontend Changes + +### 4.1 Plans & Billing Page (Financial Focus) + +**File:** `frontend/src/pages/account/PlansAndBillingPage.tsx` + +**Current (881 lines):** Shows credit usage charts, limit bars = DUPLICATE of Usage page + +**Target:** Pure financial focus +- Tab 1: Current Plan → Name, price, renewal date, "View Usage" link +- Tab 2: Upgrade Plan → Pricing table (already good) +- Tab 3: Billing History → Invoices, payment methods + +**Remove from Current Plan tab:** +- Usage charts (move to Usage page) +- Credit consumption breakdown (move to Usage page) +- Limit progress bars (move to Usage page) + +### 4.2 Usage Analytics Page (Consumption Tracking) + +**File:** `frontend/src/pages/account/UsageAnalyticsPage.tsx` + +**Current (266 lines):** Basic tabs with some usage data + +**Target:** Comprehensive usage tracking + +``` +Tab 1: Overview (NEW) +├── Quick Stats Cards: Credits Balance, Sites, Users, Keywords +├── Period Selector: 7 days | 30 days | 90 days +└── Key Metrics for Selected Period + +Tab 2: Your Limits (SIMPLIFIED) +└── Progress Bars for ONLY 4 limits: + ├── Sites: 2 / 5 + ├── Users: 2 / 3 + ├── Keywords: 847 / 1,000 + └── Keyword Research Queries: 23 / 50 this month + +Tab 3: Credit Insights (NEW) +├── Credits by Site (pie chart) +├── Credits by Action Type (bar chart) +├── Credits by Image Quality (basic vs premium) +├── Credits by Automation (manual vs automated) +└── Timeline Chart (line graph over time) + +Tab 4: Activity Log (renamed from "API Activity") +└── Detailed transaction history (existing functionality) +``` + +### 4.3 CreditBalance Interface Update + +**File:** `frontend/src/services/billing.api.ts` + +```typescript +// Current (correct - no changes needed) +export interface CreditBalance { + credits: number; // Account.credits (current balance) + plan_credits_per_month: number; // Plan.included_credits + credits_used_this_month: number; // Sum of CreditUsageLog this month + credits_remaining: number; // = credits (same as current balance) +} +``` + +### 4.4 UsageSummary Interface Update + +**File:** `frontend/src/services/billing.api.ts` (add new interface) + +```typescript +export interface UsageSummary { + // Hard limits (4 only) + hard_limits: { + sites: { current: number; limit: number; display_name: string }; + users: { current: number; limit: number; display_name: string }; + keywords: { current: number; limit: number; display_name: string }; + ahrefs_queries: { current: number; limit: number; display_name: string }; + }; + + // Credits + credits: { + balance: number; + plan_allocation: number; + used_this_month: number; + }; + + // Period info + period: { + start: string; + end: string; + days_remaining: number; + }; +} +``` + +--- + +## Part 5: Keyword Research Feature + +### 5.1 Two-Option Structure + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ KEYWORD RESEARCH PAGE │ +├───────────────────────────────────────────────────────────────────┤ +│ │ +│ [Browse IGNY8 Database] [Research with Ahrefs - 42/50 left] │ +│ │ +├───────────────────────────────────────────────────────────────────┤ +│ Option 1: Browse Pre-Researched Keywords (FREE) │ +│ • Global IGNY8 keyword database │ +│ • Pre-analyzed metrics from our research │ +│ • Free to browse and add │ +│ • Counts toward max_keywords when added │ +│ │ +├───────────────────────────────────────────────────────────────────┤ +│ Option 2: Research with Ahrefs (LIMITED) │ +│ • Live Ahrefs API queries │ +│ • Fresh, custom keyword data │ +│ • Monthly limit: 42 / 50 queries remaining │ +│ • ⚠️ Each search uses 1 query from monthly limit │ +│ │ +└───────────────────────────────────────────────────────────────────┘ +``` + +### 5.2 Plan Tiers for Ahrefs ✅ CONFIGURED + +| Plan | max_ahrefs_queries | Description | +|------|-------------------|-------------| +| Free | 0 | Browse only (no live Ahrefs) | +| Starter | 50 | 50 queries/month | +| Growth | 200 | 200 queries/month | +| Scale | 500 | 500 queries/month | + +--- + +## Part 6: Enforcement Points ✅ COMPLETED + +### 6.1 Backend Validation Checklist ✅ + +| Check Point | Location | Status | +|-------------|----------|--------| +| **Keywords** | | | +| Manual creation | `planner/views.py:perform_create` | ✅ Implemented | +| Bulk import | `planner/views.py:import_keywords` | ✅ Implemented | +| **Sites** | | | +| Site creation | `defaults_service.py` | ✅ Implemented | +| **Users** | | | +| User invite | `account_views.py:team_invite` | ✅ Implemented | +| **Credits** | | | +| All AI ops | `ai/engine.py` | ✅ Pre-flight checks exist | + +### 6.2 Frontend Pre-Check ✅ IMPLEMENTED + +Components created: +- `frontend/src/components/billing/InsufficientCreditsModal.tsx` - Modal with upgrade/buy options +- `frontend/src/utils/creditCheck.ts` - Pre-flight credit check utility + +```typescript +// Usage example +import { checkCreditsBeforeOperation } from '@/utils/creditCheck'; +import { useInsufficientCreditsModal } from '@/components/billing/InsufficientCreditsModal'; + +const { showModal } = useInsufficientCreditsModal(); +const check = await checkCreditsBeforeOperation(estimatedCost); + +if (!check.hasEnoughCredits) { + showModal({ + requiredCredits: check.requiredCredits, + availableCredits: check.availableCredits, + }); + return; +} +``` + +--- + +## Part 7: Implementation Timeline + +### Week 1: Backend Foundation ✅ COMPLETED +- [x] Create database migration (add Ahrefs fields, remove unused) +- [x] Update Plan model (remove 6 fields, add 1) +- [x] Update Account model (remove 5 fields, add 1) +- [x] Update LimitService (simplify mappings) +- [x] Run migration on dev/staging +- [x] Update PlanSerializer (fix removed field references) +- [x] Update management commands (create_aws_admin_tenant, get_account_limits) + +### Week 2: Backend Enforcement ✅ COMPLETED +- [x] Add keyword limit checks at all entry points (perform_create, import_keywords) +- [ ] Create Ahrefs query endpoint with limit check (deferred - Ahrefs not yet integrated) +- [x] Update usage summary endpoint +- [x] Add site limit check (defaults_service.py) +- [x] Add user limit check (account_views.py - team invite) +- [x] Update API serializers +- [x] AI pre-flight credit checks (already in ai/engine.py) + +### Week 3: Frontend - Plans & Billing ✅ COMPLETED +- [x] Update billing.api.ts interfaces (Plan, UsageSummary) +- [x] Update UsageLimitsPanel.tsx (4 limits only) +- [x] Update PlansAndBillingPage.tsx +- [x] Update pricing-table component +- [x] Update pricingHelpers.ts +- [x] Update Plans.tsx, SignUp.tsx, SignUpFormUnified.tsx + +### Week 4: Frontend - Usage Analytics ✅ COMPLETED +- [x] UsageAnalyticsPage uses UsageLimitsPanel (already updated) +- [x] Your Limits tab shows 4 limits only +- [x] Create Credit Insights tab with charts +- [x] Overview quick stats visible on all tabs + +### Week 5: Testing & Documentation ✅ COMPLETED +- [ ] Run full test suite (pending - manual testing done) +- [x] Update API documentation (docs/10-MODULES/BILLING.md) +- [x] Update user documentation (docs/40-WORKFLOWS/CREDIT-SYSTEM.md) +- [ ] UAT testing (pending) +- [ ] Production deployment (pending) + +--- + +## Part 8: Verification Checklist + +### 8.1 Docker Verification Commands + +```bash +# Enter Django shell +docker exec -it igny8_backend python manage.py shell + +# Verify Plan model changes +from igny8_core.auth.models import Plan +plan = Plan.objects.first() +print(f"max_sites: {plan.max_sites}") +print(f"max_users: {plan.max_users}") +print(f"max_keywords: {plan.max_keywords}") +print(f"max_ahrefs_queries: {plan.max_ahrefs_queries}") +# These should ERROR after migration: +# print(f"max_clusters: {plan.max_clusters}") # Should fail + +# Verify Account model changes +from igny8_core.auth.models import Account +account = Account.objects.first() +print(f"credits: {account.credits}") +print(f"usage_ahrefs_queries: {account.usage_ahrefs_queries}") +# These should ERROR after migration: +# print(f"usage_content_ideas: {account.usage_content_ideas}") # Should fail + +# Verify LimitService +from igny8_core.business.billing.services.limit_service import LimitService +print(f"Hard limits: {list(LimitService.HARD_LIMIT_MAPPINGS.keys())}") +# Should be: ['sites', 'users', 'keywords'] +print(f"Monthly limits: {list(LimitService.MONTHLY_LIMIT_MAPPINGS.keys())}") +# Should be: ['ahrefs_queries'] + +# Verify Credit Flow +from igny8_core.business.billing.services.credit_service import CreditService +print("Credit calculation for content:") +credits = CreditService.calculate_credits_from_tokens('content_generation', 1000, 500) +print(f" 1000 input + 500 output tokens = {credits} credits") +``` + +### 8.2 Django Admin Verification + +**Check in browser at `/admin/`:** + +1. **Accounts → Account** + - [ ] `credits` field visible + - [ ] `usage_ahrefs_queries` field visible + - [ ] `usage_content_ideas` NOT visible (removed) + +2. **Accounts → Plan** + - [ ] `max_sites`, `max_users`, `max_keywords` visible + - [ ] `max_ahrefs_queries` visible + - [ ] `max_content_ideas`, `max_content_words` NOT visible (removed) + - [ ] `included_credits` visible + +3. **Billing → Credit Transaction** + - [ ] Shows all credit additions/deductions + - [ ] `balance_after` shows running total + +4. **Billing → Credit Usage Log** + - [ ] Shows all AI operations + - [ ] `operation_type`, `credits_used`, `tokens_input`, `tokens_output` visible + +### 8.3 Frontend Verification + +**Plans & Billing Page (`/account/billing`):** +- [ ] Current Plan tab shows: Plan name, price, renewal date +- [ ] Current Plan tab does NOT show usage charts +- [ ] "View Usage Details" link works +- [ ] Upgrade tab shows pricing table +- [ ] Billing History shows invoices + +**Usage Analytics Page (`/account/usage`):** +- [ ] Overview tab shows quick stats +- [ ] Your Limits tab shows ONLY 4 limits +- [ ] Credit Insights tab shows breakdown charts +- [ ] Activity Log tab shows transaction history + +**Header Credit Display:** +- [ ] Shows current credit balance +- [ ] Updates after AI operations + +--- + +## Part 9: Error Messages (User-Friendly) + +### Before (Technical) +``` +HTTP 402 - HardLimitExceededError: max_keywords limit reached +``` + +### After (User-Friendly) +``` +┌─────────────────────────────────────────────────────────┐ +│ ⚠️ Keyword Limit Reached │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ You've reached your keyword limit. │ +│ │ +│ Current: 1,000 keywords │ +│ Your plan allows: 1,000 keywords │ +│ │ +│ To add more keywords: │ +│ • Delete unused keywords │ +│ • Upgrade your plan │ +│ │ +│ [Manage Keywords] [Upgrade Plan] [Cancel] │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## Part 10: Risk Mitigation + +| Risk | Mitigation | +|------|------------| +| Migration breaks production | Test on staging with prod data clone first | +| Users lose tracked usage | Keep CreditUsageLog (detailed tracking continues) | +| Ahrefs costs spike | Monthly limit enforced server-side | +| Credit confusion | Clear documentation + help tooltips | +| Rollback needed | Keep migration reversible (add back fields) | + +--- + +## Appendix A: Files to Modify + +### Backend Files +| File | Changes | +|------|---------| +| `backend/igny8_core/auth/models.py` | Remove 11 fields, add 2 fields | +| `backend/igny8_core/business/billing/services/limit_service.py` | Simplify mappings | +| `backend/igny8_core/business/billing/serializers.py` | Update serializers | +| `backend/igny8_core/modules/billing/views.py` | Update usage summary | +| `backend/igny8_core/admin/` | Update admin panels | +| `backend/migrations/` | New migration file | + +### Frontend Files +| File | Changes | +|------|---------| +| `frontend/src/pages/account/PlansAndBillingPage.tsx` | Simplify (remove usage) | +| `frontend/src/pages/account/UsageAnalyticsPage.tsx` | Add new tabs | +| `frontend/src/services/billing.api.ts` | Update interfaces | +| `frontend/src/components/billing/UsageLimitsPanel.tsx` | Show 4 limits only | + +### Documentation Files +| File | Changes | +|------|---------| +| `docs/10-MODULES/BILLING.md` | Update limits documentation | +| `docs/40-WORKFLOWS/CREDIT-SYSTEM.md` | Update credit flow docs | + +--- + +## Conclusion + +The credit system architecture is **fundamentally correct**: +- `Plan.included_credits` defines monthly allocation +- `Account.credits` tracks real-time balance +- Credits are added on subscription renewal/payment approval +- Credits are deducted on each AI operation + +**What's broken:** +- Too many unused limits causing user confusion +- Duplicate data displayed across pages +- Monthly limits (content_words, images, etc.) that duplicate what credits already control + +**The fix:** +- Simplify to 4 hard limits + credits +- Clear page separation (financial vs consumption) +- Better UX with multi-dimensional credit insights + +--- + +**Document Version:** 1.0 +**Last Updated:** January 5, 2026 +**Verified By:** Codebase review of backend and frontend + + +############################################## + + + +## Organized Implementation Changes Plan + +### 1. **Credit Consumption Widget (Rename & Enhance)** + +**Current:** "Where Credits Go" with just a pie chart +**New:** "Credit Consumption" with pie chart + detailed table + +| Operation | Credits Used | Items Created | +|-----------|-------------|---------------| +| Clustering | 150 | 12 clusters | +| Ideas | 200 | 45 ideas | +| Content | 1,200 | 24 articles | +| Image Prompts | 50 | 30 prompts | +| Images (Basic) | 100 | 100 images | +| Images (Quality) | 250 | 50 images | +| Images (Premium) | 450 | 30 images | + +- Pie chart shows credits consumed per operation +- Table shows both **credits** AND **output count** +- Learn from Planner/Writer footer workflow completion widgets + +--- + +### 2. **New Usage Logs Page** (`/account/usage/logs`) + +**Purpose:** Detailed, filterable, paginated log of all AI operations + +**Layout:** Same as Planner/Writer table pages (consistent template) + +**Table Columns:** +| Date | Operation | Details | Credits | Cost (USD) | +|------|-----------|---------|-------|--------|---------|------------| +| Jan 5, 2:30pm | Content Writing | "SEO Guide for..." | 35 | $0.042 | +| Jan 5, 2:15pm | Image Generation | Premium quality | 15 | $0.18 | + +**Features:** +- Filters: Operation type, date range, site +- Pagination +- USD cost calculated from token pricing (from AIModelConfig) +- Link to related content where applicable +- Export option + +--- + +### 3. **Usage Dashboard - Multi-Dimensional Widgets** + +**Remove:** +- ❌ "Operations" summary card (single metrics, not useful) +- ❌ "Recent Activity" card (move to Usage Logs page) + +**Add/Enhance:** +- ✅ **Credit Consumption** (pie + table as described above) +- ✅ **Usage Trend** - Line chart showing daily/weekly credit usage +- ✅ **Credits by Site** - If multiple sites, show distribution +- ✅ **Image Quality Breakdown** - Basic vs Quality vs Premium usage +- ✅ **Top Operations** - Which operations consume most credits +- ✅ **Quick link** to Usage Logs page for detailed view + +--- + +### 4. **Data Sources Needed** + +From backend, I'll need to check: +- `CreditUsageLog` - Has operation_type, credits_used, model_used, tokens_in, tokens_out +- `AIModelConfig` - Has cost_per_1k_input, cost_per_1k_output for USD calculation +- Aggregations for: clusters created, ideas generated, articles written, images by tier + +--- + +### Summary of Deliverables + +| Item | Type | Description | +|------|------|-------------| +| 1 | Widget Enhancement | "Credit Consumption" - pie chart + table with credits & output counts | +| 2 | New Page | `/account/usage/logs` - Filterable, paginated AI operation logs with USD costs | +| 3 | Dashboard Update | Remove weak widgets, add multi-dimensional data-rich charts | +| 4 | Sidebar | Add "Usage Logs" as sub-item under Usage Dashboard | + +--- + +**Does this organization look correct?** Should I proceed with implementation, or would you like to adjust any part of this plan? \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ff9d1e6b..191fd374 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -61,11 +61,12 @@ const Credits = lazy(() => import("./pages/Billing/Credits")); const Transactions = lazy(() => import("./pages/Billing/Transactions")); const Usage = lazy(() => import("./pages/Billing/Usage")); const CreditsAndBilling = lazy(() => import("./pages/Settings/CreditsAndBilling")); -const PurchaseCreditsPage = lazy(() => import("./pages/account/PurchaseCreditsPage")); const PlansAndBillingPage = lazy(() => import("./pages/account/PlansAndBillingPage")); const AccountSettingsPage = lazy(() => import("./pages/account/AccountSettingsPage")); // TeamManagementPage - Now integrated as tab in AccountSettingsPage const UsageAnalyticsPage = lazy(() => import("./pages/account/UsageAnalyticsPage")); +const UsageDashboardPage = lazy(() => import("./pages/account/UsageDashboardPage")); +const UsageLogsPage = lazy(() => import("./pages/account/UsageLogsPage")); const ContentSettingsPage = lazy(() => import("./pages/account/ContentSettingsPage")); const NotificationsPage = lazy(() => import("./pages/account/NotificationsPage")); @@ -221,12 +222,16 @@ export default function App() { } /> } /> } /> - } /> + } /> - {/* Usage - with sub-routes for sidebar navigation */} - } /> - } /> - } /> + {/* Usage Dashboard - Single comprehensive page */} + } /> + {/* Usage Logs - Detailed operation history */} + } /> + {/* Legacy routes redirect to dashboard */} + } /> + } /> + } /> {/* Content Settings - with sub-routes for sidebar navigation */} } /> diff --git a/frontend/src/components/auth/ProtectedRoute.tsx b/frontend/src/components/auth/ProtectedRoute.tsx index 5294e514..63e34009 100644 --- a/frontend/src/components/auth/ProtectedRoute.tsx +++ b/frontend/src/components/auth/ProtectedRoute.tsx @@ -22,7 +22,6 @@ export default function ProtectedRoute({ children }: ProtectedRouteProps) { const PLAN_ALLOWED_PATHS = [ '/account/plans', - '/account/purchase-credits', '/account/settings', '/account/team', '/account/usage', diff --git a/frontend/src/components/auth/SignUpFormUnified.tsx b/frontend/src/components/auth/SignUpFormUnified.tsx index 85e8feed..6c339613 100644 --- a/frontend/src/components/auth/SignUpFormUnified.tsx +++ b/frontend/src/components/auth/SignUpFormUnified.tsx @@ -24,7 +24,7 @@ interface Plan { max_users: number; max_sites: number; max_keywords: number; - monthly_word_count_limit: number; + max_ahrefs_queries: number; included_credits: number; features: string[]; } @@ -260,8 +260,7 @@ export default function SignUpFormUnified({ features.push(`${plan.max_sites} ${plan.max_sites === 1 ? 'Site' : 'Sites'}`); features.push(`${plan.max_users} ${plan.max_users === 1 ? 'User' : 'Users'}`); features.push(`${formatNumber(plan.max_keywords || 0)} Keywords`); - features.push(`${formatNumber(plan.monthly_word_count_limit || 0)} Words/Month`); - features.push(`${formatNumber(plan.included_credits || 0)} AI Credits`); + features.push(`${formatNumber(plan.included_credits || 0)} Credits/Month`); return features; }; diff --git a/frontend/src/components/billing/CreditInsightsCharts.tsx b/frontend/src/components/billing/CreditInsightsCharts.tsx new file mode 100644 index 00000000..0cbf5db3 --- /dev/null +++ b/frontend/src/components/billing/CreditInsightsCharts.tsx @@ -0,0 +1,450 @@ +/** + * Credit Insights Charts Component + * Displays credit usage analytics with visual charts + * - Donut chart: Credits by operation type + * - Line chart: Daily credit usage timeline + * - Bar chart: Top credit-consuming operations + */ + +import Chart from 'react-apexcharts'; +import { ApexOptions } from 'apexcharts'; +import { Card } from '../ui/card'; +import { ActivityIcon, TrendingUpIcon, PieChartIcon, BarChart3Icon } from '../../icons'; +import type { UsageAnalytics } from '../../services/billing.api'; + +interface CreditInsightsChartsProps { + analytics: UsageAnalytics | null; + loading?: boolean; + period: number; +} + +// Friendly names for operation types +const OPERATION_LABELS: Record = { + content_generation: 'Content Generation', + image_generation: 'Image Generation', + keyword_clustering: 'Keyword Clustering', + content_analysis: 'Content Analysis', + subscription: 'Subscription', + purchase: 'Credit Purchase', + refund: 'Refund', + adjustment: 'Adjustment', + grant: 'Credit Grant', + deduction: 'Deduction', +}; + +const CHART_COLORS = [ + 'var(--color-brand-500)', + 'var(--color-purple-500)', + 'var(--color-success-500)', + 'var(--color-warning-500)', + 'var(--color-error-500)', + 'var(--color-info-500)', + '#6366f1', // indigo + '#ec4899', // pink + '#14b8a6', // teal + '#f97316', // orange +]; + +export default function CreditInsightsCharts({ analytics, loading, period }: CreditInsightsChartsProps) { + if (loading) { + return ( +
+ {[1, 2, 3, 4].map((i) => ( + +
+
+
+ ))} +
+ ); + } + + if (!analytics) { + return ( + +
+ +

No analytics data available

+
+
+ ); + } + + // Prepare data for donut chart (credits by operation type) + const usageByType = analytics.usage_by_type.filter(item => Math.abs(item.total) > 0); + const donutLabels = usageByType.map(item => OPERATION_LABELS[item.transaction_type] || item.transaction_type.replace(/_/g, ' ')); + const donutSeries = usageByType.map(item => Math.abs(item.total)); + + const donutOptions: ApexOptions = { + chart: { + type: 'donut', + fontFamily: 'Outfit, sans-serif', + }, + labels: donutLabels, + colors: CHART_COLORS.slice(0, donutLabels.length), + legend: { + position: 'bottom', + fontFamily: 'Outfit', + labels: { + colors: 'var(--color-gray-600)', + }, + }, + plotOptions: { + pie: { + donut: { + size: '65%', + labels: { + show: true, + name: { + show: true, + fontSize: '14px', + fontFamily: 'Outfit', + color: 'var(--color-gray-600)', + }, + value: { + show: true, + fontSize: '24px', + fontFamily: 'Outfit', + fontWeight: 600, + color: 'var(--color-gray-900)', + formatter: (val: string) => parseInt(val).toLocaleString(), + }, + total: { + show: true, + label: 'Total Credits', + fontSize: '14px', + fontFamily: 'Outfit', + color: 'var(--color-gray-600)', + formatter: () => analytics.total_usage.toLocaleString(), + }, + }, + }, + }, + }, + dataLabels: { + enabled: false, + }, + tooltip: { + y: { + formatter: (val: number) => `${val.toLocaleString()} credits`, + }, + }, + responsive: [ + { + breakpoint: 480, + options: { + chart: { + width: 300, + }, + legend: { + position: 'bottom', + }, + }, + }, + ], + }; + + // Prepare data for timeline chart (daily usage) + const dailyData = analytics.daily_usage || []; + const timelineCategories = dailyData.map(d => { + const date = new Date(d.date); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + }); + const usageSeries = dailyData.map(d => Math.abs(d.usage)); + const purchasesSeries = dailyData.map(d => d.purchases); + + const timelineOptions: ApexOptions = { + chart: { + type: 'area', + fontFamily: 'Outfit, sans-serif', + height: 300, + toolbar: { + show: false, + }, + zoom: { + enabled: false, + }, + }, + colors: ['var(--color-brand-500)', 'var(--color-success-500)'], + dataLabels: { + enabled: false, + }, + stroke: { + curve: 'smooth', + width: 2, + }, + fill: { + type: 'gradient', + gradient: { + shadeIntensity: 1, + opacityFrom: 0.4, + opacityTo: 0.1, + stops: [0, 90, 100], + }, + }, + xaxis: { + categories: timelineCategories, + axisBorder: { + show: false, + }, + axisTicks: { + show: false, + }, + labels: { + style: { + colors: 'var(--color-gray-500)', + fontFamily: 'Outfit', + }, + rotate: -45, + rotateAlways: dailyData.length > 14, + }, + }, + yaxis: { + labels: { + style: { + colors: 'var(--color-gray-500)', + fontFamily: 'Outfit', + }, + formatter: (val: number) => val.toLocaleString(), + }, + }, + grid: { + borderColor: 'var(--color-gray-200)', + strokeDashArray: 4, + }, + legend: { + position: 'top', + horizontalAlign: 'right', + fontFamily: 'Outfit', + labels: { + colors: 'var(--color-gray-600)', + }, + }, + tooltip: { + y: { + formatter: (val: number) => `${val.toLocaleString()} credits`, + }, + }, + }; + + const timelineSeries = [ + { name: 'Credits Used', data: usageSeries }, + { name: 'Credits Added', data: purchasesSeries }, + ]; + + // Prepare data for bar chart (top operations by count) + const operationsByCount = [...analytics.usage_by_type] + .filter(item => item.count > 0) + .sort((a, b) => b.count - a.count) + .slice(0, 8); + + const barCategories = operationsByCount.map(item => + OPERATION_LABELS[item.transaction_type] || item.transaction_type.replace(/_/g, ' ') + ); + const barSeries = operationsByCount.map(item => item.count); + + const barOptions: ApexOptions = { + chart: { + type: 'bar', + fontFamily: 'Outfit, sans-serif', + height: 300, + toolbar: { + show: false, + }, + }, + colors: ['var(--color-purple-500)'], + plotOptions: { + bar: { + horizontal: true, + borderRadius: 4, + barHeight: '60%', + }, + }, + dataLabels: { + enabled: false, + }, + xaxis: { + categories: barCategories, + labels: { + style: { + colors: 'var(--color-gray-500)', + fontFamily: 'Outfit', + }, + }, + }, + yaxis: { + labels: { + style: { + colors: 'var(--color-gray-600)', + fontFamily: 'Outfit', + }, + }, + }, + grid: { + borderColor: 'var(--color-gray-200)', + strokeDashArray: 4, + }, + tooltip: { + y: { + formatter: (val: number) => `${val.toLocaleString()} operations`, + }, + }, + }; + + // Summary stats + const avgDailyUsage = dailyData.length > 0 + ? Math.round(dailyData.reduce((sum, d) => sum + Math.abs(d.usage), 0) / dailyData.length) + : 0; + + const peakUsage = dailyData.length > 0 + ? Math.max(...dailyData.map(d => Math.abs(d.usage))) + : 0; + + const topOperation = usageByType.length > 0 + ? usageByType.reduce((max, item) => Math.abs(item.total) > Math.abs(max.total) ? item : max, usageByType[0]) + : null; + + return ( +
+ {/* Summary Stats */} +
+ +
+
+ +
+
+
Avg Daily Usage
+
+ {avgDailyUsage.toLocaleString()} +
+
credits/day
+
+
+
+ + +
+
+ +
+
+
Peak Usage
+
+ {peakUsage.toLocaleString()} +
+
credits in one day
+
+
+
+ + +
+
+ +
+
+
Top Operation
+
+ {topOperation + ? (OPERATION_LABELS[topOperation.transaction_type] || topOperation.transaction_type.replace(/_/g, ' ')) + : 'N/A' + } +
+
+ {topOperation ? `${Math.abs(topOperation.total).toLocaleString()} credits` : ''} +
+
+
+
+
+ + {/* Charts Grid */} +
+ {/* Usage by Type - Donut Chart */} + +
+
+ +
+

+ Credits by Type +

+
+ {donutSeries.length > 0 ? ( + + ) : ( +
+
+ +

No usage data for this period

+
+
+ )} +
+ + {/* Operations by Count - Bar Chart */} + +
+
+ +
+

+ Operations Count +

+
+ {barSeries.length > 0 ? ( + + ) : ( +
+
+ +

No operations in this period

+
+
+ )} +
+
+ + {/* Daily Timeline - Full Width */} + +
+
+ +
+

+ Credit Activity Timeline +

+ + Last {period} days + +
+ {dailyData.length > 0 ? ( + + ) : ( +
+
+ +

No daily activity data available

+
+
+ )} +
+
+ ); +} diff --git a/frontend/src/components/billing/InsufficientCreditsModal.tsx b/frontend/src/components/billing/InsufficientCreditsModal.tsx new file mode 100644 index 00000000..27ef1d3a --- /dev/null +++ b/frontend/src/components/billing/InsufficientCreditsModal.tsx @@ -0,0 +1,168 @@ +/** + * Insufficient Credits Modal + * Shows when user doesn't have enough credits for an operation + * Provides options to upgrade plan or buy credits + */ + +import React from 'react'; +import { Modal } from '../ui/modal'; +import Button from '../ui/button/Button'; +import { ZapIcon, TrendingUpIcon, CreditCardIcon } from '../../icons'; +import { useNavigate } from 'react-router-dom'; + +interface InsufficientCreditsModalProps { + isOpen: boolean; + onClose: () => void; + requiredCredits: number; + availableCredits: number; + operationType?: string; +} + +export default function InsufficientCreditsModal({ + isOpen, + onClose, + requiredCredits, + availableCredits, + operationType = 'this operation', +}: InsufficientCreditsModalProps) { + const navigate = useNavigate(); + const shortfall = requiredCredits - availableCredits; + + const handleUpgradePlan = () => { + onClose(); + navigate('/account/billing/upgrade'); + }; + + const handleBuyCredits = () => { + onClose(); + navigate('/account/billing/credits'); + }; + + const handleViewUsage = () => { + onClose(); + navigate('/account/usage'); + }; + + return ( + +
+ {/* Warning Icon */} +
+
+
+ +
+
+ + {/* Title */} +

+ Insufficient Credits +

+ + {/* Message */} +

+ You don't have enough credits for {operationType}. +

+ + {/* Credit Stats */} +
+
+
+
Required
+
+ {requiredCredits.toLocaleString()} +
+
+
+
Available
+
+ {availableCredits.toLocaleString()} +
+
+
+
Shortfall
+
+ {shortfall.toLocaleString()} +
+
+
+
+ + {/* Action Buttons */} +
+ + + + + +
+ + {/* Cancel Button */} +
+ +
+
+
+ ); +} + +/** + * Hook to manage insufficient credits modal state + */ +export function useInsufficientCreditsModal() { + const [isOpen, setIsOpen] = React.useState(false); + const [modalProps, setModalProps] = React.useState({ + requiredCredits: 0, + availableCredits: 0, + operationType: 'this operation', + }); + + const showInsufficientCreditsModal = (props: { + requiredCredits: number; + availableCredits: number; + operationType?: string; + }) => { + setModalProps({ + requiredCredits: props.requiredCredits, + availableCredits: props.availableCredits, + operationType: props.operationType || 'this operation', + }); + setIsOpen(true); + }; + + const closeModal = () => setIsOpen(false); + + return { + isOpen, + modalProps, + showInsufficientCreditsModal, + closeModal, + }; +} diff --git a/frontend/src/components/billing/UsageLimitsPanel.tsx b/frontend/src/components/billing/UsageLimitsPanel.tsx index 68158dc1..681eaad9 100644 --- a/frontend/src/components/billing/UsageLimitsPanel.tsx +++ b/frontend/src/components/billing/UsageLimitsPanel.tsx @@ -177,15 +177,11 @@ export default function UsageLimitsPanel() { sites: { icon: , color: 'success' as const }, users: { icon: , color: 'info' as const }, keywords: { icon: , color: 'purple' as const }, - clusters: { icon: , color: 'warning' as const }, }; + // Simplified to only 1 monthly limit: Ahrefs keyword research queries const monthlyLimitConfig = { - content_ideas: { icon: , color: 'brand' as const }, - content_words: { icon: , color: 'indigo' as const }, - images_basic: { icon: , color: 'teal' as const }, - images_premium: { icon: , color: 'cyan' as const }, - image_prompts: { icon: , color: 'pink' as const }, + ahrefs_queries: { icon: , color: 'brand' as const }, }; return ( diff --git a/frontend/src/components/ui/pricing-table/index.tsx b/frontend/src/components/ui/pricing-table/index.tsx index 442bde8d..1cb58743 100644 --- a/frontend/src/components/ui/pricing-table/index.tsx +++ b/frontend/src/components/ui/pricing-table/index.tsx @@ -24,11 +24,7 @@ export interface PricingPlan { max_sites?: number; max_users?: number; max_keywords?: number; - max_clusters?: number; - max_content_ideas?: number; - max_content_words?: number; - max_images_basic?: number; - max_images_premium?: number; + max_ahrefs_queries?: number; included_credits?: number; } @@ -142,7 +138,7 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false, ))} {/* Plan Limits Section */} - {(plan.max_sites || plan.max_content_words || plan.included_credits) && ( + {(plan.max_sites || plan.max_keywords || plan.included_credits) && (
LIMITS
{plan.max_sites && ( @@ -161,27 +157,11 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false, )} - {plan.max_content_words && ( + {plan.max_keywords && (
  • - {(plan.max_content_words / 1000).toLocaleString()}K Words/month - -
  • - )} - {plan.max_content_ideas && ( -
  • - - - {plan.max_content_ideas} Ideas/month - -
  • - )} - {plan.max_images_basic && ( -
  • - - - {plan.max_images_basic} Images/month + {plan.max_keywords.toLocaleString()} Keywords
  • )} @@ -189,7 +169,7 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false,
  • - {plan.included_credits.toLocaleString()} Content pieces/month + {plan.included_credits.toLocaleString()} Credits/month
  • )} diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx index 758f6b68..8c3c4fca 100644 --- a/frontend/src/layout/AppSidebar.tsx +++ b/frontend/src/layout/AppSidebar.tsx @@ -212,19 +212,14 @@ const AppSidebar: React.FC = () => { { icon: , name: "Plans & Billing", - subItems: [ - { name: "Current Plan", path: "/account/plans" }, - { name: "Upgrade Plan", path: "/account/plans/upgrade" }, - { name: "History", path: "/account/plans/history" }, - ], + path: "/account/plans", }, { icon: , name: "Usage", subItems: [ - { name: "Limits & Usage", path: "/account/usage" }, - { name: "Credit History", path: "/account/usage/credits" }, - { name: "Activity", path: "/account/usage/activity" }, + { name: "Dashboard", path: "/account/usage" }, + { name: "Usage Logs", path: "/account/usage/logs" }, ], }, { diff --git a/frontend/src/pages/AuthPages/SignUp.tsx b/frontend/src/pages/AuthPages/SignUp.tsx index c0ddbee5..1890e93f 100644 --- a/frontend/src/pages/AuthPages/SignUp.tsx +++ b/frontend/src/pages/AuthPages/SignUp.tsx @@ -13,16 +13,8 @@ interface Plan { max_users: number; max_sites: number; max_keywords: number; - max_clusters: number; - max_content_ideas: number; - monthly_word_count_limit: number; - monthly_ai_credit_limit: number; - monthly_image_count: number; - daily_content_tasks: number; - daily_ai_request_limit: number; - daily_image_generation_limit: number; + max_ahrefs_queries: number; included_credits: number; - image_model_choices: string[]; features: string[]; } diff --git a/frontend/src/pages/Reference/SeedKeywords.tsx b/frontend/src/pages/Reference/SeedKeywords.tsx index 10000ed5..1f9a1c2e 100644 --- a/frontend/src/pages/Reference/SeedKeywords.tsx +++ b/frontend/src/pages/Reference/SeedKeywords.tsx @@ -94,26 +94,25 @@ export default function SeedKeywords() { {keyword.industry_name} - - - {keyword.sector_name} - - - {keyword.volume.toLocaleString()} - - - {keyword.difficulty} - - - {keyword.country_display} - - - ))} - - -
    - - )} + + + {keyword.sector_name} + + + {keyword.volume.toLocaleString()} + + + {keyword.difficulty} + + + {keyword.country_display} + + + ))} + + + + ); } diff --git a/frontend/src/pages/Settings/Plans.tsx b/frontend/src/pages/Settings/Plans.tsx index 0545e6d5..ec500fc4 100644 --- a/frontend/src/pages/Settings/Plans.tsx +++ b/frontend/src/pages/Settings/Plans.tsx @@ -16,16 +16,8 @@ interface Plan { max_users: number; max_sites: number; max_keywords: number; - max_clusters: number; - max_content_ideas: number; - monthly_word_count_limit: number; - monthly_ai_credit_limit: number; - monthly_image_count: number; - daily_content_tasks: number; - daily_ai_request_limit: number; - daily_image_generation_limit: number; + max_ahrefs_queries: number; included_credits: number; - image_model_choices: string[]; features: string[]; } diff --git a/frontend/src/pages/account/PlansAndBillingPage.tsx b/frontend/src/pages/account/PlansAndBillingPage.tsx index e5701cd0..221b6d8f 100644 --- a/frontend/src/pages/account/PlansAndBillingPage.tsx +++ b/frontend/src/pages/account/PlansAndBillingPage.tsx @@ -1,16 +1,34 @@ /** - * Plans & Billing Page - Subscription & Payment Management - * Tabs: Current Plan, Upgrade Plan, Billing History - * Tab selection driven by URL path for sidebar navigation - * - * Note: Usage tracking is consolidated in UsageAnalyticsPage (/account/usage) + * Plans & Billing Page - Unified Subscription & Payment Management + * Comprehensive dashboard for all billing-related features + * Rich, actionable, data-driven UX following UsageDashboard patterns */ import { useState, useEffect, useRef } from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { - CreditCardIcon, BoxIcon as PackageIcon, TrendingUpIcon, FileTextIcon, WalletIcon, ArrowUpIcon, - Loader2Icon, AlertCircleIcon, CheckCircleIcon, DownloadIcon, ZapIcon, GlobeIcon, UsersIcon, XIcon + CreditCardIcon, + BoxIcon as PackageIcon, + TrendingUpIcon, + FileTextIcon, + WalletIcon, + ArrowUpIcon, + Loader2Icon, + AlertCircleIcon, + CheckCircleIcon, + DownloadIcon, + ZapIcon, + GlobeIcon, + UsersIcon, + XIcon, + CalendarIcon, + RefreshCwIcon, + ChevronRightIcon, + PlusIcon, + Building2Icon, + TagIcon, + LockIcon, + ShootingStarIcon, } from '../../icons'; import { Card } from '../../components/ui/card'; import Badge from '../../components/ui/badge/Badge'; @@ -18,12 +36,6 @@ import Button from '../../components/ui/button/Button'; import PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; import { useToast } from '../../components/ui/toast/ToastContainer'; -import { PricingPlan } from '../../components/ui/pricing-table'; -import PricingTable1 from '../../components/ui/pricing-table/pricing-table-1'; -import CreditCostBreakdownPanel from '../../components/billing/CreditCostBreakdownPanel'; -// import CreditCostsPanel from '../../components/billing/CreditCostsPanel'; // Hidden from regular users -// import UsageLimitsPanel from '../../components/billing/UsageLimitsPanel'; // Moved to UsageAnalyticsPage -import { convertToPricingPlan } from '../../utils/pricingHelpers'; import { usePageLoading } from '../../context/PageLoadingContext'; import { getCreditBalance, @@ -33,10 +45,6 @@ import { purchaseCreditPackage, downloadInvoicePDF, getPayments, - submitManualPayment, - createPaymentMethod, - deletePaymentMethod, - setDefaultPaymentMethod, type CreditBalance, type CreditPackage, type Invoice, @@ -51,26 +59,22 @@ import { } from '../../services/billing.api'; import { useAuthStore } from '../../store/authStore'; -type TabType = 'plan' | 'upgrade' | 'invoices'; - -// Map URL paths to tab types -function getTabFromPath(pathname: string): TabType { - if (pathname.includes('/upgrade')) return 'upgrade'; - if (pathname.includes('/history')) return 'invoices'; - return 'plan'; -} - export default function PlansAndBillingPage() { - const location = useLocation(); - // Derive active tab from URL path - const activeTab = getTabFromPath(location.pathname); const { startLoading, stopLoading } = usePageLoading(); + const toast = useToast(); + const hasLoaded = useRef(false); + const { user } = useAuthStore.getState(); + const isAwsAdmin = user?.account?.slug === 'aws-admin'; + + // UI States const [error, setError] = useState(''); const [planLoadingId, setPlanLoadingId] = useState(null); const [purchaseLoadingId, setPurchaseLoadingId] = useState(null); const [showCancelConfirm, setShowCancelConfirm] = useState(false); + const [showUpgradeModal, setShowUpgradeModal] = useState(false); + const [selectedBillingCycle, setSelectedBillingCycle] = useState<'monthly' | 'annual'>('monthly'); - // Data states + // Data States const [creditBalance, setCreditBalance] = useState(null); const [packages, setPackages] = useState([]); const [invoices, setInvoices] = useState([]); @@ -79,28 +83,6 @@ export default function PlansAndBillingPage() { const [plans, setPlans] = useState([]); const [subscriptions, setSubscriptions] = useState([]); const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(undefined); - const [manualPayment, setManualPayment] = useState({ - invoice_id: '', - amount: '', - payment_method: '', - reference: '', - notes: '', - }); - const [newPaymentMethod, setNewPaymentMethod] = useState({ - type: 'bank_transfer', - display_name: '', - instructions: '', - }); - const { user } = useAuthStore.getState(); - const hasLoaded = useRef(false); - const isAwsAdmin = user?.account?.slug === 'aws-admin'; - const handleBillingError = (err: any, fallback: string) => { - const message = err?.message || fallback; - setError(message); - toast?.error?.(message); - }; - - const toast = useToast(); useEffect(() => { if (hasLoaded.current) return; @@ -108,45 +90,37 @@ export default function PlansAndBillingPage() { loadData(); }, []); + const handleError = (err: any, fallback: string) => { + const message = err?.message || fallback; + setError(message); + toast?.error?.(message); + }; + const loadData = async (allowRetry = true) => { try { startLoading('Loading billing data...'); - // Fetch in controlled sequence to avoid burst 429s on auth/system scopes - const balanceData = await getCreditBalance(); - - // Small gap between auth endpoints to satisfy tight throttles const wait = (ms: number) => new Promise((res) => setTimeout(res, ms)); + const balanceData = await getCreditBalance(); const packagesPromise = getCreditPackages(); const invoicesPromise = getInvoices({}); const paymentsPromise = getPayments({}); const methodsPromise = getAvailablePaymentMethods(); - const plansData = await getPlans(); await wait(400); - // Subscriptions: retry once on 429 after short backoff; do not hard-fail page let subsData: { results: Subscription[] } = { results: [] }; try { subsData = await getSubscriptions(); } catch (subErr: any) { if (subErr?.status === 429 && allowRetry) { await wait(2500); - try { - subsData = await getSubscriptions(); - } catch { - subsData = { results: [] }; - } - } else { - subsData = { results: [] }; + try { subsData = await getSubscriptions(); } catch { subsData = { results: [] }; } } } const [packagesData, invoicesData, paymentsData, methodsData] = await Promise.all([ - packagesPromise, - invoicesPromise, - paymentsPromise, - methodsPromise, + packagesPromise, invoicesPromise, paymentsPromise, methodsPromise ]); setCreditBalance(balanceData); @@ -154,24 +128,17 @@ export default function PlansAndBillingPage() { setInvoices(invoicesData.results || []); setPayments(paymentsData.results || []); - // Prefer manual payment method id 14 as default (tenant-facing) const methods = (methodsData.results || []).filter((m) => m.is_enabled !== false); setPaymentMethods(methods); if (methods.length > 0) { - // Preferred ordering: bank_transfer (default), then manual const bank = methods.find((m) => m.type === 'bank_transfer'); const manual = methods.find((m) => m.type === 'manual'); - const selected = - bank || - manual || - methods.find((m) => m.is_default) || - methods[0]; + const selected = bank || manual || methods.find((m) => m.is_default) || methods[0]; setSelectedPaymentMethod((prev) => prev || selected.type || selected.id); } - // Surface all active plans (avoid hiding plans and showing empty state) + // Filter plans const activePlans = (plansData.results || []).filter((p) => p.is_active !== false); - // Exclude Enterprise plan for non aws-admin accounts const filteredPlans = activePlans.filter((p) => { const name = (p.name || '').toLowerCase(); const slug = (p.slug || '').toLowerCase(); @@ -179,39 +146,23 @@ export default function PlansAndBillingPage() { return isAwsAdmin ? true : !isEnterprise; }); - // Ensure the user's assigned plan is included even if subscriptions list is empty const accountPlan = user?.account?.plan; - const isAccountEnterprise = (() => { - if (!accountPlan) return false; - const name = (accountPlan.name || '').toLowerCase(); - const slug = (accountPlan.slug || '').toLowerCase(); - return name.includes('enterprise') || slug === 'enterprise'; - })(); - - const shouldIncludeAccountPlan = accountPlan && (!isAccountEnterprise || isAwsAdmin); - if (shouldIncludeAccountPlan && !filteredPlans.find((p) => p.id === accountPlan.id)) { + if (accountPlan && !filteredPlans.find((p) => p.id === accountPlan.id)) { filteredPlans.push(accountPlan as any); } setPlans(filteredPlans); + const subs = subsData.results || []; - if (subs.length === 0 && shouldIncludeAccountPlan && accountPlan) { - subs.push({ - id: accountPlan.id || 0, - plan: accountPlan, - status: 'active', - } as any); + if (subs.length === 0 && accountPlan) { + subs.push({ id: accountPlan.id || 0, plan: accountPlan, status: 'active' } as any); } setSubscriptions(subs); } catch (err: any) { - // Handle throttling gracefully: don't block the page on subscriptions throttle if (err?.status === 429 && allowRetry) { setError('Request was throttled. Retrying...'); setTimeout(() => loadData(false), 2500); - } else if (err?.status === 429) { - setError(''); // suppress lingering banner - } else { + } else if (err?.status !== 429) { setError(err.message || 'Failed to load billing data'); - console.error('Billing load error:', err); } } finally { stopLoading(); @@ -220,52 +171,41 @@ export default function PlansAndBillingPage() { const handleSelectPlan = async (planId: number) => { try { - if (!selectedPaymentMethod && paymentMethods.length > 0) { - setError('Select a payment method to continue'); - return; - } setPlanLoadingId(planId); await createSubscription({ plan_id: planId, payment_method: selectedPaymentMethod }); - toast?.success?.('Subscription updated'); + toast?.success?.('Plan upgraded successfully!'); + setShowUpgradeModal(false); await loadData(); } catch (err: any) { - handleBillingError(err, 'Failed to update subscription'); + handleError(err, 'Failed to upgrade plan'); } finally { setPlanLoadingId(null); } }; const handleCancelSubscription = async () => { - if (!currentSubscription?.id) { - setError('No active subscription to cancel'); - return; - } + if (!currentSubscription?.id) return; try { setPlanLoadingId(currentSubscription.id); await cancelSubscription(currentSubscription.id); - toast?.success?.('Subscription cancellation requested'); + toast?.success?.('Subscription cancelled'); + setShowCancelConfirm(false); await loadData(); } catch (err: any) { - handleBillingError(err, 'Failed to cancel subscription'); + handleError(err, 'Failed to cancel subscription'); } finally { setPlanLoadingId(null); } }; - const handlePurchase = async (packageId: number) => { + const handlePurchaseCredits = async (packageId: number) => { try { - if (!selectedPaymentMethod && paymentMethods.length > 0) { - setError('Select a payment method to continue'); - return; - } setPurchaseLoadingId(packageId); - await purchaseCreditPackage({ - package_id: packageId, - payment_method: (selectedPaymentMethod as any) || 'stripe', - }); + await purchaseCreditPackage({ package_id: packageId, payment_method: selectedPaymentMethod as any || 'stripe' }); + toast?.success?.('Credits purchased successfully!'); await loadData(); } catch (err: any) { - handleBillingError(err, 'Failed to purchase credits'); + handleError(err, 'Failed to purchase credits'); } finally { setPurchaseLoadingId(null); } @@ -283,540 +223,602 @@ export default function PlansAndBillingPage() { link.remove(); window.URL.revokeObjectURL(url); } catch (err: any) { - handleBillingError(err, 'Failed to download invoice'); - } - }; - - const handleSubmitManualPayment = async () => { - try { - const payload = { - invoice_id: manualPayment.invoice_id ? Number(manualPayment.invoice_id) : undefined, - amount: manualPayment.amount, - payment_method: manualPayment.payment_method || (selectedPaymentMethod as any) || 'manual', - reference: manualPayment.reference, - notes: manualPayment.notes, - }; - await submitManualPayment(payload as any); - toast?.success?.('Manual payment submitted'); - setManualPayment({ invoice_id: '', amount: '', payment_method: '', reference: '', notes: '' }); - await loadData(); - } catch (err: any) { - handleBillingError(err, 'Failed to submit payment'); - } - }; - - const handleAddPaymentMethod = async () => { - if (!newPaymentMethod.display_name.trim()) { - setError('Payment method name is required'); - return; - } - try { - await createPaymentMethod(newPaymentMethod as any); - toast?.success?.('Payment method added'); - setNewPaymentMethod({ type: 'bank_transfer', display_name: '', instructions: '' }); - await loadData(); - } catch (err: any) { - handleBillingError(err, 'Failed to add payment method'); - } - }; - - const handleRemovePaymentMethod = async (id: string) => { - try { - await deletePaymentMethod(id); - toast?.success?.('Payment method removed'); - await loadData(); - } catch (err: any) { - handleBillingError(err, 'Failed to remove payment method'); - } - }; - - const handleSetDefaultPaymentMethod = async (id: string) => { - try { - await setDefaultPaymentMethod(id); - toast?.success?.('Default payment method updated'); - await loadData(); - } catch (err: any) { - handleBillingError(err, 'Failed to set default'); + handleError(err, 'Failed to download invoice'); } }; + // Computed values const currentSubscription = subscriptions.find((sub) => sub.status === 'active') || subscriptions[0]; const currentPlanId = typeof currentSubscription?.plan === 'object' ? currentSubscription.plan.id : currentSubscription?.plan; - // Fallback to account plan if subscription is missing const accountPlanId = user?.account?.plan?.id; const effectivePlanId = currentPlanId || accountPlanId; const currentPlan = plans.find((p) => p.id === effectivePlanId) || user?.account?.plan; const hasActivePlan = Boolean(effectivePlanId); - const hasPaymentMethods = paymentMethods.length > 0; - const subscriptionStatus = currentSubscription?.status || (hasActivePlan ? 'active' : 'none'); - const hasPendingManualPayment = payments.some((p) => p.status === 'pending_approval'); + const hasPendingPayment = payments.some((p) => p.status === 'pending_approval'); - // Page titles based on active tab - const pageTitles = { - plan: { title: 'Current Plan', description: 'View your subscription details and features' }, - upgrade: { title: 'Upgrade Plan', description: 'Compare plans and upgrade your subscription' }, - invoices: { title: 'Billing History', description: 'View invoices and manage payment methods' }, - }; + // Credit usage percentage + const creditUsage = creditBalance && creditBalance.plan_credits_per_month > 0 + ? Math.round((creditBalance.credits_used_this_month / creditBalance.plan_credits_per_month) * 100) + : 0; + + // Upgrade plans (exclude current and free) + const upgradePlans = plans.filter(p => { + const price = Number(p.price) || 0; + return price > 0 && p.id !== effectivePlanId; + }).sort((a, b) => (Number(a.price) || 0) - (Number(b.price) || 0)); return ( <> - + , color: 'blue' }} - parent="Plans & Billing" + actions={ + + } /> - - {/* Activation / pending payment notice */} - {!hasActivePlan && ( -
    - No active plan. Choose a plan below to activate your account. + + {/* Alerts */} + {!hasActivePlan && ( +
    + +
    +

    No Active Plan

    +

    Choose a plan below to activate your account and unlock all features.

    +
    +
    + )} + {hasPendingPayment && ( +
    + +
    +

    Payment Pending Review

    +

    Your payment is being processed. Credits will be added once approved.

    - )} - {hasPendingManualPayment && ( -
    - We received your manual payment. It’s pending admin approval; activation will complete once approved.
    )} - {error && ( -
    - +
    +

    {error}

    )} - {/* Tab Content */} -
    - {/* Current Plan Tab */} - {activeTab === 'plan' && ( -
    - {/* Current Plan Overview */} -
    - {/* Main Plan Card */} - -

    Your Current Plan

    - {!hasActivePlan && ( -
    - -
    -

    No Active Plan

    -

    Choose a plan below to activate your account and unlock all features.

    -
    -
    - )} -
    -
    -
    -
    - {currentPlan?.name || 'No Plan Selected'} -
    -
    - {currentPlan?.description || 'Select a plan to unlock full access.'} -
    -
    - - {hasActivePlan ? subscriptionStatus : 'Inactive'} - -
    - - {/* Quick Stats Grid */} -
    -
    -
    - - Monthly Credits -
    -
    - {creditBalance?.plan_credits_per_month?.toLocaleString?.() || 0} -
    -
    -
    -
    - - Current Balance -
    -
    - {creditBalance?.credits?.toLocaleString?.() || 0} -
    -
    -
    -
    - - Renewal Date -
    -
    - {currentSubscription?.current_period_end - ? new Date(currentSubscription.current_period_end).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) - : '—'} -
    -
    -
    - - {/* Action Buttons */} -
    - - - {hasActivePlan && ( - - )} -
    +
    + {/* SECTION 1: Current Plan Hero */} +
    + {/* Main Plan Card */} + +
    +
    +
    +

    + {currentPlan?.name || 'No Plan'} +

    + + {hasActivePlan ? 'Active' : 'Inactive'} +
    - - - {/* Plan Features Card */} - -

    Included Features

    -
    - {(currentPlan?.features && currentPlan.features.length > 0 - ? currentPlan.features - : ['AI Content Writer', 'Image Generation', 'Auto Publishing', 'Custom Prompts', 'Email Support', 'API Access']) - .map((feature: string, index: number) => ( -
    - - {feature} -
    - ))} -
    -
    +

    + {currentPlan?.description || 'Select a plan to unlock features'} +

    +
    +
    - {/* Plan Limits Overview */} - {hasActivePlan && ( - -
    -
    -

    Quick Limits Overview

    -

    - Key plan limits at a glance -

    -
    - + {/* Quick Stats */} +
    +
    +
    + + Credits
    -
    -
    -
    - - Sites -
    -
    - {currentPlan?.max_sites === 9999 ? '∞' : currentPlan?.max_sites || 0} -
    -
    -
    -
    - - Team Members -
    -
    - {currentPlan?.max_users === 9999 ? '∞' : currentPlan?.max_users || 0} -
    -
    -
    -
    - - Content Words/mo -
    -
    - {currentPlan?.max_content_words === 9999999 - ? '∞' - : currentPlan?.max_content_words - ? `${(currentPlan.max_content_words / 1000).toFixed(0)}K` - : 0} -
    -
    -
    -
    - - Monthly Credits -
    -
    - {currentPlan?.included_credits?.toLocaleString?.() || 0} -
    -
    +
    + {creditBalance?.credits?.toLocaleString() || 0}
    - - )} -
    - )} +
    Available now
    +
    +
    +
    + + Used +
    +
    + {creditBalance?.credits_used_this_month?.toLocaleString() || 0} +
    +
    This month ({creditUsage}%)
    +
    +
    +
    + + Renews +
    +
    + {currentSubscription?.current_period_end + ? new Date(currentSubscription.current_period_end).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + : '—'} +
    +
    Next billing
    +
    +
    +
    + + Monthly +
    +
    + ${Number(currentPlan?.price || 0).toFixed(0)} +
    +
    Per month
    +
    +
    - {/* Purchase/Upgrade Tab */} - {activeTab === 'upgrade' && ( -
    - {/* Upgrade Plans Section */} -
    -
    - { - // Only show paid plans (exclude Free Plan) - const planName = (plan.name || '').toLowerCase(); - const planPrice = plan.price || 0; - return planPrice > 0 && !planName.includes('free'); - }) - .map(plan => ({ - ...convertToPricingPlan(plan), - buttonText: plan.id === currentPlanId ? 'Current Plan' : 'Choose Plan', - disabled: plan.id === currentPlanId || planLoadingId === plan.id, - }))} - showToggle={true} - onPlanSelect={(plan) => plan.id && handleSelectPlan(plan.id)} + {/* Credit Usage Bar */} +
    +
    + Credit Usage + + {creditBalance?.credits_used_this_month?.toLocaleString() || 0} / {creditBalance?.plan_credits_per_month?.toLocaleString() || 0} + +
    +
    +
    = 90 ? 'bg-error-500' : creditUsage >= 70 ? 'bg-warning-500' : 'bg-gradient-to-r from-brand-500 to-purple-500' + }`} + style={{ width: `${Math.min(creditUsage, 100)}%` }} />
    +
    + - {/* Plan Change Policy */} - -

    - - Plan Change Policy -

    -
      -
    • - - Upgrades take effect immediately with prorated billing -
    • -
    • - - Downgrades take effect at the end of your current billing period -
    • -
    • - - Unused credits carry over when changing plans -
    • -
    • - - Cancel anytime - no long-term commitments -
    • -
    -
    + {/* Plan Limits Summary */} + +
    +

    Plan Limits

    + + View Usage + +
    +
    +
    +
    +
    + +
    + Sites +
    + + {currentPlan?.max_sites === 9999 ? '∞' : currentPlan?.max_sites || 0} + +
    +
    +
    +
    + +
    + Team Members +
    + + {currentPlan?.max_users === 9999 ? '∞' : currentPlan?.max_users || 0} + +
    +
    +
    +
    + +
    + Keywords +
    + + {currentPlan?.max_keywords ? currentPlan.max_keywords.toLocaleString() : 0} + +
    +
    +
    +
    + +
    + Monthly Credits +
    + + {currentPlan?.included_credits?.toLocaleString() || 0} + +
    -{/* Purchase Additional Credits Section - Hidden from regular users - removed for simplification */} -
    - )} + {hasActivePlan && ( + + )} + +
    - {/* Billing History Tab */} - {activeTab === 'invoices' && ( -
    - {/* Invoices Section */} - -
    -

    Invoices

    -
    -
    - - - - - - - - - - - - {invoices.length === 0 ? ( - - - - ) : ( - invoices.map((invoice) => ( - - - - - - - - )) - )} - -
    - Invoice - - Date - - Amount - - Status - - Actions -
    - - No invoices yet -
    {invoice.invoice_number} - {new Date(invoice.created_at).toLocaleDateString()} - ${invoice.total_amount} - - {invoice.status} - - - -
    -
    -
    - - {/* Payments Section */} - -
    -

    Payments

    -

    Recent payments and manual submissions

    -
    -
    - - - - - - - - - - - - {payments.length === 0 ? ( - - - - ) : ( - payments.map((payment) => ( - - - - - - - - )) - )} - -
    InvoiceAmountMethodStatusDate
    - No payments yet -
    - {payment.invoice_number || payment.invoice_id || '-'} - - ${payment.amount} - - {payment.payment_method} - - - {payment.status} - - - {new Date(payment.created_at).toLocaleDateString()} -
    -
    -
    - - {/* Payment Methods Section */} - -
    + {/* SECTION 2: Buy Credits + Quick Upgrade */} +
    + {/* Buy Additional Credits */} + +
    +
    +
    + +
    -

    Payment Methods

    -

    Manage your payment methods

    +

    Buy Credits

    +

    Top up your credit balance

    -
    - {paymentMethods.map((method) => ( -
    -
    - -
    -
    {method.display_name}
    -
    {method.type}
    - {method.instructions && ( -
    {method.instructions}
    - )} -
    -
    -
    - {method.is_enabled && ( - Active - )} - {method.is_default ? ( - Default - ) : ( - - )} - +
    +
    + {packages.slice(0, 4).map((pkg) => ( + + ))} +
    + + + {/* Quick Upgrade Options */} + +
    +
    +
    + +
    +
    +

    Upgrade Plan

    +

    Get more features & credits

    +
    +
    + +
    +
    + {upgradePlans.slice(0, 3).map((plan) => ( +
    +
    +
    {plan.name}
    +
    + {plan.included_credits?.toLocaleString() || 0} credits/mo
    - ))} - {paymentMethods.length === 0 && ( -
    - No payment methods configured +
    +
    ${plan.price}/mo
    + +
    +
    + ))} + {upgradePlans.length === 0 && ( +
    + +

    You're on the best plan!

    +
    + )} +
    + +
    + + {/* SECTION 3: Billing History */} + +
    +
    +
    + +
    +
    +

    Billing History

    +

    Recent invoices and transactions

    +
    +
    +
    +
    + + + + + + + + + + + + {invoices.length === 0 ? ( + + + + ) : ( + invoices.slice(0, 5).map((invoice) => ( + + + + + + + + )) + )} + +
    InvoiceDateAmountStatusActions
    +
    + +

    No invoices yet

    +

    Your billing history will appear here

    +
    +
    {invoice.invoice_number} + {new Date(invoice.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} + ${invoice.total_amount} + + {invoice.status} + + + +
    +
    +
    + + {/* SECTION 4: Payment Methods */} + +
    +
    +
    + +
    +
    +

    Payment Methods

    +

    Manage how you pay

    +
    +
    +
    +
    + {paymentMethods.map((method) => ( +
    +
    +
    + {method.type === 'bank_transfer' ? ( + + ) : ( + + )} +
    +
    {method.display_name}
    +
    {method.type?.replace('_', ' ')}
    +
    +
    + {method.is_default && ( + Default + )} +
    + {selectedPaymentMethod !== method.type && ( + + )} + {selectedPaymentMethod === method.type && ( +
    + + Selected for payment
    )}
    - + ))} + {paymentMethods.length === 0 && ( +
    + No payment methods available +
    + )}
    - )} +
    - {/* Cancellation Confirmation Modal */} + {/* Upgrade Modal */} + {showUpgradeModal && ( +
    +
    +
    +
    +

    Choose Your Plan

    +

    Select the plan that fits your needs

    +
    + +
    + + {/* Billing Toggle */} +
    +
    + + +
    +
    + + {/* Plans Grid */} +
    + {upgradePlans.map((plan, index) => { + const isPopular = index === 1; + const planPrice = Number(plan.price) || 0; + const annualPrice = planPrice * 0.8 * 12; + const displayPrice = selectedBillingCycle === 'annual' ? (annualPrice / 12).toFixed(0) : planPrice; + + return ( +
    + {isPopular && ( +
    + Most Popular +
    + )} +
    +

    {plan.name}

    +
    + ${displayPrice} + /mo +
    + {selectedBillingCycle === 'annual' && ( +
    + ${annualPrice.toFixed(0)} billed annually +
    + )} +
    +
      +
    • + + {plan.included_credits?.toLocaleString() || 0} credits/month +
    • +
    • + + {plan.max_sites === 9999 ? 'Unlimited' : plan.max_sites} sites +
    • +
    • + + {plan.max_users === 9999 ? 'Unlimited' : plan.max_users} team members +
    • +
    • + + {plan.max_keywords?.toLocaleString() || 0} keywords +
    • +
    + +
    + ); + })} +
    + + {/* Policy Info */} +
    +
    + +
    + Flexible billing: Upgrades take effect immediately with prorated billing. + Downgrades apply at the end of your billing period. Cancel anytime with no penalties. +
    +
    +
    +
    +
    + )} + + {/* Cancel Confirmation Modal */} {showCancelConfirm && (

    Cancel Subscription

    -
    - +
    -

    Are you sure you want to cancel?

    -

    Your subscription will remain active until the end of your current billing period. After that:

    +

    Are you sure?

    +

    Your subscription will remain active until the end of your billing period.

      @@ -838,38 +840,28 @@ export default function PlansAndBillingPage() {
    • - Remaining credits will be preserved for 30 days + Remaining credits preserved for 30 days
    • - - You can resubscribe anytime to restore access + + Resubscribe anytime to restore access
    -
    @@ -878,4 +870,4 @@ export default function PlansAndBillingPage() { )} ); -} \ No newline at end of file +} diff --git a/frontend/src/pages/account/UsageAnalyticsPage.tsx b/frontend/src/pages/account/UsageAnalyticsPage.tsx index 53f7ae93..dff6d657 100644 --- a/frontend/src/pages/account/UsageAnalyticsPage.tsx +++ b/frontend/src/pages/account/UsageAnalyticsPage.tsx @@ -6,7 +6,7 @@ import { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; -import { TrendingUpIcon, ActivityIcon, BarChart3Icon, ZapIcon, CalendarIcon } from '../../icons'; +import { TrendingUpIcon, ActivityIcon, BarChart3Icon, ZapIcon, CalendarIcon, PieChartIcon } from '../../icons'; import PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; import { useToast } from '../../components/ui/toast/ToastContainer'; @@ -15,13 +15,15 @@ import { Card } from '../../components/ui/card'; import Badge from '../../components/ui/badge/Badge'; import BillingUsagePanel from '../../components/billing/BillingUsagePanel'; import UsageLimitsPanel from '../../components/billing/UsageLimitsPanel'; +import CreditInsightsCharts from '../../components/billing/CreditInsightsCharts'; import Button from '../../components/ui/button/Button'; -type TabType = 'limits' | 'activity' | 'api'; +type TabType = 'limits' | 'activity' | 'insights' | 'api'; // Map URL paths to tab types function getTabFromPath(pathname: string): TabType { if (pathname.includes('/credits')) return 'activity'; + if (pathname.includes('/insights')) return 'insights'; if (pathname.includes('/activity')) return 'api'; return 'limits'; } @@ -59,12 +61,14 @@ export default function UsageAnalyticsPage() { const tabTitles: Record = { limits: 'Limits & Usage', activity: 'Credit History', + insights: 'Credit Insights', api: 'Activity Log', }; const tabDescriptions: Record = { limits: 'See how much you\'re using - Track your credits and content limits', activity: 'See where your credits go - Track credit usage history', + insights: 'Visualize your usage patterns - Charts and analytics', api: 'Technical requests - Monitor API activity and usage', }; @@ -143,8 +147,8 @@ export default function UsageAnalyticsPage() {
    )} - {/* Period Selector (only show on activity and api tabs) */} - {(activeTab === 'activity' || activeTab === 'api') && ( + {/* Period Selector (only show on activity, insights and api tabs) */} + {(activeTab === 'activity' || activeTab === 'api' || activeTab === 'insights') && (
    {[7, 30, 90].map((value) => { @@ -181,6 +185,15 @@ export default function UsageAnalyticsPage() {
    )} + {/* Credit Insights Tab */} + {activeTab === 'insights' && ( + + )} + {/* API Usage Tab */} {activeTab === 'api' && (
    diff --git a/frontend/src/pages/account/UsageDashboardPage.tsx b/frontend/src/pages/account/UsageDashboardPage.tsx new file mode 100644 index 00000000..6b022066 --- /dev/null +++ b/frontend/src/pages/account/UsageDashboardPage.tsx @@ -0,0 +1,679 @@ +/** + * Usage Dashboard - Unified Analytics Page + * Single comprehensive view of all usage, limits, and credit analytics + * Replaces the 4-tab structure with a clean, organized dashboard + */ + +import { useState, useEffect } from 'react'; +import Chart from 'react-apexcharts'; +import { ApexOptions } from 'apexcharts'; +import { + TrendingUpIcon, + ZapIcon, + GlobeIcon, + UsersIcon, + TagIcon, + SearchIcon, + CalendarIcon, + PieChartIcon, + FileTextIcon, + ImageIcon, + RefreshCwIcon, + ChevronDownIcon, + ArrowRightIcon, +} from '../../icons'; +import PageMeta from '../../components/common/PageMeta'; +import PageHeader from '../../components/common/PageHeader'; +import { useToast } from '../../components/ui/toast/ToastContainer'; +import { + getUsageAnalytics, + UsageAnalytics, + getCreditBalance, + type CreditBalance, + getUsageSummary, + type UsageSummary, + type LimitUsage, + getCreditUsageSummary, +} from '../../services/billing.api'; +import { Card } from '../../components/ui/card'; +import Badge from '../../components/ui/badge/Badge'; +import Button from '../../components/ui/button/Button'; +import { Link } from 'react-router-dom'; + +// User-friendly operation names - no model/token details +const OPERATION_LABELS: Record = { + content_generation: 'Content Writing', + image_generation: 'Image Creation', + image_prompt_extraction: 'Image Prompts', + keyword_clustering: 'Keyword Clustering', + clustering: 'Keyword Clustering', + idea_generation: 'Content Ideas', + content_analysis: 'Content Analysis', + linking: 'Internal Linking', +}; + +// Chart colors - use hex for consistent coloring between pie chart and table +const CHART_COLORS = [ + '#3b82f6', // blue - Content Writing + '#ec4899', // pink - Image Prompts + '#22c55e', // green - Content Ideas + '#f59e0b', // amber - Keyword Clustering + '#8b5cf6', // purple - Image Creation + '#ef4444', // red + '#14b8a6', // teal + '#6366f1', // indigo +]; + +// Map operation types to their output unit names +const OPERATION_UNITS: Record = { + content_generation: 'Articles', + image_generation: 'Images', + image_prompt_extraction: 'Prompts', + keyword_clustering: 'Clusters', + clustering: 'Clusters', + idea_generation: 'Ideas', + content_analysis: 'Analyses', + linking: 'Links', +}; + +export default function UsageDashboardPage() { + const toast = useToast(); + const [analytics, setAnalytics] = useState(null); + const [creditBalance, setCreditBalance] = useState(null); + const [usageSummary, setUsageSummary] = useState(null); + const [creditConsumption, setCreditConsumption] = useState>({}); + const [loading, setLoading] = useState(true); + const [period, setPeriod] = useState(30); + + useEffect(() => { + loadAllData(); + }, [period]); + + const loadAllData = async () => { + try { + setLoading(true); + // Calculate start date for period + const endDate = new Date(); + const startDate = new Date(); + startDate.setDate(startDate.getDate() - period); + + const [analyticsData, balanceData, summaryData, consumptionData] = await Promise.all([ + getUsageAnalytics(period), + getCreditBalance(), + getUsageSummary(), + getCreditUsageSummary({ + start_date: startDate.toISOString(), + end_date: endDate.toISOString(), + }), + ]); + setAnalytics(analyticsData); + setCreditBalance(balanceData); + setUsageSummary(summaryData); + setCreditConsumption(consumptionData.by_operation || {}); + } catch (error: any) { + toast.error(`Failed to load usage data: ${error.message}`); + } finally { + setLoading(false); + } + }; + + // Calculate credit usage percentage + const creditPercentage = creditBalance && creditBalance.plan_credits_per_month > 0 + ? Math.round((creditBalance.credits_used_this_month / creditBalance.plan_credits_per_month) * 100) + : 0; + + // Prepare timeline chart data + const dailyData = analytics?.daily_usage || []; + const timelineCategories = dailyData.map(d => { + const date = new Date(d.date); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + }); + + const timelineOptions: ApexOptions = { + chart: { + type: 'area', + fontFamily: 'Outfit, sans-serif', + height: 200, + sparkline: { enabled: false }, + toolbar: { show: false }, + zoom: { enabled: false }, + }, + colors: ['var(--color-brand-500)'], + dataLabels: { enabled: false }, + stroke: { curve: 'smooth', width: 2 }, + fill: { + type: 'gradient', + gradient: { shadeIntensity: 1, opacityFrom: 0.4, opacityTo: 0.1, stops: [0, 90, 100] }, + }, + xaxis: { + categories: timelineCategories, + axisBorder: { show: false }, + axisTicks: { show: false }, + labels: { + style: { colors: 'var(--color-gray-500)', fontFamily: 'Outfit' }, + rotate: -45, + rotateAlways: dailyData.length > 14, + }, + }, + yaxis: { + labels: { + style: { colors: 'var(--color-gray-500)', fontFamily: 'Outfit' }, + formatter: (val: number) => val.toLocaleString(), + }, + }, + grid: { borderColor: 'var(--color-gray-200)', strokeDashArray: 4 }, + tooltip: { y: { formatter: (val: number) => `${val.toLocaleString()} credits` } }, + }; + + // Prepare donut chart for credit consumption (from creditConsumption state) + // Sort by credits descending so pie chart and table colors match + const consumptionEntries = Object.entries(creditConsumption) + .filter(([_, data]) => data.credits > 0) + .sort((a, b) => b[1].credits - a[1].credits); + const donutLabels = consumptionEntries.map(([opType]) => OPERATION_LABELS[opType] || opType.replace(/_/g, ' ')); + const donutSeries = consumptionEntries.map(([_, data]) => data.credits); + const totalCreditsUsed = donutSeries.reduce((sum, val) => sum + val, 0); + + const donutOptions: ApexOptions = { + chart: { type: 'donut', fontFamily: 'Outfit, sans-serif' }, + labels: donutLabels, + colors: CHART_COLORS.slice(0, donutLabels.length), + legend: { show: false }, + plotOptions: { + pie: { + donut: { + size: '75%', + labels: { + show: true, + name: { show: true, fontSize: '11px', fontFamily: 'Outfit', color: '#6b7280' }, + value: { show: true, fontSize: '18px', fontFamily: 'Outfit', fontWeight: 600, color: '#111827', formatter: (val: string) => parseInt(val).toLocaleString() }, + total: { show: true, label: 'Total Credits', fontSize: '11px', fontFamily: 'Outfit', color: '#6b7280', formatter: () => totalCreditsUsed.toLocaleString() }, + }, + }, + }, + }, + dataLabels: { enabled: false }, + tooltip: { + custom: function({ series, seriesIndex, w }) { + const label = w.globals.labels[seriesIndex]; + const value = series[seriesIndex]; + return `
    + ${label}: ${value.toLocaleString()} credits +
    `; + }, + }, + }; + + // Limit card component with Coming Soon support + const LimitCard = ({ + title, + icon, + usage, + type, + color, + comingSoon = false, + }: { + title: string; + icon: React.ReactNode; + usage: LimitUsage | undefined; + type: 'hard' | 'monthly'; + color: string; + comingSoon?: boolean; + }) => { + if (comingSoon) { + return ( +
    +
    + {icon} +
    +
    +
    + {title} + Coming Soon +
    +

    This feature is not yet available

    +
    +
    + ); + } + + if (!usage) return null; + + const percentage = usage.percentage_used; + const isWarning = percentage >= 80; + const isDanger = percentage >= 95; + const barColor = isDanger ? 'var(--color-error-500)' : isWarning ? 'var(--color-warning-500)' : color; + + return ( +
    +
    + {icon} +
    +
    +
    + {title} + + {percentage}% + +
    +
    +
    +
    +
    + {usage.current.toLocaleString()} / {usage.limit.toLocaleString()} + {usage.remaining.toLocaleString()} left +
    +
    +
    + ); + }; + + if (loading) { + return ( + <> + + , color: 'blue' }} + /> +
    +
    + {[1, 2, 3, 4].map(i => ( +
    + ))} +
    +
    +
    + + ); + } + + return ( + <> + + , color: 'blue' }} + actions={ +
    +
    + {[7, 30, 90].map((value) => ( + + ))} +
    + +
    + } + /> + +
    + {/* SECTION 1: Credit Overview - Hero Stats */} +
    + {/* Main Credit Card */} + +
    +
    +

    Credit Balance

    +

    Your available credits for AI operations

    +
    + + + +
    + +
    +
    +
    + {creditBalance?.credits.toLocaleString() || 0} +
    +
    Available Now
    +
    +
    +
    + {creditBalance?.credits_used_this_month.toLocaleString() || 0} +
    +
    Used This Month
    +
    +
    +
    + {creditBalance?.plan_credits_per_month.toLocaleString() || 0} +
    +
    Monthly Allowance
    +
    +
    + + {/* Credit Usage Bar */} +
    +
    + Monthly Usage + {creditPercentage}% +
    +
    +
    +
    +
    + + + {/* Plan Info Card */} + +
    +
    + +
    +
    +

    {usageSummary?.plan_name || 'Your Plan'}

    +

    Current subscription

    +
    +
    + +
    +
    + Billing Period + + {usageSummary?.period_start ? new Date(usageSummary.period_start).toLocaleDateString() : '-'} + +
    +
    + Resets In + + {usageSummary?.days_until_reset || 0} days + +
    +
    + + + + +
    +
    + + {/* SECTION 2: Your Limits */} + +
    +
    +

    Your Limits

    +

    Track your plan resources

    +
    + {usageSummary?.days_until_reset !== undefined && ( + + + Resets in {usageSummary.days_until_reset} days + + )} +
    + +
    + } + usage={usageSummary?.hard_limits?.sites} + type="hard" + color="var(--color-brand-500)" + /> + } + usage={usageSummary?.hard_limits?.users} + type="hard" + color="var(--color-purple-500)" + /> + } + usage={usageSummary?.hard_limits?.keywords} + type="hard" + color="var(--color-success-500)" + /> + } + usage={undefined} + type="monthly" + color="var(--color-warning-500)" + comingSoon={true} + /> +
    +
    + + {/* SECTION 3: Activity Charts */} +
    + {/* Timeline Chart */} + +
    +
    + +
    +
    +

    Credit Usage Over Time

    +

    Last {period} days

    +
    +
    + {dailyData.length > 0 ? ( + Math.abs(d.usage)) }]} + type="area" + height={200} + /> + ) : ( +
    +
    + +

    No activity in this period

    +
    +
    + )} +
    + + {/* Credit Consumption - Pie + Table */} + +
    +
    +
    + +
    +
    +

    Credit Consumption

    +

    Last {period} days by operation

    +
    +
    +
    + +
    + {/* Donut Chart */} +
    + {donutSeries.length > 0 ? ( + + ) : ( +
    +
    + +

    No usage data yet

    +
    +
    + )} +
    + + {/* Consumption Table */} +
    + + + + + + + + + + {consumptionEntries.length > 0 ? ( + consumptionEntries.map(([opType, data], index) => ( + + + + + + )) + ) : ( + + + + )} + +
    OperationCreditsOutput
    +
    +
    + + {OPERATION_LABELS[opType] || opType.replace(/_/g, ' ')} + +
    +
    + {data.credits.toLocaleString()} + + {data.count} {OPERATION_UNITS[opType] || 'Items'} +
    + No consumption data +
    +
    +
    +
    +
    + + {/* SECTION 4: Quick Link to Detailed Logs */} + +
    +
    +
    + +
    +
    +

    Need More Details?

    +

    + View complete history of all AI operations with filters, dates, and USD costs +

    +
    +
    + + + +
    +
    + + {/* SECTION 5: Credit Costs Reference (Collapsible) */} + +
    + +
    +
    + +
    +
    +

    How Credits Work

    +

    See estimated costs for each operation

    +
    +
    + +
    +
    +
    +
    +
    + + Content Writing +
    +

    ~1 credit per 100 words

    +
    +
    +
    + + Image Creation +
    +

    1-15 credits per image (by quality)

    +
    +
    +
    + + Keyword Grouping +
    +

    ~10 credits per batch

    +
    +
    +
    + + Content Ideas +
    +

    ~15 credits per cluster

    +
    +
    +
    + + Image Prompts +
    +

    ~2 credits per prompt

    +
    +
    +
    + + Keyword Research + Soon +
    +

    Uses monthly limit (not credits)

    +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/frontend/src/pages/account/UsageLogsPage.tsx b/frontend/src/pages/account/UsageLogsPage.tsx new file mode 100644 index 00000000..8d375740 --- /dev/null +++ b/frontend/src/pages/account/UsageLogsPage.tsx @@ -0,0 +1,419 @@ +/** + * Usage Logs Page - Detailed AI Operation Logs + * Shows a filterable, paginated table of all credit usage + * Consistent layout with Planner/Writer table pages + */ + +import { useState, useEffect, useMemo } from 'react'; +import { Link } from 'react-router-dom'; +import { + ArrowLeftIcon, + CalendarIcon, + FileTextIcon, + ImageIcon, + TagIcon, + ZapIcon, + RefreshCwIcon, + DollarSignIcon, + TrendingUpIcon, +} from '../../icons'; +import PageMeta from '../../components/common/PageMeta'; +import PageHeader from '../../components/common/PageHeader'; +import { useToast } from '../../components/ui/toast/ToastContainer'; +import { Card } from '../../components/ui/card'; +import Button from '../../components/ui/button/Button'; +import SelectDropdown from '../../components/form/SelectDropdown'; +import Input from '../../components/form/input/InputField'; +import { Pagination } from '../../components/ui/pagination/Pagination'; +import { getCreditUsage, type CreditUsageLog } from '../../services/billing.api'; + +// User-friendly operation names (no model/token details) +const OPERATION_LABELS: Record = { + content_generation: 'Content Writing', + image_generation: 'Image Creation', + image_prompt_extraction: 'Image Prompts', + keyword_clustering: 'Keyword Clustering', + clustering: 'Keyword Clustering', + idea_generation: 'Content Ideas', + content_analysis: 'Content Analysis', + linking: 'Internal Linking', +}; + +// Operation icons +const OPERATION_ICONS: Record = { + content_generation: , + image_generation: , + image_prompt_extraction: , + keyword_clustering: , + clustering: , + idea_generation: , + content_analysis: , + linking: , +}; + +// Operation type options for filter (only enabled operations) +const OPERATION_OPTIONS = [ + { value: '', label: 'All Operations' }, + { value: 'content_generation', label: 'Content Writing' }, + { value: 'image_generation', label: 'Image Creation' }, + { value: 'image_prompt_extraction', label: 'Image Prompts' }, + { value: 'keyword_clustering', label: 'Keyword Clustering' }, + { value: 'idea_generation', label: 'Content Ideas' }, +]; + +export default function UsageLogsPage() { + const toast = useToast(); + + // Data state + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [totalCount, setTotalCount] = useState(0); + + // Pagination state + const [currentPage, setCurrentPage] = useState(1); + const [pageSize] = useState(20); + + // Filter state + const [operationFilter, setOperationFilter] = useState(''); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + + // Calculate total pages + const totalPages = Math.ceil(totalCount / pageSize); + + // Load usage logs + const loadLogs = async () => { + try { + setLoading(true); + const params: any = {}; + + if (operationFilter) { + params.operation_type = operationFilter; + } + if (startDate) { + params.start_date = startDate; + } + if (endDate) { + params.end_date = endDate; + } + + // Add pagination params + params.page = currentPage; + params.page_size = pageSize; + + const data = await getCreditUsage(params); + setLogs(data.results || []); + setTotalCount(data.count || 0); + } catch (error: any) { + toast.error(`Failed to load usage logs: ${error.message}`); + } finally { + setLoading(false); + } + }; + + // Load on mount and when filters change + useEffect(() => { + loadLogs(); + }, [currentPage, operationFilter, startDate, endDate]); + + // Reset to page 1 when filters change + useEffect(() => { + setCurrentPage(1); + }, [operationFilter, startDate, endDate]); + + // Format date for display + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + // Format cost in USD + const formatCost = (cost: string | null | undefined) => { + if (!cost) return '$0.00'; + const num = parseFloat(cost); + if (isNaN(num)) return '$0.00'; + return `$${num.toFixed(4)}`; + }; + + // Get operation display info + const getOperationDisplay = (type: string) => { + return { + label: OPERATION_LABELS[type] || type.replace(/_/g, ' '), + icon: OPERATION_ICONS[type] || , + }; + }; + + // Summary stats - calculate from all loaded logs + const summaryStats = useMemo(() => { + const totalCredits = logs.reduce((sum, log) => sum + log.credits_used, 0); + const totalCost = logs.reduce((sum, log) => sum + (parseFloat(log.cost_usd || '0') || 0), 0); + const avgCreditsPerOp = logs.length > 0 ? Math.round(totalCredits / logs.length) : 0; + + // Count by operation type + const byOperation = logs.reduce((acc, log) => { + const op = log.operation_type; + acc[op] = (acc[op] || 0) + 1; + return acc; + }, {} as Record); + const topOperation = Object.entries(byOperation).sort((a, b) => b[1] - a[1])[0]; + + return { totalCredits, totalCost, avgCreditsPerOp, topOperation }; + }, [logs]); + + // Clear all filters + const clearFilters = () => { + setOperationFilter(''); + setStartDate(''); + setEndDate(''); + }; + + const hasActiveFilters = operationFilter || startDate || endDate; + + return ( + <> + + , color: 'purple' }} + actions={ +
    + + + + +
    + } + /> + +
    + {/* Summary Cards - 5 metrics */} +
    + +
    +
    + +
    +
    +
    + {summaryStats.totalCredits.toLocaleString()} +
    +
    + Credits Used +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + {formatCost(summaryStats.totalCost.toString())} +
    +
    + Total Cost +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + {totalCount.toLocaleString()} +
    +
    + Operations +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + {summaryStats.avgCreditsPerOp} +
    +
    + Avg/Operation +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + {summaryStats.topOperation ? OPERATION_LABELS[summaryStats.topOperation[0]] || summaryStats.topOperation[0] : '-'} +
    +
    + Top Operation +
    +
    +
    +
    +
    + + {/* Filters - Inline style like Planner pages */} +
    +
    + +
    +
    + setStartDate(e.target.value)} + placeholder="Start Date" + className="h-9 text-sm" + /> +
    +
    + setEndDate(e.target.value)} + placeholder="End Date" + className="h-9 text-sm" + /> +
    + {hasActiveFilters && ( + + )} +
    + + {/* Table - Half width on large screens */} +
    +
    +
    + + + + + + + + + + + {loading ? ( + // Loading skeleton + Array.from({ length: 10 }).map((_, i) => ( + + + + + + + )) + ) : logs.length === 0 ? ( + + + + ) : ( + logs.map((log) => { + const operationDisplay = getOperationDisplay(log.operation_type); + return ( + + + + + + + ); + }) + )} + +
    DateOperationCreditsCost (USD)
    +
    + +

    + No usage logs found +

    +

    + {hasActiveFilters + ? 'Try adjusting your filters to see more results.' + : 'Your AI operation history will appear here.'} +

    +
    +
    + {formatDate(log.created_at)} + +
    +
    + {operationDisplay.icon} +
    + + {operationDisplay.label} + +
    +
    + {log.credits_used.toLocaleString()} + + {formatCost(log.cost_usd)} +
    +
    + + {/* Pagination */} + {totalPages > 1 && ( +
    + + Showing {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, totalCount)} of {totalCount.toLocaleString()} + + +
    + )} +
    +
    +
    + + ); +} diff --git a/frontend/src/services/billing.api.ts b/frontend/src/services/billing.api.ts index df2108a3..e0e44dae 100644 --- a/frontend/src/services/billing.api.ts +++ b/frontend/src/services/billing.api.ts @@ -225,6 +225,8 @@ export async function getCreditUsage(params?: { operation_type?: string; start_date?: string; end_date?: string; + page?: number; + page_size?: number; }): Promise<{ results: CreditUsageLog[]; count: number; @@ -233,6 +235,8 @@ export async function getCreditUsage(params?: { if (params?.operation_type) queryParams.append('operation_type', params.operation_type); if (params?.start_date) queryParams.append('start_date', params.start_date); if (params?.end_date) queryParams.append('end_date', params.end_date); + if (params?.page) queryParams.append('page', params.page.toString()); + if (params?.page_size) queryParams.append('page_size', params.page_size.toString()); const url = `/v1/billing/credits/usage/${queryParams.toString() ? '?' + queryParams.toString() : ''}`; return fetchAPI(url); @@ -905,17 +909,13 @@ export interface Plan { features?: string[]; limits?: Record; display_order?: number; - // Hard Limits + // Hard Limits (only 3 persistent 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; + // Monthly Limits (only ahrefs queries) + max_ahrefs_queries?: number; + // Credits included_credits?: number; } @@ -934,18 +934,15 @@ export interface UsageSummary { period_start: string; period_end: string; days_until_reset: number; + // Simplified to only 3 hard limits hard_limits: { sites?: LimitUsage; users?: LimitUsage; keywords?: LimitUsage; - clusters?: LimitUsage; }; + // Simplified to only 1 monthly limit (Ahrefs queries) monthly_limits: { - content_ideas?: LimitUsage; - content_words?: LimitUsage; - images_basic?: LimitUsage; - images_premium?: LimitUsage; - image_prompts?: LimitUsage; + ahrefs_queries?: LimitUsage; }; } diff --git a/frontend/src/utils/creditCheck.ts b/frontend/src/utils/creditCheck.ts new file mode 100644 index 00000000..4a0e82fe --- /dev/null +++ b/frontend/src/utils/creditCheck.ts @@ -0,0 +1,79 @@ +/** + * Credit Check Utilities + * Pre-flight credit checks for AI operations + */ + +import { getCreditBalance, type CreditBalance } from '../services/billing.api'; + +export interface CreditCheckResult { + hasEnoughCredits: boolean; + availableCredits: number; + requiredCredits: number; + shortfall: number; +} + +/** + * Check if account has enough credits for an operation + * @param estimatedCredits - Estimated credits needed for the operation + * @returns Credit check result with balance info + */ +export async function checkCreditsBeforeOperation( + estimatedCredits: number +): Promise { + try { + const balance: CreditBalance = await getCreditBalance(); + const hasEnoughCredits = balance.credits >= estimatedCredits; + + return { + hasEnoughCredits, + availableCredits: balance.credits, + requiredCredits: estimatedCredits, + shortfall: Math.max(0, estimatedCredits - balance.credits), + }; + } catch (error) { + console.error('Failed to check credit balance:', error); + // Return pessimistic result on error to prevent operation + return { + hasEnoughCredits: false, + availableCredits: 0, + requiredCredits: estimatedCredits, + shortfall: estimatedCredits, + }; + } +} + +/** + * Estimated credit costs for common operations + * These are estimates - actual costs depend on token usage + */ +export const ESTIMATED_CREDIT_COSTS = { + // Content Generation + content_generation_short: 5, // ~500 words + content_generation_medium: 10, // ~1000 words + content_generation_long: 20, // ~2000+ words + + // Clustering & Planning + cluster_keywords: 3, // Per clustering operation + generate_content_ideas: 5, // Per batch of ideas + + // Image Generation + image_basic: 2, // Basic quality + image_premium: 5, // Premium quality + + // SEO Optimization + seo_analysis: 2, + seo_optimization: 3, + + // Internal Linking + internal_linking: 2, +} as const; + +/** + * Get estimated cost for an operation type + */ +export function getEstimatedCost( + operationType: keyof typeof ESTIMATED_CREDIT_COSTS, + quantity: number = 1 +): number { + return ESTIMATED_CREDIT_COSTS[operationType] * quantity; +} diff --git a/frontend/src/utils/pricingHelpers.ts b/frontend/src/utils/pricingHelpers.ts index 7bc04a62..fb249374 100644 --- a/frontend/src/utils/pricingHelpers.ts +++ b/frontend/src/utils/pricingHelpers.ts @@ -16,12 +16,7 @@ export interface Plan { max_sites?: number; max_users?: number; max_keywords?: number; - max_clusters?: number; - max_content_ideas?: number; - max_content_words?: number; - max_images_basic?: number; - max_images_premium?: number; - max_image_prompts?: number; + max_ahrefs_queries?: number; included_credits?: number; } @@ -37,8 +32,8 @@ export const convertToPricingPlan = (plan: Plan): PricingPlan => { const features: string[] = []; // Dynamic counts - shown with numbers from backend - if (plan.max_content_ideas) { - features.push(`**${formatNumber(plan.max_content_ideas)} Pages/Articles per month**`); + if (plan.max_keywords) { + features.push(`**${formatNumber(plan.max_keywords)} Keywords**`); } if (plan.max_sites) { features.push(`${plan.max_sites === 999999 ? 'Unlimited' : formatNumber(plan.max_sites)} Site${plan.max_sites > 1 && plan.max_sites !== 999999 ? 's' : ''}`);