Class: CustomerFilter

Inherits:
ApplicationRecord show all
Includes:
Memery, Models::Auditable
Defined in:
app/models/customer_filter.rb

Overview

== Schema Information

Table name: customer_filters
Database name: primary

id :integer not null, primary key
cities :string(255) default([]), is an Array
customer_states :string(255) default([]), is an Array
digest :string(32)
exclude_buying_group_ids :boolean default(FALSE)
exclude_catalog_ids :boolean default(FALSE)
exclude_cities :boolean default(FALSE)
exclude_customer_states :boolean default(FALSE)
exclude_parties :boolean default(FALSE)
exclude_postal_codes :boolean default(FALSE)
exclude_profile_ids :boolean default(FALSE)
exclude_report_groupings :boolean default(FALSE)
exclude_source_ids :boolean default(FALSE)
exclude_state_codes :boolean default(FALSE)
exclude_tier2_program_pricing_ids :boolean default(FALSE)
has_email :integer default(0), not null
has_fax :integer default(0), not null
has_phone :integer default(0), not null
has_sales_order :integer default(0), not null
is_auto_generated :boolean
name :string(120) not null
opportunity_count_max :integer
opportunity_count_min :integer
opportunity_value_max :integer
opportunity_value_min :integer
phone_number_starts_with :string(12)
position :integer
postal_codes :string(10) default([]), is an Array
report_groupings :string default([]), is an Array
state_codes :string(2) default([]), is an Array
created_at :datetime not null
updated_at :datetime not null
creator_id :integer
store_id :integer
updater_id :integer

Indexes

index_customer_filters_on_digest (digest) UNIQUE
index_customer_filters_on_is_auto_generated (is_auto_generated)
index_customer_filters_on_store_id_and_name (store_id,name) UNIQUE
index_phone_number_starts_with_trgrm (phone_number_starts_with) USING gin

Foreign Keys

customer_filters_store_id_fk (store_id => stores.id)

Defined Under Namespace

Classes: QueryBuilder

Constant Summary collapse

DEFAULT =

"All w/out DB/ETL/HD/COSTCO"

138
CONDITIONS =

Represent all the accessor methods to conditional values

%i[party_ids customer_states report_groupings catalogs store profiles buying_groups
sources state_codes postal_codes cities tier2_program_pricings phone_number_starts_with
has_sales_order].freeze
UNIQUE_FIELDS =

These fields will be used to compare uniqueness of customer filters

%i[store_id exclude_catalog_ids exclude_profile_ids exclude_state_codes exclude_cities
exclude_postal_codes exclude_buying_group_ids exclude_source_ids exclude_tier2_program_pricing_ids
exclude_report_groupings exclude_customer_states exclude_parties
state_codes cities postal_codes report_groupings customer_states
catalog_ids profile_ids buying_group_ids source_ids
tier2_program_pricing_ids party_ids has_phone has_email has_fax has_sales_order].freeze
NA =

Na.

0
YES =

Yes.

1
NO =

No.

2
TRIPLE_STATE =

Triple state.

{ NA => 'Not Applicable', YES => 'Must Have One', NO => 'Must Have None' }.freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Has and belongs to many collapse

Class Method Summary collapse

Instance Method Summary collapse

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, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Class Method Details

.create_single_customer_filter(customer) ⇒ Object



119
120
121
122
123
124
125
126
127
# File 'app/models/customer_filter.rb', line 119

def self.create_single_customer_filter(customer)
  cf = nil
  if customer.is_a?(Customer) # ensure we are dealing with a real customer
    party_criteria = { parties: { id: customer.id }, is_auto_generated: true }
    create_criteria = { name: "#{customer.full_name} - #{customer.reference_number} - Customer Preference", is_auto_generated: true, party_ids: [customer.id] }
    cf = CustomerFilter.joins(:parties).where(party_criteria).first_or_create(create_criteria)
  end
  cf
end

.duplicate_filtersActiveRecord::Relation<CustomerFilter>

A relation of CustomerFilters that are duplicate filters. Active Record Scope

Returns:

See Also:



108
# File 'app/models/customer_filter.rb', line 108

scope :duplicate_filters, -> { where("EXISTS(select digest from customer_filters cf where cf.digest = customer_filters.digest group by digest having count(digest) > 1)") }

.options_for_selectObject



