Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions app/controllers/public_estimates_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# frozen_string_literal: true

class PublicEstimatesController < ApplicationController

skip_before_action :authenticate_user!

def show
@facilities = Facility.active.alphabetized
@facility = @facilities.find_by(id: params[:facility_id])
@products = @facility ? public_products : Product.none
@customer_type_options = customer_type_options
@price_group = PriceGroup.for_public_estimate(params[:customer_type] || "internal")
@priced_product_ids = priced_product_ids
@estimate = build_estimate if @price_group && requested_quantities.any?
@total = @estimate.estimate_details.sum { |estimate_detail| estimate_detail.cost || 0 } if @estimate
end

private

def customer_type_options
return [[t(".internal"), "internal"], [t(".external"), "external"]] if PriceGroup.secondary_external.blank?

[
[t(".internal"), "internal"],
[t(".external_for_profit"), "external"],
[t(".external_non_profit"), "external_non_profit"],
]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have this predefined to base and external and allow each school override it? what do you think?

end

def priced_product_ids
return [] if @price_group.blank? || @products.empty?

PricePolicy.current_for_date(Time.current).purchaseable
.where(product_id: @products.map(&:id), price_group: @price_group)
.distinct.pluck(:product_id)
end

def public_products
@facility.products.active.available_for_estimates.where.not(type: "Bundle").alphabetized
end

