billing accoutn with all the mess here
This commit is contained in:
31
backend/igny8_core/api/account_urls.py
Normal file
31
backend/igny8_core/api/account_urls.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Account API URLs
|
||||
"""
|
||||
from django.urls import path
|
||||
from igny8_core.api.account_views import (
|
||||
AccountSettingsViewSet,
|
||||
TeamManagementViewSet,
|
||||
UsageAnalyticsViewSet
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# Account Settings
|
||||
path('settings/', AccountSettingsViewSet.as_view({
|
||||
'get': 'retrieve',
|
||||
'patch': 'partial_update'
|
||||
}), name='account-settings'),
|
||||
|
||||
# Team Management
|
||||
path('team/', TeamManagementViewSet.as_view({
|
||||
'get': 'list',
|
||||
'post': 'create'
|
||||
}), name='team-list'),
|
||||
path('team/<int:pk>/', TeamManagementViewSet.as_view({
|
||||
'delete': 'destroy'
|
||||
}), name='team-detail'),
|
||||
|
||||
# Usage Analytics
|
||||
path('usage/analytics/', UsageAnalyticsViewSet.as_view({
|
||||
'get': 'overview'
|
||||
}), name='usage-analytics'),
|
||||
]
|
||||
231
backend/igny8_core/api/account_views.py
Normal file
231
backend/igny8_core/api/account_views.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
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 igny8_core.auth.models import Account
|
||||
from igny8_core.business.billing.models import CreditTransaction
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
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.credit_balance,
|
||||
'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,
|
||||
})
|
||||
26
backend/igny8_core/api/urls.py
Normal file
26
backend/igny8_core/api/urls.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
URL patterns for account management API
|
||||
"""
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .account_views import (
|
||||
AccountSettingsViewSet,
|
||||
TeamManagementViewSet,
|
||||
UsageAnalyticsViewSet
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
urlpatterns = [
|
||||
# Account settings (non-router endpoints for simplified access)
|
||||
path('settings/', AccountSettingsViewSet.as_view({'get': 'retrieve', 'patch': 'partial_update'}), name='account-settings'),
|
||||
|
||||
# Team management
|
||||
path('team/', TeamManagementViewSet.as_view({'get': 'list', 'post': 'create'}), name='team-list'),
|
||||
path('team/<int:pk>/', TeamManagementViewSet.as_view({'delete': 'destroy'}), name='team-detail'),
|
||||
|
||||
# Usage analytics
|
||||
path('usage/analytics/', UsageAnalyticsViewSet.as_view({'get': 'overview'}), name='usage-analytics'),
|
||||
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
@@ -19,5 +19,7 @@ router.register(r'transactions', CreditTransactionViewSet, basename='transaction
|
||||
router.register(r'admin', AdminBillingViewSet, basename='admin-billing')
|
||||
|
||||
urlpatterns = [
|
||||
# Payment methods alias for easier frontend access
|
||||
path('payment-methods/', PaymentViewSet.as_view({'get': 'available_methods'}), name='payment-methods'),
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
||||
@@ -385,26 +385,94 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
||||
|
||||
from django.db.models import Sum, Count
|
||||
from ...auth.models import Account
|
||||
from datetime import datetime, timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
# Date ranges
|
||||
now = timezone.now()
|
||||
this_month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
last_30_days = now - timedelta(days=30)
|
||||
|
||||
# Account stats
|
||||
total_accounts = Account.objects.count()
|
||||
active_accounts = Account.objects.filter(is_active=True).count()
|
||||
new_accounts_this_month = Account.objects.filter(
|
||||
created_at__gte=this_month_start
|
||||
).count()
|
||||
|
||||
# Subscription stats
|
||||
active_subscriptions = Account.objects.filter(
|
||||
subscriptions__status='active'
|
||||
).distinct().count()
|
||||
|
||||
# Revenue stats
|
||||
total_revenue = Payment.objects.filter(
|
||||
status='completed',
|
||||
amount__gt=0
|
||||
).aggregate(total=Sum('amount'))['total'] or 0
|
||||
|
||||
revenue_this_month = Payment.objects.filter(
|
||||
status='completed',
|
||||
processed_at__gte=this_month_start,
|
||||
amount__gt=0
|
||||
).aggregate(total=Sum('amount'))['total'] or 0
|
||||
|
||||
# Credit stats
|
||||
credits_issued = CreditTransaction.objects.filter(
|
||||
transaction_type='purchase',
|
||||
created_at__gte=last_30_days
|
||||
).aggregate(total=Sum('amount'))['total'] or 0
|
||||
|
||||
credits_used = abs(CreditTransaction.objects.filter(
|
||||
transaction_type__in=['generate_content', 'keyword_research', 'ai_task'],
|
||||
created_at__gte=last_30_days,
|
||||
amount__lt=0
|
||||
).aggregate(total=Sum('amount'))['total'] or 0)
|
||||
|
||||
# Payment/Invoice stats
|
||||
pending_approvals = Payment.objects.filter(
|
||||
status='pending_approval'
|
||||
).count()
|
||||
|
||||
invoices_pending = Invoice.objects.filter(status='pending').count()
|
||||
invoices_overdue = Invoice.objects.filter(
|
||||
status='pending',
|
||||
due_date__lt=now
|
||||
).count()
|
||||
|
||||
# Recent activity
|
||||
recent_payments = Payment.objects.filter(
|
||||
status='completed'
|
||||
).order_by('-processed_at')[:5]
|
||||
|
||||
recent_activity = [
|
||||
{
|
||||
'id': pay.id,
|
||||
'type': 'payment',
|
||||
'account_name': pay.account.name,
|
||||
'amount': str(pay.amount),
|
||||
'currency': pay.currency,
|
||||
'timestamp': pay.processed_at.isoformat(),
|
||||
'description': f'Payment received via {pay.payment_method}'
|
||||
}
|
||||
for pay in recent_payments
|
||||
]
|
||||
|
||||
return Response({
|
||||
'total_accounts': total_accounts,
|
||||
'active_accounts': active_accounts,
|
||||
'new_accounts_this_month': new_accounts_this_month,
|
||||
'active_subscriptions': active_subscriptions,
|
||||
'total_revenue': str(total_revenue),
|
||||
'revenue_this_month': str(revenue_this_month),
|
||||
'credits_issued_30d': credits_issued,
|
||||
'credits_used_30d': credits_used,
|
||||
'pending_approvals': pending_approvals,
|
||||
'invoices_pending': Invoice.objects.filter(status='pending').count(),
|
||||
'invoices_paid': Invoice.objects.filter(status='paid').count()
|
||||
'invoices_pending': invoices_pending,
|
||||
'invoices_overdue': invoices_overdue,
|
||||
'recent_activity': recent_activity,
|
||||
'system_health': {
|
||||
'status': 'operational',
|
||||
'last_check': now.isoformat()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -37,11 +37,12 @@ urlpatterns = [
|
||||
path('admin/igny8_core_auth/seedkeyword/csv-import/', seedkeyword_csv_import, name='admin_seedkeyword_csv_import'),
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/v1/auth/', include('igny8_core.auth.urls')), # Auth endpoints
|
||||
path('api/v1/account/', include('igny8_core.api.urls')), # Account management (settings, team, usage)
|
||||
path('api/v1/planner/', include('igny8_core.modules.planner.urls')),
|
||||
path('api/v1/writer/', include('igny8_core.modules.writer.urls')),
|
||||
path('api/v1/system/', include('igny8_core.modules.system.urls')),
|
||||
path('api/v1/billing/', include('igny8_core.modules.billing.urls')), # Billing endpoints (legacy)
|
||||
path('api/v1/billing/v2/', include('igny8_core.business.billing.urls')), # New billing endpoints (invoices, payments)
|
||||
path('api/v1/billing/', include('igny8_core.modules.billing.urls')), # Basic billing endpoints (credits, usage)
|
||||
path('api/v1/billing/', include('igny8_core.business.billing.urls')), # Advanced billing (invoices, payments, packages)
|
||||
path('api/v1/admin/', include('igny8_core.modules.billing.admin_urls')), # Admin billing
|
||||
path('api/v1/automation/', include('igny8_core.business.automation.urls')), # Automation endpoints
|
||||
path('api/v1/linker/', include('igny8_core.modules.linker.urls')), # Linker endpoints
|
||||
|
||||
Reference in New Issue
Block a user