Class: MyQuotesController

Inherits:
BasePortalController show all
Defined in:
app/controllers/my_quotes_controller.rb

Overview

Controller: my quotes.

Constant Summary

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 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_to_cartvoid

This method returns an undefined value.

Converts the quote (or one of its rooms) into the customer's cart order.



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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'app/controllers/my_quotes_controller.rb', line 90

def add_to_cart
  @quote = @context_user.customer.quotes.find(params[:id])
  @room = @quote.room_configurations.find(params[:room_id]) if params[:room_id].present?
  @cart = @context_user.cart
  if (@cart.quote_id == @quote.id) && @cart.line_items.non_shipping.present?
    flash[:warning] = 'This quote is already in your cart.'
    redirect_to_return_path_or_default my_cart_path
  else
    @project = @quote.opportunity
    # @quote.reset_shipping # shipping will be recalculated at checkout
    # @quote.do_not_detect_shipping = true
    # @cart.do_not_detect_shipping = true
    o = @quote.to_order(@cart)
    if o.persisted?
      cart_params = { quote_id: @quote.id }
      if @room
        msg = "Room #{@room.name_with_room}"
        cart_params[:room_id] = @room.id
      else
        msg = "Quote #{@quote.reference_number}"
      end
      flash[:info] = "#{msg} was added to your cart."
      redirect_to_return_path_or_default my_cart_path(cart_params)
    else
      flash[:error] = "Unable to add to cart. #{o.errors_to_s}"
      redirect_to_return_path_or_default my_quote_path(@quote)
    end
  end
rescue ActiveRecord::RecordNotUnique
  @cart&.reload
  if @cart&.quote_id == @quote.id && @cart.line_items.non_shipping.present?
    flash[:warning] = 'This quote is already in your cart.'
    redirect_to_return_path_or_default my_cart_path
  else
    flash[:error] = 'Unable to add to cart. Please try again.'
    redirect_to_return_path_or_default my_quote_path(@quote)
  end
rescue Quote::ConvertToOrder::QuoteUnpurchasable => e
  handle_unpurchasable_quote(e)
rescue ActiveRecord::RecordNotFound
  flash[:error] = 'Quote not found or not authorized'
  redirect_to 
end

#buy_nowvoid

This method returns an undefined value.

Starts checkout for a transmitted quote via its public payment link.



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
# File 'app/controllers/my_quotes_controller.rb', line 137

def buy_now
  # Open to anyone with the link — the pay-order page (public_payment_link)
  # needs no login, and ConvertToOrderService creates the order under the
  # quote's own customer regardless of who clicks. So we look the quote up
  # by id directly rather than scoping to the signed-in customer, gating on
  # `complete?` so only transmitted quotes are buyable.
  @quote = Quote.find_by(id: params[:id])
  unless @quote&.complete?
    flash[:error] = 'Quote not found or not available for purchase'
    return redirect_to 
  end

  # Try to find an existing order for that quote that hasn't been modified by checking the line items signature
  o = @quote.orders.order(Order[:created_at].desc).find_by(state: %w[pending cart pending_payment])
  if o && o.line_items.signature != @quote.line_items.signature
    # if signature don't match delete it and start over
    o.destroy
    o = nil
  end
  o ||= @quote.to_order
  record_ql_event('ql.checkout_started', order_id: o.id, total: @quote.total.to_s)
  url = o.public_payment_link
  redirect_to_return_path_or_default url
rescue Quote::ConvertToOrder::QuoteUnpurchasable => e
  handle_unpurchasable_quote(e)
end

#cancelvoid

This method returns an undefined value.

Cancels the quote when its state allows customer cancellation.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'app/controllers/my_quotes_controller.rb', line 167

def cancel
  @quote = @context_user.customer.quotes.find(params[:id])
  @project = @quote.opportunity
  if @quote.ok_to_customer_cancel?
    @quote.do_not_detect_shipping = true
    @quote.cancel
    flash[:info] = "Quote #{@quote.name} was cancelled"
    redirect_to_return_path_or_default my_project_path(@project)
  else
    flash[:error] = 'Quote cannot be cancelled because it contains active rooms or orders'
    redirect_to_return_path_or_default my_quote_path(@quote)
  end
rescue ActiveRecord::RecordNotFound
  flash[:error] = 'Quote not found or not authorized'
  redirect_to 
end

#download_pdfObject Also known as: generate_pdf

