Class: CustomersController

Inherits:
CrmController show all
Includes:
Controllers::DigitalAssetLinkable, Controllers::Workflowable
Defined in:
app/controllers/customers_controller.rb

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

#link_images, #link_videos

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, #initialize_crm_lazy_chunks, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download

Methods inherited from ApplicationController

#account_impersonated?, #add_to_flash, #append_token, #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::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, #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_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!, #authenticate_account_from_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, #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, #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

#accounting_actionObject



559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
# File 'app/controllers/customers_controller.rb', line 559

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_campaignObject



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'app/controllers/customers_controller.rb', line 118

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_creditObject



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

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_creditObject



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

def available_credit; end

#check_credit_limitObject



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

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

#closedObject



690
691
692
693
694
695
696
697
# File 'app/controllers/customers_controller.rb', line 690

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_teamObject



635
636
637
638
639
640
641
642
# File 'app/controllers/customers_controller.rb', line 635

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_entity
  end
end

#confirm_lead_protectionObject



669
670
671
672
673
674
675
# File 'app/controllers/customers_controller.rb', line 669

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

#confirm_notification_channelsObject



656
657
658
659
660
661
662
663
664
665
# File 'app/controllers/customers_controller.rb', line 656

def confirm_notification_channels
  if @customer.update(params[:customer] || {})
    redirect_to @customer, notice: 'Notification Channels Saved'
  else
    render :edit_notification_channels, status: :unprocessable_entity
  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_entity
end

#confirm_pricingObject



624
625
626
627
628
629
630
631
# File 'app/controllers/customers_controller.rb', line 624

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_entity
  end
end

#confirm_profileObject



607
608
609
610
611
612
613
614
615
616
617
618
# File 'app/controllers/customers_controller.rb', line 607

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_entity
  end
end

#confirm_structureObject



646
647
648
649
650
651
652
# File 'app/controllers/customers_controller.rb', line 646

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

#contacts_tabObject



452
453
454
455
456
457
458
# File 'app/controllers/customers_controller.rb', line 452

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

#createObject



47
48
49
# File 'app/controllers/customers_controller.rb', line 47

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

#create_iq_accessory_preferenceObject



296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'app/controllers/customers_controller.rb', line 296

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_entity
  end
end

#create_topic_follow_upObject



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

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_preferenceObject



323
324
325
326
327
# File 'app/controllers/customers_controller.rb', line 323

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



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'app/controllers/customers_controller.rb', line 72

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_repObject



677
678
679
680
681
682
683
684
685
686
687
688
# File 'app/controllers/customers_controller.rb', line 677

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] = 'Could not assign sales rep to account, contact system administrator'
    redirect_to (@customer)
  end
end

#do_mergeObject



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

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_eventsObject



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

def drop_events; end

#duplicatesObject Also known as: complete_qualification



524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
# File 'app/controllers/customers_controller.rb', line 524

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



36
37
38
39
40
41
42
43
44
45
# File 'app/controllers/customers_controller.rb', line 36

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_teamObject



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

def ; end

#edit_accounting_informationObject



249
250
251
252
253
254
255
256
257
258
# File 'app/controllers/customers_controller.rb', line 249

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_catalogObject



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

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

#edit_default_addressObject



468
469
470
# File 'app/controllers/customers_controller.rb', line 468

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

#edit_iq_accessory_preferenceObject



310
311
312
# File 'app/controllers/customers_controller.rb', line 310

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_protectionObject



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

def edit_lead_protection; end

#edit_notification_channelsObject



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

def edit_notification_channels; end

#edit_pricingObject



620
621
622
# File 'app/controllers/customers_controller.rb', line 620

def edit_pricing
  set_program_pricings
end

#edit_profileObject



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

def edit_profile; end

#edit_shipping_preferencesObject



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

def edit_shipping_preferences; end

#edit_structureObject



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

def edit_structure; end

#indexObject

GET /customers



10
11
12
# File 'app/controllers/customers_controller.rb', line 10

def index
  redirect_to employee_dashboard_path(current_user)
end

#iq_accessory_preferencesObject



