Class: CustomersController

Overview

Controller: customers.

Constant Summary collapse

TAB_ACTIONS =

Tab fragments never render standalone — non-frame hits redirect to show ?tab=.

%i[tab_digital_assets tab_attachments tab_communications tab_sms tab_activities
tab_opportunities tab_orders tab_accounting tab_addresses tab_certifications
tab_contacts tab_lead_protections tab_main_profile tab_marketing
tab_product_reviews tab_warranty tab_profiling tab_receipts tab_showcases
tab_stats tab_subsidiaries tab_tickets tab_training tab_outlet_purchases].freeze

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::DigitalAssetLinkable

#link_images, #link_videos

Methods included from Controllers::RequireTurboFrame

require_in_pane_tab_frames, require_turbo_frame

Methods included from Controllers::Workflowable

#render_workflow_error_stream, #render_workflow_success_stream, #workflow_action, #workflow_action_complete

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

#accounting_actionvoid

This method returns an undefined value.

Dispatches accounting-tab actions: create receipt, record payment, or
transmit documents (invoices / credit memos / statements) via a new
communication.



659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
# File 'app/controllers/customers_controller.rb', line 659

def accounting_action
  form_action, template_id = params[:transmit_instructions]&.split('|')
  case form_action
  when 'receipt'
    redirect_to new_receipt_path('credit_memos' => params['credit_memos'], 'invoices' => params['invoices'])
  when 'payment'
    redirect_to new_payment_receipts_path('customer_id' => @customer.id, 'invoices' => params['invoices'], 'credit_memos' => params['credit_memos'], :return_path => customer_path(@customer))
  when 'transmit'
    if params[:invoices].blank? && params[:credit_memos].blank? && params[:soas].blank?
      flash[:error] = 'Please select documents to transmit'
      redirect_to customer_path(@customer, tab: 'accounting')
    else
      begin
        adt = AccountingDocumentTransmitter.new(
          { INV: params['invoices'], CM: params['credit_memos'], SOA: params['soas'] },
          customer: @customer,
          template_id: template_id
        )
        co = adt.build_communication
        if co.save
          redirect_to edit_communication_path(co.id)
        else
          flash[:error] = "Problem creating communication. #{co.errors_to_s}"
          redirect_to customer_path(@customer, tab: 'accounting')
        end
      rescue Dragonfly::Job::Fetch::NotFound => e
        flash[:error] = "One or more document PDFs could not be found. The invoice PDF may need to be regenerated. (#{e.message})"
        redirect_to customer_path(@customer, tab: 'accounting')
      end
    end
  else
    flash[:error] = 'Unrecognized or unspecified action'
    redirect_to customer_path(@customer, tab: 'accounting')
  end
end

#add_to_campaignvoid

This method returns an undefined value.

Subscribes the customer to the selected marketing campaign.



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

def add_to_campaign
  campaign_id = params.dig(:new_campaign, :campaign_id)
  if campaign_id.blank?
    flash[:error] = 'You must select a campaign'
  else
    campaign = Campaign.find(campaign_id)
    res = campaign.add_customer(@customer)
    if res.subscribed?
      flash[:info] = "Customer added to #{campaign.name}"
    else
      flash[:error] = "Customer not added to #{campaign.name}, error: #{res.message}"
    end
  end
  redirect_to marketing_customer_path(@customer)
end

#auto_increase_available_creditvoid

This method returns an undefined value.

Attempts an automatic available-credit increase (terms customers only),
falling back to an Accounting approval request.



835
836
837
838
839
840
841
842
843
844
845
# File 'app/controllers/customers_controller.rb', line 835

def auto_increase_available_credit
  order = params[:order_id].nil? ? nil : Order.find(params[:order_id])
  if !@customer.has_terms?
    flash[:error] = 'Credit limit increase only available to customers with terms'
  elsif @customer.auto_increase_available_credit(params[:amount].to_d, order)
    flash[:info] = "Request approved. Available credit increased to #{number_to_currency(@customer.available_credit)}"
  else
    flash[:info] = 'Unable to automatically increase customer credit as some criteria were not met. Request has been sent to Accounting for approval.'
  end
  redirect_to_return_path_or_default(customer_path(@customer))
end

#available_creditvoid

This method returns an undefined value.

Renders the available-credit page.



285
# File 'app/controllers/customers_controller.rb', line 285

def available_credit; end

#check_credit_limitvoid

This method returns an undefined value.

Renders the credit-limit check result for a quoted or entered order
amount.



850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
# File 'app/controllers/customers_controller.rb', line 850

def check_credit_limit
  params[:check_credit_limit_params] ||= { order_amount: '' }
  @check_credit_limit_params = OpenStruct.new(params[:check_credit_limit_params])
  @customer = Customer.find(params[:id])

  quote = Quote.find(params[:check_credit_limit_params][:quote_id]) if params[:check_credit_limit_params][:quote_id].present?
  order_amount = params[:check_credit_limit_params][:order_amount]
  @amount = if quote.present?
              quote.total
            else
              (order_amount.present? ? order_amount.to_f : 0)
            end

  @result = {}
  @result = @customer.request_credit(@amount) if @amount.positive?
