diff --git a/app/models/spree/line_item.rb b/app/models/spree/line_item.rb index df88535a2a..5813511577 100644 --- a/app/models/spree/line_item.rb +++ b/app/models/spree/line_item.rb @@ -220,9 +220,9 @@ module Spree end def unit_price_price_and_unit - price = Spree::Money.new((rand * 10).round(2), currency: currency) - unit = ["item", "kg"].sample - price.to_html + " / " + unit + unit_price = UnitPrice.new(variant) + Spree::Money.new(price_with_adjustments / unit_price.denominator).to_html + + " / " + unit_price.unit end def scoper diff --git a/app/serializers/api/variant_serializer.rb b/app/serializers/api/variant_serializer.rb index 5c99fc1730..ecbfc66dc7 100644 --- a/app/serializers/api/variant_serializer.rb +++ b/app/serializers/api/variant_serializer.rb @@ -41,10 +41,16 @@ class Api::VariantSerializer < ActiveModel::Serializer end def unit_price_price - (rand * 10).round(2) + price_with_fees / unit_price.denominator end def unit_price_unit - rand > 0.5 ? "item" : "kg" + unit_price.unit + end + + private + + def unit_price + @unit_price ||= UnitPrice.new(object) end end diff --git a/app/services/unit_price.rb b/app/services/unit_price.rb new file mode 100644 index 0000000000..0b28c973d5 --- /dev/null +++ b/app/services/unit_price.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class UnitPrice + def initialize(variant) + @variant = variant + @product = variant.product + end + + def denominator + # catches any case where unit is not kg, lb, or L. + return @variant.unit_value if @product&.variant_unit == "items" + + case unit + when "lb" + @variant.unit_value / 453.6 + when "kg" + @variant.unit_value / 1000 + else # Liters + @variant.unit_value + end + end + + def unit + return "lb" if WeightsAndMeasures.new(@variant).system == "imperial" + + case @product&.variant_unit + when "weight" + "kg" + when "volume" + "L" + else + @product.variant_unit_name.presence || I18n.t("item") + end + end +end diff --git a/app/services/variant_units/option_value_namer.rb b/app/services/variant_units/option_value_namer.rb index 7cb8fc8547..9d95f58f2e 100644 --- a/app/services/variant_units/option_value_namer.rb +++ b/app/services/variant_units/option_value_namer.rb @@ -62,40 +62,7 @@ module VariantUnits end def scale_for_unit_value - units = { - 'weight' => { - 1.0 => { 'name' => 'g', 'system' => 'metric' }, - 28.35 => { 'name' => 'oz', 'system' => 'imperial' }, - 453.6 => { 'name' => 'lb', 'system' => 'imperial' }, - 1000.0 => { 'name' => 'kg', 'system' => 'metric' }, - 1_000_000.0 => { 'name' => 'T', 'system' => 'metric' } - }, - 'volume' => { - 0.001 => { 'name' => 'mL', 'system' => 'metric' }, - 1.0 => { 'name' => 'L', 'system' => 'metric' }, - 1000.0 => { 'name' => 'kL', 'system' => 'metric' } - } - } - - scales = units[@variant.product.variant_unit] - product_scale = @variant.product.variant_unit_scale - product_scale_system = scales[product_scale.to_f]['system'] - - largest_unit = find_largest_unit(scales, product_scale_system) - [largest_unit[0], largest_unit[1]["name"]] - end - - # Find the largest available and compatible unit where unit_value comes - # to >= 1 when expressed in it. - # If there is none available where this is true, use the smallest available unit. - def find_largest_unit(scales, product_scale_system) - largest_unit = scales.select { |scale, unit_info| - unit_info['system'] == product_scale_system && - @variant.unit_value / scale >= 1 - }.max - return scales.first if largest_unit.nil? - - largest_unit + WeightsAndMeasures.new(@variant).scale_for_unit_value end def pluralize(unit_name, count) diff --git a/app/services/weights_and_measures.rb b/app/services/weights_and_measures.rb new file mode 100644 index 0000000000..eaf66b3ec4 --- /dev/null +++ b/app/services/weights_and_measures.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class WeightsAndMeasures + def initialize(variant) + @variant = variant + @units = UNITS + end + + def scale_for_unit_value + largest_unit = find_largest_unit(scales_for_variant_unit, system) + return [nil, nil] unless largest_unit + + [largest_unit[0], largest_unit[1]["name"]] + end + + def system + scales = scales_for_variant_unit + return "custom" unless product_scale = @variant.product.variant_unit_scale + + scales[product_scale.to_f]['system'] + end + + private + + UNITS = { + 'weight' => { + 1.0 => { 'name' => 'g', 'system' => 'metric' }, + 28.35 => { 'name' => 'oz', 'system' => 'imperial' }, + 453.6 => { 'name' => 'lb', 'system' => 'imperial' }, + 1000.0 => { 'name' => 'kg', 'system' => 'metric' }, + 1_000_000.0 => { 'name' => 'T', 'system' => 'metric' } + }, + 'volume' => { + 0.001 => { 'name' => 'mL', 'system' => 'metric' }, + 1.0 => { 'name' => 'L', 'system' => 'metric' }, + 1000.0 => { 'name' => 'kL', 'system' => 'metric' } + } + }.freeze + + def scales_for_variant_unit + @units[@variant.product.variant_unit] + end + + # Find the largest available and compatible unit where unit_value comes + # to >= 1 when expressed in it. + # If there is none available where this is true, use the smallest available unit. + def find_largest_unit(scales, product_scale_system) + return nil unless scales + + largest_unit = scales.select { |scale, unit_info| + unit_info['system'] == product_scale_system && + @variant.unit_value / scale >= 1 + }.max + return scales.first if largest_unit.nil? + + largest_unit + end +end diff --git a/spec/services/unit_prices_spec.rb b/spec/services/unit_prices_spec.rb new file mode 100644 index 0000000000..0e836f2c29 --- /dev/null +++ b/spec/services/unit_prices_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe UnitPrice do + subject { UnitPrice.new(variant) } + let(:variant) { Spree::Variant.new } + let(:product) { instance_double(Spree::Product) } + + before do + allow(variant).to receive(:product) { product } + end + + describe "#unit" do + context "metric" do + before do + allow(product).to receive(:variant_unit_scale) { 1.0 } + end + + it "returns kg for weight" do + allow(product).to receive(:variant_unit) { "weight" } + expect(subject.unit).to eq("kg") + end + + it "returns L for volume" do + allow(product).to receive(:variant_unit) { "volume" } + expect(subject.unit).to eq("L") + end + end + + context "imperial" do + it "returns lbs" do + allow(product).to receive(:variant_unit_scale) { 453.6 } + allow(product).to receive(:variant_unit) { "weight" } + expect(subject.unit).to eq("lb") + end + end + + context "items" do + it "returns items if no unit is specified" do + allow(product).to receive(:variant_unit_name) { nil } + allow(product).to receive(:variant_unit_scale) { nil } + allow(product).to receive(:variant_unit) { "items" } + expect(subject.unit).to eq("Item") + end + + it "returns the unit if a unit is specified" do + allow(product).to receive(:variant_unit_name) { "bunch" } + allow(product).to receive(:variant_unit_scale) { nil } + allow(product).to receive(:variant_unit) { "items" } + expect(subject.unit).to eq("bunch") + end + end + end + + describe "#denominator" do + context "metric" do + it "returns 0.5 for a 500g variant" do + allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(product).to receive(:variant_unit) { "weight" } + variant.unit_value = 500 + expect(subject.denominator).to eq(0.5) + end + + it "returns 2 for a 2kg variant" do + allow(product).to receive(:variant_unit_scale) { 1000 } + allow(product).to receive(:variant_unit) { "weight" } + variant.unit_value = 2000 + expect(subject.denominator).to eq(2) + end + + it "returns 0.5 for a 500mL variant" do + allow(product).to receive(:variant_unit_scale) { 0.001 } + allow(product).to receive(:variant_unit) { "volume" } + variant.unit_value = 0.5 + expect(subject.denominator).to eq(0.5) + end + end + + context "imperial" do + it "returns 2 for a 2 pound variant" do + allow(product).to receive(:variant_unit_scale) { 453.6 } + allow(product).to receive(:variant_unit) { "weight" } + variant.unit_value = 2*453.6 + expect(subject.denominator).to eq(2) + end + end + + context "items" do + it "returns 1 if no unit is specified" do + allow(product).to receive(:variant_unit_scale) { nil } + allow(product).to receive(:variant_unit) { "items" } + variant.unit_value = 1 + expect(subject.denominator).to eq(1) + end + + it "returns 2 for multi-item units" do + allow(product).to receive(:variant_unit_scale) { nil } + allow(product).to receive(:variant_unit) { "items" } + variant.unit_value = 2 + expect(subject.denominator).to eq(2) + end + end + end +end diff --git a/spec/services/weights_and_measures_spec.rb b/spec/services/weights_and_measures_spec.rb new file mode 100644 index 0000000000..435ac0f5e4 --- /dev/null +++ b/spec/services/weights_and_measures_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe WeightsAndMeasures do + subject { WeightsAndMeasures.new(variant) } + let(:variant) { Spree::Variant.new } + let(:product) { instance_double(Spree::Product) } + + before do + allow(variant).to receive(:product) { product } + end + + describe "#system" do + context "weight" do + before do + allow(product).to receive(:variant_unit) { "weight" } + end + + it "when scale is for a metric unit" do + allow(product).to receive(:variant_unit_scale) { 1.0 } + expect(subject.system).to eq("metric") + end + + it "when scale is for an imperial unit" do + allow(product).to receive(:variant_unit_scale) { 28.35 } + expect(subject.system).to eq("imperial") + end + end + + context "volume" do + it "when scale is for a metric unit" do + allow(product).to receive(:variant_unit) { "volume" } + allow(product).to receive(:variant_unit_scale) { 1.0 } + expect(subject.system).to eq("metric") + end + end + + context "items" do + it "when scale is for items" do + allow(product).to receive(:variant_unit) { "items" } + allow(product).to receive(:variant_unit_scale) { nil } + expect(subject.system).to eq("custom") + end + end + end + + describe "#scale_for_unit_value" do + context "weight" do + before do + allow(product).to receive(:variant_unit) { "weight" } + end + + context "metric" do + it "for a unit value that should display in grams" do + allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:unit_value) { 500 } + expect(subject.scale_for_unit_value).to eq([1.0, "g"]) + end + + it "for a unit value that should display in kg" do + allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:unit_value) { 1500 } + expect(subject.scale_for_unit_value).to eq([1000.0, "kg"]) + end + end + end + + context "volume" do + it "for a unit value that should display in L" do + allow(product).to receive(:variant_unit) { "volume" } + allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:unit_value) { 1500 } + expect(subject.scale_for_unit_value).to eq([1000, "kL"]) + end + end + + context "items" do + it "when scale is for items" do + allow(product).to receive(:variant_unit) { "items" } + allow(product).to receive(:variant_unit_scale) { nil } + allow(variant).to receive(:unit_value) { 4 } + expect(subject.scale_for_unit_value).to eq([nil, nil]) + end + end + end +end