Class: MyCartsController

Inherits:
BasePortalController show all
Includes:
MyAccountHelper
Defined in:
app/controllers/my_carts_controller.rb

Overview

Controller: my carts.

Constant Summary collapse

CHECKOUT_STARTED_CART_ID_SESSION_KEY =

Session key recording which cart already fired the checkout-started event,
so track_checkout_started only tracks once per cart.

'checkout_started_cart_id'

Constants included from Controllers::MasqueradeGuarded

Controllers::MasqueradeGuarded::DEFAULT_BLOCK_MESSAGE

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 MyAccountHelper

#account_nav_active?, #can_list_contact_resources?, #is_current_user_or_customer_login_email?, #setup_contact, #setup_customer_account, #sorted_contact_points

Methods inherited from BasePortalController

#current_ability, #portal_party, #set_catalog, #set_webpack

Methods included from Controllers::MasqueradeGuarded

block_while_masquerading, #masquerade_blocks?

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

#add_coupon(coupon_code) ⇒ String? (protected)

Attempts to apply a coupon to the cart.

Parameters:

  • coupon_code (String)

    coupon code to apply

Returns:

  • (String, nil)

    error message if failed, nil if successful



814
815
816
817
818
819
820
821
822
823
824
825
826
827
# File 'app/controllers/my_carts_controller.rb', line 814

protected def add_coupon(coupon_code)
  return nil if coupon_code.blank?

  res = @cart.customer_can_apply_coupon?(coupon_code)
  coupon = res[:coupon]
  msg = res[:message]

  return msg || "Coupon '#{coupon_code}' is invalid" unless coupon

  result = Coupon::ApplyCoupon.new.perform(@cart, { coupon_id: coupon.id })
  return result.errors unless result.success?

  nil # Success - no error
end

#add_itemvoid

This method returns an undefined value.

Adds a single SKU to the cart and exposes product info for the add-to-cart
toast and analytics.



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'app/controllers/my_carts_controller.rb', line 121

def add_item
  sku_array = [{ sku: params[:sku], qty: params[:quantity], room_configuration_id: params[:room_configuration_id] }]
  begin
    @recently_added = @cart.add_multiple_items(sku_array)
  rescue Order::ItemNotFound => e
    flash.now[:error] = e.message.presence || 'We could not find that product in the catalog.'
    @recently_added = []
  end
  @added_item = @recently_added&.first

  # Reload line_items to get fresh count after add_multiple_items saves
  @cart.line_items.reload

  # Store product info for analytics and toast display
  # Use ViewProductCatalog for proper pricing/category data
  catalog = Catalog.locale_to_catalog
  vpc = ViewProductCatalog.find_by(item_sku: params[:sku], catalog_id: catalog.id)
  @product_info = {
    sku: params[:sku],
    name: vpc&.item_name,
    image: vpc&.item&.primary_image_url(size: '100x100'),
    category: vpc&.product_category_name,
    price: vpc&.effective_price&.to_f,
    quantity: params[:quantity] || 1
  }

  respond_to do |format|
    format.turbo_stream
    format.json do
      render json: cart_array
    end
    format.html do
      redirect_to my_cart_path
    end
  end
end

#add_multiple_itemsvoid

This method returns an undefined value.

Adds several items to the cart from params[:items] (sku|qty pairs) or
a JSON request body, resolving SKU aliases and reporting skipped SKUs.
Optionally applies params[:coupon_code] afterwards.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'app/controllers/my_carts_controller.rb', line 82

def add_multiple_items
  sku_array = []

  if params[:items].present?
    sku_array = Array(params[:items]).map do |item|
      sku, qty = item.split('|')
      { sku:, qty: }
    end
  elsif (payload = request.body.read).present?
    skus = MultiJson.load(payload, symbolize_names: true)
    sku_array = skus.map { |sku:, qty:| { sku:, qty: } }
  end
  resolved_items, skipped_items = resolve_skus_for_cart(sku_array)

  @cart.add_multiple_items(resolved_items) if resolved_items.present?

  if skipped_items.present?
    skipped_skus = skipped_items.pluck(:sku).join(', ')
    flash[:warning] = "We could not add the following items to your cart: #{skipped_skus}."
  end

  if params[:coupon_code]
    coupon_error = add_coupon(params[:coupon_code])
    flash[:error] = coupon_error if coupon_error
  end

  respond_to do |format|
    format.json do
      render json: cart_array
    end
    format.html do
      redirect_to my_cart_path
    end
  end
end

#adjust_sku_quantityvoid

This method returns an undefined value.

Adjusts the quantity of a SKU in the cart (or a room within the cart),
refreshing the cart in place via Turbo Stream.



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
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
217
218
219
# File 'app/controllers/my_carts_controller.rb', line 161

def adjust_sku_quantity
  if params[:sku].present?
    sku = params[:sku]
    quantity = params[:quantity].to_i
    room_configuration_id = params[:room_configuration_id]

    if sku.present? && quantity.positive? && quantity < 100
      # find catalog by locale
      if (ci = @cart.catalog.catalog_items.public_catalog_items.by_skus(sku).first)
        @cart.recalculate_shipping = true # doesn't hurt to set it
        @cart.recalculate_discounts = true # ensure tier2 and auto-apply discounts are calculated
        @cart.force_total_reset = true
        if quantity.positive?
          @new_line = @cart.add_line_item(catalog_item_id: ci.id, quantity:, room_configuration_id:) # , :do_not_autosave => true)
          logger.debug "my_carts_controller#adjust_sku_quantity: Add SKU: #{sku} #{quantity} @new_line.inspect: #{@new_line.inspect}"
          logger.debug "my_carts_controller#adjust_sku_quantity: Add SKU: #{sku} #{quantity} @new_line.item.inspect: #{@new_line.item.inspect}"
          @cart.save!
        else
          if room_configuration_id && (room = RoomConfiguration.find(room_configuration_id))
            container = room
            container_name = 'room'
          else
            container = @cart
            container_name = 'cart'
          end
          li = container.line_items.find { |l| l.sku == sku }
          if li
            container.remove_line_item(li, quantity.abs)
            logger.debug "my_carts_controller#adjust_sku_quantity: Remove SKU: #{sku} #{quantity.abs} li.inspect: #{li.inspect}"
            logger.debug "my_carts_controller#adjust_sku_quantity: Remove SKU: #{sku} #{quantity.abs} li.item.inspect: #{li.item.inspect}"
            @cart.save
          else
            flash[:error] = "We could not find sku #{sku} in #{container_name} line items."
          end
        end
      else
        flash[:error] = "We could not find sku #{sku} in catalog."
      end
    else
      flash[:error] = "sku #{sku} not found in catalog or invalid quantities specified."
    end
  end

  respond_to do |format|
    # Refresh the cart in place — replace #cart-body, no navigation. See
    # doc/tasks/202605212322_CART_TURBO_STREAMS.md.
    format.turbo_stream do
      @cart.line_items.reload
      check_material_alerts
      render partial: 'cart_streams'
    end
    format.html do
      redirect_to my_cart_path
    end
    format.json do
      render json: cart_array
    end
  end
end

#apply_couponvoid

This method returns an undefined value.

Applies params[:coupon_code] to the cart and refreshes either the
checkout coupon box or the whole cart depending on coupon_context.



762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
# File 'app/controllers/my_carts_controller.rb', line 762

def apply_coupon
  if params[:coupon_code].present?
    error_message = add_coupon(params[:coupon_code])
    if error_message
      flash[:error] = error_message
    else
      # A valid coupon that applied silently (e.g. capped out by an existing
      # discount) gave no feedback at all before — confirm it landed.
      flash[:info] = "Coupon #{params[:coupon_code]} applied."
    end
  end

  respond_to do |format|
    # TurboStreamFlashable appends the flash message to the stream response.
    format.turbo_stream do
      if params[:coupon_context] == 'checkout'
        # Checkout sidebar: refresh just the coupon-entry box (existing behavior).
        render turbo_stream: turbo_stream.update('cart_discounts',
                                                 partial: 'coupon_entry',
                                                 locals: { coupon_context: 'checkout' })
      else
        # Cart page: refresh the whole cart so the savings panel and totals
        # reflect the coupon — not just the coupon-entry box.
        check_material_alerts
        render partial: 'cart_streams'
      end
    end
    format.html { redirect_back_or_to(my_cart_path) }
  end
end

#cart_add_item(sku, qty = nil, room_configuration_id = nil) ⇒ void (protected)

This method returns an undefined value.

Adds a single SKU to the cart, recording recently added items and flashing
an error when the SKU is not in the catalog.

Parameters:

  • sku (String)

    SKU to add

  • qty (Integer, nil) (defaults to: nil)

    quantity to add

  • room_configuration_id (Integer, nil) (defaults to: nil)

    room to add the item to



799
800
801
802
803
804
805
806
807
808
809
# File 'app/controllers/my_carts_controller.rb', line 799

protected def cart_add_item(sku, qty = nil, room_configuration_id = nil)
  # add item
  return if sku.blank?

  @recently_added_items ||= []
  begin
    @recently_added_items = @cart.add_multiple_items([{ sku:, qty:, room_configuration_id: }])
  rescue Order::ItemNotFound
    flash[:error] = "We could not find sku #{sku} in catalog."
  end
end

#cart_conflictObject

Display cart conflict resolution page when user logs in with items in both guest and account carts



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'app/controllers/my_carts_controller.rb', line 392

def cart_conflict
  @guest_cart_id = session[:cart_conflict_guest_cart_id]
  @return_path = params[:return_path] || session[:cart_conflict_return_path] || customer_info_my_cart_path
  session[:cart_conflict_return_path] = @return_path

  prepare_cart_conflict_state
  return if performed?

  @resolver = Cart::ConflictResolver.new(account_cart: @account_cart, guest_cart: @guest_cart)

  if @resolver.conflict?
    @conflict_summary = @resolver.conflict_summary
    render 'my_carts/cart_conflict'
  else
    auto_resolve_cart_conflict
  end
end

#check_address_and_localevoid

This method returns an undefined value.

Switches the visitor's catalog when the requested locale's country differs
from the address country, warning about cart items dropped by the switch.



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'app/controllers/my_carts_controller.rb', line 224

def check_address_and_locale
  if params[:locale].downcase.include?(params[:country])
    head :ok
  else
    logger.info "[set_locale] -> check_address_and_locale wants to assign new catalog, guest user locale: #{@context_user.locale}, requested locale #{I18n.locale}"
    new_catalog = params[:country] == 'ca' ? Catalog.find(2) : Catalog.find(1)
    goods_before_switch = cart_goods_labels # snapshot before fix_catalog drops items with no equivalent
    result = @context_user.change_catalog(new_catalog) || {}
    if result.catalog_assigned?
      logger.info "[set_locale] -> check_address_and_locale, assigned new catalog #{new_catalog.id}"
      @context_user.reload
      flash_dropped_cart_items(goods_before_switch, params[:country] == 'ca' ? 'Canada' : 'the United States')
      locale = params[:country] == 'ca' ? :'en-CA' : :'en-US'
      redirect_url = cms_link(URI(request.referer).path, locale, host: WEB_HOSTNAME, scheme: 'https')
      render json: { redirect_url: redirect_url }
    else
      logger.error "[set_locale] -> check_address_and_locale, assigned new catalog #{new_catalog.id} failed #{result.messages.to_sentence}"
      render json: { error: result.messages.to_sentence }, status: :unprocessable_content
    end
  end
end

#check_existing_accountvoid

This method returns an undefined value.

JSON probe returning 200 when the given email already belongs to an
account (used to prompt guests to sign in during checkout).



747
748
749
750
751
752
753
754
755
756
757
# File 'app/controllers/my_carts_controller.rb', line 747

def 
  email = params[:email]
  return nil if fully_logged_in?
  return nil if email.blank?

  return unless Account.where(email:).any?

  respond_to do |format|
    format.json { head :ok }
  end
end

#check_inventory_levelsBoolean

Caps B-stock line-item quantities at available inventory, flashing a
warning when any quantity was reduced.

Returns:

  • (Boolean)

    true when any item was below the requested inventory



723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
# File 'app/controllers/my_carts_controller.rb', line 723

def check_inventory_levels
  # Only check inventory levels of b-stock
  return false if @cart.line_items.bstock.blank?

  @items_in_cart = @cart.line_items.bstock.parents_only.non_shipping
  items_below_inventory = false
  @items_in_cart.each do |li|
    available_stock = li&.catalog_item&.qty_available || 0
    if li.quantity <= available_stock
      # All good, we have inventory
    else
      ErrorReporting.error("Not enough inventory for an item in the cart. Item #{li.sku}. Qty added #{li.quantity}. Qty available #{available_stock}")
      # We update the cart to max out the available inventory
      li.update(quantity: available_stock)
      flash[:warning] = "Unfortunately we don't have enough stock available for the product '<i>#{li.name}</i>'. We updated the quantity in your cart to reflect the maximun stock available.".html_safe
      items_below_inventory = true
    end
  end
  items_below_inventory
end

#checkoutvoid

This method returns an undefined value.

Entry point of the checkout flow: routes to the cart, service scheduling,
or customer-info step depending on cart state and inventory levels.



378
379
380
381
382
383
384
385
386
387
388
389
# File 'app/controllers/my_carts_controller.rb', line 378

def checkout
  if @cart.empty?
    redirect_to my_cart_path
  elsif check_inventory_levels
    flash.keep(:warning)
    redirect_to my_cart_path
  elsif @cart.includes_schedulable_service?
    redirect_to schedule_service_my_cart_path
  else
    redirect_to customer_info_my_cart_path
  end
end

#checkout_updatevoid

This method returns an undefined value.

Saves the checkout form (with spam checking) and proceeds to the shipping
step, re-rendering customer-info with errors on failure.



609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
# File 'app/controllers/my_carts_controller.rb', line 609

def checkout_update
  params[:checkout_form] ||= {}
  params[:checkout_form][:order_id] = @cart.id
  address_option = params['customer-addresses']
  params[:checkout_form][:shipping_address][:recipient] = "#{params[:shipping_first_name]} #{params[:shipping_last_name]}" if params[:checkout_form][:shipping_address].present?
  params[:checkout_form][:billing_address][:recipient] = params[:checkout_form][:contact_name].to_s
  params[:checkout_form][:address_id] = address_option

  @checkout_form = Checkout::CheckoutForm.new(params[:checkout_form])
  spam_checker = SpamCheck.new
  is_spam = spam_checker.process(request:,
                                 author_email: @checkout_form.email,
                                 phone_number: @checkout_form.phone)
  success = !is_spam && @checkout_form.save
  if is_spam
    # We don't want to say why its spam
    @checkout_form.errors.add(:checkout, ": An unknown error occurred, please contact us to complete your order.  Reference cart number SC#{@cart.id}")
    @cart.quick_note spam_checker.spam_results_to_s
  end
  logger.info "Saving cart #{@cart.id}, is_spam: #{is_spam}, cart save: #{success}, errors are #{@checkout_form.errors_to_s}"
  initialize_address_book # Make sure all addresses are set up
  if success
    @cart = @cart.reload
    track_checkout_started
    begin
      logger.info 'Cart successfully saved.'
      if @cart.shipping_address.nil?
        @cart.update(shipping_address_id: @addresses.first.id)
        @cart.save
      end
      @cart = @cart.reload
      redirect_to shipping_my_cart_path
    rescue StandardError => e
      ErrorReporting.error(e)
      redirect_to shipping_my_cart_path
    end
  else
    @cancel_path = checkout_my_cart_path
    @new_address = true if address_option.present? && address_option == 'new'
    msg = "Error during checkout with cart #{@cart.id}. The form could not be saved. is_spam: #{is_spam}. #{@checkout_form.errors_to_s}. Params sent by the customer are: #{params[:checkout_form]}"
    ErrorReporting.warning(msg)
    logger.warn msg
    # Force HTML format - Turbo Drive handles 422 HTML responses by replacing page content
    render 'my_carts/new_checkout/customer_info', status: :unprocessable_content, formats: [:html]
  end
end

#customer_infovoid

This method returns an undefined value.

Renders the customer-info step of checkout.



443
444
445
446
447
448
449
450
451
452
453
# File 'app/controllers/my_carts_controller.rb', line 443

def customer_info
  if @cart.empty?
    redirect_to my_cart_path
  else
    logger.info "Checkout for cart #{@cart.id}"
    @show_tab = params[:show_tab]
    @material_alerts = @cart.get_material_alerts(for_www: true, for_cart: true)
    setup_checkout
    render 'my_carts/new_checkout/customer_info'
  end
end

#paymentvoid

This method returns an undefined value.

Renders the payment step of checkout, rebuilding the discount and payment
options from a freshly reloaded cart.



575
576
577
578
579
580
581
582
583
584
585
586
# File 'app/controllers/my_carts_controller.rb', line 575

def payment
  if @cart.empty?
    redirect_to my_cart_path
  else
    @cart.reload.reset_discount # Temporary fix while we fix the underlying issues with the total amount not being correct
    @cart.reload # Reload after reset_discount to pick up DB-calculated total
    @hide_shipping = false
    @payment_options = @cart.payment_options('cart')
    @payment = @cart.payments.build(category: 'Credit Card')
    render 'my_carts/new_checkout/payment'
  end
end

#remove_all_line_itemsvoid

This method returns an undefined value.

Empties the cart entirely.



295
296
297
298
299
# File 'app/controllers/my_carts_controller.rb', line 295

def remove_all_line_items
  @cart.reset_cart
  flash[:info] = 'Your shopping cart was emptied.'
  redirect_to_return_path_or_default my_cart_path
end

#remove_couponvoid

This method returns an undefined value.

Removes a customer-applied coupon (and its discount) from the cart.



687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
# File 'app/controllers/my_carts_controller.rb', line 687

def remove_coupon
  coupon_code = params[:coupon_code]
  coupon = @cart.customer_applied_coupons.where(code: coupon_code).first
  discount = nil
  discount = @cart.discounts.where(coupon_id: coupon.id).first if coupon.present?
  if coupon && discount
    res = Coupon::DeleteDiscount.new.perform(discount)
    if res.success?
      flash[:info] = "Coupon code #{coupon_code} removed."
    else
      flash[:error] ||= ''
      flash[:error] += res.errors
    end
  else
    flash[:error] ||= ''
    flash[:error] += "No customer applied coupons with code #{coupon_code} found!"
  end
  respond_to do |format|
    format.html { redirect_back_or_to my_cart_path }
  end
end

#remove_fromvoid

This method returns an undefined value.

Placeholder action for removing an item from a specific container.



683
# File 'app/controllers/my_carts_controller.rb', line 683

def remove_from; end

#remove_itemvoid

This method returns an undefined value.

Removes a line item (and its related SmartInstall service lines) from the
cart, refreshing the cart in place.



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'app/controllers/my_carts_controller.rb', line 249

def remove_item
  line_item = LineItem.find_by(id: params[:line_item_id].to_i)
  if line_item
    if line_item.is_smartinstall_service?
      @cart.line_items.select { |li| li.id == line_item.id || li.is_related_smart_service?(line_item.sku) }.each(&:destroy)
    else
      @cart.line_items.select { |li| li.id == params[:line_item_id].to_i }.each(&:destroy)
    end
    @cart.set_for_recalc
    begin
      @cart.save!
    rescue StandardError => e
      ErrorReporting.error(e)
    end
    @cart.line_items.reload
  else
    ErrorReporting.warning("Line item #{params[:line_item_id]} not found in cart #{@cart.id}")
    flash[:error] = 'We could not remove the requested line item from your cart.'
  end
  respond_to do |format|
    # In-place cart refresh — replace #cart-body (handles the empty-cart
    # transition when the last item is removed).
    format.turbo_stream do
      check_material_alerts
      render partial: 'cart_streams'
    end
    format.html { redirect_to my_cart_path }
    format.json { render json: cart_array }
  end
end

#remove_roomvoid

This method returns an undefined value.

Removes a room configuration (and its line items) from the cart.



282
283
284
285
286
287
288
289
290
291
# File 'app/controllers/my_carts_controller.rb', line 282

def remove_room
  @room = RoomConfiguration.find(params[:room_id])
  res_hash = @cart.remove_room_configuration(@room)
  if res_hash[:status]
    flash[:info] = res_hash[:message]
  else
    flash[:error] = res_hash[:message]
  end
  redirect_to_return_path_or_default my_cart_path
end

#resolve_cart_conflictObject

Handle user's choice for cart conflict resolution



411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'app/controllers/my_carts_controller.rb', line 411

def resolve_cart_conflict
  strategy = params[:strategy]&.to_sym
  @guest_cart_id = session[:cart_conflict_guest_cart_id]
  @return_path = session[:cart_conflict_return_path] || customer_info_my_cart_path

  unless @guest_cart_id && strategy
    flash[:error] = 'Unable to resolve cart conflict. Please try again.'
    redirect_to my_cart_path
    return
  end

  @guest_cart = Order.find_by(id: @guest_cart_id)
  @account_cart = @cart

  resolver = Cart::ConflictResolver.new(account_cart: @account_cart, guest_cart: @guest_cart)
  result = resolver.resolve(strategy:)

  if result[:success]
    clear_cart_conflict_session
    flash[:success] = cart_conflict_success_message(result[:strategy])
    redirect_to @return_path
  else
    # Preserve session[:cart_conflict_guest_cart_id] / :cart_conflict_return_path
    # so the customer lands back on the merge UI and can retry a different
    # strategy instead of losing their guest cart silently.
    flash[:error] = 'Unable to resolve cart conflict. Please try again.'
    redirect_to cart_conflict_my_cart_path
  end
end

#retrievevoid

This method returns an undefined value.

Cart-recovery entry point: attempts magic-link sign-in in a before action,
then redirects to the cart (preserving any referral code).



39
40
41
42
# File 'app/controllers/my_carts_controller.rb', line 39

def retrieve
  # redirect after trying to authenticate token in before action
  redirect_to my_cart_path(referral_code: params[:referral_code])
end

#schedule_servicevoid

This method returns an undefined value.

Renders the service-scheduling step of checkout.



457
458
459
460
461
462
463
464
# File 'app/controllers/my_carts_controller.rb', line 457

def schedule_service
  if @cart.empty?
    redirect_to my_cart_path
  else
    @service_form = Checkout::ServiceForm.new_from_cart(@cart)
    render 'my_carts/new_checkout/schedule_service'
  end
end

#schedule_service_updatevoid

This method returns an undefined value.

Saves the service-scheduling form and proceeds to the customer-info step,
re-rendering the form with errors on failure.



591
592
593
594
595
596
597
598
599
600
601
602
603
604
# File 'app/controllers/my_carts_controller.rb', line 591

def schedule_service_update
  params[:service_form][:order_id] = @cart.id
  @service_form = Checkout::ServiceForm.new(params[:service_form])
  success = @service_form.save
  if success
    redirect_to customer_info_my_cart_path
  else
    @cancel_path = schedule_service_my_cart_path
    ErrorReporting.error("Error while scheduling service with cart #{@cart.id}. The form could not be saved. #{@service_form.errors_to_s}. Params sent by the customer are: #{params[:service_form]}")
    logger.info "Error while scheduling service with cart #{@cart.id}. The form could not be saved. #{@service_form.errors_to_s}."
    # Force HTML format - Turbo Drive handles 422 HTML responses by replacing page content
    render 'my_carts/new_checkout/schedule_service', status: :unprocessable_content, formats: [:html]
  end
end

#set_cart_qtyvoid

This method returns an undefined value.

Stores the current cart id and item count in the session for the navbar
cart badge, and loads material alerts.



712
713
714
715
716
717
718
# File 'app/controllers/my_carts_controller.rb', line 712

def set_cart_qty
  @material_alerts = @cart.get_material_alerts(for_www: true, for_cart: true)
  @items_in_cart = @cart.line_items.parents_only.non_shipping.sum(:quantity)
  @items_in_cart ||= 0
  session[:cart_id_number] = @cart&.id
  session[:cart_qty] = @items_in_cart
end

#shippingvoid

This method returns an undefined value.

Renders the shipping step of checkout, building the per-delivery shipping
options hash and auto-selecting economy/freight options where applicable.



469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
# File 'app/controllers/my_carts_controller.rb', line 469

def shipping
  if @cart.empty?
    redirect_to my_cart_path
  else
    unless @cart.is_warehouse_pickup?
      @cart.retrieve_shipping_costs if @cart.deliveries.empty? || @cart.deliveries.nil?
      @deliveries = {}
      @hide_shipping = false

      @cart.need_to_recalculate_shipping
      @cart.reload.deliveries.each_with_index do |delivery, index|
        shipping_costs_hash = delivery&.sorted_shipping_costs_www_hash
        economy_shipping_costs = shipping_costs_hash.fetch(:economy, [])
        ground_shipping_costs = shipping_costs_hash.fetch(:ground, [])
        faster_shipping_costs = shipping_costs_hash.fetch(:faster, [])
        freight_shipping_costs = shipping_costs_hash.fetch(:freight, [])
        @deliveries[:"delivery_#{index}"] = {
          ships_from: delivery.ships_from_text,
          delivery_id: delivery.id,
          economy_shipping_costs:,
          ground_shipping_costs:,
          faster_shipping_costs:,
          freight_shipping_costs:,
          selected_shipping_cost_id: delivery.selected_shipping_cost_id
        }
      end
      @has_ground_shipping_options = begin
        @deliveries.map { |a| a[1][:ground_shipping_costs].present? }.uniq.include?(true)
      rescue StandardError
        false
      end
      @found_check = false

      # Ensure ships_economy flag reflects the current selection when a selection already exists
      # This guarantees the FREE_ECONOMY_ONLINE coupon logic runs without requiring a new click
      @cart.deliveries.each do |dq|
        next if dq.selected_shipping_cost_id.blank?

        # CRITICAL: Ensure shipping line item exists BEFORE updating economy status
        # The FREE_ECONOMY_ONLINE coupon needs a shipping line to discount.
        # Without this, reset_discount calculates $0 discount (no shipping line) and deletes it.
        if dq.selected_shipping_cost && dq.line_items.none?(&:is_shipping?)
          dq.apply_selected_shipping_cost!(dq.selected_shipping_cost, persist: true)
          dq.reload
        end

        should_be_economy = dq.shipping_option&.is_override? && !dq.ships_ltl_freight?
        next unless @cart.respond_to?(:ships_economy) && (@cart.state == 'cart')

        if should_be_economy && !@cart.ships_economy
          Shipping::UpdateShippingMethod.new.update_economy_status(@cart, true)
        elsif !should_be_economy && @cart.ships_economy
          Shipping::UpdateShippingMethod.new.update_economy_status(@cart, false)
        end

        # Auto-select shipping options only if none are selected yet
        next if dq.selected_shipping_cost_id.present?

        shipping_costs_hash = dq.sorted_shipping_costs_www_hash

        if !dq.shipping_option&.is_override? || dq.ships_ltl_freight?
          # Freight orders: disable economy and select freight option
          Shipping::UpdateShippingMethod.new.update_economy_status(@cart, false)
          freight_shipping_costs = shipping_costs_hash.fetch(:freight, [])
          dq.selected_shipping_cost_id = freight_shipping_costs&.first&.id
          dq.save
        elsif !@cart.should_ship_freight?
          # Non-freight orders: customer qualifies for economy shipping
          # Auto-select first economy option and apply free shipping discount
          Shipping::UpdateShippingMethod.new.update_economy_status(@cart, true)
          economy_shipping_costs = shipping_costs_hash.fetch(:economy, [])
          if economy_shipping_costs.any?
            first_economy = economy_shipping_costs.first
            result = Shipping::UpdateShippingMethod.new.update_from_shipping_cost(dq, first_economy, @cart)
            Rails.logger.info "Auto-selected economy shipping: #{result.success? ? 'success' : result.errors}"
          end
        end
      end

      # Reload cart and deliveries data structure after auto-selection so sidebar totals reflect discounts
      @cart.reload
      @deliveries = {}
      @cart.deliveries.each_with_index do |delivery, index|
        shipping_costs_hash = delivery&.sorted_shipping_costs_www_hash
        economy_shipping_costs = shipping_costs_hash.fetch(:economy, [])
        ground_shipping_costs = shipping_costs_hash.fetch(:ground, [])
        faster_shipping_costs = shipping_costs_hash.fetch(:faster, [])
        freight_shipping_costs = shipping_costs_hash.fetch(:freight, [])
        @deliveries[:"delivery_#{index}"] = {
          ships_from: delivery.ships_from_text,
          delivery_id: delivery.id,
          economy_shipping_costs:,
          ground_shipping_costs:,
          faster_shipping_costs:,
          freight_shipping_costs:,
          selected_shipping_cost_id: delivery.selected_shipping_cost_id
        }
      end
    end
    render 'my_carts/new_checkout/shipping'
  end
end

#shipping_addressvoid

This method returns an undefined value.

Renders the shipping-address selection, filtered to the store's country
with the warehouse address appended.



