Subscription dashboard improvements, AI text generation limit, and other fixes.

This commit is contained in:
ajaysi
2025-11-01 18:01:14 +05:30
parent cdb41aec1b
commit de4328175d
64 changed files with 5809 additions and 444 deletions

View File

@@ -5,10 +5,11 @@ Main router for blog writing operations including research, outline generation,
content creation, SEO analysis, and publishing.
"""
from fastapi import APIRouter, HTTPException
from typing import Any, Dict, List
from fastapi import APIRouter, HTTPException, Depends
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
from loguru import logger
from middleware.auth_middleware import get_current_user
from models.blog_models import (
BlogResearchRequest,
@@ -64,10 +65,21 @@ class SEOApplyRecommendationsRequest(BaseModel):
@router.post("/seo/apply-recommendations")
async def apply_seo_recommendations(request: SEOApplyRecommendationsRequest) -> Dict[str, Any]:
async def apply_seo_recommendations(
request: SEOApplyRecommendationsRequest,
current_user: Dict[str, Any] = Depends(get_current_user)
) -> Dict[str, Any]:
"""Apply actionable SEO recommendations and return updated content."""
try:
result = await recommendation_applier.apply_recommendations(request.dict())
# Extract Clerk user ID (required)
if not current_user:
raise HTTPException(status_code=401, detail="Authentication required")
user_id = str(current_user.get('id', ''))
if not user_id:
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
result = await recommendation_applier.apply_recommendations(request.dict(), user_id=user_id)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed to apply recommendations"))
return result
@@ -87,13 +99,24 @@ async def health() -> Dict[str, Any]:
# Research Endpoints
@router.post("/research/start")
async def start_research(request: BlogResearchRequest) -> Dict[str, Any]:
async def start_research(
request: BlogResearchRequest,
current_user: Dict[str, Any] = Depends(get_current_user)
) -> Dict[str, Any]:
"""Start a research operation and return a task ID for polling."""
try:
# TODO: Get user_id from authentication context
user_id = "anonymous" # This should come from auth middleware
# Extract Clerk user ID (required)
if not current_user:
raise HTTPException(status_code=401, detail="Authentication required")
user_id = str(current_user.get('id', ''))
if not user_id:
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
task_id = await task_manager.start_research_task(request, user_id)
return {"task_id": task_id, "status": "started"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to start research: {e}")
raise HTTPException(status_code=500, detail=str(e))
@@ -107,6 +130,50 @@ async def get_research_status(task_id: str) -> Dict[str, Any]:
if status is None:
raise HTTPException(status_code=404, detail="Task not found")
# If task failed with subscription error, return HTTP error so frontend interceptor can catch it
if status.get('status') == 'failed' and status.get('error_status') in [429, 402]:
error_data = status.get('error_data', {}) or {}
error_status = status.get('error_status', 429)
if not isinstance(error_data, dict):
logger.warning(f"Research task {task_id} error_data not dict: {error_data}")
error_data = {'error': str(error_data)}
# Determine provider and usage info
stored_error_message = status.get('error', error_data.get('error'))
provider = error_data.get('provider', 'unknown')
usage_info = error_data.get('usage_info')
if not usage_info:
usage_info = {
'provider': provider,
'message': stored_error_message,
'error_type': error_data.get('error_type', 'unknown')
}
# Include any known fields from error_data
for key in ['current_tokens', 'requested_tokens', 'limit', 'current_calls']:
if key in error_data:
usage_info[key] = error_data[key]
# Build error message for detail
error_msg = error_data.get('message', stored_error_message or 'Subscription limit exceeded')
# Log the subscription error with all context
logger.warning(f"Research task {task_id} failed with subscription error {error_status}: {error_msg}")
logger.warning(f" Provider: {provider}, Usage Info: {usage_info}")
# Use JSONResponse to ensure detail is returned as-is, not wrapped in an array
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=error_status,
content={
'error': error_data.get('error', stored_error_message or 'Subscription limit exceeded'),
'message': error_msg,
'provider': provider,
'usage_info': usage_info
}
)
logger.info(f"Research status request for {task_id}: {status['status']} with {len(status.get('progress_messages', []))} progress messages")
return status
except HTTPException:
@@ -310,20 +377,46 @@ async def hallucination_check(request: HallucinationCheckRequest) -> Hallucinati
# SEO Endpoints
@router.post("/seo/analyze", response_model=BlogSEOAnalyzeResponse)
async def seo_analyze(request: BlogSEOAnalyzeRequest) -> BlogSEOAnalyzeResponse:
async def seo_analyze(
request: BlogSEOAnalyzeRequest,
current_user: Dict[str, Any] = Depends(get_current_user)
) -> BlogSEOAnalyzeResponse:
"""Analyze content for SEO optimization opportunities."""
try:
return await service.seo_analyze(request)
# Extract Clerk user ID (required)
if not current_user:
raise HTTPException(status_code=401, detail="Authentication required")
user_id = str(current_user.get('id', ''))
if not user_id:
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
return await service.seo_analyze(request, user_id=user_id)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to perform SEO analysis: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/seo/metadata", response_model=BlogSEOMetadataResponse)
async def seo_metadata(request: BlogSEOMetadataRequest) -> BlogSEOMetadataResponse:
async def seo_metadata(
request: BlogSEOMetadataRequest,
current_user: Dict[str, Any] = Depends(get_current_user)
) -> BlogSEOMetadataResponse:
"""Generate SEO metadata for the blog post."""
try:
return await service.seo_metadata(request)
# Extract Clerk user ID (required)
if not current_user:
raise HTTPException(status_code=401, detail="Authentication required")
user_id = str(current_user.get('id', ''))
if not user_id:
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
return await service.seo_metadata(request, user_id=user_id)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to generate SEO metadata: {e}")
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -10,6 +10,7 @@ import asyncio
import uuid
from datetime import datetime
from typing import Any, Dict, List
from fastapi import HTTPException
from loguru import logger
from models.blog_models import (
@@ -85,6 +86,10 @@ class TaskManager:
response["result"] = task["result"]
elif task["status"] == "failed":
response["error"] = task["error"]
if "error_status" in task:
response["error_status"] = task["error_status"]
if "error_data" in task:
response["error_data"] = task["error_data"]
return response
@@ -109,14 +114,17 @@ class TaskManager:
logger.info(f"Progress update for task {task_id}: {message}")
async def start_research_task(self, request: BlogResearchRequest, user_id: str = "anonymous") -> str:
async def start_research_task(self, request: BlogResearchRequest, user_id: str) -> str:
"""Start a research operation and return a task ID."""
if self.use_database:
return await self.db_manager.start_research_task(request, user_id)
else:
task_id = self.create_task("research")
# Store user_id in task for subscription checks
if task_id in self.task_storage:
self.task_storage[task_id]["user_id"] = user_id
# Start the research operation in the background
asyncio.create_task(self._run_research_task(task_id, request))
asyncio.create_task(self._run_research_task(task_id, request, user_id))
return task_id
def start_outline_task(self, request: BlogOutlineRequest) -> str:
@@ -144,7 +152,7 @@ class TaskManager:
asyncio.create_task(self._run_medium_generation_task(task_id, request))
return task_id
async def _run_research_task(self, task_id: str, request: BlogResearchRequest):
async def _run_research_task(self, task_id: str, request: BlogResearchRequest, user_id: str):
"""Background task to run research and update status with progress messages."""
try:
# Update status to running
@@ -157,8 +165,8 @@ class TaskManager:
# Check cache first
await self.update_progress(task_id, "📋 Checking cache for existing research...")
# Run the actual research with progress updates
result = await self.service.research_with_progress(request, task_id)
# Run the actual research with progress updates (pass user_id for subscription checks)
result = await self.service.research_with_progress(request, task_id, user_id)
# Check if research failed gracefully
if not result.success:
@@ -171,6 +179,16 @@ class TaskManager:
self.task_storage[task_id]["status"] = "completed"
self.task_storage[task_id]["result"] = result.dict()
except HTTPException as http_error:
# Handle HTTPException (e.g., 429 subscription limit) - preserve error details for frontend
error_detail = http_error.detail
error_message = error_detail.get('message', str(error_detail)) if isinstance(error_detail, dict) else str(error_detail)
await self.update_progress(task_id, f"{error_message}")
self.task_storage[task_id]["status"] = "failed"
self.task_storage[task_id]["error"] = error_message
# Store HTTP error details for frontend modal
self.task_storage[task_id]["error_status"] = http_error.status_code
self.task_storage[task_id]["error_data"] = error_detail if isinstance(error_detail, dict) else {"error": str(error_detail)}
except Exception as e:
await self.update_progress(task_id, f"❌ Research failed with error: {str(e)}")
# Update status to failed

View File

@@ -1,5 +1,5 @@
from fastapi import APIRouter, HTTPException, Depends, Query, Body
from typing import Dict, Any
from typing import Dict, Any, Optional
import logging
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
@@ -64,6 +64,15 @@ async def activate_strategy_with_monitoring(
if not monitoring_success:
logger.warning(f"Failed to save monitoring data for strategy {strategy_id}")
# Trigger scheduler interval adjustment (scheduler will check more frequently now)
try:
from services.scheduler import get_scheduler
scheduler = get_scheduler()
await scheduler.trigger_interval_adjustment()
logger.info(f"Triggered scheduler interval adjustment after strategy {strategy_id} activation")
except Exception as e:
logger.warning(f"Could not trigger scheduler interval adjustment: {e}")
logger.info(f"Successfully activated strategy {strategy_id} with monitoring")
return {
"success": True,
@@ -396,6 +405,150 @@ async def get_monitoring_tasks(
logger.error(f"Error retrieving monitoring tasks: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/user/{user_id}/monitoring-tasks")
async def get_user_monitoring_tasks(
user_id: int,
db: Session = Depends(get_db),
status: Optional[str] = Query(None, description="Filter by task status"),
limit: int = Query(50, description="Maximum number of tasks to return"),
offset: int = Query(0, description="Number of tasks to skip")
):
"""
Get all monitoring tasks for a specific user with their execution status.
Uses the scheduler's task loader to get tasks filtered by user_id for proper user isolation.
"""
try:
logger.info(f"Getting monitoring tasks for user {user_id}")
# Use scheduler task loader for user-specific tasks
from services.scheduler.utils.task_loader import load_due_monitoring_tasks
# Load all tasks for user (not just due tasks - we want all user tasks)
# Join with strategy to filter by user
tasks_query = db.query(MonitoringTask).join(
EnhancedContentStrategy,
MonitoringTask.strategy_id == EnhancedContentStrategy.id
).filter(
EnhancedContentStrategy.user_id == user_id
)
# Apply status filter if provided
if status:
tasks_query = tasks_query.filter(MonitoringTask.status == status)
# Get tasks with pagination
tasks = tasks_query.order_by(desc(MonitoringTask.created_at)).offset(offset).limit(limit).all()
tasks_data = []
for task in tasks:
# Get latest execution log
latest_log = db.query(TaskExecutionLog).filter(
TaskExecutionLog.task_id == task.id
).order_by(desc(TaskExecutionLog.execution_date)).first()
# Get strategy info
strategy = db.query(EnhancedContentStrategy).filter(
EnhancedContentStrategy.id == task.strategy_id
).first()
task_data = {
"id": task.id,
"strategy_id": task.strategy_id,
"strategy_name": strategy.name if strategy else None,
"title": task.task_title,
"description": task.task_description,
"assignee": task.assignee,
"frequency": task.frequency,
"metric": task.metric,
"measurementMethod": task.measurement_method,
"successCriteria": task.success_criteria,
"alertThreshold": task.alert_threshold,
"status": task.status,
"lastExecuted": latest_log.execution_date.isoformat() if latest_log else None,
"nextExecution": task.next_execution.isoformat() if task.next_execution else None,
"executionCount": db.query(TaskExecutionLog).filter(
TaskExecutionLog.task_id == task.id
).count(),
"created_at": task.created_at.isoformat() if task.created_at else None
}
tasks_data.append(task_data)
# Get total count for pagination
total_count = db.query(MonitoringTask).join(
EnhancedContentStrategy,
MonitoringTask.strategy_id == EnhancedContentStrategy.id
).filter(
EnhancedContentStrategy.user_id == user_id
)
if status:
total_count = total_count.filter(MonitoringTask.status == status)
total_count = total_count.count()
return {
"success": True,
"data": tasks_data,
"pagination": {
"total": total_count,
"limit": limit,
"offset": offset,
"has_more": (offset + len(tasks_data)) < total_count
},
"message": f"Retrieved {len(tasks_data)} monitoring tasks for user {user_id}"
}
except Exception as e:
logger.error(f"Error retrieving user monitoring tasks: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to retrieve monitoring tasks: {str(e)}")
@router.get("/user/{user_id}/execution-logs")
async def get_user_execution_logs(
user_id: int,
db: Session = Depends(get_db),
status: Optional[str] = Query(None, description="Filter by execution status"),
limit: int = Query(50, description="Maximum number of logs to return"),
offset: int = Query(0, description="Number of logs to skip")
):
"""
Get execution logs for a specific user.
Provides user isolation by filtering execution logs by user_id.
"""
try:
logger.info(f"Getting execution logs for user {user_id}")
monitoring_service = MonitoringDataService(db)
logs_data = monitoring_service.get_user_execution_logs(
user_id=user_id,
limit=limit,
offset=offset,
status_filter=status
)
# Get total count for pagination
count_query = db.query(TaskExecutionLog).filter(
TaskExecutionLog.user_id == user_id
)
if status:
count_query = count_query.filter(TaskExecutionLog.status == status)
total_count = count_query.count()
return {
"success": True,
"data": logs_data,
"pagination": {
"total": total_count,
"limit": limit,
"offset": offset,
"has_more": (offset + len(logs_data)) < total_count
},
"message": f"Retrieved {len(logs_data)} execution logs for user {user_id}"
}
except Exception as e:
logger.error(f"Error retrieving execution logs for user {user_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to retrieve execution logs: {str(e)}")
@router.get("/{strategy_id}/data-freshness")
async def get_data_freshness(
strategy_id: int,

View File

@@ -3,13 +3,18 @@ from __future__ import annotations
import base64
import os
from typing import Optional, Dict, Any
from datetime import datetime
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel, Field
from services.llm_providers.main_image_generation import generate_image
from services.llm_providers.main_text_generation import llm_text_gen
from utils.logger_utils import get_service_logger
from middleware.auth_middleware import get_current_user
from services.database import get_db
from services.subscription import UsageTrackingService, PricingService
from models.subscription_models import APIProvider, UsageSummary
router = APIRouter(prefix="/api/images", tags=["images"])
@@ -39,9 +44,23 @@ class ImageGenerateResponse(BaseModel):
@router.post("/generate", response_model=ImageGenerateResponse)
def generate(req: ImageGenerateRequest) -> ImageGenerateResponse:
def generate(
req: ImageGenerateRequest,
current_user: Dict[str, Any] = Depends(get_current_user)
) -> ImageGenerateResponse:
"""Generate image with subscription checking."""
try:
# Extract Clerk user ID (required)
if not current_user:
raise HTTPException(status_code=401, detail="Authentication required")
user_id = str(current_user.get('id', ''))
if not user_id:
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
# Validation is now handled inside generate_image function
last_error: Optional[Exception] = None
result = None
for attempt in range(2): # simple single retry
try:
result = generate_image(
@@ -56,8 +75,79 @@ def generate(req: ImageGenerateRequest) -> ImageGenerateResponse:
"steps": req.steps,
"seed": req.seed,
},
user_id=user_id, # Pass user_id for validation inside generate_image
)
image_b64 = base64.b64encode(result.image_bytes).decode("utf-8")
# TRACK USAGE after successful image generation
if result:
logger.info(f"[images.generate] ✅ Image generation successful, tracking usage for user {user_id}")
try:
db_track = next(get_db())
try:
# Get or create usage summary
pricing = PricingService(db_track)
current_period = pricing.get_current_billing_period(user_id) or datetime.now().strftime("%Y-%m")
logger.debug(f"[images.generate] Looking for usage summary: user_id={user_id}, period={current_period}")
summary = db_track.query(UsageSummary).filter(
UsageSummary.user_id == user_id,
UsageSummary.billing_period == current_period
).first()
if not summary:
logger.info(f"[images.generate] Creating new usage summary for user {user_id}, period {current_period}")
summary = UsageSummary(
user_id=user_id,
billing_period=current_period
)
db_track.add(summary)
db_track.flush() # Ensure summary is persisted before updating
# Get "before" state for unified log
current_calls_before = getattr(summary, "stability_calls", 0) or 0
# Update provider-specific counters (stability for image generation)
# Note: All image generation goes through STABILITY provider enum regardless of actual provider
new_calls = current_calls_before + 1
setattr(summary, "stability_calls", new_calls)
logger.debug(f"[images.generate] Updated stability_calls: {current_calls_before} -> {new_calls}")
# Update totals
old_total_calls = summary.total_calls or 0
summary.total_calls = old_total_calls + 1
logger.debug(f"[images.generate] Updated totals: calls {old_total_calls} -> {summary.total_calls}")
# Get plan details for unified log
limits = pricing.get_user_limits(user_id)
plan_name = limits.get('plan_name', 'unknown') if limits else 'unknown'
tier = limits.get('tier', 'unknown') if limits else 'unknown'
call_limit = limits['limits'].get("stability_calls", 0) if limits else 0
db_track.commit()
logger.info(f"[images.generate] ✅ Successfully tracked usage: user {user_id} -> stability -> {new_calls} calls")
# UNIFIED SUBSCRIPTION LOG - Shows before/after state in one message
print(f"""
[SUBSCRIPTION] Image Generation
├─ User: {user_id}
├─ Plan: {plan_name} ({tier})
├─ Provider: stability
├─ Actual Provider: {result.provider}
├─ Model: {result.model or 'default'}
├─ Calls: {current_calls_before}{new_calls} / {call_limit if call_limit > 0 else ''}
└─ Status: ✅ Allowed & Tracked
""")
except Exception as track_error:
logger.error(f"[images.generate] ❌ Error tracking usage (non-blocking): {track_error}", exc_info=True)
db_track.rollback()
finally:
db_track.close()
except Exception as usage_error:
# Non-blocking: log error but don't fail the request
logger.error(f"[images.generate] ❌ Failed to track usage: {usage_error}", exc_info=True)
return ImageGenerateResponse(
image_base64=image_b64,
width=result.width,
@@ -106,7 +196,10 @@ class ImagePromptSuggestResponse(BaseModel):
@router.post("/suggest-prompts", response_model=ImagePromptSuggestResponse)
def suggest_prompts(req: ImagePromptSuggestRequest) -> ImagePromptSuggestResponse:
def suggest_prompts(
req: ImagePromptSuggestRequest,
current_user: Dict[str, Any] = Depends(get_current_user)
) -> ImagePromptSuggestResponse:
try:
provider = (req.provider or ("gemini" if (os.getenv("GPT_PROVIDER") or "").lower().startswith("gemini") else "huggingface")).lower()
section = req.section or {}
@@ -203,7 +296,15 @@ def suggest_prompts(req: ImagePromptSuggestRequest) -> ImagePromptSuggestRespons
If including on-image text, return it in overlay_text (short: <= 8 words).
"""
raw = llm_text_gen(prompt=prompt, system_prompt=system, json_struct=schema)
# Get user_id for llm_text_gen subscription check (required)
if not current_user:
raise HTTPException(status_code=401, detail="Authentication required")
user_id_for_llm = str(current_user.get('id', ''))
if not user_id_for_llm:
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
raw = llm_text_gen(prompt=prompt, system_prompt=system, json_struct=schema, user_id=user_id_for_llm)
data = raw if isinstance(raw, dict) else {}
suggestions = data.get("suggestions") or []
# basic fallback if provider returns string

