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 :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
- #customer_satisfaction ⇒ Object readonly
Has one collapse
Has many collapse
-
#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_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 rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity.
- #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 rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity.
-
#match_parties(force: false) ⇒ Object
rubocop:enable Metrics/AbcSize, 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
rubocop:disable Metrics/AbcSize, Naming/PredicateMethod.
- #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 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
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_source ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are by recording source. Active Record Scope
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)
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_filter ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are destination filter. Active Record Scope
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_types ⇒ Object
Define content types for embedding
515 516 517 |
# File 'app/models/call_record.rb', line 515 def self. [: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_employees ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are for destination employees. Active Record Scope
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_unlinked ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are for destination employees or unlinked. Active Record Scope
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_extensions ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are for unlinked extensions. Active Record Scope
150 |
# File 'app/models/call_record.rb', line 150 scope :for_unlinked_extensions, -> { where(destination_party_id: nil) } |
.internal_call?(number) ⇒ 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)
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_records ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are party records. Active Record Scope
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 |
.read ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are read. Active Record Scope
155 |
# File 'app/models/call_record.rb', line 155 scope :read, -> { where(unread: false) } |
.recent ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are recent. Active Record Scope
147 |
# File 'app/models/call_record.rb', line 147 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
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 ) } |
.unread ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are unread. Active Record Scope
154 |
# File 'app/models/call_record.rb', line 154 scope :unread, -> { where(unread: true) } |
.voicemails ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are voicemails. Active Record Scope
148 |
# File 'app/models/call_record.rb', line 148 scope :voicemails, -> { where(call_outcome: :voicemail) } |
.with_action_items ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are with action items. Active Record Scope
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_topic ⇒ ActiveRecord::Relation<CallRecord>
A relation of CallRecords that are with key topic. Active Record Scope
230 231 232 |
# File 'app/models/call_record.rb', line 230 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
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
392 393 394 |
# File 'app/models/call_record.rb', line 392 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).
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_improvements ⇒ Object
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_strengths ⇒ Object
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_log ⇒ Object
private
451 452 453 |
# File 'app/models/call_record.rb', line 451 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)
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_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? } })
125 |
# File 'app/models/call_record.rb', line 125 belongs_to :destination_party, class_name: 'Party', inverse_of: :destination_call_records, optional: true |
#duration_friendly ⇒ Object
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_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.
330 331 332 333 334 335 336 337 338 339 340 |
# File 'app/models/call_record.rb', line 330 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
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
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 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
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 switchvox_from_account_id.present? && (eps = EmployeePhoneStatus.find_by(switchvox_account_id: switchvox_from_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_party ⇒ Party
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_display ⇒ Object
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_class ⇒ Object
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_display ⇒ Object
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_s ⇒ Object
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_duration ⇒ Object
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_numbers ⇒ Object
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.switchvox_from_account_id = nil if switchvox_from_account_id.to_i.zero? self.switchvox_to_account_id = nil if switchvox_from_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
322 323 324 |
# File 'app/models/call_record.rb', line 322 def voicemail? call_outcome_voicemail? || destination_number.to_s.in?(VOICEMAIL_EXTENSIONS) end |