end

#closedvoid

This method returns an undefined value.

Starts the close-customer flow (drop event) when the customer can be
closed.



823
824
825
826
827
828
829
830
# File 'app/controllers/customers_controller.rb', line 823

def closed
  if @customer.sales_reps_ids.present? || @customer.can_closed?
    redirect_to new_customer_customer_drop_event_path(customer_id: @customer.id)
  else
    flash[:info] = 'Customer is not in a state that can be closed'
    redirect_to customer_path(@customer)
  end
end

#confirm_account_teamvoid

This method returns an undefined value.

Saves the account team via Customer::SalesRepAssigner.



751
752
753
754
755
756
757
758
# File 'app/controllers/customers_controller.rb', line 751

def 
  rep_assigner = Customer::SalesRepAssigner.new(@customer)
  if rep_assigner.assign_reps()
    redirect_to @customer, notice: 'Account Team Saved'
  else
    render :edit_account_team, status: :unprocessable_content
  end
end

#confirm_lead_protectionvoid

This method returns an undefined value.

Saves lead-protection changes.



797
798
799
800
801
802
803
# File 'app/controllers/customers_controller.rb', line 797

def confirm_lead_protection
  if @customer.update(params[:customer] || {})
    redirect_to @customer, notice: 'Lead Protection Saved'
  else
    render :edit_lead_protection, status: :unprocessable_content
  end
end

#confirm_notification_channelsvoid

This method returns an undefined value.

Saves notification-channel changes, surfacing duplicate-channel errors.



780
781
782
783
784
785
786
787
788
789
# File 'app/controllers/customers_controller.rb', line 780

def confirm_notification_channels
  if @customer.update(params[:customer] || {})
    redirect_to @customer, notice: 'Notification Channels Saved'
  else
    render :edit_notification_channels, status: :unprocessable_content
  end
rescue ActiveRecord::RecordNotUnique
  @customer.errors.add(:base, 'A duplicate notification channel already exists. Please refresh and try again.')
  render :edit_notification_channels, status: :unprocessable_content
end

#confirm_pricingvoid

This method returns an undefined value.

Saves the pricing selection.



736
737
738
739
740
741
742
743
# File 'app/controllers/customers_controller.rb', line 736

def confirm_pricing
  if @customer.update(customer_pricing_params)
    redirect_to @customer, notice: 'Pricing Selection Saved'
  else
    set_program_pricings
    render :edit_pricing, status: :unprocessable_content
  end
end

#confirm_profilevoid

This method returns an undefined value.

Saves profile changes; reassigns the pricing program when the profile
changed.



715
716
717
718
719
720
721
722
723
724
725
726
# File 'app/controllers/customers_controller.rb', line 715

def confirm_profile
  res = Customer::UpdateProfile.new.process(@customer, customer_profile_params)
  if res.success
    if res.profile_changed
      Customer::PricingProgramAssigner.new(customer_id: @customer.id).update_customer
      # redirect_to questions_customer_pricing_program_assigner_path(@customer), notice: 'Profile updated, please confirm pricing'
    end
    redirect_to @customer
  else
    render :edit_profile, status: :unprocessable_content
  end
end

#confirm_structurevoid

This method returns an undefined value.

Saves the parent-organization change.



766
767
768
769
770
771
772
# File 'app/controllers/customers_controller.rb', line 766

def confirm_structure
  if @customer.update(params[:customer] || {})
    redirect_to @customer, notice: 'Parent Org Saved'
  else
    render :edit_parent_org, status: :unprocessable_content
  end
end

#createvoid

This method returns an undefined value.

Legacy action — always raises; customers are created via customer_identify.

Raises:

  • (RuntimeError)

    always



80
81
82
# File 'app/controllers/customers_controller.rb', line 80

def create
  raise 'Legacy Method, should not be used'
end

#create_iq_accessory_preferencevoid

This method returns an undefined value.

Creates an IQ accessory filter for the customer from an item or exclusive
item group.



374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'app/controllers/customers_controller.rb', line 374

def create_iq_accessory_preference
  item = exclusive_item_group = nil
  item = Item.find(params[:iq_accessory_filter][:item_id]) if params[:iq_accessory_filter][:item_id].present?
  exclusive_item_group = ExclusiveItemGroup.find(params[:iq_accessory_filter][:exclusive_item_group_id]) if params[:iq_accessory_filter][:exclusive_item_group_id].present?
  item_or_exclusive_item_group = item || exclusive_item_group
  @iq_accessory_filter = IqAccessoryFilter.create_single_customer_iq_accessory_filter(@customer, item_or_exclusive_item_group, params[:iq_accessory_filter][:comment]) if item_or_exclusive_item_group
  if @iq_accessory_filter
    redirect_to(iq_accessory_preferences_customer_path(@customer))
  else
    @iq_accessory_filter ||= IqAccessoryFilter.new(params[:iq_accessory_filter])
    render action: 'new_iq_accessory_preference', status: :unprocessable_content
  end
end

#create_topic_follow_upvoid

This method returns an undefined value.

Creates a topic follow-up communication for the customer and opens it for
editing.



698
699
700
701
702
703
704
705
706
# File 'app/controllers/customers_controller.rb', line 698

def create_topic_follow_up
  com = CustomerTopic.create_communication(@customer, current_user)
  if com.persisted?
    redirect_to edit_communication_path(com)
  else
    flash[:error] = "There was a problem creating the communication. #{com.errors_to_s}"
    redirect_to customer_path(@customer, tab: 'topics')
  end
end

#delete_iq_accessory_preferencevoid

This method returns an undefined value.

Deletes an IQ accessory preference.



407
408
409
410
411
# File 'app/controllers/customers_controller.rb', line 407

def delete_iq_accessory_preference
  @iq_accessory_filter = IqAccessoryFilter.for_customer(@customer).is_auto_generated_only.find(params[:iq_accessory_filter_id])
  @iq_accessory_filter&.destroy
  redirect_to(iq_accessory_preferences_customer_path(@customer))
end

#destroyObject

DELETE /customers/1



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'app/controllers/customers_controller.rb', line 105

def destroy
  if @customer.ok_to_delete?
    @customer.destroy
    redirect_to(customers_url)
  else
    deps = +''
    @customer.opportunities.each do |o|
      break if deps.length > 200

      deps << "<a href='#{opportunity_path(o)}'>opportunity: #{o.name}</a> "
    end
    @customer.quotes.each do |q|
      break if deps.length > 200

      deps << "<a href='#{quote_path(q)}'>quote: #{q.reference_number}</a> "
    end
    @customer.orders.each do |o|
      break if deps.length > 200

      deps << "<a href='#{order_path(o)}'>order: #{o.reference_number}</a> "
    end
    deps << '...' if deps.length > 200
    flash[:error] = "Customer cannot be deleted because it is referenced by: #{deps}"
    redirect_to customer_path(@customer)
  end
end

#do_auto_assign_sales_repvoid

This method returns an undefined value.

Force-runs automatic sales-rep assignment for the customer.



807
808
809
810
811
812
813
814
815
816
817
818
# File 'app/controllers/customers_controller.rb', line 807

def do_auto_assign_sales_rep
  # Remove the do not assign flag if this action is explicitely called
  @customer.do_not_auto_assign = false
  rep_assigner = Customer::SalesRepAssigner.new(@customer, force_assign: true)
  if rep_assigner.auto_assign
    flash[:info] = 'Account team assigned'
    redirect_to customer_path(@customer)
  else
    flash[:error] = rep_assigner.assignment_failure_reason
    redirect_to (@customer)
  end
end

#do_mergevoid

This method returns an undefined value.

Enqueues the customer-merge background job and redirects to the job page.



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

def do_merge
  load_customers || return
  job_options = { customer_ids: [@survivor.id, @duplicate.id], notify_email: current_user.email }.stringify_keys
  job_id = CustomerMergeWorker.perform_async(job_options)
  if job_id.blank?
    if (job_id = BackgroundJobStatus.search(worker_klass: 'CustomerMergeWorker', args: job_options).first&.dig(:job_id))
      flash[:warning] = 'A similar job was already enqueued, watching status of that job now'
    end
  end
  if job_id.present?
    redirect_to job_path(job_id)
  else
    flash[:warning] = 'There was a problem queuing the job'
    redirect_to_return_path_or_default customer_path(@survivor.id)
  end
end

#drop_eventsvoid

This method returns an undefined value.

Renders the drop-events page.



305
# File 'app/controllers/customers_controller.rb', line 305

def drop_events; end

#duplicatesvoid Also known as: complete_qualification

This method returns an undefined value.

Lists possible duplicate customers for merging, preemptively allocating a
sales rep when qualification can complete.



621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
# File 'app/controllers/customers_controller.rb', line 621

def duplicates
  if params[:customer_id].present?
    @possible_duplicates = Customer.where(id: params[:customer_id])
    flash[:error] = "Cannot merge into itself.  This is the same customer you're giving me." if @possible_duplicates.first == @customer
  else
    @possible_duplicates = Query::CustomerFinder.new(customer: @customer).find_duplicates
  end

  if @customer.can_complete_qualification? || @customer.primary_sales_rep_id.nil?
    # Preemptive allocation of sales rep, takes a slot but that's how we go
    rep_options = Customer::SalesRepAssigner.new(@customer).determine_sales_rep
    @customer.primary_sales_rep_id = rep_options[:primary_sales_rep_id] if rep_options.present?
  end

  if request.xhr?
    render partial: 'duplicates'
  else
    render :duplicates
  end
end

#editObject

GET /customers/1/edit



66
67
68
69
70
71
72
73
74
75
# File 'app/controllers/customers_controller.rb', line 66

def edit
  if @customer
    @source_id = @customer.source_id
    set_edit_variables
    render_edit_action(status: :ok)
  else
    flash[:error] = 'Customer does not exist.'
    redirect_to customers_path
  end
end

#edit_account_teamvoid

This method returns an undefined value.

Renders the account-team editor.



747
# File 'app/controllers/customers_controller.rb', line 747

def ; end

#edit_accounting_informationvoid

This method returns an undefined value.

Renders the accounting-information editor (read-only fallback when the
user lacks permission).



314
315
316
317
318
319
320
321
322
323
# File 'app/controllers/customers_controller.rb', line 314

def edit_accounting_information
  @tax_exemptions = @customer.tax_exemptions
  respond_to do |format|
    if can? :edit_accounting_info, Customer
      format.html
    else
      format.html { render action: 'show_accounting_information' }
    end
  end
end

#edit_catalogvoid

This method returns an undefined value.

Renders the catalog-change form for the customer.



438
439
440
441
# File 'app/controllers/customers_controller.rb', line 438

def edit_catalog
  @new_catalog = Catalog.find(params[:new_catalog_id]) if params[:new_catalog_id]
  @customer.catalog = @new_catalog
end

#edit_default_addressvoid

This method returns an undefined value.

Renders the default-address editor.



537
538
539
# File 'app/controllers/customers_controller.rb', line 537

def edit_default_address
  authorize!(:update, @customer)
end

#edit_iq_accessory_preferencevoid

This method returns an undefined value.

Renders the edit form for an IQ accessory preference.



390
391
392
# File 'app/controllers/customers_controller.rb', line 390

def edit_iq_accessory_preference
  @iq_accessory_filter = IqAccessoryFilter.for_customer(@customer).is_auto_generated_only.find(params[:iq_accessory_filter_id])
end

#edit_lead_protectionvoid

This method returns an undefined value.

Renders the lead-protection editor.



793
# File 'app/controllers/customers_controller.rb', line 793

def edit_lead_protection; end

#edit_notification_channelsvoid

This method returns an undefined value.

Renders the notification-channels editor.



776
# File 'app/controllers/customers_controller.rb', line 776

def edit_notification_channels; end

#edit_pricingvoid

This method returns an undefined value.

Renders the pricing editor.



730
731
732
# File 'app/controllers/customers_controller.rb', line 730

def edit_pricing
  set_program_pricings
end

#edit_profilevoid

This method returns an undefined value.

Renders the profile editor.



710
# File 'app/controllers/customers_controller.rb', line 710

def edit_profile; end

#edit_shipping_preferencesvoid

This method returns an undefined value.

Renders the shipping-preferences editor.



347
# File 'app/controllers/customers_controller.rb', line 347

def edit_shipping_preferences; end

#edit_structurevoid

This method returns an undefined value.

Renders the structure (parent organization) editor.



762
# File 'app/controllers/customers_controller.rb', line 762

def edit_structure; end

#indexObject

GET /customers



26
27
28
# File 'app/controllers/customers_controller.rb', line 26

def index
  redirect_to employee_dashboard_path(current_user)
end

#iq_accessory_preferencesvoid

This method returns an undefined value.

Lists the customer's auto-generated IQ accessory filters.



361
362
363
# File 'app/controllers/customers_controller.rb', line 361

def iq_accessory_preferences
  @iq_accessory_filters = IqAccessoryFilter.for_customer(@customer).is_auto_generated_only
end

#lookup_emailsvoid

This method returns an undefined value.

JSON autocomplete of the customer's (and contacts') email addresses.



491
492
493
494
495
# File 'app/controllers/customers_controller.rb', line 491

def lookup_emails
  @customer = Customer.find(params[:customer_id])
  results = @customer.contacts_and_self_contact_points_by_category(ContactPoint::EMAIL).distinct.order(:detail).pluck(:detail)
  render json: TomSelect.format_json_results(self, results, params[:page], params[:per_page])
end

#lookup_faxesvoid

This method returns an undefined value.

JSON autocomplete of the customer's (and contacts') fax numbers.



507
508
509
510
511
# File 'app/controllers/customers_controller.rb', line 507

def lookup_faxes
  @customer = Customer.find(params[:customer_id])
  results = @customer.contacts_and_self_contact_points_by_category(ContactPoint::FAX).distinct.order(:detail).pluck(:detail)
  render json: TomSelect.format_json_results(self, results, params[:page], params[:per_page])
end

#lookup_phonesvoid

This method returns an undefined value.

JSON autocomplete of the customer's (and contacts') phone numbers.



499
500
501
502
503
# File 'app/controllers/customers_controller.rb', line 499

def lookup_phones
  @customer = Customer.find(params[:customer_id])
  results = @customer.contacts_and_self_contact_points_by_category([ContactPoint::PHONE, ContactPoint::CELL]).distinct.order(:detail).pluck(:detail)
  render json: TomSelect.format_json_results(self, results, params[:page], params[:per_page])
end

#marketingvoid

This method returns an undefined value.

Renders the marketing page (campaign memberships).



301
# File 'app/controllers/customers_controller.rb', line 301

def marketing; end

#mergevoid

This method returns an undefined value.

Renders the merge comparison screen for two customers, listing blocking
errors (orders under shipping) for each side.



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'app/controllers/customers_controller.rb', line 177

