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 Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Belongs to collapse

Has many collapse

Has and belongs to many collapse

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Methods included from Models::Embeddable

#content_embeddings

Delegated Instance Attributes collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Embeddable

#embeddable_locales, #embedding_content_hash, #embedding_current?, #embedding_eligible?, 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, with_shape_lock

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::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#activity_result_typeActivityResultType?

Returns the result that closed the activity.

Returns:



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

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

#activity_typeActivityType?

Returns the type of this activity (nil for notes).

Returns:

  • (ActivityType, nil)

    the type of this activity (nil for notes)



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

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

#assigned_resourceEmployee?

Returns the employee the activity is assigned to.

Returns:

  • (Employee, nil)

    the employee the activity is assigned to



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

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

#assignment_resource_idsObject

Returns the value of attribute assignment_resource_ids.



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

def assignment_resource_ids
  @assignment_resource_ids
end

#call_recordCallRecord?

Returns the call that spawned this activity.

Returns:

  • (CallRecord, nil)

    the call that spawned this activity



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

belongs_to :call_record, optional: true

#campaignCampaign?

Returns the campaign this activity belongs to.

Returns:

  • (Campaign, nil)

    the campaign this activity belongs to



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

belongs_to :campaign, optional: true

#chained_activity_resultObject

Returns the value of attribute chained_activity_result.



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

def chained_activity_result
  @chained_activity_result
end

#child_activitiesActiveRecord::Associations::CollectionProxy<Activity>

Returns activities chained from this one.

Returns:

  • (ActiveRecord::Associations::CollectionProxy<Activity>)

    activities chained from this one



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

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

#closed_byParty?

Returns who closed the activity.

Returns:

  • (Party, nil)

    who closed the activity



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

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

#communicationCommunication?

Returns the communication that spawned this activity.

Returns:

  • (Communication, nil)

    the communication that spawned this activity



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

belongs_to :communication, optional: true

#customerCustomer?

Returns the party's or resource's customer (or supplier).

Returns:

  • (Customer, nil)

    the party's or resource's customer (or supplier)



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

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

#fallback_employee_idObject

Returns the value of attribute fallback_employee_id.



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

def fallback_employee_id
  @fallback_employee_id
end

#halt_chainObject

Returns the value of attribute halt_chain.



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

def halt_chain
  @halt_chain
end

#linked_party_idsObject

Returns the value of attribute linked_party_ids.



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

def linked_party_ids
  @linked_party_ids
end

#mail_activitiesActiveRecord::Associations::CollectionProxy<MailActivity>

Returns inbound/outbound mail links.

Returns:

  • (ActiveRecord::Associations::CollectionProxy<MailActivity>)

    inbound/outbound mail links



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

has_many :mail_activities

#mileageObject

Returns the value of attribute mileage.



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

def mileage
  @mileage
end

#new_target_dateObject

Returns the value of attribute new_target_date.



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

def new_target_date
  @new_target_date
end

#new_target_timeObject

Returns the value of attribute new_target_time.



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

def new_target_time
  @new_target_time
end

#opportunityOpportunity?

Returns the linked opportunity.

Returns:



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

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

#original_assigned_resourceEmployee?

Returns the employee originally assigned.

Returns:

  • (Employee, nil)

    the employee originally assigned



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

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

#parent_activityActivity?

Returns the activity this one was chained from.

Returns:

  • (Activity, nil)

    the activity this one was chained from



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

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

#partiesActiveRecord::Associations::CollectionProxy<Party>

Returns additional linked parties.

Returns:

  • (ActiveRecord::Associations::CollectionProxy<Party>)

    additional linked parties



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

has_and_belongs_to_many :parties

#partyParty?

Returns the contact/customer the activity is for.

Returns:

  • (Party, nil)

    the contact/customer the activity is for



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

belongs_to :party, optional: true

#resourceApplicationRecord?

Returns the polymorphic resource this activity concerns.

Returns:



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

belongs_to :resource, polymorphic: true, optional: true

#resource_opportunityOpportunity?

Returns typed resource shortcut for eager loading.

Returns:

  • (Opportunity, nil)

    typed resource shortcut for eager loading



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

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

#resource_orderOrder?

Returns typed resource shortcut for eager loading.

Returns:

  • (Order, nil)

    typed resource shortcut for eager loading



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

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

#resource_quoteQuote?

Returns typed resource shortcut for eager loading.

Returns:

  • (Quote, nil)

    typed resource shortcut for eager loading



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

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

#resource_room_configurationRoomConfiguration?

Returns typed resource shortcut for eager loading.

Returns:



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

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

#resource_scheduler_bookingSchedulerBooking?

Returns typed resource shortcut for eager loading.

Returns:



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

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

#service_dateObject

Returns the value of attribute service_date.



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

def service_date
  @service_date
end

#service_durationObject

Returns the value of attribute service_duration.



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

def service_duration
  @service_duration
end

#service_timeObject

Returns the value of attribute service_time.



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

def service_time
  @service_time
end

#skip_callbacksObject

Returns the value of attribute skip_callbacks.



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

def skip_callbacks
  @skip_callbacks
end

#skip_check_for_open_sales_activityObject

Returns the value of attribute skip_check_for_open_sales_activity.



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

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.



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

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.



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

def support_case_closed_reason
  @support_case_closed_reason
end

#support_case_eventObject

Returns the value of attribute support_case_event.



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

def support_case_event
  @support_case_event
end

#travel_timeObject

Returns the value of attribute travel_time.



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

def travel_time
  @travel_time
end

#triggered_communicationsActiveRecord::Associations::CollectionProxy<Communication>

Returns communications this activity triggered.

Returns:

  • (ActiveRecord::Associations::CollectionProxy<Communication>)

    communications this activity triggered



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

has_many :triggered_communications,
class_name: 'Communication',
foreign_key: :triggered_by_activity_id,
inverse_of: :triggered_activity

#updated_activitiesObject

Returns the value of attribute updated_activities.



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

def updated_activities
  @updated_activities
end

#uploadsActiveRecord::Associations::CollectionProxy<Upload>

Returns files attached to this activity.

Returns:

  • (ActiveRecord::Associations::CollectionProxy<Upload>)

    files attached to this activity



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

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

#visitVisit?

Returns the website visit that spawned this activity.

Returns:

  • (Visit, nil)

    the website visit that spawned this activity



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

belongs_to :visit, optional: true

Class Method Details

.all_activity_filtersArray<String>

Returns resource types filterable in the activity list.

Returns:

  • (Array<String>)

    resource types filterable in the activity list



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

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:



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

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:



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

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

.embeddable_content_typesArray<Symbol>

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

Returns:

  • (Array<Symbol>)

    the content types available for embeddings



696
697
698
# File 'app/models/activity.rb', line 696

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:



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

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:



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

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

.heatwave_assistant_idInteger?

Returns the id of the Heatwave assistant employee (bot account).

Returns:

  • (Integer, nil)

    the id of the Heatwave assistant employee (bot account)



765
766
767
# File 'app/models/activity.rb', line 765

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:



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

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:



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

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:



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

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:



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

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:



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

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

.mcp_searchable?Boolean

Returns false — activities are excluded from MCP search.

Returns:

  • (Boolean)

    false — activities are excluded from MCP search



701
702
703
# File 'app/models/activity.rb', line 701

def self.mcp_searchable?
  false
end

.meetingActiveRecord::Relation<Activity>

A relation of Activities that are meeting. Active Record Scope

Returns:

See Also:



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

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:



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

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

.non_cob_time(datetime) ⇒ String?

Returns formatted time when it falls outside close-of-business rounding.

Parameters:

  • datetime (Time, DateTime, nil)

    the time to check

Returns:

  • (String, nil)

    formatted time when it falls outside close-of-business rounding



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

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:



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

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:



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'app/models/activity.rb', line 222

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(tags: { name: 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:



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

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:



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

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:



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

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

.position_of_activity(activity_id, relation) ⇒ Integer?

Returns 1-based position of the activity within the relation.

Parameters:

  • activity_id (Integer)

    the activity to locate

  • relation (ActiveRecord::Relation<Activity>)

    the ordered relation to search

Returns:

  • (Integer, nil)

    1-based position of the activity within the relation



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

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:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'app/models/activity.rb', line 191

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:



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

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

.ransackable_scopes(_auth_object = nil) ⇒ Array<Symbol>

Returns scopes ransack may call.

Parameters:

  • _auth_object (Object, nil) (defaults to: nil)

    unused; ransack API signature

Returns:

  • (Array<Symbol>)

    scopes ransack may call



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

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:



184
185
# File 'app/models/activity.rb', line 184

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

.resource_typesArray<String>

Returns recognized resource_type values.

Returns:

  • (Array<String>)

    recognized resource_type values



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



215
216
217
218
# File 'app/models/activity.rb', line 215

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_type_for_selectArray

Returns activity type options scoped to the party.

Returns:

  • (Array)

    activity type options scoped to the party



424
425
426
# File 'app/models/activity.rb', line 424

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

#age_in_daysInteger

Returns days from the effective origin date to completion (or today).

Returns:

  • (Integer)

    days from the effective origin date to completion (or today)



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

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

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

Auto-assigns this activity to the best available resource.

Parameters:

  • allocate_on (Date, Time, nil) (defaults to: nil)

    the date to allocate the activity on

  • current_user (Employee, nil) (defaults to: nil)

    the currently logged in user

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

    assignment options, passed through to the resource lookup

Options Hash (options):

  • ignore_target_date (Boolean)

    skip the rep availability check for the target date

Returns:

  • (Employee, nil)

    the assigned resource



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

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 = {}) ⇒ Boolean

Assigns and schedules this activity on the best available date.

Parameters:

  • ideal_target_date (Date, Time, nil) (defaults to: nil)

    the preferred target date

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

    assignment options, passed through to the resource lookup

Options Hash (options):

  • logger (Logger, nil)

    logger to use instead of Rails.logger

  • ignore_target_date (Boolean)

    skip the rep availability check for the target date

Returns:

  • (Boolean)

    whether the activity was assigned and saved



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
542
543
544
# File 'app/models/activity.rb', line 517

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) ⇒ Symbol

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

Parameters:

  • auto_save (Boolean) (defaults to: true)

    whether to persist the result change

Returns:

  • (Symbol)

    :not_applicable, :not_overdue, or :overdue_auto_close



565
566
567
568
569
570
571
572
573
574
# File 'app/models/activity.rb', line 565

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 = {}) ⇒ Integer?

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

Parameters:

  • current_user (Employee, nil) (defaults to: nil)

    the currently logged in user

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

    assignment options, passed through to the resource lookup

Options Hash (options):

  • ignore_target_date (Boolean)

    skip the rep availability check for the target date

Returns:

  • (Integer, nil)

    the id of the best resource



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

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 = {}) ⇒ Employee?

Returns the best available employee resource for this activity.

Parameters:

  • current_user (Employee, nil) (defaults to: nil)

    the currently logged in user

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

    assignment options, passed through to the resource lookup

Options Hash (options):

  • ignore_target_date (Boolean)

    skip the rep availability check for the target date

Returns:

  • (Employee, nil)

    the best resource employee



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

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

#campaign_options_for_selectArray<Array(String, Integer)>

Returns campaign name/id pairs for the customer.

Returns:

  • (Array<Array(String, Integer)>)

    campaign name/id pairs for the customer



453
454
455
456
457
458
459
460
# File 'app/models/activity.rb', line 453

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

#cancel(options = {}) ⇒ Boolean

Marks this activity cancelled.

Parameters:

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

    attribute overrides merged into the update

Options Hash (options):

  • activity_result_type_id (Integer)

    result type id, defaults to CANCEL

  • completion_datetime (Time)

    completion time, defaults to now

Returns:

  • (Boolean)

    whether the update succeeded



683
684
685
686
# File 'app/models/activity.rb', line 683

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:



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

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

#closed?Boolean

Returns true for notes or activities with a result.

Returns:

  • (Boolean)

    true for notes or activities with a result



501
502
503
# File 'app/models/activity.rb', line 501

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

#compact_notesString?

Returns notes (or description) with bracketed stamps stripped.

Returns:

  • (String, nil)

    notes (or description) with bracketed stamps stripped



689
690
691
692
# File 'app/models/activity.rb', line 689

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

#complete(options = {}) ⇒ Boolean

Marks this activity complete.

Parameters:

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

    attribute overrides merged into the update

Options Hash (options):

  • activity_result_type_id (Integer)

    result type id, defaults to CMP

  • completion_datetime (Time)

    completion time, defaults to now

