story writer backend migration complete, Blog writer SEO and story writer backend migration complete, Blog writer SEO and story writer frontend migration complete

This commit is contained in:
ajaysi
2025-11-13 16:14:26 +05:30
parent 7191c7e7f0
commit 3b9356e2c8
124 changed files with 20055 additions and 1208 deletions

View File

@@ -5,6 +5,7 @@ Provides endpoints for subscription management and usage monitoring.
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import desc, func
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
from loguru import logger
@@ -12,12 +13,14 @@ from functools import lru_cache
from services.database import get_db
from services.subscription import UsageTrackingService, PricingService
from services.subscription.log_wrapping_service import LogWrappingService
from services.subscription.schema_utils import ensure_subscription_plan_columns
import sqlite3
from middleware.auth_middleware import get_current_user
from models.subscription_models import (
APIProvider, SubscriptionPlan, UserSubscription, UsageSummary,
APIProviderPricing, UsageAlert, SubscriptionTier, BillingCycle, UsageStatus
APIProviderPricing, UsageAlert, SubscriptionTier, BillingCycle, UsageStatus,
APIUsageLog, SubscriptionRenewalHistory
)
router = APIRouter(prefix="/api/subscription", tags=["subscription"])
@@ -525,8 +528,67 @@ async def subscribe_to_plan(
).first()
now = datetime.utcnow()
# Track renewal history - capture BEFORE updating subscription
previous_period_start = None
previous_period_end = None
previous_plan_name = None
previous_plan_tier = None
renewal_type = "new"
renewal_count = 0
# Get usage snapshot BEFORE renewal (capture current state)
usage_before_snapshot = None
current_period = datetime.utcnow().strftime("%Y-%m")
usage_before = db.query(UsageSummary).filter(
UsageSummary.user_id == user_id,
UsageSummary.billing_period == current_period
).first()
if usage_before:
usage_before_snapshot = {
"total_calls": usage_before.total_calls or 0,
"total_tokens": usage_before.total_tokens or 0,
"total_cost": float(usage_before.total_cost) if usage_before.total_cost else 0.0,
"gemini_calls": usage_before.gemini_calls or 0,
"mistral_calls": usage_before.mistral_calls or 0,
"usage_status": usage_before.usage_status.value if hasattr(usage_before.usage_status, 'value') else str(usage_before.usage_status)
}
if existing_subscription:
# This is a renewal/update - capture previous subscription state BEFORE updating
previous_period_start = existing_subscription.current_period_start
previous_period_end = existing_subscription.current_period_end
previous_plan = existing_subscription.plan
previous_plan_name = previous_plan.name if previous_plan else None
previous_plan_tier = previous_plan.tier.value if previous_plan else None
# Determine renewal type
if previous_plan and previous_plan.id == plan_id:
# Same plan - this is a renewal
renewal_type = "renewal"
elif previous_plan:
# Different plan - check if upgrade or downgrade
tier_order = {"free": 0, "basic": 1, "pro": 2, "enterprise": 3}
previous_tier_order = tier_order.get(previous_plan_tier or "free", 0)
new_tier_order = tier_order.get(plan.tier.value, 0)
if new_tier_order > previous_tier_order:
renewal_type = "upgrade"
elif new_tier_order < previous_tier_order:
renewal_type = "downgrade"
else:
renewal_type = "renewal" # Same tier, different plan name
# Get renewal count (how many times this user has renewed)
last_renewal = db.query(SubscriptionRenewalHistory).filter(
SubscriptionRenewalHistory.user_id == user_id
).order_by(SubscriptionRenewalHistory.created_at.desc()).first()
if last_renewal:
renewal_count = last_renewal.renewal_count + 1
else:
renewal_count = 1 # First renewal
# Update existing subscription
existing_subscription.plan_id = plan_id
existing_subscription.billing_cycle = BillingCycle(billing_cycle)
@@ -552,7 +614,30 @@ async def subscribe_to_plan(
auto_renew=True
)
db.add(subscription)
db.commit()
# Create renewal history record AFTER subscription update (so we have the new period_end)
renewal_history = SubscriptionRenewalHistory(
user_id=user_id,
plan_id=plan_id,
plan_name=plan.name,
plan_tier=plan.tier.value,
previous_period_start=previous_period_start,
previous_period_end=previous_period_end,
new_period_start=now,
new_period_end=subscription.current_period_end,
billing_cycle=BillingCycle(billing_cycle),
renewal_type=renewal_type,
renewal_count=renewal_count,
previous_plan_name=previous_plan_name,
previous_plan_tier=previous_plan_tier,
usage_before_renewal=usage_before_snapshot, # Usage snapshot captured BEFORE renewal
payment_amount=plan.price_yearly if billing_cycle == 'yearly' else plan.price_monthly,
payment_status="paid", # Assume paid for now (can be updated if payment processing is added)
payment_date=now
)
db.add(renewal_history)
db.commit()
# Get current usage BEFORE reset for logging
@@ -883,4 +968,222 @@ async def get_dashboard_data(
except Exception as e:
logger.error(f"Error getting dashboard data: {e}")
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e))
@router.get("/renewal-history/{user_id}")
async def get_renewal_history(
user_id: str,
limit: int = Query(50, ge=1, le=100, description="Number of records to return"),
offset: int = Query(0, ge=0, description="Pagination offset"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db)
) -> Dict[str, Any]:
"""
Get subscription renewal history for a user.
Returns:
- List of renewal history records
- Total count for pagination
"""
try:
# Verify user can only access their own data
if current_user.get('id') != user_id:
raise HTTPException(status_code=403, detail="Access denied")
# Get total count
total_count = db.query(SubscriptionRenewalHistory).filter(
SubscriptionRenewalHistory.user_id == user_id
).count()
# Get paginated results, ordered by created_at descending (most recent first)
renewals = db.query(SubscriptionRenewalHistory).filter(
SubscriptionRenewalHistory.user_id == user_id
).order_by(SubscriptionRenewalHistory.created_at.desc()).offset(offset).limit(limit).all()
# Format renewal history for response
renewal_history = []
for renewal in renewals:
renewal_history.append({
'id': renewal.id,
'plan_name': renewal.plan_name,
'plan_tier': renewal.plan_tier,
'previous_period_start': renewal.previous_period_start.isoformat() if renewal.previous_period_start else None,
'previous_period_end': renewal.previous_period_end.isoformat() if renewal.previous_period_end else None,
'new_period_start': renewal.new_period_start.isoformat() if renewal.new_period_start else None,
'new_period_end': renewal.new_period_end.isoformat() if renewal.new_period_end else None,
'billing_cycle': renewal.billing_cycle.value if renewal.billing_cycle else None,
'renewal_type': renewal.renewal_type,
'renewal_count': renewal.renewal_count,
'previous_plan_name': renewal.previous_plan_name,
'previous_plan_tier': renewal.previous_plan_tier,
'usage_before_renewal': renewal.usage_before_renewal,
'payment_amount': float(renewal.payment_amount) if renewal.payment_amount else 0.0,
'payment_status': renewal.payment_status,
'payment_date': renewal.payment_date.isoformat() if renewal.payment_date else None,
'created_at': renewal.created_at.isoformat() if renewal.created_at else None
})
return {
"success": True,
"data": {
"renewals": renewal_history,
"total_count": total_count,
"limit": limit,
"offset": offset,
"has_more": (offset + limit) < total_count
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting renewal history: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.get("/usage-logs")
async def get_usage_logs(
limit: int = Query(50, ge=1, le=5000, description="Number of logs to return"),
offset: int = Query(0, ge=0, description="Pagination offset"),
provider: Optional[str] = Query(None, description="Filter by provider"),
status_code: Optional[int] = Query(None, description="Filter by HTTP status code"),
billing_period: Optional[str] = Query(None, description="Filter by billing period (YYYY-MM)"),
current_user: Dict[str, Any] = Depends(get_current_user),
db: Session = Depends(get_db)
) -> Dict[str, Any]:
"""
Get API usage logs for the current user.
Query Params:
- limit: Number of logs to return (1-500, default: 50)
- offset: Pagination offset (default: 0)
- provider: Filter by provider (e.g., "gemini", "openai", "huggingface")
- status_code: Filter by HTTP status code (e.g., 200 for success, 400+ for errors)
- billing_period: Filter by billing period (YYYY-MM format)
Returns:
- List of usage logs with API call details
- Total count for pagination
"""
try:
# Get user_id from current_user
user_id = str(current_user.get('id', '')) if current_user else None
if not user_id:
raise HTTPException(status_code=401, detail="User not authenticated")
# Build query
query = db.query(APIUsageLog).filter(
APIUsageLog.user_id == user_id
)
# Apply filters
if provider:
provider_lower = provider.lower()
# Handle special case: huggingface maps to MISTRAL enum in database
if provider_lower == "huggingface":
provider_enum = APIProvider.MISTRAL
else:
try:
provider_enum = APIProvider(provider_lower)
except ValueError:
# Invalid provider, return empty results
return {
"logs": [],
"total_count": 0,
"limit": limit,
"offset": offset,
"has_more": False
}
query = query.filter(APIUsageLog.provider == provider_enum)
if status_code is not None:
query = query.filter(APIUsageLog.status_code == status_code)
if billing_period:
query = query.filter(APIUsageLog.billing_period == billing_period)
# Check and wrap logs if necessary (before getting count)
wrapping_service = LogWrappingService(db)
wrap_result = wrapping_service.check_and_wrap_logs(user_id)
if wrap_result.get('wrapped'):
logger.info(f"[UsageLogs] Log wrapping completed for user {user_id}: {wrap_result.get('message')}")
# Rebuild query after wrapping (in case filters changed)
query = db.query(APIUsageLog).filter(
APIUsageLog.user_id == user_id
)
# Reapply filters
if provider:
provider_lower = provider.lower()
if provider_lower == "huggingface":
provider_enum = APIProvider.MISTRAL
else:
try:
provider_enum = APIProvider(provider_lower)
except ValueError:
return {
"logs": [],
"total_count": 0,
"limit": limit,
"offset": offset,
"has_more": False
}
query = query.filter(APIUsageLog.provider == provider_enum)
if status_code is not None:
query = query.filter(APIUsageLog.status_code == status_code)
if billing_period:
query = query.filter(APIUsageLog.billing_period == billing_period)
# Get total count
total_count = query.count()
# Get paginated results, ordered by timestamp descending (most recent first)
logs = query.order_by(desc(APIUsageLog.timestamp)).offset(offset).limit(limit).all()
# Format logs for response
formatted_logs = []
for log in logs:
# Determine status based on status_code
status = 'success' if 200 <= log.status_code < 300 else 'failed'
# Handle provider display name - ALL MISTRAL enum logs are actually HuggingFace
# (HuggingFace always maps to MISTRAL enum in the database)
provider_display = log.provider.value if log.provider else None
if provider_display == "mistral":
# All MISTRAL provider logs are HuggingFace calls
provider_display = "huggingface"
formatted_logs.append({
'id': log.id,
'timestamp': log.timestamp.isoformat() if log.timestamp else None,
'provider': provider_display,
'model_used': log.model_used,
'endpoint': log.endpoint,
'method': log.method,
'tokens_input': log.tokens_input or 0,
'tokens_output': log.tokens_output or 0,
'tokens_total': log.tokens_total or 0,
'cost_input': float(log.cost_input) if log.cost_input else 0.0,
'cost_output': float(log.cost_output) if log.cost_output else 0.0,
'cost_total': float(log.cost_total) if log.cost_total else 0.0,
'response_time': float(log.response_time) if log.response_time else 0.0,
'status_code': log.status_code,
'status': status,
'error_message': log.error_message,
'billing_period': log.billing_period,
'retry_count': log.retry_count or 0,
'is_aggregated': log.endpoint == "[AGGREGATED]" # Flag to indicate aggregated log
})
return {
"logs": formatted_logs,
"total_count": total_count,
"limit": limit,
"offset": offset,
"has_more": (offset + limit) < total_count
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting usage logs: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to get usage logs: {str(e)}")