288
289
290
# File 'app/controllers/customers_controller.rb', line 288

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

#lookup_emailsObject



434
435
436
437
438
# File 'app/controllers/customers_controller.rb', line 434

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_faxesObject



446
447
448
449
450
# File 'app/controllers/customers_controller.rb', line 446

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_phonesObject



440
441
442
443
444
# File 'app/controllers/customers_controller.rb', line 440

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

#marketingObject



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

def marketing; end

#mergeObject



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'app/controllers/customers_controller.rb', line 134

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_addressesObject



209
210
211
212
213
214
215
216
217
218
# File 'app/controllers/customers_controller.rb', line 209

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_pointsObject



220
221
222
223
224
225
226
227
228
229
# File 'app/controllers/customers_controller.rb', line 220

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



31
32
33
# File 'app/controllers/customers_controller.rb', line 31

def new
  redirect_to new_customer_identify_path
end

#new_iq_accessory_preferenceObject



292
293
294
# File 'app/controllers/customers_controller.rb', line 292

def new_iq_accessory_preference
  @iq_accessory_filter = IqAccessoryFilter.new
end

#promotionsObject



493
494
495
496
497
# File 'app/controllers/customers_controller.rb', line 493

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_rewardsObject



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

def radiant_rewards; end

#recalculate_profiling_informationObject



329
330
331
# File 'app/controllers/customers_controller.rb', line 329

def recalculate_profiling_information
  @customer.recalculate_profiling_information
end

#release_from_lead_qualificationObject



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'app/controllers/customers_controller.rb', line 192

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_analysisObject



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

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

#showObject

GET /customers/1

Raises:

  • (ActiveRecord::RecordNotFound)


15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'app/controllers/customers_controller.rb', line 15

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

  respond_to do |format|
    format.html do
      @contacts = @customer.contacts.active.includes(:contact_points).order(Contact[:full_name])
      # Queue profile image lookup if not already set (lazy backfill)
      enqueue_profile_image_lookup(@customer)
    end
    format.json do
      render json: @customer.to_json
    end
  end
end

#show_accounting_informationObject



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

def show_accounting_information; end

#show_accounting_tabObject



414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'app/controllers/customers_controller.rb', line 414

