Class: CallRecord

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

Overview

Phone call records from the Switchvox phone system.
Stores call metadata, transcriptions, and AI analysis results.
rubocop:disable Metrics/ClassLength
== Schema Information

Table name: call_records
Database name: primary

id :integer not null, primary key
action_items :jsonb
agent_performance_score :integer
agent_speaker_label :string
ai_summary :text
audio_channels :integer
call_direction :integer
call_outcome :integer
call_phases :jsonb
customer_satisfaction :string
destination_name :string(255)
destination_number :string(255)
duration_secs :integer default(0)
imported :boolean
key_topics :jsonb
lemur_analyzed_at :datetime
note :string(255)
notes :text
origin_name :string(255)
origin_number :string(255)
recording_source :string
reference1 :string(255)
reference2 :string(255)
structured_transcript_json :jsonb
summarized_at :datetime
transcribed_at :datetime
transcript :text
transcription_state :integer default("pending")
tsvector_search_tsearch :tsvector
tsvector_transcript_tsearch :tsvector
twilio_call_details :jsonb
twilio_call_sid :string
twilio_recording_sid :string
unread :boolean default(FALSE), not null
unrestricted :boolean default(FALSE), not null
created_at :datetime not null
assemblyai_transcript_id :string
destination_party_id :integer
origin_party_id :integer
switchvox_from_account_id :integer
switchvox_recorded_call_id :integer
switchvox_to_account_id :integer

Indexes

idx_call_records_id_where_dp_id (id) WHERE (destination_party_id IS NULL)
idx_call_records_id_where_op_id (id) WHERE (origin_party_id IS NULL)
idx_origin_id_dest_id_created_at (origin_party_id,destination_party_id,created_at)
index_call_records_on_assemblyai_transcript_id (assemblyai_transcript_id) UNIQUE
index_call_records_on_created_at (created_at)
index_call_records_on_destination_party_id (destination_party_id)
index_call_records_on_switchvox_recorded_call_id (switchvox_recorded_call_id) UNIQUE
index_call_records_on_transcribed_at (transcribed_at)
index_call_records_on_transcription_state (transcription_state)
index_call_records_on_tsvector_search_tsearch (tsvector_search_tsearch) USING gin
index_call_records_on_tsvector_transcript_tsearch (tsvector_transcript_tsearch) USING gin
index_call_records_on_twilio_recording_sid (twilio_recording_sid) UNIQUE
index_call_records_on_unread (unread) WHERE (unread = true)

Defined Under Namespace

Classes: SwitchvoxImporterFile, SwitchvoxImporterSftp, TwilioRecordingImporter

Constant Summary collapse

VOICEMAIL_EXTENSIONS =
%w[402].freeze
MIN_TRANSCRIPTION_DURATION =
30
MIN_TRANSCRIPTION_DURATION_VOICEMAIL =
5
SATISFACTION_LEVELS =

Customer satisfaction levels (stored as string, set by LeMUR analysis)

%w[very_satisfied satisfied neutral frustrated angry].freeze
SATISFACTION_DISPLAY =
{
  'very_satisfied' => '😊 Very Satisfied',
  'satisfied' => '🙂 Satisfied',
  'neutral' => '😐 Neutral',
  'frustrated' => '😤 Frustrated',
  'angry' => '😠 Angry'
}.freeze
COMPANY_MAIN_NUMBERS =

Company main line numbers that should NOT be matched to parties
These are toll-free/main numbers that route to the IVR/queue, not individual employees

%w[
  +18008755285
  +18664361444
  +18475502400
].freeze

Constants included from Models::Embeddable

Models::Embeddable::DEFAULT_MODEL, Models::Embeddable::MAX_CONTENT_LENGTH

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Has one collapse

Has many collapse

Methods included from Models::Embeddable

#content_embeddings

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Embeddable

