Class: Activity

Inherits:
ApplicationRecord show all
Includes:
ActionView::Helpers::TextHelper, AfterCommitEverywhere, Models::Auditable, Models::Embeddable, PgSearch::Model
Defined in:
app/models/activity.rb

Overview

== Schema Information

Table name: activities
Database name: primary

id :integer not null, primary key
completion_datetime :datetime
description :string(255)
lock_target_datetime :boolean default(FALSE)
notes :text
original_target_datetime :datetime
resource_type :string(255)
start_datetime :datetime
target_datetime :datetime
uploads_count :integer default(0)
created_at :datetime
updated_at :datetime
activity_result_type_id :integer
activity_type_id :integer
assigned_resource_id :integer
campaign_id :integer
closed_by_id :integer
communication_id :integer
creator_id :integer
customer_id :integer
opportunity_id :integer
original_assigned_resource_id :integer
parent_activity_id :integer
party_id :integer
resource_id :integer
updater_id :integer
visit_id :integer

Indexes

by_act_type_w (activity_type_id) WHERE (activity_result_type_id IS NULL)
by_type_and_assi_resrc_w (activity_type_id,assigned_resource_id) WHERE (activity_result_type_id IS NULL)
index_activities_on_activity_result_type_id (activity_result_type_id)
index_activities_on_activity_type_id (activity_type_id)
index_activities_on_assigned_resource_id (assigned_resource_id)
index_activities_on_campaign_id (campaign_id)
index_activities_on_closed_by_id (closed_by_id)
index_activities_on_communication_id (communication_id)
index_activities_on_creator_id (creator_id)
index_activities_on_customer_id (customer_id)
index_activities_on_opportunity_id (opportunity_id)
index_activities_on_party_id (party_id)
index_activities_on_resource_type_and_resource_id (resource_type,resource_id) WHERE ((resource_type IS NOT NULL) AND (resource_id IS NOT NULL))
index_activities_on_target_datetime (target_datetime) USING brin
index_activities_on_updater_id (updater_id)
index_activities_on_visit_id (visit_id)
index_activities_resource_id (resource_id)

Foreign Keys

activities_activity_result_type_id_fkey (activity_result_type_id => activity_result_types.id)
activities_activity_type_id_fkey (activity_type_id => activity_types.id)
activities_communication_id_fk (communication_id => communications.id) ON DELETE => nullify
activities_creator_id_fk (creator_id => parties.id) ON DELETE => nullify
activities_party_id_fk (party_id => parties.id) ON DELETE => cascade
activities_updater_id_fk (updater_id => parties.id) ON DELETE => nullify
fk_rails_... (customer_id => parties.id)
fk_rails_... (opportunity_id => opportunities.id) ON DELETE => nullify ON UPDATE => cascade
fk_rails_... (visit_id => visits.id)

Defined Under Namespace

Classes: AssignmentNotificationHandler, AutoChainCloser, ChainRunner, ChainRunnerHandler, CompactNotes, EmailNotificationHandler, ExpiredActivityCloser, OpenActivitiesStamper, Prioritizer, ResourceList, SalesActivityHandler, SynchronousEffectsHandler, TypeRulesCreatedHandler, TypeRulesUpdatedHandler, VoicemailReadHandler

Constant Summary collapse

NOTE_RESULT_ID =
2
NATURAL_ORDER =
'activities.activity_result_type_id IS NULL DESC, COALESCE(activities.completion_datetime,activities.target_datetime) DESC'
RESOURCE_TYPES =
%w[Delivery Opportunity Order SupportCase Party SpiffEnrollment Receipt CreditMemo SupportCaseParticipant StatementOfAccount Rma Invoice RoomConfiguration ExportedCatalogItemPacket LocatorRecord CreditApplication CampaignDelivery
PurchaseOrder Payment Quote SchedulerBooking CallRecord].freeze
FINAL_SERVICES =
[ActivityTypeConstants::ONSITESERVICE, ActivityTypeConstants::SSI_INSTALL, ActivityTypeConstants::SGS_ONSITESUPPORT, ActivityTypeConstants::SGS_REMOTESUPPORT, ActivityTypeConstants::SSFIX_ONSITE_SERVICE]
INITIAL_SERVICES =
[ActivityTypeConstants::SERVICECONFIRM, ActivityTypeConstants::SSI_PREPLAN_MEET, ActivityTypeConstants::SGS_ONSITE_PREPLAN_MEET, ActivityTypeConstants::SGS_REMOTE_PREPLAN_MEET, ActivityTypeConstants::SSFIX_SERVICE_CONFIRM]
MIN_EMBEDDABLE_NOTES_LENGTH =
50

Constants included from Models::Embeddable

Models::Embeddable::DEFAULT_MODEL, Models::Embeddable::MAX_CONTENT_LENGTH

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Methods included from Models::Embeddable

#content_embeddings

Has and belongs to many collapse

Delegated Instance Attributes collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Embeddable

#embeddable_locales, #embedding_content_hash, embedding_partition_class, #embedding_stale?, #embedding_type_name, #embedding_vector, #find_content_embedding, #find_similar, #generate_all_embeddings!, #generate_chunked_embeddings!, #generate_embedding!, #has_embedding?, #locale_for_embedding, #needs_chunking?, regenerate_all_embeddings, semantic_search

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#assignment_resource_idsObject

Returns the value of attribute assignment_resource_ids.



243
244
245
# File 'app/models/activity.rb', line 243

def assignment_resource_ids
  @assignment_resource_ids
end

#chained_activity_resultObject

Returns the value of attribute chained_activity_result.



243
244
245
# File 'app/models/activity.rb', line 243