113
114
115
116
117
# File 'app/models/customer_filter.rb', line 113

def self.options_for_select
  cfs = CustomerFilter.all
  cfs = yield(cfs) if block_given?
  cfs.sorted.pluck(:name, :id)
end

.sortedActiveRecord::Relation<CustomerFilter>

A relation of CustomerFilters that are sorted. Active Record Scope

Returns:

See Also:



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

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

Instance Method Details

#activity_typesActiveRecord::Relation<ActivityType>

Returns:

See Also:



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

has_many :activity_types, inverse_of: :customer_filter

#all_catalog_idsObject



456
457
458
459
460
461
462
463
464
465
466
467
468
# File 'app/models/customer_filter.rb', line 456

def all_catalog_ids
  # Convenience method that will retrieve all catalog_ids associated with the filter
  cids = []
  cids += parties.map { |p| p.try(:catalog_id) }
  if catalog_ids.present?
    if exclude_catalog_ids
      cids -= catalog_ids
    else
      cids += catalog_ids
    end
  end
  cids.compact.uniq.sort
end

#applies_to_customer?(customer) ⇒ Boolean

Returns:

  • (Boolean)


231
232
233
# File 'app/models/customer_filter.rb', line 231

def applies_to_customer?(customer)
  build_matcher(customer).match?
end

#auto_apply_couponsActiveRecord::Relation<Coupon>

Returns:

  • (ActiveRecord::Relation<Coupon>)

See Also:



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

has_many :auto_apply_coupons, inverse_of: :auto_apply_customer_filter, class_name: 'Coupon', foreign_key: :auto_apply_customer_filter_id

#build_matcher(customer) ⇒ Object



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'app/models/customer_filter.rb', line 369

def build_matcher(customer)
  CustomerRuleMatcher.new(self, customer,
                :party_match?,
                :customer_state_match?,
                :report_grouping_match?,
                :catalog_match?,
                :store_match?,
                :profile_match?,
                :tier2_program_pricing_match?,
                :buying_group_match?,
                :source_match?,
                :state_code_match?,
                :postal_code_match?,
                :city_match?,
                :has_phone_match?,
                :has_fax_match?,
                :has_email_match?,
                :has_phone_number_match?,
                :has_sales_order_match?,
                :has_opportunity_match?)
end

#buying_group_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


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

def buying_group_match?(customer)
  meta_assoc_match buying_groups, customer.buying_group_id, exclude_buying_group_ids
end

#buying_groupsActiveRecord::Relation<BuyingGroup>

Returns:

See Also:



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

has_and_belongs_to_many :buying_groups

#calculate_digestObject



418
419
420
421
422
# File 'app/models/customer_filter.rb', line 418

def calculate_digest
  dg = Digest::MD5.hexdigest raw_digest_fields
  logger.debug "Calculated digest #{dg} for customer filter #{id}"
  dg
end

#catalog_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


159
160
161
# File 'app/models/customer_filter.rb', line 159

def catalog_match?(customer)
  meta_assoc_match catalogs, customer.catalog_id, exclude_catalog_ids
end

#catalogsActiveRecord::Relation<Catalog>

Returns:

  • (ActiveRecord::Relation<Catalog>)

See Also:



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

has_and_belongs_to_many :catalogs

#chain_buying_groups_query(customers_query) ⇒ Object



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

def chain_buying_groups_query(customers_query)
  return customers_query if buying_group_ids.blank?

  if exclude_buying_group_ids
    customers_query.where.not(buying_group_id: buying_group_ids)
  else
    customers_query.where(buying_group_id: buying_group_ids)
  end
end

#chain_catalogs_query(customers_query) ⇒ Object



262
263
264
265
266
267
268
269
270
# File 'app/models/customer_filter.rb', line 262

def chain_catalogs_query(customers_query)
  return customers_query if catalog_ids.blank?

  if exclude_catalog_ids
    customers_query.where.not(catalog_id: catalog_ids)
  else
    customers_query.where(catalog_id: catalog_ids)
  end
end

#chain_cities_query(customers_query) ⇒ Object



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

def chain_cities_query(customers_query)
  return customers_query if cities.blank?

  if exclude_cities
    customers_query.joins(:addresses).where(Address[:city].not_in(cities).or(Address[:city].eq(nil)))
  else
    customers_query.joins(:addresses).where(Address[:city].in(cities))
  end
