This commit is contained in:
IGNY8 VPS (Salman)
2025-12-08 07:11:06 +00:00
parent 7483de6aba
commit d144f5d19a
13 changed files with 2209 additions and 842 deletions

View File

@@ -1,821 +1,168 @@
"""
Billing API Views
Comprehensive billing endpoints for invoices, payments, credit packages
Billing Views - Payment confirmation and management
"""
from rest_framework import viewsets, status, serializers
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.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.db import models
from drf_spectacular.utils import extend_schema, extend_schema_view
from django.db import transaction
from django.utils import timezone
from datetime import timedelta
from igny8_core.api.response import success_response, error_response
from igny8_core.api.permissions import IsAdminOrOwner
from igny8_core.auth.models import Account, Subscription
from igny8_core.business.billing.services.credit_service import CreditService
from igny8_core.business.billing.models import CreditTransaction
import logging
from .models import (
Invoice,
Payment,
CreditPackage,
PaymentMethodConfig,
CreditTransaction,
AccountPaymentMethod,
)
from .services.invoice_service import InvoiceService
from .services.payment_service import PaymentService
logger = logging.getLogger(__name__)
class InvoiceViewSet(viewsets.ViewSet):
"""Invoice management endpoints"""
permission_classes = [IsAuthenticated]
def list(self, request):
"""List invoices for current account"""
account = request.user.account
status_filter = request.query_params.get('status')
invoices = InvoiceService.get_account_invoices(
account=account,
status=status_filter
)
return Response({
'results': [
{
'id': inv.id,
'invoice_number': inv.invoice_number,
'status': inv.status,
'total_amount': str(inv.total_amount),
'subtotal': str(inv.subtotal),
'tax_amount': str(inv.tax_amount),
'currency': inv.currency,
'created_at': inv.created_at.isoformat(),
'paid_at': inv.paid_at.isoformat() if inv.paid_at else None,
'due_date': inv.due_date.isoformat() if inv.due_date else None,
'line_items': inv.line_items,
'billing_period_start': inv.billing_period_start.isoformat() if inv.billing_period_start else None,
'billing_period_end': inv.billing_period_end.isoformat() if inv.billing_period_end else None
}
for inv in invoices
],
'count': len(invoices)
})
def retrieve(self, request, pk=None):
"""Get invoice details"""
account = request.user.account
invoice = get_object_or_404(Invoice, id=pk, account=account)
return Response({
'id': invoice.id,
'invoice_number': invoice.invoice_number,
'status': invoice.status,
'total_amount': str(invoice.total_amount),
'subtotal': str(invoice.subtotal),
'tax_amount': str(invoice.tax_amount),
'currency': invoice.currency,
'created_at': invoice.created_at.isoformat(),
'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None,
'due_date': invoice.due_date.isoformat() if invoice.due_date else None,
'line_items': invoice.line_items,
'billing_email': invoice.billing_email,
'notes': invoice.notes,
'stripe_invoice_id': invoice.stripe_invoice_id,
'billing_period_start': invoice.billing_period_start.isoformat() if invoice.billing_period_start else None,
'billing_period_end': invoice.billing_period_end.isoformat() if invoice.billing_period_end else None
})
@action(detail=True, methods=['get'])
def download_pdf(self, request, pk=None):
"""Download invoice as PDF"""
account = request.user.account
invoice = get_object_or_404(Invoice, id=pk, account=account)
pdf_data = InvoiceService.generate_pdf(invoice)
response = HttpResponse(pdf_data, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="invoice-{invoice.invoice_number}.pdf"'
return response
class PaymentViewSet(viewsets.ViewSet):
"""Payment processing endpoints"""
permission_classes = [IsAuthenticated]
def list(self, request):
"""List payments for current account"""
account = request.user.account
status_filter = request.query_params.get('status')
payments = PaymentService.get_account_payments(
account=account,
status=status_filter
)
return Response({
'results': [
{
'id': pay.id,
'amount': str(pay.amount),
'currency': pay.currency,
'payment_method': pay.payment_method,
'status': pay.status,
'created_at': pay.created_at.isoformat(),
'processed_at': pay.processed_at.isoformat() if pay.processed_at else None,
'invoice_id': pay.invoice_id,
'invoice_number': pay.invoice.invoice_number if pay.invoice else None,
'transaction_reference': pay.transaction_reference,
'failure_reason': pay.failure_reason
}
for pay in payments
],
'count': len(payments)
})
@action(detail=False, methods=['get'])
def available_methods(self, request):
"""Get available payment methods for current account"""
account = request.user.account
methods = PaymentService.get_available_payment_methods(account)
method_list = methods.pop('methods', [])
return Response({
'results': method_list,
'count': len(method_list),
**methods
})
@action(detail=False, methods=['post'], url_path='manual')
def create_manual_payment(self, request):
"""Submit manual payment for approval"""
account = request.user.account
invoice_id = request.data.get('invoice_id')
payment_method = request.data.get('payment_method') # 'bank_transfer' or 'local_wallet'
transaction_reference = request.data.get('transaction_reference') or request.data.get('reference')
notes = request.data.get('notes')
if not all([invoice_id, payment_method, transaction_reference]):
return Response(
{'error': 'Missing required fields'},
status=status.HTTP_400_BAD_REQUEST
)
invoice = get_object_or_404(Invoice, id=invoice_id, account=account)
if invoice.status == 'paid':
return Response(
{'error': 'Invoice already paid'},
status=status.HTTP_400_BAD_REQUEST
)
payment = PaymentService.create_manual_payment(
invoice=invoice,
payment_method=payment_method,
transaction_reference=transaction_reference,
admin_notes=notes
)
return Response({
'id': payment.id,
'status': payment.status,
'message': 'Payment submitted for approval. You will be notified once it is reviewed.'
}, status=status.HTTP_201_CREATED)
class AccountPaymentMethodSerializer(serializers.ModelSerializer):
class Meta:
model = AccountPaymentMethod
fields = [
'id',
'type',
'display_name',
'is_default',
'is_enabled',
'is_verified',
'country_code',
'instructions',
'metadata',
'created_at',
'updated_at',
]
read_only_fields = ['id', 'is_verified', 'created_at', 'updated_at']
class AccountPaymentMethodViewSet(viewsets.ModelViewSet):
class BillingViewSet(viewsets.GenericViewSet):
"""
CRUD for account-scoped payment methods (Stripe/PayPal/manual bank/local_wallet).
ViewSet for billing operations (admin-only).
"""
serializer_class = AccountPaymentMethodSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
account = getattr(self.request.user, 'account', None)
qs = AccountPaymentMethod.objects.all()
if account:
qs = qs.filter(account=account)
else:
qs = qs.none()
return qs.order_by('-is_default', 'display_name', 'id')
def perform_create(self, serializer):
account = self.request.user.account
with models.transaction.atomic():
obj = serializer.save(account=account)
make_default = serializer.validated_data.get('is_default') or not AccountPaymentMethod.objects.filter(account=account, is_default=True).exists()
if make_default:
AccountPaymentMethod.objects.filter(account=account).exclude(id=obj.id).update(is_default=False)
obj.is_default = True
obj.save(update_fields=['is_default'])
def perform_update(self, serializer):
account = self.request.user.account
with models.transaction.atomic():
obj = serializer.save()
if serializer.validated_data.get('is_default'):
AccountPaymentMethod.objects.filter(account=account).exclude(id=obj.id).update(is_default=False)
@action(detail=True, methods=['post'])
def set_default(self, request, pk=None):
account = request.user.account
method = get_object_or_404(AccountPaymentMethod, id=pk, account=account)
with models.transaction.atomic():
AccountPaymentMethod.objects.filter(account=account).update(is_default=False)
method.is_default = True
method.save(update_fields=['is_default'])
return Response({'message': 'Default payment method updated', 'id': method.id})
class CreditPackageViewSet(viewsets.ViewSet):
"""Credit package endpoints"""
permission_classes = [IsAuthenticated]
permission_classes = [IsAdminOrOwner]
def list(self, request):
"""List available credit packages"""
packages = CreditPackage.objects.filter(is_active=True).order_by('price')
@action(detail=False, methods=['post'], url_path='confirm-bank-transfer')
def confirm_bank_transfer(self, request):
"""
Confirm a bank transfer payment and activate/renew subscription.
return Response({
'results': [
{
'id': pkg.id,
'name': pkg.name,
'slug': pkg.slug,
'credits': pkg.credits,
'price': str(pkg.price),
'discount_percentage': pkg.discount_percentage,
'is_featured': pkg.is_featured,
'description': pkg.description,
'display_order': pkg.sort_order
}
for pkg in packages
],
'count': packages.count()
})
@action(detail=True, methods=['post'])
def purchase(self, request, pk=None):
"""Purchase a credit package"""
account = request.user.account
package = get_object_or_404(CreditPackage, id=pk, is_active=True)
payment_method = request.data.get('payment_method', 'stripe')
# Create invoice for credit package
invoice = InvoiceService.create_credit_package_invoice(
account=account,
credit_package=package
)
# Store credit package info in metadata
metadata = {
'credit_package_id': package.id,
'credit_amount': package.credits
Request body:
{
"account_id": 123,
"external_payment_id": "BT-2025-001",
"amount": "29.99",
"payer_name": "John Doe",
"proof_url": "https://...",
"period_months": 1
}
"""
account_id = request.data.get('account_id')
subscription_id = request.data.get('subscription_id')
external_payment_id = request.data.get('external_payment_id')
amount = request.data.get('amount')
payer_name = request.data.get('payer_name')
proof_url = request.data.get('proof_url')
period_months = int(request.data.get('period_months', 1))
if payment_method == 'stripe':
# TODO: Create Stripe payment intent
return Response({
'invoice_id': invoice.id,
'message': 'Stripe integration pending',
'next_action': 'redirect_to_stripe_checkout'
})
elif payment_method == 'paypal':
# TODO: Create PayPal order
return Response({
'invoice_id': invoice.id,
'message': 'PayPal integration pending',
'next_action': 'redirect_to_paypal_checkout'
})
else:
# Manual payment
return Response({
'invoice_id': invoice.id,
'invoice_number': invoice.invoice_number,
'total_amount': str(invoice.total_amount),
'message': 'Invoice created. Please submit payment details.',
'next_action': 'submit_manual_payment'
})
class CreditTransactionViewSet(viewsets.ViewSet):
"""Credit transaction history"""
permission_classes = [IsAuthenticated]
def list(self, request):
"""List credit transactions for current account"""
account = request.user.account
transactions = CreditTransaction.objects.filter(
account=account
).order_by('-created_at')[:100]
return Response({
'results': [
{
'id': txn.id,
'amount': txn.amount,
'transaction_type': txn.transaction_type,
'description': txn.description,
'created_at': txn.created_at.isoformat(),
'reference_id': txn.reference_id,
'metadata': txn.metadata
}
for txn in transactions
],
'count': transactions.count(),
'current_balance': account.credits
})
@action(detail=False, methods=['get'])
def balance(self, request):
"""Get current credit balance"""
account = request.user.account
from django.utils import timezone
from datetime import timedelta
now = timezone.now()
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
used_this_month = abs(
CreditTransaction.objects.filter(
account=account,
created_at__gte=month_start,
amount__lt=0
).aggregate(total=models.Sum('amount'))['total'] or 0
)
plan = getattr(account, 'plan', None)
included = plan.included_credits if plan else 0
return Response({
'credits': account.credits,
'plan_credits_per_month': included,
'credits_used_this_month': used_this_month,
'credits_remaining': max(account.credits, 0),
})
@extend_schema_view(
invoices=extend_schema(tags=['Admin Billing']),
payments=extend_schema(tags=['Admin Billing']),
pending_payments=extend_schema(tags=['Admin Billing']),
approve_payment=extend_schema(tags=['Admin Billing']),
reject_payment=extend_schema(tags=['Admin Billing']),
stats=extend_schema(tags=['Admin Billing']),
)
class AdminBillingViewSet(viewsets.ViewSet):
"""Admin billing management"""
permission_classes = [IsAuthenticated]
def _require_admin(self, request):
if not request.user.is_staff and not getattr(request.user, 'is_superuser', False):
return Response(
{'error': 'Admin access required'},
status=status.HTTP_403_FORBIDDEN
if not all([external_payment_id, amount, payer_name]):
return error_response(
error='external_payment_id, amount, and payer_name are required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
return None
@action(detail=False, methods=['get'])
def invoices(self, request):
"""List invoices across all accounts (admin)"""
error = self._require_admin(request)
if error:
return error
status_filter = request.query_params.get('status')
account_id = request.query_params.get('account_id')
qs = Invoice.objects.all().select_related('account').order_by('-created_at')
if status_filter:
qs = qs.filter(status=status_filter)
if account_id:
qs = qs.filter(account_id=account_id)
invoices = qs[:200]
return Response({
'results': [
{
'id': inv.id,
'invoice_number': inv.invoice_number,
'status': inv.status,
'total_amount': str(getattr(inv, 'total_amount', inv.total)),
'subtotal': str(inv.subtotal),
'tax_amount': str(getattr(inv, 'tax_amount', inv.tax)),
'currency': inv.currency,
'created_at': inv.created_at.isoformat(),
'paid_at': inv.paid_at.isoformat() if inv.paid_at else None,
'due_date': inv.due_date.isoformat() if inv.due_date else None,
'line_items': inv.line_items,
'account_name': inv.account.name if inv.account else None,
}
for inv in invoices
],
'count': qs.count(),
})
@action(detail=False, methods=['get'])
def payments(self, request):
"""List payments across all accounts (admin)"""
error = self._require_admin(request)
if error:
return error
status_filter = request.query_params.get('status')
account_id = request.query_params.get('account_id')
payment_method = request.query_params.get('payment_method')
qs = Payment.objects.all().select_related('account', 'invoice').order_by('-created_at')
if status_filter:
qs = qs.filter(status=status_filter)
if account_id:
qs = qs.filter(account_id=account_id)
if payment_method:
qs = qs.filter(payment_method=payment_method)
payments = qs[:200]
return Response({
'results': [
{
'id': pay.id,
'account_name': pay.account.name if pay.account else None,
'amount': str(pay.amount),
'currency': pay.currency,
'status': pay.status,
'payment_method': pay.payment_method,
'created_at': pay.created_at.isoformat(),
'invoice_id': pay.invoice_id,
'invoice_number': pay.invoice.invoice_number if pay.invoice else None,
'transaction_reference': pay.transaction_reference,
}
for pay in payments
],
'count': qs.count(),
})
@action(detail=False, methods=['get'])
def pending_payments(self, request):
"""List payments pending approval"""
error = self._require_admin(request)
if error:
return error
payments = PaymentService.get_pending_approvals()
return Response({
'results': [
{
'id': pay.id,
'account_name': pay.account.name,
'amount': str(pay.amount),
'currency': pay.currency,
'payment_method': pay.payment_method,
'transaction_reference': pay.transaction_reference,
'created_at': pay.created_at.isoformat(),
'invoice_number': pay.invoice.invoice_number if pay.invoice else None,
'admin_notes': pay.admin_notes
}
for pay in payments
],
'count': len(payments)
})
@action(detail=True, methods=['post'])
def approve_payment(self, request, pk=None):
"""Approve a manual payment"""
error = self._require_admin(request)
if error:
return error
payment = get_object_or_404(Payment, id=pk)
admin_notes = request.data.get('notes')
if not account_id and not subscription_id:
return error_response(
error='Either account_id or subscription_id is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
try:
payment = PaymentService.approve_manual_payment(
payment=payment,
approved_by_user_id=request.user.id,
admin_notes=admin_notes
with transaction.atomic():
# Get account
if account_id:
account = Account.objects.select_related('plan').get(id=account_id)
subscription = getattr(account, 'subscription', None)
else:
subscription = Subscription.objects.select_related('account', 'account__plan').get(id=subscription_id)
account = subscription.account
if not account or not account.plan:
return error_response(
error='Account or plan not found',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
# Calculate period dates based on billing cycle
now = timezone.now()
if account.plan.billing_cycle == 'monthly':
period_end = now + timedelta(days=30 * period_months)
elif account.plan.billing_cycle == 'annual':
period_end = now + timedelta(days=365 * period_months)
else:
period_end = now + timedelta(days=30 * period_months)
# Create or update subscription
if not subscription:
subscription = Subscription.objects.create(
account=account,
payment_method='bank_transfer',
external_payment_id=external_payment_id,
status='active',
current_period_start=now,
current_period_end=period_end,
cancel_at_period_end=False
)
else:
subscription.payment_method = 'bank_transfer'
subscription.external_payment_id = external_payment_id
subscription.status = 'active'
subscription.current_period_start = now
subscription.current_period_end = period_end
subscription.cancel_at_period_end = False
subscription.save()
# Update account
account.payment_method = 'bank_transfer'
account.status = 'active'
monthly_credits = account.plan.get_effective_credits_per_month()
account.credits = monthly_credits
account.save()
# Log transaction
CreditTransaction.objects.create(
account=account,
transaction_type='subscription',
amount=monthly_credits,
balance_after=monthly_credits,
description=f'Bank transfer payment confirmed: {external_payment_id}',
metadata={
'external_payment_id': external_payment_id,
'amount': str(amount),
'payer_name': payer_name,
'proof_url': proof_url if proof_url else '',
'period_months': period_months,
'confirmed_by': request.user.email
}
)
logger.info(
f'Bank transfer confirmed for account {account.id}: '
f'{external_payment_id}, {amount}, {monthly_credits} credits added'
)
return success_response(
data={
'account_id': account.id,
'subscription_id': subscription.id,
'status': 'active',
'credits': account.credits,
'period_start': subscription.current_period_start.isoformat(),
'period_end': subscription.current_period_end.isoformat()
},
message='Bank transfer confirmed successfully',
request=request
)
except Account.DoesNotExist:
return error_response(
error='Account not found',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
return Response({
'id': payment.id,
'status': payment.status,
'message': 'Payment approved successfully'
})
except ValueError as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
except Subscription.DoesNotExist:
return error_response(
error='Subscription not found',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
@action(detail=True, methods=['post'])
def reject_payment(self, request, pk=None):
"""Reject a manual payment"""
error = self._require_admin(request)
if error:
return error
payment = get_object_or_404(Payment, id=pk)
rejection_reason = request.data.get('reason', 'No reason provided')
try:
payment = PaymentService.reject_manual_payment(
payment=payment,
rejected_by_user_id=request.user.id,
rejection_reason=rejection_reason
except Exception as e:
logger.error(f'Error confirming bank transfer: {str(e)}', exc_info=True)
return error_response(
error=f'Failed to confirm payment: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
return Response({
'id': payment.id,
'status': payment.status,
'message': 'Payment rejected'
})
except ValueError as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['get', 'post'])
def payment_method_configs(self, request):
"""List/create payment method configs (country-level)"""
error = self._require_admin(request)
if error:
return error
class PMConfigSerializer(serializers.ModelSerializer):
class Meta:
model = PaymentMethodConfig
fields = [
'id',
'country_code',
'payment_method',
'display_name',
'is_enabled',
'instructions',
'sort_order',
'created_at',
'updated_at',
]
read_only_fields = ['id', 'created_at', 'updated_at']
if request.method.lower() == 'post':
serializer = PMConfigSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
return Response(PMConfigSerializer(obj).data, status=status.HTTP_201_CREATED)
qs = PaymentMethodConfig.objects.all().order_by('country_code', 'sort_order', 'payment_method')
country = request.query_params.get('country_code')
method = request.query_params.get('payment_method')
if country:
qs = qs.filter(country_code=country)
if method:
qs = qs.filter(payment_method=method)
data = PMConfigSerializer(qs, many=True).data
return Response({'results': data, 'count': len(data)})
@action(detail=True, methods=['get', 'patch', 'put', 'delete'], url_path='payment_method_config')
@extend_schema(tags=['Admin Billing'])
def payment_method_config(self, request, pk=None):
"""Retrieve/update/delete a payment method config"""
error = self._require_admin(request)
if error:
return error
obj = get_object_or_404(PaymentMethodConfig, id=pk)
class PMConfigSerializer(serializers.ModelSerializer):
class Meta:
model = PaymentMethodConfig
fields = [
'id',
'country_code',
'payment_method',
'display_name',
'is_enabled',
'instructions',
'sort_order',
'created_at',
'updated_at',
]
read_only_fields = ['id', 'created_at', 'updated_at']
if request.method.lower() == 'get':
return Response(PMConfigSerializer(obj).data)
if request.method.lower() in ['patch', 'put']:
partial = request.method.lower() == 'patch'
serializer = PMConfigSerializer(obj, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
return Response(PMConfigSerializer(obj).data)
# delete
obj.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=False, methods=['get', 'post'])
@extend_schema(tags=['Admin Billing'])
def account_payment_methods(self, request):
"""List/create account payment methods (admin)"""
error = self._require_admin(request)
if error:
return error
class AccountPMSerializer(serializers.ModelSerializer):
class Meta:
model = AccountPaymentMethod
fields = [
'id',
'account',
'type',
'display_name',
'is_default',
'is_enabled',
'is_verified',
'country_code',
'instructions',
'metadata',
'created_at',
'updated_at',
]
read_only_fields = ['id', 'is_verified', 'created_at', 'updated_at']
if request.method.lower() == 'post':
serializer = AccountPMSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
return Response(AccountPMSerializer(obj).data, status=status.HTTP_201_CREATED)
qs = AccountPaymentMethod.objects.select_related('account').order_by('account_id', '-is_default', 'display_name')
account_id = request.query_params.get('account_id')
if account_id:
qs = qs.filter(account_id=account_id)
data = AccountPMSerializer(qs, many=True).data
return Response({'results': data, 'count': len(data)})
@action(detail=True, methods=['get', 'patch', 'put', 'delete'], url_path='account_payment_method')
@extend_schema(tags=['Admin Billing'])
def account_payment_method(self, request, pk=None):
"""Retrieve/update/delete an account payment method (admin)"""
error = self._require_admin(request)
if error:
return error
obj = get_object_or_404(AccountPaymentMethod, id=pk)
class AccountPMSerializer(serializers.ModelSerializer):
class Meta:
model = AccountPaymentMethod
fields = [
'id',
'account',
'type',
'display_name',
'is_default',
'is_enabled',
'is_verified',
'country_code',
'instructions',
'metadata',
'created_at',
'updated_at',
]
read_only_fields = ['id', 'is_verified', 'created_at', 'updated_at']
if request.method.lower() == 'get':
return Response(AccountPMSerializer(obj).data)
if request.method.lower() in ['patch', 'put']:
partial = request.method.lower() == 'patch'
serializer = AccountPMSerializer(obj, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
if serializer.validated_data.get('is_default'):
AccountPaymentMethod.objects.filter(account=obj.account).exclude(id=obj.id).update(is_default=False)
return Response(AccountPMSerializer(obj).data)
obj.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=True, methods=['post'], url_path='account_payment_method/set_default')
@extend_schema(tags=['Admin Billing'])
def set_default_account_payment_method(self, request, pk=None):
"""Set default account payment method (admin)"""
error = self._require_admin(request)
if error:
return error
obj = get_object_or_404(AccountPaymentMethod, id=pk)
AccountPaymentMethod.objects.filter(account=obj.account).update(is_default=False)
obj.is_default = True
obj.save(update_fields=['is_default'])
return Response({'message': 'Default payment method updated', 'id': obj.id})
@action(detail=False, methods=['get'])
def stats(self, request):
"""System billing stats"""
error = self._require_admin(request)
if error:
return error
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(status='active').count()
new_accounts_this_month = Account.objects.filter(
created_at__gte=this_month_start
).count()
# Subscription stats
# Subscriptions are linked via OneToOne "subscription"
active_subscriptions = Account.objects.filter(
subscription__status='active'
).distinct().count()
# Revenue stats
total_revenue = Payment.objects.filter(
status__in=['completed', 'succeeded'],
amount__gt=0
).aggregate(total=Sum('amount'))['total'] or 0
revenue_this_month = Payment.objects.filter(
status__in=['completed', 'succeeded'],
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
# Usage transactions are stored as deductions (negative amounts)
credits_used = abs(CreditTransaction.objects.filter(
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__in=['completed', 'succeeded']
).order_by('-processed_at')[:5]
recent_activity = []
for pay in recent_payments:
account_name = getattr(pay.account, 'name', 'Unknown')
currency = pay.currency or 'USD'
ts = pay.processed_at.isoformat() if pay.processed_at else now.isoformat()
recent_activity.append({
'id': pay.id,
'type': 'payment',
'account_name': account_name,
'amount': str(pay.amount),
'currency': currency,
'timestamp': ts,
'description': f'Payment received via {pay.payment_method or "unknown"}'
})
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': invoices_pending,
'invoices_overdue': invoices_overdue,
'recent_activity': recent_activity,
'system_health': {
'status': 'operational',
'last_check': now.isoformat()
}
})