""" Physicians API Views - Doctor Rating Import API endpoints for HIS integration and manual rating import. """ import logging from django.shortcuts import get_object_or_404 from rest_framework import status, serializers from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.authentication import TokenAuthentication from apps.accounts.permissions import IsPXAdminOrHospitalAdmin from apps.core.services import AuditService from apps.organizations.models import Hospital from .adapter import DoctorRatingAdapter from .models import DoctorRatingImportJob, PhysicianIndividualRating from .tasks import process_doctor_rating_job, aggregate_monthly_ratings_task logger = logging.getLogger(__name__) # ============================================================================ # Serializers # ============================================================================ class DoctorRatingImportSerializer(serializers.Serializer): """Serializer for single doctor rating import via API.""" uhid = serializers.CharField(required=True, help_text="Patient UHID/MRN") patient_name = serializers.CharField(required=True) gender = serializers.CharField(required=False, allow_blank=True) age = serializers.CharField(required=False, allow_blank=True) nationality = serializers.CharField(required=False, allow_blank=True) mobile_no = serializers.CharField(required=False, allow_blank=True) patient_type = serializers.CharField(required=False, allow_blank=True, help_text="IP, OP, ER, DC") admit_date = serializers.CharField(required=False, allow_blank=True, help_text="Format: DD-MMM-YYYY HH:MM:SS") discharge_date = serializers.CharField(required=False, allow_blank=True, help_text="Format: DD-MMM-YYYY HH:MM:SS") doctor_name = serializers.CharField(required=True, help_text="Format: ID-NAME (e.g., '10738-OMAYMAH YAQOUB')") rating = serializers.IntegerField(required=True, min_value=1, max_value=5) feedback = serializers.CharField(required=False, allow_blank=True) rating_date = serializers.CharField(required=True, help_text="Format: DD-MMM-YYYY HH:MM:SS") department = serializers.CharField(required=False, allow_blank=True) class BulkDoctorRatingImportSerializer(serializers.Serializer): """Serializer for bulk doctor rating import via API.""" hospital_id = serializers.UUIDField(required=True) ratings = DoctorRatingImportSerializer(many=True, required=True) source_reference = serializers.CharField(required=False, allow_blank=True, help_text="Reference ID from HIS system") class DoctorRatingResponseSerializer(serializers.Serializer): """Serializer for doctor rating import response.""" success = serializers.BooleanField() rating_id = serializers.UUIDField(required=False) message = serializers.CharField(required=False) staff_matched = serializers.BooleanField(required=False) staff_id = serializers.UUIDField(required=False) # ============================================================================ # API Endpoints # ============================================================================ @api_view(['POST']) @authentication_classes([TokenAuthentication]) @permission_classes([IsAuthenticated]) def import_single_rating(request): """ Import a single doctor rating from HIS. POST /api/physicians/ratings/import/single/ Expected payload: { "hospital_id": "uuid", "uhid": "ALHH.0030223126", "patient_name": "Tamam Saud Aljunaybi", "gender": "Female", "age": "36 Years", "nationality": "Saudi Arabia", "mobile_no": "0504884011", "patient_type": "OP", "admit_date": "22-Dec-2024 19:12:24", "discharge_date": "", "doctor_name": "10738-OMAYMAH YAQOUB ELAMEIAN", "rating": 5, "feedback": "Great service", "rating_date": "28-Dec-2024 22:31:29", "department": "ACCIDENT AND EMERGENCY" } Returns: { "success": true, "rating_id": "uuid", "message": "Rating imported successfully", "staff_matched": true, "staff_id": "uuid" } """ try: # Validate request data serializer = DoctorRatingImportSerializer(data=request.data) if not serializer.is_valid(): return Response( {'success': False, 'errors': serializer.errors}, status=status.HTTP_400_BAD_REQUEST ) data = serializer.validated_data # Get hospital hospital_id = request.data.get('hospital_id') if not hospital_id: return Response( {'success': False, 'message': 'hospital_id is required'}, status=status.HTTP_400_BAD_REQUEST ) hospital = get_object_or_404(Hospital, id=hospital_id) # Check permission user = request.user if not user.is_px_admin() and user.hospital != hospital: return Response( {'success': False, 'message': 'Permission denied for this hospital'}, status=status.HTTP_403_FORBIDDEN ) # Process the rating result = DoctorRatingAdapter.process_single_rating( data=data, hospital=hospital, source=PhysicianIndividualRating.RatingSource.HIS_API ) # Log audit if result['success']: AuditService.log_event( event_type='doctor_rating_import', description=f"Doctor rating imported for {data.get('doctor_name')}", user=user, metadata={ 'hospital': hospital.name, 'doctor_name': data.get('doctor_name'), 'rating': data.get('rating'), 'staff_matched': result['staff_matched'] } ) return Response(result, status=status.HTTP_201_CREATED) else: return Response(result, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"Error importing doctor rating: {str(e)}", exc_info=True) return Response( {'success': False, 'message': f"Server error: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) @api_view(['POST']) @authentication_classes([TokenAuthentication]) @permission_classes([IsAuthenticated]) def import_bulk_ratings(request): """ Import multiple doctor ratings from HIS (background processing). POST /api/physicians/ratings/import/bulk/ Expected payload: { "hospital_id": "uuid", "source_reference": "HIS_BATCH_20240115_001", "ratings": [ { "uhid": "ALHH.0030223126", "patient_name": "Tamam Saud Aljunaybi", "doctor_name": "10738-OMAYMAH YAQOUB ELAMEIAN", "rating": 5, "rating_date": "28-Dec-2024 22:31:29", ... }, ... ] } Returns: { "success": true, "job_id": "uuid", "job_status": "pending", "message": "Bulk import job queued", "total_records": 150 } """ try: # Validate request data serializer = BulkDoctorRatingImportSerializer(data=request.data) if not serializer.is_valid(): return Response( {'success': False, 'errors': serializer.errors}, status=status.HTTP_400_BAD_REQUEST ) data = serializer.validated_data hospital = get_object_or_404(Hospital, id=data['hospital_id']) # Check permission user = request.user if not user.is_px_admin() and user.hospital != hospital: return Response( {'success': False, 'message': 'Permission denied for this hospital'}, status=status.HTTP_403_FORBIDDEN ) # Create import job ratings = data['ratings'] job = DoctorRatingImportJob.objects.create( name=f"HIS Bulk Import - {hospital.name} - {len(ratings)} records", status=DoctorRatingImportJob.JobStatus.PENDING, source=DoctorRatingImportJob.JobSource.HIS_API, created_by=user, hospital=hospital, total_records=len(ratings), raw_data=[dict(r) for r in ratings], results={'source_reference': data.get('source_reference', '')} ) # Queue background task process_doctor_rating_job.delay(str(job.id)) # Log audit AuditService.log_event( event_type='doctor_rating_bulk_import', description=f"Bulk doctor rating import queued: {len(ratings)} records", user=user, metadata={ 'hospital': hospital.name, 'job_id': str(job.id), 'total_records': len(ratings), 'source_reference': data.get('source_reference', '') } ) return Response({ 'success': True, 'job_id': str(job.id), 'job_status': job.status, 'message': 'Bulk import job queued for processing', 'total_records': len(ratings) }, status=status.HTTP_202_ACCEPTED) except Exception as e: logger.error(f"Error queuing bulk doctor rating import: {str(e)}", exc_info=True) return Response( {'success': False, 'message': f"Server error: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) @api_view(['GET']) @permission_classes([IsAuthenticated]) def import_job_status(request, job_id): """ Get status of a doctor rating import job. GET /api/physicians/ratings/import/jobs/{job_id}/ Returns: { "job_id": "uuid", "name": "HIS Bulk Import - ...", "status": "completed", "progress_percentage": 100, "total_records": 150, "processed_count": 150, "success_count": 145, "failed_count": 5, "started_at": "2024-01-15T10:30:00Z", "completed_at": "2024-01-15T10:35:00Z", "duration_seconds": 300, "results": {...} } """ try: job = get_object_or_404(DoctorRatingImportJob, id=job_id) # Check permission user = request.user if not user.is_px_admin() and job.hospital != user.hospital: return Response( {'success': False, 'message': 'Permission denied'}, status=status.HTTP_403_FORBIDDEN ) return Response({ 'job_id': str(job.id), 'name': job.name, 'status': job.status, 'progress_percentage': job.progress_percentage, 'total_records': job.total_records, 'processed_count': job.processed_count, 'success_count': job.success_count, 'failed_count': job.failed_count, 'skipped_count': job.skipped_count, 'is_complete': job.is_complete, 'started_at': job.started_at, 'completed_at': job.completed_at, 'duration_seconds': job.duration_seconds, 'results': job.results, 'error_message': job.error_message }) except Exception as e: logger.error(f"Error getting import job status: {str(e)}", exc_info=True) return Response( {'success': False, 'message': f"Server error: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) @api_view(['GET']) @permission_classes([IsAuthenticated]) def import_job_list(request): """ List doctor rating import jobs for the user's hospital. GET /api/physicians/ratings/import/jobs/?hospital_id={uuid}&limit=50 Returns list of jobs with status and progress. """ try: user = request.user hospital_id = request.query_params.get('hospital_id') limit = int(request.query_params.get('limit', 50)) # Build queryset queryset = DoctorRatingImportJob.objects.all() if not user.is_px_admin(): if user.hospital: queryset = queryset.filter(hospital=user.hospital) else: queryset = queryset.filter(created_by=user) if hospital_id: queryset = queryset.filter(hospital_id=hospital_id) queryset = queryset.order_by('-created_at')[:limit] jobs = [] for job in queryset: jobs.append({ 'job_id': str(job.id), 'name': job.name, 'status': job.status, 'source': job.source, 'progress_percentage': job.progress_percentage, 'total_records': job.total_records, 'success_count': job.success_count, 'failed_count': job.failed_count, 'is_complete': job.is_complete, 'created_at': job.created_at, 'hospital_name': job.hospital.name }) return Response({'jobs': jobs}) except Exception as e: logger.error(f"Error listing import jobs: {str(e)}", exc_info=True) return Response( {'success': False, 'message': f"Server error: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) @api_view(['POST']) @permission_classes([IsPXAdminOrHospitalAdmin]) def trigger_monthly_aggregation(request): """ Trigger monthly aggregation of individual ratings. POST /api/physicians/ratings/aggregate/ Expected payload: { "year": 2024, "month": 12, "hospital_id": "uuid" // optional } Returns: { "success": true, "task_id": "celery-task-id", "message": "Monthly aggregation queued" } """ try: year = request.data.get('year') month = request.data.get('month') hospital_id = request.data.get('hospital_id') if not year or not month: return Response( {'success': False, 'message': 'year and month are required'}, status=status.HTTP_400_BAD_REQUEST ) hospital = None if hospital_id: hospital = get_object_or_404(Hospital, id=hospital_id) # Check permission user = request.user if not user.is_px_admin(): if hospital and hospital != user.hospital: return Response( {'success': False, 'message': 'Permission denied'}, status=status.HTTP_403_FORBIDDEN ) if not hospital and not user.hospital: return Response( {'success': False, 'message': 'hospital_id required'}, status=status.HTTP_400_BAD_REQUEST ) # Queue aggregation task task = aggregate_monthly_ratings_task.delay( year=int(year), month=int(month), hospital_id=str(hospital.id) if hospital else None ) return Response({ 'success': True, 'task_id': task.id, 'message': f'Monthly aggregation queued for {year}-{month:02d}' }) except Exception as e: logger.error(f"Error triggering aggregation: {str(e)}", exc_info=True) return Response( {'success': False, 'message': f"Server error: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # ============================================================================ # Simple HIS-compatible endpoint (similar to patient HIS endpoint) # ============================================================================ @api_view(['POST']) def his_doctor_rating_handler(request): """ HIS Doctor Rating API Endpoint - Compatible with HIS format. This endpoint is designed to be compatible with HIS system integration, accepting data in the same format as the Doctor Rating Report CSV. POST /api/physicians/ratings/his/ Expected payload (single or array): { "hospital_code": "ALHH", "ratings": [ { "UHID": "ALHH.0030223126", "PatientName": "Tamam Saud Aljunaybi", "Gender": "Female", "Age": "36 Years", "Nationality": "Saudi Arabia", "MobileNo": "0504884011", "PatientType": "OP", "AdmitDate": "22-Dec-2024 19:12:24", "DischargeDate": "", "DoctorName": "10738-OMAYMAH YAQOUB ELAMEIAN", "Rating": 5, "Feedback": "Great service", "RatingDate": "28-Dec-2024 22:31:29", "Department": "ACCIDENT AND EMERGENCY" } ], "source_reference": "HIS_BATCH_001" } Or simplified single record: { "hospital_code": "ALHH", "UHID": "ALHH.0030223126", "PatientName": "Tamam Saud Aljunaybi", ... } Returns: { "success": true, "processed": 1, "failed": 0, "results": [...] } """ try: data = request.data # Get hospital code hospital_code = data.get('hospital_code') if not hospital_code: return Response( {'success': False, 'message': 'hospital_code is required'}, status=status.HTTP_400_BAD_REQUEST ) try: hospital = Hospital.objects.get(code__iexact=hospital_code) except Hospital.DoesNotExist: return Response( {'success': False, 'message': f'Hospital with code {hospital_code} not found'}, status=status.HTTP_404_NOT_FOUND ) # Normalize input to list of ratings if 'ratings' in data: ratings_list = data['ratings'] else: # Single record format ratings_list = [data] # Map field names (HIS format -> internal format) field_mapping = { 'UHID': 'uhid', 'PatientName': 'patient_name', 'Gender': 'gender', 'FullAge': 'age', 'Age': 'age', 'Nationality': 'nationality', 'MobileNo': 'mobile_no', 'PatientType': 'patient_type', 'AdmitDate': 'admit_date', 'DischargeDate': 'discharge_date', 'DoctorName': 'doctor_name', 'Rating': 'rating', 'FeedBack': 'feedback', 'Feedback': 'feedback', 'RatingDate': 'rating_date', 'Department': 'department', } results = [] success_count = 0 failed_count = 0 for record in ratings_list: # Map fields mapped_record = {} for his_field, internal_field in field_mapping.items(): if his_field in record: mapped_record[internal_field] = record[his_field] # Process the rating result = DoctorRatingAdapter.process_single_rating( data=mapped_record, hospital=hospital, source=PhysicianIndividualRating.RatingSource.HIS_API, source_reference=data.get('source_reference', '') ) results.append(result) if result['success']: success_count += 1 else: failed_count += 1 return Response({ 'success': True, 'processed': success_count, 'failed': failed_count, 'total': len(ratings_list), 'results': results }) except Exception as e: logger.error(f"Error in HIS doctor rating handler: {str(e)}", exc_info=True) return Response( {'success': False, 'message': f"Server error: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR )