Module: Models::PartyContactInfo

Extended by:
ActiveSupport::Concern
Included in:
Party
Defined in:
app/concerns/models/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_casesActiveRecord::Relation<SupportCase>

Returns all support cases this party (or its customer) participates in.

Returns:



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

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?

Whether the party has any call records.

Returns:

  • (Boolean)


245
246
247
# File 'app/concerns/models/party_contact_info.rb', line 245

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

#cell_phoneString?

The primary cell phone number (raw dial string).

Returns:

  • (String, nil)


106
107
108
# File 'app/concerns/models/party_contact_info.rb', line 106

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

#cell_phone=(value) ⇒ ContactPoint?

Sets the primary cell phone contact point.

Parameters:

  • value (String, nil)

    the cell phone number to set

Returns:



121
122
123
# File 'app/concerns/models/party_contact_info.rb', line 121

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

#cell_phone_formattedString?

The primary cell phone number formatted for display.

Returns:

  • (String, nil)


113
114
115
# File 'app/concerns/models/party_contact_info.rb', line 113

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

#contact_point_options_for_select(category) ⇒ Array<Array(String, Integer)>

Options for a select dropdown of contact points in the given category.

Parameters:

  • category (String, Symbol)

    the contact point category, or :voice_callable

Returns:

  • (Array<Array(String, Integer)>)


279
280
281
282
# File 'app/concerns/models/party_contact_info.rb', line 279

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

Whether the party has any reachable contact point or address.

Returns:

  • (Boolean)


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

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

#emailString?

The primary email address for this party.

Prefers an email attribute that may have been selected onto the record
(e.g. for guest authentication), falling back to the first email contact point.

Returns:

  • (String, nil)


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

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) ⇒ ContactPoint?

Sets the primary email contact point.

Parameters:

  • value (String, nil)

    the email address to set

Returns:



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

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

#email_options_for_selectArray<Array(String, Integer)>

Options for a select dropdown of email addresses.

Returns:

  • (Array<Array(String, Integer)>)


301
302
303
# File 'app/concerns/models/party_contact_info.rb', line 301

def email_options_for_select
  contact_point_options_for_select(ContactPoint::EMAIL)
end

#email_with_nameString

The primary email address formatted with the party's display name.

Returns:

  • (String)


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

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

#emailsArray<String>

All email addresses associated with this party.

Returns:

  • (Array<String>)


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

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)


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

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

#faxString?

The primary fax number (raw dial string).

Prefers a primary_fax attribute when one has been selected onto the record.

Returns:

  • (String, nil)


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

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) ⇒ ContactPoint?

Sets the primary fax contact point.

Parameters:

  • value (String, nil)

    the fax number to set

Returns:



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

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

#fax_formattedString?

The primary fax number formatted for display.

Returns:

  • (String, nil)


66
67
68
# File 'app/concerns/models/party_contact_info.rb', line 66

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

#fax_options_for_selectArray<Array(String, Integer)>

Options for a select dropdown of fax numbers.

Returns:

  • (Array<Array(String, Integer)>)


294
295
296
# File 'app/concerns/models/party_contact_info.rb', line 294

def fax_options_for_select
  contact_point_options_for_select(ContactPoint::FAX)
end

#first_callable_contact_pointContactPoint?

The first contact point that can receive a voice call.

Returns:



253
254
255
# File 'app/concerns/models/party_contact_info.rb', line 253

def first_callable_contact_point
  contact_points.voice_callable.sorted.first
end

#first_contact_point_by_category(category) ⇒ ContactPoint?

The first contact point matching the given category.

Parameters:

  • category (String)

    the contact point category (e.g. ContactPoint::EMAIL)

Returns:



261
262
263
# File 'app/concerns/models/party_contact_info.rb', line 261

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

#phoneString?

The primary phone number (raw dial string).

Prefers a primary_phone attribute when one has been selected onto the record.

Returns:

  • (String, nil)


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

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

#phone=(value) ⇒ ContactPoint?

Sets the primary phone contact point.

Parameters:

  • value (String, nil)

    the phone number to set

Returns:



99
100
101
# File 'app/concerns/models/party_contact_info.rb', line 99

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

#phone_formattedString?

The primary phone number formatted for display.

Returns:

  • (String, nil)


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

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

#phone_options_for_selectArray<Array(String, Integer)>

Options for a select dropdown of voice-callable phone numbers.

Returns:

  • (Array<Array(String, Integer)>)


287
288
289
# File 'app/concerns/models/party_contact_info.rb', line 287

def phone_options_for_select
  contact_point_options_for_select(:voice_callable)
end

#research_locationHash{String => String}?

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.

Returns:

  • (Hash{String => String}, nil)


194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'app/concerns/models/party_contact_info.rb', line 194

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_accountContactPoint?

Builds an email contact point from the associated account if one does not already exist.

Returns:



268
269
270
271
272
273
# File 'app/concerns/models/party_contact_info.rb', line 268

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) ⇒ ContactPoint?

Builds or updates the primary contact point for the given category.

Parameters:

  • category (String)

    the contact point category

  • value (String, nil)

    the raw contact detail

Returns:



317
318
319
320
321
# File 'app/concerns/models/party_contact_info.rb', line 317

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_numbersArray<String>

SMS-enabled numbers formatted for sending.

Returns:

  • (Array<String>)


224
225
226
# File 'app/concerns/models/party_contact_info.rb', line 224

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

#sms_messagesActiveRecord::Relation<SmsMessage>

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).

Returns:



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

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

#tracking_email_addressString

A unique tracking email address that routes back to this party.

Returns:

  • (String)


145
146
147
148
149
# File 'app/concerns/models/party_contact_info.rb', line 145

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_prefixString

Three-letter prefix used when constructing a tracking email address.

Returns:

  • (String)


154
155
156
# File 'app/concerns/models/party_contact_info.rb', line 154

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

#websiteString?

The primary website URL.

Returns:

  • (String, nil)


128
129
130
131
132
# File 'app/concerns/models/party_contact_info.rb', line 128

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

#website=(value) ⇒ ContactPoint?

Sets the primary website contact point.

Parameters:

  • value (String, nil)

    the website URL to set

Returns:



138
139
140
# File 'app/concerns/models/party_contact_info.rb', line 138

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

#website_options_for_selectArray<Array(String, Integer)>

Options for a select dropdown of websites.

Returns:

  • (Array<Array(String, Integer)>)


308
309
310
# File 'app/concerns/models/party_contact_info.rb', line 308

def website_options_for_select
  contact_point_options_for_select(ContactPoint::WEBSITE)
end