def merge
  @error_count = 0
  first_customer_id = params.dig(:merge, :first_customer_id) || params.dig(:first_customer_id)
  second_customer_id = params.dig(:merge, :second_customer_id) || params.dig(:second_customer_id)
  @customer1 = Customer.find(first_customer_id)
  @customer2 = Customer.find(second_customer_id)

  if @customer1.catalog_id == @customer2.catalog_id
    @customer1_inventory = Merger::CustomerMerger.inventory(@customer1)
    @customer1_errors = []
    @customer1.orders.all_awaiting_deliveries.each do |order|
      @customer1_errors << { record_type: order.class.name, record_id: order.id, record_description: order.state, errors: ['Order is under shipping states'], record: order }
    end
    # @customer1_inventory.each do |r|
    #   @customer1_errors += 1 unless r.valid?
    # end

    @customer2_inventory = Merger::CustomerMerger.inventory(@customer2)
    @customer2_errors = []
    @customer2.orders.all_awaiting_deliveries.each do |order|
      @customer2_errors << { record_type: order.class.name, record_id: order.id, record_description: order.state, errors: ['Order is under shipping states'], record: order }
    end
    # @customer2_inventory.each do |r|
    #   @customer2_errors += 1 unless r.valid?
    # end

    @error_count = @customer1_errors.size + @customer2_errors.size
    if params[:survivor_customer_id].present?
      @customer1_selected = params[:survivor_customer_id] == @customer1.id
      @customer2_selected = params[:survivor_customer_id] == @customer2.id
    elsif @customer1.created_at < @customer2.created_at
      @customer1_selected = true
    else
      @customer2_selected = true
    end
  else
    flash[:error] = 'Customer master and duplicate have different catalogs, resolve catalog discrepancy first'
    redirect_to_return_path_or_default customer_path(@customer1)
  end
end

#merge_addressesvoid

This method returns an undefined value.

Runs the address-duplicate merger for the customer's addresses.



259
260
261
262
263
264
265
266
267
268
# File 'app/controllers/customers_controller.rb', line 259

def merge_addresses
  am = Merger::AddressMerger.new(@customer.id)
  begin
    am.perform_merge!
    flash[:info] = 'Merge check completed.'
  rescue StandardError => e
    flash[:error] = "Merge could not be completed. #{e.message}"
  end
  redirect_to_return_path_or_default customer_addresses_path(@customer)
end

#merge_contact_pointsvoid

This method returns an undefined value.

Runs the contact-point merger for the customer's contact points.



272
273
274
275
276
277
278
279
280
281
# File 'app/controllers/customers_controller.rb', line 272

def merge_contact_points
  cpm = Merger::ContactPointMerge.new(@customer.id)
  begin
    cpm.perform_merge!
    flash[:info] = 'Merge check completed.'
  rescue StandardError => e
    flash[:error] = "Merge could not be completed. #{e.message}"
  end
  redirect_to_return_path_or_default customer_contacts_path(@customer)
end

#newObject

GET /customers/new



61
62
63
# File 'app/controllers/customers_controller.rb', line 61

def new
  redirect_to new_customer_identify_path
end

#new_iq_accessory_preferencevoid

This method returns an undefined value.

Renders the new IQ accessory preference form.



367
368
369
# File 'app/controllers/customers_controller.rb', line 367

def new_iq_accessory_preference
  @iq_accessory_filter = IqAccessoryFilter.new
end

#promotionsvoid

This method returns an undefined value.

Renders the promotions list: active tier-3 coupons targeting the customer.



566
567
568
569
570
# File 'app/controllers/customers_controller.rb', line 566

def promotions
  qb = Coupon::QueryBuilder.new(Coupon.active.tier3)
  @coupons = qb.for_specific_customer(@customer).query.order('coupons.expiration_date desc')
  render layout: false
end

#radiant_rewardsvoid

This method returns an undefined value.

Renders the Radiant Rewards page.



459
# File 'app/controllers/customers_controller.rb', line 459

def radiant_rewards; end

#recalculate_profiling_informationvoid

This method returns an undefined value.

Recomputes the customer's profiling information.



415
416
417
# File 'app/controllers/customers_controller.rb', line 415

def recalculate_profiling_information
  @customer.recalculate_profiling_information
end

#release_from_lead_qualificationvoid

This method returns an undefined value.

Completes lead qualification for the customer, optionally pre-assigning a
primary sales rep, and warns about held orders.



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'app/controllers/customers_controller.rb', line 240

def release_from_lead_qualification
  if @customer.can_complete_qualification?
    if (psr_id = params.dig(:customer, :primary_sales_rep_id))
      # preassign rep
      Customer::SalesRepAssigner.new(@customer).assign_reps(primary_sales_rep_id: psr_id)
    end
    res = @customer.complete_qualification # this will allow the updating
    if res
      flash[:info] = +'Customer was released from lead qualification stage. '
      flash[:info] << "#{@customer.orders.held.length} order(s) are currently on hold, please review them for correctness and release." if @customer.orders.held.present?
    else
      flash[:error] = "Customer was not released from lead qualification stage. #{@customer.errors_to_s}"
    end
  end
  redirect_to customer_path(@customer)