#embeddable_locales, #embedding_content_hash, embedding_partition_class, #embedding_stale?, #embedding_type_name, #embedding_vector, #find_content_embedding, #find_similar, #generate_all_embeddings!, #generate_chunked_embeddings!, #generate_embedding!, #has_embedding?, #locale_for_embedding, #needs_chunking?, regenerate_all_embeddings, semantic_search

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#customer_satisfactionObject (readonly)



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

validates :customer_satisfaction, inclusion: { in: SATISFACTION_LEVELS, allow_nil: true }

Class Method Details

.analyzedActiveRecord::Relation<CallRecord>

A relation of CallRecords that are analyzed. Active Record Scope

Returns:

See Also:



252
253
254
# File 'app/models/call_record.rb', line 252

scope :analyzed, ->(value = true) {
  value.to_s == 'true' ? where.not(lemur_analyzed_at: nil) : where(lemur_analyzed_at: nil)
}

.by_recording_sourceActiveRecord::Relation<CallRecord>

A relation of CallRecords that are by recording source. Active Record Scope

Returns:

See Also:



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

scope :by_recording_source, ->(source) {
  if source == 'switchvox'
    where(recording_source: [nil, 'switchvox'])
  elsif source.present?
    where(recording_source: source)
  else
    all
  end
}

.company_main_number?(number) ⇒ Boolean

Check if number is a company main line (should not be matched to a party)

Returns:

  • (Boolean)


288
289
290
291
292
# File 'app/models/call_record.rb', line 288

def self.company_main_number?(number)
  return false if number.blank?

  COMPANY_MAIN_NUMBERS.include?(number)
end

.destination_filterActiveRecord::Relation<CallRecord>

A relation of CallRecords that are destination filter. Active Record Scope

Returns:

See Also:



196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'app/models/call_record.rb', line 196

scope :destination_filter, ->(*args) {
  ids = args.flatten.map(&:to_s).reject(&:blank?)
  include_null = ids.delete('null').present?
  integer_ids = ids.map(&:to_i).select(&:positive?)

  if include_null && integer_ids.any?
    where(destination_party_id: integer_ids).or(where(destination_party_id: nil))
  elsif include_null
    where(destination_party_id: nil)
  elsif integer_ids.any?
    where(destination_party_id: integer_ids)
  end
}

.distinct_key_topics(limit: 50) ⇒ Object

Get distinct key topics for filter dropdown



266
267
268
269
270
271
272
273
# File 'app/models/call_record.rb', line 266

def self.distinct_key_topics(limit: 50)
  where.not(key_topics: nil)
       .pluck(Arel.sql('jsonb_array_elements_text(key_topics)'))
       .tally
       .sort_by { |_, count| -count }
       .first(limit)
       .map(&:first)
end

.embeddable_content_typesObject

Define content types for embedding



515
516
517
# File 'app/models/call_record.rb', line 515

def self.embeddable_content_types
  [:primary]
end

.find_party(number) ⇒ Object



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'app/models/call_record.rb', line 294

def self.find_party(number)
  return unless number
  return if company_main_number?(number) # Don't match main company lines

  parties = Party
            .joins(:contact_points)
            .active
            .select('parties.id,parties.full_name')
            .order('parties.updated_at DESC')
            .limit(1)

  parties = if internal_call?(number)
              parties.where(type: 'Employee')
                     .where('(contact_points.detail = :number or contact_points.extension = :number)', number: number)
            else
              parties.where(type: %w[Customer Contact])
                     .where(ContactPoint[:detail].eq(number))
            end
  party_type, party_id, party_full_name = parties.pick(:type, :id, :full_name)
  return unless party_id

  {
    party_type: party_type,
    party_id: party_id,
    party_full_name: party_full_name
  }
end

.for_destination_employeesActiveRecord::Relation<CallRecord>

A relation of CallRecords that are for destination employees. Active Record Scope

Returns:

See Also:



149
# File 'app/models/call_record.rb', line 149

scope :for_destination_employees, ->(employee_ids) { where(destination_party_id: employee_ids) }

.for_destination_employees_or_unlinkedActiveRecord::Relation<CallRecord>

