Class: Crm::SmsMessagesController

Inherits:
CrmController show all
Includes:
Controllers::Attachable, Controllers::SmsTab
Defined in:
app/controllers/crm/sms_messages_controller.rb

Overview

CRUD, threading, and moderation actions for SmsMessage records — the
standalone /sms_messages index plus the per-party SMS tab
(Controllers::SmsTab).

Constant Summary

Constants included from Controllers::Attachable

Controllers::Attachable::PUBLICATIONS_PER_PAGE

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 included from Controllers::Attachable

#attach, #publication_modal, #remove_attachment, #search_library

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_to_partyvoid

This method returns an undefined value.

Attaches an unmatched inbound message's sender to the party chosen in
params[:attach][:party_id], and registers the number as a contact
point.



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'app/controllers/crm/sms_messages_controller.rb', line 281

def attach_to_party
  @sms_message = SmsMessage.find(params[:id])
  if @sms_message.inbound? && @sms_message.sender_party.nil? && (new_party_id = params.dig(:attach, :party_id).presence)
    @sms_message.sender_party_id = new_party_id
    @sms_message.save
    @sms_message.reload
    # Add this number to the party
    @sms_message.sender_party.contact_points.create!(category: ContactPoint::CELL, detail: @sms_message.sender)
    SmsMessage.match_unknown(numbers: [@sms_message.sender], party: @sms_message.sender_party)
    flash[:info] = "#{@sms_message.sender_party.full_name} attached to message"
  else
    flash[:error] = 'Message is not inbound or already has a party'
  end
  redirect_to_return_path_or_default sms_message_path(@sms_message)
end

#block_sendervoid

This method returns an undefined value.

Blocks the message's sender number (inbound messages only).



300
301
302
303
304
305
306
307
308
# File 'app/controllers/crm/sms_messages_controller.rb', line 300

def block_sender
  @sms_message = SmsMessage.find(params[:id])
  if @sms_message.inbound?
    @sms_message.block_sender
  else
    flash[:error] = 'Can only block inbounds'
  end
  redirect_to_return_path_or_default sms_message_path(@sms_message)
end

#bulk_destroyObject

DELETE /sms_messages/bulk_destroy

Mass delete from the index checkboxes. Selection is bounded by the page
size, but cap defensively anyway; each record is authorized individually
so a crafted id list can't delete anything the per-row button couldn't.



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'app/controllers/crm/sms_messages_controller.rb', line 210

def bulk_destroy
  authorize!(:destroy, SmsMessage)
  ids = Array(params[:sms_message_ids]).map(&:to_i).uniq.first(500)
  destroyed = 0
  failed = []
  SmsMessage.where(id: ids).find_each do |sms_message|
    if can?(:destroy, sms_message) && sms_message.destroy
      destroyed += 1
    else
      failed << sms_message.id
    end
  end
  flash[:info] = "#{destroyed} SMS #{'message'.pluralize(destroyed)} deleted" if destroyed.positive?
  flash[:error] = "Could not delete SMS #{'message'.pluralize(failed.size)}: #{failed.join(', ')}" if failed.any?
  flash[:warning] = 'No messages selected' if ids.empty?
  redirect_to_return_path_or_default sms_messages_path
end

#createvoid

This method returns an undefined value.

Creates and sends an outbound message.



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'app/controllers/crm/sms_messages_controller.rb', line 117

def create
  # Extract our upload ids
  @sms_message = SmsMessage.new(params[:sms_message])
  authorize!(:create, @sms_message)
  @sms_message.direction = :outbound
  if @sms_message.valid? && @sms_message.send_message
    # Sending a reply means the operator read the thread they're replying to.
    @sms_message.mark_conversation_read!
    # Inline replies (thread / tab reply box) return to where they were sent
    # from; the standalone composer lands on the sent message's thread.
    if params[:inline_reply].present?
      redirect_to_return_path_or_default sms_message_path(@sms_message)
    else
      redirect_to sms_message_path(@sms_message, return_path: params[:return_path])
    end
  else
    flash.now[:error] = "Unable to create outbound sms message. #{@sms_message.errors_to_s}"
    load_destination_numbers
    load_thread_preview
    render :new, status: :unprocessable_content
  end