end

#sales_priority_index_analysisvoid

This method returns an undefined value.

Renders the sales-priority-index score breakdown for the customer.



463
464
465
466
# File 'app/controllers/customers_controller.rb', line 463

def sales_priority_index_analysis
  @results = Customer::SalesPriorityCalculation.score_single_customer(@customer.id)
  render layout: 'crm/crm'
end

#showvoid

This method returns an undefined value.

Shows the customer's details.
For HTML: preloads key associations, loads the customer's active contacts with contact points, and enqueues a profile-image lookup.
For JSON: renders the customer as JSON.

Raises:

  • (ActiveRecord::RecordNotFound)

    if the customer is not found.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'app/controllers/customers_controller.rb', line 36

def show
  raise ActiveRecord::RecordNotFound if @customer.nil?

  respond_to do |format|
    format.html do
      ActiveRecord::Associations::Preloader.new(
        records: [@customer],
        associations: [
          { primary_sales_rep: %i[profile_image employee_phone_status] },
          :profile, :buying_group, :source,
          :profile_image, :parent_organization, :customer_record,
          { catalog: :store },
          { addresses: :country }
        ]
      ).call
      @contacts = @customer.contacts.active.includes(:contact_points).order(Contact[:full_name])
      enqueue_profile_image_lookup(@customer)
    end
    format.json do
      render json: @customer.to_json
    end
  end
end

#show_accounting_informationvoid

This method returns an undefined value.

Renders the read-only accounting-information view.



309
# File 'app/controllers/customers_controller.rb', line 309

def show_accounting_information; end

#statement_pdfvoid

This method returns an undefined value.

Generates the statement-of-account PDF and routes to a new communication
carrying it (or back to the customer when there are no open documents).



422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'app/controllers/customers_controller.rb', line 422

def statement_pdf
  @soa = StatementOfAccount.generate_pdf(@customer, params[:per_currency].present? || false)
  if @soa.nil?
    redirect_to customer_path(@customer.id)
    flash[:info] = 'Unable to generate statement, customer has no open documents.'
  else
    recipient_contact_point_id = @soa.primary_transmission_contact_point_id
    redirect_to new_communication_path(upload_ids: @soa.uploads.map(&:id),
                                       recipient_party_id: @customer.id,
                                       recipient_contact_point_id: recipient_contact_point_id,
                                       template_system_code: 'STATEMENTOFACCOUNT')
  end
end

#sync_creditvoid

This method returns an undefined value.

Re-synchronizes the customer's credit limit with the accounting system.



289
290
291
292
293
294
295
296
297
# File 'app/controllers/customers_controller.rb', line 289

def sync_credit
  res = @customer.sync_credit_limit
  if res[:ok]
    flash[:info] = 'Credit Limits Synchronized'
  else
    flash[:error] = "Error while synchronizing credit limits. #{res[:message]}"
  end
  redirect_to_return_path_or_default available_credit_customer_path(@customer)
end

#tab_accountingvoid

This method returns an undefined value.

Renders the accounting tab: paginated AR listings plus per-currency open
totals.



471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'app/controllers/customers_controller.rb', line 471

def tab_accounting
  ar_scope = @customer.ar_listings(show_open_only: params[:show_open_only])
  # countless skips the pagy COUNT(*) — view_ar_listings can't prune its correlated
  # subqueries from a COUNT projection across UNION ALL, so the count costs ~100ms
  # for high-AR customers (AppSignal #393). Prev/next nav is sufficient for this listing.
  @pagy_ar, @ar_listings = pagy(:countless, ar_scope, limit: 50, page: params[:page])
  # Calculate currency totals using database-level aggregation instead of Ruby iteration
  # Use reorder(nil) to remove inherited ORDER BY which causes GROUP BY errors
  @grouped_ar_listings = ar_scope.reorder(nil).where.not(currency: [nil, 'ALL']).group(:currency).sum(:open_amount_calc).sort

  respond_to do |format|
    format.html { render layout: false }
    format.turbo_stream do
      render turbo_stream: turbo_stream.replace('ar-listings-frame', partial: 'ar_listings', locals: { show_customer: false, show_check: true })
    end
  end
end

#tab_activitiesvoid

This method returns an undefined value.

Renders the activities tab fragment.



912
913
914
915
916
917
918
919
# File 'app/controllers/customers_controller.rb', line 912

def tab_activities
  authorize!(:read, @customer)
  build_activities_tab_data(@customer)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_addressesvoid

This method returns an undefined value.

Renders the addresses tab fragment.



531
532
533
# File 'app/controllers/customers_controller.rb', line 531

def tab_addresses
  render layout: false
end

#tab_attachmentsvoid

This method returns an undefined value.

Renders the attachments (uploads) tab fragment.



879
880
881
882
883
884
885
886
# File 'app/controllers/customers_controller.rb', line 879

def tab_attachments
  authorize!(:read, @customer)
  build_uploads_tab_data(@customer)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_certificationsvoid

This method returns an undefined value.

Renders the certifications tab fragment.



150
151
152
153
154
# File 'app/controllers/customers_controller.rb', line 150

