""" DRF API ViewSets for Finance app. """ from rest_framework import viewsets, filters, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from django_filters.rest_framework import DjangoFilterBackend from django.utils import timezone from django.db.models import Sum, Q from .models import Invoice, Payment, Service, Package, PackagePurchase, Payer from .serializers import ( InvoiceListSerializer, InvoiceDetailSerializer, InvoiceCreateSerializer, PaymentSerializer, ServiceSerializer, PackageSerializer, PackagePurchaseSerializer, PayerSerializer ) class InvoiceViewSet(viewsets.ModelViewSet): """ API endpoint for Invoice CRUD operations. list: Get list of invoices retrieve: Get invoice details create: Create new invoice update: Update invoice destroy: Delete invoice Custom actions: - issue: Issue draft invoice - payments: Get invoice payments - mark_paid: Mark invoice as paid """ permission_classes = [IsAuthenticated] filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields = ['patient', 'status', 'issue_date'] search_fields = ['invoice_number', 'patient__mrn', 'patient__first_name_en'] ordering_fields = ['issue_date', 'due_date', 'total'] ordering = ['-issue_date'] def get_queryset(self): """Filter by tenant.""" return Invoice.objects.filter( tenant=self.request.user.tenant ).select_related('patient', 'appointment').prefetch_related('line_items', 'payments') def get_serializer_class(self): """Return appropriate serializer based on action.""" if self.action == 'list': return InvoiceListSerializer elif self.action == 'create': return InvoiceCreateSerializer return InvoiceDetailSerializer def perform_create(self, serializer): """Set tenant on create.""" serializer.save(tenant=self.request.user.tenant) @action(detail=True, methods=['post']) def issue(self, request, pk=None): """Issue a draft invoice.""" invoice = self.get_object() if invoice.status != 'DRAFT': return Response( {'error': 'Only draft invoices can be issued'}, status=status.HTTP_400_BAD_REQUEST ) invoice.status = 'ISSUED' invoice.save() serializer = InvoiceDetailSerializer(invoice) return Response(serializer.data) @action(detail=True, methods=['get']) def payments(self, request, pk=None): """Get invoice payments.""" invoice = self.get_object() payments = invoice.payments.all() serializer = PaymentSerializer(payments, many=True) return Response(serializer.data) @action(detail=True, methods=['post']) def mark_paid(self, request, pk=None): """Mark invoice as fully paid.""" invoice = self.get_object() # Calculate total paid total_paid = invoice.payments.filter( status='COMPLETED' ).aggregate(Sum('amount'))['amount__sum'] or 0 if total_paid >= invoice.total: invoice.status = 'PAID' invoice.save() serializer = InvoiceDetailSerializer(invoice) return Response(serializer.data) else: return Response( { 'error': 'Invoice not fully paid', 'total': float(invoice.total), 'paid': float(total_paid), 'due': float(invoice.total - total_paid) }, status=status.HTTP_400_BAD_REQUEST ) @action(detail=False, methods=['get']) def overdue(self, request): """Get overdue invoices.""" today = timezone.now().date() invoices = self.get_queryset().filter( due_date__lt=today, status__in=['ISSUED', 'PARTIALLY_PAID'] ) serializer = InvoiceListSerializer(invoices, many=True) return Response(serializer.data) @action(detail=False, methods=['get']) def statistics(self, request): """Get invoice statistics.""" queryset = self.get_queryset() stats = { 'total_invoices': queryset.count(), 'total_amount': queryset.aggregate(Sum('total'))['total__sum'] or 0, 'by_status': dict(queryset.values('status').annotate( count=Count('id'), amount=Sum('total') ).values_list('status', 'amount')), 'overdue_count': queryset.filter( due_date__lt=timezone.now().date(), status__in=['ISSUED', 'PARTIALLY_PAID'] ).count(), } return Response(stats) class PaymentViewSet(viewsets.ModelViewSet): """ API endpoint for Payment CRUD operations. list: Get list of payments retrieve: Get payment details create: Record new payment update: Update payment destroy: Delete payment Custom actions: - process: Process pending payment - refund: Refund completed payment """ permission_classes = [IsAuthenticated] serializer_class = PaymentSerializer filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields = ['invoice', 'method', 'status', 'payment_date'] search_fields = ['transaction_id', 'reference', 'invoice__invoice_number'] ordering_fields = ['payment_date', 'amount'] ordering = ['-payment_date'] def get_queryset(self): """Filter by tenant.""" return Payment.objects.filter( tenant=self.request.user.tenant ).select_related('invoice__patient', 'processed_by') def perform_create(self, serializer): """Set tenant and processed_by on create.""" payment = serializer.save( tenant=self.request.user.tenant, processed_by=self.request.user, status='PENDING' ) # Update invoice status if fully paid invoice = payment.invoice total_paid = invoice.payments.filter( status='COMPLETED' ).aggregate(Sum('amount'))['amount__sum'] or 0 if total_paid >= invoice.total: invoice.status = 'PAID' elif total_paid > 0: invoice.status = 'PARTIALLY_PAID' invoice.save() @action(detail=True, methods=['post']) def process(self, request, pk=None): """Process a pending payment.""" payment = self.get_object() if payment.status != 'PENDING': return Response( {'error': 'Only pending payments can be processed'}, status=status.HTTP_400_BAD_REQUEST ) payment.status = 'COMPLETED' payment.save() # Update invoice status invoice = payment.invoice total_paid = invoice.payments.filter( status='COMPLETED' ).aggregate(Sum('amount'))['amount__sum'] or 0 if total_paid >= invoice.total: invoice.status = 'PAID' else: invoice.status = 'PARTIALLY_PAID' invoice.save() serializer = PaymentSerializer(payment) return Response(serializer.data) @action(detail=True, methods=['post']) def refund(self, request, pk=None): """Refund a completed payment.""" payment = self.get_object() if payment.status != 'COMPLETED': return Response( {'error': 'Only completed payments can be refunded'}, status=status.HTTP_400_BAD_REQUEST ) payment.status = 'REFUNDED' payment.save() # Update invoice status invoice = payment.invoice total_paid = invoice.payments.filter( status='COMPLETED' ).aggregate(Sum('amount'))['amount__sum'] or 0 if total_paid == 0: invoice.status = 'ISSUED' elif total_paid < invoice.total: invoice.status = 'PARTIALLY_PAID' invoice.save() serializer = PaymentSerializer(payment) return Response(serializer.data) class ServiceViewSet(viewsets.ReadOnlyModelViewSet): """ API endpoint for Service (read-only). list: Get list of services retrieve: Get service details """ permission_classes = [IsAuthenticated] serializer_class = ServiceSerializer filter_backends = [DjangoFilterBackend, filters.SearchFilter] filterset_fields = ['clinic', 'is_active'] search_fields = ['code', 'name_en', 'name_ar'] def get_queryset(self): """Filter by tenant and active services.""" return Service.objects.filter( tenant=self.request.user.tenant, is_active=True ).select_related('clinic') class PackageViewSet(viewsets.ReadOnlyModelViewSet): """ API endpoint for Package (read-only). list: Get list of packages retrieve: Get package details """ permission_classes = [IsAuthenticated] serializer_class = PackageSerializer filter_backends = [DjangoFilterBackend, filters.SearchFilter] filterset_fields = ['is_active'] search_fields = ['name_en', 'name_ar'] def get_queryset(self): """Filter by tenant and active packages.""" return Package.objects.filter( tenant=self.request.user.tenant, is_active=True ).prefetch_related('services') class PackagePurchaseViewSet(viewsets.ModelViewSet): """ API endpoint for PackagePurchase CRUD operations. list: Get list of package purchases retrieve: Get package purchase details create: Create new package purchase update: Update package purchase """ permission_classes = [IsAuthenticated] serializer_class = PackagePurchaseSerializer filter_backends = [DjangoFilterBackend, filters.SearchFilter] filterset_fields = ['patient', 'package', 'is_active'] search_fields = ['patient__mrn', 'patient__first_name_en'] def get_queryset(self): """Filter by tenant.""" return PackagePurchase.objects.filter( tenant=self.request.user.tenant ).select_related('patient', 'package', 'invoice') def perform_create(self, serializer): """Set tenant on create.""" serializer.save(tenant=self.request.user.tenant) @action(detail=True, methods=['post']) def use_session(self, request, pk=None): """Use one session from the package.""" purchase = self.get_object() if purchase.remaining_sessions <= 0: return Response( {'error': 'No sessions remaining'}, status=status.HTTP_400_BAD_REQUEST ) if not purchase.is_active: return Response( {'error': 'Package is not active'}, status=status.HTTP_400_BAD_REQUEST ) purchase.used_sessions += 1 purchase.save() serializer = PackagePurchaseSerializer(purchase) return Response(serializer.data) class PayerViewSet(viewsets.ModelViewSet): """ API endpoint for Payer CRUD operations. list: Get list of payers retrieve: Get payer details create: Create new payer update: Update payer destroy: Delete payer """ permission_classes = [IsAuthenticated] serializer_class = PayerSerializer filter_backends = [DjangoFilterBackend, filters.SearchFilter] filterset_fields = ['patient', 'type', 'is_active'] search_fields = ['name', 'policy_number'] def get_queryset(self): """Filter by tenant.""" return Payer.objects.filter( tenant=self.request.user.tenant ).select_related('patient') def perform_create(self, serializer): """Set tenant on create.""" serializer.save(tenant=self.request.user.tenant)