Class: MyQuotesController
- Inherits:
-
BasePortalController
- Object
- ActionController::Base
- ApplicationController
- BasePortalController
- MyQuotesController
- 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
-
#add_to_cart ⇒ void
Converts the quote (or one of its rooms) into the customer's cart order.
-
#buy_now ⇒ void
Starts checkout for a transmitted quote via its public payment link.
-
#cancel ⇒ void
Cancels the quote when its state allows customer cancellation.
-
#download_pdf ⇒ Object
(also: #generate_pdf)
Download the quote PDF.
-
#find_quote ⇒ void
Finds a quote by reference number or encrypted id and redirects to it.
-
#finish_move ⇒ void
Persists the quote's move to the selected project.
-
#index ⇒ void
Lists the customer's active quotes, most recent first.
-
#invite_account ⇒ void
Sends an account invitation email for the party when not already signed in.
-
#move ⇒ void
Renders the project picker for moving the quote to another project.
-
#quote_lookup ⇒ void
Anonymous-friendly entry point that bounces a matched quote to its account page.
-
#quote_viewer ⇒ void
Customer-facing read-only quote page reached via the encrypted
/ql/<token>link. -
#show ⇒ void
Shows a single quote to its owning customer.
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
Methods included from Controllers::SubdomainDetection
#is_crm_request?, #is_www_request?, #json_request?
Methods included from Controllers::TurboSafeRedirect
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_cart ⇒ void
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 my_account_url end |
#buy_now ⇒ void
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 my_account_url 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 |
#cancel ⇒ void
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 my_account_url end |
#download_pdf ⇒ Object Also known as: generate_pdf
Download the quote PDF. Resolves the quote two ways:
params[:token](anonymous /ql//download_pdf path) — the
encrypted token is the capability. Same visibility rules as the
/ql/ viewer: quote must be incompletestate.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]) : my_account_quote_path(@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_quote ⇒ void
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_move ⇒ void
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 my_account_url end |
#index ⇒ void
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_account ⇒ void
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 invite_account return if account_signed_in? email = params[:email] party = Party.find(params[:party_id]) login = email if email.present? && party.present? if Account.where(login:).exists? name_part, = email.split('@') base_login = name_part counter = 0 login = base_login while Account.where(login:).exists? counter += 1 login = "#{base_login}-#{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 |
#move ⇒ void
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 my_account_url end |
#quote_lookup ⇒ void
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.account.present? && @context_user.account == @customer.account redirect_to my_account_quote_path(@quote.id) end |
#quote_viewer ⇒ void
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 |
#show ⇒ void
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 |