fixes
This commit is contained in:
14
backend/igny8_core/modules/billing/admin_urls.py
Normal file
14
backend/igny8_core/modules/billing/admin_urls.py
Normal 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
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user