Class: CallRecordsController

Inherits:
CrmController show all
Defined in:
app/controllers/call_records_controller.rb

Overview

Controller: call records.

Constant Summary

Constants included from Controllers::ReferenceFindable

Controllers::ReferenceFindable::ID_EMBEDDED_PATTERNS

Constants included from Controllers::AnalyticsEvents

Controllers::AnalyticsEvents::MAX_QUEUED_EVENTS, Controllers::AnalyticsEvents::SESSION_KEY

Constants included from Controllers::ErrorRendering

Controllers::ErrorRendering::NON_CONTENT_PATH_PREFIXES

Instance Method Summary collapse

Methods inherited from CrmController

#access_denied, #context_id, #context_object, #crm_home_path, #current_ability, #default_url_options, #download_temp, #get_tempfile_path_for_download, #init_status_job_collector, #initialize_crm_lazy_chunks, #persist_enqueued_status_jobs, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download, #sync_admin_presence_cookie, #touch_employee_last_seen

Methods inherited from ApplicationController

#account_impersonated?, #add_to_flash, #after_sign_in_path_for, #bypass_forgery_protection?, #chat_enabled?, #cloudflare_cleared?, #default_catalog, #default_url_options, #enable_turbo_frames, #find_publication, #fix_invalid_accept_header, #init_js_utils, #is_globals_call?, #layout_by_resource, #locale_store, #redirect_to, #require_employee_for_crm, #set_base_host, #set_real_ip, #set_report_errors_for, #should_render_layout?, #skip_layout_for_turbo_frame?, #stamp_impersonation_context, #tab_frame_breakout_request?, #warmlyyours_canada_ip?, #warmlyyours_ip?, #y

Methods included from Controllers::ReturnPathHandling

#check_for_return_path, #redirect_to_return_path_or_default

Methods included from Controllers::AnalyticsEvents

#consume_queued_analytics_events, #registration_lead_type, #track_event

Methods included from Controllers::DeviceDetection

#device_detector, #is_ie?

Methods included from Controllers::SubdomainDetection

#is_crm_request?, #is_www_request?, #json_request?

Methods included from Controllers::TurboSafeRedirect

#redirect_to

Methods included from Controllers::TrackingDetection

#bot_request?, #gdpr_country?, #gdpr_country_data, #prevent_bots, #set_tracking_cookie, #track_visitor?

Methods included from Controllers::AcceleratedFileSending

#send_file_accelerated, #send_upload_accelerated

Methods included from Controllers::ErrorRendering

#excp_string, #mail_to_for_error_reporting, #render_400, #render_404, #render_406, #render_410, #render_500, #render_invalid_authenticity_token, #render_ip_spoof_error, #render_unpermitted_parameters, #safe_referer_or_fallback

Methods included from Controllers::TurnstileVerification

#load_turnstile_script_tag, #turnstile_lazy_widget, #turnstile_script_tag, #turnstile_widget, #validate_turnstile!

Methods included from Controllers::CloudflareCaching

edge_cached, #edge_cached_action?, #reset_cloudflare_cache, #set_cloudflare_cache, #skip_edge_cache!, #skip_session

Methods included from Controllers::Webpackable

#preload_webpack_fonts, #webpack_css_include, #webpack_css_url, #webpack_js_include, #wpd_is_running?

Methods included from Controllers::Localizable

#cloudflare_country_locale, #determine_request_locale, #geocoder_locale, #guest_user_locale_check, #locale_optional_www_auth_path?, #param_locale, #set_locale, #set_request_locale, #skip_localization?, #warmlyyours_ip_locale

Methods included from Controllers::Authenticable

#access_denied, #authenticate_account, #authenticate_account!, #authenticate_account_from_login_token!, #check_is_a_manager, #check_is_a_sales_manager, #check_is_an_admin, #check_is_an_employee, #check_party, #clear_mismatched_guest_user, #create_guest_user, #credentials?, #current_or_guest_user, #current_or_guest_user_id_read_only, #current_user, #devise_mapping, #fully_logged_in?, #generate_bot_id, #guest_user, #identifiable?, #init_current_user, #initialize_guest, #load_context_user, #logging_in, #resource, #resource_name, #restrict_access_for_non_employees, #scrubbed_request_path, #user_object, #warn_on_session_guest_id_leak