Returns:

  • (Boolean)

    whether the update succeeded



672
673
674
675
# File 'app/models/activity.rb', line 672

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:



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

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

#contact_options_for_selectArray

Returns contact/organization options from the address book builder.

Returns:

  • (Array)

    contact/organization options from the address book builder



439
440
441
442
443
444
445
446
447
448
449
450
# File 'app/models/activity.rb', line 439

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) ⇒ String?

Builds the text embedded for semantic search.

Parameters:

  • _content_type (Symbol) (defaults to: :primary)

    only :primary is supported

Returns:

  • (String, nil)

    nil when notes are blank or too short



711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
# File 'app/models/activity.rb', line 711

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

#default_resource_for_selectArray

Returns single [display, identifier] option for the current resource.

Returns:

  • (Array)

    single [display, identifier] option for the current resource



804
805
806
807
808
809
# File 'app/models/activity.rb', line 804

def default_resource_for_select
  return [] unless resource

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

#descriptionString?

Returns first 500 chars of notes, else the type's description.

Returns:

  • (String, nil)

    first 500 chars of notes, else the type's description



812
813
814
# File 'app/models/activity.rb', line 812

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

#display_dateString

Returns effective date formatted for CRM display.

Returns:

  • (String)

    effective date formatted for CRM display



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

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

#display_typeString

Returns the activity type's task type, or "Note".

Returns:

  • (String)

    the activity type's task type, or "Note"



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

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

#effective_dateTime

Returns completion, else target, else creation timestamp.

Returns:

  • (Time)

    completion, else target, else creation timestamp



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

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

#effective_origin_dateDate

Returns original target date, falling back to target or creation date.

Returns:

  • (Date)

    original target date, falling back to target or creation date



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

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

#embedding_content_changed?Boolean

Returns true when notes changed (embedding needs refresh).

Returns:

  • (Boolean)

    true when notes changed (embedding needs refresh)



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

def embedding_content_changed?
  saved_change_to_notes?
end

#has_time?Boolean

Returns true when the effective date carries a non-midnight time.

Returns:

  • (Boolean)

    true when the effective date carries a non-midnight time



637
638
639
# File 'app/models/activity.rb', line 637

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:



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

has_many :historical_open_activities

#is_email?Object

Alias for Activity_type#is_email?

Returns:

  • (Object)

    Activity_type#is_email?

See Also:



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

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

#is_note?Boolean

Returns true when the activity is a plain note (no type).

Returns:

  • (Boolean)

    true when the activity is a plain note (no type)



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

def is_note?
  activity_type_id.nil?
end

#is_onsite_service?Boolean

Returns true for on-site service activity types.

Returns:

  • (Boolean)

    true for on-site service activity types



657
658
659
# File 'app/models/activity.rb', line 657

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 true for service-confirm activity types.

Returns:

  • (Boolean)

    true for service-confirm activity types



652
653
654
# File 'app/models/activity.rb', line 652

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 true when a service-confirm type completed with CMP.

Returns:

  • (Boolean, nil)

    true when a service-confirm type completed with CMP



923
924
925
926
927
928
929
# File 'app/models/activity.rb', line 923

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 true when the type is any SmartService activity type.

Returns:

  • (Boolean)

    true when the type is any SmartService activity type



909
910
911
912
913
914
915
916
917
918
919
920
# File 'app/models/activity.rb', line 909

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

#lockBoolean

Locks the target datetime, skipping callbacks.

Returns:

  • (Boolean)

    result of the save



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

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

#new_noteString

Returns always blank; pairs with #new_note= for form binding.

Returns:

  • (String)

    always blank; pairs with #new_note= for form binding



489
490
491
# File 'app/models/activity.rb', line 489

def new_note
  ''
end

#new_note=(n) ⇒ void

This method returns an undefined value.

Appends a timestamped note to the notes field.

Parameters:

  • n (String, nil)

    the note text to append (blank is ignored)



478
479
480
481
482
483
484
485
486
# File 'app/models/activity.rb', line 478

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 true when no result type has been set.

Returns:

  • (Boolean)

    true when no result type has been set



662
663
664
# File 'app/models/activity.rb', line 662

def open?
  activity_result_type.nil?
