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

Constants included from Www::SeoHelper

Www::SeoHelper::AWARDS, Www::SeoHelper::CA_ADDRESS, Www::SeoHelper::CA_BUSINESS_HOURS, Www::SeoHelper::CA_CONTACT_POINT, Www::SeoHelper::CA_CURRENCIES, Www::SeoHelper::CA_DESCRIPTION, Www::SeoHelper::CA_FOUNDING_DATE, Www::SeoHelper::CA_GLOBAL_LOCATION_NUMBER, Www::SeoHelper::CA_LEGAL_NAME, Www::SeoHelper::CA_LOCAL_BUSINESS, Www::SeoHelper::CA_ONLINE_STORE, Www::SeoHelper::CA_RETURN_POLICY, Www::SeoHelper::CA_SALES_DEPARTMENT, Www::SeoHelper::CA_SERVICE_AREA, Www::SeoHelper::CA_URL, Www::SeoHelper::CA_VAT_ID, Www::SeoHelper::CA_WAREHOUSE_DEPARTMENT, Www::SeoHelper::CA_WAREHOUSE_HOURS, Www::SeoHelper::COMPANY_EMAIL, Www::SeoHelper::COMPANY_LOGO, Www::SeoHelper::COMPANY_NAME, Www::SeoHelper::COMPANY_SLOGAN, Www::SeoHelper::EXPERTISE, Www::SeoHelper::FAX_NUMBER, Www::SeoHelper::GS1_COMPANY_PREFIX, Www::SeoHelper::ISO6523_CODE, Www::SeoHelper::PAYMENT_METHODS, Www::SeoHelper::PHONE_NUMBER, Www::SeoHelper::PRIMARY_NAICS, Www::SeoHelper::REFUND_TYPE, Www::SeoHelper::RETURN_FEES, Www::SeoHelper::RETURN_METHOD, Www::SeoHelper::RETURN_POLICY_CATEGORY, Www::SeoHelper::SECONDARY_NAICS, Www::SeoHelper::SOCIAL_PROFILES, Www::SeoHelper::US_ADDRESS, Www::SeoHelper::US_BUSINESS_HOURS, Www::SeoHelper::US_CONTACT_POINT, Www::SeoHelper::US_CURRENCIES, Www::SeoHelper::US_DESCRIPTION, Www::SeoHelper::US_FOUNDING_DATE, Www::SeoHelper::US_GLOBAL_LOCATION_NUMBER, Www::SeoHelper::US_IMAGE, Www::SeoHelper::US_LEGAL_NAME, Www::SeoHelper::US_LOCAL_BUSINESS, Www::SeoHelper::US_ONLINE_STORE, Www::SeoHelper::US_RETURN_POLICY, Www::SeoHelper::US_SALES_DEPARTMENT, Www::SeoHelper::US_SERVICE_AREA, Www::SeoHelper::US_TAX_ID, Www::SeoHelper::US_URL, Www::SeoHelper::US_WAREHOUSE_DEPARTMENT, Www::SeoHelper::US_WAREHOUSE_HOURS

Constants included from IconHelper

IconHelper::CUSTOM_ICON_MAP, IconHelper::CUSTOM_SVG_DIR, IconHelper::DEFAULT_FAMILY

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

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?, #stamp_impersonation_context, #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, #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 ApplicationHelper

#better_number_to_currency, #check_force_logout, #check_or_cross, #check_or_times, #embedded_tab_frame_id, #error_messages, #general_disclaimer_on_product_installation_and_local_codes, #gridjs_from_html_table, #gridjs_table, #is_wy_ip, #line_break, #parent_layout, #pass_or_fail, #render_error_messages_list, #render_video_card, #resolved_auth_form_turbo_frame, #return_path_or, #safe_css_color, #set_return_path_if_present, #set_section_if_present, #tab_frame_id, #to_underscore, #track_page?, #turbo_section_wrapper, #turbo_tabs_request?, #url_on_same_domain_as_request, #widget_index_daily_focus_index_path, #working_hours?, #yes_or_no, #yes_or_no_highlighted, #yes_or_no_with_check_or_cross, #youtube_video

Methods included from UppyUploaderHelper

#file_uploader, #image_uploader, #large_file_uploader_s3, #lead_sketch_uploader, #rma_image_uploader, #rma_image_uploader_s3, #uppy_uploader, #video_uploader

Methods included from Www::ImagesHelper

#image_asset_tag, #image_asset_url

Methods included from Www::SeoHelper

#add_page_schema, #add_webpage_schema, #canada?, #company_social_links, #ensure_context_json, #json_ld_script_tag, #local_business_schema, #online_store_id, #online_store_schema, #page_main_entity, #page_main_entity_json, #render_auto_collection_page_schema, #render_collection_page_schema, #render_local_business_schema, #render_online_store_schema, #render_page_schemas, #render_page_video_schemas, #render_webpage_schema, #render_webpage_schema_with_collections, #usa?

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?

Methods included from IconHelper

#account_nav_icon, #fa_icon, #star_rating_html

Instance Method Details

#attach_callerObject

POST /call_records/:id/attach_caller



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'app/controllers/call_records_controller.rb', line 248

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.



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'app/controllers/call_records_controller.rb', line 216

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

#downloadObject



81
82
83
84
# File 'app/controllers/call_records_controller.rb', line 81

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



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

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

  if @call_record.generate_embedding!(:primary, force: true)
    flash[:info] = 'Embedding generated successfully'
  else
    flash[:warning] = 'Failed to generate embedding'
  end

  redirect_to @call_record
end

#indexObject



4
5
6
7
8
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
# File 'app/controllers/call_records_controller.rb', line 4

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_readObject



58
59
60
61
62
63
64
65
66
67
68
# File 'app/controllers/call_records_controller.rb', line 58

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



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'app/controllers/call_records_controller.rb', line 194

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.



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

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

#showObject



40
41
42
43
44
45
46
47
48
49
# File 'app/controllers/call_records_controller.rb', line 40

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



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

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



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

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_unreadObject



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

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



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'app/controllers/call_records_controller.rb', line 91

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

#updateObject



70
71
72
73
74
75
76
77
78
79
# File 'app/controllers/call_records_controller.rb', line 70

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