Module: PartyContactInfo

Extended by:
ActiveSupport::Concern
Included in:
Party
Defined in:
app/models/concerns/party_contact_info.rb

Overview

Email, phone, fax, and website accessors backed by ContactPoint records.

See Also:

Instance Method Summary collapse

Instance Method Details

#all_support_casesObject



10
11
12
# File 'app/models/concerns/party_contact_info.rb', line 10

def all_support_cases
  SupportCase.joins(support_case_participants: :party).where('parties.id = ? or parties.customer_id = ?', id, id).distinct
end

#call_records?Boolean Also known as: has_call_records?

Returns:

  • (Boolean)


164
165
166
# File 'app/models/concerns/party_contact_info.rb', line 164

def call_records?
  CallRecord.party_records(id).present?
end

#cell_phoneObject



60
61
62
# File 'app/models/concerns/party_contact_info.rb', line 60

def cell_phone
  first_contact_point_by_category(ContactPoint::CELL)&.dial_string
end

#cell_phone=(value) ⇒ Object



68
69
70
# File 'app/models/concerns/party_contact_info.rb', line 68

def cell_phone=(value)
  set_primary_contact_point(ContactPoint::CELL, value)
end

#cell_phone_formattedObject



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

def cell_phone_formatted
  first_contact_point_by_category(ContactPoint::CELL)&.formatted_dial_string
end

#contact_point_options_for_select(category) ⇒ Object



184
185
186
187
# File 'app/models/concerns/party_contact_info.rb', line 184

def contact_point_options_for_select(category)
  scope = category == :voice_callable ? contact_points.voice_callable : contact_points.by_category(category)
  scope.map { |cp| [cp.detail.to_s, cp.id] }
end

#contactable?Boolean

Returns:

  • (Boolean)


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

def contactable?
  contact_points.contactable.present? || addresses.present?
end

#emailObject



18
19
20
21
22
# File 'app/models/concerns/party_contact_info.rb', line 18

def email
  # We use attributes email here in case the email was retrieved using a select custom append,
  # such as when loading guest in authenticable
  @email ||= attributes[:email] || first_contact_point_by_category(ContactPoint::EMAIL)&.detail
end

#email=(value) ⇒ Object



30
31
32
# File 'app/models/concerns/party_contact_info.rb', line 30

def email=(value)
  set_primary_contact_point(ContactPoint::EMAIL, value)
end

#email_options_for_selectObject



197
198
199
# File 'app/models/concerns/party_contact_info.rb', line 197

def email_options_for_select
  contact_point_options_for_select(ContactPoint::EMAIL)
end

#email_with_nameObject



24
25
26
27
28
# File 'app/models/concerns/party_contact_info.rb', line 24

def email_with_name
  address = Mail::Address.new email
  address.display_name = name.dup
  address.format
end

#emailsObject



14
15
16
# File 'app/models/concerns/party_contact_info.rb', line 14

def emails
  contact_points.emails.reorder(:detail).distinct.pluck(:detail)
end

#enrichable_via_research?Boolean

Whether the party has enough starting signal (phone, email, address,
or geo-IP location from a tracked visit) for the Lead Enrichment
feature to do anything useful. Name-only parties can't be enriched
confidently — Apollo would fall back to a fuzzy name search, PDL
has nothing to anchor on, etc.

For a Customer, signal on any of its Contacts also counts (a
Contact's phone is the customer's reach-out number).

Returns:

  • (Boolean)


104
105
106
107
108
109
110
111
# File 'app/models/concerns/party_contact_info.rb', line 104

def enrichable_via_research?
  return false if respond_to?(:guest?) && guest?
  return true if contactable?
  return true if research_location.present?
  return contacts.any? { |c| c.contactable? || c.research_location.present? } if is_a?(Customer)

  false
end

#faxObject



34
35
36
37
# File 'app/models/concerns/party_contact_info.rb', line 34

def fax
  # primary_fax is sometime selected directly in advanced searches
  respond_to?(:primary_fax) ? primary_fax : first_contact_point_by_category(ContactPoint::FAX)&.dial_string
end

#fax=(value) ⇒ Object



43
44
45
# File 'app/models/concerns/party_contact_info.rb', line 43

def fax=(value)
  set_primary_contact_point(ContactPoint::FAX, value)
end

#fax_formattedObject



39
40
41
# File 'app/models/concerns/party_contact_info.rb', line 39

def fax_formatted
  first_contact_point_by_category(ContactPoint::FAX)&.formatted_dial_string
end

#fax_options_for_selectObject



193
194
195
# File 'app/models/concerns/party_contact_info.rb', line 193

def fax_options_for_select
  contact_point_options_for_select(ContactPoint::FAX)
end

#first_callable_contact_pointObject



169
170
171
# File 'app/models/concerns/party_contact_info.rb', line 169

def first_callable_contact_point
  contact_points.voice_callable.sorted.first
end

#first_contact_point_by_category(category) ⇒ Object