A relation of CallRecords that are for destination employees or unlinked. Active Record Scope

Returns:

See Also:



151
152
153
# File 'app/models/call_record.rb', line 151

scope :for_destination_employees_or_unlinked, ->(employee_ids) {
  where(destination_party_id: employee_ids).or(where(destination_party_id: nil))
}

.for_unlinked_extensionsActiveRecord::Relation<CallRecord>

A relation of CallRecords that are for unlinked extensions. Active Record Scope

Returns:

See Also:



150
# File 'app/models/call_record.rb', line 150

scope :for_unlinked_extensions, -> { where(destination_party_id: nil) }

.internal_call?(number) ⇒ Boolean

Returns:

  • (Boolean)


283
284
285
# File 'app/models/call_record.rb', line 283

def self.internal_call?(number)
  number && (number.length == 3 || number.match(/^\+184755024/).present?)
end

.mcp_searchable?Boolean

Privacy: Call records are NOT exposed via MCP (sensitive customer data)

Returns:

  • (Boolean)


520
521
522
# File 'app/models/call_record.rb', line 520

def self.mcp_searchable?
  false
end

.parse_phone_number(number) ⇒ Object



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

def self.parse_phone_number(number)
  return if number.blank?

  n = number.match(/\d+/).to_s.match(/^9?1?(\d{10})$/).try(:[], 1) || number
  PhoneNumber.parse_and_format(n)
end

.party_recordsActiveRecord::Relation<CallRecord>

A relation of CallRecords that are party records. Active Record Scope

Returns:

See Also:



180
181
182
183
# File 'app/models/call_record.rb', line 180

scope :party_records, ->(party_id) {
  pids = [party_id] + Contact.where(customer_id: party_id).pluck(:id)
  where('origin_party_id IN (?) or destination_party_id IN (?)', pids, pids)
}

.ransackable_associations(_auth_object = nil) ⇒ Object



225
226
227
# File 'app/models/call_record.rb', line 225

def self.ransackable_associations(_auth_object = nil)
  %w[origin_party destination_party upload]
end

.ransackable_attributes(_auth_object = nil) ⇒ Object



215
216
217
218
219
220
221
222
223
# File 'app/models/call_record.rb', line 215

def self.ransackable_attributes(_auth_object = nil)
  %w[
    id created_at origin_party_id destination_party_id origin_number destination_number
    origin_name destination_name duration_secs unrestricted unread
    transcription_state call_direction call_outcome customer_satisfaction
    agent_performance_score transcribed_at summarized_at lemur_analyzed_at
    recording_source audio_channels
  ]
end

.ransackable_scopes(_auth_object = nil) ⇒ Object

Ransack configuration for filtering



211
212
213
# File 'app/models/call_record.rb', line 211

def self.ransackable_scopes(_auth_object = nil)
  %i[search transcript_search with_key_topic analyzed with_action_items by_recording_source destination_filter]
end

.readActiveRecord::Relation<CallRecord>

A relation of CallRecords that are read. Active Record Scope

Returns:

See Also:



155
# File 'app/models/call_record.rb', line 155

scope :read, -> { where(unread: false) }

.recentActiveRecord::Relation<CallRecord>

A relation of CallRecords that are recent. Active Record Scope

Returns:

See Also:



147
# File 'app/models/call_record.rb', line 147

scope :recent, ->(days = 90) { where(call_records: { created_at: days.days.ago.. }) }

.transcription_eligibleActiveRecord::Relation<CallRecord>

A relation of CallRecords that are transcription eligible. Active Record Scope

Returns:

See Also:



134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'app/models/call_record.rb', line 134

scope :transcription_eligible, -> {
  joins(:upload)
    .where(transcription_state: %i[pending error])
    .where(
      'call_records.duration_secs >= :standard OR ' \
      '((call_records.call_outcome = :vm_outcome OR call_records.destination_number IN (:vm_exts)) ' \
      'AND call_records.duration_secs >= :vm_min)',
      standard: MIN_TRANSCRIPTION_DURATION,
      vm_outcome: call_outcomes[:voicemail],
      vm_exts: VOICEMAIL_EXTENSIONS,
      vm_min: MIN_TRANSCRIPTION_DURATION_VOICEMAIL
    )
}

