Class: LcpRuby::Workflow::TransitionExecutor

Inherits:
Object
  • Object
show all
Defined in:
lib/lcp_ruby/workflow/transition_executor.rb

Constant Summary collapse

TRANSITION_ACTIVE_IVAR =
:@_workflow_transition_active

Class Method Summary collapse

Class Method Details

.execute(record, transition_name, user:, evaluator:, comment: nil, triggered_by: nil, defer_events: false) ⇒ TransitionResult

Executes a workflow transition on a record.

Returns:



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/lcp_ruby/workflow/transition_executor.rb', line 17

def self.execute(record, transition_name, user:, evaluator:, comment: nil, triggered_by: nil, defer_events: false)
  workflow_def = resolve_workflow(record)
  machine = StateMachine.new(workflow_def)

  # 1. Early validation (fast-fail before locking)
  machine.validate_transition!(record, transition_name, user: user, evaluator: evaluator)

  transition = workflow_def.transition(transition_name)

  # 2. Check require_comment (static check, doesn't need lock)
  if transition.require_comment && (comment.nil? || comment.to_s.strip.empty?)
    raise CommentRequiredError,
      "Transition '#{transition_name}' requires a comment"
  end

  to_state = transition.to
  from_state = nil
  deferred = []

  # Wrap entire execution in Current.set so Current.user is available
  # for all downstream code (events, audit, userstamps) including system triggers.
  LcpRuby::Current.set(user: user) do
    # 3. Wrap in transaction with row lock
    record.class.transaction do
      # 3a. Acquire row lock and reload fresh data
      record.lock!

      from_state = record.send(workflow_def.field).to_s

      # 3b. Re-validate after lock (state may have changed concurrently)
      machine.validate_transition!(record, transition_name, user: user, evaluator: evaluator)

      # 3c. Resolve and apply set_fields (after lock, with fresh record data)
      transition.set_fields.each do |field_name, expression|
        value = ValueResolver.resolve(expression, record: record, user: user)
        if record.respond_to?("#{field_name}=")
          record.send("#{field_name}=", value)
        end
      end

      # 4. Assign workflow field
      record.send("#{workflow_def.field}=", to_state)

      # 5. Set flag to bypass before_save guard
      set_transition_active(record)

      # 6. Save
      record.save!

      # 7. Audit log
      if AuditRegistry.available? && workflow_def.audit_log
        AuditWriter.log(
          record: record,
          workflow_definition: workflow_def,
          transition: transition,
          from_state: from_state,
          to_state: to_state,
          user: user,
          comment: comment,
          metadata: { triggered_by: triggered_by }
        )
      end
    end

    # 8. Fire events or collect them for deferred dispatch
    if defer_events
      deferred = collect_events(workflow_def, record, transition, from_state, to_state)
    else
      dispatch_events(workflow_def, record, transition, from_state, to_state)
      deferred = []
    end
  end

  ActiveSupport::Notifications.instrument("workflow_transition.lcp_ruby", {
    model: record.class.name.demodulize.underscore,
    from_state: from_state,
    to_state: to_state
  })

  TransitionResult.new(
    success: true,
    transition_name: transition_name,
    from_state: from_state,
    to_state: to_state,
    record: record,
    deferred_events: deferred
  )
ensure
  reset_transition_flag(record) if record
end

.reset_transition_flag(record) ⇒ Object



10
11
12
# File 'lib/lcp_ruby/workflow/transition_executor.rb', line 10

def self.reset_transition_flag(record)
  record.instance_variable_set(TRANSITION_ACTIVE_IVAR, false)
end

.set_transition_active(record, value = true) ⇒ Object



6
7
8
# File 'lib/lcp_ruby/workflow/transition_executor.rb', line 6

def self.set_transition_active(record, value = true)
  record.instance_variable_set(TRANSITION_ACTIVE_IVAR, value)
end