Class: Activity::ChainRunner

Inherits:
Object
  • Object
show all
Defined in:
app/services/activity/chain_runner.rb

Overview

Service object: chain runner.

Defined Under Namespace

Classes: NoActivityResultError, NoActivityTypeError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(activity, options = {}) ⇒ void

Parameters:

  • activity (Activity)

    the activity whose chains will be executed

  • options (Hash) (defaults to: {})

    chain runner options

Options Hash (options):

  • new_activity_result_type (ActivityResultType)

    result type overriding the activity's current one

  • logger (Logger)

    logger to use (defaults to Rails.logger)

Raises:



33
34
35
36
37
38
39
40
41
42
43
# File 'app/services/activity/chain_runner.rb', line 33

def initialize(activity, options = {})
  @activity = activity
  @activity_type = activity.activity_type
  @activity_result_type = options[:new_activity_result_type] || activity.activity_result_type
  @logger = options[:logger] || Rails.logger

  raise NoActivityTypeError unless @activity_type
  raise NoActivityResultError unless @activity_result_type

  @chains_for_result = @activity_type.activity_chain_types.select { |act| act.activity_result_type_id == @activity_result_type.id }
end

Instance Attribute Details

#activityObject (readonly)

Returns the value of attribute activity.



10
11
12
# File 'app/services/activity/chain_runner.rb', line 10

def activity
  @activity
end

#activity_result_typeObject (readonly)

Returns the value of attribute activity_result_type.



10
11
12
# File 'app/services/activity/chain_runner.rb', line 10

def activity_result_type
  @activity_result_type
end

#activity_typeObject (readonly)

Returns the value of attribute activity_type.



10
11
12
# File 'app/services/activity/chain_runner.rb', line 10

def activity_type
  @activity_type
end

#chained_activitiesObject (readonly)

Returns the value of attribute chained_activities.



10
11
12
# File 'app/services/activity/chain_runner.rb', line 10

def chained_activities
  @chained_activities
end

#loggerObject (readonly)

Returns the value of attribute logger.



10
11
12
# File 'app/services/activity/chain_runner.rb', line 10

def logger
  @logger
end

Class Method Details

.initialize_and_execute_chains(activity, options = {}) ⇒ Symbol?

Returns :no_activity_type or :no_activity_result on failure.

Parameters:

  • activity (Activity)

    the activity whose chains will be executed

  • options (Hash) (defaults to: {})

    chain runner options

Options Hash (options):

  • new_activity_result_type (ActivityResultType)

    result type overriding the activity's current one

  • logger (Logger)

    logger to use (defaults to Rails.logger)

Returns:

  • (Symbol, nil)

    :no_activity_type or :no_activity_result on failure



17
18
19
20
21
22
23
24
# File 'app/services/activity/chain_runner.rb', line 17

def self.initialize_and_execute_chains(activity, options = {})
  chain_runner = new(activity, options)
  chain_runner.execute_chains
rescue NoActivityTypeError
  :no_activity_type
rescue NoActivityResultError
  :no_activity_result
end

Instance Method Details

#assign_resource_and_time(new_activity, offset_days = 0) ⇒ Activity

Assigns the best resource employee and computes the target datetime,
falling back to a week out (with a validation error) when the employee's
schedule can't produce one.

Parameters:

  • new_activity (Activity)

    chained activity being built

  • offset_days (Integer) (defaults to: 0)

    days to offset the target date by

Returns:

  • (Activity)

    the activity with assignment and target set



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'app/services/activity/chain_runner.rb', line 158

def assign_resource_and_time(new_activity, offset_days = 0)
  new_activity.assigned_resource = new_activity.best_resource_employee

  if new_activity.assigned_resource.instance_of? Employee
    begin
      # next_working_day returns a Date; coerce to an app-zone datetime so the
      # :datetime attribute holds a TimeWithZone (a bare Date is passed through
      # uncast by AR and later blows up on #hour, and would round-trip as
      # midnight-UTC — i.e. the previous calendar day in Central).
      new_activity.target_datetime = new_activity.assigned_resource.next_working_day(Time.current, offset_days).in_time_zone
    rescue StandardError => e
      # Add validation error instead of using fallback date
      new_activity.errors.add(:assigned_resource, e.message)
      logger.error "Cannot assign activity to employee #{new_activity.assigned_resource.id}: #{e.message}"
      # Set a reasonable fallback for now, but the validation error will prevent saving
      new_activity.target_datetime = (Time.current + offset_days.days + 1.week).beginning_of_day
    end
  else
    new_activity.target_datetime = offset_days.days.from_now
  end
  new_activity
