adsasdasd
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
"""Billing routes including bank transfer confirmation and credit endpoints."""
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import BillingViewSet
|
||||
from .views import (
|
||||
BillingViewSet,
|
||||
InvoiceViewSet,
|
||||
PaymentViewSet,
|
||||
CreditPackageViewSet,
|
||||
AccountPaymentMethodViewSet,
|
||||
)
|
||||
from igny8_core.modules.billing.views import (
|
||||
CreditBalanceViewSet,
|
||||
CreditUsageViewSet,
|
||||
@@ -14,6 +20,11 @@ router.register(r'admin', BillingViewSet, basename='billing-admin')
|
||||
router.register(r'credits/balance', CreditBalanceViewSet, basename='credit-balance')
|
||||
router.register(r'credits/usage', CreditUsageViewSet, basename='credit-usage')
|
||||
router.register(r'credits/transactions', CreditTransactionViewSet, basename='credit-transactions')
|
||||
# User-facing billing endpoints
|
||||
router.register(r'invoices', InvoiceViewSet, basename='invoices')
|
||||
router.register(r'payments', PaymentViewSet, basename='payments')
|
||||
router.register(r'credit-packages', CreditPackageViewSet, basename='credit-packages')
|
||||
router.register(r'payment-methods', AccountPaymentMethodViewSet, basename='payment-methods')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
|
||||
@@ -3,14 +3,19 @@ Billing Views - Payment confirmation and management
|
||||
"""
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from django.http import HttpResponse
|
||||
from datetime import timedelta
|
||||
from igny8_core.api.response import success_response, error_response
|
||||
from igny8_core.api.permissions import IsAdminOrOwner
|
||||
from igny8_core.api.response import success_response, error_response, paginated_response
|
||||
from igny8_core.api.permissions import IsAdminOrOwner, IsAuthenticatedAndActive, HasTenantAccess
|
||||
from igny8_core.api.base import AccountModelViewSet
|
||||
from igny8_core.api.pagination import CustomPageNumberPagination
|
||||
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
|
||||
from igny8_core.business.billing.services.invoice_service import InvoiceService
|
||||
from igny8_core.business.billing.models import CreditTransaction, Invoice, Payment, CreditPackage, AccountPaymentMethod
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -166,3 +171,242 @@ class BillingViewSet(viewsets.GenericViewSet):
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
|
||||
class InvoiceViewSet(AccountModelViewSet):
|
||||
"""ViewSet for user-facing invoices"""
|
||||
queryset = Invoice.objects.all().select_related('account')
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter invoices by account"""
|
||||
queryset = super().get_queryset()
|
||||
if hasattr(self.request, 'account') and self.request.account:
|
||||
queryset = queryset.filter(account=self.request.account)
|
||||
return queryset.order_by('-invoice_date', '-created_at')
|
||||
|
||||
def list(self, request):
|
||||
"""List invoices for current account"""
|
||||
queryset = self.get_queryset()
|
||||
|
||||
# Filter by status if provided
|
||||
status_param = request.query_params.get('status')
|
||||
if status_param:
|
||||
queryset = queryset.filter(status=status_param)
|
||||
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset, request)
|
||||
|
||||
# Serialize invoice data
|
||||
results = []
|
||||
for invoice in page:
|
||||
results.append({
|
||||
'id': invoice.id,
|
||||
'invoice_number': invoice.invoice_number,
|
||||
'status': invoice.status,
|
||||
'total_amount': str(invoice.total),
|
||||
'subtotal': str(invoice.subtotal),
|
||||
'tax_amount': str(invoice.tax),
|
||||
'currency': invoice.currency,
|
||||
'invoice_date': invoice.invoice_date.isoformat(),
|
||||
'due_date': invoice.due_date.isoformat(),
|
||||
'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None,
|
||||
'line_items': invoice.line_items,
|
||||
'billing_email': invoice.billing_email,
|
||||
'notes': invoice.notes,
|
||||
'created_at': invoice.created_at.isoformat(),
|
||||
})
|
||||
|
||||
paginated_data = paginator.get_paginated_response({'results': results}).data
|
||||
return paginated_response(paginated_data, request=request)
|
||||
|
||||
def retrieve(self, request, pk=None):
|
||||
"""Get invoice detail"""
|
||||
try:
|
||||
invoice = self.get_queryset().get(pk=pk)
|
||||
data = {
|
||||
'id': invoice.id,
|
||||
'invoice_number': invoice.invoice_number,
|
||||
'status': invoice.status,
|
||||
'total_amount': str(invoice.total),
|
||||
'subtotal': str(invoice.subtotal),
|
||||
'tax_amount': str(invoice.tax),
|
||||
'currency': invoice.currency,
|
||||
'invoice_date': invoice.invoice_date.isoformat(),
|
||||
'due_date': invoice.due_date.isoformat(),
|
||||
'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None,
|
||||
'line_items': invoice.line_items,
|
||||
'billing_email': invoice.billing_email,
|
||||
'notes': invoice.notes,
|
||||
'created_at': invoice.created_at.isoformat(),
|
||||
}
|
||||
return success_response(data=data, request=request)
|
||||
except Invoice.DoesNotExist:
|
||||
return error_response(error='Invoice not found', status_code=404, request=request)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def download_pdf(self, request, pk=None):
|
||||
"""Download invoice PDF"""
|
||||
try:
|
||||
invoice = self.get_queryset().get(pk=pk)
|
||||
pdf_bytes = InvoiceService.generate_pdf(invoice)
|
||||
|
||||
response = HttpResponse(pdf_bytes, content_type='application/pdf')
|
||||
response['Content-Disposition'] = f'attachment; filename="invoice-{invoice.invoice_number}.pdf"'
|
||||
return response
|
||||
except Invoice.DoesNotExist:
|
||||
return error_response(error='Invoice not found', status_code=404, request=request)
|
||||
|
||||
|
||||
class PaymentViewSet(AccountModelViewSet):
|
||||
"""ViewSet for user-facing payments"""
|
||||
queryset = Payment.objects.all().select_related('account', 'invoice')
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter payments by account"""
|
||||
queryset = super().get_queryset()
|
||||
if hasattr(self.request, 'account') and self.request.account:
|
||||
queryset = queryset.filter(account=self.request.account)
|
||||
return queryset.order_by('-created_at')
|
||||
|
||||
def list(self, request):
|
||||
"""List payments for current account"""
|
||||
queryset = self.get_queryset()
|
||||
|
||||
# Filter by status if provided
|
||||
status_param = request.query_params.get('status')
|
||||
if status_param:
|
||||
queryset = queryset.filter(status=status_param)
|
||||
|
||||
# Filter by invoice if provided
|
||||
invoice_id = request.query_params.get('invoice_id')
|
||||
if invoice_id:
|
||||
queryset = queryset.filter(invoice_id=invoice_id)
|
||||
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset, request)
|
||||
|
||||
# Serialize payment data
|
||||
results = []
|
||||
for payment in page:
|
||||
results.append({
|
||||
'id': payment.id,
|
||||
'invoice_id': payment.invoice_id,
|
||||
'invoice_number': payment.invoice.invoice_number if payment.invoice else None,
|
||||
'amount': str(payment.amount),
|
||||
'currency': payment.currency,
|
||||
'status': payment.status,
|
||||
'payment_method': payment.payment_method,
|
||||
'created_at': payment.created_at.isoformat(),
|
||||
'processed_at': payment.processed_at.isoformat() if payment.processed_at else None,
|
||||
'manual_reference': payment.manual_reference,
|
||||
'manual_notes': payment.manual_notes,
|
||||
})
|
||||
|
||||
paginated_data = paginator.get_paginated_response({'results': results}).data
|
||||
return paginated_response(paginated_data, request=request)
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def manual(self, request):
|
||||
"""Submit manual payment for approval"""
|
||||
invoice_id = request.data.get('invoice_id')
|
||||
amount = request.data.get('amount')
|
||||
payment_method = request.data.get('payment_method', 'bank_transfer')
|
||||
reference = request.data.get('reference', '')
|
||||
notes = request.data.get('notes', '')
|
||||
|
||||
if not amount:
|
||||
return error_response(error='Amount is required', status_code=400, request=request)
|
||||
|
||||
try:
|
||||
account = request.account
|
||||
invoice = None
|
||||
if invoice_id:
|
||||
invoice = Invoice.objects.get(id=invoice_id, account=account)
|
||||
|
||||
payment = Payment.objects.create(
|
||||
account=account,
|
||||
invoice=invoice,
|
||||
amount=amount,
|
||||
currency='USD',
|
||||
payment_method=payment_method,
|
||||
status='pending_approval',
|
||||
manual_reference=reference,
|
||||
manual_notes=notes,
|
||||
)
|
||||
|
||||
return success_response(
|
||||
data={'id': payment.id, 'status': payment.status},
|
||||
message='Manual payment submitted for approval',
|
||||
status_code=201,
|
||||
request=request
|
||||
)
|
||||
except Invoice.DoesNotExist:
|
||||
return error_response(error='Invoice not found', status_code=404, request=request)
|
||||
|
||||
|
||||
class CreditPackageViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for credit packages (read-only for users)"""
|
||||
queryset = CreditPackage.objects.filter(is_active=True).order_by('sort_order')
|
||||
permission_classes = [IsAuthenticatedAndActive]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
|
||||
def list(self, request):
|
||||
"""List available credit packages"""
|
||||
queryset = self.get_queryset()
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset, request)
|
||||
|
||||
results = []
|
||||
for package in page:
|
||||
results.append({
|
||||
'id': package.id,
|
||||
'name': package.name,
|
||||
'slug': package.slug,
|
||||
'credits': package.credits,
|
||||
'price': str(package.price),
|
||||
'discount_percentage': package.discount_percentage,
|
||||
'is_featured': package.is_featured,
|
||||
'description': package.description,
|
||||
'display_order': package.sort_order,
|
||||
})
|
||||
|
||||
paginated_data = paginator.get_paginated_response({'results': results}).data
|
||||
return paginated_response(paginated_data, request=request)
|
||||
|
||||
|
||||
class AccountPaymentMethodViewSet(AccountModelViewSet):
|
||||
"""ViewSet for account payment methods"""
|
||||
queryset = AccountPaymentMethod.objects.all()
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter payment methods by account"""
|
||||
queryset = super().get_queryset()
|
||||
if hasattr(self.request, 'account') and self.request.account:
|
||||
queryset = queryset.filter(account=self.request.account)
|
||||
return queryset.order_by('-is_default', 'type')
|
||||
|
||||
def list(self, request):
|
||||
"""List payment methods for current account"""
|
||||
queryset = self.get_queryset()
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset, request)
|
||||
|
||||
results = []
|
||||
for method in page:
|
||||
results.append({
|
||||
'id': str(method.id),
|
||||
'type': method.type,
|
||||
'display_name': method.display_name,
|
||||
'is_default': method.is_default,
|
||||
'is_enabled': method.is_enabled if hasattr(method, 'is_enabled') else True,
|
||||
'instructions': method.instructions,
|
||||
})
|
||||
|
||||
paginated_data = paginator.get_paginated_response({'results': results}).data
|
||||
return paginated_response(paginated_data, request=request)
|
||||
|
||||
Reference in New Issue
Block a user