def chained_activity_result
  @chained_activity_result
end

#fallback_employee_idObject

Returns the value of attribute fallback_employee_id.



243
244
245
# File 'app/models/activity.rb', line 243

def fallback_employee_id
  @fallback_employee_id
end

#halt_chainObject

Returns the value of attribute halt_chain.



243
244
245
# File 'app/models/activity.rb', line 243

def halt_chain
  @halt_chain
end

#linked_party_idsObject

Returns the value of attribute linked_party_ids.



243
244
245
# File 'app/models/activity.rb', line 243

def linked_party_ids
  @linked_party_ids
end

#mileageObject

Returns the value of attribute mileage.



243
244
245
# File 'app/models/activity.rb', line 243

def mileage
  @mileage
end

#new_target_dateObject

Returns the value of attribute new_target_date.



243
244
245
# File 'app/models/activity.rb', line 243

def new_target_date
  @new_target_date
end

#new_target_timeObject

Returns the value of attribute new_target_time.



243
244
245
# File 'app/models/activity.rb', line 243

def new_target_time
  @new_target_time
end

#service_dateObject

Returns the value of attribute service_date.



243
244
245
# File 'app/models/activity.rb', line 243

def service_date
  @service_date
end

#service_durationObject

Returns the value of attribute service_duration.



243
244
245
# File 'app/models/activity.rb', line 243

def service_duration
  @service_duration
end

#service_timeObject

Returns the value of attribute service_time.



243
244
245
# File 'app/models/activity.rb', line 243

def service_time
  @service_time
end

#skip_callbacksObject

Returns the value of attribute skip_callbacks.



243
244
245
# File 'app/models/activity.rb', line 243

def skip_callbacks
  @skip_callbacks
end

#skip_check_for_open_sales_activityObject

Returns the value of attribute skip_check_for_open_sales_activity.



243
244
245
# File 'app/models/activity.rb', line 243

def skip_check_for_open_sales_activity
  @skip_check_for_open_sales_activity
end

#skip_check_for_valid_partyObject

Returns the value of attribute skip_check_for_valid_party.



243
244
245
# File 'app/models/activity.rb', line 243

def skip_check_for_valid_party
  @skip_check_for_valid_party
end

#support_case_closed_reasonObject

Returns the value of attribute support_case_closed_reason.



243
244
245
# File 'app/models/activity.rb', line 243

def support_case_closed_reason
  @support_case_closed_reason
end

#support_case_eventObject

Returns the value of attribute support_case_event.



243
244
245
# File 'app/models/activity.rb', line 243

def support_case_event
  @support_case_event
end

#travel_timeObject

Returns the value of attribute travel_time.



243
244
245
# File 'app/models/activity.rb', line 243

def travel_time
  @travel_time
end

#updated_activitiesObject

Returns the value of attribute updated_activities.



243
244
245
# File 'app/models/activity.rb', line 243

def updated_activities
  @updated_activities
end

Class Method Details

.all_activity_filtersObject



495
496
497
# File 'app/models/activity.rb', line 495

def self.all_activity_filters
  %w[Party Contact Opportunity]
end

.completedActiveRecord::Relation<Activity>

A relation of Activities that are completed. Active Record Scope

Returns:

See Also:



115
# File 'app/models/activity.rb', line 115

scope :completed, -> { joins(:activity_result_type).merge(ActivityResultType.completed) }

.completion_orderActiveRecord::Relation<Activity>

A relation of Activities that are completion order. Active Record Scope

Returns:

See Also:



120
# File 'app/models/activity.rb', line 120

scope :completion_order, -> { order('activities.completion_datetime desc') }

.embeddable_content_typesObject

-- Embeddable interface ---------------------------------------------------



549
550
551
# File 'app/models/activity.rb', line 549

def self.embeddable_content_types
  [:primary]
end

.final_service_completedActiveRecord::Relation<Activity>

A relation of Activities that are final service completed. Active Record Scope

Returns:

See Also:



169
# File 'app/models/activity.rb', line 169

scope :final_service_completed, -> { where(activity_type_id: FINAL_SERVICES, activity_result_type_id: ActivityResultTypeConstants::CMP) }

.for_customer_and_contactsActiveRecord::Relation<Activity>

A relation of Activities that are for customer and contacts. Active Record Scope

Returns:

See Also:



147
# File 'app/models/activity.rb', line 147

scope :for_customer_and_contacts, ->(customer) { where(party_id: customer.self_and_contacts_party_ids_arr) }

.heatwave_assistant_idObject



608
609
610
# File 'app/models/activity.rb', line 608

def self.heatwave_assistant_id
  Employee.joins(:contact_points).merge(ContactPoint.emails).where(ContactPoint[:detail].eq('hwav@warmlyyours.com')).pick(:id)
end

.high_priorityActiveRecord::Relation<Activity>

A relation of Activities that are high priority. Active Record Scope

Returns:

See Also:



118
# File 'app/models/activity.rb', line 118

scope :high_priority, -> { joins(:activity_type).where(activity_types: { priority: 1 }) }

.inbound_emailsActiveRecord::Relation<Activity>

A relation of Activities that are inbound emails. Active Record Scope

Returns:

See Also:



167
# File 'app/models/activity.rb', line 167

scope :inbound_emails, -> { where(activity_type_id: ActivityTypeConstants::EMAILIN_TYPES) }

.initial_service_completedActiveRecord::Relation<Activity>

A relation of Activities that are initial service completed. Active Record Scope

Returns:

See Also:



168
# File 'app/models/activity.rb', line 168

scope :initial_service_completed, -> { where(activity_type_id: INITIAL_SERVICES, activity_result_type_id: ActivityResultTypeConstants::CMP) }

