Class: Source

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable, Models::LiquidMethods, Models::LtreeLineage, Models::LtreePathBuilder, PgSearch::Model
Defined in:
app/models/source.rb

Overview

== Schema Information

Table name: sources
Database name: primary

id :integer not null, primary key
analytics_source :string(255)
campaign :string(255)
customers_count :integer
description :string(255)
featured_until :datetime
full_name :string(500)
last_used_at :datetime
ltree_path_ids :ltree
ltree_path_slugs :ltree
name :string(255)
referral_code :string(20)
referrer_regexp :string is an Array
tracking_phone_number :string
utm_campaign :string is an Array
utm_medium :string is an Array
utm_source :string is an Array
visibility :enum default("active"), not null
created_at :datetime
updated_at :datetime
creator_id :integer
google_campaign_id :string
parent_id :integer
updater_id :integer
utm_id :string is an Array

Indexes

index_sources_on_full_name (full_name)
index_sources_on_google_campaign_id_unique (google_campaign_id) UNIQUE WHERE ((google_campaign_id IS NOT NULL) AND ((google_campaign_id)::text <> ''::text))
index_sources_on_last_used_at (last_used_at)
index_sources_on_ltree_path_ids (ltree_path_ids) USING gist
index_sources_on_ltree_path_slugs (ltree_path_slugs) USING gist
index_sources_on_name (name)
index_sources_on_parent_id (parent_id)
index_sources_on_referral_code (referral_code)

Defined Under Namespace

Classes: SetInvoiceSource

Constant Summary collapse

UNKNOWN_ID =
851
VISIBILITIES =
%w[active hidden archived].freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Attributes included from Models::LtreePathBuilder

#skip_ltree_rebuild

Has one collapse

Has many collapse

Methods included from Models::LtreeLineage

#children

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #creator, #should_not_save_version, #stamp_record, #updater

Methods included from Models::LtreePathBuilder

#build_ltree_path_ids_value, #build_ltree_path_slugs_value, #build_slug_ltree_value, builds_ltree_path, #compute_ltree_ancestor_ids, #derive_cached_ancestor_ids, #ltree_descendant_ids, rebuild_all_ltree_paths!, rebuild_subtree_ltree_paths!, syncs_items_on_ltree_change?

Methods included from Models::LtreeLineage

acts_as_ltree_lineage, ancestors_ids, #ancestors_ids, define_ltree_scopes, define_ordered_ltree_methods, #descendant_of_path?, descendants_ids, #descendants_ids, #generate_full_name, #generate_full_name_array, #lineage, #lineage_array, #lineage_simple, #ltree_ancestor_of?, #ltree_descendant_of?, #ltree_slug, #ltree_slug_path, #parent, #path_includes?, #root, #root?, #root_id, root_ids, self_ancestors_and_descendants_ids, #self_ancestors_and_descendants_ids, self_and_ancestors_ids, #self_and_ancestors_ids, #self_and_children, self_and_descendants_ids, #self_and_descendants_ids, #self_and_siblings, #siblings

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#analytics_sourceObject (readonly)



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

validates :name, :description, :campaign, :analytics_source, length: { maximum: 255 }

#campaignObject (readonly)



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

validates :name, :description, :campaign, :analytics_source, length: { maximum: 255 }

#descriptionObject (readonly)



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

validates :name, :description, :campaign, :analytics_source, length: { maximum: 255 }

#full_nameObject (readonly)



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

validates :full_name, length: { maximum: 500 }

#nameObject (readonly)



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

validates :name, presence: true

#referral_codeObject (readonly)



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

validates :referral_code, uniqueness: { allow_blank: true }

Class Method Details

.all_options_for_select(exclude_source = nil) ⇒ Object



294
295
296
297
298
299
300
301
# File 'app/models/source.rb', line 294