def tab_certifications
  # @certifications = @customer.certifications
  # @course_enrollments = @customer.course_enrollments
  render layout: false
end

#tab_communicationsvoid

This method returns an undefined value.

Renders the communications tab fragment.



890
891
892
893
894
895
896
897
# File 'app/controllers/customers_controller.rb', line 890

def tab_communications
  authorize!(:read, @customer)
  build_communications_tab_data(@customer)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_contactsvoid

This method returns an undefined value.

Renders the contacts tab fragment.



515
516
517
518
519
520
521
# File 'app/controllers/customers_controller.rb', line 515

def tab_contacts
  @show_inactive = params[:show_inactive] == 'true'
  respond_to do |format|
    format.html { render layout: false }
    format.turbo_stream
  end
end

#tab_digital_assetsvoid

This method returns an undefined value.

Renders the digital-assets tab fragment.



869
870
871
872
873
874
875
# File 'app/controllers/customers_controller.rb', line 869

def tab_digital_assets
  authorize!(:read, @customer)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_lead_protectionsvoid

This method returns an undefined value.

Renders the lead-protections tab: customers this customer protects.



587
588
589
590
# File 'app/controllers/customers_controller.rb', line 587

def tab_lead_protections
  @lead_protections = Customer.joins(:customer_record).where('customer_records.lead_protector_id' => @customer.id)
  render layout: false
end

#tab_main_profilevoid

This method returns an undefined value.

Renders the main-profile tab fragment.



574
575
576
# File 'app/controllers/customers_controller.rb', line 574

def tab_main_profile
  render layout: false
end

#tab_marketingvoid

This method returns an undefined value.

Renders the marketing tab fragment.



560
561
562
# File 'app/controllers/customers_controller.rb', line 560

def tab_marketing
  render layout: false
end

#tab_opportunitiesvoid

This method returns an undefined value.

Renders the opportunities tab fragment.



938
939
940
941
942
943
944
945
# File 'app/controllers/customers_controller.rb', line 938

def tab_opportunities
  authorize!(:read, @customer)
  build_opportunities_tab_data(@customer)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_ordersvoid

This method returns an undefined value.

Renders the orders tab fragment.



949
950
951
952
953
954
955
956
# File 'app/controllers/customers_controller.rb', line 949

def tab_orders
  authorize!(:read, @customer)
  build_orders_tab_data(@customer)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_outlet_purchasesObject

Every retailer purchase we know this customer made, whether or not it ties
to a project. Unlike the opportunity tab, this includes customer-level links
— purchases that share no product line with any quote.
See doc/tasks/202608081330_OUTLET_PURCHASE_ATTRIBUTION.md.



925
926
927
928
929
930
931
932
933
934
# File 'app/controllers/customers_controller.rb', line 925

def tab_outlet_purchases
  authorize!(:read, @customer)
  @outlet_purchases = @customer.outlet_purchases
                               .includes(:verified_by, :opportunity, invoice: { customer: :catalog })
                               .order(created_at: :desc)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_product_reviewsvoid

This method returns an undefined value.

Renders the product-reviews tab: Reviews.io reviews plus the company
review link.



595
596
597
598
599
# File 'app/controllers/customers_controller.rb', line 595

def tab_product_reviews
  @reviews_io_reviews = fetch_reviews_io_reviews
  @company_review_link_data = prepare_company_review_link
  render layout: false
end

#tab_profilingvoid

This method returns an undefined value.

Renders the profiling tab fragment.



525
526
527
# File 'app/controllers/customers_controller.rb', line 525

def tab_profiling
  render layout: false
end

#tab_receiptsvoid

This method returns an undefined value.

Renders the receipts tab fragment.



651
652
653
# File 'app/controllers/customers_controller.rb', line 651

def tab_receipts
  render layout: false
end

#tab_showcasesvoid

This method returns an undefined value.

Renders the showcases tab fragment.



613
614
615
616
# File 'app/controllers/customers_controller.rb', line 613

def tab_showcases
  # Render showcases tab content only for AJAX tabs to avoid full-page reload loops
  render layout: false
end

#tab_smsvoid

This method returns an undefined value.

Renders the SMS tab fragment.



901
902
903
904
905
906
907
908
# File 'app/controllers/customers_controller.rb', line 901

def tab_sms
  authorize!(:read, @customer)
  build_sms_tab_data(@customer)
  respond_to do |format|
    format.html { render layout: should_render_layout? }
    format.turbo_stream
  end
end

#tab_statsvoid

This method returns an undefined value.

Renders the stats tab fragment.



554
555
556
# File 'app/controllers/customers_controller.rb', line 554

def tab_stats
  render layout: false
end

#tab_subsidiariesvoid

This method returns an undefined value.

Renders the subsidiaries tab fragment.



580
581
582
583
# File 'app/controllers/customers_controller.rb', line 580

def tab_subsidiaries
  @child_organizations = @customer.child_organizations unless @customer.child_organizations.empty?
  render layout: false
end

#tab_ticketsvoid

This method returns an undefined value.

Renders the tickets tab fragment.



