agdar/finance/package_scheduling_service.py
Marwan Alwali 2f1681b18c update
2025-11-11 13:44:48 +03:00

264 lines
9.5 KiB
Python

"""
Package Auto-Scheduling Service.
This service automatically schedules all sessions when a package is purchased,
respecting session order and therapist availability.
"""
from datetime import datetime, timedelta, time
from django.db import transaction
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from finance.models import Package, PackagePurchase, PackageService
from appointments.models import Appointment, Provider, Room
from appointments.room_conflict_service import RoomAvailabilityService
from core.models import Patient, Clinic
class PackageSchedulingError(Exception):
"""Raised when package scheduling fails."""
pass
class PackageSchedulingService:
"""
Service for automatically scheduling all sessions in a package.
Respects session order, therapist availability, and room conflicts.
"""
@staticmethod
def auto_schedule_package(package_purchase, start_date=None, preferred_time=None, preferred_provider=None):
"""
Automatically schedule all sessions for a package purchase.
Args:
package_purchase: PackagePurchase instance
start_date: Optional start date (defaults to tomorrow)
preferred_time: Optional preferred time (defaults to 9:00 AM)
preferred_provider: Optional preferred provider
Returns:
list: Created appointments
Raises:
PackageSchedulingError: If scheduling fails
"""
from datetime import date
if not start_date:
start_date = date.today() + timedelta(days=1)
if not preferred_time:
preferred_time = time(9, 0) # 9:00 AM
# Get package services ordered by session_order
package_services = package_purchase.package.packageservice_set.all().order_by('session_order')
if not package_services.exists():
raise PackageSchedulingError("Package has no services defined")
created_appointments = []
current_date = start_date
current_time = preferred_time
with transaction.atomic():
for package_service in package_services:
service = package_service.service
clinic = service.clinic
# Create appointments for each session
for session_num in range(package_service.sessions):
# Find available provider
provider = preferred_provider
if not provider:
provider = PackageSchedulingService._find_available_provider(
clinic, current_date, current_time, service.duration_minutes
)
if not provider:
raise PackageSchedulingError(
f"No available provider found for {clinic.name_en} on {current_date}"
)
# Find available room
room = PackageSchedulingService._find_available_room(
clinic, current_date, current_time, service.duration_minutes, package_purchase.patient.tenant
)
# Create appointment
appointment = Appointment.objects.create(
patient=package_purchase.patient,
clinic=clinic,
provider=provider,
room=room,
service_type=service.name_en,
scheduled_date=current_date,
scheduled_time=current_time,
duration=service.duration_minutes,
tenant=package_purchase.patient.tenant,
status=Appointment.Status.BOOKED
)
created_appointments.append(appointment)
# Move to next available slot
current_date, current_time = PackageSchedulingService._get_next_slot(
current_date, current_time, service.duration_minutes
)
return created_appointments
@staticmethod
def _find_available_provider(clinic, date, time, duration):
"""Find an available provider for the clinic at the specified time."""
providers = Provider.objects.filter(
specialties=clinic,
is_available=True
)
for provider in providers:
# Check if provider has conflicting appointments
conflicts = Appointment.objects.filter(
provider=provider,
scheduled_date=date,
scheduled_time=time,
status__in=[
Appointment.Status.BOOKED,
Appointment.Status.CONFIRMED,
Appointment.Status.ARRIVED,
Appointment.Status.IN_PROGRESS,
]
)
if not conflicts.exists():
return provider
return None
@staticmethod
def _find_available_room(clinic, date, time, duration, tenant):
"""Find an available room for the clinic at the specified time."""
available_rooms = RoomAvailabilityService.get_available_rooms(
clinic, date, time, duration, tenant
)
return available_rooms.first() if available_rooms.exists() else None
@staticmethod
def _get_next_slot(current_date, current_time, duration):
"""
Get the next available time slot.
Args:
current_date: Current date
current_time: Current time
duration: Duration in minutes
Returns:
tuple: (next_date, next_time)
"""
# Add duration + 30 min buffer
current_datetime = datetime.combine(current_date, current_time)
next_datetime = current_datetime + timedelta(minutes=duration + 30)
# If past 5 PM, move to next day at 9 AM
if next_datetime.time() >= time(17, 0):
next_date = current_date + timedelta(days=1)
# Skip weekends (Friday, Saturday in Saudi Arabia)
while next_date.weekday() in [4, 5]:
next_date += timedelta(days=1)
next_time = time(9, 0)
else:
next_date = next_datetime.date()
next_time = next_datetime.time()
return next_date, next_time
@staticmethod
def reschedule_package_session(appointment, new_date, new_time, reason=""):
"""
Reschedule a package session and update package tracking.
Args:
appointment: Appointment instance
new_date: New date
new_time: New time
reason: Reason for rescheduling
Returns:
Appointment: Updated appointment
"""
# Validate room availability at new time
if appointment.room:
RoomAvailabilityService.validate_room_availability(
appointment.room,
new_date,
new_time,
appointment.duration,
exclude_appointment=appointment
)
# Update appointment
appointment.scheduled_date = new_date
appointment.scheduled_time = new_time
appointment.reschedule_reason = reason
appointment.reschedule_count += 1
appointment.status = Appointment.Status.RESCHEDULED
appointment.save()
return appointment
@staticmethod
def cancel_package_session_with_credit(appointment, reason="", cancelled_by=None):
"""
Cancel a package session and restore credit to package.
Args:
appointment: Appointment instance
reason: Cancellation reason
cancelled_by: User who cancelled
Returns:
bool: True if credit restored
"""
# Find associated package purchase
# This would need to be linked via invoice or other means
# For now, we'll mark the appointment as cancelled
appointment.status = Appointment.Status.CANCELLED
appointment.cancel_reason = reason
appointment.cancelled_by = cancelled_by
appointment.save()
# TODO: Restore session credit to package
# This requires linking appointments to package purchases
# For now, return True to indicate cancellation successful
return True
@staticmethod
def get_package_schedule_summary(package_purchase):
"""
Get a summary of all scheduled sessions for a package.
Args:
package_purchase: PackagePurchase instance
Returns:
dict: Schedule summary
"""
# This would require linking appointments to package purchases
# For now, return basic info
return {
'package': package_purchase.package.name_en,
'patient': str(package_purchase.patient),
'total_sessions': package_purchase.total_sessions,
'sessions_used': package_purchase.sessions_used,
'sessions_remaining': package_purchase.sessions_remaining,
'purchase_date': package_purchase.purchase_date,
'expiry_date': package_purchase.expiry_date,
'status': package_purchase.get_status_display(),
}