end

#create_customervoid

This method returns an undefined value.

Creates a new Customer from an unmatched inbound message's sender
number, or attaches an existing matching party if one is found.



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'app/controllers/crm/sms_messages_controller.rb', line 232

def create_customer
  @sms_message = SmsMessage.find(params[:id])
  if @sms_message.inbound? && @sms_message.sender_party.nil?
    # Try first a match just in case
    @sms_message.match_inbound_sender
    if @sms_message.sender_party
      flash[:info] = "An existing matching party was discovered: #{@sms_message.sender_party.full_name}"
      @sms_message.save!
      redirect_to_return_path_or_default sms_message_path(@sms_message)
    else
      new_customer = {
        person_name: params.dig(:new_customer, :person_name),
        cell: @sms_message.sender,
        source_id: Source.find_by(referral_code: 'SMS'),
        country_iso: @sms_message.from_country_iso
      }
      redirect_to new_customer_identify_path(new_customer: new_customer)
    end
  else
    flash[:error] = 'Cannot create customer from message, message must be inbound and not already have a party'
    redirect_to_return_path_or_default sms_message_path(@sms_message)
  end
end

#destroyvoid

This method returns an undefined value.

Deletes a message.



183
184
185
186
187
188
189
190
191
192
193
# File 'app/controllers/crm/sms_messages_controller.rb', line 183

def destroy
  @sms_message = SmsMessage.find(params[:id])
  authorize!(:destroy, @sms_message)
  if @sms_message.destroy
    flash[:info] = "SMS Message #{@sms_message.id} deleted"
    redirect_to_return_path_or_default redirect_url(@sms_message)
  else
    flash[:error] = "SMS Message could not be deleted. #{@sms_message.full_messages.join(', ')}"
    redirect_to_return_path_or_default sms_message_path(@sms_message)
  end
end

#destroy_and_block_sendervoid

This method returns an undefined value.

Blocks the message's sender number, then deletes it via #destroy.



198
199
200
201
202
203
# File 'app/controllers/crm/sms_messages_controller.rb', line 198

def destroy_and_block_sender
  @sms_message = SmsMessage.find(params[:id])
  authorize!(:destroy, @sms_message)
  @sms_message.block_sender
  destroy
end

#do_forwardvoid

This method returns an undefined value.

Reassigns the message to another SMS-enabled employee and marks it
unread for them.



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'app/controllers/crm/sms_messages_controller.rb', line 335

def do_forward
  target_employee = Employee.active_employees.find_by(id: params[:target_employee_id])
  unless target_employee
    flash[:error] = 'Employee not found'
    return redirect_to_return_path_or_default sms_message_path(@sms_message)
  end

  target_number = target_employee.sms_enabled_numbers.first
  unless target_number
    flash[:error] = "#{target_employee.full_name} does not have an SMS-enabled number"
    return redirect_to_return_path_or_default sms_message_path(@sms_message)
  end

  if target_employee.id == @sms_message.recipient_party_id
    flash[:warning] = 'Message is already assigned to this employee'
    return redirect_to_return_path_or_default sms_message_path(@sms_message)
  end

  @sms_message.raw_data ||= {}
  @sms_message.raw_data['forwarded_from'] = {
    'original_recipient' => @sms_message.recipient,
    'original_recipient_party_id' => @sms_message.recipient_party_id,
    'forwarded_by' => current_user.id,
    'forwarded_at' => Time.current.iso8601
  }
  @sms_message.recipient = target_number
  @sms_message.recipient_party = target_employee
  @sms_message.unread = true
  # Skip validations: state-machine callbacks (e.g. trigger_send) must not fire
  # on a received message; only recipient, recipient_party_id, unread, and raw_data change.
  @sms_message.save!(validate: false)

  flash[:info] = "Message forwarded to #{target_employee.full_name}"
  redirect_to_return_path_or_default sms_message_path(@sms_message)