Methods included from UrlsHelper

#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?

Instance Method Details

#attach_callerObject

POST /call_records/:id/attach_caller



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'app/controllers/call_records_controller.rb', line 273

def attach_caller
  @call_record = CallRecord.find(params[:id])
  authorize!(:update, @call_record)

  new_party_id = params.dig(:attach, :party_id).presence
  unless @call_record.voicemail? && @call_record.origin_party.nil? && new_party_id
    flash[:error] = 'Caller is already matched, record is not a voicemail, or no party selected'
    return redirect_to call_record_path(@call_record)
  end

  party = Party.find_by(id: new_party_id)
  unless party
    flash[:error] = 'Party not found'
    return redirect_to call_record_path(@call_record)
  end

  @call_record.origin_party = party
  @call_record.origin_name = party.full_name
  @call_record.save!

  party.contact_points.create!(category: ContactPoint::PHONE, detail: @call_record.origin_number) if @call_record.origin_number.present? && party.contact_points.where(detail: @call_record.origin_number).none?

  flash[:info] = "#{party.full_name} linked as caller"
  redirect_to call_record_path(@call_record)
end

#create_customerObject

POST /call_records/:id/create_customer
Creates a lead Customer for an unmatched voicemail caller and a completed
follow-up Activity, then returns to the call record.



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'app/controllers/call_records_controller.rb', line 241

def create_customer
  @call_record = CallRecord.find(params[:id])
  authorize!(:update, @call_record)

  unless @call_record.voicemail? && @call_record.origin_party.nil?
    flash[:error] = 'Caller is already matched or record is not a voicemail'
    return redirect_to call_record_path(@call_record)
  end

  # A matching party may have appeared since the voicemail arrived.
  @call_record.match_origin_party(force: true)
  if @call_record.origin_party
    @call_record.save!
    flash[:info] = "An existing matching party was discovered: #{@call_record.origin_party.full_name}"
    return redirect_to call_record_path(@call_record)
  end

  followup = CallRecordProcessing::VoicemailFollowup.new(@call_record)
  customer = followup.create_customer(person_name: params.dig(:new_customer, :person_name))
  if customer&.persisted?
    if followup.create_activity(completed: true)
      flash[:info] = "Created customer #{customer.full_name} and logged the voicemail activity."
    else
      flash[:warning] = "Created customer #{customer.full_name}, but the voicemail activity could not be logged."
    end
  else
    flash[:error] = 'Could not create a customer for this voicemail.'
  end
  redirect_to call_record_path(@call_record)
end

#downloadvoid

This method returns an undefined value.

GET /call_records/:id/download — downloads the call's audio recording.



102
103
104
105
# File 'app/controllers/call_records_controller.rb', line 102

def download
  cr = CallRecord.find(params[:id])
  send_upload_accelerated(cr.upload, file_name: "call_record_#{cr.id}_#{cr.origin_number}_#{cr.destination_number}.oga", download: true)
end

#generate_embeddingObject

POST /call_records/:id/generate_embedding
Creates semantic embedding for search



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'app/controllers/call_records_controller.rb', line 177

def generate_embedding
  @call_record = CallRecord.find(params[:id])
  authorize!(:update, @call_record)

  if @call_record.transcript.blank?
    flash[:warning] = 'No transcript available. Run transcription first.'
    return redirect_to @call_record
  end

  begin
    if @call_record.generate_embedding!(:primary, force: true)
      flash[:info] = 'Embedding generated successfully'
    else
      flash[:warning] = 'Failed to generate embedding'
    end
  rescue Embedding::Gemini::Error => e
    flash[:warning] = "Failed to generate embedding: #{e.message}"
  end

  redirect_to @call_record
end

#indexvoid

