Class: ContactPoint

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable, PgSearch::Model
Defined in:
app/models/contact_point.rb

Overview

rubocop:disable Metrics/ClassLength
== Schema Information

Table name: contact_points
Database name: primary

id :integer not null, primary key
area_code :string(10)
category :string(255)
country_code :integer
detail :string(255)
extension :string(10)
notes :string(255)
position :integer
restricted_notes :text
skip_validation :boolean default(FALSE), not null
sms_status :enum default("sms_none"), not null
state :string(25)
system_notes :text
timezone_name :string
created_at :datetime
updated_at :datetime
locator_record_id :integer
party_id :integer

Indexes

by_pid_category_w (party_id,category) WHERE (detail IS NOT NULL)
contact_points_party_id_index (party_id)
idx_category_party_id_detail (category,party_id,detail)
idx_detail_party_id (detail,party_id)
idx_trigram_detail (detail) USING gin
index_contact_points_locator_record_id (locator_record_id)
index_contact_points_on_party_id_and_sms_status (party_id,sms_status)
index_contact_points_on_position_and_party_id (position,party_id)

Foreign Keys

contact_points_party_id_fk (party_id => parties.id) ON DELETE => nullify

Defined Under Namespace

Classes: AddressBookBuilder

Constant Summary collapse

HOUZZ_REGEX =

Platform-specific regexes — require https:// (normalise_format ensures this before validation)