def self.all_options_for_select(exclude_source = nil)
  sources = Source.all
  if exclude_source
    self_and_descendants_ids = exclude_source.self_and_descendants_ids
    sources = sources.where.not(id: exclude_source.self_and_descendants_ids) if self_and_descendants_ids.present?
  end
  sources.order('full_name').map { |s| [s.name_for_select, s.id] }
end

.bing_cpcObject



116
117
118
# File 'app/models/source.rb', line 116

def self.bing_cpc
  Source.find_by(referral_code: 'BINGPPC')
end

.bing_organicObject



120
121
122
# File 'app/models/source.rb', line 120

def self.bing_organic
  Source.find_by(referral_code: 'BINGORG')
end

.campaign_options_for_select(customer, opportunity = nil, order = nil) ⇒ Object



309
310
311
312
313
314
# File 'app/models/source.rb', line 309

def self.campaign_options_for_select(customer, opportunity = nil, order = nil)
  options = customer.campaigns.active.collect { |c| c.source }.map { |s| [s.full_name, s.id] }.sort_by { |s| s[0] }
  options << [opportunity.source.full_name, opportunity.source_id] if opportunity and opportunity.source and opportunity.source.campaigns.any?
  options << [order.source.full_name, order.source_id] if order and order.source and order.source.campaigns.any?
  options
end


316
317
318
# File 'app/models/source.rb', line 316

def self.campaign_sources_featured_for_customer(customer)
  Source.joins(:campaigns).merge(customer.campaigns.active).order(:name).pluck(:name, :id)
end

.email_campaign_idObject



234
235
236
# File 'app/models/source.rb', line 234

def self.email_campaign_id
  Source.find_by(full_name: 'Campaigns > Email')&.id
end

.facebookObject



112
113
114
# File 'app/models/source.rb', line 112

def self.facebook
  Source.find_by(referral_code: 'H8SB1D')
end

.find_from_params(params, referrer: nil) ⇒ Object

Used to find source based on various web landing page urls etc



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'app/models/source.rb', line 141