This method returns an undefined value.

GET /call_records — filterable, paginated call/voicemail list, scoped to
the current user unless a party filter, manager role, or manage
permission applies.



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'app/controllers/call_records_controller.rb', line 9

def index
  authorize!(:read, CallRecord)

  # Find party from various route params
  party_id = params[:party_id] || params[:customer_id] || params[:contact_id] || params[:employee_id]
  @party = Party.find_by(id: party_id) if party_id.present?

  # Initialize ransack params
  params[:q] ||= {}

  # Sync party filter into ransack params so the form displays correctly.
  # Skip the default current_user scope when:
  #   - destination_filter is present (voicemail watchlist already scopes)
  #   - user is a manager (can view "All Unread Voicemails")
  #   - user can :manage CallRecord (admin)
  if @party
    params[:q][:origin_party_id_or_destination_party_id_eq] ||= @party.id
  elsif params[:q][:origin_party_id_or_destination_party_id_eq].blank? &&
        params[:q][:destination_filter].blank? &&
        !&.is_manager? &&
        !can?(:manage, CallRecord)
    params[:q][:origin_party_id_or_destination_party_id_eq] = current_user.id
  end

  pre_process_filters
  @q = CallRecord.ransack(params[:q])

  # Default to newest first, but preserve pg_search's relevance ranking when using transcript_search
  @q.sorts = 'id desc' if @q.sorts.blank? && params.dig(:q, :transcript_search).blank?

  @pagy, @call_records = pagy(@q.result.includes(:origin_party, :destination_party, :upload, :call_record_embeddings), limit: 100)

  # Load filter options for AI analysis fields
  load_filter_options
end

#mark_as_readvoid

This method returns an undefined value.

PATCH /call_records/:id/mark_as_read — marks a voicemail read.



73
74
75
76
77
78
79
80
81
82
83
# File 'app/controllers/call_records_controller.rb', line 73

def mark_as_read
  @call_record = CallRecord.find(params[:id])
  authorize!(:update, @call_record)
  @call_record.mark_as_read!
  @return_path = params[:return_path].presence || call_records_path

  respond_to do |format|
    format.turbo_stream
    format.html { redirect_to @return_path }
  end
end

#match_callerObject

POST /call_records/:id/match_caller



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'app/controllers/call_records_controller.rb', line 219

def match_caller
  @call_record = CallRecord.find(params[:id])
  authorize!(:update, @call_record)

  unless @call_record.voicemail? && @call_record.origin_party.nil?
    flash[:error] = 'Caller is already matched or record is not a voicemail'
    return redirect_to call_record_path(@call_record)
  end

  @call_record.match_origin_party(force: true)
  if @call_record.origin_party
    @call_record.save!
    flash[:info] = "Found matching party: #{@call_record.origin_party.full_name}"
  else
    flash[:warning] = 'No matching party found'
  end
  redirect_to call_record_path(@call_record)
end

#rerun_enrichmentObject

POST /call_records/:id/rerun_enrichment
Re-runs call analysis so voicemail caller identity can update the placeholder Customer/Contact records.



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'app/controllers/call_records_controller.rb', line 154

def rerun_enrichment
  @call_record = CallRecord.find(params[:id])
  authorize!(:update, @call_record)

  unless @call_record.voicemail?
    flash[:warning] = 'Caller enrichment is only available for voicemails.'
    return redirect_to @call_record
  end

  if @call_record.assemblyai_transcript_id.blank?
    flash[:warning] = 'No completed transcript available. Run transcription first.'
    return redirect_to @call_record
  end

  CallRecordProcessing::TranscriptionService.new(@call_record).run_call_analysis(@call_record.assemblyai_transcript_id)
  Rails.logger.info "[CallRecordsController#rerun_enrichment] Re-ran enrichment for CallRecord #{@call_record.id}"

  flash[:info] = 'Caller enrichment completed. Customer/contact details were updated if caller details were found.'
  redirect_to @call_record
end

#showvoid

This method returns an undefined value.