end

#chains_for_resultArray<ActivityChainType>

Chain types configured for the activity's current result type.

Returns:



47
48
49
# File 'app/services/activity/chain_runner.rb', line 47

def chains_for_result
  activity_type.activity_chain_types.select { |chain| chain.activity_result_type_id == @activity_result_type.id }
end

#chains_for_result_has_campaigns?Boolean

Whether any chain for the current result is campaign-scoped.

Returns:

  • (Boolean)


53
54
55
# File 'app/services/activity/chain_runner.rb', line 53

def chains_for_result_has_campaigns?
  chains_for_result.any?(&:campaign_id)
end

#chains_to_executeArray<ActivityChainType>

Chains applicable to this activity, filtered by campaign when the
result's chains are campaign-scoped.

Returns:



60
61
62
63
64
65
66
67
68
# File 'app/services/activity/chain_runner.rb', line 60

def chains_to_execute
  # If our list of chains has campaigns specified, we focus results only on those with the same campaign specified
  # or nil campaign matching
  if chains_for_result_has_campaigns?
    @chains_for_result.select { |chain| chain.campaign_id == @activity.campaign_id }
  else # All results are valid which have no campaign
    @chains_for_result.select { |chain| chain.campaign_id.nil? }
  end
end

#execute_activity_chain(chain) ⇒ void

This method returns an undefined value.

Creates the chained follow-up activity for a chain, computing the
target date from opportunity promise dates or the chain's day offset.

Parameters:



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'app/services/activity/chain_runner.rb', line 93

def execute_activity_chain(chain)
  return unless chain.chain_activity_type

  if @activity.party.respond_to?(:assign_chained_activities?) && !@activity.party.assign_chained_activities?
    logger.warn "Could not run chained activity #{chain.chain_activity_type} triggered on activity id:#{@activity.id} because party assign_chained_activities returned false"
    return
  end
  # Chain activity
  new_activity = Activity.new activity_type: chain.chain_activity_type,
                              party: @activity.party,
                              resource: @activity.resource,
                              parent_activity_id: @activity.id,
                              campaign_id: @activity.campaign_id,
                              creator: @activity.closed_by || @activity.creator

  # If our activity chain is opportunity_promise we make some calculations
  if chain.opportunity_promise? && @activity.resource.respond_to?(:close_date)
    opp = @activity.resource
    # When is the opportunity promise date?
    target_date = opp.close_date || opp.planned_installation_date
    if target_date
      # What's the offset in days
      offset_days = (target_date.to_date - Date.current).to_i
      # Go 7 days earlier
      offset_days -= 7
      # Is it negative or zero, its probably garbage data or too close so lets' ignore it and let the
      # default offset take it over
      offset_days = nil if offset_days <= 0
    end
  end
  # The static offset is the default
  offset_days ||= chain.offset_days
  offset_days ||= 0

  assign_resource_and_time(new_activity, offset_days)

  if new_activity.save
    @chained_activities << new_activity
  else
    logger.error "Could not save chained activity #{new_activity.inspect} triggered on activity id:#{@activity.id}, errors: #{new_activity.errors_to_s}"
  end
end

#execute_chain(chain) ⇒ void

This method returns an undefined value.

Executes one chain: creates the chained activity and sends its email template.

Parameters:



84
85
86
87
# File 'app/services/activity/chain_runner.rb', line 84

def execute_chain(chain)
  execute_activity_chain chain
  execute_email_template chain
end

#execute_chainsvoid

This method returns an undefined value.

Runs every applicable chain and records the created activities on
activity.chained_activity_result.



73
74
75
76
77
78
79
# File 'app/services/activity/chain_runner.rb', line 73

def execute_chains
  @chained_activities = []
  chains_to_execute.each do |chain|
    execute_chain(chain)
  end
  @activity.chained_activity_result = @chained_activities
end

#execute_email_template(chain) ⇒ void

This method returns an undefined value.

Sends the chain's email template as a communication to the activity's party.

Parameters:



139
140
141
142
143
144
145
146
147
148
149
150
# File 'app/services/activity/chain_runner.rb', line 139

def execute_email_template(chain)
  return unless chain.email_template

  comm = CommunicationBuilder.new(sender_party: @activity.sender_party,
                                  resource: @activity.resource,
                                  recipient_party: @activity.party,
                                  template: chain.email_template,
                                  current_user: @activity.updater,
                                  transmit_at: chain.email_transmit_at_time,
                                  use_best_email: true).create
  Rails.logger.info "Chain Runner for chain #{chain.id} created communication #{comm.id}"
end