This commit is contained in:
IGNY8 VPS (Salman)
2025-12-04 17:58:41 +00:00
parent 40dfe20ead
commit 1521f3ff8c
13 changed files with 506 additions and 120 deletions

View File

@@ -0,0 +1,14 @@
from django.urls import path
from rest_framework.routers import DefaultRouter
from .views import AdminBillingViewSet
router = DefaultRouter()
urlpatterns = [
path('billing/stats/', AdminBillingViewSet.as_view({'get': 'stats'}), name='admin-billing-stats'),
path('users/', AdminBillingViewSet.as_view({'get': 'list_users'}), name='admin-users-list'),
path('users/<int:user_id>/adjust-credits/', AdminBillingViewSet.as_view({'post': 'adjust_credits'}), name='admin-adjust-credits'),
path('credit-costs/', AdminBillingViewSet.as_view({'get': 'list_credit_costs', 'post': 'update_credit_costs'}), name='admin-credit-costs'),
]
urlpatterns += router.urls

View File

@@ -3,7 +3,13 @@ URL patterns for billing module
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import CreditBalanceViewSet, CreditUsageViewSet, CreditTransactionViewSet
from .views import (
CreditBalanceViewSet,
CreditUsageViewSet,
CreditTransactionViewSet,
BillingOverviewViewSet,
AdminBillingViewSet
)
router = DefaultRouter()
router.register(r'credits/balance', CreditBalanceViewSet, basename='credit-balance')
@@ -12,5 +18,13 @@ router.register(r'credits/transactions', CreditTransactionViewSet, basename='cre
urlpatterns = [
path('', include(router.urls)),
# User-facing billing overview
path('account_balance/', BillingOverviewViewSet.as_view({'get': 'account_balance'}), name='account-balance'),
path('transactions/', CreditTransactionViewSet.as_view({'get': 'list'}), name='transactions'),
path('usage/', CreditUsageViewSet.as_view({'get': 'list'}), name='usage'),
# Admin billing endpoints
path('admin/billing/stats/', AdminBillingViewSet.as_view({'get': 'stats'}), name='admin-billing-stats'),
path('admin/users/', AdminBillingViewSet.as_view({'get': 'list_users'}), name='admin-users-list'),
path('admin/credit-costs/', AdminBillingViewSet.as_view({'get': 'credit_costs'}), name='admin-credit-costs'),
]

View File

@@ -404,3 +404,170 @@ class CreditTransactionViewSet(AccountModelViewSet):
return queryset.order_by('-created_at')
class BillingOverviewViewSet(viewsets.ViewSet):
"""User-facing billing overview API"""
permission_classes = [IsAuthenticatedAndActive]
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
def account_balance(self, request):
"""Get account balance with subscription info"""
account = getattr(request, 'account', None) or request.user.account
# Get subscription plan
subscription_plan = 'Free'
monthly_credits_included = 0
if account.plan:
subscription_plan = account.plan.name
monthly_credits_included = account.plan.get_effective_credits_per_month()
# Calculate bonus credits (credits beyond monthly allowance)
bonus_credits = max(0, account.credits - monthly_credits_included)
data = {
'credits': account.credits or 0,
'subscription_plan': subscription_plan,
'monthly_credits_included': monthly_credits_included,
'bonus_credits': bonus_credits,
}
return Response(data)
class AdminBillingViewSet(viewsets.ViewSet):
"""Admin-only billing management API"""
permission_classes = [IsAuthenticatedAndActive, permissions.IsAdminUser]
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
def stats(self, request):
"""Get system-wide billing statistics"""
from igny8_core.auth.models import Account
total_users = Account.objects.filter(status='active').count()
active_users = Account.objects.filter(status='active').exclude(users__last_login__isnull=True).count()
total_credits_issued = Account.objects.aggregate(
total=Sum('credits')
)['total'] or 0
total_credits_used = CreditUsageLog.objects.aggregate(
total=Sum('credits_used')
)['total'] or 0
return Response({
'total_users': total_users,
'active_users': active_users,
'total_credits_issued': total_credits_issued,
'total_credits_used': total_credits_used,
})
def list_users(self, request):
"""List all users with credit information"""
from igny8_core.auth.models import Account
from django.db.models import Q
# Get search query from request
search = request.query_params.get('search', '')
queryset = Account.objects.filter(status='active').prefetch_related('users')
# Apply search filter
if search:
queryset = queryset.filter(
Q(user__username__icontains=search) |
Q(user__email__icontains=search)
)
accounts = queryset[:100]
data = []
for acc in accounts:
user = acc.users.first() if acc.users.exists() else None
data.append({
'id': acc.id,
'username': user.username if user else 'N/A',
'email': user.email if user else 'N/A',
'credits': acc.credits or 0,
'subscription_plan': acc.plan.name if acc.plan else 'Free',
'is_active': acc.status == 'active',
'date_joined': acc.created_at
})
return Response({'results': data})
def adjust_credits(self, request, user_id):
"""Adjust credits for a specific user"""
from igny8_core.auth.models import Account
try:
account = Account.objects.get(id=user_id)
except Account.DoesNotExist:
return Response({'error': 'User not found'}, status=404)
amount = request.data.get('amount', 0)
reason = request.data.get('reason', 'Admin adjustment')
try:
amount = int(amount)
except (ValueError, TypeError):
return Response({'error': 'Invalid amount'}, status=400)
# Adjust credits
old_balance = account.credits
account.credits = (account.credits or 0) + amount
account.save()
# Log the adjustment
CreditUsageLog.objects.create(
account=account,
operation_type='admin_adjustment',
credits_used=-amount, # Negative for additions
credits_balance_after=account.credits,
details={'reason': reason, 'old_balance': old_balance, 'adjusted_by': request.user.id}
)
return Response({
'success': True,
'new_balance': account.credits,
'old_balance': old_balance,
'adjustment': amount
})
def list_credit_costs(self, request):
"""List credit cost configurations"""
from igny8_core.business.billing.models import CreditCostConfig
configs = CreditCostConfig.objects.filter(is_active=True)
data = [{
'id': c.id,
'operation_type': c.operation_type,
'display_name': c.display_name,
'credits_cost': c.credits_cost,
'unit': c.unit,
'is_active': c.is_active,
'created_at': c.created_at
} for c in configs]
return Response({'results': data})
def update_credit_costs(self, request):
"""Update credit cost configurations"""
from igny8_core.business.billing.models import CreditCostConfig
updates = request.data.get('updates', [])
for update in updates:
config_id = update.get('id')
new_cost = update.get('cost')
if config_id and new_cost is not None:
try:
config = CreditCostConfig.objects.get(id=config_id)
config.cost = new_cost
config.save()
except CreditCostConfig.DoesNotExist:
continue
return Response({'success': True})