View File

@@ -94,6 +94,7 @@ async def get_subscription_plans(
"description": plan.description,
"features": plan.features or [],
"limits": {
"ai_text_generation_calls": getattr(plan, 'ai_text_generation_calls_limit', None) or 0,
"gemini_calls": plan.gemini_calls_limit,
"openai_calls": plan.openai_calls_limit,
"anthropic_calls": plan.anthropic_calls_limit,
@@ -162,6 +163,7 @@ async def get_user_subscription(
},
"status": "free",
"limits": {
"ai_text_generation_calls": getattr(free_plan, 'ai_text_generation_calls_limit', None) or 0,
"gemini_calls": free_plan.gemini_calls_limit,
"openai_calls": free_plan.openai_calls_limit,
"anthropic_calls": free_plan.anthropic_calls_limit,
@@ -200,6 +202,7 @@ async def get_user_subscription(
"is_free": False
},
"limits": {
"ai_text_generation_calls": getattr(subscription.plan, 'ai_text_generation_calls_limit', None) or 0,
"gemini_calls": subscription.plan.gemini_calls_limit,
"openai_calls": subscription.plan.openai_calls_limit,
"anthropic_calls": subscription.plan.anthropic_calls_limit,
@@ -252,6 +255,7 @@ async def get_subscription_status(
"tier": "free",
"can_use_api": True,
"limits": {
"ai_text_generation_calls": getattr(free_plan, 'ai_text_generation_calls_limit', None) or 0,
"gemini_calls": free_plan.gemini_calls_limit,
"openai_calls": free_plan.openai_calls_limit,
"anthropic_calls": free_plan.anthropic_calls_limit,
@@ -309,6 +313,7 @@ async def get_subscription_status(
"tier": subscription.plan.tier.value,
"can_use_api": True,
"limits": {
"ai_text_generation_calls": getattr(subscription.plan, 'ai_text_generation_calls_limit', None) or 0,
"gemini_calls": subscription.plan.gemini_calls_limit,
"openai_calls": subscription.plan.openai_calls_limit,
"anthropic_calls": subscription.plan.anthropic_calls_limit,
@@ -331,9 +336,14 @@ async def get_subscription_status(
async def subscribe_to_plan(
user_id: str,
subscription_data: dict,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user)
) -> Dict[str, Any]:
"""Create or update a user's subscription."""
"""Create or update a user's subscription (renewal)."""
# Verify user can only subscribe/renew their own subscription
if current_user.get('id') != user_id:
raise HTTPException(status_code=403, detail="Access denied")
try:
plan_id = subscription_data.get('plan_id')
@@ -388,12 +398,75 @@ async def subscribe_to_plan(
db.commit()
# Get current usage BEFORE reset for logging
current_period = datetime.utcnow().strftime("%Y-%m")
usage_before = db.query(UsageSummary).filter(
UsageSummary.user_id == user_id,
UsageSummary.billing_period == current_period
).first()
# Log renewal request details
logger.info("=" * 80)
logger.info(f"[SUBSCRIPTION RENEWAL] 🔄 Processing renewal request")
logger.info(f" ├─ User: {user_id}")
logger.info(f" ├─ Plan: {plan.name} (ID: {plan_id}, Tier: {plan.tier.value})")
logger.info(f" ├─ Billing Cycle: {billing_cycle}")
logger.info(f" ├─ Period Start: {now.strftime('%Y-%m-%d %H:%M:%S')}")
logger.info(f" └─ Period End: {subscription.current_period_end.strftime('%Y-%m-%d %H:%M:%S')}")
if usage_before:
logger.info(f" 📊 Current Usage BEFORE Reset (Period: {current_period}):")
logger.info(f" ├─ Gemini: {usage_before.gemini_tokens or 0} tokens / {usage_before.gemini_calls or 0} calls")
logger.info(f" ├─ Mistral/HF: {usage_before.mistral_tokens or 0} tokens / {usage_before.mistral_calls or 0} calls")
logger.info(f" ├─ OpenAI: {usage_before.openai_tokens or 0} tokens / {usage_before.openai_calls or 0} calls")
logger.info(f" ├─ Stability (Images): {usage_before.stability_calls or 0} calls")
logger.info(f" ├─ Total Tokens: {usage_before.total_tokens or 0}")
logger.info(f" ├─ Total Calls: {usage_before.total_calls or 0}")
logger.info(f" └─ Usage Status: {usage_before.usage_status.value}")
else:
logger.info(f" 📊 No usage summary found for period {current_period} (will be created on reset)")
# Clear subscription limits cache to force refresh on next check
try:
from services.subscription import PricingService
# Clear cache for this specific user (class-level cache shared across all instances)
cleared_count = PricingService.clear_user_cache(user_id)
logger.info(f" 🗑️ Cleared {cleared_count} subscription cache entries for user {user_id}")
except Exception as cache_err:
logger.error(f" ❌ Failed to clear cache after subscribe: {cache_err}")
# Reset usage status for current billing period so new plan takes effect immediately
reset_result = None
try:
usage_service = UsageTrackingService(db)
await usage_service.reset_current_billing_period(user_id)
reset_result = await usage_service.reset_current_billing_period(user_id)
# Re-query usage summary from DB after reset to get fresh data
usage_after = db.query(UsageSummary).filter(
UsageSummary.user_id == user_id,
UsageSummary.billing_period == current_period
).first()
if reset_result.get('reset'):
logger.info(f" ✅ Usage counters RESET successfully")
if usage_after:
logger.info(f" 📊 New Usage AFTER Reset:")
logger.info(f" ├─ Gemini: {usage_after.gemini_tokens or 0} tokens / {usage_after.gemini_calls or 0} calls")
logger.info(f" ├─ Mistral/HF: {usage_after.mistral_tokens or 0} tokens / {usage_after.mistral_calls or 0} calls")
logger.info(f" ├─ OpenAI: {usage_after.openai_tokens or 0} tokens / {usage_after.openai_calls or 0} calls")
logger.info(f" ├─ Stability (Images): {usage_after.stability_calls or 0} calls")
logger.info(f" ├─ Total Tokens: {usage_after.total_tokens or 0}")
logger.info(f" ├─ Total Calls: {usage_after.total_calls or 0}")
logger.info(f" └─ Usage Status: {usage_after.usage_status.value}")
else:
logger.warning(f" ⚠️ Usage summary not found after reset - may need to be created on next API call")
else:
logger.warning(f" ⚠️ Reset returned: {reset_result.get('reason', 'unknown')}")
except Exception as reset_err:
logger.error(f"Failed to reset usage after subscribe: {reset_err}")
logger.error(f"Failed to reset usage after subscribe: {reset_err}", exc_info=True)
logger.info(f" ✅ Renewal completed: User {user_id}{plan.name} ({billing_cycle})")
logger.info("=" * 80)
return {
"success": True,
@@ -404,7 +477,20 @@ async def subscribe_to_plan(
"billing_cycle": billing_cycle,
"current_period_start": subscription.current_period_start.isoformat(),
"current_period_end": subscription.current_period_end.isoformat(),
"status": subscription.status.value
"status": subscription.status.value,
"limits": {
"ai_text_generation_calls": getattr(plan, 'ai_text_generation_calls_limit', None) or 0,
"gemini_calls": plan.gemini_calls_limit,
"openai_calls": plan.openai_calls_limit,
"anthropic_calls": plan.anthropic_calls_limit,
"mistral_calls": plan.mistral_calls_limit,
"tavily_calls": plan.tavily_calls_limit,
"serper_calls": plan.serper_calls_limit,
"metaphor_calls": plan.metaphor_calls_limit,
"firecrawl_calls": plan.firecrawl_calls_limit,
"stability_calls": plan.stability_calls_limit,
"monthly_cost": plan.monthly_cost_limit
}
}
}

View File

@@ -477,6 +477,39 @@ async def test_publish_to_wix(request: WixPublishRequest) -> Dict[str, Any]:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/refresh-token")
async def refresh_wix_token(request: Dict[str, Any]) -> Dict[str, Any]:
"""
Refresh Wix access token using refresh token
Args:
request: Dict containing refresh_token
Returns:
New token information with access_token, refresh_token, expires_in
"""
try:
refresh_token = request.get("refresh_token")
if not refresh_token:
raise HTTPException(status_code=400, detail="Missing refresh_token")
# Refresh the token
new_tokens = wix_service.refresh_access_token(refresh_token)
return {
"success": True,
"access_token": new_tokens.get("access_token"),
"refresh_token": new_tokens.get("refresh_token"),
"expires_in": new_tokens.get("expires_in"),
"token_type": new_tokens.get("token_type", "Bearer")
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to refresh Wix token: {e}")
raise HTTPException(status_code=500, detail=f"Failed to refresh token: {str(e)}")
@router.post("/test/publish/real")
async def test_publish_real(payload: Dict[str, Any]) -> Dict[str, Any]:
"""