Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
81 changes: 81 additions & 0 deletions app/controllers/public_estimates_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 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])
@customer_type = customer_type
@customer_type_options = customer_type_options
@price_group = PriceGroup.for_public_estimate(@customer_type)
@products = @facility ? priced_products : Product.none
@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_types
Settings.public_estimates.customer_types.map(&:to_s)
end

def customer_type
customer_types.include?(params[:customer_type]) ? params[:customer_type] : customer_types.first
end

def customer_type_options
customer_types.map { |key| [t(".customer_types.#{key}"), key] }
end

def priced_products
return Product.none if @price_group.blank?

facility_products.where(
id: PricePolicy.current_for_date(Time.current).purchaseable
.where(price_group: @price_group).select(:product_id),
)
end

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

def requested_quantities
@requested_quantities ||= permitted_product_values(:quantities).select { |_id, quantity| quantity.to_i.positive? }
end

def requested_durations
@requested_durations ||= permitted_product_values(:durations)
end

def permitted_product_values(key)
values = params[key]
return {} unless values.is_a?(ActionController::Parameters)

values.permit(@products.map { |product| product.id.to_s }).to_h
end

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

products = @products.where(id: requested_quantities.keys).index_by { |product| product.id.to_s }

requested_quantities.each do |product_id, quantity|
product = products[product_id]
next if product.blank?

estimate.estimate_details.build(
product:,
quantity: quantity.to_i,
duration: requested_durations[product_id].presence,
duration_unit: product.time_unit,
)
end

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

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

def self.for_public_estimate(customer_type)
name = Settings.price_group.name.to_h[customer_type.to_s.to_sym]
return if name.blank?

globals.find_by(name:)
end

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

%p= t(".intro")

= form_tag estimate_path, method: :get do
.hide-from-print
.inline-form-controls
%div
= label_tag :customer_type, t(".customer_type")
= select_tag :customer_type, options_for_select(@customer_type_options, @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
%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)
= submit_tag t(".calculate"), class: "btn btn-primary", name: nil

- if @estimate.present?
%h3= t(".results")
.show-for-print
%p
%strong= "#{t('.customer_type')}:"
= t(".customer_types.#{@customer_type}")
%p
%strong= "#{Facility.model_name.human}:"
= @facility.name
%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)
.hide-from-print
= button_tag t(".print"), type: "button", class: "btn btn-default", onclick: "window.print();"
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
17 changes: 17 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,22 @@ 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
customer_types:
base: Internal
external: External
cancer_center: Cancer Center
Comment on lines +125 to +127

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 make it consistent with the price groups we have defined for open (without external_2)

choose_products: Choose products
calculate: Calculate estimate
results: Estimated cost
no_public_rate: No public rate available
total: 'Total:'
print: Print estimate

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
7 changes: 7 additions & 0 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ price_group:
external: 'External Rate'
cancer_center: 'Cancer Center Rate'

public_estimates:
# Keys from price_group.name. Each school can override this list.
customer_types:
- base
- external

time_zone: "Central Time (US & Canada)"

accounts:
Expand Down Expand Up @@ -221,6 +227,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
26 changes: 26 additions & 0 deletions spec/models/price_group_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -241,4 +241,30 @@
end
end

describe ".for_public_estimate" do
it "returns the group named by the matching price_group setting" do
expect(described_class.for_public_estimate("base")).to eq(described_class.base)
expect(described_class.for_public_estimate("external")).to eq(described_class.external)
end

it "returns nil for a key with no configured name" do
expect(described_class.for_public_estimate("external_2")).to be_nil
expect(described_class.for_public_estimate("nonsense")).to be_nil
end

context "when a school configures an extra key" do
let(:initial_name) { Settings.price_group.name.external_2 }
let!(:non_profit) 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

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

it "resolves that key to its group" do
expect(described_class.for_public_estimate("external_2")).to eq(non_profit)
end
end
end

end
Loading
Loading