def requested_quantities
@requested_quantities ||=
params[:quantities].presence&.to_unsafe_h&.select { |_id, quantity| quantity.to_i.positive? } || {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use strong parameters instead of to_unsafe_h

end

def build_estimate
estimate = Estimate.new(facility: @facility, price_group: @price_group)

requested_quantities.each do |product_id, quantity|
product = @products.find_by(id: product_id)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a chance we can make a single db query?

next if product.blank?

estimate.estimate_details.build(
product:,
quantity: quantity.to_i,
duration: params.dig(:durations, product_id).presence,
duration_unit: product.time_unit,
)
end

estimate.estimate_details.each(&:assign_price_policy_and_cost)
estimate
end

end
14 changes: 14 additions & 0 deletions app/models/price_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ def self.external
globals.find_by(name: Settings.price_group.name.external)
end

def self.secondary_external
return if Settings.price_group.name.external_2.blank?

globals.find_by(name: Settings.price_group.name.external_2)
end

def self.for_public_estimate(customer_type)
case customer_type
when "internal" then base
when "external_non_profit" then secondary_external || external
else external
end
end

def self.nonbillable
base
end
Expand Down
55 changes: 55 additions & 0 deletions app/views/public_estimates/show.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
= content_for :h1 do
= t(".title")

%p= t(".intro")

= form_tag estimate_path, method: :get do
.inline-form-controls
%div
= label_tag :customer_type, t(".customer_type")
= select_tag :customer_type, options_for_select(@customer_type_options, params[:customer_type]), class: "form-control", onchange: "this.form.submit();"
.margin_x
= label_tag :facility_id, Facility.model_name.human
= select_tag :facility_id, options_from_collection_for_select(@facilities, :id, :name, params[:facility_id]), include_blank: true, class: "form-control", onchange: "this.form.submit();"

- if @facility.present?
%h3= t(".choose_products")
%table.table
%thead
%tr
%th= Product.model_name.human
%th= EstimateDetail.human_attribute_name(:quantity)
%th= EstimateDetail.human_attribute_name(:duration)
%tbody
- @products.each do |product|
%tr
%td= product.name
- if @priced_product_ids.include?(product.id)
%td= number_field_tag "quantities[#{product.id}]", params.dig(:quantities, product.id.to_s), min: 0, style: "width: 6em;"
%td
- if product.time_unit.present?
= number_field_tag "durations[#{product.id}]", params.dig(:durations, product.id.to_s), min: 1, style: "width: 6em;"
= EstimateDetail.human_attribute_name("duration_unit.#{product.time_unit}", count: 2)
- else
%td.text-muted{ colspan: 2 }= t(".no_public_rate")
= submit_tag t(".calculate"), class: "btn btn-primary", name: nil

- if @estimate.present?
%h3= t(".results")
%table.table.table-striped
%thead
%tr
%th= Product.model_name.human
%th= EstimateDetail.human_attribute_name(:quantity)
%th= EstimateDetail.human_attribute_name(:duration)
%th.text-right= EstimateDetail.human_attribute_name(:cost)
%tbody
- @estimate.estimate_details.map { |detail| EstimateDetailPresenter.new(detail) }.each do |estimate_detail|
%tr
%td= estimate_detail.product_display
%td= estimate_detail.quantity
%td= estimate_detail.duration_display
%td.text-right= estimate_detail.cost ? estimate_detail.cost_display : t(".no_public_rate")
.text-right
%strong= t(".total")
%span= number_to_currency(@total)
3 changes: 3 additions & 0 deletions app/views/shared/_header.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- if session_user.nil?
%ul.nav.navbar-nav.navbar-right.hide-from-print
= render "/shared/support"
= render "/shared/public_estimate_link"
%li= link_to t("pages.login"), :new_user_session
- else
-# collapsed at < 979px
Expand All @@ -28,6 +29,7 @@
%li.navbar-text= "#{acting_user.full_name} (#{acting_user.username})"
%li.divider-vertical
= render "/shared/support"
= render "/shared/public_estimate_link"
%li= link_to t("pages.cart"), :cart, class: "js--cart_count", data: { url: orders_cart_count_url }
- else
- if UserPreference.options_for(current_user).any?
Expand All @@ -40,6 +42,7 @@
%li.divider-vertical
-# .visible-with-nav is visible > 979px
= render "/shared/support"
= render "/shared/public_estimate_link"
%li.visible-with-nav= link_to t("pages.cart"), :cart, class: "js--cart_count", data: { url: orders_cart_count_url }
%li.divider-vertical
= render "shared/message_summary"
Expand Down
2 changes: 2 additions & 0 deletions app/views/shared/_public_estimate_link.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- if SettingsHelper.feature_on?(:public_estimates)
%li= link_to t("pages.public_estimate"), estimate_path
3 changes: 3 additions & 0 deletions config/locales/en.models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@ en:
days:
one: Day
other: Days
mins:
one: Minute
other: Minutes
schedule_rule:
start_time: Start Time
end_time: End Time
Expand Down
16 changes: 16 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ en:
movable_transactions: My Movable Transactions
notices: Notices
support: Support
public_estimate: Get an Estimate

affiliates:
add: Add Affiliate
Expand All @@ -115,6 +116,21 @@ en:
confirm: "Really remove affiliate %{name}?"
label: Remove

public_estimates:
show:
title: Estimate
intro: Estimate the cost of using our facilities. No account required.
customer_type: I am
internal: Internal
external: External
external_for_profit: External - for-profit
external_non_profit: External - non-profit
choose_products: Choose products
calculate: Calculate estimate
results: Estimated cost
no_public_rate: No public rate available
total: 'Total:'

bundle_products:
new:
head: Add Bundled Product
Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
match "/users/password/reset", to: "user_password#reset", as: "reset_password", via: [:get, :post]
end

if SettingsHelper.feature_on?(:public_estimates) && SettingsHelper.feature_on?(:show_estimates_option)
get "estimate", to: "public_estimates#show"
end

# root route
root to: "public#index"

Expand Down
1 change: 1 addition & 0 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ feature:
granular_permissions: true
kiosk_view: true
show_estimates_option: true
public_estimates: false
training_requests: true

split_accounts:
Expand Down
48 changes: 48 additions & 0 deletions spec/models/estimate_detail_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe EstimateDetail do
let(:facility) { create(:setup_facility) }
let(:price_group) { facility.price_groups.first }
let!(:item) { create(:setup_item, facility:) }
let!(:item_price_policy) do
create(:item_price_policy, product: item, price_group:, unit_cost: 25, unit_subsidy: 5)
end

describe "#assign_price_policy_and_cost without a user" do
let(:persisted_detail) do
estimate = create(:estimate, facility:, price_group:)
estimate.estimate_details.create!(product: item, quantity: 3)
end

let(:anonymous_detail) do
estimate = Estimate.new(facility:, price_group:)
estimate.estimate_details.build(product: item, quantity: 3)
end

it "resolves the price policy on an unsaved record" do
expect(anonymous_detail.assign_price_policy_and_cost).to be true
expect(anonymous_detail.price_policy).to eq(item_price_policy)
end

it "computes the same cost as the persisted equivalent" do
anonymous_detail.assign_price_policy_and_cost

expect(anonymous_detail.cost).to eq(persisted_detail.cost)
expect(anonymous_detail.cost).to eq(60)
end

it "persists nothing" do
expect { anonymous_detail.assign_price_policy_and_cost }.not_to change(EstimateDetail, :count)
expect(anonymous_detail).not_to be_persisted
end

it "returns false when no price policy matches the price group" do
other_item = create(:setup_item, facility:)
detail = Estimate.new(facility:, price_group:).estimate_details.build(product: other_item, quantity: 1)

expect(detail.assign_price_policy_and_cost).to be false
end
end
end
45 changes: 45 additions & 0 deletions spec/models/price_group_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -241,4 +241,49 @@
end
end

describe ".for_public_estimate" do
let(:initial_secondary_external) { Settings.price_group.name.external_2 }

after { Settings.price_group.name.external_2 = initial_secondary_external }

it "returns the base group for internal customers" do
expect(described_class.for_public_estimate("internal")).to eq(described_class.base)
end

it "returns the external group for external customers" do
expect(described_class.for_public_estimate("external"))
.to eq(described_class.external)
end

context "when a second external group is not configured" do
before { Settings.price_group.name.external_2 = nil }

it "returns nil from .secondary_external" do
expect(described_class.secondary_external).to be_nil
end

it "falls back to the external group for non-profit customers" do
expect(described_class.for_public_estimate("external_non_profit"))
.to eq(described_class.external)
end
end

context "when a second external group is configured" do
let!(:secondary_external) do
Settings.price_group.name.external_2 = "External Non-Profit Rate"
described_class.setup_global(name: "External Non-Profit Rate", is_internal: false, display_order: 2)
end

it "returns it for non-profit customers" do
expect(described_class.for_public_estimate("external_non_profit"))
.to eq(secondary_external)
end

it "still returns the external group for for-profit customers" do
expect(described_class.for_public_estimate("external"))
.to eq(described_class.external)
end
end
end

end
Loading
Loading