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.
Instance Method Summary collapse
- #all_support_cases ⇒ Object
- #call_records? ⇒ Boolean (also: #has_call_records?)
- #cell_phone ⇒ Object
- #cell_phone=(value) ⇒ Object
- #cell_phone_formatted ⇒ Object
- #contact_point_options_for_select(category) ⇒ Object
- #contactable? ⇒ Boolean
- #email ⇒ Object
- #email=(value) ⇒ Object
- #email_options_for_select ⇒ Object
- #email_with_name ⇒ Object
- #emails ⇒ Object
-
#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.
- #fax ⇒ Object
- #fax=(value) ⇒ Object
- #fax_formatted ⇒ Object
- #fax_options_for_select ⇒ Object
- #first_callable_contact_point ⇒ Object
- #first_contact_point_by_category(category) ⇒ Object
- #phone ⇒ Object
- #phone=(value) ⇒ Object
- #phone_formatted ⇒ Object
- #phone_options_for_select ⇒ Object
-
#research_location ⇒ Object
Best-effort location for enrichment purposes, in priority order: 1.
- #set_email_from_account ⇒ Object
- #set_primary_contact_point(category, value) ⇒ Object
- #sms_enabled_numbers ⇒ Object
-
#sms_messages ⇒ Object
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.
- #tracking_email_address ⇒ Object
- #tracking_email_prefix ⇒ Object
- #website ⇒ Object
- #website=(value) ⇒ Object
- #website_options_for_select ⇒ Object
Instance Method Details
#all_support_cases ⇒ Object
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?
164 165 166 |
# File 'app/models/concerns/party_contact_info.rb', line 164 def call_records? CallRecord.party_records(id).present? end |
#cell_phone ⇒ Object
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_formatted ⇒ Object
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 (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
92 93 94 |
# File 'app/models/concerns/party_contact_info.rb', line 92 def contactable? contact_points.contactable.present? || addresses.present? end |
#email ⇒ Object
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_select ⇒ Object
197 198 199 |
# File 'app/models/concerns/party_contact_info.rb', line 197 def (ContactPoint::EMAIL) end |
#email_with_name ⇒ Object
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 |
#emails ⇒ Object
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).
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 |
#fax ⇒ Object
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_formatted ⇒ Object
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_select ⇒ Object
193 194 195 |
# File 'app/models/concerns/party_contact_info.rb', line 193 def (ContactPoint::FAX) end |
#first_callable_contact_point ⇒ Object
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 |
#phone ⇒ Object
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_formatted ⇒ Object
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_select ⇒ Object
189 190 191 |
# File 'app/models/concerns/party_contact_info.rb', line 189 def (:voice_callable) end |
#research_location ⇒ Object
Best-effort location for enrichment purposes, in priority order:
- Main billing/shipping/mailing address (street + city + state)
- Most recent tracked visit's geo-IP (city + region + postal_code
- country) — useful even when the party hasn't entered any
address yet
- country) — useful even when the party hasn't entered any
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_account ⇒ Object
177 178 179 180 181 182 |
# File 'app/models/concerns/party_contact_info.rb', line 177 def set_email_from_account return unless account && account.email.present? return if contact_points.any? { |cp| cp.email? && cp.detail == account.email } contact_points.build(category: ContactPoint::EMAIL, detail: account.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_numbers ⇒ Object
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_messages ⇒ Object
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 SmsMessage.where('sender_party_id = :id OR recipient_party_id = :id', id: id) end |
#tracking_email_address ⇒ Object
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_prefix ⇒ Object
88 89 90 |
# File 'app/models/concerns/party_contact_info.rb', line 88 def tracking_email_prefix type.to_s.downcase[0, 3] end |
#website ⇒ Object
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_select ⇒ Object
201 202 203 |
# File 'app/models/concerns/party_contact_info.rb', line 201 def (ContactPoint::WEBSITE) end |