Class: CustomerSearch

Inherits:
Search show all
Includes:
Models::RepSearchable, Models::SearchAddressable
Defined in:
app/models/customer_search.rb

Overview

== Schema Information

Table name: searches
Database name: primary

id :integer not null, primary key
name :string(255)
persist :boolean default(FALSE)
pinned :boolean default(FALSE), not null
query_params :jsonb
result_set_size :integer default(0)
selected_columns :string is an Array
set_limit :integer default(25)
sort_column :string(255)
sort_columns :string default([]), not null, is an Array
sort_direction :string(255) default("ASC")
type :string(255) not null
created_at :datetime
updated_at :datetime
employee_id :integer

Indexes

employee_id_pinned (employee_id,pinned)
employee_id_type (employee_id,type)

Constant Summary collapse

CUSTOM_CRITERIA_KEYS =

Keys consumed by apply_custom_criteria — excluded from the Ransack strict probe
so we don't get false-positive InvalidSearchError reports in AppSignal.

%w[
  has_ordered_item_id has_ordered_product_category_id has_ordered_product_line_id
  has_ordered_days has_ordered_days_time_range_gteq has_ordered_days_time_range_lteq
  minimum_number_of_orders has_ordered_type
  campaign_id_in campaign_id_not_in has_campaign_activity_status
  custom_drop_reason_codes custom_drop_rep_ids
  profile_in profile_not_in is_certified_installer
].freeze

Constants inherited from Search

Search::DISTANCE_SEARCH_KEYS

Instance Attribute Summary

Attributes inherited from Search

#composite_columns, #pagy_count, #pagy_limit, #pagy_page, #sort_column1, #sort_column2, #sort_column3, #sql_select_columns

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::SearchAddressable

#mass_googlemap

Methods inherited from Search

#all_composite_columns, allowed_role_ids, #append_custom_column, #append_ouput_column, #append_to_sql_select_columns, #applicable_sort_terms, #apply_array_match, #apply_criteria, #apply_customer_cross_reference, #apply_distance_search, #apply_select, #apply_sort, #available_events, available_output_columns, #available_output_columns, base_search_class_name, #cleanup_search_results, composite_column, #composite_column, database_columns, default_columns, default_sort, #discard_excess, #effective_name, #effective_short_name, #employee, #enqueue_navbar_pinned_refresh, #fast_count, favorites, friendly_column_name, #get_pinned_results, #get_sort_term, global_favorite, has_role_for_search?, #human_query_params, instantiate_query_template, #instantiate_resource_updater, #limit_options, main_resource_class, main_resource_table, mass_actions_for_select, #mass_export, maximum_unpersisted_queries, #normalize_for_mass_update, options, options_classes, #output_columns_for_select, #pagy_from_search, #perform, #perform_selected, #pinned_query, #prepend_custom_column, #prepend_output_column, query_favorites_templates, #ransack_probe_params, recent_searches_for_employee_id, #record_list, #refresh_pinned_results, #remove_other_pins, search_name, #search_name, #search_results, search_type, search_type_humanized, select_sort_columns, #select_sort_columns, #select_statement, #set_sort_term, #sort_columns_for_select, #sort_columns_to_hash, #target_geocoding, unpersisted_queries_quote_reached?, #unrecord_list, #validated_selected_columns, view_resource_class, view_resource_klass, #view_resource_klass, view_resource_table, visible?, with_search_results

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Class Method Details

.custom_criteria_keysObject



93
94
95
# File 'app/models/customer_search.rb', line 93

def self.custom_criteria_keys
  CUSTOM_CRITERIA_KEYS
end

.scope_options_for_mass_schedule_activityObject



325
326
327
# File 'app/models/customer_search.rb', line 325

def self.scope_options_for_mass_schedule_activity
  [['Customer Only', 'customer_only'], ['Primary Contact', 'primary_contact'], ['All Active Contacts', 'all_active_contacts']]