.iqoppfuActiveRecord::Relation<Activity>

A relation of Activities that are iqoppfu. Active Record Scope

Returns:

See Also:



113
# File 'app/models/activity.rb', line 113

scope :iqoppfu, -> { where(activity_type_id: ActivityTypeConstants::IQOPPFUS_IDS) }

.leadsActiveRecord::Relation<Activity>

A relation of Activities that are leads. Active Record Scope

Returns:

See Also:



114
# File 'app/models/activity.rb', line 114

scope :leads, -> { where(activity_type_id: ActivityTypeConstants::LEAD_FORM) }

.mcp_searchable?Boolean

Returns:

  • (Boolean)


553
554
555
# File 'app/models/activity.rb', line 553

def self.mcp_searchable?
  false
end

.meetingActiveRecord::Relation<Activity>

A relation of Activities that are meeting. Active Record Scope

Returns:

See Also:



122
# File 'app/models/activity.rb', line 122

scope :meeting, -> { joins(:activity_type).merge(ActivityType.meeting) }

.natural_orderActiveRecord::Relation<Activity>

A relation of Activities that are natural order. Active Record Scope

Returns:

See Also:



119
# File 'app/models/activity.rb', line 119

scope :natural_order, -> { order(Arel.sql(NATURAL_ORDER)) }

.non_cob_time(datetime) ⇒ Object



584
585
586
587
588
589
590
591
# File 'app/models/activity.rb', line 584

def self.non_cob_time(datetime)
  return unless datetime

  closing_time = WorkingHours.advance_to_closing_time(datetime)
  return unless datetime != closing_time && (datetime.min != 0)

  datetime.strftime('%I:%M %p')
end

.non_notesActiveRecord::Relation<Activity>

A relation of Activities that are non notes. Active Record Scope

Returns:

See Also:



111
# File 'app/models/activity.rb', line 111

scope :non_notes, -> { where.not(activity_type_id: nil) }

.not_tagged_withActiveRecord::Relation<Activity>

A relation of Activities that are not tagged with. Active Record Scope

Returns:

See Also:



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'app/models/activity.rb', line 149

scope :not_tagged_with, ->(*tags) {
  clean_tags = [tags].flatten.map(&:presence).compact.uniq.map(&:downcase)
  return all if clean_tags.empty?

  # Exclude activities whose activity_type has ANY of the specified tags
  # Activity types without the tags OR activities without an activity type are included
  tagged_activity_type_ids = ActivityType.joins(:tag_records)
                                         .where('LOWER(tags.name) IN (?)', clean_tags)
                                         .distinct
                                         .pluck(:id)

  if tagged_activity_type_ids.any?
    where('activities.activity_type_id IS NULL OR activities.activity_type_id NOT IN (?)', tagged_activity_type_ids)
  else
    all
  end
}

.notes_onlyActiveRecord::Relation<Activity>

A relation of Activities that are notes only. Active Record Scope

Returns:

See Also:



110
# File 'app/models/activity.rb', line 110

scope :notes_only, -> { where(activity_type_id: nil) }

.open_activitiesActiveRecord::Relation<Activity>

A relation of Activities that are open activities. Active Record Scope

Returns:

See Also:



116
# File 'app/models/activity.rb', line 116

scope :open_activities, -> { where(activity_result_type_id: nil).non_notes }

.overdue_activitiesActiveRecord::Relation<Activity>

A relation of Activities that are overdue activities. Active Record Scope

Returns:

See Also:



117
# File 'app/models/activity.rb', line 117

scope :overdue_activities, -> { open_activities.where(Activity[:target_datetime].lteq(Time.current)) }

.position_of_activity(activity_id, relation) ⇒ Object



283
284
285
# File 'app/models/activity.rb', line 283

def self.position_of_activity(activity_id, relation)
  relation.order(relation.values[:order] || :id).pluck(:id).index(activity_id).try(:+, 1)
end

.preloadedActiveRecord::Relation<Activity>

A relation of Activities that are preloaded. Active Record Scope

Returns:

See Also:



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'app/models/activity.rb', line 124

scope :preloaded, -> {
  includes(
    :party,
    :activity_result_type,
    :campaign,
    { activity_type: :tag_records },
    { assigned_resource: :profile_image },
    { closed_by: :profile_image },
    { communication: :uploads },
    # Type-specific resource preloading (polymorphic can't be preloaded directly)
    :resource_support_case,
    :resource_opportunity,
    :resource_order,
    :resource_quote,
    :resource_room_configuration,
    :resource_scheduler_booking
  )
}

.quofuActiveRecord::Relation<Activity>

A relation of Activities that are quofu. Active Record Scope

Returns:

See Also:



112
# File 'app/models/activity.rb', line 112

scope :quofu, -> { where(activity_type_id: ActivityTypeConstants::QUOFUS_IDS) }

.ransackable_scopes(_auth_object = nil) ⇒ Object



275
276
277
# File 'app/models/activity.rb', line 275

def self.ransackable_scopes(_auth_object = nil)
  %i[tagged_with not_tagged_with]
end

.resource_typesObject



279
280
281
# File 'app/models/activity.rb', line 279

def self.resource_types
  RESOURCE_TYPES
end

.sales_activitiesActiveRecord::Relation<Activity>

A relation of Activities that are sales activities. Active Record Scope

Returns:

See Also:



123
# File 'app/models/activity.rb', line 123

scope :sales_activities, -> { joins(:activity_type).merge(ActivityType.sales_activities) }

.tagged_withActiveRecord::Relation<Activity>

A relation of Activities that are tagged with. Active Record Scope

