import json import logging import requests from datetime import datetime from typing import Dict, Any, List, Optional, Tuple from django.utils import timezone from django.conf import settings from django.core.files.base import ContentFile from django.http import HttpRequest from .models import Source, Candidate, JobPosting, IntegrationLog logger = logging.getLogger(__name__) class CandidateSyncService: """ Service to handle synchronization of hired candidates to external sources """ def __init__(self): self.logger = logging.getLogger(__name__) def sync_hired_candidates_to_all_sources(self, job: JobPosting) -> Dict[str, Any]: """ Sync all hired candidates for a job to all active external sources Returns: Dictionary with sync results for each source """ results = { 'total_candidates': 0, 'successful_syncs': 0, 'failed_syncs': 0, 'source_results': {}, 'sync_time': timezone.now().isoformat() } # Get all hired candidates for this job hired_candidates = list(job.hired_applications.select_related('job')) results['total_candidates'] = len(hired_candidates) if not hired_candidates: self.logger.info(f"No hired candidates found for job {job.title}") return results # Get all active sources that support outbound sync active_sources = Source.objects.filter( is_active=True, sync_endpoint__isnull=False ).exclude(sync_endpoint='') if not active_sources: self.logger.warning("No active sources with sync endpoints configured") return results # Sync to each source for source in active_sources: try: source_result = self.sync_to_source(source, hired_candidates, job) results['source_results'][source.name] = source_result if source_result['success']: results['successful_syncs'] += 1 else: results['failed_syncs'] += 1 except Exception as e: error_msg = f"Unexpected error syncing to {source.name}: {str(e)}" self.logger.error(error_msg) results['source_results'][source.name] = { 'success': False, 'error': error_msg, 'candidates_synced': 0 } results['failed_syncs'] += 1 return results def sync_to_source(self, source: Source, candidates: List[Candidate], job: JobPosting) -> Dict[str, Any]: """ Sync candidates to a specific external source Returns: Dictionary with sync result for this source """ result = { 'success': False, 'error': None, 'candidates_synced': 0, 'candidates_failed': 0, 'candidate_results': [] } try: # Prepare headers for the request headers = self._prepare_headers(source) # Sync each candidate for candidate in candidates: try: candidate_data = self._format_candidate_data(candidate, job) sync_result = self._send_candidate_to_source(source, candidate_data, headers) result['candidate_results'].append({ 'candidate_id': candidate.id, 'candidate_name': candidate.name, 'success': sync_result['success'], 'error': sync_result.get('error'), 'response_data': sync_result.get('response_data') }) if sync_result['success']: result['candidates_synced'] += 1 else: result['candidates_failed'] += 1 except Exception as e: error_msg = f"Error syncing candidate {candidate.name}: {str(e)}" self.logger.error(error_msg) result['candidate_results'].append({ 'candidate_id': candidate.id, 'candidate_name': candidate.name, 'success': False, 'error': error_msg }) result['candidates_failed'] += 1 # Consider sync successful if at least one candidate was synced result['success'] = result['candidates_synced'] > 0 # Log the sync operation self._log_sync_operation(source, result, len(candidates)) except Exception as e: error_msg = f"Failed to sync to source {source.name}: {str(e)}" self.logger.error(error_msg) result['error'] = error_msg return result def _prepare_headers(self, source: Source) -> Dict[str, str]: """Prepare HTTP headers for the sync request""" headers = { 'Content-Type': 'application/json', 'User-Agent': f'KAAUH-ATS-Sync/1.0' } # Add API key if configured if source.api_key: headers['X-API-Key'] = source.api_key # Add custom headers if any if hasattr(source, 'custom_headers') and source.custom_headers: try: custom_headers = json.loads(source.custom_headers) headers.update(custom_headers) except json.JSONDecodeError: self.logger.warning(f"Invalid custom_headers JSON for source {source.name}") return headers def _format_candidate_data(self, candidate: Candidate, job: JobPosting) -> Dict[str, Any]: """Format candidate data for external source""" data = { 'candidate': { 'id': candidate.id, 'slug': candidate.slug, 'first_name': candidate.first_name, 'last_name': candidate.last_name, 'full_name': candidate.name, 'email': candidate.email, 'phone': candidate.phone, 'address': candidate.address, # 'applied_at': candidate.created_at.isoformat(), # 'hired_date': candidate.offer_date.isoformat() if candidate.offer_date else None, # 'join_date': candidate.join_date.isoformat() if candidate.join_date else None, }, # 'job': { # 'id': job.id, # 'internal_job_id': job.internal_job_id, # 'title': job.title, # 'department': job.department, # 'job_type': job.job_type, # 'workplace_type': job.workplace_type, # 'location': job.get_location_display(), # }, # 'ai_analysis': { # 'match_score': candidate.match_score, # 'years_of_experience': candidate.years_of_experience, # 'screening_rating': candidate.screening_stage_rating, # 'professional_category': candidate.professional_category, # 'top_skills': candidate.top_3_keywords, # 'strengths': candidate.strengths, # 'weaknesses': candidate.weaknesses, # 'recommendation': candidate.recommendation, # 'job_fit_narrative': candidate.job_fit_narrative, # }, # 'sync_metadata': { # 'synced_at': timezone.now().isoformat(), # 'sync_source': 'KAAUH-ATS', # 'sync_version': '1.0' # } } # # Add resume information if available # if candidate.resume: # data['candidate']['resume'] = { # 'filename': candidate.resume.name, # 'size': candidate.resume.size, # 'url': candidate.resume.url if hasattr(candidate.resume, 'url') else None # } # # Add additional AI analysis data if available # if candidate.ai_analysis_data: # data['ai_analysis']['full_analysis'] = candidate.ai_analysis_data return data def _send_candidate_to_source(self, source: Source, candidate_data: Dict[str, Any], headers: Dict[str, str]) -> Dict[str, Any]: """ Send candidate data to external source Returns: Dictionary with send result """ result = { 'success': False, 'error': None, 'response_data': None, 'status_code': None } try: # Determine HTTP method (default to POST) method = getattr(source, 'sync_method', 'POST').upper() # Prepare request data json_data = json.dumps(candidate_data) # Make the HTTP request if method == 'POST': response = requests.post( source.sync_endpoint, data=json_data, headers=headers, timeout=30 ) elif method == 'PUT': response = requests.put( source.sync_endpoint, data=json_data, headers=headers, timeout=30 ) else: raise ValueError(f"Unsupported HTTP method: {method}") result['status_code'] = response.status_code result['response_data'] = response.text # Check if request was successful if response.status_code in [200, 201, 202]: try: response_json = response.json() result['response_data'] = response_json result['success'] = True except json.JSONDecodeError: # If response is not JSON, still consider it successful if status code is good result['success'] = True else: result['error'] = f"HTTP {response.status_code}: {response.text}" except requests.exceptions.Timeout: result['error'] = "Request timeout" except requests.exceptions.ConnectionError: result['error'] = "Connection error" except requests.exceptions.RequestException as e: result['error'] = f"Request error: {str(e)}" except Exception as e: result['error'] = f"Unexpected error: {str(e)}" return result def _log_sync_operation(self, source: Source, result: Dict[str, Any], total_candidates: int): """Log the sync operation to IntegrationLog""" try: IntegrationLog.objects.create( source=source, action='SYNC', endpoint=source.sync_endpoint, method=getattr(source, 'sync_method', 'POST'), request_data={ 'total_candidates': total_candidates, 'candidates_synced': result['candidates_synced'], 'candidates_failed': result['candidates_failed'] }, response_data=result, status_code='200' if result['success'] else '400', error_message=result.get('error'), ip_address='127.0.0.1', # Internal sync user_agent='KAAUH-ATS-Sync/1.0' ) except Exception as e: self.logger.error(f"Failed to log sync operation: {str(e)}") def test_source_connection(self, source: Source) -> Dict[str, Any]: """ Test connection to an external source Returns: Dictionary with test result """ result = { 'success': False, 'error': None, 'response_time': None, 'status_code': None } try: headers = self._prepare_headers(source) test_data = { 'test': True, 'timestamp': timezone.now().isoformat(), 'source': 'KAAUH-ATS Connection Test' } start_time = datetime.now() # Use GET method for testing if available, otherwise POST test_method = getattr(source, 'test_method', 'GET').upper() if test_method == 'GET': response = requests.get( source.sync_endpoint, headers=headers, timeout=10 ) else: response = requests.post( source.sync_endpoint, data=json.dumps(test_data), headers=headers, timeout=10 ) end_time = datetime.now() result['response_time'] = (end_time - start_time).total_seconds() result['status_code'] = response.status_code if response.status_code in [200, 201, 202]: result['success'] = True else: result['error'] = f"HTTP {response.status_code}: {response.text}" except requests.exceptions.Timeout: result['error'] = "Connection timeout" except requests.exceptions.ConnectionError: result['error'] = "Connection failed" except Exception as e: result['error'] = f"Test failed: {str(e)}" return result