""" Billing API Views Comprehensive billing endpoints for invoices, payments, credit packages """ 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 .models import Invoice, Payment, CreditPackage, PaymentMethodConfig, CreditTransaction from .services.invoice_service import InvoiceService from .services.payment_service import PaymentService 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) return Response(methods) @action(detail=False, methods=['post']) 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') 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 CreditPackageViewSet(viewsets.ViewSet): """Credit package endpoints""" permission_classes = [IsAuthenticated] def list(self, request): """List available credit packages""" packages = CreditPackage.objects.filter(is_active=True).order_by('price') 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 } 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.credit_balance }) @action(detail=False, methods=['get']) def balance(self, request): """Get current credit balance""" account = request.user.account # Get subscription details active_subscription = account.subscriptions.filter(status='active').first() return Response({ 'balance': account.credit_balance, 'subscription_plan': active_subscription.plan.name if active_subscription else 'None', 'monthly_credits': active_subscription.plan.monthly_credits if active_subscription else 0, 'subscription_status': active_subscription.status if active_subscription else None }) class AdminBillingViewSet(viewsets.ViewSet): """Admin billing management""" permission_classes = [IsAuthenticated] @action(detail=False, methods=['get']) def pending_payments(self, request): """List payments pending approval""" # Check admin permission if not request.user.is_staff: return Response( {'error': 'Admin access required'}, status=status.HTTP_403_FORBIDDEN ) 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""" if not request.user.is_staff: return Response( {'error': 'Admin access required'}, status=status.HTTP_403_FORBIDDEN ) payment = get_object_or_404(Payment, id=pk) admin_notes = request.data.get('notes') try: payment = PaymentService.approve_manual_payment( payment=payment, approved_by_user_id=request.user.id, admin_notes=admin_notes ) 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 ) @action(detail=True, methods=['post']) def reject_payment(self, request, pk=None): """Reject a manual payment""" if not request.user.is_staff: return Response( {'error': 'Admin access required'}, status=status.HTTP_403_FORBIDDEN ) 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 ) 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']) def stats(self, request): """System billing stats""" if not request.user.is_staff: return Response( {'error': 'Admin access required'}, status=status.HTTP_403_FORBIDDEN ) 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': invoices_pending, 'invoices_overdue': invoices_overdue, 'recent_activity': recent_activity, 'system_health': { 'status': 'operational', 'last_check': now.isoformat() } })