%r{\Ahttps?://(www\.)?houzz\.com/}i
INSTAGRAM_REGEX =

Regex pattern matching instagram.

%r{\Ahttps?://(www\.)?instagram\.com/}i
FACEBOOK_REGEX =

Regex pattern matching facebook.

%r{\Ahttps?://(www\.)?facebook\.com/}i
PINTEREST_REGEX =

Regex pattern matching pinterest.

%r{\Ahttps?://(www\.)?pinterest\.com/}i
LINKEDIN_REGEX =

Regex pattern matching linkedin.

%r{\Ahttps?://(www\.)?linkedin\.com/}i
WEBSITE_REGEX =

Generic website: must start with https:// or http://, have a real host with a dot.
Full structural validation is done via Addressable::URI in valid_website_url?.

%r{\Ahttps?://[^/\s]+\.[^/\s]}i
TWITTER_REGEX =

Regex pattern matching twitter.

/@.*/
EMAIL_REGEXP =

Email regexp.

RFC822::EMAIL
PHONE =

Phone.

'phone'.freeze
EMAIL =

Email.

'email'.freeze
FAX =

Fax.

'fax'.freeze
CELL =

Cell.

'cell'.freeze
WEBSITE =

Website.

'website'.freeze
FACEBOOK =

Facebook.

'facebook'.freeze
TWITTER =

Twitter.

'twitter'.freeze
PINTEREST =

Pinterest.

'pinterest'.freeze
LINKEDIN =

Linkedin.

'linkedin'.freeze
INSTAGRAM =

Instagram.

'instagram'.freeze
HOUZZ =

Houzz.

'houzz'.freeze
CAN_DIAL =

Can dial.

[CELL, FAX, PHONE].freeze
CAN_TRANSMIT =

Can transmit.

[FAX, EMAIL].freeze
CAN_VOICE =

Can voice.

[CELL, PHONE].freeze
ALL_CATEGORIES =

All categories.

[PHONE, CELL, FAX, EMAIL, WEBSITE, FACEBOOK, TWITTER, PINTEREST, LINKEDIN, INSTAGRAM, HOUZZ].freeze
CONTACTABLE =

Contactable.

[EMAIL, PHONE, CELL, FAX].freeze
INVALID_URL_MESSAGE =

Invalid url message.

'must be a valid URL starting with https:// (e.g. https://example.com)'.freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has one collapse

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

Instance Attribute Details

#categoryObject (readonly)

-- validation messages intentionally inline; locale extraction deferred

Validations:



142
# File 'app/models/contact_point.rb', line 142

validates :category, :detail, presence: true

#detailObject (readonly)

-- validation messages intentionally inline; locale extraction deferred

Validations:

  • Presence
  • Format ({ with: FACEBOOK_REGEX, if: :facebook?, message: 'must be a valid facebook.com url starting with http:// or https://' })
  • Format ({ with: TWITTER_REGEX, if: :twitter?, message: 'must be a valid twitter @username' })
  • Format ({ with: PINTEREST_REGEX, if: :pinterest?, message: 'must be a valid pinterest.com url starting with http:// or https://' })
  • Format ({ with: LINKEDIN_REGEX, if: :linkedin?, message: 'must be a valid linkedin.com url starting with http:// or https://' })
  • Length ({ maximum: 255 })

Validations (if => -> { !skip_validation && can_be_dialed? } ):

  • Phone_format

Validations (if => #email? ):

  • Email_format


142
# File 'app/models/contact_point.rb', line 142

validates :category, :detail, presence: true

#extensionObject (readonly)

rubocop:enable Rails/I18nLocaleTexts

Validations:



160
# File 'app/models/contact_point.rb', line 160

validates :extension, length: { maximum: 10 }

#requiredObject

Returns the value of attribute required.



190
191
192
# File 'app/models/contact_point.rb', line 190

def required
  @required
end

Class Method Details

.begins_withActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are begins with. Active Record Scope

Returns:

See Also:



176
# File 'app/models/contact_point.rb', line 176

scope :begins_with,      ->(term)  { where(ContactPoint[:detail].matches("#{term}%")) }

.belonging_to_partyActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are belonging to party. Active Record Scope

Returns:

See Also:



178
# File 'app/models/contact_point.rb', line 178

scope :belonging_to_party, -> { where ContactPoint[:party_id].not_eq(nil) }

.build_from_string(val, contact_points = nil, category = nil) ⇒ Object

Tries to build a contact point identifying the string.
Returns an existing contact point from the collection if detail matches,
otherwise builds a new one.



240
241
242
243
244
245
246
247
248
249
# File 'app/models/contact_point.rb', line 240

def self.build_from_string(val, contact_points = nil, category = nil)
  return if val.blank?

  found = contact_points.find { |cp| cp.detail == val }
  return found if found

  ncp = (contact_points || ContactPoint).new(detail: val, category: category)
  ncp.normalize_format
  ncp
end

.by_categoryActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are by category. Active Record Scope

Returns:

See Also:



163
# File 'app/models/contact_point.rb', line 163

scope :by_category,      ->(categories) { where(category: categories).where.not(detail: nil) }

.category_options_for_selectObject



251
252
253
# File 'app/models/contact_point.rb', line 251

def self.category_options_for_select
  ALL_CATEGORIES.map { |cat| [cat.humanize, cat] }
end

.company_sms_numbersActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are company sms numbers. Active Record Scope

Returns:

See Also:



182
# File 'app/models/contact_point.rb', line 182

scope :company_sms_numbers, -> { joins(:employee_phone_status).sorted }

.contactableActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are contactable. Active Record Scope

Returns:

See Also:



171
# File 'app/models/contact_point.rb', line 171

scope :contactable,      -> { by_category(CAN_DIAL + [EMAIL]) }

.contactable_options_for_selectObject



255
256
257
# File 'app/models/contact_point.rb', line 255

def self.contactable_options_for_select
  CONTACTABLE.map { |cat| [cat.humanize, cat] }
end

.containsActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are contains. Active Record Scope

Returns:

See Also:



175
# File 'app/models/contact_point.rb', line 175

scope :contains,         ->(term)  { where(ContactPoint[:detail].matches("%#{term}%")) }

.dialableActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are dialable. Active Record Scope

Returns:

See Also:



169
# File 'app/models/contact_point.rb', line 169

scope :dialable,         -> { by_category(CAN_DIAL) }

.emailsActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are emails. Active Record Scope

Returns:

See Also:



165
# File 'app/models/contact_point.rb', line 165

scope :emails,           -> { by_category(EMAIL) }

.failedActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are failed. Active Record Scope

Returns:

See Also:



177
# File 'app/models/contact_point.rb', line 177

scope :failed,           -> { where(state: 'failed') }

.faxesActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are faxes. Active Record Scope

Returns:

See Also:



167
# File 'app/models/contact_point.rb', line 167

scope :faxes,            -> { by_category(FAX) }

.find_emailsActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are find emails. Active Record Scope

Returns:

See Also:



173
# File 'app/models/contact_point.rb', line 173

scope :find_emails,      ->(email) { emails.where(ContactPoint[:detail].matches(email)) }

.find_phonesActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are find phones. Active Record Scope

Returns:

See Also:



174
# File 'app/models/contact_point.rb', line 174

scope :find_phones,      ->(phone) { dialable.begins_with(phone) }

.mark_email_as_failed(email, notes = nil) ⇒ Object



203
204
205
206
207
208
# File 'app/models/contact_point.rb', line 203

def self.mark_email_as_failed(email, notes = nil)
  ContactPoint.find_emails(email).each do |cp|
    cp.system_notes = notes if notes.present?
    cp.mark_as_failed
  end
end

.mark_email_as_working(email) ⇒ Object



221
222
223
224
225
226
227
# File 'app/models/contact_point.rb', line 221

def self.mark_email_as_working(email)
  ContactPoint.find_emails(email).where(state: %w[unknown failed]).find_each do |cp|
    cp.system_notes = nil
    Sendgrid::Toolkit.delete_invalids(emails: [email])
    cp.mark_as_working
  end
end

.migrate_dataObject

One-time migration utility — run via rails runner, not in production request cycle.



395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'app/models/contact_point.rb', line 395

def self.migrate_data
  contact_points = ContactPoint.dialable
  total_records = contact_points.size
  current_record = 0
  contact_points.find_each do |cp|
    Rails.logger.info "[#{current_record += 1}/#{total_records}] #{cp.id}"
    cp.send(:normalize_format)
    raise "Encountered anomaly: #{cp.id} #{cp.detail}" if cp.detail.blank?

    cp.update_columns(cp.attributes)
  end

  CallRecord.lease_connection.execute "update call_records set origin_number = '+1' || origin_number where length(origin_number) = 10;"
  CallRecord.lease_connection.execute "update call_records set destination_number = '+1' || destination_number where length(destination_number) = 10;"
  CallLog.lease_connection.execute "update call_logs set from_number = '+1' || from_number where length(from_number) = 10;"
  CallLog.lease_connection.execute "update call_logs set to_number = '+1' || to_number where length(to_number) = 10;"

  DoNotCall.lease_connection.execute "update do_not_calls set number = '+1' || number where length(number) = 10"

  Order.lease_connection.execute "update orders set shipping_phone = '+1' || shipping_phone where length(shipping_phone) = 10"
end

.pbx_didsActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are pbx dids. Active Record Scope

Returns:

See Also:



172
# File 'app/models/contact_point.rb', line 172

scope :pbx_dids,         -> { by_category(PHONE).joins(:employee_phone_status) }

.sms_numbersActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are sms numbers. Active Record Scope

Returns:

See Also:



188
# File 'app/models/contact_point.rb', line 188

scope :sms_numbers,         -> { sms_enabled }

.sms_statuses_for_selectObject



259
260
261
262
263
264
# File 'app/models/contact_point.rb', line 259

def self.sms_statuses_for_select
  [
    ['Not SMS capable', 'sms_none'],
    ['SMS enabled', 'sms_enabled']
  ]
end

.sortedActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are sorted. Active Record Scope

Returns:

See Also:



170
# File 'app/models/contact_point.rb', line 170

scope :sorted,           -> { order('contact_points.category, contact_points.position') }

.transmittableActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are transmittable. Active Record Scope

Returns:

See Also:



164
# File 'app/models/contact_point.rb', line 164

scope :transmittable,    -> { by_category(CAN_TRANSMIT) }

.under_customer_idsActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are under customer ids. Active Record Scope

Returns:

See Also:



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

scope :under_customer_ids, ->(customer_ids) {
  where('contact_points.party_id IN (:customer_ids) or exists(select 1 from parties cnt where cnt.customer_id IN (:customer_ids) and cnt.id = contact_points.party_id)', customer_ids: [customer_ids].flatten)
}

.voice_callableActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are voice callable. Active Record Scope

Returns:

See Also:



168
# File 'app/models/contact_point.rb', line 168

scope :voice_callable,   -> { by_category(CAN_VOICE) }

.website_category(uri) ⇒ Object

Returns the canonical ContactPoint category string for the given URL, or nil.



211
212
213
214
215
216
217
218
219
# File 'app/models/contact_point.rb', line 211

def self.website_category(uri)
  return HOUZZ     if HOUZZ_REGEX.match?(uri)
  return INSTAGRAM if INSTAGRAM_REGEX.match?(uri)
  return FACEBOOK  if FACEBOOK_REGEX.match?(uri)
  return PINTEREST if PINTEREST_REGEX.match?(uri)
  return LINKEDIN  if LINKEDIN_REGEX.match?(uri)

  WEBSITE if WEBSITE_REGEX.match?(uri)
end

.websitesActiveRecord::Relation<ContactPoint>

A relation of ContactPoints that are websites. Active Record Scope

Returns:

See Also:



166
# File 'app/models/contact_point.rb', line 166

scope :websites,         -> { by_category(WEBSITE) }

Instance Method Details

#area_code_objectAreaCode

-- polymorphic FK/PK pair; inverse not supported

Returns:

See Also:



110
# File 'app/models/contact_point.rb', line 110

belongs_to :area_code_object, class_name: 'AreaCode', inverse_of: :contact_points, foreign_key: :area_code, primary_key: :code, optional: true

#blocked?Boolean

Returns:

  • (Boolean)


345
346
347
# File 'app/models/contact_point.rb', line 345

def blocked?
  email_blocked? || dnc_blocked?
end

#call_blocked?Boolean

Returns:

  • (Boolean)


329
330
331
# File 'app/models/contact_point.rb', line 329

def call_blocked?
  callable? && do_not_call&.do_not_call&.to_b
end

#callable?Boolean

Returns:

  • (Boolean)


317
318
319
# File 'app/models/contact_point.rb', line 317

def callable?
  CAN_VOICE.include?(category)
end

#can_be_dialed?Boolean

Returns:

  • (Boolean)


281
282
283
# File 'app/models/contact_point.rb', line 281

def can_be_dialed?
  CAN_DIAL.include?(category)
end

#category_and_detailObject



277
278
279
# File 'app/models/contact_point.rb', line 277

def category_and_detail
  "#{category}: #{detail}"
end

#communication_recipientsActiveRecord::Relation<CommunicationRecipient>

Returns:

See Also:



118
# File 'app/models/contact_point.rb', line 118

has_many :communication_recipients, dependent: :nullify, inverse_of: :contact_point

#contactable?Boolean

Returns:

  • (Boolean)


229
230
231
# File 'app/models/contact_point.rb', line 229

def contactable?
  (CAN_DIAL + [EMAIL]).include?(category)
end

#dependents?Boolean

Returns:

  • (Boolean)


353
354
355
356
357
358
359
360
# File 'app/models/contact_point.rb', line 353

def dependents?
  quotes.exists? ||
    room_configurations.exists? ||
    communication_recipients.exists? ||
    opportunity_participants.exists? ||
    feeds.exists? ||
    service_job_dependent?
end

#dial_stringObject



366
367
368
369
370
# File 'app/models/contact_point.rb', line 366

def dial_string
  return phone_number.to_s if phone_number?

  detail
end

#dnc_blocked?Boolean

Returns:

  • (Boolean)


341
342
343
# File 'app/models/contact_point.rb', line 341

def dnc_blocked?
  call_blocked? || fax_blocked? || text_blocked?
end

#do_not_callDoNotCall

Returns:

See Also:



112
# File 'app/models/contact_point.rb', line 112

belongs_to :do_not_call, foreign_key: :detail, primary_key: :number, optional: true

#email?Boolean

Returns:

  • (Boolean)


285
286
287
# File 'app/models/contact_point.rb', line 285

def email?
  category == ContactPoint::EMAIL
end

#email_blocked?Boolean

Returns:

  • (Boolean)


325
326
327
# File 'app/models/contact_point.rb', line 325

def email_blocked?
  email? && email_preference&.any?
end

#email_preferenceEmailPreference



111
# File 'app/models/contact_point.rb', line 111

belongs_to :email_preference, foreign_key: :detail, primary_key: :email, optional: true

#email_support_case_participantsActiveRecord::Relation<SupportCaseParticipant>

Returns:

See Also:



124
# File 'app/models/contact_point.rb', line 124

has_many :email_support_case_participants, class_name: 'SupportCaseParticipant', dependent: :nullify, foreign_key: :email_id, inverse_of: :email

#employee_phone_statusEmployeePhoneStatus

rubocop:enable Rails/InverseOf



114
# File 'app/models/contact_point.rb', line 114

has_one :employee_phone_status, dependent: :destroy

#facebook?Boolean

Returns:

  • (Boolean)


293
294
295
# File 'app/models/contact_point.rb', line 293

def facebook?
  category == ContactPoint::FACEBOOK
end

#failed_before?Boolean

Returns:

  • (Boolean)


233
234
235
# File 'app/models/contact_point.rb', line 233

def failed_before?
  ContactPoint.exists?(detail: detail, state: 'failed')
end

#fax?Boolean

Returns:

  • (Boolean)


321
322
323
# File 'app/models/contact_point.rb', line 321

def fax?
  category == ContactPoint::FAX
end

#fax_blocked?Boolean

Returns:

  • (Boolean)


333
334
335
# File 'app/models/contact_point.rb', line 333

def fax_blocked?
  fax? && do_not_call&.do_not_fax&.to_b
end

#fax_support_case_participantsActiveRecord::Relation<SupportCaseParticipant>

Returns:

See Also:



125
# File 'app/models/contact_point.rb', line 125

has_many :fax_support_case_participants,   class_name: 'SupportCaseParticipant', dependent: :nullify, foreign_key: :fax_id,   inverse_of: :fax

#feedsActiveRecord::Relation<Feed>

Returns:

  • (ActiveRecord::Relation<Feed>)

See Also:



129
# File 'app/models/contact_point.rb', line 129

has_and_belongs_to_many :feeds,  inverse_of: :contact_points

#formatted_dial_stringObject



372
373
374
375
376
377
378
379
380
381
382
# File 'app/models/contact_point.rb', line 372

def formatted_dial_string
  if phone_number? && phone_number
    if party&.is_employee? && party.employee_record.hide_signature_pbx_extension
      phone_number.format(:crm_no_extension)
    else
      phone_number.format(:crm)
    end
  else
    detail
  end
end

#formatted_for_smsObject



384
385
386
387
388
# File 'app/models/contact_point.rb', line 384

def formatted_for_sms
  return unless phone_number? && sms_enabled?

  phone_number&.format(:pbx_dial)
end

#linkedin?Boolean

Returns:

  • (Boolean)


305
306
307
# File 'app/models/contact_point.rb', line 305

def linkedin?
  category == ContactPoint::LINKEDIN
end

#locator_recordLocatorRecord



109
# File 'app/models/contact_point.rb', line 109

belongs_to :locator_record, inverse_of: :contact_points, optional: true

#mark_as_working_if_detail_changedObject (protected)



474
475
476
# File 'app/models/contact_point.rb', line 474

protected def mark_as_working_if_detail_changed
  self.state = 'working' if (category == EMAIL) && detail_changed?
end

#normalize_formatObject

rubocop:enable Rails/SkipsModelValidations



418
419
420
421
422
423
424
425
426
427
428
429
430
431
# File 'app/models/contact_point.rb', line 418

def normalize_format
  return if detail.blank?

  normalize_detail_characters
  if RFC822::EMAIL.match?(detail)
    normalize_as_email
  elsif website?
    normalize_as_website
  elsif (parsed = PhoneNumber.parse(detail))
    normalize_as_phone(parsed)
  end

  true
end

#notification_channelsActiveRecord::Relation<NotificationChannel>

Returns:

See Also:



117
# File 'app/models/contact_point.rb', line 117

has_many :notification_channels, dependent: :destroy

#opportunity_participantsActiveRecord::Relation<OpportunityParticipant>

rubocop:enable Rails/InverseOf, Rails/HasManyOrHasOneDependent

Returns:

See Also:



122
# File 'app/models/contact_point.rb', line 122

has_many :opportunity_participants, dependent: :nullify, inverse_of: :contact_point

#partyParty

Returns:

See Also:



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

belongs_to :party, inverse_of: :contact_points, optional: true

#phone_numberObject



362
363
364
# File 'app/models/contact_point.rb', line 362

def phone_number
  PhoneNumber.parse(detail)
end

#phone_number?Boolean

Returns:

  • (Boolean)


313
314
315
# File 'app/models/contact_point.rb', line 313

def phone_number?
  CAN_DIAL.include?(category)
end

#phone_support_case_participantsActiveRecord::Relation<SupportCaseParticipant>

Returns:

See Also:



123
# File 'app/models/contact_point.rb', line 123

has_many :phone_support_case_participants, class_name: 'SupportCaseParticipant', dependent: :nullify, foreign_key: :phone_id, inverse_of: :phone

#pinterest?Boolean

Returns:

  • (Boolean)


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

def pinterest?
  category == ContactPoint::PINTEREST
end

#preferred?Boolean

Returns:

  • (Boolean)


349
350
351
# File 'app/models/contact_point.rb', line 349

def preferred?
  position == 1
end

#quotesActiveRecord::Relation<Quote>

-- join tables pre-date has_many :through; migrate when touched

Returns:

  • (ActiveRecord::Relation<Quote>)

See Also:



128
# File 'app/models/contact_point.rb', line 128

has_and_belongs_to_many :quotes, inverse_of: :contact_points

#room_configurationsActiveRecord::Relation<RoomConfiguration>

Returns:

  • (ActiveRecord::Relation<RoomConfiguration>)

See Also:



116
# File 'app/models/contact_point.rb', line 116

has_many :room_configurations, dependent: :nullify

#set_default_sms_statusvoid

This method returns an undefined value.

Bootstrap sms_status before we have an authoritative answer from Twilio
Lookup. CELL is treated as optimistically enabled; everything else stays
disabled until ContactPointLineTypeLookupHandler fires (or an outbound
send flips it via mark_external_numbers_as_sms_enabled).

Logic Details

sms_status_source is preserved across saves so a Twilio-derived answer
stays put — but only as long as the evidence underlying it is unchanged.
If the user edits detail (corrects the phone number) or flips category
(PHONE → CELL), the prior twilio_lookup answer no longer describes this
row and we re-bootstrap to 'default' so the next ContactPointPhoneChanged
event re-runs the lookup. Without this, an old "this is a landline"
answer would silently persist onto the new number.



457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'app/models/contact_point.rb', line 457

def set_default_sms_status
  evidence_changed = will_save_change_to_detail? || will_save_change_to_category?
  return if sms_status_source.present? && !evidence_changed

  # phone_number? covers CAN_DIAL = [CELL, FAX, PHONE]. Only CELL is
  # optimistically SMS-able; PHONE/FAX get sms_none until Twilio Lookup
  # confirms otherwise. The explicit ternary (vs. an `if … else`) closes
  # the CELL→PHONE category-change hole where falling through would leave
  # a stale sms_enabled stamp on what's now flagged as a landline category.
  self.sms_status = phone_number? && category == CELL ? :sms_enabled : :sms_none

  return unless sms_status_changed? || new_record? || evidence_changed

  self.sms_status_source = 'default'
  self.sms_status_verified_at = nil if evidence_changed
end

#subscribersActiveRecord::Relation<Subscriber>

rubocop:disable Rails/InverseOf

Returns:

See Also:



120
# File 'app/models/contact_point.rb', line 120

has_many :subscribers, foreign_key: :email_address, primary_key: :detail

#text_blocked?Boolean

Returns:

  • (Boolean)


337
338
339
# File 'app/models/contact_point.rb', line 337

def text_blocked?
  phone_number? && sms_enabled? && do_not_call&.do_not_text&.to_b
end

#time_at_locationObject



271
272
273
274
275
# File 'app/models/contact_point.rb', line 271

def time_at_location
  return unless (tz = timezone)

  tz.utc_to_local(Time.current).strftime('%I:%M %P')
end

#timezoneObject



266
267
268
269
# File 'app/models/contact_point.rb', line 266

def timezone
  tzn = timezone_name || area_code_object&.timezone_name
  Timezone[tzn.to_s] # returns nil for invalid timezone strings
end

#to_sObject



390
391
392
# File 'app/models/contact_point.rb', line 390

def to_s
  "#{category} : #{formatted_dial_string}"
end

#transmittable?Boolean

Returns:

  • (Boolean)


309
310
311
# File 'app/models/contact_point.rb', line 309

def transmittable?
  CAN_TRANSMIT.include?(category)
end

#twitter?Boolean

Returns:

  • (Boolean)


297
298
299
# File 'app/models/contact_point.rb', line 297

def twitter?
  category == ContactPoint::TWITTER
end

#website?Boolean

Returns:

  • (Boolean)


289
290
291
# File 'app/models/contact_point.rb', line 289

def website?
  [ContactPoint::WEBSITE, ContactPoint::FACEBOOK, ContactPoint::PINTEREST, ContactPoint::LINKEDIN].include?(category)
end

#website_url_formatObject



433
434
435
436
437
438
439
# File 'app/models/contact_point.rb', line 433

def website_url_format
  return if detail.blank?

  errors.add(:detail, INVALID_URL_MESSAGE) unless valid_website_uri?
rescue Addressable::URI::InvalidURIError
  errors.add(:detail, INVALID_URL_MESSAGE)
end