""" 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"