Files
igny8/backend/igny8_core/business/billing/views.py
IGNY8 VPS (Salman) da3b45d1c7 adsasdasd
2025-12-08 11:51:00 +00:00

413 lines
17 KiB
Python

"""
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, 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.services.invoice_service import InvoiceService
from igny8_core.business.billing.models import CreditTransaction, Invoice, Payment, CreditPackage, AccountPaymentMethod
import logging
logger = logging.getLogger(__name__)
class BillingViewSet(viewsets.GenericViewSet):
"""
ViewSet for billing operations (admin-only).
"""
permission_classes = [IsAdminOrOwner]
@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.
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 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
)
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:
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
)
except Subscription.DoesNotExist:
return error_response(
error='Subscription not found',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
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
)
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)