end

#editvoid

This method returns an undefined value.

Loads a draft message for editing (only new-state messages qualify).



101
102
103
104
105
106
107
108
109
110
111
112
# File 'app/controllers/crm/sms_messages_controller.rb', line 101

def edit
  @sms_message = SmsMessage.find(params[:id])
  authorize!(:update, @sms_message)
  if @sms_message.new?
    load_destination_numbers
    load_thread_preview
    render :edit
  else
    flash[:error] = 'Only messages in new state can be edited'
    redirect_to_return_path_or_default redirect_url(@sms_message)
  end
end

#forwardvoid

This method returns an undefined value.

Renders the employee picker for forwarding the message loaded by
#load_and_authorize_inbound_sms_message.



327
328
329
# File 'app/controllers/crm/sms_messages_controller.rb', line 327

def forward
  @sms_enabled_employee_options = Employee.sms_enabled_for_select
end

#indexvoid

This method returns an undefined value.

Lists SMS messages (standalone) or renders the per-party SMS tab data
when a context object (customer/contact) is present.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'app/controllers/crm/sms_messages_controller.rb', line 16

def index
  authorize!(:show, SmsMessage)
  if context_object
    build_sms_tab_data(context_object)
  else
    @context_object = nil
    @q = SmsMessage.ransack(params[:q])
    @q.sorts = 'created_at desc' if @q.sorts.blank?
    @return_path = sms_messages_path(q: params[:q])
    @context_sms_contact_points = ContactPoint.none
    @pagy, @sms_messages = pagy(@q.result.includes(:recipient_party, :sender_party), limit: params[:per])
    # Gates the bulk-delete checkbox column (standalone /sms_messages page
    # only — the per-parent SMS tabs don't get it).
    @sms_bulk_select = can?(:destroy, SmsMessage)
  end
  respond_to do |format|
    format.html { render layout: should_render_layout? }
  end
end

#match_inbound_sendervoid

This method returns an undefined value.

Re-attempts matching an unmatched inbound message's sender to an
existing party.



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'app/controllers/crm/sms_messages_controller.rb', line 260

def match_inbound_sender
  @sms_message = SmsMessage.find(params[:id])
  if @sms_message.inbound? && @sms_message.sender_party.nil?
    @sms_message.match_inbound_sender
    if @sms_message.sender_party
      flash[:info] = "Found matching party: #{@sms_message.sender_party.full_name}"
      @sms_message.save!
    else
      flash[:warning] = 'No matching party found'
    end
  else
    flash[:error] = 'Cannot create customer from message, message must be inbound and not already have a party'
  end
  redirect_to_return_path_or_default sms_message_path(@sms_message)
end

#newvoid

This method returns an undefined value.

Builds a new outbound message, defaulting the sender from a preset
param, the user's own SMS number, or a shared company number.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'app/controllers/crm/sms_messages_controller.rb', line 77

def new
  @sms_message = SmsMessage.new(params[:sms_message] || {})
  authorize!(:create, SmsMessage)
  @original_sms_message = nil
  if params[:mark_as_read_sms_message_id].present?
    @original_sms_message = SmsMessage.find_by(id: params[:mark_as_read_sms_message_id])
    @original_sms_message&.update_attribute!(:unread, false)
  end
  # @sms_message.sender_party = current_user
  @sms_message.direction = :outbound
  # Sender precedence: a preset from params (the Reply link passes the number
  # the customer actually texted), then the user's own work SMS number, then
  # the shared company numbers. The old hard error for users without a
  # personal number was needless — load_destination_numbers already offers
  # SMS_GLOBAL_NUMBERS to everyone in the sender dropdown, and it blocked
  # replying even when the reply had preset a perfectly valid sender.
  @sms_message.sender ||= current_user.sms_enabled_numbers.first || SmsMessage::SMS_GLOBAL_NUMBERS.first
  load_destination_numbers
  load_thread_preview
