docs and billing adn acaoutn 40%
This commit is contained in:
@@ -1,54 +1,410 @@
|
||||
"""
|
||||
Billing API Views
|
||||
Stub endpoints for billing pages
|
||||
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 BillingViewSet(viewsets.ViewSet):
|
||||
"""Billing endpoints"""
|
||||
class InvoiceViewSet(viewsets.ViewSet):
|
||||
"""Invoice management endpoints"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='account_balance')
|
||||
def account_balance(self, request):
|
||||
"""Get user's credit balance"""
|
||||
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({
|
||||
'credits': 0,
|
||||
'subscription_plan': 'Free',
|
||||
'monthly_credits_included': 0,
|
||||
'bonus_credits': 0
|
||||
'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 transactions(self, request):
|
||||
"""List credit transactions"""
|
||||
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({
|
||||
'results': [],
|
||||
'count': 0
|
||||
'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 usage(self, request):
|
||||
"""List credit usage"""
|
||||
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({
|
||||
'results': [],
|
||||
'count': 0
|
||||
'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 endpoints"""
|
||||
"""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
|
||||
|
||||
total_accounts = Account.objects.count()
|
||||
active_subscriptions = Account.objects.filter(
|
||||
subscriptions__status='active'
|
||||
).distinct().count()
|
||||
|
||||
total_revenue = Payment.objects.filter(
|
||||
status='completed',
|
||||
amount__gt=0
|
||||
).aggregate(total=Sum('amount'))['total'] or 0
|
||||
|
||||
pending_approvals = Payment.objects.filter(
|
||||
status='pending_approval'
|
||||
).count()
|
||||
|
||||
return Response({
|
||||
'total_users': 0,
|
||||
'active_users': 0,
|
||||
'total_credits_issued': 0,
|
||||
'total_credits_used': 0
|
||||
'total_accounts': total_accounts,
|
||||
'active_subscriptions': active_subscriptions,
|
||||
'total_revenue': str(total_revenue),
|
||||
'pending_approvals': pending_approvals,
|
||||
'invoices_pending': Invoice.objects.filter(status='pending').count(),
|
||||
'invoices_paid': Invoice.objects.filter(status='paid').count()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user