def self.find_from_params(params, referrer: nil)
  s = nil
  referrer = referrer.presence
  rc = params[:referral_code].presence || params[:rc].presence || params[:utm_id].presence || params[:utm_campaign].presence

  if rc.nil? && referrer
    parsed_ref = begin
      Addressable::URI.parse(referrer)
    rescue StandardError
      nil
    end
    # Parse the referral_code or rc in the query string of the referring page
    # This is useful for detecting the source from cached page entries
    # where the first hit to our server and session creation is by global.js
    rc = parsed_ref.query_values['referral_code'] || parsed_ref.query_values['rc'] || parsed_ref.query_values['utm_id'] if parsed_ref&.query_values
  end

  # Sanitize the rc, it has to be 6 alphanum
  rc = rc&.scan(/\w{6}/)&.first&.presence

  if rc
    # The referral code always takes precedence
    s = find_by(referral_code: rc)
  end

  # utm id are some of the most specific and tried first
  if s.nil? && params[:utm_id].present?
    s = where.overlap(utm_id: [params[:utm_id]]).first
    # Try a ref code match
    s ||= find_by(referral_code: params[:utm_id])
  end

  # utm campaign? this is the most specific therefore tried first
  if s.nil? && params[:utm_campaign].present?
    s = where.overlap(utm_campaign: [params[:utm_campaign]]).first
    # Try a ref code match
    s ||= find_by(referral_code: params[:utm_campaign])
  end

  # Special case: Google organic free product listing clicks append srsltid
  # If detected, map explicitly to GOOGPL subsource
  s = find_by(referral_code: 'GOOGPL') if s.nil? && params[:srsltid].present?

  # Lookup by utm_source & medium
  if s.nil? && params[:utm_source].present?
    candidates = where.overlap(utm_source: [params[:utm_source]])
    s = candidates.where.overlap(utm_medium: [params[:utm_medium]]).first if params[:utm_medium].present?
    # If no match on utm medium we take the first match on source
    s ||= candidates.first
  end

  # referrer, we're going to use ruby's regular expression, so first detect which
  # source to evaluate then pass them through the regexp evaluator
  if s.nil? && referrer.present?
    s = where('referrer_regexp IS NOT NULL AND cardinality(referrer_regexp) > 0').detect do |source|
      source.safe_referrer_regexp.any? { |regexp| regexp.match?(referrer) }
    end
  end

  # Now fallback to legacy methods

  # _vsrefdom or other indicator or a
  s ||= google_ads if params['_vsrefdom'] == 'googleppc' ||
                      params['utm_source'] == 'googleppc' ||
                      params['gclid'].present? ||
                      referrer =~ %r{\Ahttp(s)?://www\.googleadservices\.}
  s ||= google_organic if %r{\Ahttp(s)?://www\.google\.}.match?(referrer)
  s ||= bing_cpc if params['utm_source'] == 'bing' &&
                    params['utm_medium'] == 'cpc'
  s ||= bing_organic if /bing\./.match?(referrer)
  s ||= yahoo if /yahoo\./.match?(referrer)
  s ||= twitter if %r{/t\.co/}.match?(referrer)
  s ||= facebook if /facebook\./.match?(referrer)
  s ||= pinterest if /pinterest\./.match?(referrer)
  s ||= generic_web if referrer.present? && (referrer =~ /warmlyyours\.com/).nil?
  s
end

.generic_webObject



128
129
130
# File 'app/models/source.rb', line 128

def self.generic_web
  Source.find_by(referral_code: 'WEBSE')
end


100
101
102
# File 'app/models/source.rb', line 100

def self.google_ads
  Source.find_by(referral_code: '9S6XPT')
end

.google_organicObject



104
105
106
# File 'app/models/source.rb', line 104

def self.google_organic
  Source.find_by(referral_code: '21TKYJ')
end

.grouped_options_for_select(parent_id = nil) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'app/models/source.rb', line 260

def self.grouped_options_for_select(parent_id = nil)
  sources_grouped = {}
  source = begin
    Source.find(parent_id)
  rescue StandardError
    nil
  end
  if source.nil?
    sources_grouped['Categories'] = Source.visible.roots_sorted.map { |s| [s.name, s.id] }
  else
    all_sources = []
    source_children = source.children.visible.sort_by { |s| s.full_name.to_s }
    if source_children.empty?
      # Basically do not render the tree down any further
      source.name = source.full_name
      all_sources << source
    else
      source.name = "#{source.full_name} (#{source_children.length} more choices below)"
      all_sources << source
      all_sources.concat(source_children.each { |s| s.name = " > #{s.name} [#{s.referral_code}]" })
    end
    sources_grouped["Sources for #{source.name}"] = all_sources.map { |s| [s.name, s.id] }
  end
  sources_grouped
end

.not_archivedActiveRecord::Relation<Source>

A relation of Sources that are not archived. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Source>)

See Also:



72
# File 'app/models/source.rb', line 72

scope :not_archived, -> { where.not(visibility: :archived) }

.options_for_select_by_ref(ref_code) ⇒ Object



286
287
288
289
290
291
292
# File 'app/models/source.rb', line 286

def self.options_for_select_by_ref(ref_code)
  source_name = "%#{ref_code}%"
  return Source.grouped_options_for_select(nil) if ref_code.blank?

  all_sources = Source.where('full_name ILIKE ?', source_name)
  [["Search results for #{ref_code}", all_sources.map { |s| [s.full_name, s.id] }]]
end

.outside_sales_campaign_idObject



230
231
232
# File 'app/models/source.rb', line 230

def self.outside_sales_campaign_id
  Source.find_by(full_name: 'Campaigns > Outside Sales')&.id
end

.parent_options_for_selectObject



320
321
322
# File 'app/models/source.rb', line 320

def self.parent_options_for_select
  Source.roots.order('full_name').map { |s| [s.full_name, s.id] }
end

.pinterestObject



136
137
138
# File 'app/models/source.rb', line 136

def self.pinterest
  Source.find_by(referral_code: 'Y88HHI')
end

.roots_sortedActiveRecord::Relation<Source>

A relation of Sources that are roots sorted. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Source>)

