Files
multitenetsaas/backend/src/modules/healthcare/services/appointment_service.py
AHMET YILMAZ b3fff546e9
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
project initialization
2025-10-05 02:37:33 +08:00

690 lines
25 KiB
Python

"""
Healthcare Appointment Service
Handles appointment scheduling, management, and healthcare operations
"""
from django.db import transaction, models
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.contrib.auth import get_user_model
from datetime import datetime, timedelta
from core.models.tenant import Tenant
from ..models.patient import Patient
from ..models.appointment import (
Appointment, AppointmentResource, AppointmentNote
)
User = get_user_model()
class AppointmentService:
"""
Service class for managing healthcare appointments
"""
@transaction.atomic
def create_appointment(self, tenant: Tenant, appointment_data: dict, created_by=None) -> Appointment:
"""
Create a new appointment with validation
Args:
tenant: The tenant organization
appointment_data: Appointment information dictionary
created_by: User creating the appointment
Returns:
Appointment: Created appointment instance
Raises:
ValidationError: If appointment data is invalid
"""
try:
# Generate unique appointment ID
appointment_id = self._generate_appointment_id(tenant)
# Extract and validate resources and notes
resources = appointment_data.pop('resources', [])
notes = appointment_data.pop('notes', [])
# Validate appointment time
self._validate_appointment_time(appointment_data)
# Create appointment
appointment = Appointment.objects.create(
tenant=tenant,
appointment_id=appointment_id,
created_by=created_by,
**appointment_data
)
# Create resources
for resource_data in resources:
AppointmentResource.objects.create(
appointment=appointment,
**resource_data
)
# Create notes
for note_data in notes:
AppointmentNote.objects.create(
appointment=appointment,
created_by=created_by,
**note_data
)
return appointment
except Exception as e:
raise ValidationError(f"Failed to create appointment: {str(e)}")
def update_appointment(self, appointment: Appointment, appointment_data: dict, updated_by=None) -> Appointment:
"""
Update appointment information
Args:
appointment: Appointment instance to update
appointment_data: Updated appointment information
updated_by: User updating the appointment
Returns:
Appointment: Updated appointment instance
"""
try:
# Validate appointment time if being updated
if 'appointment_time' in appointment_data or 'appointment_date' in appointment_data:
temp_data = appointment_data.copy()
if 'appointment_time' not in temp_data:
temp_data['appointment_time'] = appointment.appointment_time
if 'appointment_date' not in temp_data:
temp_data['appointment_date'] = appointment.appointment_date
self._validate_appointment_time(temp_data)
# Handle resources updates
if 'resources' in appointment_data:
self._update_resources(appointment, appointment_data.pop('resources'))
# Handle notes updates
if 'notes' in appointment_data:
self._update_notes(appointment, appointment_data.pop('notes'), updated_by)
# Update appointment fields
for field, value in appointment_data.items():
if hasattr(appointment, field):
setattr(appointment, field, value)
appointment.updated_by = updated_by
appointment.save()
return appointment
except Exception as e:
raise ValidationError(f"Failed to update appointment: {str(e)}")
def get_appointment_by_id(self, tenant: Tenant, appointment_id: str) -> Appointment:
"""
Get appointment by ID within tenant
Args:
tenant: Tenant organization
appointment_id: Appointment identifier
Returns:
Appointment: Appointment instance
Raises:
Appointment.DoesNotExist: If appointment not found
"""
return Appointment.objects.get(
tenant=tenant,
appointment_id=appointment_id
)
def get_patient_appointments(self, patient: Patient, start_date=None, end_date=None, status=None):
"""
Get appointments for a specific patient
Args:
patient: Patient instance
start_date: Start date filter
end_date: End date filter
status: Status filter
Returns:
QuerySet: Filtered appointments
"""
queryset = Appointment.objects.filter(patient=patient)
if start_date:
queryset = queryset.filter(appointment_date__gte=start_date)
if end_date:
queryset = queryset.filter(appointment_date__lte=end_date)
if status:
queryset = queryset.filter(status=status)
return queryset.order_by('appointment_date', 'appointment_time')
def get_doctor_appointments(self, doctor: User, start_date=None, end_date=None, status=None):
"""
Get appointments for a specific doctor
Args:
doctor: Doctor user instance
start_date: Start date filter
end_date: End date filter
status: Status filter
Returns:
QuerySet: Filtered appointments
"""
queryset = Appointment.objects.filter(doctor=doctor)
if start_date:
queryset = queryset.filter(appointment_date__gte=start_date)
if end_date:
queryset = queryset.filter(appointment_date__lte=end_date)
if status:
queryset = queryset.filter(status=status)
return queryset.order_by('appointment_date', 'appointment_time')
def get_available_time_slots(self, tenant: Tenant, doctor: User, date: datetime.date):
"""
Get available time slots for a doctor on a specific date
Args:
tenant: Tenant organization
doctor: Doctor user instance
date: Date to check availability
Returns:
list: Available time slots
"""
# Get existing appointments for the day
existing_appointments = Appointment.objects.filter(
tenant=tenant,
doctor=doctor,
appointment_date=date,
status__in=['scheduled', 'confirmed', 'in_progress']
)
# Define working hours (9 AM to 5 PM)
working_hours_start = datetime.time(9, 0)
working_hours_end = datetime.time(17, 0)
slot_duration = 30 # 30-minute slots
available_slots = []
current_time = datetime.combine(date, working_hours_start)
end_time = datetime.combine(date, working_hours_end)
while current_time < end_time:
slot_end = current_time + timedelta(minutes=slot_duration)
# Check if slot is available
is_available = True
for appointment in existing_appointments:
appt_start = datetime.combine(date, appointment.appointment_time)
appt_end = appt_start + timedelta(minutes=appointment.duration)
if not (slot_end <= appt_start or current_time >= appt_end):
is_available = False
break
if is_available:
available_slots.append({
'start_time': current_time.time(),
'end_time': slot_end.time(),
'duration': slot_duration
})
current_time = slot_end
return available_slots
def check_appointment_conflicts(self, tenant: Tenant, doctor: User, appointment_date: datetime.date,
appointment_time: datetime.time, duration: int, exclude_appointment=None):
"""
Check for appointment conflicts
Args:
tenant: Tenant organization
doctor: Doctor user instance
appointment_date: Appointment date
appointment_time: Appointment time
duration: Appointment duration in minutes
exclude_appointment: Appointment to exclude from conflict check
Returns:
list: List of conflicting appointments
"""
start_datetime = datetime.combine(appointment_date, appointment_time)
end_datetime = start_datetime + timedelta(minutes=duration)
# Get overlapping appointments
conflicting_appointments = Appointment.objects.filter(
tenant=tenant,
doctor=doctor,
appointment_date=appointment_date,
status__in=['scheduled', 'confirmed', 'in_progress']
)
if exclude_appointment:
conflicting_appointments = conflicting_appointments.exclude(id=exclude_appointment.id)
conflicts = []
for appointment in conflicting_appointments:
appt_start = datetime.combine(appointment.appointment_date, appointment.appointment_time)
appt_end = appt_start + timedelta(minutes=appointment.duration)
# Check for overlap
if not (end_datetime <= appt_start or start_datetime >= appt_end):
conflicts.append(appointment)
return conflicts
def schedule_appointment(self, tenant: Tenant, patient: Patient, doctor: User,
appointment_date: datetime.date, appointment_time: datetime.time,
duration: int = 30, appointment_type: str = 'consultation',
reason_for_visit: str = '', created_by=None) -> Appointment:
"""
Schedule a new appointment with conflict checking
Args:
tenant: Tenant organization
patient: Patient instance
doctor: Doctor user instance
appointment_date: Appointment date
appointment_time: Appointment time
duration: Appointment duration in minutes
appointment_type: Type of appointment
reason_for_visit: Reason for visit
created_by: User creating the appointment
Returns:
Appointment: Created appointment
Raises:
ValidationError: If there are scheduling conflicts
"""
# Check for conflicts
conflicts = self.check_appointment_conflicts(
tenant, doctor, appointment_date, appointment_time, duration
)
if conflicts:
conflict_times = [f"{conf.appointment_time} ({conf.duration}min)" for conf in conflicts]
raise ValidationError(
f"Scheduling conflict with appointments at: {', '.join(conflict_times)}"
)
# Create appointment
appointment_data = {
'patient': patient,
'doctor': doctor,
'appointment_date': appointment_date,
'appointment_time': appointment_time,
'duration': duration,
'appointment_type': appointment_type,
'reason_for_visit': reason_for_visit,
'status': 'scheduled'
}
return self.create_appointment(tenant, appointment_data, created_by)
def reschedule_appointment(self, appointment: Appointment, new_date: datetime.date,
new_time: datetime.time, rescheduled_by=None):
"""
Reschedule an existing appointment
Args:
appointment: Appointment to reschedule
new_date: New appointment date
new_time: New appointment time
rescheduled_by: User rescheduling the appointment
Returns:
Appointment: Updated appointment
Raises:
ValidationError: If there are scheduling conflicts
"""
# Check for conflicts
conflicts = self.check_appointment_conflicts(
appointment.tenant, appointment.doctor, new_date, new_time,
appointment.duration, exclude_appointment=appointment
)
if conflicts:
conflict_times = [f"{conf.appointment_time} ({conf.duration}min)" for conf in conflicts]
raise ValidationError(
f"Scheduling conflict with appointments at: {', '.join(conflict_times)}"
)
# Create a new appointment record
old_appointment_data = {
'patient': appointment.patient,
'doctor': appointment.doctor,
'appointment_date': new_date,
'appointment_time': new_time,
'duration': appointment.duration,
'appointment_type': appointment.appointment_type,
'reason_for_visit': appointment.reason_for_visit,
'status': 'scheduled',
'notes': f'Rescheduled from {appointment.appointment_date} at {appointment.appointment_time}',
'rescheduled_from': appointment
}
new_appointment = self.create_appointment(
appointment.tenant, old_appointment_data, rescheduled_by
)
# Cancel the old appointment
appointment.cancel_appointment('Rescheduled', rescheduled_by)
return new_appointment
def cancel_appointment(self, appointment: Appointment, reason: str, cancelled_by=None):
"""
Cancel an appointment
Args:
appointment: Appointment to cancel
reason: Cancellation reason
cancelled_by: User cancelling the appointment
"""
if not appointment.can_be_cancelled():
raise ValidationError("This appointment cannot be cancelled")
appointment.cancel_appointment(reason, cancelled_by)
def send_appointment_reminders(self, tenant: Tenant, hours_before: int = 24):
"""
Send appointment reminders
Args:
tenant: Tenant organization
hours_before: Hours before appointment to send reminder
Returns:
dict: Reminder sending statistics
"""
reminder_time = timezone.now() + timedelta(hours=hours_before)
# Get appointments that need reminders
appointments = Appointment.objects.filter(
tenant=tenant,
appointment_date=reminder_time.date(),
status__in=['scheduled', 'confirmed'],
reminder_sent=False
).filter(
models.Q(appointment_time__hour=reminder_time.hour) |
models.Q(appointment_time__hour=reminder_time.hour - 1)
)
sent_count = 0
failed_count = 0
for appointment in appointments:
try:
# Send reminder (integrate with email/SMS service)
appointment.send_reminder()
sent_count += 1
except Exception as e:
failed_count += 1
# Log error
print(f"Failed to send reminder for appointment {appointment.appointment_id}: {e}")
return {
'total_appointments': appointments.count(),
'sent': sent_count,
'failed': failed_count
}
def get_appointment_statistics(self, tenant: Tenant, start_date=None, end_date=None):
"""
Get appointment statistics for a tenant
Args:
tenant: Tenant organization
start_date: Start date for statistics
end_date: End date for statistics
Returns:
dict: Appointment statistics
"""
queryset = Appointment.objects.filter(tenant=tenant)
if start_date:
queryset = queryset.filter(appointment_date__gte=start_date)
if end_date:
queryset = queryset.filter(appointment_date__lte=end_date)
# Basic counts
total_appointments = queryset.count()
completed_appointments = queryset.filter(status='completed').count()
cancelled_appointments = queryset.filter(status='cancelled').count()
no_show_appointments = queryset.filter(status='no_show').count()
# Status distribution
status_distribution = {}
for status_choice in Appointment.STATUS_CHOICES:
status_code = status_choice[0]
count = queryset.filter(status=status_code).count()
status_distribution[status_code] = {
'name': status_choice[1],
'count': count,
'percentage': (count / total_appointments * 100) if total_appointments > 0 else 0
}
# Type distribution
type_distribution = {}
for type_choice in Appointment.APPOINTMENT_TYPE_CHOICES:
type_code = type_choice[0]
count = queryset.filter(appointment_type=type_code).count()
type_distribution[type_code] = {
'name': type_choice[1],
'count': count,
'percentage': (count / total_appointments * 100) if total_appointments > 0 else 0
}
# Doctor workload
doctor_workload = {}
doctors = User.objects.filter(
id__in=queryset.values_list('doctor', flat=True)
).distinct()
for doctor in doctors:
doctor_appointments = queryset.filter(doctor=doctor)
doctor_workload[doctor.id] = {
'name': f"{doctor.first_name} {doctor.last_name}",
'total_appointments': doctor_appointments.count(),
'completed_appointments': doctor_appointments.filter(status='completed').count(),
'average_duration': doctor_appointments.aggregate(
avg_duration=models.Avg('duration')
)['avg_duration'] or 0
}
# Daily trends
daily_trends = {}
if start_date and end_date:
current_date = start_date
while current_date <= end_date:
day_count = queryset.filter(appointment_date=current_date).count()
daily_trends[current_date.strftime('%Y-%m-%d')] = day_count
current_date += timedelta(days=1)
return {
'total_appointments': total_appointments,
'completed_appointments': completed_appointments,
'cancelled_appointments': cancelled_appointments,
'no_show_appointments': no_show_appointments,
'completion_rate': (completed_appointments / total_appointments * 100) if total_appointments > 0 else 0,
'cancellation_rate': (cancelled_appointments / total_appointments * 100) if total_appointments > 0 else 0,
'no_show_rate': (no_show_appointments / total_appointments * 100) if total_appointments > 0 else 0,
'status_distribution': status_distribution,
'type_distribution': type_distribution,
'doctor_workload': doctor_workload,
'daily_trends': daily_trends
}
def check_in_patient(self, appointment: Appointment, checked_in_by=None):
"""
Check in patient for appointment
Args:
appointment: Appointment instance
checked_in_by: User checking in the patient
"""
appointment.check_in(checked_in_by)
def start_appointment(self, appointment: Appointment, started_by=None):
"""
Start appointment
Args:
appointment: Appointment instance
started_by: User starting the appointment
"""
appointment.start_appointment(started_by)
def complete_appointment(self, appointment: Appointment, completed_by=None,
diagnosis: str = '', treatment_plan: str = '', notes: str = ''):
"""
Complete appointment
Args:
appointment: Appointment instance
completed_by: User completing the appointment
diagnosis: Diagnosis information
treatment_plan: Treatment plan
notes: Additional notes
"""
# Update appointment with completion details
if diagnosis:
appointment.diagnosis = diagnosis
if treatment_plan:
appointment.treatment_plan = treatment_plan
if notes:
appointment.notes = notes
appointment.complete_appointment(completed_by)
def add_appointment_note(self, appointment: Appointment, note_type: str, title: str,
content: str, created_by=None, is_confidential=False):
"""
Add note to appointment
Args:
appointment: Appointment instance
note_type: Type of note
title: Note title
content: Note content
created_by: User creating the note
is_confidential: Whether note is confidential
"""
AppointmentNote.objects.create(
appointment=appointment,
note_type=note_type,
title=title,
content=content,
is_confidential=is_confidential,
created_by=created_by
)
def _generate_appointment_id(self, tenant: Tenant) -> str:
"""
Generate unique appointment ID for tenant
Args:
tenant: Tenant organization
Returns:
str: Unique appointment ID
"""
# Use tenant slug + date + sequence
tenant_slug = tenant.slug.upper()
today = timezone.now().strftime('%Y%m%d')
# Get today's appointment count
today_count = Appointment.objects.filter(
tenant=tenant,
appointment_date=timezone.now().date()
).count()
sequence = today_count + 1
return f"{tenant_slug}-{today}-{sequence:03d}"
def _validate_appointment_time(self, appointment_data: dict):
"""
Validate appointment time
Args:
appointment_data: Appointment data dictionary
Raises:
ValidationError: If appointment time is invalid
"""
appointment_date = appointment_data.get('appointment_date')
appointment_time = appointment_data.get('appointment_time')
if not appointment_date or not appointment_time:
raise ValidationError("Appointment date and time are required")
# Check if appointment is in the past
appointment_datetime = datetime.combine(appointment_date, appointment_time)
if appointment_datetime < timezone.now():
raise ValidationError("Appointment cannot be scheduled in the past")
# Check if appointment is outside working hours (9 AM to 6 PM)
if appointment_time.hour < 9 or appointment_time.hour >= 18:
raise ValidationError("Appointment must be scheduled between 9 AM and 6 PM")
def _update_resources(self, appointment: Appointment, resources_data: list):
"""
Update resources for appointment
Args:
appointment: Appointment instance
resources_data: List of resource data
"""
# Remove existing resources not in the update
existing_ids = [resource.get('id') for resource in resources_data if 'id' in resource]
appointment.resources.exclude(id__in=existing_ids).delete()
# Update or create resources
for resource_data in resources_data:
resource_id = resource_data.pop('id', None)
if resource_id:
try:
resource = appointment.resources.get(id=resource_id)
for field, value in resource_data.items():
setattr(resource, field, value)
resource.save()
except AppointmentResource.DoesNotExist:
AppointmentResource.objects.create(
appointment=appointment,
**resource_data
)
else:
AppointmentResource.objects.create(
appointment=appointment,
**resource_data
)
def _update_notes(self, appointment: Appointment, notes_data: list, created_by=None):
"""
Update notes for appointment
Args:
appointment: Appointment instance
notes_data: List of note data
created_by: User creating notes
"""
# Add new notes (notes are not updated, only added)
for note_data in notes_data:
if 'id' not in note_data: # Only create new notes
AppointmentNote.objects.create(
appointment=appointment,
created_by=created_by,
**note_data
)