645
646
647
# File 'app/controllers/customers_controller.rb', line 645

def tab_tickets
  render layout: false
end

#tab_trainingvoid

This method returns an undefined value.

Renders the training tab: the customer's contacts' training topics grouped
by topic category.



135
136
137
138
139
140
141
142
143
144
145
146
# File 'app/controllers/customers_controller.rb', line 135

def tab_training
  params[:q] = { state_in: %w[assigned requested certified] } if params[:q].blank?
  @q = ContactTrainingTopic.joins(:contact, { topic: :topic_category })
                           .merge(@customer.contacts)
                           .merge(Topic.sorted).ransack(params[:q])
  @contact_training_topics = @q.result
  @contact_training_topics_grouped = @contact_training_topics.group_by { |pt| pt.topic.topic_category }
  respond_to do |format|
    format.html { render layout: false }
    format.turbo_stream
  end
end

#tab_warrantyvoid

This method returns an undefined value.

Renders the warranty tab fragment, loading the customer's warranties.



603
604
605
606
607
608
609
# File 'app/controllers/customers_controller.rb', line 603

def tab_warranty
  authorize!(:read, @customer)
  @warranties = @customer.warranties
                         .includes(:order, :created_by, :products, :uploads)
                         .order(submitted_at: :desc)
  render layout: false
end

#updateObject

PUT /customers/1



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'app/controllers/customers_controller.rb', line 85

def update
  if @customer.update(params[:customer] || {})
    @customer.qualify_lead if @customer.guest?
    PartyRecordTimezone.new.process(@customer)
    redirect_to_return_path_or_default(customer_path(@customer))
  else
    set_edit_variables
    render_edit_action(status: :unprocessable_content)
  end
rescue ActiveRecord::RecordNotSaved => e
  ErrorReporting.from_controller(self).informational(
    'Customer update failed: RecordNotSaved',
    customer_id: @customer.id, error: e.message
  )
  @customer.errors.add(:base, 'Could not save the customer record. Please try again or contact support.')
  set_edit_variables
  render_edit_action(status: :unprocessable_content)
end

#update_accounting_informationvoid

This method returns an undefined value.

Saves accounting information; restricted to admin and accounting_rep roles.



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'app/controllers/customers_controller.rb', line 327

def update_accounting_information
  @can_update = .has_role?('admin') || .has_role?('accounting_rep')
  unless @can_update
    redirect_to customer_path(@customer), alert: 'You are not authorized to update accounting information.'
    return
  end
  respond_to do |format|
    if @customer.update(params[:customer] || {})
      format.html { redirect_to(customer_path(@customer)) }
      format.xml  { head :ok }
    else
      set_edit_variables
      format.html { render action: 'edit_accounting_information', status: :unprocessable_content }
      format.xml  { render xml: @document.errors, status: :unprocessable_content }
    end
  end
end

#update_catalogvoid

This method returns an undefined value.

Assigns the customer to a new catalog, converting orders, quotes, and
rooms.



446
447
448
449
450
451
452
453
454
455
# File 'app/controllers/customers_controller.rb', line 446

def update_catalog
  new_catalog = Catalog.find(params[:customer][:catalog_id])
  result = Catalog::AssignCatalogToCustomer.new.process(@customer, new_catalog)
  if result.catalog_assigned?
    flash[:info] = "Catalog changed to #{new_catalog.name} and all orders, quotes and rooms converted successfully. #{result.messages.join}".first(500)
  else
    flash[:error] = result.messages.to_sentence.first(500)
  end
  redirect_to_return_path_or_default customer_path(@customer)
end

#update_default_addressvoid

This method returns an undefined value.

Saves the customer's default address selection.



543
544
545
546
547
548
549
550
# File 'app/controllers/customers_controller.rb', line 543

def update_default_address
  authorize!(:update, @customer)
  if @customer.update(params[:customer])
    redirect_to_return_path_or_default customer_path(@customer, tab: 'addresses')
  else
    render :edit_default_address, status: :unprocessable_content
  end
end

#update_iq_accessory_preferencevoid

This method returns an undefined value.

Saves changes to an IQ accessory preference.



396
397
398
399
400
401
402
403
# File 'app/controllers/customers_controller.rb', line 396

def update_iq_accessory_preference
  @iq_accessory_filter = IqAccessoryFilter.for_customer(@customer).is_auto_generated_only.find(params[:iq_accessory_filter_id])
  if @iq_accessory_filter.update(params[:iq_accessory_filter])
    redirect_to(iq_accessory_preferences_customer_path(@customer))
  else
    render action: 'edit_iq_accessory_preference', status: :unprocessable_content
  end
end

#update_shipping_preferencesvoid

This method returns an undefined value.

Saves the customer's shipping preferences.



351
352
353
354
355
356
357
# File 'app/controllers/customers_controller.rb', line 351

def update_shipping_preferences
  if @customer.update(params[:customer] || {})
    redirect_to_return_path_or_default customer_path(@customer, tab: 'addresses')
  else
    render action: 'edit_shipping_preferences', status: :unprocessable_content
  end
end