end

#overdue?Boolean

Returns true when open and past its target datetime.

Returns:

  • (Boolean)

    true when open and past its target datetime



463
464
465
466
467
468
# File 'app/models/activity.rb', line 463

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

  (target_datetime < Time.current)
end

#party_nameString?

Returns the party's full name.

Returns:

  • (String, nil)

    the party's full name



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

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)


191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'app/models/activity.rb', line 191

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_idInteger?

Returns the customer's mailing/shipping/billing address id.

Returns:

  • (Integer, nil)

    the customer's mailing/shipping/billing address id



622
623
624
# File 'app/models/activity.rb', line 622

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

#resource_combo=(val) ⇒ void

This method returns an undefined value.

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.

Parameters:

  • val (String, nil)

    "ClassName|id" combo, or blank to clear



794
795
796
797
798
799
800
801
# File 'app/models/activity.rb', line 794

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_gidString?

Returns GlobalID URI for the polymorphic resource.

Returns:

  • (String, nil)

    GlobalID URI for the polymorphic resource



770
771
772
773
774
# File 'app/models/activity.rb', line 770

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) ⇒ void

This method returns an undefined value.

Sets the polymorphic resource from a GlobalID string.

Parameters:

  • gid_string (String, nil)

    the GlobalID URI, or blank to clear



779
780
781
782
783
784
785
786
787
# File 'app/models/activity.rb', line 779

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_support_caseSupportCase

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



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

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:



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

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

#result_options_for_selectArray?

Returns result options for the activity's type.

Returns:

  • (Array, nil)

    result options for the activity's type



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

def result_options_for_select
  return unless activity_type

  activity_type.result_options_for_select
end

#return_toObject?

Returns record to redirect back to (resource, else party).

Returns:

  • (Object, nil)

    record to redirect back to (resource, else party)



434
435
436
# File 'app/models/activity.rb', line 434

def return_to
  resource || party
end

#sales_activity?Boolean?

Returns whether the activity type is a sales activity.

Returns:

  • (Boolean, nil)

    whether the activity type is a sales activity



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

def sales_activity?
  activity_type.try(:sales_activity)
end

#sender_for_email_templatesEmployee?

Returns assigned resource, falling back to updater then creator.

Returns:

  • (Employee, nil)

    assigned resource, falling back to updater then creator



471
472
473
# File 'app/models/activity.rb', line 471

def sender_for_email_templates
  assigned_resource || updater || creator
end

#sender_partyParty

For emails, who is the sender?

Returns:

  • (Party)

    the party that should be used as the sender

Raises:

  • (StandardError)


750
751
752
753
754
755
756
757
758
759
760
761
762
# File 'app/models/activity.rb', line 750

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_repBoolean, ...

Defaults assigned/original resource for open activities before save.

Returns:

  • (Boolean, Employee, nil)

    true when closed, else the assignment



883
884
885
886
887
888
# File 'app/models/activity.rb', line 883

def set_assigned_rep
  return true if closed?

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

#set_campaignvoid

This method returns an undefined value.

Subscribes the customer to, and assigns, the applicable campaign.



892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
# File 'app/models/activity.rb', line 892

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_partiesvoid

This method returns an undefined value.

Reconciles the parties join table with party_id, linked_party_ids, and
resource-reported ids, dropping ids that no longer exist.



819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
# File 'app/models/activity.rb', line 819

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_dateString?

Returns target date formatted for CRM display.

Returns:

  • (String, nil)

    target date formatted for CRM display



402
403
404
# File 'app/models/activity.rb', line 402

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 true when no target datetime or it falls outside working hours.

Returns:

  • (Boolean)

    true when no target datetime or it falls outside working hours



932
933
934
935
936
937
938
# File 'app/models/activity.rb', line 932

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_timeString?

Returns target time formatted "03:30 pm".

Returns:

  • (String, nil)

    target time formatted "03:30 pm"



407
408
409
410
411
412
413
414
# File 'app/models/activity.rb', line 407

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_sString Also known as: name

Returns "date rep type" summary line.

Returns:

  • (String)

    "date rep type" summary line



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

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

#unlockBoolean

Unlocks the target datetime, skipping callbacks.

Returns:

  • (Boolean)

    result of the save



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

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