Class: Query::CustomerFinder

Inherits:
Object
  • Object
show all
Defined in:
app/services/query/customer_finder.rb

Overview

Service object: customer finder.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(search_params = {}) ⇒ CustomerFinder

Returns a new instance of CustomerFinder.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'app/services/query/customer_finder.rb', line 6

def initialize(search_params = {})
  # Callers key this hash differently: the customers controller passes
  # symbol keys ({ customer: … }), while Customer::NewCustomer passes its
  # ActiveModel attributes hash, which has STRING keys. Every accessor below
  # reads @search_params[:symbol], so normalize to indifferent access — a
  # Virtus→ActiveModel::Attributes migration flipped NewCustomer#attributes
  # to string keys and silently broke the new-customer duplicate check.
  @search_params = (search_params || {}).to_h.with_indifferent_access
  # Extract components
  if (customer = @search_params[:customer])
    @search_params[:email] = customer.all_emails
    @search_params[:phone] = customer.all_phone_numbers
    @search_params[:company_name] = customer.full_name
    @search_params[:person_names] = customer.contacts.active.pluck(:full_name)
    @search_params[:person_names] << customer.full_name if customer.is_homeowner?

    @search_params[:coordinates] = customer.addresses.filter_map { |a| a.lat.present? && a.lng.present? ? [a.lat, a.lng] : nil }.uniq
    @search_params[:exclude_customer_ids] = [customer.id]
  end
  @customer_scope = search_params[:customer_scope] || Customer.all
  @customer_scope = @customer_scope.where.not(id: exclude_customer_ids) if exclude_customer_ids.present?
end

Instance Attribute Details

#customer_scopeObject (readonly)

Returns the value of attribute customer_scope.



4
5
6
# File 'app/services/query/customer_finder.rb', line 4

def customer_scope
  @customer_scope
end

#search_paramsObject (readonly)

Returns the value of attribute search_params.



4
5
6
# File 'app/services/query/customer_finder.rb', line 4

def search_params
  @search_params
end

Instance Method Details

#cellObject



45
46
47
# File 'app/services/query/customer_finder.rb', line 45

def cell
  @search_params[:cell].presence
end

#company_nameObject



53
54
55
# File 'app/services/query/customer_finder.rb', line 53

def company_name
  @search_params[:company_name].presence
end

#coordinateObject



78
79
80
# File 'app/services/query/customer_finder.rb', line 78

def coordinate
  lat.nil? || lng.nil? ? nil : [lat, lng]
end

#coordinatesObject



73
74
75
76
# File 'app/services/query/customer_finder.rb', line 73

def coordinates
  res = @search_params[:coordinates] || [coordinate]
  res.uniq.compact.select { |coord| coord.length == 2 }
end

#emailObject



29
30
31
# File 'app/services/query/customer_finder.rb', line 29

def email
  @search_params[:email].presence
end

#email2Object



33
34
35
# File 'app/services/query/customer_finder.rb', line 33

def email2
  @search_params[:email2].presence
end

#exclude_customer_idsObject



82
83
84
# File 'app/services/query/customer_finder.rb', line 82

def exclude_customer_ids
  [@search_params[:exclude_customer_ids]].flatten.compact.uniq
end

#faxObject



49
50
51
# File 'app/services/query/customer_finder.rb', line 49

def fax
  @search_params[:fax].presence
end

#find_contact_pointsObject



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'app/services/query/customer_finder.rb', line 101

def find_contact_points
  phones = [phone, phone2, cell].flatten.map(&:presence).uniq.compact
  phones = phones.filter_map { |p| PhoneNumber.parse_and_format(p) }
  details = phones + [email, email2].flatten.map(&:presence).uniq.compact
  return ContactPoint.none if details.blank?

  contact_points = ContactPoint.belonging_to_party.where(detail: details)
  party_ids = []
  if contact_points.present?
    party_ids = contact_points.pluck(:party_id)
  else # Try email match on domain
    [email, email2].compact.each do |e|
      domain = QuickSearch::ContactPointEmailQuickSearch.domain_scan(e)
      if domain
        domain_contact_points = ContactPoint.emails.joins(:party).where.not(Party[:inactive].eq(true)).contains("@#{domain}")
        party_ids += domain_contact_points.pluck(:party_id)
      end
    end
  end
  party_ids.compact.uniq