See Also:



73
# File 'app/models/source.rb', line 73

scope :roots_sorted, -> { where(Source[:parent_id].eq(nil)).sorted }

.select_optionsObject



250
251
252
# File 'app/models/source.rb', line 250

def self.select_options
  Source.visible.roots_sorted.map { |s| [s.name, s.name] }
end

.select_suboptions(option) ⇒ Object



254
255
256
257
258
# File 'app/models/source.rb', line 254

def self.select_suboptions(option)
  source_option = "%#{option}%"
  sources_suboptions = Source.where('full_name ILIKE ?', source_option)
  sources_suboptions.map { |s| s.id }
end

.sortedActiveRecord::Relation<Source>

A relation of Sources that are sorted. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Source>)

See Also:



70
# File 'app/models/source.rb', line 70

scope :sorted, -> { order(:name) }

.twitterObject



132
133
134
# File 'app/models/source.rb', line 132

def self.twitter
  Source.find_by(referral_code: 'W73BKY')
end

.unknown_sourceObject



108
109
110
# File 'app/models/source.rb', line 108

def self.unknown_source
  Source.find(UNKNOWN_ID)
end

.utm_campaign_select_optionsObject



375
376
377
378
379
# File 'app/models/source.rb', line 375

def self.utm_campaign_select_options
  Rails.cache.fetch(%i[sources utm_campaign select], expires_in: 24.hours) do
    Visit.where.not(utm_campaign: nil).distinct.pluck(:utm_campaign).map { |o| Addressable::URI.unescape(o) }.compact.uniq.sort
  end || []
end

.utm_id_select_optionsObject



387
388
389
390
391
# File 'app/models/source.rb', line 387

def self.utm_id_select_options
  Rails.cache.fetch(%i[sources utm_id select], expires_in: 24.hours) do
    Visit.where.not(utm_id: nil).distinct.pluck(:utm_id).map { |o| Addressable::URI.unescape(o) }.compact.uniq.sort
  end || []
end

.utm_medium_select_optionsObject



399
400
401
402
403
404
405
406
407
# File 'app/models/source.rb', line 399

def self.utm_medium_select_options
  Rails.cache.fetch(%i[sources utm_sources], expires_in: 24.hours) do
    Visit.where.not(utm_medium: nil).order(:utm_medium).pluck('distinct utm_medium').map(&:presence).compact.uniq.map do |o|
      Addressable::URI.unescape(o)
    rescue StandardError
      nil
    end.compact
  end || []
end

.utm_sources_select_optionsObject



359
360
361
362
363
364
365
366
367
# File 'app/models/source.rb', line 359

def self.utm_sources_select_options
  Rails.cache.fetch(%i[sources utm_sources], expires_in: 24.hours) do
    Visit.where.not(utm_source: nil).order(:utm_source).pluck('distinct utm_source').map(&:presence).compact.uniq.map do |o|
      Addressable::URI.unescape(o)
    rescue StandardError
      nil
    end.compact
  end || []
end

.visibleActiveRecord::Relation<Source>

A relation of Sources that are visible. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Source>)

See Also:



71
# File 'app/models/source.rb', line 71

scope :visible, -> { where(visibility: :active) }

.visible_options_for_select(account: nil) ⇒ Object



303
304
305
306
307
# File 'app/models/source.rb', line 303

def self.visible_options_for_select(account: nil)
  full_source_access =  && (.has_role?('marketing_rep') || .can?(:manage, Source))
  sources = full_source_access ?  Source.all : Source.visible
  sources.order(:full_name).map { |s| [s.name_for_select, s.id] }
end

.with_children_countActiveRecord::Relation<Source>

