diff --git a/lib/money-rails/active_record/monetizable.rb b/lib/money-rails/active_record/monetizable.rb index c2272930ec..a412e70409 100644 --- a/lib/money-rails/active_record/monetizable.rb +++ b/lib/money-rails/active_record/monetizable.rb @@ -55,6 +55,15 @@ def monetize(*fields) "Use :as option to explicitly specify the name or change the amount column postfix in the initializer." end + # Infers precision based on field's column type when desired + if !options.has_key?(:infinite_precision) && MoneyRails.infer_precision? + # Only attempt column introspection when backing table exists + if self.table_exists? + column_definition = self.columns_hash[subunit_name] + options[:infinite_precision] = column_definition && column_definition.type == :decimal + end + end + # Optional accessor to be run on an instance to detect currency instance_currency_name = options[:with_model_currency] || options[:model_currency] || @@ -194,14 +203,14 @@ def read_monetized(name, subunit_name, options = {}, *args) if memoized.currency == attr_currency result = memoized else - memoized_amount = memoized.amount.to_money(attr_currency) + memoized_amount = memoized.amount.to_money(attr_currency, options.slice(:infinite_precision)) write_attribute subunit_name, memoized_amount.cents # Cache the value (it may be nil) result = instance_variable_set("@#{name}", memoized_amount) end elsif amount.present? # If amount is NOT nil (or empty string) load the amount in a Money - amount = Money.new(amount, attr_currency) + amount = Money.new(amount, attr_currency, options.slice(:infinite_precision)) # Cache the value (it may be nil) result = instance_variable_set("@#{name}", amount) @@ -230,7 +239,7 @@ def write_monetized(name, subunit_name, value, validation_enabled, instance_curr money = value else begin - money = value.to_money(public_send("currency_for_#{name}")) + money = value.to_money(public_send("currency_for_#{name}"), options.slice(:infinite_precision)) rescue NoMethodError return nil rescue Money::Currency::UnknownCurrency, Monetize::ParseError => e diff --git a/lib/money-rails/configuration.rb b/lib/money-rails/configuration.rb index 129dba97f9..07302cb18f 100644 --- a/lib/money-rails/configuration.rb +++ b/lib/money-rails/configuration.rb @@ -63,6 +63,15 @@ def rounding_mode=(mode) # Provide exchange rates delegate :add_rate, to: :Money + # Set infinite precision behavior + # If infer_precision? is set to true, will set the precision based on the column type + delegate :default_infinite_precision=, :default_infinite_precision, to: :Money + mattr_accessor :infer_precision + @@infer_precision = false + def infer_precision? + @@infer_precision + end + # Use (by default) validation of numericality for each monetized field. mattr_accessor :include_validations @@include_validations = true diff --git a/spec/active_record/monetizable_spec.rb b/spec/active_record/monetizable_spec.rb index 15b4fff663..3dcddb9a4d 100644 --- a/spec/active_record/monetizable_spec.rb +++ b/spec/active_record/monetizable_spec.rb @@ -12,7 +12,9 @@ class Sub < Product; end sale_price_amount: 1200, delivery_fee_cents: 100, restock_fee_cents: 2000, reduced_price_cents: 1500, reduced_price_currency: :lvl, - lambda_price_cents: 4000) + lambda_price_cents: 4000, + unit_cost_cents: 1234.8765 + ) end describe ".monetize" do @@ -161,6 +163,132 @@ class SubProduct < Product expect(product).to be_valid end + context "when infinite_precision option" do + before(:example) do + @prior_default_infinite_precision = MoneyRails.default_infinite_precision + @prior_infer_precision = MoneyRails.infer_precision + end + after(:example) do + MoneyRails.default_infinite_precision = @prior_default_infinite_precision + MoneyRails.infer_precision = @prior_infer_precision + end + + context "is specified as true" do + it "returns infinite_precision money objects regardless of false default or column type" do + MoneyRails.default_infinite_precision = false + MoneyRails.infer_precision = true + class PreciseProduct < Product + monetize :price_cents, infinite_precision: true + monetize :unit_cost_cents, infinite_precision: true + monetize :skip_validation_price_cents, infinite_precision: true + end + precise_product = PreciseProduct.new(price: 11.1051, unit_cost: 100.2387, skip_validation_price: "5.0065") + expect(precise_product.price.infinite_precision?).to eq(true) + # Price is an integer column so the value is truncated on assignment + expect(precise_product.price).to eq(Money.new(1110.0, "EUR", infinite_precision: true)) + expect(precise_product.price.to_s).to eq("11.10") + expect(precise_product.unit_cost.infinite_precision?).to eq(true) + expect(precise_product.unit_cost).to eq(Money.new(10023.87, "EUR", infinite_precision: true)) + expect(precise_product.unit_cost.to_s).to eq("100.2387") + expect(precise_product.skip_validation_price.infinite_precision?).to eq(true) + expect(precise_product.skip_validation_price).to eq(Money.new(500.65, "EUR", infinite_precision: true)) + expect(precise_product.skip_validation_price.to_s).to eq("5.0065") + end + end + + context "is specified as false" do + it "returns subunit precision money objects regardless of true default or column type" do + MoneyRails.default_infinite_precision = true + MoneyRails.infer_precision = true + class ImpreciseProduct < Product + monetize :price_cents, infinite_precision: false + monetize :unit_cost_cents, infinite_precision: false + monetize :skip_validation_price_cents, infinite_precision: false + end + imprecise_product = ImpreciseProduct.new(price: 11.1051, unit_cost: 100.2387, skip_validation_price: "5.0065") + expect(imprecise_product.price.infinite_precision?).to eq(false) + expect(imprecise_product.price).to eq(Money.new(1111, "EUR", infinite_precision: false)) + expect(imprecise_product.price.to_s).to eq("11.11") + expect(imprecise_product.unit_cost.infinite_precision?).to eq(false) + expect(imprecise_product.unit_cost).to eq(Money.new(10024, "EUR", infinite_precision: false)) + expect(imprecise_product.unit_cost.to_s).to eq("100.24") + expect(imprecise_product.skip_validation_price.infinite_precision?).to eq(false) + expect(imprecise_product.skip_validation_price).to eq(Money.new(501, "EUR", infinite_precision: false)) + expect(imprecise_product.skip_validation_price.to_s).to eq("5.01") + end + end + + context "is not specified" do + let(:unspecified_precision_product) { + unspecified_precision_klass = Class.new(Product) do + monetize :price_cents + monetize :unit_cost_cents + monetize :skip_validation_price_cents + end + unspecified_precision_klass.new(price: 11.1051, unit_cost: 100.238765, skip_validation_price: "5.0065") + } + context "and infer_precision? is false" do + it "uses the specified default precision when false" do + MoneyRails.infer_precision = false + MoneyRails.default_infinite_precision = false + expect(unspecified_precision_product.price.infinite_precision?).to eq(false) + expect(unspecified_precision_product.price).to eq(Money.new(1111, "EUR", infinite_precision: false)) + expect(unspecified_precision_product.price.to_s).to eq("11.11") + expect(unspecified_precision_product.unit_cost.infinite_precision?).to eq(false) + expect(unspecified_precision_product.unit_cost).to eq(Money.new(10024, "EUR", infinite_precision: false)) + expect(unspecified_precision_product.unit_cost.to_s).to eq("100.24") + expect(unspecified_precision_product.skip_validation_price.infinite_precision?).to eq(false) + expect(unspecified_precision_product.skip_validation_price).to eq(Money.new(501, "EUR", infinite_precision: false)) + expect(unspecified_precision_product.skip_validation_price.to_s).to eq("5.01") + end + + it "uses the specified default precision when true" do + MoneyRails.infer_precision = false + MoneyRails.default_infinite_precision = true + expect(unspecified_precision_product.price.infinite_precision?).to eq(true) + expect(unspecified_precision_product.price).to eq(Money.new(1110.0, "EUR", infinite_precision: true)) + expect(unspecified_precision_product.price.to_s).to eq("11.10") + expect(unspecified_precision_product.unit_cost.infinite_precision?).to eq(true) + expect(unspecified_precision_product.unit_cost).to eq(Money.new(10023.8765, "EUR", infinite_precision: true)) + expect(unspecified_precision_product.unit_cost.to_s).to eq("100.238765") + expect(unspecified_precision_product.skip_validation_price.infinite_precision?).to eq(true) + expect(unspecified_precision_product.skip_validation_price).to eq(Money.new(500.65, "EUR", infinite_precision: true)) + expect(unspecified_precision_product.skip_validation_price.to_s).to eq("5.0065") + end + end + + context "and infer_precision? is true" do + it "assigns instance precision based on the column type regardless of false default" do + MoneyRails.infer_precision = true + MoneyRails.default_infinite_precision = false + expect(unspecified_precision_product.price.infinite_precision?).to eq(false) + expect(unspecified_precision_product.price).to eq(Money.new(1111, "EUR", infinite_precision: true)) + expect(unspecified_precision_product.price.to_s).to eq("11.11") + expect(unspecified_precision_product.unit_cost.infinite_precision?).to eq(true) + expect(unspecified_precision_product.unit_cost).to eq(Money.new(10023.8765, "EUR", infinite_precision: true)) + expect(unspecified_precision_product.unit_cost.to_s).to eq("100.238765") + expect(unspecified_precision_product.skip_validation_price.infinite_precision?).to eq(false) + expect(unspecified_precision_product.skip_validation_price).to eq(Money.new(501, "EUR", infinite_precision: false)) + expect(unspecified_precision_product.skip_validation_price.to_s).to eq("5.01") + end + + it "assigns instance precision based on the column type regardless of true default" do + MoneyRails.infer_precision = true + MoneyRails.default_infinite_precision = true + expect(unspecified_precision_product.price.infinite_precision?).to eq(false) + expect(unspecified_precision_product.price).to eq(Money.new(1111, "EUR", infinite_precision: true)) + expect(unspecified_precision_product.price.to_s).to eq("11.11") + expect(unspecified_precision_product.unit_cost.infinite_precision?).to eq(true) + expect(unspecified_precision_product.unit_cost).to eq(Money.new(10023.8765, "EUR", infinite_precision: true)) + expect(unspecified_precision_product.unit_cost.to_s).to eq("100.238765") + expect(unspecified_precision_product.skip_validation_price.infinite_precision?).to eq(false) + expect(unspecified_precision_product.skip_validation_price).to eq(Money.new(501, "EUR", infinite_precision: false)) + expect(unspecified_precision_product.skip_validation_price.to_s).to eq("5.01") + end + end + end + end + context "when MoneyRails.raise_error_on_money_parsing is true" do before { MoneyRails.raise_error_on_money_parsing = true } after { MoneyRails.raise_error_on_money_parsing = false } diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index d3f8969241..220d387d7d 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -125,6 +125,19 @@ MoneyRails.default_bank = old_bank end + it "assigns a default infinite precision" do + old_precision = MoneyRails.default_infinite_precision + + MoneyRails.default_infinite_precision = true + expect(Money.default_infinite_precision).to eq(true) + + MoneyRails.default_infinite_precision = old_precision + end + + it "sets infer_precision? to false by default" do + expect(MoneyRails.infer_precision?).to eq(false) + end + describe "rounding mode" do [BigDecimal::ROUND_UP, BigDecimal::ROUND_DOWN, BigDecimal::ROUND_HALF_UP, BigDecimal::ROUND_HALF_DOWN, BigDecimal::ROUND_HALF_EVEN, BigDecimal::ROUND_CEILING, BigDecimal::ROUND_FLOOR].each do |mode| diff --git a/spec/dummy/app/models/product.rb b/spec/dummy/app/models/product.rb index 3724678614..c9da15a492 100644 --- a/spec/dummy/app/models/product.rb +++ b/spec/dummy/app/models/product.rb @@ -56,4 +56,7 @@ class Product < ActiveRecord::Base # Using postfix to determine currency column (reduced_price_currency) monetize :reduced_price_cents, allow_nil: true + + # Enable infinite_precision on this attribute + monetize :unit_cost_cents, allow_nil: true end diff --git a/spec/dummy/db/migrate/20190814150759_add_precision_unit_cost_to_products.rb b/spec/dummy/db/migrate/20190814150759_add_precision_unit_cost_to_products.rb new file mode 100644 index 0000000000..cbdb36d6d7 --- /dev/null +++ b/spec/dummy/db/migrate/20190814150759_add_precision_unit_cost_to_products.rb @@ -0,0 +1,5 @@ +class AddPrecisionUnitCostToProducts < (Rails::VERSION::MAJOR >= 5 ? ActiveRecord::Migration[4.2] : ActiveRecord::Migration) + def change + add_column :products, :unit_cost_cents, :decimal + end +end diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index 90bba06ceb..cfdb62117f 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151026220420) do +ActiveRecord::Schema.define(version: 20190814150759) do create_table "dummy_products", force: :cascade do |t| t.string "currency" @@ -39,6 +39,7 @@ t.integer "special_price_cents" t.integer "lambda_price_cents" t.string "skip_validation_price_cents" + t.decimal "unit_cost_cents" end create_table "services", force: :cascade do |t|