659
660
661
662
663
# File 'app/controllers/my_carts_controller.rb', line 659

def shipping_address
  @addresses = @addresses.select { |a| a.country_iso3 == @cart.store.country_iso3 }.sort_by { |a| a.default_shipping ? -10 : 0 }
  @addresses << @cart.store.warehouse_address
  @hide_new_address_form = true
end

#shipping_updatevoid

This method returns an undefined value.

Placeholder action for shipping updates.



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

def shipping_update; end

#showvoid

This method returns an undefined value.

Renders the cart page, stamping first_cart_view_date on first view and
surfacing quote/room load errors when arriving from a quote link.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/controllers/my_carts_controller.rb', line 47

def show
  @cart.first_cart_view_date ||= Time.current
  @cart.save
  flash.keep(:warning)
  if params[:quote_id].present?
    msg = nil
    @quote = (begin
      @context_user.customer.quotes.find(params[:quote_id])
    rescue StandardError
      nil
    end)
    msg = 'Could not find quote. Your session may have expired.' if @quote.nil?
    if @quote.present? && params[:room_id].present?
      @room = (begin
        @quote.room_configurations.find(params[:room_id])
      rescue StandardError
        nil
      end)
      msg = 'Could not find room. Your session may have expired.' if @room.nil?
    end
    flash[:info] = msg if msg.present?
  end
  check_material_alerts
  respond_to do |format|
    format.html
    format.json do
      render json: cart_array
    end
  end
end

#updatevoid

This method returns an undefined value.

Updates cart quantities, shipping selections, and coupon codes from the
cart form, then either refreshes the cart in place or proceeds to
checkout depending on the submitted button.



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'app/controllers/my_carts_controller.rb', line 348

def update
  update_cart
  @cart.save
  @cart.prune_cart_rooms
  set_cart_qty
  @redirect_to_checkout = /CheckOut|Coupon/i.match?(params['commit'])
  # When not navigating on to checkout, update.turbo_stream.erb refreshes
  # #cart-body in place — needs fresh line items + recommendations.
  unless @redirect_to_checkout
    @cart.line_items.reload
    check_material_alerts
  end
  respond_to do |format|
    format.turbo_stream
    format.html do
      if @redirect_to_checkout
        redirect_to checkout_my_cart_path
      else
        redirect_to my_cart_path
      end
    end
    format.json do
      render json: cart_array
    end
  end
end

#update_from_paymentvoid

This method returns an undefined value.

Applies the shipping selection submitted from the payment step via
Shipping::UpdateShippingMethod.



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'app/controllers/my_carts_controller.rb', line 304

def update_from_payment
  if params.dig(:cart, :shipping_cost_id).blank?
    respond_to do |format|
      format.turbo_stream { head :unprocessable_content }
      format.html { redirect_to my_cart_path }
    end
    return
  end

  # Use service to handle shipping selection
  # Pass params directly - the service will extract what it needs
  result = Shipping::UpdateShippingMethod.new.update_from_params(@cart, params)

  unless result.success?
    Rails.logger.error "Failed to update shipping: #{result.errors}"
    respond_to do |format|
      format.turbo_stream { head :unprocessable_content }
      format.html { redirect_to my_cart_path, alert: result.errors }
    end
    return
  end

  result.messages.each { |msg| Rails.logger.info msg }
  @cart = @cart.reload

  if @cart.deliveries.empty?
    Rails.logger.error "update_from_payment: cart #{@cart.id} has no deliveries after shipping update"
    respond_to do |format|
      format.turbo_stream { head :unprocessable_content }
      format.html { redirect_to shipping_my_cart_path }
    end
    return
  end

  respond_to do |format|
    format.turbo_stream
    format.html { redirect_to my_cart_path }
  end
end

#update_shipping_addressvoid

This method returns an undefined value.

Saves the shipping address chosen or entered on the shipping step.



671
672
673
# File 'app/controllers/my_carts_controller.rb', line 671

def update_shipping_address
  update_method_for_cart
end

#update_shipping_address_from_modalvoid

This method returns an undefined value.

Saves the shipping address submitted from the address modal.



677
678
679
# File 'app/controllers/my_carts_controller.rb', line 677

def update_shipping_address_from_modal
  update_method_for_cart
end