Class: CallRecord
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- CallRecord
- 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
- #customer_satisfaction ⇒ Object readonly
Has one collapse
Has many collapse
- #activities ⇒ ActiveRecord::Relation<Activity>
-
#call_record_embeddings ⇒ ActiveRecord::Relation<ContentEmbedding::CallRecordEmbedding>
Direct association to partitioned embeddings table (more efficient than polymorphic).
Methods included from Models::Embeddable
Belongs to collapse
Methods included from Models::Auditable
Class Method Summary collapse
-
.analyzed ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are analyzed.
-
.by_recording_source ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are by recording source.
-
.company_main_number?(number) ⇒ Boolean
Check if number is a company main line (should not be matched to a party).
-
.destination_filter ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are destination filter.
-
.distinct_key_topics(limit: 50) ⇒ Object
Get distinct key topics for filter dropdown.
-
.embeddable_content_types ⇒ Object
Define content types for embedding.
- .find_party(number) ⇒ Object
-
.for_destination_employees ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are for destination employees.
-
.for_destination_employees_or_unlinked ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are for destination employees or unlinked.
-
.for_party ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are for party.
-
.for_unlinked_extensions ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are for unlinked extensions.
- .internal_call?(number) ⇒ Boolean
-
.mcp_searchable? ⇒ Boolean
Privacy: Call records are NOT exposed via MCP (sensitive customer data).
- .parse_phone_number(number) ⇒ Object
-
.party_records ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are party records.
- .ransackable_associations(_auth_object = nil) ⇒ Object
- .ransackable_attributes(_auth_object = nil) ⇒ Object
-
.ransackable_scopes(_auth_object = nil) ⇒ Object
Ransack configuration for filtering.
-
.read ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are read.
-
.recent ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are recent.
-
.transcription_eligible ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are transcription eligible.
-
.unread ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are unread.
-
.voicemails ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are voicemails.
-
.with_action_items ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are with action items.
-
.with_key_topic ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are with key topic.
-
.with_transcript ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are with transcript.
Instance Method Summary collapse
-
#action_items? ⇒ Boolean
Check if there are action items.
-
#action_items_task_summary ⇒ Object
Single-line task text for prompts / embeddings.
-
#agent_improvements ⇒ Object
Get agent's improvement areas from analysis.
-
#agent_strengths ⇒ Object
Get agent's strengths from analysis.
-
#call_log ⇒ Object
private.
-
#content_for_embedding(_content_type = :primary) ⇒ Object
Generate content for embedding - includes call metadata, transcript, and summary.
- #duration_friendly ⇒ Object
-
#enqueue_crm_navbar_voicemail_refresh ⇒ Object
Only enqueue when the change can affect the navbar badge — i.e.
-
#high_priority_action_items ⇒ Object
Get high priority action items.
-
#lemur_analyzed? ⇒ Boolean
Check if LeMUR analysis has been run.
- #mark_as_read! ⇒ Object
- #mark_as_unread! ⇒ Object
-
#match_destination_party(force: false) ⇒ Object
Method to match destination party based on switchvox acount id if specified or lookup on destination number.
-
#match_origin_party(force: false) ⇒ Object
Method to match origin party based on switchvox acount id if specified or lookup on origin number.
-
#match_parties(force: false) ⇒ Object
rubocop:enable Naming/PredicateMethod.
-
#performance_score_display ⇒ Object
Get performance score display with color.
-
#satisfaction_badge_class ⇒ Object
Get satisfaction CSS class for styling.
-
#satisfaction_display ⇒ Object
Get formatted satisfaction with emoji.
- #to_s ⇒ Object
- #transcription_min_duration ⇒ Object
- #trim_numbers ⇒ Object
- #voicemail? ⇒ Boolean
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
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#customer_satisfaction ⇒ Object (readonly)
112 |
# File 'app/models/call_record.rb', line 112 validates :customer_satisfaction, inclusion: { in: SATISFACTION_LEVELS, allow_nil: true } |
Class Method Details
.analyzed ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are analyzed. Active Record Scope
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_source ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are by recording source. Active Record Scope
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)
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_filter ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are destination filter. Active Record Scope
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_types ⇒ Object
Define content types for embedding
525 526 527 |
# File 'app/models/call_record.rb', line 525 def self. [: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_employees ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are for destination employees. Active Record Scope
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_unlinked ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are for destination employees or unlinked. Active Record Scope
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_party ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are for party. Active Record Scope
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_extensions ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are for unlinked extensions. Active Record Scope
163 |
# File 'app/models/call_record.rb', line 163 scope :for_unlinked_extensions, -> { where(destination_party_id: nil) } |
.internal_call?(number) ⇒ 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)
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_records ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are party records. Active Record Scope
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 |
.read ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are read. Active Record Scope
168 |
# File 'app/models/call_record.rb', line 168 scope :read, -> { where(unread: false) } |
.recent ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are recent. Active Record Scope
148 |
# File 'app/models/call_record.rb', line 148 scope :recent, ->(days = 90) { where(call_records: { created_at: days.days.ago.. }) } |
.transcription_eligible ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are transcription eligible. Active Record Scope
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 ) } |
.unread ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are unread. Active Record Scope
167 |
# File 'app/models/call_record.rb', line 167 scope :unread, -> { where(unread: true) } |
.voicemails ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are voicemails. Active Record Scope
149 |
# File 'app/models/call_record.rb', line 149 scope :voicemails, -> { where(call_outcome: :voicemail) } |
.with_action_items ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are with action items. Active Record Scope
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_topic ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are with key topic. Active Record Scope
243 |
# File 'app/models/call_record.rb', line 243 scope :with_key_topic, ->(topic) { where('key_topics @> ?', [topic].to_json) } |
.with_transcript ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are with transcript. Active Record Scope
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
402 403 404 |
# File 'app/models/call_record.rb', line 402 def action_items? action_items.present? end |
#action_items_task_summary ⇒ Object
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 |
#activities ⇒ ActiveRecord::Relation<Activity>
116 |
# File 'app/models/call_record.rb', line 116 has_many :activities, dependent: :nullify, inverse_of: :call_record |
#agent_improvements ⇒ Object
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_strengths ⇒ Object
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_log ⇒ Object
private
461 462 463 |
# File 'app/models/call_record.rb', line 461 def call_log call_log_event.try(:call_log) end |
#call_log_event ⇒ CallLogEvent
115 |
# File 'app/models/call_record.rb', line 115 has_one :call_log_event, dependent: :nullify |
#call_record_embeddings ⇒ ActiveRecord::Relation<ContentEmbedding::CallRecordEmbedding>
Direct association to partitioned embeddings table (more efficient than polymorphic)
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_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_party ⇒ Party
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_friendly ⇒ Object
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_refresh ⇒ Object
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 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_items ⇒ Object
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
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 switchvox_to_account_id.present? && (eps = EmployeePhoneStatus.find_by(switchvox_account_id: switchvox_to_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 switchvox_from_account_id.present? && (eps = EmployeePhoneStatus.find_by(switchvox_account_id: switchvox_from_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_party ⇒ Party
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_display ⇒ Object
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_class ⇒ Object
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_display ⇒ Object
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_s ⇒ Object
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_duration ⇒ Object
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_numbers ⇒ Object
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.switchvox_from_account_id = nil if switchvox_from_account_id.to_i.zero? self.switchvox_to_account_id = nil if switchvox_to_account_id.to_i.zero? true end |
#upload ⇒ Upload
114 |
# File 'app/models/call_record.rb', line 114 has_one :upload, as: :resource, dependent: :destroy |
#voicemail? ⇒ 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 |