Download the quote PDF. Resolves the quote two ways:

  1. params[:token] (anonymous /ql//download_pdf path) — the
    encrypted token is the capability. Same visibility rules as the
    /ql/ viewer: quote must be in complete state.
  2. params[:id] (signed-in /my_account/quotes/:id/download_pdf path)
    scoped to the customer's own quotes.

If a PDF already exists in organization_quote_pdf uploads, serve
it immediately. If none exists, kick off GenerateQuoteWorker and
bounce the visitor back to the /ql/ page with a "preparing" notice
— by the time they refresh, the worker has dropped a fresh PDF
into the Files section.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'app/controllers/my_quotes_controller.rb', line 67

def download_pdf
  @quote = resolve_quote_for_pdf
  return unless @quote

  pdf = @quote.uploads.in_category('organization_quote_pdf').order(created_at: :desc).first

  if pdf
    record_ql_event('ql.pdf_downloaded', kind: 'quote_pdf', upload_id: pdf.id)
    send_upload_accelerated(pdf, file_name: "WarmlyYours_Quote_#{@quote.reference_number}.pdf")
  else
    queue_pdf_generation(@quote)
    record_ql_event('ql.pdf_requested')
    back_to = params[:token].present? ?
                short_quote_lookup_path(token: params[:token]) :
                (@quote)
    redirect_to back_to,
                notice: "We're preparing your PDF — it'll show up in the Files section in about a minute. Refresh the page to grab it."
  end
end

#find_quotevoid

This method returns an undefined value.

Finds a quote by reference number or encrypted id and redirects to it.



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

def find_quote
  @quote = Quote.find_by(reference_number: params[:quote_number])
  if @quote.present?
    redirect_to quote_lookup_path(quote_id: @quote.id)
  else
    # Let's see if we got an encrypted ID
    quote_id = begin
      Encryption.decrypt_string(params[:quote_number].to_s)
    rescue StandardError
      nil
    end
    if quote_id.present? && (@quote = Quote.find(quote_id))
      redirect_to "/quote-viewer/#{params[:quote_number]}"
    else
      flash.now[:info] = "We couldn't find the quote"
      render :quote_lookup, status: :unprocessable_content
    end
  end
end

#finish_movevoid

This method returns an undefined value.

Persists the quote's move to the selected project.



199
200
201
202
203
204
205
206
207
208
209
210
# File 'app/controllers/my_quotes_controller.rb', line 199

def finish_move
  @quote = @context_user.customer.quotes.find(params[:id])
  if @quote.update(params[:quote])
    flash[:info] = "Quote was moved to #{@quote.opportunity.name}."
  else
    flash[:error] = 'Unable to move quote.'
  end
  redirect_to_return_path_or_default my_quote_path(@quote)
rescue ActiveRecord::RecordNotFound
  flash[:error] = 'Quote not found or not authorized'
  redirect_to 
end

#indexvoid

This method returns an undefined value.

Lists the customer's active quotes, most recent first.



26
27
28
29
30
# File 'app/controllers/my_quotes_controller.rb', line 26

def index
  base_quotes = (@context_user || @party).quotes.active.last_revisions
  @q = base_quotes.ransack(params[:q])
  @pagy, @quotes = pagy(@q.result.order(created_at: :desc))
end

#invite_accountvoid

This method returns an undefined value.

Sends an account invitation email for the party when not already signed in.



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'app/controllers/my_quotes_controller.rb', line 323

def 
  return if 

  email = params[:email]
  party = Party.find(params[:party_id])
   = email

  if email.present? && party.present?
    if Account.where(login:).exists?
      name_part, = email.split('@')
       = name_part
      counter = 0
       = 
      while Account.where(login:).exists?
        counter += 1
         = "#{}-#{counter}"
      end
    end
    Account::Inviter.new.process(party:, email:, login:)
    flash[:info] = 'We sent you an email with an invitation to create an account. Please follow the instructions of the email.'
  end
  redirect_to quote_lookup_path
end

#movevoid

This method returns an undefined value.

Renders the project picker for moving the quote to another project.



187
188
189
190
191
192
193
194
# File 'app/controllers/my_quotes_controller.rb', line 187

def move
  @quote = @context_user.customer.quotes.find(params[:id])
  @project = @quote.opportunity
  @move_to_project_options = (@context_user || @party).opportunities.not_cancelled.to_a.push(@project).uniq.map { |p| [p.name, p.id] }
rescue ActiveRecord::RecordNotFound
  flash[:error] = 'Quote not found or not authorized'
  redirect_to 
end

#quote_lookupvoid

This method returns an undefined value.

Anonymous-friendly entry point that bounces a matched quote to its account page.



215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'app/controllers/my_quotes_controller.rb', line 215

def quote_lookup
  @quote = begin
    Quote.find(params[:quote_id])
  rescue StandardError
    nil
  end
  return if @quote.blank?

  @customer = @quote.customer
  return unless @customer..present? && @context_user. == @customer.

  redirect_to (@quote.id)
end

#quote_viewervoid

This method returns an undefined value.

Customer-facing read-only quote page reached via the encrypted /ql/<token> link.



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'app/controllers/my_quotes_controller.rb', line 255

def quote_viewer
  # State guard. `/ql/<token>` is the customer-facing post-transmit
  # view of a quote — visible only after the rep has actually sent
  # the quote (state transitions to `complete` the moment the
  # transmission Communication is queued; see
  # Quote#post_communication_queued_hook). Earlier states are still
  # rep-in-progress; `cancelled` quotes are closed; `complete` quotes
  # with an active linked order swap in the "already ordered" page so
  # the customer is steered to the order detail screen instead of a
  # quote that's effectively read-only post-purchase. Each blocked
  # state gets a friendly page with the rep's name + email — never
  # raise / 404, since customers bookmark these links and a hard
  # error reads as broken-site to them.
  @rep = @quote.sender

  # `rep_preview` is a signed, 1h token minted only from the CRM's
  # select_pdf_template "Customer link" / "Preview as customer" controls
  # (Quote::RepPreviewTokenService). It lets a rep see the read-only /ql/
  # page while the quote is still pending/in-progress, bypassing the
  # state gate below. Never generated for an actual customer-facing send.
  if rep_previewing?
    @project = @quote.opportunity
    @material_alerts = @quote.get_material_alerts(for_www: true) unless @quote.editing_locked?
    @can_manage_cart = false
    @restricted_view = true
    render :show
    return
  end

  case @quote.state
  when 'complete'
    # Only swap to the "already ordered" view once the customer has
    # actually completed payment. While they're still in checkout — Buy Now
    # leaves a `pending` order, Add to Cart a `cart` one, a half-finished
    # card entry a `pending_payment` one — they must be able to reopen the
    # quote and resume as many times as they like (buy_now/add_to_cart reuse
    # the existing order, so this never double-books). See Quote#placed_order.
    @active_order = @quote.placed_order
    if @active_order
      render 'my_quotes/blocked/already_ordered', status: :ok
    else
      first_view = @quote.first_view_date.nil?
      @quote.update_column(:first_view_date, Time.current) if first_view
      record_ql_event('ql.viewed', first_view: first_view)
      @project = @quote.opportunity
      @material_alerts = @quote.get_material_alerts(for_www: true) unless @quote.editing_locked?
      # The /ql/<token> page is read-only for everyone — the quote is locked
      # once transmitted. Anyone (signed in or not) can Buy Now via the
      # pay-order page (no login needed; the order is created under the
      # quote's own customer). Only the quote's customer or one of its
      # contacts, signed in, can Add to cart / check out. They stay on the
      # full-width `/ql/<token>` layout (the email link is the source of
      # truth) rather than getting bumped to `/my_account/quotes/:id`.
      @can_manage_cart = !@context_user.try(:guest?) &&
                         @context_user&.customer&.quotes&.exists?(id: @quote.id)
      @restricted_view = !@can_manage_cart
      render :show
    end
  when 'cancelled'
    render 'my_quotes/blocked/cancelled', status: :ok
  else
    render 'my_quotes/blocked/not_ready', status: :ok
  end
end

#showvoid

This method returns an undefined value.

Shows a single quote to its owning customer.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'app/controllers/my_quotes_controller.rb', line 35

def show
  @quote = @context_user.customer.quotes.find(params[:id])
  @quote.update_column(:first_view_date, Time.current)
  @project = @quote.opportunity
  @restricted_view = @context_user.try(:guest?)
  # Only the signed-in owning customer (or one of its contacts) may add the
  # quote to a cart / check out. Buy Now is open to anyone via the pay-order
  # page, so it isn't gated by this.
  @can_manage_cart = !@restricted_view
  # For my_quotes, we ABSOLUTELY need share_key in the quote_builder_url -- Ramie
  share_key = params[:share_key].present? ? "?share_key=#{params[:share_key]}" : ''
  @quote_builder_url = cms_link("floor-heating/quote-builder#{share_key}")
  Rails.logger.debug { "MyQuotesController#show: @quote_builder_url: #{@quote_builder_url}" }
  @material_alerts = @quote.get_material_alerts(for_www: true) unless @quote.editing_locked?
rescue ActiveRecord::RecordNotFound
  flash[:error] = 'Quote not found or not authorized'
  redirect_to cms_link('/my_account')
end