A relation of Sources that are with children count. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Source>)

See Also:



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

scope :with_children_count, -> { select('sources.*,(select count(sc.id) from sources sc where sc.parent_id = sources.id) as children_count') }

.yahooObject



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

def self.yahoo
  Source.find_by(referral_code: 'YAHOO')
end

Instance Method Details

#add_customer_to_campaign(customer) ⇒ Object



419
420
421
422
423
424
425
426
427
# File 'app/models/source.rb', line 419

def add_customer_to_campaign(customer)
  return if customer.guest?
  return unless customer.contactable?

  campaign = campaigns.first || campaign_emails.first.try(:campaign)
  return if campaign.nil?

  campaign.add_customer(customer)
end

#call_logsActiveRecord::Relation<CallLog>

Returns:

  • (ActiveRecord::Relation<CallLog>)

See Also:



64
# File 'app/models/source.rb', line 64

has_many :call_logs

#campaign_emailsActiveRecord::Relation<CampaignEmail>

Returns:

See Also:



63
# File 'app/models/source.rb', line 63

has_many :campaign_emails

#campaignsActiveRecord::Relation<Campaign>

Returns:

See Also:



62
# File 'app/models/source.rb', line 62

has_many :campaigns

#customersActiveRecord::Relation<Customer>

Returns:

See Also:



60
# File 'app/models/source.rb', line 60

has_many :customers, dependent: :nullify

#descendant_of?(other_source) ⇒ Boolean

Returns true if this source is a more specific child of the given source.
Used for "narrowing" attribution: Google (4003) → Google Ads (1155).

Returns:

  • (Boolean)


244
245
246
247
248
# File 'app/models/source.rb', line 244

def descendant_of?(other_source)
  return false if other_source.nil?

  ltree_descendant_of?(other_source)
end

#generate_ref_codeObject



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

def generate_ref_code
  begin
    # I avoid 0 and O since they could lead to confusion when printing/reciting
    alphanumerics = [('1'..'9'), ('A'..'N'), ('P'..'Z')].map { |range| range.to_a }.flatten
    new_ref_code = (0...6).map { alphanumerics[Kernel.rand(alphanumerics.size)] }.join
  end until Source.where(referral_code: new_ref_code).empty?
  new_ref_code
end

#invoicesActiveRecord::Relation<Invoice>

Returns:

  • (ActiveRecord::Relation<Invoice>)

See Also:



59
# File 'app/models/source.rb', line 59

has_many :invoices

#lead_countObject



433
434
435
# File 'app/models/source.rb', line 433

def lead_count
  Customer.non_guests.where(source_id: self_and_descendants_ids).count
end

#linked_to_campaign?Boolean

Returns:

  • (Boolean)


355
356
357
# File 'app/models/source.rb', line 355

def linked_to_campaign?
  campaigns.present? || campaign_emails.present?
end

#name_for_selectObject



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

def name_for_select
  base = read_attribute(:full_name).presence || read_attribute(:name).presence || "Source #{id}"
  n = +base.to_s
  n << " [#{referral_code}]" if referral_code.present? && !n.include?(referral_code.to_s)
  n
end

#normalize_formatObject



462
463
464
# File 'app/models/source.rb', line 462

def normalize_format
  self.tracking_phone_number = PhoneNumber.parse_and_format(tracking_phone_number)
end

#ok_to_destroy?Boolean

Returns:

  • (Boolean)


429
430
431
# File 'app/models/source.rb', line 429

def ok_to_destroy?
  customers.empty? && children.each(&:ok_to_destroy?)
end

#opportunitiesActiveRecord::Relation<Opportunity>

Returns:

See Also:



57
# File 'app/models/source.rb', line 57

has_many :opportunities

#opportunities_countObject



441
442
443
# File 'app/models/source.rb', line 441

def opportunities_count
  Opportunity.joins(:customer).where(source_id: self_and_descendants_ids).where("parties.state <> 'guest'").count
