agdar/appointments/state_machine.py
2025-11-02 14:35:35 +03:00

254 lines
8.5 KiB
Python

"""
Appointment State Machine.
This module defines the valid state transitions for appointments and provides
validation logic to ensure appointments follow the correct lifecycle.
"""
import logging
from typing import Dict, List, Optional, Set
logger = logging.getLogger(__name__)
class AppointmentStateMachine:
"""
State machine for appointment lifecycle management.
Defines valid state transitions and provides validation methods.
"""
# Define valid state transitions
# Format: {current_state: [list of valid next states]}
VALID_TRANSITIONS: Dict[str, List[str]] = {
'BOOKED': ['CONFIRMED', 'RESCHEDULED', 'CANCELLED'],
'CONFIRMED': ['ARRIVED', 'RESCHEDULED', 'CANCELLED', 'NO_SHOW'],
'RESCHEDULED': ['CONFIRMED', 'CANCELLED'],
'ARRIVED': ['IN_PROGRESS', 'CANCELLED'],
'IN_PROGRESS': ['COMPLETED', 'CANCELLED'],
'COMPLETED': [], # Terminal state
'CANCELLED': [], # Terminal state
'NO_SHOW': ['RESCHEDULED'], # Allow rescheduling after no-show
}
# Terminal states (cannot transition from these)
TERMINAL_STATES: Set[str] = {'COMPLETED', 'CANCELLED'}
# States that allow cancellation
CANCELLABLE_STATES: Set[str] = {'BOOKED', 'CONFIRMED', 'RESCHEDULED', 'ARRIVED', 'IN_PROGRESS'}
# States that allow rescheduling
RESCHEDULABLE_STATES: Set[str] = {'BOOKED', 'CONFIRMED', 'RESCHEDULED', 'NO_SHOW'}
@classmethod
def can_transition(cls, from_state: str, to_state: str) -> bool:
"""
Check if transition from one state to another is valid.
Args:
from_state: Current state
to_state: Desired next state
Returns:
bool: True if transition is valid
"""
if from_state not in cls.VALID_TRANSITIONS:
logger.error(f"Invalid state: {from_state}")
return False
return to_state in cls.VALID_TRANSITIONS[from_state]
@classmethod
def validate_transition(cls, from_state: str, to_state: str) -> tuple[bool, str]:
"""
Validate state transition and return detailed result.
Args:
from_state: Current state
to_state: Desired next state
Returns:
tuple: (is_valid, message)
"""
# Check if from_state is valid
if from_state not in cls.VALID_TRANSITIONS:
return False, f"Invalid current state: {from_state}"
# Check if already in terminal state
if from_state in cls.TERMINAL_STATES:
return False, f"Cannot transition from terminal state: {from_state}"
# Check if transition is valid
if to_state not in cls.VALID_TRANSITIONS[from_state]:
valid_states = ', '.join(cls.VALID_TRANSITIONS[from_state])
return False, (
f"Invalid transition from {from_state} to {to_state}. "
f"Valid next states: {valid_states}"
)
return True, f"Valid transition from {from_state} to {to_state}"
@classmethod
def get_valid_next_states(cls, current_state: str) -> List[str]:
"""
Get list of valid next states from current state.
Args:
current_state: Current state
Returns:
list: Valid next states
"""
return cls.VALID_TRANSITIONS.get(current_state, [])
@classmethod
def can_cancel(cls, current_state: str) -> bool:
"""
Check if appointment can be cancelled from current state.
Args:
current_state: Current state
Returns:
bool: True if cancellation is allowed
"""
return current_state in cls.CANCELLABLE_STATES
@classmethod
def can_reschedule(cls, current_state: str) -> bool:
"""
Check if appointment can be rescheduled from current state.
Args:
current_state: Current state
Returns:
bool: True if rescheduling is allowed
"""
return current_state in cls.RESCHEDULABLE_STATES
@classmethod
def is_terminal(cls, state: str) -> bool:
"""
Check if state is terminal (no further transitions).
Args:
state: State to check
Returns:
bool: True if terminal state
"""
return state in cls.TERMINAL_STATES
@classmethod
def get_state_description(cls, state: str) -> str:
"""
Get human-readable description of state.
Args:
state: State to describe
Returns:
str: State description
"""
descriptions = {
'BOOKED': 'Appointment has been booked and is awaiting confirmation',
'CONFIRMED': 'Appointment has been confirmed by patient or staff',
'RESCHEDULED': 'Appointment has been rescheduled to a new time',
'ARRIVED': 'Patient has arrived and checked in',
'IN_PROGRESS': 'Appointment is currently in progress',
'COMPLETED': 'Appointment has been completed',
'CANCELLED': 'Appointment has been cancelled',
'NO_SHOW': 'Patient did not show up for appointment',
}
return descriptions.get(state, f"Unknown state: {state}")
@classmethod
def get_transition_requirements(cls, from_state: str, to_state: str) -> Optional[Dict]:
"""
Get requirements for specific state transition.
Args:
from_state: Current state
to_state: Desired next state
Returns:
dict: Requirements for transition, or None if invalid
"""
if not cls.can_transition(from_state, to_state):
return None
requirements = {
('CONFIRMED', 'ARRIVED'): {
'financial_clearance': True,
'consent_verification': True,
'description': 'Financial clearance and consent verification required'
},
('ARRIVED', 'IN_PROGRESS'): {
'provider_present': True,
'description': 'Provider must be present to start appointment'
},
('IN_PROGRESS', 'COMPLETED'): {
'clinical_notes': False, # Recommended but not required
'description': 'Appointment can be marked as completed'
},
}
return requirements.get((from_state, to_state), {
'description': f'Standard transition from {from_state} to {to_state}'
})
class StateTransitionValidator:
"""
Validator for appointment state transitions with detailed error messages.
"""
@staticmethod
def validate_and_transition(appointment, new_status: str, **kwargs) -> tuple[bool, str]:
"""
Validate and perform state transition.
Args:
appointment: Appointment instance
new_status: Desired new status
**kwargs: Additional validation parameters
Returns:
tuple: (success, message)
"""
current_status = appointment.status
# Validate transition
is_valid, message = AppointmentStateMachine.validate_transition(
current_status, new_status
)
if not is_valid:
logger.warning(
f"Invalid state transition for appointment {appointment.id}: "
f"{current_status} -> {new_status}. Reason: {message}"
)
return False, message
# Check specific requirements
requirements = AppointmentStateMachine.get_transition_requirements(
current_status, new_status
)
if requirements:
# Check financial clearance requirement
if requirements.get('financial_clearance') and not appointment.finance_cleared:
return False, "Financial clearance required before arrival"
# Check consent verification requirement
if requirements.get('consent_verification') and not appointment.consent_verified:
return False, "Consent verification required before arrival"
logger.info(
f"Valid state transition for appointment {appointment.id}: "
f"{current_status} -> {new_status}"
)
return True, f"Transition to {new_status} approved"