Returns:

See Also:



148
# File 'app/models/activity.rb', line 148

scope :tagged_with, ->(*tags) { joins(:activity_type).merge(ActivityType.tagged_with(tags)) }

.trainingActiveRecord::Relation<Activity>

A relation of Activities that are training. Active Record Scope

Returns:

See Also:



121
# File 'app/models/activity.rb', line 121

scope :training, -> { joins(:activity_type).merge(ActivityType.training) }

.visible_by_defaultActiveRecord::Relation<Activity>

A relation of Activities that are visible by default. Active Record Scope

Returns:

See Also:



166
# File 'app/models/activity.rb', line 166

scope :visible_by_default, -> { not_tagged_with(['Drip']) }

.with_activity_uploads_countActiveRecord::Relation<Activity>

A relation of Activities that are with activity uploads count. Active Record Scope

Returns:

See Also:



146
# File 'app/models/activity.rb', line 146

scope :with_activity_uploads_count, -> { select("activities.*, (select count(u.id) from uploads u where u.resource_type = 'Activity' and u.resource_id = activities.id) as uploads_count") }

.with_customer_as_cActiveRecord::Relation<Activity>

A relation of Activities that are with customer as c. Active Record Scope

Returns:

See Also:



142
143
144
145
# File 'app/models/activity.rb', line 142

scope :with_customer_as_c, -> {
  joins('inner join parties p on p.id = activities.party_id')
    .joins("left join parties c on ((p.type = 'Contact' and p.customer_id = c.id) or (p.type = 'Customer' and p.id = c.id))")
}

Instance Method Details

#activity_result_typeActivityResultType



89
# File 'app/models/activity.rb', line 89

belongs_to :activity_result_type, inverse_of: :activities, optional: true

#activity_typeActivityType



75
# File 'app/models/activity.rb', line 75

belongs_to :activity_type, inverse_of: :activities, optional: true

#activity_type_for_selectObject



331
332
333
# File 'app/models/activity.rb', line 331

def activity_type_for_select
  ActivityType.options_for_select(party:, include_activity_type_id: activity_type_id)
end

#age_in_daysObject



287
288
289
290
# File 'app/models/activity.rb', line 287

def age_in_days
  end_date = closed? ? completion_datetime.to_date : Date.current
  (end_date - effective_origin_date).to_i
end

#assigned_resourceEmployee

Returns:

See Also:



79
# File 'app/models/activity.rb', line 79

belongs_to :assigned_resource, class_name: 'Employee', optional: true

#auto_assign(allocate_on = nil, current_user = nil, options = {}) ⇒ Object



437
438
439
440
441
442
443
# File 'app/models/activity.rb', line 437

def auto_assign(allocate_on = nil, current_user = nil, options = {})
  self.target_datetime ||= created_at # Set a default if we don't have one for an odd reason
  self.original_target_datetime ||= target_datetime
  self.target_datetime = allocate_on if allocate_on && !lock_target_datetime # if allocate on is passed and our activity is not time locked we use it
  self.original_assigned_resource ||= assigned_resource
  self.assigned_resource = best_resource_employee(current_user, options)
end

#auto_assign_and_schedule(ideal_target_date = nil, options = {}) ⇒ Object



408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'app/models/activity.rb', line 408

def auto_assign_and_schedule(ideal_target_date = nil, options = {})
  # What is the best target date to schedule this activity?
  logger = options[:logger] || Rails.logger
  original_scheduled_date = ideal_target_date || target_datetime
  allocate_on = original_scheduled_date
  return false unless allocate_on

  begin
    logger.debug "Processing activity #{id} with a target date of #{allocate_on}."
    # Pass the date processed to the calling block so special pre-loading can occur in case of batch reassignments
    # This is necessary so that as we advance the days we scoop them in our deck
    yield(allocate_on) if allocate_on.present? && block_given?
    auto_assign(allocate_on, options) # Provides a resource for that day
    success = false # Sensible default
    if assigned_resource.present? # Was a resource available and assigned on that day?
      save # Then save the record
      success = true # And mark the operation as a success
      allocate_on = nil # To break the loop, set allocate_on to nil
    elsif allocate_on > (original_scheduled_date + 1.month) # Try for one month, if we can't succeed at this point break we'll end up in a never ending loop
      allocate_on = nil # Breaks the loop
      logger.error ' * Could not find a suitable resource for activity after 30 days'
    else # No assigned resource and not past the 1 month limit, so move up one day and try again
      allocate_on += 1.day
    end
    logger.debug " ** assigned resource is #{assigned_resource.try(:full_name)} allocate on is now #{allocate_on}"
  end while allocate_on
  success
end

#auto_close_if_applicable(auto_save: true) ⇒ Object

Our activity chain might have special rules, such as an auto close result when the due date is reached, useful for triggering
drip email chains



447
448
449
450
451
452
453
454
455
456
# File 'app/models/activity.rb', line 447

def auto_close_if_applicable(auto_save: true)
  return :not_applicable if is_note? || closed?
  return :not_applicable unless art = activity_type.auto_close_result_chain

  return :not_overdue unless overdue? && art.due_date?

  self.activity_result_type = art.activity_result_type
  save! if auto_save
  :overdue_auto_close
end

#best_resource(current_user = nil, options = {}) ⇒ Object

current_user: a value to use as the currently logged in user



459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'app/models/activity.rb', line 459