end

#ordersActiveRecord::Relation<Order>

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



58
# File 'app/models/source.rb', line 58

has_many :orders

#orders_countObject



437
438
439
# File 'app/models/source.rb', line 437

def orders_count
  Order.where(source_id: self_and_descendants_ids).count
end

#postPost

Returns:

See Also:



55
# File 'app/models/source.rb', line 55

has_one :post

#profitObject



449
450
451
# File 'app/models/source.rb', line 449

def profit
  Invoice.where(source_id: self_and_descendants_ids).sum(:profit_consolidated)
end

#referrer_regexp_select_optionsObject



415
416
417
# File 'app/models/source.rb', line 415

def referrer_regexp_select_options
  (referrer_regexp || []).map(&:presence).compact.uniq
end

#revenueObject



445
446
447
# File 'app/models/source.rb', line 445

def revenue
  Invoice.where(source_id: self_and_descendants_ids).sum(:revenue_consolidated)
end

#safe_referrer_regexpObject

Makes sure you get a clean array of Regexp object for referrer_regexp



220
221
222
223
224
225
226
227
228
# File 'app/models/source.rb', line 220

def safe_referrer_regexp
  return [] if referrer_regexp.blank?

  referrer_regexp.map do |regexp|
    Regexp.new(regexp)
  rescue StandardError
    nil
  end.compact
end

#self_and_descendants_customer_sizeObject



343
344
345
# File 'app/models/source.rb', line 343

def self_and_descendants_customer_size
  Customer.non_guests.where(source_id: self_and_descendants_ids).count
end

#self_and_descendants_events_sizeObject



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

def self_and_descendants_events_size
  Visit.where(source_id: self_and_descendants_ids).joins(:visit_events).count
end

#self_and_descendants_opportunity_sizeObject



347
348
349
# File 'app/models/source.rb', line 347

def self_and_descendants_opportunity_size
  Opportunity.where(source_id: self_and_descendants_ids).count
end

#self_and_descendants_order_sizeObject



351
352
353
# File 'app/models/source.rb', line 351

def self_and_descendants_order_size
  Order.where(source_id: self_and_descendants_ids, state: 'invoiced', order_type: 'SO').count
end

#self_and_descendants_referral_codeObject



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

def self_and_descendants_referral_code
  @self_and_descendants_referral_code ||= self_and_descendants.where.not(referral_code: nil).pluck(:referral_code)
end

#self_and_descendants_visits_sizeObject



335
336
337
# File 'app/models/source.rb', line 335

def self_and_descendants_visits_size
  Visit.by_source_id_with_descendants(id).count
end

#unknown_source?Boolean

Returns:

  • (Boolean)


238
239
240
# File 'app/models/source.rb', line 238

def unknown_source?
  id == UNKNOWN_ID
end

#utm_campaign_select_optionsObject



381
382
383
384
385
# File 'app/models/source.rb', line 381

def utm_campaign_select_options
  res = self.class.utm_campaign_select_options
  res += utm_campaign || []
  res.compact.uniq.sort
end

#utm_id_select_optionsObject



393
394
395
396
397
# File 'app/models/source.rb', line 393

def utm_id_select_options
  res = self.class.utm_id_select_options
  res += utm_id || []
  res.compact.uniq.sort
end

#utm_medium_select_optionsObject



409
410
411
412
413
# File 'app/models/source.rb', line 409

def utm_medium_select_options
  res = self.class.utm_medium_select_options
  res += utm_medium || []
  res.compact.uniq.sort
end

#utm_sources_select_optionsObject



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

def utm_sources_select_options
  res = self.class.utm_sources_select_options
  res += utm_source || []
  res.compact.uniq.sort
end

#visitsActiveRecord::Relation<Visit>

Returns:

  • (ActiveRecord::Relation<Visit>)

See Also:



61
# File 'app/models/source.rb', line 61

has_many :visits