end

#chain_customer_states_query(customers_query) ⇒ Object



318
319
320
321
322
323
324
325
326
# File 'app/models/customer_filter.rb', line 318

def chain_customer_states_query(customers_query)
  return customers_query if customer_states.blank?

  if exclude_customer_states
    customers_query.where.not(state: customer_states)
  else
    customers_query.where(state: customer_states)
  end
end

#chain_parties_query(customers_query) ⇒ Object



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

def chain_parties_query(customers_query)
  return customers_query if party_ids.blank?

  if exclude_parties
    customers_query.where.not(id: party_ids)
  else
    customers_query.where(id: party_ids)
  end
end

#chain_postal_codes_query(customers_query) ⇒ Object



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

def chain_postal_codes_query(customers_query)
  return customers_query if postal_codes.blank?

  if exclude_postal_codes
    customers_query.joins(:addresses).where(Address[:zip].not_in(postal_codes).or(Address[:zip].eq(nil)))
  else
    customers_query.joins(:addresses).where(Address[:zip].in(postal_codes))
  end
end

#chain_profiles_query(customers_query) ⇒ Object



252
253
254
255
256
257
258
259
260
# File 'app/models/customer_filter.rb', line 252

def chain_profiles_query(customers_query)
  return customers_query if profile_ids.blank?

  if exclude_profile_ids
    customers_query.where(Customer[:profile_id].not_in(profile_ids).or(Customer[:profile_id].eq(nil)))
  else
    customers_query.where(profile_id: profile_ids)
  end
end

#chain_program_pricing_query(customers_query) ⇒ Object



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

def chain_program_pricing_query(customers_query)
  return customers_query if tier2_program_pricing_ids.blank?

  if exclude_tier2_program_pricing_ids
    customers_query.where.not(tier2_program_pricing_id: tier2_program_pricing_ids)
  else
    customers_query.where(tier2_program_pricing_id: tier2_program_pricing_ids)
  end
end

#chain_query(customers_query) ⇒ Object



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'app/models/customer_filter.rb', line 236

def chain_query(customers_query)
  customers_query = chain_report_groupings_query(customers_query)
  customers_query = chain_buying_groups_query(customers_query)
  customers_query = chain_parties_query(customers_query)
  customers_query = chain_program_pricing_query(customers_query)
  customers_query = chain_customer_states_query(customers_query)
  customers_query = chain_store_query(customers_query)
  customers_query = chain_cities_query(customers_query)
  customers_query = chain_postal_codes_query(customers_query)
  customers_query = chain_state_codes_query(customers_query)
  customers_query = chain_sources_query(customers_query)
  customers_query = chain_catalogs_query(customers_query)
  chain_profiles_query(customers_query)
  # Todo phone match, email match, fax match, order match
end

#chain_report_groupings_query(customers_query) ⇒ Object



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

def chain_report_groupings_query(customers_query)
  return customers_query if report_groupings.blank?

  if exclude_report_groupings
    # a customer with nil is considered excluded too we have to test for this condition specifically in sql
    customers_query.where(Customer[:report_grouping].eq(nil).or(Customer[:report_grouping].not_in(report_groupings)))
  else
    customers_query.where(Customer[:report_grouping].in(report_groupings))
  end
end

#chain_sources_query(customers_query) ⇒ Object



272
273
274
275
276
277
278
279
280
# File 'app/models/customer_filter.rb', line 272

def chain_sources_query(customers_query)
  return customers_query if source_ids.blank?

  if exclude_source_ids
    customers_query.where(Customer[:source_id].not_in(source_ids).or(Customer[:source_id].eq(nil)))
  else
    customers_query.where(Customer[:source_id].in(source_ids))
  end
end

#chain_state_codes_query(customers_query) ⇒ Object



282
283
284
285
286
287
288
289
290
# File 'app/models/customer_filter.rb', line 282

def chain_state_codes_query(customers_query)
  return customers_query if state_codes.blank?

  if exclude_state_codes
    customers_query.where(Customer[:state_code].not_in(state_codes).or(Customer[:state_code].eq(nil)))
  else
    customers_query.where(Customer[:state_code].in(state_codes))
  end
end

#chain_store_query(customers_query) ⇒ Object



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

