Class: Activity

Inherits:
ApplicationRecord show all
Includes:
ActionView::Helpers::TextHelper, 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
call_record_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_call_record_id (call_record_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_... (call_record_id => call_records.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 =

Note result id.

2
NATURAL_ORDER =

Natural order.

'activities.activity_result_type_id IS NULL DESC, COALESCE(activities.completion_datetime,activities.target_datetime) DESC'
RESOURCE_TYPES =

Recognised resource types.

%w[Delivery Opportunity Order SupportCase Party SpiffEnrollment Receipt CreditMemo SupportCaseParticipant StatementOfAccount Rma Invoice RoomConfiguration ExportedCatalogItemPacket LocatorRecord CreditApplication CampaignDelivery
PurchaseOrder Payment Quote SchedulerBooking].freeze
FINAL_SERVICES =

Final services.

[ActivityTypeConstants::ONSITESERVICE, ActivityTypeConstants::SSI_INSTALL, ActivityTypeConstants::SGS_ONSITESUPPORT, ActivityTypeConstants::SGS_REMOTESUPPORT, ActivityTypeConstants::SSFIX_ONSITE_SERVICE].freeze
INITIAL_SERVICES =

Initial services.

[ActivityTypeConstants::SERVICECONFIRM, ActivityTypeConstants::SSI_PREPLAN_MEET, ActivityTypeConstants::SGS_ONSITE_PREPLAN_MEET, ActivityTypeConstants::SGS_REMOTE_PREPLAN_MEET, ActivityTypeConstants::SSFIX_SERVICE_CONFIRM].freeze
MIN_EMBEDDABLE_NOTES_LENGTH =

Minimum embeddable notes length.

50

Constants included from Models::Embeddable

Models::Embeddable::MAX_CONTENT_LENGTH

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

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 Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#assignment_resource_idsObject

Returns the value of attribute assignment_resource_ids.



269
270
271
# File 'app/models/activity.rb', line 269

def assignment_resource_ids
  @assignment_resource_ids
end

#chained_activity_resultObject

Returns the value of attribute chained_activity_result.



269
270
271
# File 'app/models/activity.rb', line 269

def chained_activity_result
  @chained_activity_result
end

#fallback_employee_idObject

Returns the value of attribute fallback_employee_id.



269
270
271
# File 'app/models/activity.rb', line 269

def fallback_employee_id
  @fallback_employee_id
end

#halt_chainObject

Returns the value of attribute halt_chain.



269
270
271
# File 'app/models/activity.rb', line 269

def halt_chain
  @halt_chain
end

#linked_party_idsObject

Returns the value of attribute linked_party_ids.



269
270
271
# File 'app/models/activity.rb', line 269

def linked_party_ids
  @linked_party_ids
end

#mileageObject

Returns the value of attribute mileage.



269
270
271
# File 'app/models/activity.rb', line 269

def mileage
  @mileage
end

#new_target_dateObject

Returns the value of attribute new_target_date.



269
270
271
# File 'app/models/activity.rb', line 269

def new_target_date
  @new_target_date
end

#new_target_timeObject

Returns the value of attribute new_target_time.



269
270
271
# File 'app/models/activity.rb', line 269

def new_target_time
  @new_target_time
end

#service_dateObject

Returns the value of attribute service_date.



269
270
271
# File 'app/models/activity.rb', line 269

def service_date
  @service_date
end

#service_durationObject

Returns the value of attribute service_duration.



269
270
271
# File 'app/models/activity.rb', line 269

def service_duration
  @service_duration
end

#service_timeObject

Returns the value of attribute service_time.



269
270
271
# File 'app/models/activity.rb', line 269

def service_time
  @service_time
end

#skip_callbacksObject

Returns the value of attribute skip_callbacks.



269
270
271
# File 'app/models/activity.rb', line 269

def skip_callbacks
  @skip_callbacks
end

#skip_check_for_open_sales_activityObject

Returns the value of attribute skip_check_for_open_sales_activity.



269
270
271
# File 'app/models/activity.rb', line 269

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.



269
270
271
# File 'app/models/activity.rb', line 269

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.



269
270
271
# File 'app/models/activity.rb', line 269

def support_case_closed_reason
  @support_case_closed_reason
end

#support_case_eventObject

Returns the value of attribute support_case_event.



269
270
271
# File 'app/models/activity.rb', line 269

def support_case_event
  @support_case_event
end

#travel_timeObject

Returns the value of attribute travel_time.



269
270
271
# File 'app/models/activity.rb', line 269

def travel_time
  @travel_time
end

#updated_activitiesObject

Returns the value of attribute updated_activities.



269
270
271
# File 'app/models/activity.rb', line 269

def updated_activities
  @updated_activities
end

Class Method Details

.all_activity_filtersObject



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

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:



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

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:



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

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

.embeddable_content_typesObject

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



566
567
568
# File 'app/models/activity.rb', line 566

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:



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

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:



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

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

.heatwave_assistant_idObject



626
627
628
# File 'app/models/activity.rb', line 626

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:



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

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:



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

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:



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

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:



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

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:



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

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

.mcp_searchable?Boolean

Returns:

  • (Boolean)


570
571
572
# File 'app/models/activity.rb', line 570

def self.mcp_searchable?
  false
end

.meetingActiveRecord::Relation<Activity>

A relation of Activities that are meeting. Active Record Scope

Returns:

See Also:



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

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:



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

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

.non_cob_time(datetime) ⇒ Object



602
603
604
605
606
607
608
609
# File 'app/models/activity.rb', line 602

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:



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

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:



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'app/models/activity.rb', line 172

scope :not_tagged_with, ->(*tags) {
  clean_tags = [tags].flatten.filter_map(&:presence).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
                                         .ids

  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:



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

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:



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

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:



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

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

.position_of_activity(activity_id, relation) ⇒ Object



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

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

.preloadedActiveRecord::Relation<Activity>

A relation of Activities that are preloaded. Active Record Scope

Returns:

See Also:



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'app/models/activity.rb', line 141

scope :preloaded, -> {
  includes(
    :party,
    :activity_result_type,
    :campaign,
    :call_record,
    :creator,
    :updater,
    :uploads,
    { activity_type: :tag_records },
    { assigned_resource: :profile_image },
    { closed_by: :profile_image },
    { communication: :uploads },
    # parent_activity (with its activity_type) is rendered in the metadata
    # footer of the selected activity's form (_activity_form.html.erb).
    { parent_activity: :activity_type },
    # Polymorphic resource — Rails groups by resource_type and runs one
    # query per type. Covers SupportCase / Opportunity / Order / Quote /
    # RoomConfiguration / SchedulerBooking *and* Invoice / Delivery / Rma /
    # CreditApplication / PurchaseOrder / CreditMemo / LocatorRecord /
    # SpiffEnrollment which were previously N+1.
    :resource
  )
}

.quofuActiveRecord::Relation<Activity>

A relation of Activities that are quofu. Active Record Scope

Returns:

See Also:



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

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

.ransackable_scopes(_auth_object = nil) ⇒ Object



301
302
303
# File 'app/models/activity.rb', line 301

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

.recency_orderActiveRecord::Relation<Activity>

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

Returns:

See Also:



134
135
# File 'app/models/activity.rb', line 134

scope :recency_order,
-> { order(Arel.sql('COALESCE(activities.completion_datetime, activities.target_datetime) DESC NULLS LAST')) }

.resource_typesObject



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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



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

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

#activity_typeActivityType



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

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

#activity_type_for_selectObject



357
358
359
# File 'app/models/activity.rb', line 357

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

#age_in_daysObject



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

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:



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

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

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



459
460
461
462
463
464
465
# File 'app/models/activity.rb', line 459

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



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
# File 'app/models/activity.rb', line 430

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



469
470
471
472
473
474
475
476
477
478
# File 'app/models/activity.rb', line 469

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



481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'app/models/activity.rb', line 481

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.instance_of?(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



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

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

#call_recordCallRecord

Returns:

See Also:



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

belongs_to :call_record, optional: true

#campaignCampaign

Returns:

See Also:



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

belongs_to :campaign, optional: true

#campaign_options_for_selectObject



382
383
384
385
386
387
388
389
# File 'app/models/activity.rb', line 382

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



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

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:



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

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

#child_activitiesActiveRecord::Relation<Activity>

Returns:

See Also:



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

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

#closed?Boolean

Returns:

  • (Boolean)


422
423
424
# File 'app/models/activity.rb', line 422

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

#closed_byParty

Returns:

See Also:



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

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

#communicationCommunication



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

belongs_to :communication, optional: true

#compact_notesObject



559
560
561
562
# File 'app/models/activity.rb', line 559

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

#complete(options = {}) ⇒ Object



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

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:



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

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

#contact_options_for_selectObject



369
370
371
372
373
374
375
376
377
378
379
380
# File 'app/models/activity.rb', line 369

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



577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
# File 'app/models/activity.rb', line 577

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:



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

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

#default_resource_for_selectObject



658
659
660
661
662
663
# File 'app/models/activity.rb', line 658

def default_resource_for_select
  return [] unless resource

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

#descriptionObject



665
666
667
# File 'app/models/activity.rb', line 665

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

#display_dateObject



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

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

#display_typeObject



426
427
428
# File 'app/models/activity.rb', line 426

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

#effective_dateObject



529
530
531
# File 'app/models/activity.rb', line 529

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

#effective_origin_dateObject



334
335
336
# File 'app/models/activity.rb', line 334

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

#embedding_content_changed?Boolean

Returns:

  • (Boolean)


598
599
600
# File 'app/models/activity.rb', line 598

def embedding_content_changed?
  saved_change_to_notes?
end

#has_time?Boolean

Returns:

  • (Boolean)


525
526
527
# File 'app/models/activity.rb', line 525

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:



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

has_many :historical_open_activities

#is_email?Object

Alias for Activity_type#is_email?

Returns:

  • (Object)

    Activity_type#is_email?

See Also:



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

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

#is_note?Boolean

Returns:

  • (Boolean)


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

def is_note?
  activity_type_id.nil?
end

#is_onsite_service?Boolean

Returns:

  • (Boolean)


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

def is_onsite_service?
  [ActivityTypeConstants::ONSITESERVICE, ActivityTypeConstants::SSI_INSTALL, ActivityTypeConstants::SSFIX_ONSITE_SERVICE, ActivityTypeConstants::SGS_ONSITESUPPORT].include?(activity_type_id)
end

#is_service_confirm?Boolean

Returns:

  • (Boolean)


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

def is_service_confirm?
  [ActivityTypeConstants::SERVICECONFIRM, ActivityTypeConstants::SSI_PREPLAN_MEET, ActivityTypeConstants::SSFIX_SERVICE_CONFIRM].include?(activity_type_id)
end

#is_service_confirm_completed?Boolean

Returns:

  • (Boolean)


767
768
769
770
771
772
773
# File 'app/models/activity.rb', line 767

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]) && (activity_result_type_id == ActivityResultTypeConstants::CMP)
end

#is_smartservice_related?Boolean

Returns:

  • (Boolean)


754
755
756
757
758
759
760
761
762
763
764
765
# File 'app/models/activity.rb', line 754

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



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

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

#mail_activitiesActiveRecord::Relation<MailActivity>

Returns:

See Also:



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

has_many :mail_activities

#new_noteObject



412
413
414
# File 'app/models/activity.rb', line 412

def new_note
  ''
end

#new_note=(n) ⇒ Object



402
403
404
405
406
407
408
409
410
# File 'app/models/activity.rb', line 402

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.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)


545
546
547
# File 'app/models/activity.rb', line 545

def open?
  activity_result_type.nil?
end

#opportunityOpportunity



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

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

#original_assigned_resourceEmployee

Returns:

See Also:



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

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

#overdue?Boolean

Returns:

  • (Boolean)


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

def overdue?
  return false if closed?
  return false if target_datetime.blank?

  (target_datetime < Time.current)
end

#parent_activityActivity

Returns:

See Also:



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

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

#partiesActiveRecord::Relation<Party>

Returns:

  • (ActiveRecord::Relation<Party>)

See Also:



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

has_and_belongs_to_many :parties

#partyParty

Returns:

See Also:



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

belongs_to :party, optional: true

#party_nameObject



418
419
420
# File 'app/models/activity.rb', line 418

def party_name
  party&.full_name
end

#preloadedActiveRecord::Relation

Eager-loads all associations required to render activity cards without N+1 queries.

Returns:

  • (ActiveRecord::Relation)


141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'app/models/activity.rb', line 141

scope :preloaded, -> {
  includes(
    :party,
    :activity_result_type,
    :campaign,
    :call_record,
    :creator,
    :updater,
    :uploads,
    { activity_type: :tag_records },
    { assigned_resource: :profile_image },
    { closed_by: :profile_image },
    { communication: :uploads },
    # parent_activity (with its activity_type) is rendered in the metadata
    # footer of the selected activity's form (_activity_form.html.erb).
    { parent_activity: :activity_type },
    # Polymorphic resource — Rails groups by resource_type and runs one
    # query per type. Covers SupportCase / Opportunity / Order / Quote /
    # RoomConfiguration / SchedulerBooking *and* Invoice / Delivery / Rma /
    # CreditApplication / PurchaseOrder / CreditMemo / LocatorRecord /
    # SpiffEnrollment which were previously N+1.
    :resource
  )
}

#primary_address_idObject



513
514
515
# File 'app/models/activity.rb', line 513

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:



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

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.



649
650
651
652
653
654
655
656
# File 'app/models/activity.rb', line 649

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



630
631
632
633
634
# File 'app/models/activity.rb', line 630

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



636
637
638
639
640
641
642
643
644
# File 'app/models/activity.rb', line 636

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



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

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

#resource_orderOrder

Returns:

See Also:



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

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

#resource_quoteQuote

Returns:

See Also:



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

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

#resource_room_configurationRoomConfiguration

Returns:

  • (RoomConfiguration)

See Also:



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

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

#resource_scheduler_bookingSchedulerBooking



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

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)



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

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:



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

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

