Module: Models::CustomerSalesMarketing

Extended by:
ActiveSupport::Concern
Included in:
Customer
Defined in:
app/concerns/models/customer_sales_marketing.rb

Overview

Campaigns, sources, visit attribution, Reviews.io, watch flags, sales reps, activities.

See Also:

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.rating_options_for_selectArray<Array<String>>

Customer-rating options for select inputs: [label, code] pairs from A
(retained high potential) through F (no potential).

Returns:

  • (Array<Array<String>>)


257
258
259
260
261
262
263
264
# File 'app/concerns/models/customer_sales_marketing.rb', line 257

def rating_options_for_select
  [['Retained high potential account (1 call / 30 day)', 'A'],
   ['Retained (1 call / 45 day)', 'B'],
   ['Retained Transactional (1 call / 60 day)', 'C'],
   %w[Challenging D],
   %w[Uncommunicative E],
   ['No Potential', 'F']]
end

.rating_options_for_select_with_rankArray<Array<String>>

Rating options with the rank code prefixed into the label
(e.g. "[A] Retained high potential account (1 call / 30 day)").

Returns:

  • (Array<Array<String>>)


270
271
272
# File 'app/concerns/models/customer_sales_marketing.rb', line 270

def rating_options_for_select_with_rank
  rating_options_for_select.map { |e| ["[#{e[1]}] #{e[0]}", e[1]] }
end

.watch_states_for_selectArray<Array<String>>

Watch-state options for select inputs as [label, key] pairs.

Returns:

  • (Array<Array<String>>)


278
279
280
# File 'app/concerns/models/customer_sales_marketing.rb', line 278

def watch_states_for_select
  Customer.watches.map { |key, _val| [Customer::WATCH_STATES[key], key] }
end

Instance Method Details

#active_sales_subscriptionsActiveRecord::Relation<AudienceMember>

Active audience memberships tied to outside-sales campaigns for this
customer (by customer id or any known email address).

Returns:



31
32
33
34
35
36
# File 'app/concerns/models/customer_sales_marketing.rb', line 31

def active_sales_subscriptions
  emails = all_emails
  sql_cond = +'audience_members.customer_id = :customer_id'
  sql_cond << ' OR audience_members.email_address IN (:emails)' if emails.present?
  AudienceMember.active.where(sql_cond, customer_id: id, emails:).joins(audience: :campaigns).merge(Campaign.active.outside_sales).order('audience_members.created_at DESC'.sql_safe)
end

#active_subscriptionsActiveRecord::Relation<AudienceMember>

Active audience memberships on any campaign for this customer (by
customer id or any known email address).

Returns:



42
43
44
45
46
47
# File 'app/concerns/models/customer_sales_marketing.rb', line 42

def active_subscriptions
  emails = all_emails
  sql_cond = +'audience_members.customer_id = :customer_id'
  sql_cond << ' OR audience_members.email_address IN (:emails)' if emails.present?
  AudienceMember.active.where(sql_cond, customer_id: id, emails:).joins(audience: :campaigns).merge(Campaign.active)
end

All visits recorded for any party belonging to this customer.

Returns:

  • (ActiveRecord::Relation<Visit>)


77
78
79
# File 'app/concerns/models/customer_sales_marketing.rb', line 77

def all_related_visits
  Visit.where(user_id: all_my_party_ids)
end

#broadcast_customer_source_changedvoid

This method returns an undefined value.

Published after_commit when source change passes the guard.
CustomerSourceChangedHandler subscribes.



100
101
102
103
104
105
# File 'app/concerns/models/customer_sales_marketing.rb', line 100

def broadcast_customer_source_changed
  Rails.configuration.event_store.publish(
    Events::CustomerSourceChanged.new(data: { customer_id: id, source_id: source_id }),
    stream_name: "Customer-#{id}"
  )
end

#campaignsActiveRecord::Relation<Campaign>

Campaigns this customer is subscribed to, matched by customer id or any of
the customer's email addresses.

Returns:



20
21
22
23
24
25
# File 'app/concerns/models/customer_sales_marketing.rb', line 20

def campaigns
  emails = all_emails
  sql_cond = +'audience_members.customer_id = :customer_id'
  sql_cond << ' OR audience_members.email_address IN (:emails)' if emails.present?
  Campaign.joins(audiences: :audience_members).merge(AudienceMember.active).where(sql_cond, customer_id: id, emails:).order('audience_members.created_at DESC'.sql_safe)
end

#can_spiff?Boolean

Whether any Spiff is currently valid for this customer.

Returns:

  • (Boolean)


52
53
54
# File 'app/concerns/models/customer_sales_marketing.rb', line 52