def show_accounting_tab
  ar_scope = @customer.ar_listings(show_open_only: params[:show_open_only])
  @pagy_ar, @ar_listings = pagy(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

#show_addresses_tabObject



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

def show_addresses_tab
  render layout: false
end

#show_attachments_tabObject



429
430
431
432
# File 'app/controllers/customers_controller.rb', line 429

def show_attachments_tab
  @uploads = @customer.uploads.order(Upload[:created_at].desc)
  render layout: false
end

#show_certifications_tabObject



112
113
114
115
116
# File 'app/controllers/customers_controller.rb', line 112

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

#show_communications_tabObject



546
547
548
549
# File 'app/controllers/customers_controller.rb', line 546

def show_communications_tab
  @pagy_communications, @communications = pagy(@customer.all_related_communications)
  render layout: false
end

#show_lead_protections_tabObject



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

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

#show_main_profile_tabObject



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

def show_main_profile_tab
  render layout: false
end

#show_marketing_tabObject



489
490
491
# File 'app/controllers/customers_controller.rb', line 489

def show_marketing_tab
  render layout: false
end

#show_opportunities_tabObject



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

def show_opportunities_tab
  @pagy_opportunities, @opportunities = pagy(Opportunity.includes([{ quotes: :active_orders }, :room_configurations, :orders])
                              .references(:room_configurations)
                              .where('opportunities.customer_id = :customer_id or room_configurations.system_owner_id = :customer_id or room_configurations.installer_id = :customer_id or room_configurations.electrician_id = :customer_id',
                                     customer_id: @customer.id)
                              .order('opportunities.created_at DESC'), page: params[:opportunities_page])

  respond_to do |format|
    format.html do
      render partial: 'opportunities_tab',
             locals: {
               opportunities: @opportunities, pagy: @pagy_opportunities, ajaxify: true, ajax_controller: 'customers', ajax_action: 'show_opportunities_tab', ajax_id: @customer.id
             }
    end
    format.turbo_stream do
      render turbo_stream: turbo_stream.replace('opportunities-container', partial: 'opportunities/index', locals: { opportunities: @opportunities, pagy: @pagy_opportunities, context_object: @customer })
    end
  end
end

#show_orders_tabObject



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'app/controllers/customers_controller.rb', line 390

def show_orders_tab
  @pagy_orders, orders = pagy(@customer.all_orders
                    .non_credit
                    .includes(:payments, :opportunity, :customer, shipping_address: :state)
                    .order('orders.created_at DESC'), page: params[:orders_page])

  respond_to do |format|
    format.html do
      render partial: 'orders_tab',
             locals: {
               orders: orders,
               ajaxify: true,
               ajax_controller: 'customers',
               ajax_action: 'show_orders_tab',
               ajax_id: @customer.id,
               current_customer: @customer
             }
    end
    format.turbo_stream do
      render turbo_stream: turbo_stream.replace('orders-container', partial: 'orders/index', locals: { orders: orders, pagy: @pagy_orders, context_object: @customer })
    end
  end
end

#show_product_reviews_tabObject



513
514
515
516
517
# File 'app/controllers/customers_controller.rb', line 513

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

#show_profiling_tabObject



460
461
462
# File 'app/controllers/customers_controller.rb', line 460

def show_profiling_tab
  render layout: false
end

#show_receipts_tabObject



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

def show_receipts_tab
  render layout: false
end

#show_rmas_tabObject



481
482
483
# File 'app/controllers/customers_controller.rb', line 481

def show_rmas_tab
  render layout: false
end

#show_showcases_tabObject



519
520
521
522
# File 'app/controllers/customers_controller.rb', line 519

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

#show_stats_tabObject



485
486
487
# File 'app/controllers/customers_controller.rb', line 485

def show_stats_tab
  render layout: false
end

#show_subsidiaries_tabObject



503
504
505
506
# File 'app/controllers/customers_controller.rb', line 503

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

#show_tickets_tabObject



551
552
553
# File 'app/controllers/customers_controller.rb', line 551

def show_tickets_tab
  render layout: false
end

#show_training_tabObject



99
100
101
102
103
104
105
106
107
108
109
110
# File 'app/controllers/customers_controller.rb', line 99

def show_training_tab
  params[:q] = { state_in: %w[assigned requested certified] } unless params[:q].present?
  @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

#statement_pdfObject



333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'app/controllers/customers_controller.rb', line 333

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.collect(&:id),
                                       recipient_party_id: @customer.id,
                                       recipient_contact_point_id: recipient_contact_point_id,
                                       template_system_code: 'STATEMENTOFACCOUNT')
  end
end

#sync_creditObject



233
234
235
236
237
238
239
240
241
# File 'app/controllers/customers_controller.rb', line 233

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_digital_assetsObject



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

def tab_digital_assets
  authorize!(:read, @customer)
  render layout: should_render_layout?
end

#updateObject

PUT /customers/1



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'app/controllers/customers_controller.rb', line 52

def update
  if @customer.update(params[:customer] || {})
    @customer.qualify_lead if @customer.guest?
    Party::RecordTimezone.new.process(@customer)
    redirect_to_return_path_or_default(customer_path(@customer))
  else
    set_edit_variables
    render_edit_action(status: :unprocessable_entity)
  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_entity)
end

#update_accounting_informationObject



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

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_entity }
      format.xml  { render xml: @document.errors, status: :unprocessable_entity }
    end
  end
end

#update_catalogObject



352
353
354
355
356
357
358
359
360
361
# File 'app/controllers/customers_controller.rb', line 352

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_addressObject



472
473
474
475
476
477
478
479
# File 'app/controllers/customers_controller.rb', line 472

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_entity
  end
end

#update_iq_accessory_preferenceObject



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

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_entity
  end
end

#update_shipping_preferencesObject



280
281
282
283
284
285
286
# File 'app/controllers/customers_controller.rb', line 280

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_entity
  end
end