#result_options_for_selectObject



351
352
353
354
355
# File 'app/models/activity.rb', line 351

def result_options_for_select
  return unless activity_type

  activity_type.result_options_for_select
end

#return_toObject



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

def return_to
  resource || party
end

#sales_activity?Boolean

Returns:

  • (Boolean)


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

def sales_activity?
  activity_type.try(:sales_activity)
end

#sender_for_email_templatesObject



398
399
400
# File 'app/models/activity.rb', line 398

def sender_for_email_templates
  assigned_resource || updater || creator
end

#sender_partyObject

For emails, who is the sender?

Raises:

  • (StandardError)


612
613
614
615
616
617
618
619
620
621
622
623
624
# File 'app/models/activity.rb', line 612

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, "Unable to determine sender_party for activity id #{id}" unless sp

  sp
end

#set_assigned_repObject



731
732
733
734
735
736
# File 'app/models/activity.rb', line 731

def set_assigned_rep
  return true if closed?

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

#set_campaignObject



738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
# File 'app/models/activity.rb', line 738

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



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
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
# File 'app/models/activity.rb', line 669

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).ids
  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 if pids.blank?

  # 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.lease_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



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

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)


775
776
777
778
779
780
781
# File 'app/models/activity.rb', line 775

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



342
343
344
345
346
347
348
349
# File 'app/models/activity.rb', line 342

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



504
505
506
507
508
509
510
# File 'app/models/activity.rb', line 504

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:



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

has_many :triggered_communications, class_name: 'Communication'

#unlockObject



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

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

#uploadsActiveRecord::Relation<Upload>

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



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

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

#visitVisit

Returns:

See Also:



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

belongs_to :visit, optional: true