def can_spiff?
  Spiff.valid_for_customer(self).present?
end

#check_for_open_sales_activityvoid

This method returns an undefined value.

Re-syncs the customer's open-sales-activity flag via
CustomerSalesActivityFlagSync.



148
149
150
# File 'app/concerns/models/customer_sales_marketing.rb', line 148

def check_for_open_sales_activity
  CustomerSalesActivityFlagSync.call(self)
end

#company_review_urlString

Reviews.io review-link URL for this customer's company.

Returns:

  • (String)


110
111
112
# File 'app/concerns/models/customer_sales_marketing.rb', line 110

def company_review_url
  Api::ReviewsIo::DynamicLinkBuilder.for_customer(self)
end

#create_intro_activity(description = nil) ⇒ Activity

Returns the created introduction activity.

Parameters:

  • description (String, nil) (defaults to: nil)

    optional description for the NEWAM activity

Returns:

  • (Activity)

    the created introduction activity



70
71
72
# File 'app/concerns/models/customer_sales_marketing.rb', line 70

def create_intro_activity(description = nil)
  create_activity('NEWAM', description:)
end

#human_watch_nameString

Human-readable label for the customer's watch state.

Returns:

  • (String)


124
125
126
# File 'app/concerns/models/customer_sales_marketing.rb', line 124

def human_watch_name
  Customer::WATCH_STATES[watch] || 'Unknown'
end

#last_completed_sales_activity_dateDate?

Date of the most recently completed sales activity.

Returns:

  • (Date, nil)


131
132
133
134
135
# File 'app/concerns/models/customer_sales_marketing.rb', line 131

def last_completed_sales_activity_date
  related_activities.completed.sales_activities
                    .where.not(completion_datetime: nil)
                    .order(Activity[:completion_datetime].desc).pick(:completion_datetime)&.to_date
end

#last_meeting_activities(num = 3) ⇒ ActiveRecord::Relation<Activity>

Returns most recently completed meeting activities.

Parameters:

  • num (Integer) (defaults to: 3)

    maximum number of activities returned

Returns:

  • (ActiveRecord::Relation<Activity>)

    most recently completed meeting activities



64
65
66
# File 'app/concerns/models/customer_sales_marketing.rb', line 64

def last_meeting_activities(num = 3)
  related_activities.meeting.completed.completion_order.limit(num)
end

#last_training_activities(num = 3) ⇒ ActiveRecord::Relation<Activity>

Returns most recently completed training activities.

Parameters:

  • num (Integer) (defaults to: 3)

    maximum number of activities returned

Returns:

  • (ActiveRecord::Relation<Activity>)

    most recently completed training activities



58
59
60
# File 'app/concerns/models/customer_sales_marketing.rb', line 58

def last_training_activities(num = 3)
  related_activities.training.completed.completion_order.limit(num)
end

#local_sales_rep_nameString?

Returns full name of the local sales rep.

Returns:

  • (String, nil)

    full name of the local sales rep



217
218
219
# File 'app/concerns/models/customer_sales_marketing.rb', line 217

def local_sales_rep_name
  local_sales_rep&.full_name
end

#most_recent_attributable_visit_source(window: 90.days) ⇒ Source?

Source of the customer's most recent Google-Ads-attributable visit.

Parameters:

  • window (ActiveSupport::Duration) (defaults to: 90.days)

    lookback window

Returns:



180
181
182
183
184
185
186
187
188
189
# File 'app/concerns/models/customer_sales_marketing.rb', line 180

def most_recent_attributable_visit_source(window: 90.days)
  visits
    .where(started_at: window.ago..)
    .with_any_marketing_key(Visit::GOOGLE_ADS_MARKETING_KEYS)
    .where.not(source_id: nil)
    .order(started_at: :desc)
    .limit(1)
    .pick(:source_id)
    &.then { |sid| Source.find_by(id: sid) }
end

#open_activities_counterInteger

Count of the customer's open, default-visible activities.

Returns:

  • (Integer)


162
163
164
# File 'app/concerns/models/customer_sales_marketing.rb', line 162

def open_activities_counter
  related_activities.open_activities.visible_by_default.size
end

#open_sales_activity?Boolean

Whether the customer has any open sales activities.

Returns:

  • (Boolean)


140
141
142
# File 'app/concerns/models/customer_sales_marketing.rb', line 140

def open_sales_activity?
  related_activities.open_activities.sales_activities.present?
end

#primary_sales_rep_nameString?

Returns full name of the primary sales rep.

Returns:

  • (String, nil)

    full name of the primary sales rep



207
208
209
# File 'app/concerns/models/customer_sales_marketing.rb', line 207

