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 :enum
call_outcome :enum
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 :enum 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)

Constant Summary collapse

VOICEMAIL_EXTENSIONS =

Voicemail extensions.

%w[402].freeze
MIN_TRANSCRIPTION_DURATION =

Minimum transcription duration.

30
MIN_TRANSCRIPTION_DURATION_VOICEMAIL =

Minimum 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 =

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::MAX_CONTENT_LENGTH

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

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 Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

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:



263
264
265
# File 'app/models/call_record.rb', line 263

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:



252
253
254
255
256
257
258
259
260
# File 'app/models/call_record.rb', line 252

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)


299
300
301
302
303
# File 'app/models/call_record.rb', line 299

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:



209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'app/models/call_record.rb', line 209

scope :destination_filter, ->(*args) {
  ids = args.flatten.map(&:to_s).compact_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



277
278
279
280
281
282
283
284
# File 'app/models/call_record.rb', line 277

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



525
526
527
# File 'app/models/call_record.rb', line 525

def self.embeddable_content_types
  [:primary]
end

.find_party(number) ⇒ Object



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'app/models/call_record.rb', line 305

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:



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

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:



164
165
166
# File 'app/models/call_record.rb', line 164

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

.for_partyActiveRecord::Relation<CallRecord>

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

Returns:

See Also:



154
155
156
157
158
159
160
161
162
# File 'app/models/call_record.rb', line 154

scope :for_party, ->(party_or_ids) {
  # Guard against nil/blank input — without this, Rails turns the OR
  # branches into `IS NULL` predicates, matching every unlinked call
  # record (calls where the origin or destination party is unknown).
  ids = Array.wrap(party_or_ids).compact.uniq
  next none if ids.empty?

  where.any_of({ origin_party_id: ids }, { destination_party_id: ids })
}

.for_unlinked_extensionsActiveRecord::Relation<CallRecord>

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

Returns:

See Also:



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

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

.internal_call?(number) ⇒ Boolean

Returns:

  • (Boolean)


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

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)


530
531
532
# File 'app/models/call_record.rb', line 530

def self.mcp_searchable?
  false
end

.parse_phone_number(number) ⇒ Object



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

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:



193
194
195
# File 'app/models/call_record.rb', line 193

scope :party_records, ->(party_id) {
  for_party([party_id] + Contact.where(customer_id: party_id).ids)
}

.ransackable_associations(_auth_object = nil) ⇒ Object



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

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

.ransackable_attributes(_auth_object = nil) ⇒ Object



228
229
230
231
232
233
234
235
236
# File 'app/models/call_record.rb', line 228

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



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

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:



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

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

.recentActiveRecord::Relation<CallRecord>

A relation of CallRecords that are recent. Active Record Scope

Returns:

See Also:



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

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:



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

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:



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

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

.voicemailsActiveRecord::Relation<CallRecord>

A relation of CallRecords that are voicemails. Active Record Scope

Returns:

See Also:



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

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:



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

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:



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

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:



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

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

Instance Method Details

#action_items?Boolean

Check if there are action items

Returns:

  • (Boolean)


402
403
404
# File 'app/models/call_record.rb', line 402

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



415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'app/models/call_record.rb', line 415

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

#activitiesActiveRecord::Relation<Activity>

Returns:

See Also:



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

has_many :activities, dependent: :nullify, inverse_of: :call_record

#agent_improvementsObject

Get agent's improvement areas from analysis



436
437
438
# File 'app/models/call_record.rb', line 436

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

#agent_strengthsObject

Get agent's strengths from analysis



431
432
433
# File 'app/models/call_record.rb', line 431

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

#call_logObject

private



461
462
463
# File 'app/models/call_record.rb', line 461

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:



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

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



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
562
563
564
565
566
567
568
569
570
# File 'app/models/call_record.rb', line 535

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? } })


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

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

#duration_friendlyObject



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

def duration_friendly
  Heatwave::Duration.humanize(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.



341
342
343
344
345
346
347
348
349
350
351
# File 'app/models/call_record.rb', line 341

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



407
408
409
410
411
# File 'app/models/call_record.rb', line 407

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)


381
382
383
# File 'app/models/call_record.rb', line 381

def lemur_analyzed?
  lemur_analyzed_at.present?
end

#mark_as_read!Object



353
354
355
# File 'app/models/call_record.rb', line 353

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

#mark_as_unread!Object



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

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



507
508
509
510
511
512
513
514
515
516
517
518
519
# File 'app/models/call_record.rb', line 507

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



492
493
494
495
496
497
498
499
500
501
502
503
504
# File 'app/models/call_record.rb', line 492

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 if origin_name.blank?
    self.origin_party = eps.employee
  elsif origin_number && (match = self.class.find_party(origin_number))
    self.origin_name = match[:party_full_name] if origin_name.blank?
    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 Naming/PredicateMethod



486
487
488
489
# File 'app/models/call_record.rb', line 486

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? } })


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

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

#performance_score_displayObject

Get performance score display with color



441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'app/models/call_record.rb', line 441

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



391
392
393
394
395
396
397
398
399
# File 'app/models/call_record.rb', line 391

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



386
387
388
# File 'app/models/call_record.rb', line 386

def satisfaction_display
  SATISFACTION_DISPLAY[customer_satisfaction]
end

#to_sObject



456
457
458
# File 'app/models/call_record.rb', line 456

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

#transcription_min_durationObject



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

def transcription_min_duration
  voicemail? ? MIN_TRANSCRIPTION_DURATION_VOICEMAIL : MIN_TRANSCRIPTION_DURATION
end

#trim_numbersObject



465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'app/models/call_record.rb', line 465

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

  # Drop blank/zero Switchvox account ids. Each side is checked against ITSELF:
  # the `to` line used to test `from` (typo), which nulled the agent's
  # to_account_id on every inbound call (external caller => blank from_account_id)
  # — ~29% of recordings — breaking account-id-based matching and agent
  # attribution for those calls.
  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)


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

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