def chain_store_query(customers_query)
  return customers_query if store_id.blank?

  customers_query.joins(:catalog).where(Catalog[:store_id].eq(store_id))
end

#city_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


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

def city_match?(customer)
  meta_string_match customer.city, cities, exclude_cities
end

#couponsActiveRecord::Relation<Coupon>

Returns:

  • (ActiveRecord::Relation<Coupon>)

See Also:



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

has_many :coupons, inverse_of: :customer_filter

#customer_state_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


171
172
173
# File 'app/models/customer_filter.rb', line 171

def customer_state_match?(customer)
  meta_string_match customer.state, customer_states, exclude_customer_states
end

#duplicate_filtersObject



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

def duplicate_filters
  filters = CustomerFilter.where(digest: calculate_digest)
  filters = filters.where(CustomerFilter[:id].not_eq(id)) unless new_record?
  filters
end

#fields_arrayObject



401
402
403
404
405
406
407
408
# File 'app/models/customer_filter.rb', line 401

def fields_array
  raw_fields = UNIQUE_FIELDS.each_with_object([]) do |field, array|
    val = send(field)
    val = val.compact.sort if !val.nil? && val.is_a?(Array)
    array << "#{field}:#{val}" if val.present?
  end
  (raw_fields || []).sort
end

#friendly_digestObject



410
411
412
# File 'app/models/customer_filter.rb', line 410

def friendly_digest
  fields_array.to_sentence
end

#has_a_condition?Boolean

Returns:

  • (Boolean)


452
453
454
# File 'app/models/customer_filter.rb', line 452

def has_a_condition?
  CONDITIONS.any? { |cnd| send(cnd).present? }
end

#has_duplicate_filter?Boolean

Returns:

  • (Boolean)


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

def has_duplicate_filter?
  duplicate_filters.present?
end

#has_email_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


220
221
222
# File 'app/models/customer_filter.rb', line 220

def has_email_match?(customer)
  relation_triple_state_presence_check(customer.all_contact_points.emails, has_email)
end

#has_fax_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


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

def has_fax_match?(customer)
  relation_triple_state_presence_check(customer.all_contact_points.faxes, has_fax)
end

#has_opportunity_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'app/models/customer_filter.rb', line 203

def has_opportunity_match?(customer)
  # The lack of filter is a true check
  return true unless opportunity_value_min || opportunity_value_max || opportunity_count_min || opportunity_count_max

  opp_query = customer.opportunities.open_opportunities.where(opportunity_type: 'S')

  opp_query = opp_query.where(value: opportunity_value_min..) if opportunity_value_min.present?
  opp_query = opp_query.where(value: opportunity_value_max..) if opportunity_value_max.present?
  if opportunity_count_max.present?
    return false if opp_query.size > opportunity_count_max
  end
  if opportunity_count_min.present?
    return false if opp_query.size < opportunity_count_min
  end
  true # Logically you'll exit before this
end

#has_phone_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


191
192
193
# File 'app/models/customer_filter.rb', line 191

def has_phone_match?(customer)
  relation_triple_state_presence_check(customer.all_contact_points.voice_callable, has_phone)
end

#has_phone_number_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


224
225
226
227
228
229
# File 'app/models/customer_filter.rb', line 224

def has_phone_number_match?(customer)
  phones = customer.all_contact_points.dialable
  return true if phones.empty?

  phones.find { |cp| meta_string_match cp.detail, phone_number_starts_with, false, :allow_partial }
end

#has_sales_order_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


199
200
201
# File 'app/models/customer_filter.rb', line 199

def has_sales_order_match?(customer)
  relation_triple_state_presence_check(customer.orders.so_only.active, has_sales_order)
end

#iq_accessory_filtersActiveRecord::Relation<IqAccessoryFilter>

Returns:

See Also:



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

has_many :iq_accessory_filters, inverse_of: :customer_filter

#meta_assoc_match(assoc, value, exclude) ⇒ Object

Convenience method to rescue us from repetition. Association to check against, value to check for, exclude turned on or off
Optimized to use in-memory lookup when association is preloaded to avoid N+1 queries



426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'app/models/customer_filter.rb', line 426