.unreadActiveRecord::Relation<CallRecord>

A relation of CallRecords that are unread. Active Record Scope

Returns:

See Also:



154
# File 'app/models/call_record.rb', line 154

scope :unread, -> { where(unread: true) }

.voicemailsActiveRecord::Relation<CallRecord>

A relation of CallRecords that are voicemails. Active Record Scope

Returns:

See Also:



148
# File 'app/models/call_record.rb', line 148

scope :voicemails, -> { where(call_outcome: :voicemail) }

.with_action_itemsActiveRecord::Relation<CallRecord>

A relation of CallRecords that are with action items. Active Record Scope

Returns:

See Also:



257
258
259
260
261
262
263
# File 'app/models/call_record.rb', line 257

scope :with_action_items, ->(value = true) {
  if value.to_s == 'true'
    where('action_items IS NOT NULL AND jsonb_array_length(action_items) > 0')
  else
    where('action_items IS NULL OR jsonb_array_length(action_items) = 0')
  end
}

.with_key_topicActiveRecord::Relation<CallRecord>

A relation of CallRecords that are with key topic. Active Record Scope

Returns:

See Also:



230
231
232
# File 'app/models/call_record.rb', line 230

scope :with_key_topic, ->(topic) {
  where('key_topics @> ?', [topic].to_json)
}

.with_transcriptActiveRecord::Relation<CallRecord>

A relation of CallRecords that are with transcript. Active Record Scope

Returns:

See Also:



133
# File 'app/models/call_record.rb', line 133

scope :with_transcript, -> { where.not(transcript: nil).where.not(transcript: '') }

Instance Method Details

#action_items?Boolean

Check if there are action items

Returns:

  • (Boolean)


392
393
394
# File 'app/models/call_record.rb', line 392

def action_items?
  action_items.present?
end

#action_items_task_summaryObject