end

#find_duplicatesObject



94
95
96
97
98
99
# File 'app/services/query/customer_finder.rb', line 94

def find_duplicates
  party_ids = [find_matching_customer_names, find_matching_person_names, find_contact_points, find_party_nearby].flatten.compact.uniq
  customers = customer_scope.where("parties.id IN (:party_id) or exists(select 1 from parties cnt where cnt.id IN (:party_id) and cnt.customer_id = parties.id)", party_id: party_ids)
  customers = customers.where.not(id: exclude_customer_ids) if exclude_customer_ids.present?
  customers
end

#find_matching_customer_namesObject



123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'app/services/query/customer_finder.rb', line 123

def find_matching_customer_names
  [company_name, person_name].flatten.map(&:presence).uniq.compact.map do |customer_name|
    # filter out noise words
    search_name = customer_name.split(/[ ,.\-&]/).delete_if do |x|
      segment = x.squish.downcase
      %w[inc inc llc ltd].include?(segment) || segment.length <= 2
    end&.join(' ')
    cids = []
    cids += customer_scope.where(Customer[:full_name].matches("%#{search_name}%")).limit(5).ids
    cids += customer_scope.lookup(search_name).limit(6).ids
    # cids += customer_scope.where("full_name ILIKE ?", "%#{search_name}%").limit(2).pluck(:id)
    cids
  end.flatten.uniq
end

#find_matching_person_namesObject



138
139
140
141
142
143
144
145
146
147
148
149
# File 'app/services/query/customer_finder.rb', line 138

def find_matching_person_names
  [person_name].flatten.map(&:presence).uniq.compact.map do |full_name|
    # For the purpose of finding contact matches, we will only use firstname, lastname but we will still try an exact match
    pnp = PersonNameParser.new(full_name)
    # Only attempt match if we have first and last name or we'll have a lot of junk
    next unless pnp.first && pnp.last

    Contact.where(Contact[:full_name].matches("%#{pnp.simple_name}%")).where.not(customer_id: nil).limit(5).pluck(:customer_id) +
      Contact.lookup(pnp.full_name).limit(6).pluck(:customer_id)
    # Contact.where( "full_name ILIKE ?", "%#{pnp.simple_name}%").limit(2).pluck(:customer_id)
  end.flatten.uniq
end

#find_party_nearbyObject



151
152
153
154
155
# File 'app/services/query/customer_finder.rb', line 151

def find_party_nearby
  coordinates.map do |coord|
    Address.geocoded.where.not(party_id: nil).near(coord, 0.1).map(&:party_id)
  end
end

#latObject



86
87
88
# File 'app/services/query/customer_finder.rb', line 86

def lat
  @search_params[:lat].presence ? @search_params[:lat].to_f : nil
end

#lngObject



90
91
92
# File 'app/services/query/customer_finder.rb', line 90

def lng
  @search_params[:lng].presence ? @search_params[:lng].to_f : nil
end

#person_first_nameObject



61
62
63
# File 'app/services/query/customer_finder.rb', line 61

def person_first_name
  split_person_name.first
end

#person_last_nameObject



65
66
67
# File 'app/services/query/customer_finder.rb', line 65

def person_last_name
  split_person_name.last
end

#person_nameObject



57
58
59
# File 'app/services/query/customer_finder.rb', line 57

def person_name
  @search_params[:person_name].presence
end

#phoneObject



37
38
39
# File 'app/services/query/customer_finder.rb', line 37

def phone
  @search_params[:phone].presence
end

#phone2Object



41
42
43
# File 'app/services/query/customer_finder.rb', line 41

def phone2
  @search_params[:phone2].presence
end

#split_person_nameObject



69
70
71
# File 'app/services/query/customer_finder.rb', line 69

def split_person_name
  @split_person_name ||= PersonNameParser.new(person_name)
end