254 lines
8.5 KiB
Python
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"
|