def best_resource(current_user = nil, options = {})
  return nil if is_note?

  current_user = nil unless current_user.is_a?(Employee)
  # Sensible default when operating in batch mode.
  current_user ||= self.original_assigned_resource
  current_user ||= creator if creator.is_a?(Employee)
  current_user ||= updater if updater.is_a?(Employee)
  at = activity_type
  if (resource.class == SupportCase) && customer.nil?
    resource.assigned_to_id || at.activity_type_assignment_queues.first.try(:fallback_employee_id) || current_user
  elsif customer
    at.determine_assigned_resource_id(customer, current_user.try(:id), self.target_datetime, resource, options)
  end
end

#best_resource_employee(current_user = nil, options = {}) ⇒ Object



475
476
477
478
479
480
# File 'app/models/activity.rb', line 475

def best_resource_employee(current_user = nil, options = {})
  rep_id = best_resource(current_user, options)
  return Employee.find(rep_id) if rep_id

  nil
end

#campaignCampaign

Returns:

See Also:



91
# File 'app/models/activity.rb', line 91

belongs_to :campaign, optional: true

#campaign_options_for_selectObject



356
357
358
359
360
361
362
363
# File 'app/models/activity.rb', line 356

def campaign_options_for_select
  options = []
  if customer = party.try(:customer) || resource.try(:customer)
    options = Campaign.joins(subscriber_lists: :subscribers).where(subscribers: { customer_id: customer.id }).pluck(:name, :id)
    options << [campaign.name, campaign.id] if campaign
  end
  options.compact.uniq
end

#cancel(options = {}) ⇒ Object



537
538
539
540
# File 'app/models/activity.rb', line 537

def cancel(options = {})
  opts = { activity_result_type_id: ActivityResultTypeConstants::CANCEL, completion_datetime: Time.current }.merge(options)
  update(opts)
end

#cancelled?Object

Alias for Activity_result_type#cancelled?

Returns:

  • (Object)

    Activity_result_type#cancelled?

See Also:



183
# File 'app/models/activity.rb', line 183

delegate :complete?, :cancelled?, :result_code, to: :activity_result_type, allow_nil: true

#child_activitiesActiveRecord::Relation<Activity>

Returns:

See Also:



96
# File 'app/models/activity.rb', line 96

has_many :child_activities, class_name: 'Activity', inverse_of: :parent_activity

#closed?Boolean

Returns:

  • (Boolean)


400
401
402
# File 'app/models/activity.rb', line 400

def closed?
  activity_type_id.nil? || activity_result_type_id.present?
end

#closed_byParty

Returns:

See Also:



74
# File 'app/models/activity.rb', line 74

belongs_to :closed_by, class_name: 'Party', optional: true

#communicationCommunication



90
# File 'app/models/activity.rb', line 90

belongs_to :communication, optional: true

#compact_notesObject



542
543
544
545
# File 'app/models/activity.rb', line 542

def compact_notes
  d = notes || description
  d.gsub(/\[.*\]/, '').strip
end

#complete(options = {}) ⇒ Object



532
533
534
535
# File 'app/models/activity.rb', line 532

def complete(options = {})
  opts = { activity_result_type_id: ActivityResultTypeConstants::CMP, completion_datetime: Time.current }.merge(options)
  update(opts)
end

#complete?Object

Alias for Activity_result_type#complete?

Returns:

  • (Object)

    Activity_result_type#complete?

See Also:



183
# File 'app/models/activity.rb', line 183

delegate :complete?, :cancelled?, :result_code, to: :activity_result_type, allow_nil: true

#contact_options_for_selectObject



343
344
345
346
347
348
349
350
351
352
353
354
# File 'app/models/activity.rb', line 343

def contact_options_for_select
  # Memoize these values to ensure the guard check and the builder call use the same values.
  # This prevents edge cases where association state changes between the check and the call
  # (e.g., during failed updates where party_id is cleared but not saved).
  p = party
  r = resource
  return [] unless p || r

  skip_persons = activity_type&.party_must_be_company?
  skip_organizations = activity_type&.party_must_be_contact?
  ContactPoint::AddressBookBuilder.options_for_select(resource: r, party: p, skip_persons:, skip_organizations:)
end

#content_for_embedding(_content_type = :primary) ⇒ Object



559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
# File 'app/models/activity.rb', line 559

def content_for_embedding(_content_type = :primary)
  return nil if notes.blank? || notes.length < MIN_EMBEDDABLE_NOTES_LENGTH

  parts = []
  parts << "Activity: #{activity_type&.task_type || 'Note'}"
  parts << "Tags: #{activity_type.tags.join(', ')}" if activity_type&.tags&.any?
  parts << "Status: #{result_code || 'Open'}"
  parts << "Priority: #{activity_type.priority}" if activity_type&.priority.present?
  parts << "Party: #{party&.full_name}" if party
  parts << "Customer: #{customer&.full_name}" if customer && customer != party
  parts << "Assigned to: #{assigned_resource&.full_name}" if assigned_resource
  parts << "Date: #{effective_date.strftime('%B %d, %Y')}" if effective_date
  parts << "Due: #{target_datetime.strftime('%B %d, %Y')}" if target_datetime && open?
  parts << "Overdue" if overdue?
  parts << "Completed: #{completion_datetime.strftime('%B %d, %Y')}" if completion_datetime
  parts << "Resource: #{resource_type} ##{resource_id}" if resource_type.present?
  parts << "Campaign: #{campaign&.name}" if campaign
  parts << compact_notes
  parts.compact.join("\n\n")
end

#customerCustomer

Returns:

See Also:



77
# File 'app/models/activity.rb', line 77

belongs_to :customer, optional: true, inverse_of: :related_activities

#default_resource_for_selectObject



640
641
642
643
644
645
# File 'app/models/activity.rb', line 640