end

.select_options_for_profileObject



394
395
396
# File 'app/models/customer_search.rb', line 394

def self.select_options_for_profile
  [['Any Organization', 'ORG']] + Profile.options_for_select(false)
end

Instance Method Details

#address_id_fieldsObject



390
391
392
# File 'app/models/customer_search.rb', line 390

def address_id_fields
  [%w[Shipping shipping_address_id], %w[Billing billing_address_id], %w[Mailing mailing_address_id]]
end

#apply_custom_criteria(results) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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
# File 'app/models/customer_search.rb', line 108

def apply_custom_criteria(results)
  results = apply_distance_search(results, custom_table_join: 'INNER JOIN addresses on addresses.party_id = view_customers.id', custom_table_alias: 'addresses')

  has_ordered_query = []
  has_quoted_query = []
  unless query_params[:has_ordered_item_id].blank?
    cond = "line_items.item_id in (#{query_params[:has_ordered_item_id].join(',')})"
    has_ordered_query << cond
    has_quoted_query << cond
  end
  unless query_params[:has_ordered_product_category_id].blank?
    # Use ltree inline subselect for descendants - single query, no ID expansion needed
    pc_ids = [query_params[:has_ordered_product_category_id]].flatten.compact.map(&:to_i)
    cond = "items.pc_path_ids <@ ANY(SELECT ltree_path_ids FROM product_categories WHERE id = ANY(ARRAY[#{pc_ids.join(',')}]::integer[]))"
    has_ordered_query << cond
    has_quoted_query << cond
  end
  unless query_params[:has_ordered_product_line_id].blank?
    # Use ltree inline subselect for descendants - single query, no ID expansion needed
    pl_ids = [query_params[:has_ordered_product_line_id]].flatten.compact.map(&:to_i)
    cond = "items.primary_pl_path_ids <@ ANY(SELECT ltree_path_ids FROM product_lines WHERE id = ANY(ARRAY[#{pl_ids.join(',')}]::integer[]))"
    has_ordered_query << cond
    has_quoted_query << cond
  end

  unless query_params[:has_ordered_days].blank?
    has_ordered_query << "orders.shipped_date >= '#{Date.current.days_ago(query_params[:has_ordered_days].to_i)}'"
    has_quoted_query << "quotes.complete_datetime >= '#{Date.current.days_ago(query_params[:has_ordered_days].to_i)}'"
  end

  unless query_params[:has_ordered_days_time_range_gteq].blank?
    if query_params[:has_ordered_days_time_range_gteq].present? && query_params[:has_ordered_days_time_range_lteq].present?
      has_ordered_query << "orders.shipped_date between '#{Date.current.days_ago(query_params[:has_ordered_days_time_range_lteq].to_i)}' and '#{Date.current.days_ago(query_params[:has_ordered_days_time_range_gteq].to_i)}'"
      has_quoted_query << "quotes.complete_datetime between '#{Date.current.days_ago(query_params[:has_ordered_days_time_range_lteq].to_i)}' and '#{Date.current.days_ago(query_params[:has_ordered_days_time_range_gteq].to_i)}'"
    else
      has_ordered_query << "orders.shipped_date >= '#{Date.current.days_ago(query_params[:has_ordered_days_time_range_gteq].to_i)}'"
      has_quoted_query << "quotes.complete_datetime >= '#{Date.current.days_ago(query_params[:has_ordered_days_time_range_gteq].to_i)}'"
    end
  end
  if has_ordered_query.any?
    oq = "from line_items inner join orders on orders.id = line_items.resource_id and line_items.resource_type = 'Order' inner join items on items.id = line_items.item_id inner join item_product_lines on item_product_lines.item_id = items.id where orders.customer_id = view_customers.id and orders.state = 'invoiced' and #{has_ordered_query.join(' and ')}"
    qq = "from line_items inner join quotes on quotes.id = line_items.resource_id and line_items.resource_type = 'Quote' inner join opportunities on opportunities.id = quotes.opportunity_id inner join items on items.id = line_items.item_id inner join item_product_lines on item_product_lines.item_id = items.id where opportunities.customer_id = view_customers.id and quotes.state = 'complete' and #{has_quoted_query.join(' and ')}"
    order_query = "EXISTS(select 1 #{oq})"
    quote_query = "EXISTS(select 1 #{qq})"
    unless query_params[:minimum_number_of_orders].blank?
      order_query += " and (select COUNT(distinct resource_id) #{oq}) >= #{query_params[:minimum_number_of_orders]}"
      quote_query += " and (select COUNT(distinct resource_id) #{qq}) >= #{query_params[:minimum_number_of_orders]}"
    end
    results = case query_params[:has_ordered_type]
              when 'both'
                results.where("(#{order_query} or #{quote_query})")
              when 'quotes'
                results.where(quote_query)
              else
                results.where(order_query)
              end
  end
  unless query_params[:campaign_id_in].blank?
    results = results.where("view_customers.id in (select distinct subscribers.customer_id from campaigns
                              LEFT OUTER JOIN campaigns_subscriber_lists ON campaigns_subscriber_lists.campaign_id = campaigns.id
                              LEFT OUTER JOIN subscriber_lists ON subscriber_lists.id = campaigns_subscriber_lists.subscriber_list_id
                              LEFT OUTER JOIN subscribers ON subscribers.subscriber_list_id = subscriber_lists.id
                              where campaigns.campaign_type = 'outside_sales' and campaigns.id in (#{query_params[:campaign_id_in].join(',')}))")
    case query_params[:has_campaign_activity_status]
    when 'open'
      results = results.where("EXISTS(select 1 from activities where activities.party_id = view_customers.id and campaign_id in (#{query_params[:campaign_id_in].join(',')}) and activity_type_id is not null and activity_result_type_id is null)")
    when 'any'
      results = results.where("EXISTS(select 1 from activities where activities.party_id = view_customers.id and campaign_id in (#{query_params[:campaign_id_in].join(',')}) and activity_type_id is not null)")
    when 'none'
      results = results.where("NOT EXISTS(select 1 from activities where activities.party_id = view_customers.id and campaign_id in (#{query_params[:campaign_id_in].join(',')}) and activity_type_id is not null)")
    end
  end
  unless query_params[:campaign_id_not_in].blank?
    results = results.where("NOT EXISTS(select 1 from campaigns LEFT OUTER JOIN campaigns_subscriber_lists ON campaigns_subscriber_lists.campaign_id = campaigns.id LEFT OUTER JOIN subscriber_lists ON subscriber_lists.id = campaigns_subscriber_lists.subscriber_list_id LEFT OUTER JOIN subscribers ON subscribers.subscriber_list_id = subscriber_lists.id LEFT OUTER JOIN contact_points ON contact_points.detail = subscribers.email_address LEFT OUTER JOIN parties ON parties.id = contact_points.party_id where (subscribers.customer_id = view_customers.id or parties.id = view_customers.id) and campaigns.id in (#{query_params[:campaign_id_not_in].join(',')}))")
  end

  drop_reason_codes = [query_params[:custom_drop_reason_codes]].flatten.map(&:presence).compact
  if drop_reason_codes.present?
    results = results.joins(:customer_drop_events).where(customer_drop_events: { reason_code: drop_reason_codes })
  end

  drop_rep_ids = [query_params[:custom_drop_rep_ids]].flatten.map(&:presence).compact
  if drop_rep_ids.present?
    results = results.joins(:customer_drop_events).where(customer_drop_events: { sales_rep_id: drop_rep_ids })
  end

  profile_ids = [query_params[:profile_in]].flatten.map(&:presence).compact
  if profile_ids.present?
    if profile_ids.delete('ORG')
      results = results.where.not(profile_id: [ProfileConstants::HOMEOWNER])
    end
    results = results.where(profile_id: profile_ids) if profile_ids.present?
  end
  not_profile_ids = [query_params[:profile_not_in]].flatten.map(&:presence).compact
  if not_profile_ids.present?
    results = results.where(profile_id: ProfileConstants::HOMEOWNER) if not_profile_ids.delete('ORG')
    results = results.where.not(profile_id: not_profile_ids) if not_profile_ids.present?
  end

  if query_params[:is_certified_installer].present? && query_params[:is_certified_installer].to_b
    results = results.joins(:certifications)
  end
  results
end

#drop_action_reps_for_selectObject

Mass actions section #



217
218
219
220
# File 'app/models/customer_search.rb', line 217

def drop_action_reps_for_select
  selected_customer_ids = search_results.pluck(:resource_id)
  Employee.assigned_sales_rep_options_for_select(customer_ids: selected_customer_ids)
end

#mass_assign_reps(params, cur_user = nil) ⇒ Object



222
223
224
225
226
227
228
229
230
# File 'app/models/customer_search.rb', line 222

def mass_assign_reps(params, cur_user = nil)
  jid = MassSearch::CustomerAssignRepsWorker.perform_async(
    'search_id'     => id,
    'action_params' => { 'resource_params' => (params[:resource] || {}).to_h, 'activity' => (params[:activity] || {}).to_h },
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_assign_source(params, cur_user = nil) ⇒ Object



246
247
248
249
250
251
252
253
254
# File 'app/models/customer_search.rb', line 246

def mass_assign_source(params, cur_user = nil)
  jid = ::SearchResourceUpdateWorker.perform_async(
    'search_id'     => id,
    'action_params' => { 'resource_params' => { 'source_id' => params[:source_id] } },
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_assign_to_campaign(params, cur_user = nil) ⇒ Object



276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'app/models/customer_search.rb', line 276

def mass_assign_to_campaign(params, cur_user = nil)
  campaign_params = params[:campaign] || {}
  if campaign_params[:campaign_id].blank? && campaign_params[:new_campaign_name].blank?
    return { status: :error, message: 'You must specify an existing campaign or enter a name for a new campaign' }
  end

  jid = MassSearch::CustomerAssignToCampaignWorker.perform_async(
    'search_id'     => id,
    'action_params' => { 'campaign' => campaign_params.to_h },
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_assign_to_subscriber_list(params, cur_user = nil) ⇒ Object



266
267
268
269
270
271
272
273
274
# File 'app/models/customer_search.rb', line 266

def mass_assign_to_subscriber_list(params, cur_user = nil)
  jid = MassSearch::CustomerAssignToSubscriberListWorker.perform_async(
    'search_id'     => id,
    'action_params' => { 'subscriber_list_id' => params[:subscriber_list_id] },
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_auto_assign_sales_rep(params, cur_user = nil) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'app/models/customer_search.rb', line 232

def mass_auto_assign_sales_rep(params, cur_user = nil)
  jid = MassSearch::CustomerAutoAssignSalesRepWorker.perform_async(
    'search_id'     => id,
    'action_params' => {
      'remove_existing_reps'    => params[:remove_existing_reps],
      'skip_lead_assignment'    => params[:skip_lead_assignment],
      'activity'                => (params[:activity] || {}).to_h
    },
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_create_subscriber_list(params, cur_user = nil) ⇒ Object



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

def mass_create_subscriber_list(params, cur_user = nil)
  jid = MassSearch::CustomerCreateSubscriberListWorker.perform_async(
    'search_id'     => id,
    'action_params' => { 'subscriber_list' => (params[:subscriber_list] || {}).to_h },
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_delete_on_lead_qualify_and_guest_state(_params, cur_user = nil) ⇒ Object



301
302
303
304
305
306
307
308
309
# File 'app/models/customer_search.rb', line 301

def mass_delete_on_lead_qualify_and_guest_state(_params, cur_user = nil)
  jid = MassSearch::CustomerDeleteLeadsWorker.perform_async(
    'search_id'     => id,
    'action_params' => {},
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_drop_sales_rep(params, cur_user) ⇒ Object



329
330
331
332
333
334
335
336
337
# File 'app/models/customer_search.rb', line 329

def mass_drop_sales_rep(params, cur_user)
  jid = MassSearch::CustomerDropSalesRepWorker.perform_async(
    'search_id'     => id,
    'action_params' => { 'sales_rep_id' => params[:sales_rep_id], 'reason_code' => params[:reason_code], 'comment' => params[:comment] },
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_locator_blacklist(params, cur_user) ⇒ Object



349
350
351
352
353
354
355
356
357
# File 'app/models/customer_search.rb', line 349

def mass_locator_blacklist(params, cur_user)
  jid = MassSearch::CustomerLocatorBlacklistWorker.perform_async(
    'search_id'     => id,
    'action_params' => { 'reason' => params[:reason] },
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_locator_unblacklist(params, cur_user) ⇒ Object



369
370
371
372
373
374
375
376
377
# File 'app/models/customer_search.rb', line 369

def mass_locator_unblacklist(params, cur_user)
  jid = MassSearch::CustomerLocatorUnblacklistWorker.perform_async(
    'search_id'     => id,
    'action_params' => { 'inform_customer' => params[:inform_customer] },
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_locator_unwhitelist(_params, cur_user) ⇒ Object



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

def mass_locator_unwhitelist(_params, cur_user)
  jid = MassSearch::CustomerLocatorUnwhitelistWorker.perform_async(
    'search_id'     => id,
    'action_params' => {},
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_locator_whitelist(params, cur_user) ⇒ Object



339
340
341
342
343
344
345
346
347
# File 'app/models/customer_search.rb', line 339

def mass_locator_whitelist(params, cur_user)
  jid = MassSearch::CustomerLocatorWhitelistWorker.perform_async(
    'search_id'     => id,
    'action_params' => { 'reason' => params[:reason] },
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_online_account_invite(_params, cur_user = nil) ⇒ Object



379
380
381
382
383
384
385
386
387
# File 'app/models/customer_search.rb', line 379

def (_params, cur_user = nil)
  jid = MassSearch::CustomerOnlineAccountInviteWorker.perform_async(
    'search_id'     => id,
    'action_params' => {},
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_schedule_activity(params, cur_user = nil) ⇒ Object



311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'app/models/customer_search.rb', line 311

def mass_schedule_activity(params, cur_user = nil)
  activity_params = (params[:activity] || {}).to_h
  apply_scope = activity_params.delete('apply_scope')
  campaign_id = activity_params.delete('campaign_id')

  jid = MassSearch::CustomerScheduleActivityWorker.perform_async(
    'search_id'     => id,
    'action_params' => { 'activity' => activity_params, 'apply_scope' => apply_scope, 'campaign_id' => campaign_id },
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#mass_switch_rep_roles(_params, cur_user = nil) ⇒ Object



291
292
293
294
295
296
297
298
299
# File 'app/models/customer_search.rb', line 291

def mass_switch_rep_roles(_params, cur_user = nil)
  jid = MassSearch::CustomerSwitchRepRolesWorker.perform_async(
    'search_id'     => id,
    'action_params' => {},
    'user_id'       => cur_user&.id,
    'locale'        => I18n.locale.to_s
  )
  { status: :ok, job_id: jid }
end

#set_defaultsObject

Default new customer searches to exclude guest accounts



98
99
100
101
102
103
104
105
106
# File 'app/models/customer_search.rb', line 98

def set_defaults
  super
  return unless new_record?

  qp = (query_params || {}).with_indifferent_access
  if qp[:state_in].blank? && qp[:state_not_in].blank?
    self.query_params = (query_params || {}).merge('state_not_in' => ['guest'])
  end
end