end

#resendObject

If you resend a new message, you're just editing it
If you resend any other state of messages, it's a clone and new



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'app/controllers/crm/sms_messages_controller.rb', line 142

def resend
  @sms_message = SmsMessage.find(params[:id])
  authorize!(:create, @sms_message)
  if @sms_message.outbound?
    load_destination_numbers
    if @sms_message.draft? # only new messages can be edited as is
      render :edit
    else # Clone and send to new
      @sms_message = @sms_message.deep_dup
      load_thread_preview
      render :new
    end
  else
    flash[:error] = 'Can only resend outbound messages'
    redirect_to_return_path_or_default redirect_url(@sms_message)
  end
end

#showvoid

This method returns an undefined value.

Shows a message and its thread, marking it read when requested or when
the current user is the recipient of an unread message.



59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'app/controllers/crm/sms_messages_controller.rb', line 59

def show
  @sms_message = SmsMessage.find(params[:id])
  authorize!(:show, @sms_message)
  load_conversation_thread
  return unless params[:mark_as_read].to_b || (@sms_message.unread && @sms_message.recipient_party_id == current_user.id)

  @sms_message.update_attribute!(:unread, false)
  notice = <<-EOS
      Message was marked as read,
      #{view_context.link_to('undo and mark unread', toggle_unread_sms_message_path(@sms_message, return_path: @return_path), data: { turbo_method: :post })}
  EOS
  flash.now[:notice] = notice
end

#thread_previewObject

Live conversation panel for the composer — the Stimulus controller
(crm-sms-conversation) points a Turbo Frame here whenever the sender or
recipient number changes, so the thread appears without submitting.



39
40
41
42
43
# File 'app/controllers/crm/sms_messages_controller.rb', line 39

def thread_preview
  authorize!(:show, SmsMessage)
  @conversation_messages = conversation_between(params[:sender], params[:recipient])
  render partial: 'conversation_preview', layout: false
end

#toggle_unreadvoid

This method returns an undefined value.

Flips the message's unread flag (inbound messages only).



48
49
50
51
52
53
# File 'app/controllers/crm/sms_messages_controller.rb', line 48

def toggle_unread
  @sms_message = SmsMessage.find(params[:id])
  authorize!(:update, SmsMessage)
  @sms_message.toggle!(:unread) if @sms_message.inbound?
  redirect_to_return_path_or_default redirect_url(@sms_message)
end

#unblock_sendervoid

This method returns an undefined value.

Unblocks the message's sender number.



313
314
315
316
317
318
319
320
321
# File 'app/controllers/crm/sms_messages_controller.rb', line 313

def unblock_sender
  @sms_message = SmsMessage.find(params[:id])
  if @sms_message.unblock_sender
    flash[:info] = "Sender #{@sms_message.sender} was unblocked"
  else
    flash[:error] = "Could not unblock #{@sms_message.sender}. Could be already unblocked"
  end
  redirect_to_return_path_or_default sms_message_path(@sms_message)
end

#updatevoid

This method returns an undefined value.

Updates a draft message and attempts to send it.



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'app/controllers/crm/sms_messages_controller.rb', line 163

def update
  @sms_message = SmsMessage.find(params[:id])
  authorize!(:update, @sms_message)
  if @sms_message.update(params[:sms_message])
    if @sms_message.send_message
      @sms_message.mark_conversation_read!
      flash[:info] = 'SMS message sent'
      redirect_to sms_message_path(@sms_message, return_path: params[:return_path])
    else
      flash[:error] = @sms_message.errors_to_s.presence || 'Problem sending message'
      redirect_to_return_path_or_default redirect_url(@sms_message)
    end
  else
    render :edit, status: :unprocessable_content
  end
end