def default_resource_for_select
  return [] unless resource

  formatter = Activity::ResourceList.build_formatter(resource)
  [[formatter.display, formatter.identifier]]
end

#descriptionObject



647
648
649
# File 'app/models/activity.rb', line 647

def description
  notes.try(:first, 500) || activity_type.try(:description)
end

#display_dateObject



499
500
501
# File 'app/models/activity.rb', line 499

def display_date
  effective_date.to_s(has_time? ? :crm_default : :crm_date_only)
end

#display_typeObject



404
405
406
# File 'app/models/activity.rb', line 404

def display_type
  activity_type&.task_type || 'Note'
end

#effective_dateObject



507
508
509
# File 'app/models/activity.rb', line 507

def effective_date
  completion_datetime || self.target_datetime || created_at
end

#effective_origin_dateObject



308
309
310
# File 'app/models/activity.rb', line 308

def effective_origin_date
  (original_target_datetime || target_datetime || created_at).to_date
end

#embedding_content_changed?Boolean

Returns:

  • (Boolean)


580
581
582
# File 'app/models/activity.rb', line 580

def embedding_content_changed?
  saved_change_to_notes?
end

#has_time?Boolean

Returns:

  • (Boolean)


503
504
505
# File 'app/models/activity.rb', line 503

def has_time?
  effective_date != effective_date.beginning_of_day
end

#historical_open_activitiesActiveRecord::Relation<HistoricalOpenActivity>

Track each time the activity was left open at end of day

Returns:

See Also:



99
# File 'app/models/activity.rb', line 99

has_many :historical_open_activities

#is_email?Object

Alias for Activity_type#is_email?

Returns:

  • (Object)

    Activity_type#is_email?

See Also:



182
# File 'app/models/activity.rb', line 182

delegate :is_email?, to: :activity_type, allow_nil: true

#is_note?Boolean

Returns:

  • (Boolean)


511
512
513
# File 'app/models/activity.rb', line 511

def is_note?
  activity_type_id.nil?
end

#is_onsite_service?Boolean

Returns:

  • (Boolean)


521
522
523
524
525
526
# File 'app/models/activity.rb', line 521

def is_onsite_service?
  activity_type_id == ActivityTypeConstants::ONSITESERVICE or
    activity_type_id == ActivityTypeConstants::SSI_INSTALL or
    activity_type_id == ActivityTypeConstants::SSFIX_ONSITE_SERVICE or
    activity_type_id == ActivityTypeConstants::SGS_ONSITESUPPORT
end

#is_service_confirm?Boolean

Returns:

  • (Boolean)


515
516
517
518
519
# File 'app/models/activity.rb', line 515

def is_service_confirm?
  activity_type_id == ActivityTypeConstants::SERVICECONFIRM or
    activity_type_id == ActivityTypeConstants::SSI_PREPLAN_MEET or
    activity_type_id == ActivityTypeConstants::SSFIX_SERVICE_CONFIRM
end

#is_service_confirm_completed?Boolean

Returns:

  • (Boolean)


749
750
751
752
753
754
755
# File 'app/models/activity.rb', line 749

def is_service_confirm_completed?
  true if activity_type_id.in?([ActivityTypeConstants::SERVICECONFIRM,
                                ActivityTypeConstants::SSI_PREPLAN_MEET,
                                ActivityTypeConstants::SGS_ONSITE_PREPLAN_MEET,
                                ActivityTypeConstants::SGS_REMOTE_PREPLAN_MEET,
                                ActivityTypeConstants::SSFIX_SERVICE_CONFIRM]) and activity_result_type_id == ActivityResultTypeConstants::CMP
end

#is_smartservice_related?Boolean

Returns:

  • (Boolean)


736
737
738
739
740
741
742
743
744
745
746
747
# File 'app/models/activity.rb', line 736

def is_smartservice_related?
  activity_type_id.in?([ActivityTypeConstants::SERVICECONFIRM,
                        ActivityTypeConstants::ONSITESERVICE,
                        ActivityTypeConstants::SSI_PREPLAN_MEET,
                        ActivityTypeConstants::SSI_INSTALL,
                        ActivityTypeConstants::SGS_ONSITE_PREPLAN_MEET,
                        ActivityTypeConstants::SGS_REMOTE_PREPLAN_MEET,
                        ActivityTypeConstants::SGS_ONSITESUPPORT,
                        ActivityTypeConstants::SGS_REMOTESUPPORT,
                        ActivityTypeConstants::SSFIX_ONSITE_SERVICE,
                        ActivityTypeConstants::SSFIX_SERVICE_CONFIRM])
end

#lockObject



292
293
294
295
296
# File 'app/models/activity.rb', line 292

def lock
  self.skip_callbacks = true
  self.lock_target_datetime = true
  save
end

#mail_activitiesActiveRecord::Relation<MailActivity>

Returns:

See Also:



98
# File 'app/models/activity.rb', line 98

has_many :mail_activities

#new_noteObject



386
387
388
# File 'app/models/activity.rb', line 386

def new_note
  ''
end

#new_note=(n) ⇒ Object



376
377
378
379
380
381
382
383
384
# File 'app/models/activity.rb', line 376

def new_note=(n)
  return if n.blank?

  date_stamp = []
  date_stamp << Time.current.to_fs(:crm_default)
  date_stamp << CurrentScope.user&.show_name_4 if CurrentScope.user && CurrentScope.user.respond_to?(:show_name_4)
  self.notes = "[#{date_stamp.compact.join(' ')}]\n#{n}\n#{notes}"
  self.description = n&.first(80)
end

#open?Boolean

Returns:

  • (Boolean)


396
397
398
# File 'app/models/activity.rb', line 396

def open?
  !closed?