GET /call_records/:id — a single call record, marking a voicemail read
when appropriate.



49
50
51
52
53
54
55
56
57
58
# File 'app/controllers/call_records_controller.rb', line 49

def show
  @call_record = CallRecord.find(params[:id])
  authorize!(:read, @call_record)

  return unless @call_record.unread? && (params[:mark_as_read].to_b || @call_record.destination_party_id == current_user.id)

  @call_record.mark_as_read!
  notice = "Voicemail marked as read. #{view_context.link_to('Undo', toggle_unread_call_record_path(@call_record, return_path: request.path), data: { turbo_method: :post })}"
  flash.now[:notice] = notice
end

#summarizeObject

POST /call_records/:id/summarize
Generates AI summary from the transcript



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'app/controllers/call_records_controller.rb', line 136

def summarize
  @call_record = CallRecord.find(params[:id])
  authorize!(:update, @call_record)

  if @call_record.transcript.blank?
    flash[:warning] = 'No transcript available. Run transcription first.'
    return redirect_to @call_record
  end

  jid = CallRecordSummaryWorker.perform_async(call_record_id: @call_record.id)
  Rails.logger.info "[CallRecordsController#summarize] Queued summary job #{jid} for CallRecord #{@call_record.id}"

  flash[:info] = 'AI summary generation queued.'
  redirect_to @call_record
end

#swap_speakersObject

POST /call_records/:id/swap_speakers
Swaps speaker labels in the transcript (Agent <-> Customer)
Used when AI speaker detection assigned speakers incorrectly



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'app/controllers/call_records_controller.rb', line 202

def swap_speakers
  @call_record = CallRecord.find(params[:id])
  authorize!(:update, @call_record)

  if @call_record.structured_transcript_json.blank?
    flash[:warning] = 'No transcript available to swap speakers.'
    return redirect_to @call_record
  end

  # Swap speakers in the structured transcript
  swap_speaker_labels!

  flash[:info] = 'Speaker labels swapped successfully.'
  redirect_to @call_record
end

#toggle_unreadvoid

This method returns an undefined value.

PATCH /call_records/:id/toggle_unread — flips a voicemail's unread flag.



63
64
65
66
67
68
# File 'app/controllers/call_records_controller.rb', line 63

def toggle_unread
  @call_record = CallRecord.find(params[:id])
  authorize!(:update, @call_record)
  @call_record.toggle!(:unread) if @call_record.voicemail?
  redirect_to url_from(params[:return_path]) || @call_record
end

#transcribeObject

POST /call_records/:id/transcribe
Queues transcription via AssemblyAI with speaker diarization



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'app/controllers/call_records_controller.rb', line 112

def transcribe
  @call_record = CallRecord.find(params[:id])
  authorize!(:update, @call_record)

  if @call_record.upload.blank?
    flash[:warning] = 'No audio file attached to this call record'
    return redirect_to @call_record
  end

  if @call_record.duration_secs.to_i < @call_record.transcription_min_duration
    min = @call_record.transcription_min_duration
    flash[:warning] = "Call is too short to transcribe (minimum #{min} seconds)"
    return redirect_to @call_record
  end

  jid = CallRecordTranscriptionWorker.perform_async(call_record_id: @call_record.id, force: true)
  Rails.logger.info "[CallRecordsController#transcribe] Queued transcription job #{jid} for CallRecord #{@call_record.id}"

  flash[:info] = 'Transcription queued. This may take a few minutes.'
  redirect_to @call_record
end

#updatevoid

This method returns an undefined value.

PATCH/PUT /call_records/:id — updates a call record.



88
89
90
91
92
93
94
95
96
97
# File 'app/controllers/call_records_controller.rb', line 88

def update
  @call_record = CallRecord.find(params[:id])
  authorize!(:update, @call_record)
  if @call_record.update(params[:call_record])
    redirect_to @call_record
  else
    flash.now[:error] = @call_record.errors_to_s.presence || 'Could not update call record.'
    render :show, status: :unprocessable_content
  end
end