""" Transition execution orchestrator for the FSM engine. This module handles the execution of state transitions, coordinating between the registry and transitions without importing StateManager to avoid circular dependencies. StateManager imports from this module and provides its methods as parameters. """ import logging from typing import Any, Dict, Type from django.db.models import Model from fsm.registry import get_state_model_for_entity, transition_registry from fsm.state_models import BaseState from fsm.transitions import TransitionContext logger = logging.getLogger(__name__) def execute_transition_with_state_manager( entity: Model, transition_name: str, transition_data: Dict[str, Any], user, state_manager_class: Type, **context_kwargs, ) -> BaseState: """ Execute a registered transition using StateManager methods passed as parameters. This function is called by StateManager.execute_transition() to avoid circular imports. StateManager imports this module and passes itself as a parameter. Args: entity: The entity to transition transition_name: Name of the registered transition transition_data: Data for the transition (validated by Pydantic) user: User executing the transition state_manager_class: The StateManager class to use for state operations **context_kwargs: Additional context data Returns: The newly created state record Raises: ValueError: If transition is not found or state model is not registered TransitionValidationError: If transition validation fails """ entity_name = entity._meta.model_name.lower() transition_data = transition_data or {} # Get the transition class from registry transition_class = transition_registry.get_transition(entity_name, transition_name) if not transition_class: raise ValueError(f"Transition '{transition_name}' not found for entity '{entity_name}'") # Create transition instance transition = transition_class(**transition_data) # Extract organization_id from context_kwargs if provided, otherwise use entity's org_id organization_id = context_kwargs.pop('organization_id', getattr(entity, 'organization_id', None)) # Create minimal context with just entity for target_state computation minimal_context = TransitionContext( entity=entity, current_user=user, current_state_object=None, current_state=None, target_state=None, # Will be computed organization_id=organization_id, ) # Get target_state (can now use entity from context) target_state = transition.get_target_state(minimal_context) is_side_effect_only = target_state is None if is_side_effect_only: # No state model needed for side-effect only transitions state_model = None current_state_object = None current_state = None else: # Get the state model for the entity state_model = get_state_model_for_entity(entity) if not state_model: raise ValueError(f"No state model registered for entity '{entity_name}'") # Get current state information directly from state model current_state_object = state_model.get_current_state(entity) current_state = current_state_object.state if current_state_object else None # Build full transition context context = TransitionContext( entity=entity, current_user=user, current_state_object=current_state_object, current_state=current_state, target_state=target_state, organization_id=organization_id, **context_kwargs, ) logger.info( 'Executing transition', extra={ 'event': 'fsm.transition_execute', 'entity_type': entity_name, 'entity_id': entity.pk, 'transition_name': transition_name, 'from_state': current_state, 'to_state': target_state, 'user_id': user.id if user else None, }, ) # Execute the transition in phases # Phase 1: Prepare and validate the transition transition_context_data = transition.prepare_and_validate(context) # Merge any additional context_data from TransitionContext # This allows callers to add extra data (e.g., workspace_from_id) to the state record if context.context_data: transition_context_data = {**transition_context_data, **context.context_data} # Phase 2: Create the state record via StateManager methods (skip for side-effect only transitions) if is_side_effect_only: # For side-effect only transitions, execute hooks without creating state records logger.info( 'Executing side-effect only transition', extra={ 'event': 'fsm.side_effect_transition', 'entity_type': entity_name, 'entity_id': entity.pk, 'transition_name': transition_name, }, ) transition.post_transition_hook(context, None) return None # Check if this transition forces state record creation (for audit trails) force_state_record = getattr(transition, '_force_state_record', False) # Use context.reason if provided (caller override), otherwise use transition's default reason = context.reason if context.reason else transition.get_reason(context) success = state_manager_class.transition_state( entity=entity, new_state=target_state, transition_name=transition.transition_name, user=user, context=transition_context_data, reason=reason, force_state_record=force_state_record, ) if not success: raise ValueError(f'Failed to create state record for {transition_name}') # Get the newly created state record via StateManager state_record = state_manager_class.get_current_state_object(entity) # Phase 3: Finalize the transition transition.finalize(context, state_record) return state_record