end

#opportunityOpportunity



92
# File 'app/models/activity.rb', line 92

belongs_to :opportunity, inverse_of: :linked_activities, optional: true

#original_assigned_resourceEmployee

Returns:

See Also:



80
# File 'app/models/activity.rb', line 80

belongs_to :original_assigned_resource, class_name: 'Employee', optional: true

#overdue?Boolean

Returns:

  • (Boolean)


365
366
367
368
369
370
# File 'app/models/activity.rb', line 365

def overdue?
  return false if closed?
  return false unless target_datetime.present?

  (target_datetime < Time.current)
end

#parent_activityActivity

Returns:

See Also:



78
# File 'app/models/activity.rb', line 78

belongs_to :parent_activity, class_name: 'Activity', inverse_of: :child_activities, optional: true

#partiesActiveRecord::Relation<Party>

Returns:

  • (ActiveRecord::Relation<Party>)

See Also:



101
# File 'app/models/activity.rb', line 101

has_and_belongs_to_many :parties

#partyParty

Returns:

See Also:



76
# File 'app/models/activity.rb', line 76

belongs_to :party, optional: true

#party_nameObject



392
393
394
# File 'app/models/activity.rb', line 392

def party_name
  party&.full_name
end

#primary_address_idObject



491
492
493
# File 'app/models/activity.rb', line 491

def primary_address_id
  customer.try(:mailing_address_id) || customer.try(:shipping_address_id) || customer.try(:billing_address_id)
end

#resourceResource

Returns:

  • (Resource)

See Also:



81
# File 'app/models/activity.rb', line 81

belongs_to :resource, polymorphic: true, optional: true

#resource_combo=(val) ⇒ Object

Backwards-compat: cached browser forms may still submit the old
"ClassName|id" format. Silently convert to GlobalID so
Activity.new(params[:activity]) doesn't blow up.



631
632
633
634
635
636
637
638
# File 'app/models/activity.rb', line 631

def resource_combo=(val)
  if val.present?
    class_name, rid = val.split("|", 2)
    self.resource_gid = URI::GID.build(app: GlobalID.app, model_name: class_name, model_id: rid.to_s).to_s
  else
    self.resource_gid = nil
  end
end

#resource_gidObject



612
613
614
615
616
# File 'app/models/activity.rb', line 612

def resource_gid
  return unless resource_type.present? && resource_id.present?

  URI::GID.build(app: GlobalID.app, model_name: resource_type, model_id: resource_id.to_s).to_s
end

#resource_gid=(gid_string) ⇒ Object



618
619
620
621
622
623
624
625
626
# File 'app/models/activity.rb', line 618

def resource_gid=(gid_string)
  if gid_string.present? && (gid = GlobalID.parse(gid_string))
    self.resource_type = gid.model_name
    self.resource_id = gid.model_id
  else
    self.resource_type = nil
    self.resource_id = nil
  end
end

#resource_opportunityOpportunity



84
# File 'app/models/activity.rb', line 84

belongs_to :resource_opportunity, class_name: 'Opportunity', foreign_key: :resource_id, optional: true

#resource_orderOrder

Returns:

See Also:



85
# File 'app/models/activity.rb', line 85

belongs_to :resource_order, class_name: 'Order', foreign_key: :resource_id, optional: true

#resource_quoteQuote

Returns:

See Also:



86
# File 'app/models/activity.rb', line 86

belongs_to :resource_quote, class_name: 'Quote', foreign_key: :resource_id, optional: true

#resource_room_configurationRoomConfiguration

Returns:

  • (RoomConfiguration)

See Also:



87
# File 'app/models/activity.rb', line 87

belongs_to :resource_room_configuration, class_name: 'RoomConfiguration', foreign_key: :resource_id, optional: true

#resource_scheduler_bookingSchedulerBooking



88
# File 'app/models/activity.rb', line 88

belongs_to :resource_scheduler_booking, class_name: 'SchedulerBooking', foreign_key: :resource_id, optional: true

#resource_support_caseSupportCase