Single-line task text for prompts / embeddings. Handles JSON where task is a String,
an Array of strings, or other nested shapes (avoids calling String#truncate on an Array).



405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'app/models/call_record.rb', line 405

def action_items_task_summary
  return +'' if action_items.blank?

  Array(action_items).flat_map do |item|
    next [] unless item.is_a?(Hash)

    task_val = if item.key?('task')
                 item['task']
               elsif item.key?(:task)
                 item[:task]
               end
    extract_action_item_task_strings(task_val)
  end.compact_blank.join('; ')
end

#agent_improvementsObject

Get agent's improvement areas from analysis



426
427
428
# File 'app/models/call_record.rb', line 426

def agent_improvements
  structured_transcript_json&.dig('agent_performance', 'improvements') || []
end

#agent_strengthsObject

Get agent's strengths from analysis



421
422
423
# File 'app/models/call_record.rb', line 421

def agent_strengths
  structured_transcript_json&.dig('agent_performance', 'strengths') || []
end

#call_logObject

private



451
452
453
# File 'app/models/call_record.rb', line 451

def call_log
  call_log_event.try(:call_log)
end

#call_log_eventCallLogEvent



115
# File 'app/models/call_record.rb', line 115

has_one :call_log_event, dependent: :nullify

#call_record_embeddingsActiveRecord::Relation<ContentEmbedding::CallRecordEmbedding>

Direct association to partitioned embeddings table (more efficient than polymorphic)

Returns:

See Also:



118
119
120
121
122
# File 'app/models/call_record.rb', line 118

has_many :call_record_embeddings,
class_name: 'ContentEmbedding::CallRecordEmbedding',
foreign_key: :embeddable_id,
inverse_of: :embeddable,
dependent: false

#content_for_embedding(_content_type = :primary) ⇒ Object

Generate content for embedding - includes call metadata, transcript, and summary
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity



526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
# File 'app/models/call_record.rb', line 526

def content_for_embedding(_content_type = :primary)
  parts = []

  # Call metadata
  parts << "Phone call from #{origin_name.presence || origin_number} to #{destination_name.presence || destination_number}"
  parts << "Duration: #{duration_friendly}"
  parts << "Date: #{created_at.strftime('%B %d, %Y at %I:%M %p')}"
  parts << "Direction: #{call_direction}" if call_direction.present?
  parts << "Outcome: #{call_outcome}" if call_outcome.present? && call_outcome != 'unknown'
  parts << "Customer satisfaction: #{customer_satisfaction}" if customer_satisfaction.present?

  # Customer context if available
  if origin_party.is_a?(Customer)
    parts << "Customer: #{origin_party.full_name}"
  elsif destination_party.is_a?(Customer)
    parts << "Customer: #{destination_party.full_name}"
  end

  # Key topics (from LeMUR analysis)
  parts << "Topics discussed: #{key_topics.join(', ')}" if key_topics.present?

  # AI Summary
  parts << "\nSummary: #{ai_summary}" if ai_summary.present?

  # Action items (for searchability)
  summary = action_items_task_summary
  parts << "\nAction items: #{summary}" if summary.present?

  # Transcript (main searchable content - truncate for embedding efficiency)
  if transcript.present?
    # Limit transcript to ~8000 chars to keep embedding focused on key content
    parts << "\nTranscript:\n#{transcript.truncate(8000, omission: '...[transcript continues]')}"
  end

  parts.compact.join("\n")
end

#destination_partyParty

Returns:

See Also:

Validations:

  • Presence ({ if: -> { destination_party_id.present? } })


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

belongs_to :destination_party, class_name: 'Party', inverse_of: :destination_call_records, optional: true

#duration_friendlyObject



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

def duration_friendly
  require 'chronic_duration'
  ChronicDuration.output(duration_secs || 0)
end

#enqueue_crm_navbar_voicemail_refreshObject

Only enqueue when the change can affect the navbar badge — i.e. a
voicemail's unread state flipped, or a brand-new unread voicemail
arrived. Non-voicemail calls (sales, support, etc.) are noisy but not
badged.



330
331
332
333
334
335
336
337
338
339
340
# File 'app/models/call_record.rb', line 330

def enqueue_crm_navbar_voicemail_refresh
  return unless voicemail?

  if previously_new_record?
    return unless unread?
  elsif !saved_change_to_unread?
    return
  end

  CrmNavbarFanoutWorker.perform_async('voicemail')
end

#high_priority_action_itemsObject

Get high priority action items



397
398
399
400
401
# File 'app/models/call_record.rb', line 397

def high_priority_action_items
  return [] if action_items.blank?

  action_items.select { |item| item['priority'] == 'high' }
end

#lemur_analyzed?Boolean

Check if LeMUR analysis has been run

Returns:

  • (Boolean)


371
372
373
# File 'app/models/call_record.rb', line 371

def lemur_analyzed?
  lemur_analyzed_at.present?
end

#mark_as_read!Object



342
343
344
# File 'app/models/call_record.rb', line 342

def mark_as_read!
  update_column(:unread, false) if unread?
end

#mark_as_unread!Object



346
347
348
# File 'app/models/call_record.rb', line 346

def mark_as_unread!
  update_column(:unread, true) unless unread?
end

#match_destination_party(force: false) ⇒ Object

Method to match destination party based on switchvox acount id if specified or lookup on destination number



495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'app/models/call_record.rb', line 495

def match_destination_party(force: false)
  return :already_matched if destination_party_id && !force # already matched

  if .present? && (eps = EmployeePhoneStatus.find_by(switchvox_account_id: ))
    self.destination_name = eps.employee.full_name
    self.destination_party = eps.employee
  elsif destination_number && (match = self.class.find_party(destination_number))
    self.destination_name = match[:party_full_name]
    self.destination_party_id = match[:party_id]
  end
  logger&.info "Looking for destination party for call record #{id} for #{destination_number}, found #{destination_party_id || 'None'} #{destination_party.try(:full_name)}"
  destination_party_id
end

#match_origin_party(force: false) ⇒ Object

Method to match origin party based on switchvox acount id if specified or lookup on origin number
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity



480
481
482
483
484
485
486
487
488
489
490
491
492
# File 'app/models/call_record.rb', line 480

def match_origin_party(force: false)
  return :already_matched if origin_party_id && !force # already matched

  if .present? && (eps = EmployeePhoneStatus.find_by(switchvox_account_id: ))
    self.origin_name = eps.employee.full_name
    self.origin_party = eps.employee
  elsif origin_number && (match = self.class.find_party(origin_number))
    self.origin_name = match[:party_full_name]
    self.origin_party_id = match[:party_id]
  end
  logger&.info "Looking for originating party for call record #{id} for #{origin_number}, found #{origin_party_id || 'None'} #{origin_party.try(:full_name)}"
  origin_party_id
end

#match_parties(force: false) ⇒ Object

rubocop:enable Metrics/AbcSize, Naming/PredicateMethod



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

def match_parties(force: false)
  match_origin_party(force: force)
  match_destination_party(force: force)
end

#origin_partyParty

Returns:

See Also:

Validations:

  • Presence ({ if: -> { origin_party_id.present? } })


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

belongs_to :origin_party, class_name: 'Party', inverse_of: :origin_call_records, optional: true

#performance_score_displayObject

Get performance score display with color



431
432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'app/models/call_record.rb', line 431

def performance_score_display
  return nil if agent_performance_score.blank?

  score = agent_performance_score
  if score >= 80
    { score: score, class: 'text-success', label: 'Excellent' }
  elsif score >= 60
    { score: score, class: 'text-primary', label: 'Good' }
  elsif score >= 40
    { score: score, class: 'text-warning', label: 'Needs Improvement' }
  else
    { score: score, class: 'text-danger', label: 'Poor' }
  end
end

#satisfaction_badge_classObject

Get satisfaction CSS class for styling



381
382
383
384
385
386
387
388
389
# File 'app/models/call_record.rb', line 381

def satisfaction_badge_class
  case customer_satisfaction
  when 'very_satisfied', 'satisfied' then 'bg-success'
  when 'neutral' then 'bg-secondary'
  when 'frustrated', 'angry' then 'bg-danger'
  else
    'bg-light text-dark'
  end
end

#satisfaction_displayObject

Get formatted satisfaction with emoji



376
377
378
# File 'app/models/call_record.rb', line 376

def satisfaction_display
  SATISFACTION_DISPLAY[customer_satisfaction]
end

#to_sObject



446
447
448
# File 'app/models/call_record.rb', line 446

def to_s
  "CallRecord: #{id}, From #{origin_number} to #{destination_number} at #{created_at}"
end

#transcription_min_durationObject



350
351
352
# File 'app/models/call_record.rb', line 350

def transcription_min_duration
  voicemail? ? MIN_TRANSCRIPTION_DURATION_VOICEMAIL : MIN_TRANSCRIPTION_DURATION
end

#trim_numbersObject

rubocop:disable Metrics/AbcSize, Naming/PredicateMethod



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

def trim_numbers
  # North American Match 10 digits, strip 1 strip 9
  if (n = self.class.parse_phone_number(origin_number))
    self.origin_number = n
  end

  if (n = self.class.parse_phone_number(destination_number))
    self.destination_number = n
  end

  # Fix bad from account id
  self. = nil if .to_i.zero?
  self. = nil if .to_i.zero?
  true
end

#uploadUpload

Returns:

See Also:



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

has_one :upload, as: :resource, dependent: :destroy

#voicemail?Boolean

Returns:

  • (Boolean)


322
323
324
# File 'app/models/call_record.rb', line 322

def voicemail?
  call_outcome_voicemail? || destination_number.to_s.in?(VOICEMAIL_EXTENSIONS)
end