# frozen_string_literal: true require 'money' require 'monetize' require 'active_support/core_ext/string/output_safety' module Spree # Spree::Money is a relatively thin wrapper around Monetize which handles # formatting via Spree::Config. class Money include Comparable DifferentCurrencyError = Class.new(StandardError) RUBY_NUMERIC_STRING = /\A-?\d+(\.\d+)?\z/ class <<self attr_accessor :default_formatting_rules def parse(amount, currency = Spree::Config[:currency]) new(parse_to_money(amount, currency)) end # @api private def parse_to_money(amount, currency) ::Monetize.parse(amount, currency) end end self.default_formatting_rules = { # Ruby money currently has this as false, which is wrong for the vast # majority of locales. sign_before_symbol: true } attr_reader :money delegate :cents, :currency, :to_d, :zero?, to: :money # @param amount [Money, #to_s] the value of the money object # @param options [Hash] the default options for formatting the money object See #format def initialize(amount, options = {}) if amount.is_a?(::Money) @money = amount else currency = (options[:currency] || Spree::Config[:currency]) if amount.to_s =~ RUBY_NUMERIC_STRING @money = Monetize.from_string(amount, currency) else @money = Spree::Money.parse_to_money(amount, currency) Spree::Deprecation.warn <<-WARN.squish, caller Spree::Money was initialized with #{amount.inspect}, which will not be supported in the future. Instead use Spree::Money.new(#{@money.to_s.inspect}, options) or Spree::Money.parse(#{amount.inspect}) WARN end end @options = Spree::Money.default_formatting_rules.merge(options) end # @return [String] the value of this money object formatted according to # its options def to_s format end # @param options [Hash, String] the options for formatting the money object # @option options [Boolean] with_currency when true, show the currency # @option options [Boolean] no_cents when true, round to the closest dollar # @option options [String] decimal_mark the mark for delimiting the # decimals # @option options [String, false, nil] thousands_separator the character to # delimit powers of 1000, if one is desired, otherwise false or nil # @option options [Boolean] sign_before_symbol when true the sign of the # value comes before the currency symbol # @option options [:before, :after] symbol_position the position of the # currency symbol # @return [String] the value of this money object formatted according to # its options def format(options = {}) @money.format(@options.merge(options)) end # @note If you pass in options, ensure you pass in the html: true as well. # @param options [Hash] additional formatting options # @return [String] the value of this money object formatted according to # its options and any additional options, by default as html. def to_html(options = { html: true }) output = format(options) if options[:html] # 1) prevent blank, breaking spaces # 2) prevent escaping of HTML character entities output = output.sub(" ", " ").html_safe end output end # (see #to_s) def as_json(*) to_s end def <=>(other) if !other.respond_to?(:money) raise TypeError, "Can't compare #{other.class} to Spree::Money" end if currency != other.currency # By default, ::Money will try to run a conversion on `other.money` and # try a comparison on that. We do not want any currency conversion to # take place so we'll catch this here and raise an error. raise( DifferentCurrencyError, "Can't compare #{currency} with #{other.currency}" ) end @money <=> other.money end # Delegates comparison to the internal ruby money instance. # # @see http://www.rubydoc.info/gems/money/Money/Arithmetic#%3D%3D-instance_method def ==(other) raise TypeError, "Can't compare #{other.class} to Spree::Money" if !other.respond_to?(:money) @money == other.money end def -(other) raise TypeError, "Can't subtract #{other.class} to Spree::Money" if !other.respond_to?(:money) self.class.new(@money - other.money) end def +(other) raise TypeError, "Can't add #{other.class} to Spree::Money" if !other.respond_to?(:money) self.class.new(@money + other.money) end end end