Type-specific resource associations for eager loading (polymorphic can't be preloaded)



83
# File 'app/models/activity.rb', line 83

belongs_to :resource_support_case, class_name: 'SupportCase', foreign_key: :resource_id, optional: true

#result_codeObject Also known as: activity_result_code

Alias for Activity_result_type#result_code

Returns:

  • (Object)

    Activity_result_type#result_code

See Also:



183
# File 'app/models/activity.rb', line 183

delegate :complete?, :cancelled?, :result_code, to: :activity_result_type, allow_nil: true

#result_options_for_selectObject



325
326
327
328
329
# File 'app/models/activity.rb', line 325

def result_options_for_select
  return unless activity_type

  activity_type.result_options_for_select
end

#return_toObject



339
340
341
# File 'app/models/activity.rb', line 339

def return_to
  resource || party
end

#sales_activity?Boolean

Returns:

  • (Boolean)


304
305
306
# File 'app/models/activity.rb', line 304

def sales_activity?
  activity_type.try(:sales_activity)
end

#sender_for_email_templatesObject



372
373
374
# File 'app/models/activity.rb', line 372

def sender_for_email_templates
  assigned_resource || updater || creator
end

#sender_partyObject

For emails, who is the sender?

Raises:

  • (StandardError)


594
595
596
597
598
599
600
601
602
603
604
605
606
# File 'app/models/activity.rb', line 594

def sender_party
  primary_rep = party&.customer&.primary_sales_rep
  sp = primary_rep if activity_type.sales_rep_as_sender
  sp ||= activity_type.sender_party
  sp ||= assigned_resource unless assigned_resource_id == self.class.heatwave_assistant_id
  sp ||= primary_rep = party.try(:primary_sales_rep)
  sp ||= creator if creator.is_a?(Employee)
  sp = nil if sp&.id == self.class.heatwave_assistant_id # just in case we never never allow the bot
  sp ||= Employee.find(105) # 'Julia Billen'
  raise StandardError.new("Unable to determine sender_party for activity id #{id}") unless sp

  sp
end

#set_assigned_repObject



713
714
715
716
717
718
# File 'app/models/activity.rb', line 713

def set_assigned_rep
  return true if closed?

  self.assigned_resource ||= best_resource_employee
  self.original_assigned_resource ||= self.assigned_resource
end

#set_campaignObject



720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
# File 'app/models/activity.rb', line 720

def set_campaign
  use_campaign = campaign || activity_type&.campaign

  return unless use_campaign

  # customer can be determined?
  return unless (cust = customer)

  # add customer to campaign
  res = use_campaign.add_customer cust
  return unless res.subscribed?

  # assign this activity to that campaign
  self.campaign = use_campaign
end

#set_linked_partiesObject



651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
# File 'app/models/activity.rb', line 651

def set_linked_parties
  pids = [party_id] # Initially we will use the linked party
  if linked_party_ids.present? # Array of additional party linked to us
    if linked_party_ids.is_a?(Array)
      pids += linked_party_ids
    elsif linked_party_ids.is_a?(String)
      pids += linked_party_ids.split(',')
    end
  end
  pids += resource.link_party_ids if resource.respond_to?(:link_party_ids) # Pull those from our resources
  pids = pids.compact.uniq.map(&:to_i)
  # Load up customer of contacts (compact to remove nil customer_ids)
  pids += Contact.where(id: pids).pluck(Arel.sql('distinct customer_id')).compact
  pids.uniq!

  # Drop party IDs that no longer exist (e.g. customer merge destroyed the party while a
  # resource still reports the old id via #link_party_ids — AppSignal #3373 / FK on activities_parties).
  valid_pids = Party.where(id: pids).pluck(:id)
  if valid_pids.size < pids.size
    logger.warn(
      "Activity#set_linked_parties skipping missing party_ids=#{(pids - valid_pids).join(', ')} " \
      "activity_id=#{persisted? ? id : 'new'}"
    )
  end
  pids = valid_pids
  return unless pids.present?

  # For new records, use standard assignment - no race condition possible yet
  unless persisted?
    logger.info "Activity (new) linked party ids will become #{pids.join(', ')}"
    self.party_ids = pids
    return
  end

  # For existing records, only add party_ids that don't already exist to avoid
  # race conditions when multiple workers process the same record concurrently
  existing_pids = party_ids.to_a
  new_pids = pids - existing_pids
  return if new_pids.empty?

  logger.info "Activity #{id} linked party ids adding #{new_pids.join(', ')} (existing: #{existing_pids.join(', ')})"

  # Use upsert to avoid RecordNotUnique errors from concurrent workers
  # trying to add the same party-activity associations
  new_pids.each do |pid|
    self.class.connection.execute(
      self.class.sanitize_sql_array([
        'INSERT INTO activities_parties (activity_id, party_id) VALUES (?, ?) ON CONFLICT (party_id, activity_id) DO NOTHING',
        id, pid
      ])
    )
  end

  # Reload the association to reflect the new state
  parties.reset
rescue ActiveRecord::RecordNotUnique
  # Race condition - another process already created this record
  # This is expected when multiple workers process the same invoicing concurrently
  logger.info "Activity #{id} set_linked_parties encountered RecordNotUnique - records already exist"
  parties.reset
end

#target_dateObject



312
313
314
# File 'app/models/activity.rb', line 312

def target_date
  target_datetime.nil? ? nil : target_datetime.to_datetime.to_date.to_fs(:crm_default)
end

#target_datetime_outside_business_hours?Boolean

Returns:

  • (Boolean)


757
758
759
760
761
762
763
# File 'app/models/activity.rb', line 757

def target_datetime_outside_business_hours?
  # The lack of a target datetime will imply we are outside business hours
  return true unless target_datetime

  # Because the closing time is exclusive, we need to subtract 1 second to do this check
  !(target_datetime - 1.second).in_working_hours?
end

#target_timeObject



316
317
318
319
320
321
322
323
# File 'app/models/activity.rb', line 316

def target_time
  return unless target_datetime

  # The following code causes time zone jumps based on the user
  # rounded = Time.at((target_datetime.to_time.to_i / 900.0).round * 900)
  # rounded_dt = target_datetime.is_a?(DateTime) ? rounded.to_datetime : rounded
  target_datetime.strftime('%I:%M %P')
end

#to_sObject Also known as: name



482
483
484
485
486
487
488
# File 'app/models/activity.rb', line 482

def to_s
  [
    target_datetime.try(:to_s).try(:crm_date_only),
    assigned_resource.try(:name),
    activity_type.try(:task_type)
  ].compact.join(' ')
end

#triggered_communicationsActiveRecord::Relation<Communication>

Returns:

See Also:



97
# File 'app/models/activity.rb', line 97

has_many :triggered_communications, class_name: 'Communication'

#unlockObject



298
299
300
301
302
# File 'app/models/activity.rb', line 298

def unlock
  self.skip_callbacks = true
  self.lock_target_datetime = false
  save
end

#uploadsActiveRecord::Relation<Upload>

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



95
# File 'app/models/activity.rb', line 95

has_many :uploads, as: :resource, dependent: :destroy

#visitVisit

Returns:

See Also:



93
# File 'app/models/activity.rb', line 93

belongs_to :visit, optional: true