459 lines
17 KiB
Python
459 lines
17 KiB
Python
"""
|
|
Account Management API Views
|
|
Handles account settings, team management, and usage analytics
|
|
"""
|
|
from rest_framework import viewsets, status
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from django.contrib.auth import get_user_model
|
|
from django.db.models import Q, Count, Sum
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
from decimal import Decimal
|
|
from drf_spectacular.utils import extend_schema, extend_schema_view
|
|
|
|
from igny8_core.auth.models import Account
|
|
from igny8_core.business.billing.models import CreditTransaction
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
@extend_schema_view(
|
|
retrieve=extend_schema(tags=['Account']),
|
|
partial_update=extend_schema(tags=['Account']),
|
|
)
|
|
class AccountSettingsViewSet(viewsets.ViewSet):
|
|
"""Account settings management"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def retrieve(self, request):
|
|
"""Get account settings"""
|
|
account = request.user.account
|
|
|
|
return Response({
|
|
'id': account.id,
|
|
'name': account.name,
|
|
'slug': account.slug,
|
|
'billing_address_line1': account.billing_address_line1 or '',
|
|
'billing_address_line2': account.billing_address_line2 or '',
|
|
'billing_city': account.billing_city or '',
|
|
'billing_state': account.billing_state or '',
|
|
'billing_postal_code': account.billing_postal_code or '',
|
|
'billing_country': account.billing_country or '',
|
|
'tax_id': account.tax_id or '',
|
|
'billing_email': account.billing_email or '',
|
|
'credits': account.credits,
|
|
'created_at': account.created_at.isoformat(),
|
|
'updated_at': account.updated_at.isoformat(),
|
|
})
|
|
|
|
def partial_update(self, request):
|
|
"""Update account settings"""
|
|
account = request.user.account
|
|
|
|
# Update allowed fields
|
|
allowed_fields = [
|
|
'name', 'billing_address_line1', 'billing_address_line2',
|
|
'billing_city', 'billing_state', 'billing_postal_code',
|
|
'billing_country', 'tax_id', 'billing_email'
|
|
]
|
|
|
|
for field in allowed_fields:
|
|
if field in request.data:
|
|
setattr(account, field, request.data[field])
|
|
|
|
account.save()
|
|
|
|
return Response({
|
|
'message': 'Account settings updated successfully',
|
|
'account': {
|
|
'id': account.id,
|
|
'name': account.name,
|
|
'slug': account.slug,
|
|
'billing_address_line1': account.billing_address_line1,
|
|
'billing_address_line2': account.billing_address_line2,
|
|
'billing_city': account.billing_city,
|
|
'billing_state': account.billing_state,
|
|
'billing_postal_code': account.billing_postal_code,
|
|
'billing_country': account.billing_country,
|
|
'tax_id': account.tax_id,
|
|
'billing_email': account.billing_email,
|
|
}
|
|
})
|
|
|
|
|
|
@extend_schema_view(
|
|
list=extend_schema(tags=['Account']),
|
|
create=extend_schema(tags=['Account']),
|
|
destroy=extend_schema(tags=['Account']),
|
|
)
|
|
class TeamManagementViewSet(viewsets.ViewSet):
|
|
"""Team members management"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def list(self, request):
|
|
"""List team members"""
|
|
account = request.user.account
|
|
users = User.objects.filter(account=account)
|
|
|
|
return Response({
|
|
'results': [
|
|
{
|
|
'id': user.id,
|
|
'email': user.email,
|
|
'first_name': user.first_name,
|
|
'last_name': user.last_name,
|
|
'is_active': user.is_active,
|
|
'is_staff': user.is_staff,
|
|
'date_joined': user.date_joined.isoformat(),
|
|
'last_login': user.last_login.isoformat() if user.last_login else None,
|
|
}
|
|
for user in users
|
|
],
|
|
'count': users.count()
|
|
})
|
|
|
|
def create(self, request):
|
|
"""Invite new team member"""
|
|
account = request.user.account
|
|
email = request.data.get('email')
|
|
|
|
if not email:
|
|
return Response(
|
|
{'error': 'Email is required'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Check if user already exists
|
|
if User.objects.filter(email=email).exists():
|
|
return Response(
|
|
{'error': 'User with this email already exists'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Create user (simplified - in production, send invitation email)
|
|
user = User.objects.create_user(
|
|
email=email,
|
|
first_name=request.data.get('first_name', ''),
|
|
last_name=request.data.get('last_name', ''),
|
|
account=account
|
|
)
|
|
|
|
return Response({
|
|
'message': 'Team member invited successfully',
|
|
'user': {
|
|
'id': user.id,
|
|
'email': user.email,
|
|
'first_name': user.first_name,
|
|
'last_name': user.last_name,
|
|
}
|
|
}, status=status.HTTP_201_CREATED)
|
|
|
|
def destroy(self, request, pk=None):
|
|
"""Remove team member"""
|
|
account = request.user.account
|
|
|
|
try:
|
|
user = User.objects.get(id=pk, account=account)
|
|
|
|
# Prevent removing yourself
|
|
if user.id == request.user.id:
|
|
return Response(
|
|
{'error': 'Cannot remove yourself'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
user.is_active = False
|
|
user.save()
|
|
|
|
return Response({
|
|
'message': 'Team member removed successfully'
|
|
})
|
|
except User.DoesNotExist:
|
|
return Response(
|
|
{'error': 'User not found'},
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
|
|
|
|
@extend_schema_view(
|
|
overview=extend_schema(tags=['Account']),
|
|
)
|
|
class UsageAnalyticsViewSet(viewsets.ViewSet):
|
|
"""Usage analytics and statistics"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def overview(self, request):
|
|
"""Get usage analytics overview"""
|
|
account = request.user.account
|
|
|
|
# Get date range (default: last 30 days)
|
|
days = int(request.query_params.get('days', 30))
|
|
start_date = timezone.now() - timedelta(days=days)
|
|
|
|
# Get transactions in period
|
|
transactions = CreditTransaction.objects.filter(
|
|
account=account,
|
|
created_at__gte=start_date
|
|
)
|
|
|
|
# Calculate totals by type
|
|
usage_by_type = transactions.filter(
|
|
amount__lt=0
|
|
).values('transaction_type').annotate(
|
|
total=Sum('amount'),
|
|
count=Count('id')
|
|
)
|
|
|
|
purchases_by_type = transactions.filter(
|
|
amount__gt=0
|
|
).values('transaction_type').annotate(
|
|
total=Sum('amount'),
|
|
count=Count('id')
|
|
)
|
|
|
|
# Daily usage
|
|
daily_usage = []
|
|
for i in range(days):
|
|
date = start_date + timedelta(days=i)
|
|
day_txns = transactions.filter(
|
|
created_at__date=date.date()
|
|
)
|
|
|
|
usage = day_txns.filter(amount__lt=0).aggregate(Sum('amount'))['amount__sum'] or 0
|
|
purchases = day_txns.filter(amount__gt=0).aggregate(Sum('amount'))['amount__sum'] or 0
|
|
|
|
daily_usage.append({
|
|
'date': date.date().isoformat(),
|
|
'usage': abs(usage),
|
|
'purchases': purchases,
|
|
'net': purchases + usage
|
|
})
|
|
|
|
return Response({
|
|
'period_days': days,
|
|
'start_date': start_date.isoformat(),
|
|
'end_date': timezone.now().isoformat(),
|
|
'current_balance': account.credits,
|
|
'usage_by_type': list(usage_by_type),
|
|
'purchases_by_type': list(purchases_by_type),
|
|
'daily_usage': daily_usage,
|
|
'total_usage': abs(transactions.filter(amount__lt=0).aggregate(Sum('amount'))['amount__sum'] or 0),
|
|
'total_purchases': transactions.filter(amount__gt=0).aggregate(Sum('amount'))['amount__sum'] or 0,
|
|
})
|
|
|
|
|
|
@extend_schema_view(
|
|
stats=extend_schema(tags=['Account']),
|
|
)
|
|
class DashboardStatsViewSet(viewsets.ViewSet):
|
|
"""Dashboard statistics - real data for home page widgets"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def stats(self, request):
|
|
"""
|
|
Get dashboard statistics for the home page.
|
|
|
|
Query params:
|
|
- site_id: Filter by site (optional, defaults to all sites)
|
|
- days: Number of days for AI operations (default: 7)
|
|
|
|
Returns:
|
|
- ai_operations: Real credit usage by operation type
|
|
- recent_activity: Recent notifications
|
|
- content_velocity: Content created this week/month
|
|
- images_count: Actual total images count
|
|
- published_count: Actual published content count
|
|
"""
|
|
account = request.user.account
|
|
site_id = request.query_params.get('site_id')
|
|
days = int(request.query_params.get('days', 7))
|
|
|
|
# Import models here to avoid circular imports
|
|
from igny8_core.modules.writer.models import Images, Content
|
|
from igny8_core.modules.planner.models import Keywords, Clusters, ContentIdeas
|
|
from igny8_core.business.notifications.models import Notification
|
|
from igny8_core.business.billing.models import CreditUsageLog
|
|
from igny8_core.auth.models import Site
|
|
|
|
# Build base filter for site
|
|
site_filter = {}
|
|
if site_id:
|
|
try:
|
|
site_filter['site_id'] = int(site_id)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# ========== AI OPERATIONS (from CreditUsageLog) ==========
|
|
start_date = timezone.now() - timedelta(days=days)
|
|
usage_query = CreditUsageLog.objects.filter(
|
|
account=account,
|
|
created_at__gte=start_date
|
|
)
|
|
|
|
# Get operations grouped by type
|
|
operations_data = usage_query.values('operation_type').annotate(
|
|
count=Count('id'),
|
|
credits=Sum('credits_used')
|
|
).order_by('-credits')
|
|
|
|
# Calculate totals
|
|
total_ops = usage_query.count()
|
|
total_credits = usage_query.aggregate(total=Sum('credits_used'))['total'] or 0
|
|
|
|
# Format operations for frontend
|
|
operations = []
|
|
for op in operations_data:
|
|
op_type = op['operation_type'] or 'other'
|
|
operations.append({
|
|
'type': op_type,
|
|
'count': op['count'] or 0,
|
|
'credits': op['credits'] or 0,
|
|
})
|
|
|
|
ai_operations = {
|
|
'period': f'{days}d',
|
|
'operations': operations,
|
|
'totals': {
|
|
'count': total_ops,
|
|
'credits': total_credits,
|
|
'successRate': 98.5, # TODO: calculate from actual success/failure
|
|
'avgCreditsPerOp': round(total_credits / total_ops, 1) if total_ops > 0 else 0,
|
|
}
|
|
}
|
|
|
|
# ========== RECENT ACTIVITY (from Notifications) ==========
|
|
recent_notifications = Notification.objects.filter(
|
|
account=account
|
|
).order_by('-created_at')[:10]
|
|
|
|
recent_activity = []
|
|
for notif in recent_notifications:
|
|
# Map notification type to activity type
|
|
activity_type_map = {
|
|
'ai_clustering_complete': 'clustering',
|
|
'ai_ideas_complete': 'ideas',
|
|
'ai_content_complete': 'content',
|
|
'ai_images_complete': 'images',
|
|
'ai_prompts_complete': 'images',
|
|
'content_published': 'published',
|
|
'wp_sync_success': 'published',
|
|
}
|
|
activity_type = activity_type_map.get(notif.notification_type, 'system')
|
|
|
|
# Map notification type to href
|
|
href_map = {
|
|
'clustering': '/planner/clusters',
|
|
'ideas': '/planner/ideas',
|
|
'content': '/writer/content',
|
|
'images': '/writer/images',
|
|
'published': '/writer/published',
|
|
}
|
|
|
|
recent_activity.append({
|
|
'id': str(notif.id),
|
|
'type': activity_type,
|
|
'title': notif.title,
|
|
'description': notif.message[:100] if notif.message else '',
|
|
'timestamp': notif.created_at.isoformat(),
|
|
'href': href_map.get(activity_type, '/dashboard'),
|
|
})
|
|
|
|
# ========== CONTENT COUNTS ==========
|
|
content_base = Content.objects.filter(account=account)
|
|
if site_filter:
|
|
content_base = content_base.filter(**site_filter)
|
|
|
|
total_content = content_base.count()
|
|
draft_content = content_base.filter(status='draft').count()
|
|
review_content = content_base.filter(status='review').count()
|
|
published_content = content_base.filter(status='published').count()
|
|
|
|
# ========== IMAGES COUNT (actual images, not content with images) ==========
|
|
images_base = Images.objects.filter(account=account)
|
|
if site_filter:
|
|
images_base = images_base.filter(**site_filter)
|
|
|
|
total_images = images_base.count()
|
|
generated_images = images_base.filter(status='generated').count()
|
|
pending_images = images_base.filter(status='pending').count()
|
|
|
|
# ========== CONTENT VELOCITY ==========
|
|
now = timezone.now()
|
|
week_ago = now - timedelta(days=7)
|
|
month_ago = now - timedelta(days=30)
|
|
|
|
# This week's content
|
|
week_content = content_base.filter(created_at__gte=week_ago).count()
|
|
week_images = images_base.filter(created_at__gte=week_ago).count()
|
|
|
|
# This month's content
|
|
month_content = content_base.filter(created_at__gte=month_ago).count()
|
|
month_images = images_base.filter(created_at__gte=month_ago).count()
|
|
|
|
# Estimate words (avg 1500 per article)
|
|
content_velocity = {
|
|
'thisWeek': {
|
|
'articles': week_content,
|
|
'words': week_content * 1500,
|
|
'images': week_images,
|
|
},
|
|
'thisMonth': {
|
|
'articles': month_content,
|
|
'words': month_content * 1500,
|
|
'images': month_images,
|
|
},
|
|
'total': {
|
|
'articles': total_content,
|
|
'words': total_content * 1500,
|
|
'images': total_images,
|
|
},
|
|
'trend': 0, # TODO: calculate actual trend
|
|
}
|
|
|
|
# ========== PIPELINE COUNTS ==========
|
|
keywords_base = Keywords.objects.filter(account=account)
|
|
clusters_base = Clusters.objects.filter(account=account)
|
|
ideas_base = ContentIdeas.objects.filter(account=account)
|
|
|
|
if site_filter:
|
|
keywords_base = keywords_base.filter(**site_filter)
|
|
clusters_base = clusters_base.filter(**site_filter)
|
|
ideas_base = ideas_base.filter(**site_filter)
|
|
|
|
# Get site count
|
|
sites_count = Site.objects.filter(account=account, is_active=True).count()
|
|
|
|
pipeline = {
|
|
'sites': sites_count,
|
|
'keywords': keywords_base.count(),
|
|
'clusters': clusters_base.count(),
|
|
'ideas': ideas_base.count(),
|
|
'tasks': ideas_base.filter(status='queued').count() + ideas_base.filter(status='completed').count(),
|
|
'drafts': draft_content + review_content,
|
|
'published': published_content,
|
|
}
|
|
|
|
return Response({
|
|
'ai_operations': ai_operations,
|
|
'recent_activity': recent_activity,
|
|
'content_velocity': content_velocity,
|
|
'pipeline': pipeline,
|
|
'counts': {
|
|
'content': {
|
|
'total': total_content,
|
|
'draft': draft_content,
|
|
'review': review_content,
|
|
'published': published_content,
|
|
},
|
|
'images': {
|
|
'total': total_images,
|
|
'generated': generated_images,
|
|
'pending': pending_images,
|
|
},
|
|
}
|
|
})
|