Class: Activity::Prioritizer
- Inherits:
-
BaseService
- Object
- BaseService
- Activity::Prioritizer
- Defined in:
- app/services/activity/prioritizer.rb
Overview
Service object: prioritizer.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- PRIORITIZATION_SELECT =
Prioritization select.
<<-EOS.freeze activities.*, COALESCE(cat.company_id, pc.company_id, 1) as company_id, activity_types.priority as activity_type_priority, pc.sales_priority_index, COALESCE(activities.original_target_datetime,activities.target_datetime,activities.created_at) as original_target_datetime, pc.full_name as customer_full_name, pc.id as customer_id, pc.primary_sales_rep_id, pc.secondary_sales_rep_id, pc.local_sales_rep_id, pc.service_rep_id, er.backup_rep_id EOS
Instance Attribute Summary collapse
-
#activity_deck ⇒ Object
readonly
Pass these options to the initializer :allocation_start, if not specified, will look at start_range and if not use today's date :start_range, how far back to look for activities to reschedule :end_range, how far ahead to look for activities to reschedule.
Attributes inherited from BaseService
Class Method Summary collapse
-
.assignable_activity?(a) ⇒ Boolean
Whether an activity can be assigned to at least one rep.
-
.high_priority_activity_levels ⇒ Array<Integer>
Priority tier levels considered high priority.
-
.standard_priority_activity_levels ⇒ Array<Integer>
Priority tier levels considered standard priority.
-
.workload_based_on_matrix(assignment_matrix, rep_id, target_date, priorities = []) ⇒ Integer
Count of activities already assigned to a rep on a day.
Instance Method Summary collapse
-
#activities_to_process? ⇒ Boolean
Whether unallocated activities remain in the deck.
-
#allocate_priority_activities(activities, allocation_start) ⇒ void
Assigns high-priority activities day by day to reps who still have priority capacity, advancing to the next business day when a day fills up.
-
#allocate_standard_activities(activities, allocation_start) ⇒ void
Assigns standard-tier activities, grouped per rep, filling each rep's working days in order until their activities are exhausted.
-
#append_to_decks(activities) ⇒ void
Takes an list of activities, divide them in time sensitive and non time sensitive queues and load up our decks accordingly with a sort.
-
#assign_activity(activities, assign_rep_id, target_date) ⇒ void
Records activities into the assignment matrix for a rep/day, grouped by priority tier, and marks them processed.
-
#available_activities_on_day_for_rep(rep_id, target_date) ⇒ Integer
Returns the number of activity slots available for a rep_id on a given day This is the maximum allocatable per day.
-
#available_priority_activities_on_day_for_rep(rep_id, target_date) ⇒ Integer
Priority-tier slots available for a rep on a given day.
-
#can_take_activities_on_day?(rep_id, target_date) ⇒ Boolean
Whether a rep has any free activity capacity on a day.
-
#can_take_priority_activities_on_day?(rep_id, target_date) ⇒ Boolean
Whether a rep has any free priority-tier capacity on a day.
-
#commit_assignment_matrix ⇒ void
Persists the assignment matrix: moves each activity to its rep's closing time on the assigned day, keeping audit trails.
-
#employees_hash ⇒ Hash{Integer => Employee}
Active employees keyed by id, memoised for the whole run.
-
#extract_time_locked_activities ⇒ void
this method extract all time locked activities in the future and pre-fills them in our assignment matrix.
-
#initialize_deck ⇒ Boolean
Initialize the deck of activities to sort.
-
#load_activities_for_processing(start_range = nil, end_range = nil) ⇒ ActiveRecord::Relation<Activity>
Loads up activities suitable for reorganization based on a date range.
-
#log_error(msg) ⇒ void
Logs an error message and reports progress to the optional block.
-
#log_info(msg) ⇒ void
Logs an info message and reports progress to the optional block.
-
#log_warning(msg) ⇒ void
Logs a warning message and reports progress to the optional block.
-
#mark_processed(success, a) ⇒ void
This marks an activity as having been processed by keeping track in an internal array.
-
#prepare_activity(activity) ⇒ Activity?
Resolves the assignable rep ids for an activity from its assignment queue, falling back to the current assignee/creator/rep when no queue matches.
-
#process(options = {}, &block) ⇒ Result
Start prioritization routine, will return a result object Yields back position, total to process, and info message to a block (optional).
-
#reps_available_for_activities_on_day(day) ⇒ Array<Integer>
Returns an array of employee ids that have room for activities on a given day.
-
#reset_time_locks ⇒ void
Unlocks time-locked activities that lost their assignable rep.
-
#sort_deck ⇒ void
Sort the decks.
-
#to_s ⇒ String
Short identifier for log lines.
Methods inherited from BaseService
#initialize, #log_debug, #logger, #tagged_logger
Constructor Details
This class inherits a constructor from BaseService
Instance Attribute Details
#activity_deck ⇒ Object (readonly)
Pass these options to the initializer
:allocation_start, if not specified, will look at start_range and if not use today's date
:start_range, how far back to look for activities to reschedule
:end_range, how far ahead to look for activities to reschedule
8 9 10 |
# File 'app/services/activity/prioritizer.rb', line 8 def activity_deck @activity_deck end |
Class Method Details
.assignable_activity?(a) ⇒ Boolean
Whether an activity can be assigned to at least one rep.
437 438 439 440 |
# File 'app/services/activity/prioritizer.rb', line 437 def self.assignable_activity?(a) (a.activity_type_priority <= ActivityType::PRIORITY_MAX and (a.assignment_resource_ids.present? or a.fallback_employee_id.present?)) or (a.activity_type_priority > ActivityType::PRIORITY_MAX and a.assignment_resource_ids.present?) end |
.high_priority_activity_levels ⇒ Array<Integer>
Priority tier levels considered high priority.
444 445 446 |
# File 'app/services/activity/prioritizer.rb', line 444 def self.high_priority_activity_levels (1..ActivityType::PRIORITY_MAX).to_a end |
.standard_priority_activity_levels ⇒ Array<Integer>
Priority tier levels considered standard priority.
450 451 452 |
# File 'app/services/activity/prioritizer.rb', line 450 def self.standard_priority_activity_levels ((ActivityType::PRIORITY_MAX + 1)..ActivityType::TOTAL_TIERS).to_a end |
.workload_based_on_matrix(assignment_matrix, rep_id, target_date, priorities = []) ⇒ Integer
Count of activities already assigned to a rep on a day.
240 241 242 243 244 245 246 247 248 |
# File 'app/services/activity/prioritizer.rb', line 240 def self.workload_based_on_matrix(assignment_matrix, rep_id, target_date, priorities = []) assignment_matrix[rep_id] ||= {} assignment_matrix[rep_id][target_date] ||= {} wl = 0 assignment_matrix[rep_id][target_date].each do |priority, activities| wl += activities.size if priorities.empty? || priorities.include?(priority) end wl end |
Instance Method Details
#activities_to_process? ⇒ Boolean
Whether unallocated activities remain in the deck.
342 343 344 |
# File 'app/services/activity/prioritizer.rb', line 342 def activities_to_process? @activity_deck.present? end |
#allocate_priority_activities(activities, allocation_start) ⇒ void
This method returns an undefined value.
Assigns high-priority activities day by day to reps who still have
priority capacity, advancing to the next business day when a day fills up.
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'app/services/activity/prioritizer.rb', line 123 def allocate_priority_activities(activities, allocation_start) # allocation start can only start at the earliest with the original_target_datetime while activities.present? play_deck = activities.select { |a| a.original_target_datetime.to_date <= allocation_start } full_rep_list_ids = employees_hash.values.map(&:id) working_rep_ids = reps_available_for_activities_on_day(allocation_start) rep_ids_with_cap_reached = [] # Resets the rep with their cap reached, it's a new day! list_size = play_deck.size log_info "Allocating priority activities on #{allocation_start}, list is #{list_size}, working reps: #{working_rep_ids}, full list of reps: #{full_rep_list_ids}" rep_ids_in_activity_pool = play_deck.map(&:assignment_resource_ids).flatten.uniq play_deck.each_with_index do |a, i| log_info "[#{i}/#{list_size}] AID: #{a.id}, Running through priority activity, assignable to #{a.assignment_resource_ids}, working_rep_ids: #{working_rep_ids}, rep_ids_with_cap_reached: #{rep_ids_with_cap_reached} on #{allocation_start}" assignable_rep_ids = working_rep_ids - rep_ids_with_cap_reached rep_ids = (a.assignment_resource_ids & assignable_rep_ids).compact.uniq log_info "[#{i}/#{list_size}] AID: #{a.id}, final assignment possible: #{rep_ids}" rep_ids.each do |rep_id| if can_take_priority_activities_on_day?(rep_id, allocation_start) # Remove it from the global list and mark it for processing assign_activity(a, rep_id, allocation_start) # Remove it from our priority list activities.delete(a) break # no need to keep evaluating the other reps else log_warning "Cap reached for rep id #{rep_id}" rep_ids_with_cap_reached << rep_id rep_ids_in_activity_pool.delete(rep_id) end end if rep_ids_in_activity_pool.empty? log_warning 'No one left to allocate activities to, moving on to next day' break end end # Jump to the next day or the original target datetime of the earliest activity allocation_start = allocation_start.next_business_day rep_ids_with_cap_reached = [] end end |
#allocate_standard_activities(activities, allocation_start) ⇒ void
This method returns an undefined value.
Assigns standard-tier activities, grouped per rep, filling each rep's
working days in order until their activities are exhausted.
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'app/services/activity/prioritizer.rb', line 168 def allocate_standard_activities(activities, allocation_start) # Standard run is next activities.size # Reject unassignable activities just in case raise 'Some activities are unassignable, abnormal at this state' if activities.find { |a| a.assignment_resource_ids.empty? } # Group by the first assignable rep id (the others do not count) grouped_activities = activities.group_by { |a| a.assignment_resource_ids.first } # Evaluate by rep grouped_activities.each do |rep_id, rep_activities| # Go through each day and fill in the rep's activities log_info "[Rep: #{rep_id}] looking for next working day from #{allocation_start}" rep_allocation_date = employees_hash[rep_id].next_working_day(allocation_start, 0) # Start on the first working day if rep_allocation_date.nil? log_info "[Rep: #{rep_id}] rep_allocation_date is not found, total activities for this rep: #{rep_activities.size}, skipping this rep" @unprocessed += rep_activities next end log_info "[Rep: #{rep_id}] [#{rep_allocation_date}] Total activities for this rep: #{rep_activities.size}" while rep_activities.present? allocatable_activities = rep_activities.select { |a| a.original_target_datetime <= rep_allocation_date.end_of_day } # how many can they take today? max_activities_today = available_activities_on_day_for_rep(rep_id, rep_allocation_date) log_info "[Rep: #{rep_id}] [#{rep_allocation_date}] Allocatable: #{allocatable_activities.size}, Available Slots: #{max_activities_today}" # pick the maximum possible for this day for this rep allocate_activities = allocatable_activities.shift(max_activities_today) # Allocate them assign_activity(allocate_activities, rep_id, rep_allocation_date) rep_activities -= allocate_activities log_info "[Rep: #{rep_id}] [#{rep_allocation_date}] Rep has #{rep_activities.size} remaining to allocate, moving on to next day" # Roll on to the next day with activities next_allocatable_activities_date = rep_activities.filter_map { |a| a.original_target_datetime.try(:to_date) }.min next_business_day = employees_hash[rep_id].next_working_day(rep_allocation_date) rep_allocation_date = [next_allocatable_activities_date, next_business_day].compact.max log_info "[Rep: #{rep_id}] About to process next working day: #{rep_allocation_date}" end end end |
#append_to_decks(activities) ⇒ void
This method returns an undefined value.
Takes an list of activities, divide them in time sensitive and non time sensitive queues and load up our decks accordingly with a sort
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 |
# File 'app/services/activity/prioritizer.rb', line 457 def append_to_decks(activities) log_info "Reviewing #{activities.size} activities for addition to deck" # Append activities to deck and also fix returned values from custom select to proper type cast. new_rep_ids = [] # Here we will keep track of possible rep, this way we can be smart and not load the whole employee db new_activities = activities.select { |a| a.activity_type_priority.to_i.positive? }.map do |a| a.activity_type_priority = a.activity_type_priority.to_i a.sales_priority_index = a.sales_priority_index.to_i a end good_counter = 0 bad_counter = 0 new_activities.each do |na| a = prepare_activity(na) if a && self.class.assignable_activity?(a) good_counter += 1 @activity_deck[a.id] = a new_rep_ids |= [a.fallback_employee_id, a.assignment_resource_ids].flatten.compact else bad_counter += 1 log_warning " * Activity #{na.id} marked as unprocessable, no one could be assigned to it, review queue and customer data." @unprocessed << a end end log_info " * Added #{good_counter} activities to activity deck, now we have #{@activity_deck.size} good activities." log_info " * Unprocessable: #{bad_counter}, total in unprocessable: #{@unprocessed.size}." log_info " * Rep Matrix: #{new_rep_ids.size}, we now have #{employees_hash.size} loaded" sort_deck # Perform magical sorting end |
#assign_activity(activities, assign_rep_id, target_date) ⇒ void
This method returns an undefined value.
Records activities into the assignment matrix for a rep/day, grouped by
priority tier, and marks them processed.
222 223 224 225 226 227 228 229 230 231 232 |
# File 'app/services/activity/prioritizer.rb', line 222 def assign_activity(activities, assign_rep_id, target_date) activities = [activities].flatten log_info "[Rep: #{assign_rep_id}] [#{target_date}] Assigning activities #{activities.map(&:id)}" @assignment_matrix[assign_rep_id] ||= {} @assignment_matrix[assign_rep_id][target_date] ||= {} activities.group_by(&:activity_type_priority).each do |activity_type_priority, sub_activities| @assignment_matrix[assign_rep_id][target_date][activity_type_priority] ||= [] @assignment_matrix[assign_rep_id][target_date][activity_type_priority] += sub_activities.map(&:id) end activities.each { |a| mark_processed(true, a) } end |
#available_activities_on_day_for_rep(rep_id, target_date) ⇒ Integer
Returns the number of activity slots available for a rep_id on a given day
This is the maximum allocatable per day
296 297 298 299 300 301 302 303 304 305 306 307 308 |
# File 'app/services/activity/prioritizer.rb', line 296 def available_activities_on_day_for_rep(rep_id, target_date) target_date = target_date.to_date unless target_date.is_a?(Date) if (emp_obj = employees_hash[rep_id]) current = self.class.workload_based_on_matrix(@assignment_matrix, rep_id, target_date) max = employees_hash[rep_id].maximum_activities_per_day(target_date) available = [max - current, 0].max log_info "[Rep: #{rep_id}] [#{target_date}] Activity Load is #{current}/#{max}, #{available} available activity slots" available else log_error "[Rep: #{rep_id}] [#{target_date}] #{rep_id} is missing from employees_hash" unless emp_obj 0 end end |
#available_priority_activities_on_day_for_rep(rep_id, target_date) ⇒ Integer
Priority-tier slots available for a rep on a given day.
314 315 316 317 318 319 320 |
# File 'app/services/activity/prioritizer.rb', line 314 def available_priority_activities_on_day_for_rep(rep_id, target_date) current = self.class.workload_based_on_matrix(@assignment_matrix, rep_id, target_date, self.class.high_priority_activity_levels) max = employees_hash[rep_id].maximum_priority_tier_activities_per_day(target_date) available = [max - current, 0].max log_info "[Rep: #{rep_id}] [#{target_date}] Priority Activity Load is #{current}/#{max}, #{available} available priority activity slots" available end |
#can_take_activities_on_day?(rep_id, target_date) ⇒ Boolean
Whether a rep has any free activity capacity on a day.
326 327 328 329 |
# File 'app/services/activity/prioritizer.rb', line 326 def can_take_activities_on_day?(rep_id, target_date) target_date = target_date.to_date unless target_date.is_a?(Date) available_activities_on_day_for_rep(rep_id, target_date) > 0 end |
#can_take_priority_activities_on_day?(rep_id, target_date) ⇒ Boolean
Whether a rep has any free priority-tier capacity on a day.
335 336 337 338 |
# File 'app/services/activity/prioritizer.rb', line 335 def can_take_priority_activities_on_day?(rep_id, target_date) target_date = target_date.to_date unless target_date.is_a?(Date) available_priority_activities_on_day_for_rep(rep_id, target_date) > 0 end |
#commit_assignment_matrix ⇒ void
This method returns an undefined value.
Persists the assignment matrix: moves each activity to its rep's closing
time on the assigned day, keeping audit trails.
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'app/services/activity/prioritizer.rb', line 262 def commit_assignment_matrix Time.current.utc.to_fs(:db) @assignment_matrix.each do |rep_id, dates| Activity.transaction do # Grouping updates in one transaction speed things up dates.each do |date, priorities| target_datetime = WorkingHours.advance_to_closing_time(date) # target_datetime = target_datetime.utc.to_fs(:db) act_ids = [] priorities.each do |_priority, activities| act_ids += (activities || []).select { |i| i > 0 } # Negative ids are fake stubs at this point we ignore them end next if act_ids.blank? logger.info "Employee id: #{rep_id} -> Assigning #{act_ids.size} activities on #{target_datetime}, ids: #{act_ids.join(',')}" Activity.where(id: act_ids, activity_result_type_id: nil).find_each do |activity| activity.original_target_datetime ||= activity.target_datetime activity.original_assigned_resource_id ||= activity.assigned_resource_id activity.target_datetime = target_datetime activity.assigned_resource_id = rep_id activity.skip_callbacks = true # Don't run any validation or callbacks except for audit trail activity.skip_check_for_open_sales_activity = true activity.save # We want audit trails end end end end end |
#employees_hash ⇒ Hash{Integer => Employee}
Active employees keyed by id, memoised for the whole run.
411 412 413 414 415 416 417 418 |
# File 'app/services/activity/prioritizer.rb', line 411 def employees_hash @employees_hash ||= Employee .includes(:employee_events, :company, :employee_record) .active .select(:id, :full_name, :company_id) .to_a .index_by(&:id) end |
#extract_time_locked_activities ⇒ void
This method returns an undefined value.
this method extract all time locked activities in the future and pre-fills them in our assignment matrix
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 |
# File 'app/services/activity/prioritizer.rb', line 362 def extract_time_locked_activities log_info 'Extracting Time Locked Activities that are not overdue and where rep is working' time_locked_counter = 0 @activity_deck.select { |_aid, a| a.lock_target_datetime }.each do |aid, a| log_info "[AID:#{aid}] is time locked" if a.assigned_resource_id && (employee = employees_hash[a.assigned_resource_id]) if a.overdue? || (overloaded = !can_take_activities_on_day?(a.assigned_resource_id, a.target_datetime.to_date)) log_info "[AID:#{aid}] time locked and overdue on #{a.target_datetime.to_date}, looking for a new date" if a.overdue? log_info "[AID:#{aid}] time locked and rep #{employee.full_name} [#{employee.id}] is overloaded on #{a.target_datetime.to_date}, looking for a new date" if overloaded new_date = [Date.current, a.target_datetime.to_date].max until can_take_activities_on_day?(a.assigned_resource_id, new_date) new_date += 1.day new_date = employee.next_working_day(new_date, 0) end log_info "[AID:#{aid}] found new date for rep #{employee.full_name} on #{new_date}" new_target_datetime = WorkingHours.advance_to_closing_time(new_date) a.target_datetime = new_target_datetime log_info "[AID:#{aid}] is overdue and will be moved to next working day for employee from today #{new_target_datetime}" # a.new_note = "Time lock was overdue and moved to next working day for assigned employee" a.skip_callbacks = true a.save end # employees_hash[a.assigned_resource_id].working_on_day?(a.target_datetime) # The activity can be time locked time_locked_counter += 1 # Store in assignment matrix a stub to fool our counter @assignment_matrix[a.assigned_resource_id] ||= {} @assignment_matrix[a.assigned_resource_id][a.target_datetime.to_date] ||= {} @assignment_matrix[a.assigned_resource_id][a.target_datetime.to_date][a.activity_type_priority] ||= [] @assignment_matrix[a.assigned_resource_id][a.target_datetime.to_date][a.activity_type_priority] << -a.id # Negative activity id why not. mark_processed(true, a) else # The activity is marked time locked but is now invalid log_warning "[AID:#{aid}] was originally time locked but can no longer remain in that state" @reset_time_lock_aids << aid end end @assignment_matrix.each do |resource_id, target_datetime_hsh| target_datetime_hsh.each do |target_date, priority_hsh| priority_hsh.each do |priority, activity_ids| log_info "[Rep #{resource_id}] [#{target_date}] [Priority: #{priority}] #{activity_ids.size} locked activities: #{activity_ids.map(&:abs).join(', ')}" end end end log_info "Extracted #{time_locked_counter} Time Locked Activities that are assignable" end |
#initialize_deck ⇒ Boolean
Initialize the deck of activities to sort.
422 423 424 425 426 427 428 429 430 431 432 |
# File 'app/services/activity/prioritizer.rb', line 422 def initialize_deck ardeck = load_activities_for_processing # Append to decks and sort + clear date time as needed append_to_decks ardeck # Take all time locked activities and do something else extract_time_locked_activities # Non processable time locks get reset reset_time_locks # return true if values are present activities_to_process? end |
#load_activities_for_processing(start_range = nil, end_range = nil) ⇒ ActiveRecord::Relation<Activity>
Loads up activities suitable for reorganization based on a date range
547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 |
# File 'app/services/activity/prioritizer.rb', line 547 def load_activities_for_processing(start_range = nil, end_range = nil) ardeck = Activity.non_notes.open_activities.joins(:activity_type) .where(target_datetime: ...1.year.from_now) # rule of thumb to help with strays .joins('INNER JOIN parties p ON p.id = activities.party_id') .joins('INNER JOIN parties pc ON pc.id = COALESCE(p.customer_id, p.id)') .joins('INNER JOIN catalogs cat ON cat.id = pc.catalog_id') .joins('LEFT JOIN parties e ON pc.primary_sales_rep_id = e.id') .joins('LEFT JOIN employee_records er ON e.id = er.party_id') .except(:select) .select(PRIORITIZATION_SELECT) ardeck = ardeck.where(activity_types: { priority: @priorities }) if @priorities.present? ardeck = ardeck.where(Activity[:target_datetime].gteq(start_range.beginning_of_day)) if start_range.present? ardeck = ardeck.where(Activity[:target_datetime].lteq(end_range.end_of_day)) if end_range.present? ardeck = ardeck.limit(@deck_limit) if @deck_limit.present? if @rep_ids.present? rep_ids = Array(@rep_ids) ardeck = ardeck.where.any_of({ assigned_resource_id: rep_ids }, { original_assigned_resource_id: rep_ids }) end @activity_deck_size = ardeck.size ardeck end |
#log_error(msg) ⇒ void
This method returns an undefined value.
Logs an error message and reports progress to the optional block.
580 581 582 583 |
# File 'app/services/activity/prioritizer.rb', line 580 def log_error(msg) super @block&.call(@total_processed, @activity_deck_size, msg) end |
#log_info(msg) ⇒ void
This method returns an undefined value.
Logs an info message and reports progress to the optional block.
572 573 574 575 |
# File 'app/services/activity/prioritizer.rb', line 572 def log_info(msg) super @block&.call(@total_processed, @activity_deck_size, msg) end |
#log_warning(msg) ⇒ void
This method returns an undefined value.
Logs a warning message and reports progress to the optional block.
588 589 590 591 |
# File 'app/services/activity/prioritizer.rb', line 588 def log_warning(msg) super @block&.call(@total_processed, @activity_deck_size, msg) end |
#mark_processed(success, a) ⇒ void
This method returns an undefined value.
This marks an activity as having been processed by keeping track in an internal array
350 351 352 353 354 355 356 357 358 |
# File 'app/services/activity/prioritizer.rb', line 350 def mark_processed(success, a) if success @processed << a else @unprocessed << a end @total_processed += 1 @activity_deck.delete(a.id) end |
#prepare_activity(activity) ⇒ Activity?
Resolves the assignable rep ids for an activity from its assignment queue,
falling back to the current assignee/creator/rep when no queue matches.
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 |
# File 'app/services/activity/prioritizer.rb', line 502 def prepare_activity(activity) # Pre-fetch the assignment queue, saves us time. at = @activity_types[activity.activity_type_id] company_id = activity.company_id.to_i resource_ids = [] # find which assignement queue to use if (ataq = at.activity_type_assignment_queues.find { |ataq| ataq.company_id == company_id }) # Setup a fake open struct customer that quacks like a customer customer = OpenStruct.new customer.id = activity.customer_id.to_i customer.primary_sales_rep_id = activity.primary_sales_rep_id.to_i if activity.primary_sales_rep_id.present? customer.secondary_sales_rep_id = activity.secondary_sales_rep_id.to_i if activity.secondary_sales_rep_id.present? customer.local_sales_rep_id = activity.local_sales_rep_id.to_i if activity.local_sales_rep_id.present? customer.service_rep_id = activity.service_rep_id.to_i if activity.service_rep_id.present? customer.backup_rep_id = activity.backup_rep_id.to_i if activity.backup_rep_id.present? current_user_id = activity.assigned_resource_id || activity.creator_id # Current user id must excludes non reps, activity creator could be customers current_user_id = nil unless current_user_id.in?(employees_hash.keys) resource_ids += ataq.get_full_queue_resources(customer, current_user_id) activity.fallback_employee_id = ataq.fallback_employee_id # High priority activity have the fallback assignable resource_ids << ataq.fallback_employee_id if ataq.fallback_employee_id && activity.activity_type_priority <= ActivityType::PRIORITY_MAX end if resource_ids.empty? resource_ids << activity.assigned_resource_id resource_ids << activity.creator_id resource_ids << activity.primary_sales_rep_id end # Sometime a customer ends up in the resource ids, e.g creator_id is most commong # by doing this intersect we ensure we only take potential reps resource_ids = resource_ids.compact.uniq & employees_hash.keys activity.assignment_resource_ids = resource_ids return activity if activity.assignment_resource_ids.present? # log_info "Activity: #{activity.id}, Activity Type #{activity.activity_type_id} possible assignments are #{activity.assignment_resource_ids.join(',')}" log_warning "Activity: #{activity.id}, Activity Type #{activity.activity_type_id} does not have a valid assignment possible" nil end |
#process(options = {}, &block) ⇒ Result
Start prioritization routine, will return a result object
Yields back position, total to process, and info message to a block (optional)
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 107 108 109 110 111 112 113 114 115 116 |
# File 'app/services/activity/prioritizer.rb', line 52 def process( = {}, &block) @block = block @allocation_start = [:allocation_start] || Date.current @allocation_end = @allocation_start raise 'Allocation cannot start in the past' if @allocation_start < Date.current @deck_limit = [:deck_limit] @priorities = [:priorities] @rep_ids = [:rep_ids] @current_user_id = [:current_user_id] # Initialize these arrays which will store our workload @activity_deck = {} @unprocessed = [] @processed = [] @loaded_days = [] @total_processed = 0 @reset_time_lock_aids = [] # Now pre-load the activity types and associated assignment queues in a nice fast hash @activity_types = ActivityType.eager_load(:assignment_queues).index_by(&:id) @assignment_matrix = {} whodunnit = ['Activity', 'Prioritizer', @current_user_id].compact.join('::') res = nil PaperTrail.request(whodunnit: whodunnit) do # Preload employees and their events initialize_deck log_info '* Activity Prioritizer ready to roll, call #prioritize' log_info " #{@activity_deck.size} activities loaded" log_info " #{@unprocessed.size} won't be processed" log_info " #{employees_hash.size} employees loaded" start_time = Time.current @allocation_end = @allocation_start.dup @total_activities = @activity_deck.size @total_processed = 0 log_info "Prioritization started at #{start_time.to_fs(:crm_default)} for #{@total_activities} activities" # Start on first allocation day @activity_deck.size # Splitting log_info 'Splitting into priority and standard list' priority_list, standard_list = @activity_deck.values.partition { |a| a.activity_type_priority <= ActivityType::PRIORITY_MAX } allocate_priority_activities priority_list, @allocation_end allocate_standard_activities standard_list, @allocation_end # Commit matrix Here commit_assignment_matrix # Wrap up end_time = Time.current duration = (end_time - start_time).round(2) throughput = (@total_activities / duration).round(2) = [] << "Prioritization ended at #{end_time.to_fs(:crm_default)}, duration: #{Heatwave::Duration.humanize(duration)}, #{throughput} activities / sec" << "!!! some activities remains and could find no allocation, total count #{@activity_deck.size}" if @activity_deck.present? res = Result.new(total_activities: @total_activities, duration: duration, throughput: throughput, unprocessable: @activity_deck.size, messages: ) res..each { |m| log_info m } end res end |
#reps_available_for_activities_on_day(day) ⇒ Array<Integer>
Returns an array of employee ids that have room for activities on a given day
212 213 214 |
# File 'app/services/activity/prioritizer.rb', line 212 def reps_available_for_activities_on_day(day) employees_hash.values.map(&:id).select { |eid| can_take_activities_on_day?(eid, day) } end |
#reset_time_locks ⇒ void
This method returns an undefined value.
Unlocks time-locked activities that lost their assignable rep.
252 253 254 255 256 257 |
# File 'app/services/activity/prioritizer.rb', line 252 def reset_time_locks return if @reset_time_lock_aids.blank? log_info "Resetting time locks for #{@reset_time_lock_aids.size}" Activity.where(id: @reset_time_lock_aids).find_each(&:unlock) end |
#sort_deck ⇒ void
This method returns an undefined value.
Sort the decks
489 490 491 492 493 494 495 496 |
# File 'app/services/activity/prioritizer.rb', line 489 def sort_deck log_info 'Sorting Deck' # The time sensitive deck will now be sorted according to the activity priority, the original activity date and the sales priority index last @activity_deck = @activity_deck.sort_by do |_aid, a| [a.activity_type_priority, a.original_target_datetime, -a.sales_priority_index] end.to_h log_info 'Sorting Deck Complete' end |
#to_s ⇒ String
Short identifier for log lines.
39 40 41 |
# File 'app/services/activity/prioritizer.rb', line 39 def to_s "Activity:Prioritizer: #{@allocation_end} << #{@activity_deck.size}" end |