Class: SmsMessage
Overview
rubocop:disable Metrics/ClassLength
== Schema Information
Table name: sms_messages
Database name: primary
id :integer not null, primary key
autoreplied_at :datetime
body :text
direction :enum default("inbound"), not null
num_media :integer default(0), not null
raw_data :jsonb
recipient :string not null
sender :string not null
state :string default("draft"), not null
status_message :string
transmit_at :datetime
unread :boolean default(TRUE), not null
created_at :datetime not null
updated_at :datetime not null
creator_id :integer
recipient_party_id :integer
sender_party_id :integer
unique_id :string
updater_id :integer
Indexes
idx_direction_recipient (direction,recipient)
index_sms_messages_on_recipient (recipient)
index_sms_messages_on_recipient_party_id (recipient_party_id)
index_sms_messages_on_sender (sender)
index_sms_messages_on_sender_party_id (sender_party_id)
index_sms_messages_on_state (state)
index_sms_messages_on_transmit_at (transmit_at)
sms_messages_unique_id (unique_id) UNIQUE
Foreign Keys
fk_rails_... (recipient_party_id => parties.id)
fk_rails_... (sender_party_id => parties.id)
Constant Summary
collapse
- SMS_GLOBAL_NUMBERS =
['+18008755285', '+18475407775', '+18473183355', '+18473501641',
'+18473501655', '+18473489826', '+18473507429', '+18473501980',
'+18472629814', '+18473507262', '+18473507212'].freeze
- TWILIO_ERROR_MESSAGES =
{
'30003' => 'Recipient phone is unreachable',
'30004' => 'Recipient has blocked messages from this number',
'30005' => 'Recipient phone number is unknown or no longer in service',
'30006' => 'Recipient number is a landline or an unreachable carrier and cannot receive SMS',
'30007' => 'Message was flagged as spam and filtered by the carrier',
'30008' => 'Unknown delivery failure reported by the carrier'
}.freeze
Models::Auditable::ALWAYS_IGNORED
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
#creator, #updater
Has and belongs to many
collapse
Delegated Instance Attributes
collapse
Class Method Summary
collapse
-
.between_participants ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are between participants.
-
.employee_names_by_number(numbers) ⇒ Object
Map SMS numbers to the active employee who owns them (their pbx_did / company SMS line), for the sender dropdown.
-
.for_employee_ids ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are for employee ids.
-
.for_numbers ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are for numbers.
-
.global_numbers_for_select ⇒ Object
-
.latest_per_pair ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are latest per pair.
-
.match_inbound_unknown_senders(numbers: nil, party: nil, date_range: nil) ⇒ Object
Bulk inbound sender match.
-
.match_outbound_unknown_recipient(numbers: nil, party: nil, date_range: nil) ⇒ Object
-
.match_unknown(**args) ⇒ void
Matches both inbound unknown senders and outbound unknown recipients.
-
.pair_counts(scope = all) ⇒ Object
Total + unread message counts per number-pair, keyed by [low, high] number.
-
.pair_expressions ⇒ Object
SQL for the unordered number-pair of a message, so the same exchange keys the same regardless of send direction.
-
.parties_matching_number(number) ⇒ Object
-
.party_names_by_number(numbers) ⇒ Object
Map SMS numbers to a party (contact/customer) that owns them, for the recipient dropdown.
-
.ransackable_scopes(_auth_object = nil) ⇒ Object
-
.sender_line_label(number, owner_name = nil) ⇒ Object
Friendly label for a company SMS line: the owning rep if known, else the toll-free / local identity from PhoneNumbers, else a generic shared line.
-
.unread ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are unread.
-
.update_status(params) ⇒ Object
Processes events coming in from the twilio gateway basically discover the message that was sent and update its status.
Instance Method Summary
collapse
#publish_event
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
ransackable_associations, ransackable_attributes, ransortable_attributes, #to_relation
config
#after_commit
Instance Attribute Details
#body ⇒ Object
85
|
# File 'app/models/sms_message.rb', line 85
validates :body, length: { maximum: 1600 }, if: :outbound?
|
#direction ⇒ Object
84
|
# File 'app/models/sms_message.rb', line 84
validates :sender, :recipient, :direction, presence: true
|
#recipient ⇒ Object
84
|
# File 'app/models/sms_message.rb', line 84
validates :sender, :recipient, :direction, presence: true
|
#sender ⇒ Object
84
|
# File 'app/models/sms_message.rb', line 84
validates :sender, :recipient, :direction, presence: true
|
Class Method Details
.between_participants ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are between participants. Active Record Scope
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
# File 'app/models/sms_message.rb', line 95
scope :between_participants, ->(*numbers) {
number1, number2 = [numbers].flatten.filter_map(&:presence).filter_map { |n| PhoneNumber.parse_and_format(n) }
if number1 && number2
where('
(sms_messages.sender = :number1 OR sms_messages.recipient = :number1)
AND
(sms_messages.sender = :number2 OR sms_messages.recipient = :number2)
', number1:, number2:)
elsif (number = number1 || number2)
for_numbers(number)
else
SmsMessage.none
end
}
|
.employee_names_by_number(numbers) ⇒ Object
Map SMS numbers to the active employee who owns them (their pbx_did /
company SMS line), for the sender dropdown.
150
151
152
153
154
155
156
157
|
# File 'app/models/sms_message.rb', line 150
def self.employee_names_by_number(numbers)
return {} if numbers.blank?
ContactPoint.joins(:party, :employee_phone_status)
.where(detail: numbers)
.pluck(:detail, 'parties.full_name')
.to_h
end
|
.for_employee_ids ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are for employee ids. Active Record Scope
87
88
89
90
91
|
# File 'app/models/sms_message.rb', line 87
scope :for_employee_ids, ->(*rep_ids, include_global: true) {
sms_numbers = Employee.all_active_employees_sms_numbers(rep_ids:)
sms_numbers += SMS_GLOBAL_NUMBERS if include_global
for_numbers(sms_numbers)
}
|
.for_numbers ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are for numbers. Active Record Scope
92
93
94
|
# File 'app/models/sms_message.rb', line 92
scope :for_numbers, ->(*numbers) {
where('sms_messages.sender IN (:numbers) OR sms_messages.recipient IN (:numbers)', numbers: [numbers].flatten.compact.uniq)
}
|
.global_numbers_for_select ⇒ Object
402
403
404
|
# File 'app/models/sms_message.rb', line 402
def self.global_numbers_for_select
SMS_GLOBAL_NUMBERS.map { |n| [n, n] }
end
|
.latest_per_pair ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are latest per pair. Active Record Scope
122
123
124
125
126
|
# File 'app/models/sms_message.rb', line 122
scope :latest_per_pair, lambda {
exprs = pair_expressions
select("DISTINCT ON (#{exprs.join(', ')}) sms_messages.*")
.order(Arel.sql("#{exprs.join(', ')}, created_at DESC"))
}
|
.match_inbound_unknown_senders(numbers: nil, party: nil, date_range: nil) ⇒ Object
Bulk inbound sender match
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
|
# File 'app/models/sms_message.rb', line 578
def self.match_inbound_unknown_senders(numbers: nil, party: nil, date_range: nil)
messages = SmsMessage.inbound.where(sender_party_id: nil)
messages = messages.where(created_at: date_range) if date_range
messages = messages.where(sender: numbers) if numbers.present?
messages.each do |m|
m.sender_party = party
m.sender_party ||= parties_matching_number(m.sender).active.find_by(type: 'Contact')
m.sender_party ||= parties_matching_number(m.sender).active.find_by(type: 'Customer')
m.sender_party ||= parties_matching_number(m.sender).inactive.find_by(type: 'Contact')
m.sender_party ||= parties_matching_number(m.sender).inactive.find_by(type: 'Customer')
m.save if m.sender_party
end
end
|
.match_outbound_unknown_recipient(numbers: nil, party: nil, date_range: nil) ⇒ Object
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
|
# File 'app/models/sms_message.rb', line 597
def self.match_outbound_unknown_recipient(numbers: nil, party: nil, date_range: nil)
messages = SmsMessage.outbound.where(recipient_party_id: nil)
messages = messages.where(created_at: date_range) if date_range
messages = messages.where(recipient: numbers) if numbers.present?
messages.each do |m|
m.recipient_party = party
m.recipient_party ||= parties_matching_number(m.recipient).active.where(type: 'Contact').first
m.recipient_party ||= parties_matching_number(m.recipient).active.where(type: 'Customer').first
m.recipient_party ||= parties_matching_number(m.recipient).inactive.where(type: 'Contact').first
m.recipient_party ||= parties_matching_number(m.recipient).inactive.where(type: 'Customer').first
m.save if m.recipient_party
end
end
|
.match_unknown(**args) ⇒ void
This method returns an undefined value.
Matches both inbound unknown senders and outbound unknown recipients.
572
573
574
575
|
# File 'app/models/sms_message.rb', line 572
def self.match_unknown(**args)
match_inbound_unknown_senders(**args)
match_outbound_unknown_recipient(**args)
end
|
.pair_counts(scope = all) ⇒ Object
Total + unread message counts per number-pair, keyed by [low, high] number.
129
130
131
132
133
|
# File 'app/models/sms_message.rb', line 129
def self.pair_counts(scope = all)
scope.group(*pair_expressions)
.pluck(*pair_expressions, Arel.sql('COUNT(*)'), Arel.sql('COUNT(*) FILTER (WHERE unread)'))
.to_h { |low, high, total, unread| [[low, high], [total, unread]] }
end
|
.pair_expressions ⇒ Object
SQL for the unordered number-pair of a message, so the same exchange keys
the same regardless of send direction. Mirrors #conversation_id in SQL.
117
118
119
|
# File 'app/models/sms_message.rb', line 117
def self.pair_expressions
[Arel.sql('LEAST(sender, recipient)'), Arel.sql('GREATEST(sender, recipient)')]
end
|
.parties_matching_number(number) ⇒ Object
616
617
618
619
620
|
# File 'app/models/sms_message.rb', line 616
def self.parties_matching_number(number)
return Party.none if number.blank?
Party.joins(:contact_points).where(ContactPoint[:detail].eq(number))
end
|
.party_names_by_number(numbers) ⇒ Object
Map SMS numbers to a party (contact/customer) that owns them, for the
recipient dropdown. First non-blank name per number wins.
139
140
141
142
143
144
145
146
|
# File 'app/models/sms_message.rb', line 139
def self.party_names_by_number(numbers)
return {} if numbers.blank?
ContactPoint.eager_load(:party)
.where(detail: numbers)
.pluck(:detail, 'parties.full_name')
.each_with_object({}) { |(number, name), map| map[number] ||= name if name.present? }
end
|
.ransackable_scopes(_auth_object = nil) ⇒ Object
177
178
179
|
# File 'app/models/sms_message.rb', line 177
def self.ransackable_scopes(_auth_object = nil)
[:between_participants]
end
|
.sender_line_label(number, owner_name = nil) ⇒ Object
Friendly label for a company SMS line: the owning rep if known, else the
toll-free / local identity from PhoneNumbers, else a generic shared line.
161
162
163
164
165
166
167
168
169
|
# File 'app/models/sms_message.rb', line 161
def self.sender_line_label(number, owner_name = nil)
owner = owner_name ||
case number
when "+#{PhoneNumbers::TOLLFREE_LINK}" then 'WarmlyYours (toll-free)'
when "+#{PhoneNumbers::LOCAL_LINK}" then 'WarmlyYours (local)'
else 'Company line'
end
"#{number} - #{owner}"
end
|
.unread ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are unread. Active Record Scope
113
|
# File 'app/models/sms_message.rb', line 113
scope :unread, -> { where(unread: true) }
|
.update_status(params) ⇒ Object
Processes events coming in from the twilio gateway basically discover
the message that was sent and update its status
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
|
# File 'app/models/sms_message.rb', line 496
def self.update_status(params)
return false if params.blank?
return false unless (sid = params['SmsSid'].presence)
return false unless (msg = SmsMessage.outbound.where(unique_id: sid).first)
msg.raw_data = params.except(:action, :controller)
case params['SmsStatus']
when 'delivered'
msg.trigger_delivered
when 'failed', 'undelivered'
msg.trigger_exception
else
msg.save
end
true
end
|
Instance Method Details
482
483
484
|
# File 'app/models/sms_message.rb', line 482
def all_related_sms_messages
SmsMessage.where('sms_messages.sender IN (:participants) OR sms_messages.recipient IN (:participants)', participants: [sender, recipient]).order('sms_messages.created_at DESC')
end
|
#auto_reply_outside_business_hours ⇒ Object
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
|
# File 'app/models/sms_message.rb', line 363
def auto_reply_outside_business_hours
return unless received_outside_business_hours? && autoreplied_at.nil? && !messages_in_conversation.where(transmit_at: 1.hour.ago..).exists?
return if PhoneNumber.shortcode?(sender)
body = 'Thank you for your message. We will get back to you during business hours, Mon-Fri from 7:30am to 5:30pm CST'
begin
TwilioClient.send_message(from: recipient, to: sender, body:)
update(autoreplied_at: Time.current)
rescue Twilio::REST::RestError => e
raise unless e.code == 21_610
Rails.logger.info "[SmsMessage] Auto-reply skipped - recipient #{sender} has unsubscribed from SMS"
end
end
|
#block_sender ⇒ Object
283
284
285
286
287
288
|
# File 'app/models/sms_message.rb', line 283
def block_sender
return false unless inbound?
return true if sender_sms_block
build_sender_sms_block.save
end
|
#conversation_id ⇒ Object
337
338
339
|
# File 'app/models/sms_message.rb', line 337
def conversation_id
[sender, recipient].sort
end
|
326
327
328
329
330
331
332
333
334
335
|
# File 'app/models/sms_message.rb', line 326
def create_media_urls
twilio_config = Heatwave::Configuration.fetch(:twilio)
base_url = twilio_config[:sms_base_wehook_url]
raise 'Unable to configure base URL for media download' unless base_url
uploads.map do |upload|
token = upload.active_download_token
"#{base_url}/media_download/#{token}"
end.presence
end
|
#deep_dup ⇒ Object
181
182
183
184
185
186
187
188
|
# File 'app/models/sms_message.rb', line 181
def deep_dup
deep_clone(
include: :uploads,
except: %i[unique_id transmit_at created_at updated_at]
) do |_original, copy|
copy.state = 'draft' if copy.is_a?(SmsMessage)
end
end
|
#editable? ⇒ Boolean
490
491
492
|
# File 'app/models/sms_message.rb', line 490
def editable?
draft? && outbound?
end
|
#enqueue_crm_navbar_refresh ⇒ Object
Only enqueue when the change can affect the navbar badge — i.e. the
inbound/unread tuple. Skips chatty outbound updates (state machine churn,
delivery callbacks) and any inbound update that doesn't move unread.
629
630
631
632
633
634
635
636
637
638
639
|
# File 'app/models/sms_message.rb', line 629
def enqueue_crm_navbar_refresh
return unless inbound?
if previously_new_record?
return unless unread?
elsif !saved_change_to_unread?
return
end
CrmNavbarFanoutWorker.perform_async('sms')
end
|
#exception_reason ⇒ Object
Human-readable reason an exception-state message failed. Media-fetch
failures already populate status_message; send failures only get a
Twilio ErrorCode via the status webhook (see #update_status).
435
436
437
438
439
440
441
|
# File 'app/models/sms_message.rb', line 435
def exception_reason
return nil unless exception?
status_message.presence ||
TWILIO_ERROR_MESSAGES[raw_data.is_a?(Hash) ? raw_data['ErrorCode'].to_s : nil] ||
'Message could not be delivered'
end
|
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
|
# File 'app/models/sms_message.rb', line 443
def fetch_media
return true unless num_media.positive?
begin
self.raw_data ||= {}
(0..(num_media - 1)).to_a.each do |i|
media_content_type = raw_data["MediaContentType#{i}"]
media_url = raw_data["MediaUrl#{i}"]
next unless media_url
uri = Addressable::URI.parse(media_url)
uri.user = Heatwave::Configuration.fetch(:twilio, :account_sid)
uri.password = Heatwave::Configuration.fetch(:twilio, :auth_token)
ext = Mime::Type.lookup(media_content_type).symbol.to_s
upload = Upload.uploadify_from_url(file_name: "attachment_#{i}.#{ext}",
url: uri.to_s,
category: nil,
resource: self)
uploads << upload
end
true
rescue Down::NotFound => e
self.status_message = e.to_s
ErrorReporting.warning(e, reason: 'twilio_media_expired', sms_message_id: id)
false
rescue Down::ClientError => e
self.status_message = e.to_s
ErrorReporting.warning(e, reason: 'twilio_media_access_denied', sms_message_id: id)
false
rescue StandardError => e
self.status_message = e.to_s
ErrorReporting.error(e)
false
end
end
|
#from_country_iso ⇒ Object
418
419
420
|
# File 'app/models/sms_message.rb', line 418
def from_country_iso
raw_data['FromCountry'].presence || PhoneNumber.parse(sender).country
end
|
#increment_publication_counters ⇒ Object
Increment the counter of our publications, sort of a way to keep track of popularity
662
663
664
|
# File 'app/models/sms_message.rb', line 662
def increment_publication_counters
publications.each { |p| p.increment!(:requested_counter) }
end
|
#literatures ⇒ Object
Alias for Uploads#literatures
655
|
# File 'app/models/sms_message.rb', line 655
delegate :literatures, to: :uploads
|
#mark_conversation_read! ⇒ Integer
Clear unread on every inbound message in this conversation. Replying to
someone means you read what they sent (Mary Godawa, 2026-07-27) — before
this, only the single message whose Reply link was clicked lost its unread
flag, so the rest of the thread stayed bold and kept feeding the navbar
badge. Updated one row at a time on purpose: the unread after_commit is
what refreshes that badge, and update_all would skip it.
353
354
355
356
357
|
# File 'app/models/sms_message.rb', line 353
def mark_conversation_read!
marked = messages_in_conversation.inbound.unread.to_a
marked.each { |message| message.update_attribute!(:unread, false) }
marked.size
end
|
#mark_external_numbers_as_sms_enabled ⇒ Object
622
623
624
|
# File 'app/models/sms_message.rb', line 622
def mark_external_numbers_as_sms_enabled
ContactPoint.dialable.where('contact_points.detail = :sender OR contact_points.detail = :recipient', sender:, recipient:).sms_none.each(&:sms_enabled!)
end
|
#match_inbound_recipient ⇒ Object
517
518
519
520
521
522
523
524
525
|
# File 'app/models/sms_message.rb', line 517
def match_inbound_recipient
return unless inbound? && recipient_party.nil?
employee = Employee.joins(:contact_points, :employee_phone_status).active_employees.find_by(ContactPoint[:detail].eq(recipient))
return unless employee
self.recipient_party = employee
end
|
#match_inbound_sender ⇒ Object
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
|
# File 'app/models/sms_message.rb', line 549
def match_inbound_sender
return unless inbound?
prev_message = SmsMessage.outbound.order(created_at: :desc).where.not(recipient_party_id: nil).find_by(recipient: sender)
self.sender_party ||= prev_message.recipient_party if prev_message
self.sender_party ||= self.class.parties_matching_number(sender).active.order(created_at: :desc).find_by(type: 'Contact')
self.sender_party ||= self.class.parties_matching_number(sender).active.order(created_at: :desc).find_by(type: 'Customer')
self.sender_party ||= self.class.parties_matching_number(sender).inactive.order(created_at: :desc).find_by(type: 'Contact')
self.sender_party ||= self.class.parties_matching_number(sender).inactive.order(created_at: :desc).find_by(type: 'Customer')
end
|
#match_outbound_recipient ⇒ Object
536
537
538
539
540
541
542
543
544
545
546
547
|
# File 'app/models/sms_message.rb', line 536
def match_outbound_recipient
return unless outbound?
self.recipient_party ||= self.class.parties_matching_number(recipient).order(created_at: :desc).active.find_by(type: 'Contact')
self.recipient_party ||= self.class.parties_matching_number(recipient).order(created_at: :desc).active.find_by(type: 'Customer')
self.recipient_party ||= self.class.parties_matching_number(recipient).order(created_at: :desc).inactive.find_by(type: 'Contact')
self.recipient_party ||= self.class.parties_matching_number(recipient).order(created_at: :desc).inactive.find_by(type: 'Customer')
end
|
#match_outbound_sender ⇒ Object
527
528
529
530
531
532
533
534
|
# File 'app/models/sms_message.rb', line 527
def match_outbound_sender
return unless outbound? && sender_party.nil?
employee = Employee.joins(:contact_points, :employee_phone_status).active_employees.find_by(ContactPoint[:detail].eq(sender))
return unless employee
self.sender_party = employee
end
|
#messages_in_conversation ⇒ Object
341
342
343
|
# File 'app/models/sms_message.rb', line 341
def messages_in_conversation
self.class.between_participants(sender, recipient)
end
|
513
514
515
|
# File 'app/models/sms_message.rb', line 513
def no_media?
uploads.blank?
end
|
#publications ⇒ Object
657
658
659
|
# File 'app/models/sms_message.rb', line 657
def publications
Item.joins(:literature).merge(literatures)
end
|
#receive_message ⇒ Object
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
|
# File 'app/models/sms_message.rb', line 385
def receive_message
return false unless can_trigger_received?
mark_external_numbers_as_sms_enabled
auto_reply_outside_business_hours
match_inbound_recipient
match_inbound_sender
if fetch_media
trigger_received
else
trigger_exception
end
end
|
#received_outside_business_hours? ⇒ Boolean
359
360
361
|
# File 'app/models/sms_message.rb', line 359
def received_outside_business_hours?
!WorkingHours.in_working_hours?(created_at)
end
|
#recipient_do_not_text ⇒ DoNotCall
68
|
# File 'app/models/sms_message.rb', line 68
has_one :recipient_do_not_text, -> { where(do_not_text: true) }, class_name: 'DoNotCall', primary_key: :recipient, foreign_key: :number
|
#recipient_party ⇒ Party
65
|
# File 'app/models/sms_message.rb', line 65
belongs_to :recipient_party, class_name: 'Party', inverse_of: :recipient_sms_messages, optional: true
|
#recipient_sms_block ⇒ SmsBlock
67
|
# File 'app/models/sms_message.rb', line 67
has_one :recipient_sms_block, class_name: 'SmsBlock', primary_key: :recipient, foreign_key: :number
|
#recipient_to_s ⇒ Object
412
413
414
415
416
|
# File 'app/models/sms_message.rb', line 412
def recipient_to_s
r = PhoneNumber.parse_and_format(recipient).to_s
r << " (#{recipient_party.full_name})" if recipient_party
r
end
|
#send_message ⇒ Object
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
|
# File 'app/models/sms_message.rb', line 298
def send_message
return false unless valid? && can_trigger_send?
match_outbound_sender
match_outbound_recipient
mark_external_numbers_as_sms_enabled
media_urls = create_media_urls
res = TwilioClient.send_message(from: sender, to: recipient, body:, media_urls:)
self.unique_id = res.sid
trigger_send
rescue Twilio::REST::RestError => e
Rails.logger.warn("[SmsMessage##{id}] Twilio send failed: #{e.code} #{e.message}")
errors.add(:base, twilio_error_message(e))
false
end
|
#sender_blocked? ⇒ Boolean
276
277
278
279
280
281
|
# File 'app/models/sms_message.rb', line 276
def sender_blocked?
return false unless inbound?
SmsBlock.where(number: sender).exists?
end
|
#sender_do_not_text ⇒ DoNotCall
69
|
# File 'app/models/sms_message.rb', line 69
has_one :sender_do_not_text, -> { where(do_not_text: true) }, class_name: 'DoNotCall', primary_key: :sender, foreign_key: :number
|
#sender_party ⇒ Party
64
|
# File 'app/models/sms_message.rb', line 64
belongs_to :sender_party, class_name: 'Party', inverse_of: :sender_sms_messages, optional: true
|
#sender_sms_block ⇒ SmsBlock
66
|
# File 'app/models/sms_message.rb', line 66
has_one :sender_sms_block, class_name: 'SmsBlock', primary_key: :sender, foreign_key: :number
|
#sender_to_s ⇒ Object
406
407
408
409
410
|
# File 'app/models/sms_message.rb', line 406
def sender_to_s
r = PhoneNumber.parse_and_format(sender).to_s
r << " (#{sender_party.full_name})" if sender_party
r
end
|
#set_defaults ⇒ Object
641
642
643
644
645
646
647
648
649
650
651
652
|
# File 'app/models/sms_message.rb', line 641
def set_defaults
self.unread = false if outbound?
if (sn = PhoneNumber.parse_and_format(sender)).present?
self.sender = sn
end
if (rn = PhoneNumber.parse_and_format(recipient)).present?
self.recipient = rn
end
:continue
end
|
#to_partial_path ⇒ Object
The partial for this model lives under app/views/crm/sms_messages/,
not the conventional app/views/sms_messages/. Without this override
render @sms_messages from any non-crm/sms_messages controller
(CustomersController#tab_sms, OrdersController#tab_sms, …) resolves
the derived sms_messages/sms_message path against the calling
controller's view dir and raises ActionView::MissingTemplate.
See AppSignal #5422 / #5532.
55
56
57
|
# File 'app/models/sms_message.rb', line 55
def to_partial_path
'crm/sms_messages/sms_message'
end
|
#to_s ⇒ Object
486
487
488
|
# File 'app/models/sms_message.rb', line 486
def to_s
"SMS #{id}"
end
|
#twilio_error_message(error) ⇒ Object
Twilio surfaces the human-readable reason after the "NN : " code prefix;
fall back to the whole message if the shape ever changes.
321
322
323
324
|
# File 'app/models/sms_message.rb', line 321
def twilio_error_message(error)
detail = error.message.to_s.split(' : ', 2).last.presence || error.message.to_s
"Twilio could not send this message: #{detail.split("\n").first.strip}"
end
|
#unblock_sender ⇒ Object
290
291
292
293
294
295
296
|
# File 'app/models/sms_message.rb', line 290
def unblock_sender
return false unless inbound?
return true unless sender_sms_block
sender_sms_block.destroy
true
end
|
#uploads ⇒ ActiveRecord::Relation<Upload>
71
|
# File 'app/models/sms_message.rb', line 71
has_and_belongs_to_many :uploads
|