kaauh_ats/recruitment/erp_integration_service.py
2025-12-10 13:56:51 +03:00

271 lines
10 KiB
Python

import json
import logging
from datetime import datetime
from typing import Dict, Any, Optional
from django.http import HttpRequest
from .models import Source, JobPosting, IntegrationLog
logger = logging.getLogger(__name__)
class ERPIntegrationService:
"""
Service to handle integration between external ERP system and ATS
"""
def __init__(self, source: Source):
self.source = source
self.logger = logging.getLogger(f'{__name__}.{source.name}')
def validate_request(self, request: HttpRequest) -> tuple[bool, str]:
"""
Validate the incoming request from ERP system
Returns: (is_valid, error_message)
"""
# Check if source is active
if not self.source.is_active:
return False, "Source is not active"
# Check if trusted IPs are configured and validate request IP
if self.source.trusted_ips:
client_ip = self.get_client_ip(request)
trusted_ips = [ip.strip() for ip in self.source.trusted_ips.split(',')]
if client_ip not in trusted_ips:
self.logger.warning(f"Request from untrusted IP: {client_ip}")
return False, f"Request from untrusted IP: {client_ip}"
# Check API key if provided
if self.source.api_key:
api_key = request.headers.get('X-API-Key') or request.GET.get('api_key')
if not api_key or api_key != self.source.api_key:
self.logger.warning("Invalid or missing API key")
return False, "Invalid or missing API key"
return True, ""
def log_integration_request(self, request: HttpRequest, action: str, **kwargs):
"""
Log the integration request/response
"""
IntegrationLog.objects.create(
source=self.source,
action=action,
endpoint=request.path,
method=request.method,
request_data=self.get_request_data(request),
ip_address=self.get_client_ip(request),
user_agent=request.META.get('HTTP_USER_AGENT', ''),
**kwargs
)
def create_job_from_erp(self, request_data: Dict[str, Any]) -> tuple[Optional[JobPosting], str]:
"""
Create a JobPosting from ERP request data
Returns: (job, error_message)
"""
try:
# Map ERP fields to JobPosting fields
job_data = {
'internal_job_id': request_data.get('job_id', '').strip(),
'title': request_data.get('title', '').strip(),
'department': request_data.get('department', '').strip(),
'job_type': self.map_job_type(request_data.get('job_type', 'FULL_TIME')),
'workplace_type': self.map_workplace_type(request_data.get('workplace_type', 'ON_SITE')),
'location_city': request_data.get('location_city', '').strip(),
'location_state': request_data.get('location_state', '').strip(),
'location_country': request_data.get('location_country', 'United States').strip(),
'description': request_data.get('description', '').strip(),
'qualifications': request_data.get('qualifications', '').strip(),
'salary_range': request_data.get('salary_range', '').strip(),
'benefits': request_data.get('benefits', '').strip(),
'application_url': request_data.get('application_url', '').strip(),
'application_deadline': self.parse_date(request_data.get('application_deadline')),
'application_instructions': request_data.get('application_instructions', '').strip(),
'created_by': f'ERP Integration: {self.source.name}',
'status': 'DRAFT' if request_data.get('auto_publish', False) else 'DRAFT',
'source': self.source
}
# Validate required fields
if not job_data['title']:
return None, "Job title is required"
# Create the job
job = JobPosting(**job_data)
job.save()
self.logger.info(f"Created job {job.internal_job_id} from ERP integration")
return job, ""
except Exception as e:
error_msg = f"Error creating job from ERP: {str(e)}"
self.logger.error(error_msg)
return None, error_msg
def update_job_from_erp(self, job_id: str, request_data: Dict[str, Any]) -> tuple[Optional[JobPosting], str]:
"""
Update an existing JobPosting from ERP request data
Returns: (job, error_message)
"""
try:
job = JobPosting.objects.get(internal_job_id=job_id)
# Update fields from ERP data
updatable_fields = [
'title', 'department', 'job_type', 'workplace_type',
'location_city', 'location_state', 'location_country',
'description', 'qualifications', 'salary_range', 'benefits',
'application_url', 'application_deadline', 'application_instructions',
'status'
]
for field in updatable_fields:
if field in request_data:
value = request_data[field]
# Special handling for date fields
if field == 'application_deadline':
value = self.parse_date(value)
setattr(job, field, value)
# Update source if provided
if 'source_id' in request_data:
try:
source = Source.objects.get(id=request_data['source_id'])
job.source = source
except Source.DoesNotExist:
pass
job.save()
self.logger.info(f"Updated job {job.internal_job_id} from ERP integration")
return job, ""
except JobPosting.DoesNotExist:
return None, f"Job with ID {job_id} not found"
except Exception as e:
error_msg = f"Error updating job from ERP: {str(e)}"
self.logger.error(error_msg)
return None, error_msg
def validate_erp_data(self, data: Dict[str, Any]) -> tuple[bool, str]:
"""
Validate ERP request data structure
Returns: (is_valid, error_message)
"""
required_fields = ['title']
for field in required_fields:
if field not in data or not data[field]:
return False, f"Required field '{field}' is missing or empty"
# Validate URL format
if data.get('application_url'):
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError as DjangoValidationError
try:
URLValidator()(data['application_url'])
except DjangoValidationError:
return False, "Invalid application URL format"
# Validate job type
if 'job_type' in data and data['job_type']:
valid_job_types = dict(JobPosting.JOB_TYPES)
if data['job_type'] not in valid_job_types:
return False, f"Invalid job type: {data['job_type']}"
# Validate workplace type
if 'workplace_type' in data and data['workplace_type']:
valid_workplace_types = dict(JobPosting.WORKPLACE_TYPES)
if data['workplace_type'] not in valid_workplace_types:
return False, f"Invalid workplace type: {data['workplace_type']}"
return True, ""
# Helper methods
def get_client_ip(self, request: HttpRequest) -> str:
"""Get the client IP address from request"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def get_request_data(self, request: HttpRequest) -> Dict[str, Any]:
"""Get request data from request object"""
if request.method == 'GET':
return dict(request.GET)
elif request.method in ['POST', 'PUT', 'PATCH']:
try:
if request.content_type == 'application/json':
return json.loads(request.body.decode('utf-8'))
else:
return dict(request.POST)
except:
return {}
return {}
def parse_date(self, date_str: str) -> Optional[datetime.date]:
"""Parse date string from ERP"""
if not date_str:
return None
try:
# Try different date formats
date_formats = [
'%Y-%m-%d',
'%m/%d/%Y',
'%d/%m/%Y',
'%Y-%m-%d %H:%M:%S',
'%m/%d/%Y %H:%M:%S',
'%d/%m/%Y %H:%M:%S'
]
for fmt in date_formats:
try:
dt = datetime.strptime(date_str, fmt)
if fmt.endswith('%H:%M:%S'):
return dt.date()
return dt.date()
except ValueError:
continue
# If no format matches, try to parse with dateutil
from dateutil import parser
dt = parser.parse(date_str)
return dt.date()
except Exception as e:
self.logger.warning(f"Could not parse date '{date_str}': {str(e)}")
return None
def map_job_type(self, erp_job_type: str) -> str:
"""Map ERP job type to ATS job type"""
mapping = {
'full-time': 'FULL_TIME',
'part-time': 'PART_TIME',
'contract': 'CONTRACT',
'internship': 'INTERNSHIP',
'faculty': 'FACULTY',
'temporary': 'TEMPORARY',
}
return mapping.get(erp_job_type.lower(), 'FULL_TIME')
def map_workplace_type(self, erp_workplace_type: str) -> str:
"""Map ERP workplace type to ATS workplace type"""
mapping = {
'onsite': 'ON_SITE',
'on-site': 'ON_SITE',
'remote': 'REMOTE',
'hybrid': 'HYBRID',
}
return mapping.get(erp_workplace_type.lower(), 'ON_SITE')