173
174
175
# File 'app/models/concerns/party_contact_info.rb', line 173

def first_contact_point_by_category(category)
  contact_points.sorted.by_category(category).first
end

#phoneObject



47
48
49
50
# File 'app/models/concerns/party_contact_info.rb', line 47

def phone
  # primary_phone is sometime selected directly in advanced searches
  first_contact_point_by_category(ContactPoint::PHONE)&.dial_string
end

#phone=(value) ⇒ Object



56
57
58
# File 'app/models/concerns/party_contact_info.rb', line 56

def phone=(value)
  set_primary_contact_point(ContactPoint::PHONE, value)
end

#phone_formattedObject



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

def phone_formatted
  first_contact_point_by_category(ContactPoint::PHONE)&.formatted_dial_string
end

#phone_options_for_selectObject



189
190
191
# File 'app/models/concerns/party_contact_info.rb', line 189

def phone_options_for_select
  contact_point_options_for_select(:voice_callable)
end

#research_locationObject

Best-effort location for enrichment purposes, in priority order:

  1. Main billing/shipping/mailing address (street + city + state)
  2. Most recent tracked visit's geo-IP (city + region + postal_code
    • country) — useful even when the party hasn't entered any
      address yet

Returns a Hash with string keys (matches the shape adapters consume),
or nil when neither source has anything.



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
# File 'app/models/concerns/party_contact_info.rb', line 121

def research_location
  if (addr = main_address)
    return {
      'street_line_1' => addr.street1,
      'city' => addr.city,
      'state_code' => addr.state_code,
      'postal_code' => addr.zip,
      'country_code' => addr.country_iso3,
      'source' => 'address'
    }.compact
  end

  visit = visits.order(started_at: :desc).limit(1).first
  return nil unless visit && (visit.city.present? || visit.region.present? || visit.postal_code.present?)

  # Shape MUST match the address branch (state_code + country_code)
  # so adapters can consume `loc['state_code']` and
  # `loc['country_code']` uniformly regardless of source.
  {
    'city' => visit.city,
    'state_code' => visit.state_code,
    'postal_code' => visit.postal_code,
    'country_code' => visit.country_iso3,
    'source' => 'visit'
  }.compact
end

#set_email_from_accountObject



177
178
179
180
181
182
# File 'app/models/concerns/party_contact_info.rb', line 177

def 
  return unless  && .email.present?
  return if contact_points.any? { |cp| cp.email? && cp.detail == .email }

  contact_points.build(category: ContactPoint::EMAIL, detail: .email)
end

#set_primary_contact_point(category, value) ⇒ Object



205
206
207
208
209
# File 'app/models/concerns/party_contact_info.rb', line 205

def set_primary_contact_point(category, value)
  cp = ContactPoint.build_from_string(value, contact_points, category)
  cp.move_to_top if cp&.persisted?
  cp
end

#sms_enabled_numbersObject



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

def sms_enabled_numbers
  contact_points.sms_numbers.order(:detail).map(&:formatted_for_sms).uniq
end

#sms_messagesObject

Messages directly attributed to this party via the denormalized FKs that
match_inbound_sender / match_outbound_recipient (and the manual attach-to-party
flow) populate at SMS save time. Decoupled from contact_points.sms_status:
a number's sms_status is a sender-routing concern, not a visibility filter on
conversations that already happened (AppSignal trace re David Grégoire,
2026-05-06: contact_point was sms_none for several days while real messages
piled up under correct sender_party_id/recipient_party_id, leaving the SMS
tab empty until an outbound attempt flipped the flag).



160
161
162
# File 'app/models/concerns/party_contact_info.rb', line 160

def sms_messages
  SmsMessage.where('sender_party_id = :id OR recipient_party_id = :id', id: id)
end

#tracking_email_addressObject



82
83
84
85
86
# File 'app/models/concerns/party_contact_info.rb', line 82

def tracking_email_address
  domain = Rails.application.config.x.email_domain
  encrypted_id = Encryption.encrypt_string(id.to_s)
  "#{tracking_email_prefix}+id#{encrypted_id}@#{domain}"
end

#tracking_email_prefixObject



88
89
90
# File 'app/models/concerns/party_contact_info.rb', line 88

def tracking_email_prefix
  type.to_s.downcase[0, 3]
end

#websiteObject



72
73
74
75
76
# File 'app/models/concerns/party_contact_info.rb', line 72

def website
  first_contact_point_by_category(ContactPoint::WEBSITE).detail
rescue StandardError
  nil
end

#website=(value) ⇒ Object



78
79
80
# File 'app/models/concerns/party_contact_info.rb', line 78

def website=(value)
  set_primary_contact_point(ContactPoint::WEBSITE, value)
end

#website_options_for_selectObject



201
202
203
# File 'app/models/concerns/party_contact_info.rb', line 201

def website_options_for_select
  contact_point_options_for_select(ContactPoint::WEBSITE)
end