def meta_assoc_match(assoc, value, exclude)
  # Use loaded? check to avoid N+1 - if preloaded, check in memory
  if assoc.loaded?
    return true if assoc.empty?

    value_in_list = assoc.any? { |record| record.id == value }
  else
    # When not preloaded, fall back to database queries
    return true if assoc.empty?

    value_in_list = assoc.exists?(id: value)
  end
  exclude ? !value_in_list : value_in_list
end

#meta_string_match(value, values, exclude, allow_partial = false) ⇒ Object



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

def meta_string_match(value, values, exclude, allow_partial = false)
  return true if values.blank?

  value_in_list = if allow_partial
                    value&.starts_with?(*values)
                  else
                    value.in?(values)
                  end
  exclude ? !value_in_list : value_in_list
end

#partiesActiveRecord::Relation<Party>

Returns:

  • (ActiveRecord::Relation<Party>)

See Also:



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

has_and_belongs_to_many :parties, validate: false

#party_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


167
168
169
# File 'app/models/customer_filter.rb', line 167

def party_match?(customer)
  meta_assoc_match parties, customer.id, exclude_parties
end

#postal_code_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


179
180
181
# File 'app/models/customer_filter.rb', line 179

def postal_code_match?(customer)
  meta_string_match customer.zip, postal_codes, exclude_postal_codes, :allow_partial
end

#profile_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


147
148
149
# File 'app/models/customer_filter.rb', line 147

def profile_match?(customer)
  meta_assoc_match profiles, customer.profile_id, exclude_profile_ids
end

#profilesActiveRecord::Relation<Profile>

Returns:

  • (ActiveRecord::Relation<Profile>)

See Also:



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

has_and_belongs_to_many :profiles

#raw_digest_fieldsObject



414
415
416
# File 'app/models/customer_filter.rb', line 414

def raw_digest_fields
  fields_array.join('|').to_s
end

#referenced?Boolean

Returns:

  • (Boolean)


133
134
135
136
137
138
139
140
141
# File 'app/models/customer_filter.rb', line 133

def referenced?
  (
    sales_rep_queue_entries.present? or
    topics.present? or
    activity_types.present? or
    coupons.present? or
    auto_apply_coupons.present?
  )
end

#report_grouping_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


187
188
189
# File 'app/models/customer_filter.rb', line 187

def report_grouping_match?(customer)
  meta_string_match customer.effective_report_grouping, report_groupings, exclude_report_groupings
end

#sales_rep_queue_entriesActiveRecord::Relation<SalesRepQueueEntry>

Returns:

See Also:



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

has_many :sales_rep_queue_entries, inverse_of: :customer_filter

#source_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


163
164
165
# File 'app/models/customer_filter.rb', line 163

def source_match?(customer)
  meta_assoc_match sources, customer.source_id, exclude_source_ids
end

#sourcesActiveRecord::Relation<Source>

Returns:

  • (ActiveRecord::Relation<Source>)

See Also:



98
# File 'app/models/customer_filter.rb', line 98

has_and_belongs_to_many :sources

#state_code_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


175
176
177
# File 'app/models/customer_filter.rb', line 175

def state_code_match?(customer)
  meta_string_match customer.state_code, state_codes, exclude_state_codes
end

#storeStore

Returns:

See Also:



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

belongs_to :store, optional: true

#store_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


143
144
145
# File 'app/models/customer_filter.rb', line 143

def store_match?(customer)
  store.nil? or customer.store == store
end

#tier2_program_pricing_match?(customer) ⇒ Boolean

Returns:

  • (Boolean)


151
152
153
# File 'app/models/customer_filter.rb', line 151

def tier2_program_pricing_match?(customer)
  meta_assoc_match tier2_program_pricings, customer.tier2_program_pricing_id, exclude_tier2_program_pricing_ids
end

#tier2_program_pricingsActiveRecord::Relation<Coupon::Tier2ProgramPricing>

Returns:

See Also:



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

has_and_belongs_to_many :tier2_program_pricings, class_name: 'Coupon::Tier2ProgramPricing',
join_table: 'customer_filters_program_pricings',
association_foreign_key: 'tier2_program_pricing_id'

#to_sObject



129
130
131
# File 'app/models/customer_filter.rb', line 129

def to_s
  "#{name} (#{id})"
end

#topicsActiveRecord::Relation<Topic>

Returns:

  • (ActiveRecord::Relation<Topic>)

See Also:



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

has_many :topics, inverse_of: :customer_filter