def primary_sales_rep_name
  primary_sales_rep&.full_name
end

#profile_nameString?

Returns name of the customer's profile.

Returns:

  • (String, nil)

    name of the customer's profile



222
223
224
# File 'app/concerns/models/customer_sales_marketing.rb', line 222

def profile_name
  profile&.name
end

#protected_leadsActiveRecord::Relation<Customer>

Leads protected by (assigned under) this customer.

Returns:



155
156
157
# File 'app/concerns/models/customer_sales_marketing.rb', line 155

def protected_leads
  Customer.protected_by(id)
end

#recalculate_profiling_information(_logger = Rails.logger) ⇒ true

-- imperative side effect: enqueues worker

Parameters:

  • _logger (Logger) (defaults to: Rails.logger)

    unused; kept for call-site compatibility

Returns:

  • (true)


246
247
248
249
# File 'app/concerns/models/customer_sales_marketing.rb', line 246

def recalculate_profiling_information(_logger = Rails.logger)
  CustomerProfilingWorker.perform_async(id)
  true
end

#resources_for_select(options = {}) ⇒ Activity::ResourceList

Returns processed resource list for select inputs.

Parameters:

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

    pagination and filtering for Activity::ResourceList

Options Hash (options):

  • :page (Integer)

    page number

  • :per_page (Integer)

    page size (default 20)

  • :filters (Hash)

    resource-type filters

  • :query (String)

    free-text filter

Returns:



239
240
241
# File 'app/concerns/models/customer_sales_marketing.rb', line 239

def resources_for_select(options = {})
  Activity::ResourceList.new.process(self, options)
end

#sales_managerParty?

The sales manager (party) responsible for the customer's catalog.

Returns:



229
230
231
# File 'app/concerns/models/customer_sales_marketing.rb', line 229

def sales_manager
  catalog.sales_manager&.party
end

#sales_repsArray<Employee>

All sales reps attached to the customer (primary, secondary, local),
deduplicated.

Returns:



195
196
197
# File 'app/concerns/models/customer_sales_marketing.rb', line 195

def sales_reps
  [primary_sales_rep, secondary_sales_rep, local_sales_rep].compact.uniq
end

#sales_reps_idsArray<Integer>

Ids of all sales reps attached to the customer, deduplicated.

Returns:

  • (Array<Integer>)


202
203
204
# File 'app/concerns/models/customer_sales_marketing.rb', line 202

def sales_reps_ids
  [primary_sales_rep_id, secondary_sales_rep_id, local_sales_rep_id].compact.uniq
end

#secondary_sales_rep_nameString?

Returns full name of the secondary sales rep.

Returns:

  • (String, nil)

    full name of the secondary sales rep



212
213
214
# File 'app/concerns/models/customer_sales_marketing.rb', line 212

def secondary_sales_rep_name
  secondary_sales_rep&.full_name
end

#should_broadcast_source_change?Boolean

Predicate used by Customer's after_commit guard. Captures the historical
condition: source_id just changed AND the new source is linked to a campaign.
saved_change_to_*? is available in after_commit (unlike *_changed?).

Returns:

  • (Boolean)


92
93
94
# File 'app/concerns/models/customer_sales_marketing.rb', line 92

def should_broadcast_source_change?
  saved_change_to_source_id? && source&.linked_to_campaign?
end

#signup_visitVisit?

The visit that created this customer, or the first recorded visit.

Returns:



12
13
14
# File 'app/concerns/models/customer_sales_marketing.rb', line 12

def 
  visit || visits.first
end

#source_for_opps_and_orders(effective_date = Date.current) ⇒ Source?

Default source for new opportunities/orders: an auto-assign campaign
with an open per-member window wins over the account's own source.

Parameters:

  • effective_date (Date) (defaults to: Date.current)

    evaluation date (historical for the order marketing tab)

Returns:



171
172
173
174
# File 'app/concerns/models/customer_sales_marketing.rb', line 171

def source_for_opps_and_orders(effective_date = Date.current)
  auto_assign_campaign = resolve_auto_assign_campaign(effective_date)
  auto_assign_campaign&.source || source
end

#source_unknown?Boolean

Whether the customer's acquisition source is unset or the placeholder
'Unknown' source.

Returns:

  • (Boolean)


85
86
87
# File 'app/concerns/models/customer_sales_marketing.rb', line 85

def source_unknown?
  source.nil? || (source && (source.name == 'Unknown'))
end

#watched?Boolean

Whether the customer is flagged for watching (automatically or forced).

Returns:

  • (Boolean)


117
118
119
# File 'app/concerns/models/customer_sales_marketing.rb', line 117

def watched?
  watch_auto? || watch_forced?
end