money_attribute 0.14.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MoneyAttribute
4
+ # @private Constructor for integer (subunit) columns used by +composed_of+.
5
+ INTEGER_CONSTRUCTOR = lambda do |amount, currency|
6
+ next nil if amount.nil?
7
+
8
+ resolved = Money::Currency.resolve(currency.presence || MoneyAttribute.default_currency) || 'XXX'
9
+ Mint::Money.from_subunits(amount, resolved)
10
+ end.freeze
11
+
12
+ # @private Constructor for decimal (unit value) columns used by +composed_of+.
13
+ DECIMAL_CONSTRUCTOR = lambda do |amount, currency|
14
+ next nil if amount.nil?
15
+
16
+ resolved = Money::Currency.resolve(currency.presence || MoneyAttribute.default_currency) || 'XXX'
17
+ Mint::Money.from(amount, resolved)
18
+ end.freeze
19
+
20
+ # Value object holding metadata for a registered money attribute.
21
+ #
22
+ # Created by +money_attribute+ or +money_amount+ and stored in the class-level registry.
23
+ # Used by query helpers to resolve column names, build Money values, and generate SQL.
24
+ AttributeSpec = Struct.new(:name, :kind, :amount_col, :currency_col, :amount_type, keyword_init: true) do
25
+ # @return [Boolean] +true+ when the spec describes a two-column (amount + currency) attribute.
26
+ def composite? = kind == :composite
27
+
28
+ # @return [Boolean] +true+ when the spec describes a single-column (fixed currency) attribute.
29
+ def single? = kind == :single
30
+
31
+ # Returns the backing database columns for the attribute.
32
+ #
33
+ # @return [Array<String>] two-element array for composite, one-element for single.
34
+ def columns
35
+ @columns ||= (composite? ? [amount_col, currency_col] : [amount_col]).freeze
36
+ end
37
+
38
+ # @return [Boolean] +true+ when the amount column stores subunits (bigint).
39
+ def integer_amount? = amount_type == :integer
40
+
41
+ # @return [Symbol] +:subunits+ for integer columns, +:to_d+ for decimal columns.
42
+ def amount_extractor = integer_amount? ? :subunits : :to_d
43
+
44
+ # @return [Hash{String => Symbol}] mapping suitable for +composed_of+.
45
+ def composed_of_mapping = { amount_col => amount_extractor, currency_col => :currency_code }
46
+
47
+ # @return [Proc] the constructor lambda used by +composed_of+ to instantiate Money values.
48
+ def constructor
49
+ integer_amount? ? INTEGER_CONSTRUCTOR : DECIMAL_CONSTRUCTOR
50
+ end
51
+
52
+ # Converts a raw amount and currency into a +Mint::Money+ value.
53
+ #
54
+ # @param amount [Integer, BigDecimal, nil] the raw column value
55
+ # @param currency [String, Mint::Currency, nil] the currency code or object
56
+ # @return [Mint::Money, nil] the resolved money value, or nil when amount is nil
57
+ def build_money(amount, currency)
58
+ return unless amount
59
+ return amount if amount.is_a?(Mint::Money)
60
+
61
+ constructor.call(amount, currency)
62
+ end
63
+
64
+ # Normalizes a query value to the column's storage format.
65
+ #
66
+ # Composite attributes: decomposes +Mint::Money+ to subunits via +.subunits+.
67
+ # Single-column attributes: passes through (the registered Type handles serialization).
68
+ #
69
+ # @param value [Mint::Money, Numeric] the query value
70
+ # @return [Integer, BigDecimal, Mint::Money] the normalized value
71
+ def normalize_query_value(value)
72
+ return value unless integer_amount?
73
+ return value.subunits if value.is_a?(Mint::Money)
74
+
75
+ value
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent/map'
4
+
5
+ module MoneyAttribute
6
+ # Stores money attribute metadata on the model class.
7
+ module AttributeSpecRegistry
8
+ extend ActiveSupport::Concern
9
+
10
+ REGISTRY = Concurrent::Map.new
11
+
12
+ class_methods do
13
+ # Registers a money attribute spec for the current model class.
14
+ #
15
+ # @param name [Symbol, String] the attribute name
16
+ # @param kind [Symbol] +:composite+ or +:single+
17
+ # @param amount_col [Symbol, String] the amount column name
18
+ # @param currency_col [Symbol, String, nil] the currency column name (composite only)
19
+ # @param amount_type [Symbol, nil] +:integer+ or +:decimal+
20
+ # @return [AttributeSpec]
21
+ def register_money_attribute_spec(name, kind:, amount_col:, currency_col: nil, amount_type: nil)
22
+ spec = MoneyAttribute::AttributeSpec.new(
23
+ name: name.to_s,
24
+ kind: kind,
25
+ amount_col: amount_col.to_s,
26
+ currency_col: currency_col&.to_s,
27
+ amount_type: amount_type
28
+ )
29
+
30
+ money_attribute_specs[spec.name] = spec
31
+ spec
32
+ end
33
+
34
+ # Returns the registered money attribute spec for the given name.
35
+ #
36
+ # @param name [Symbol, String] the attribute name
37
+ # @return [AttributeSpec, nil]
38
+ def money_attribute_spec(name)
39
+ REGISTRY[self]&.fetch(name.to_s, nil)
40
+ end
41
+
42
+ # Returns the registry hash for the current model class.
43
+ #
44
+ # @return [Hash{String => AttributeSpec}]
45
+ def money_attribute_specs
46
+ REGISTRY.fetch_or_store(self) { {} }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,28 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
- class Configuration
5
- attr_accessor :added_currencies, :default_currency
4
+ # @private
5
+ CONFIG_MUTEX = Mutex.new
6
6
 
7
- def initialize
8
- @added_currencies = []
9
- @default_currency = 'USD'
7
+ class << self
8
+ # Returns the lazily initialized gem configuration.
9
+ #
10
+ # @return [Config]
11
+ def config
12
+ CONFIG_MUTEX.synchronize { @config ||= Config.new }
10
13
  end
11
- end
12
14
 
13
- # Class-level caches written during Rails boot (single-threaded),
14
- # read-only during request handling. Safe without synchronization.
15
- def self.config
16
- @config ||= Configuration.new
17
- end
15
+ # Yields the current configuration object for mutation.
16
+ #
17
+ # @yield [config] the current configuration
18
+ # @return [void]
19
+ def configure = yield config
20
+
21
+ # Returns the current request or default currency as a resolved currency.
22
+ # Memoized per-thread — within a request, Current.currency is stable.
23
+ #
24
+ # @return [Mint::Currency]
25
+ def default_currency
26
+ code = MoneyAttribute::Current.currency.presence || config.default_currency
27
+
28
+ last_code, last_currency = Thread.current[:money_attribute_default_currency]
29
+ return last_currency if last_code == code
18
30
 
19
- def self.configure
20
- yield config if block_given?
21
- @default_currency = nil
22
- config
31
+ currency = Money::Currency.resolve!(code)
32
+ Thread.current[:money_attribute_default_currency] = [code, currency]
33
+ currency
34
+ end
23
35
  end
24
36
 
25
- def self.default_currency
26
- @default_currency ||= ::Mint::Currency.resolve!(config.default_currency)
37
+ # Gem configuration holding the default currency and registered custom currencies.
38
+ #
39
+ # MoneyAttribute.configure do |config|
40
+ # config.default_currency = 'BRL'
41
+ # end
42
+ class Config
43
+ # @return [String] ISO 4217 currency code used when no per-request or per-row currency is set.
44
+ attr_accessor :default_currency
45
+
46
+ # @return [Array<Hash>] custom currencies registered via +register_custom_currencies!+.
47
+ attr_accessor :added_currencies
48
+
49
+ # Initializes the default gem configuration values.
50
+ def initialize
51
+ @default_currency = 'USD'
52
+ @added_currencies = []
53
+ end
27
54
  end
28
55
  end
@@ -1,19 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
+ # :nodoc:
4
5
  class Converter
5
- def initialize(currency = MoneyAttribute.default_currency)
6
- @default_currency = currency
6
+ DEFAULT = new.freeze
7
+
8
+ # Initializes a converter with an optional fixed currency.
9
+ def initialize(currency = nil)
10
+ @static_currency = currency
11
+ end
12
+
13
+ # Returns the shared default converter instance.
14
+ def self.default
15
+ DEFAULT
7
16
  end
8
17
 
9
- def parse(amount, currency = @default_currency)
18
+ # Converts raw input into a `Money` value.
19
+ def parse(amount)
20
+ currency = @static_currency || MoneyAttribute.default_currency
10
21
  case amount
11
- when Mint::Money, NilClass then amount
12
- when Numeric then Mint::Money.from(amount, currency)
13
- when String then Mint.parse(amount, currency)
22
+ when Money, NilClass then amount
23
+ when Numeric then Money.from(amount, currency)
24
+ when String then Money.parse(amount, currency)
14
25
  else raise ArgumentError, "Cannot convert #{amount.inspect} (#{amount.class}) to Money"
15
26
  end
16
27
  end
28
+
17
29
  alias call parse
18
30
  end
19
31
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ class Numeric
5
+ remove_method :to_money if method_defined?(:to_money)
6
+
7
+ def to_money(currency = MoneyAttribute.default_currency) = Money.from(self, currency)
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ class String
5
+ remove_method :to_money if method_defined?(:to_money)
6
+
7
+ def to_money(currency = MoneyAttribute.default_currency) = Money.parse(self, currency)
8
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/current_attributes'
4
+
5
+ module MoneyAttribute
6
+ # Per-request currency container. Set MoneyAttribute::Current.currency in your
7
+ # controller (or a before_action) to override the configured default for that request.
8
+ # Automatically reset after each request by MoneyAttribute::Middleware.
9
+ class Current < ::ActiveSupport::CurrentAttributes
10
+ # @return [String, nil] per-request ISO 4217 currency code override.
11
+ attribute :currency
12
+ end
13
+ end
@@ -1,15 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
+ # :nodoc:
4
5
  module FormBuilderExtension
6
+ # Renders a text input for a composed money attribute.
5
7
  def money_field(method, options = {})
6
8
  money = object.public_send(method)
7
- value = money&.to_fs(:currency)
9
+ value = money&.to_fs
8
10
 
9
11
  @template.text_field_tag(field_name(method), value,
10
12
  { id: field_id(method) }.merge(options))
11
13
  end
12
14
 
15
+ # Renders a number input for a single-column money attribute.
13
16
  def money_amount_field(method, options = {})
14
17
  money_from_column = object.public_send(method)
15
18
  value = money_from_column&.to_d
@@ -1,103 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
+ # :nodoc:
4
5
  module Macro
5
6
  extend ActiveSupport::Concern
6
7
 
7
- class_methods do
8
- def money_attribute(name, currency: MoneyAttribute.default_currency, mapping: nil)
9
- columns = attribute_names
10
- currency = ::Mint::Currency.resolve!(currency)
11
- name = name.to_s
12
- resolved_mapping = mapping || resolve_mapping(name, columns)
8
+ # :nodoc:
9
+ module CompositeClassMethods
10
+ # Normalizes the requested mapping by applying conventions and overrides.
11
+ def resolve_mapping(name, mapping_override)
12
+ override = mapping_override.compact
13
+ override.slice!(:amount, :currency)
14
+ override.transform_values!(&:to_s)
13
15
 
14
- if columns.include?(name) && resolved_mapping.nil?
15
- define_single_column_money_attribute(name, currency)
16
- else
17
- define_composite_money_attribute(name, resolved_mapping, currency)
18
- end
19
- end
20
-
21
- private
16
+ mapping = default_mapping(name).merge(override)
22
17
 
23
- # --- Preparation (no side effects) ---
18
+ assert_columns_exist!(name, mapping)
19
+ mapping
20
+ end
24
21
 
25
- def resolve_mapping(name, columns)
26
- return nil unless columns.include?(name)
22
+ # Returns the default amount/currency mapping for the attribute name.
23
+ def default_mapping(name)
24
+ name = name.to_s
25
+ names = column_names
27
26
 
28
- if columns.include?("#{name}_currency")
29
- { amount: name, currency: :"#{name}_currency" }
30
- elsif columns.include?('currency') && name == 'amount'
31
- { amount: name, currency: :currency }
27
+ if names.include?("#{name}_currency") && names.include?(name)
28
+ { amount: name, currency: "#{name}_currency" }
29
+ elsif name == 'amount' && names.include?('currency')
30
+ { amount: name, currency: 'currency' }
31
+ else
32
+ { amount: "#{name}_amount", currency: "#{name}_currency" }
32
33
  end
33
34
  end
34
35
 
35
- def resolve_composite_for(name, mapping:)
36
- composite = { amount: "#{name}_amount", currency: "#{name}_currency" }
37
-
38
- composite[:amount] = mapping[:amount].to_s if mapping&.key?(:amount)
39
- composite[:currency] = mapping[:currency].to_s if mapping&.key?(:currency)
40
-
41
- assert_columns_exist!(name, composite)
42
- composite
36
+ # Registers the composite money attribute spec for the model.
37
+ def register_composite_spec(name, mapping)
38
+ column = column_for_attribute(mapping[:amount])
39
+ register_money_attribute_spec(
40
+ name,
41
+ kind: :composite,
42
+ amount_col: mapping[:amount],
43
+ currency_col: mapping[:currency],
44
+ amount_type: %i[integer bigint].include?(column.type) ? :integer : :decimal
45
+ )
43
46
  end
44
47
 
45
- def assert_columns_exist!(name, composite)
46
- missing = composite.values - attribute_names
48
+ # Raises when the resolved columns are not present on the model.
49
+ def assert_columns_exist!(name, mapping)
50
+ missing = mapping.values - column_names
47
51
  return if missing.empty?
48
52
 
49
53
  raise ArgumentError,
50
54
  "Could not find columns for :#{name} money attribute. " \
51
- "Expected: #{composite.values.join(', ')}, " \
55
+ "Expected: #{mapping.values.join(', ')}, " \
52
56
  "Found: #{attribute_names.join(', ')}"
53
57
  end
58
+ end
54
59
 
55
- def amount_extractor_for(column_name) = integer_column?(column_name) ? :subunits : :to_d
56
-
57
- def money_constructor_for(amount_column)
58
- default = MoneyAttribute.default_currency
59
- if integer_column?(amount_column)
60
- lambda { |amount, currency|
61
- return nil if amount.nil?
62
-
63
- Mint::Money.from_subunits(amount, currency.presence || default)
64
- }
65
- else
66
- lambda { |amount, currency|
67
- return nil if amount.nil?
68
-
69
- Mint::Money.from(amount, currency.presence || default)
70
- }
71
- end
72
- end
73
-
74
- def integer_column?(column_name)
75
- col = columns.find { |c| c.name == column_name }
76
- %i[integer bigint].include?(col&.type)
77
- end
78
-
79
- # --- Configuration (registers types, normalizers, composed_of) ---
80
-
81
- def define_single_column_money_attribute(name, currency)
82
- column_type = integer_column?(name) ? ActiveRecord::Type::Integer.new : ActiveRecord::Type::Decimal.new
83
- attribute(name.to_sym, :mint_money, currency:, column_type: column_type)
84
- normalizes(name.to_sym, with: Converter.new(currency))
85
- end
86
-
87
- def define_composite_money_attribute(name, mapping, currency)
88
- aggregated = resolve_composite_for(name, mapping:)
60
+ class_methods do
61
+ # Declares a composite money attribute on the model.
62
+ def money_attribute(name, mapping: {})
63
+ mapping = resolve_mapping(name, mapping)
64
+ spec = register_composite_spec(name, mapping)
89
65
 
90
- composed_of(name.to_sym, {
66
+ composed_of(name, {
91
67
  allow_nil: true,
92
68
  class_name: 'Mint::Money',
93
- constructor: money_constructor_for(aggregated[:amount]),
94
- converter: Converter.new(currency),
95
- mapping: {
96
- aggregated[:amount] => amount_extractor_for(aggregated[:amount]),
97
- aggregated[:currency] => :currency_code
98
- }
69
+ constructor: spec.constructor,
70
+ converter: Converter.default,
71
+ mapping: spec.composed_of_mapping
99
72
  })
100
73
  end
101
74
  end
75
+
76
+ included do
77
+ extend CompositeClassMethods
78
+ end
102
79
  end
103
80
  end
@@ -2,61 +2,68 @@
2
2
 
3
3
  module MoneyAttribute
4
4
  module MigrationExtensions
5
+ # :nodoc:
5
6
  module Helper
6
- private
7
+ AMOUNT_CONFIG = {
8
+ crypto_decimal: { type: :decimal, precision: 36, scale: 18 },
9
+ fiat_decimal: { type: :decimal, precision: 20, scale: 4 },
10
+ fiat_integer: { type: :bigint }
11
+ }.freeze
7
12
 
8
- def parse_money_args(accessor, options = {})
9
- name = accessor.to_s
13
+ CURRENCY_MIN_LIMIT = 8
14
+ CURRENCY_DEFAULT_LIMIT = 20
15
+
16
+ private
10
17
 
11
- if options.key?(:amount) && options[:amount].is_a?(Hash)
12
- amount_col = options[:amount][:column]&.to_s || name
13
- opts = options[:amount]
14
- amount_opts = {
15
- type: opts[:type],
16
- null: opts[:null],
17
- default: opts[:default],
18
- precision: opts[:precision],
19
- scale: opts[:scale]
20
- }.compact
21
- else
22
- amount_col = name
23
- amount_opts = {}
18
+ def parse_money_amount_args(accessor, options)
19
+ options ||= {}
20
+ if options.key?(:precision) || options.key?(:scale)
21
+ raise ArgumentError,
22
+ 'precision:/scale: are not configurable — money_attribute uses fixed, ' \
23
+ 'vetted values per type (:crypto_decimal, :fiat_decimal, :fiat_integer) ' \
24
+ 'to prevent under-precision bugs, particularly for crypto amounts.'
24
25
  end
25
26
 
26
- amount_opts[:type] ||= :decimal
27
+ column = (options[:column] || accessor).to_s
27
28
 
28
- if amount_opts[:type] == :decimal && !amount_opts.key?(:precision) && !amount_opts.key?(:scale)
29
- amount_opts[:precision] = 16
30
- amount_opts[:scale] = 4
31
- elsif amount_opts[:type] != :decimal
32
- amount_opts.delete(:precision)
33
- amount_opts.delete(:scale)
29
+ config = AMOUNT_CONFIG[options[:type] || :fiat_decimal]
30
+ unless config
31
+ raise ArgumentError, "Invalid type #{options[:type]}. Use :crypto_decimal, :fiat_decimal or :fiat_integer"
34
32
  end
35
33
 
36
- stripped = name.end_with?('_amount') ? name.sub(/_amount$/, '') : name
37
- default_currency_col = if name == 'amount' && !(options[:currency].is_a?(Hash) && options[:currency][:column])
38
- 'currency'
39
- else
40
- "#{stripped}_currency"
41
- end
42
-
43
- if options.key?(:currency) && options[:currency].is_a?(Hash)
44
- currency_col = options[:currency][:column]&.to_s || default_currency_col
45
- opts = options[:currency]
46
- currency_opts = {
47
- limit: opts[:limit],
48
- null: opts[:null],
49
- default: opts[:default]
50
- }.compact
51
- elsif options[:currency] == false
52
- currency_col = nil
53
- currency_opts = {}
54
- else
55
- currency_col = default_currency_col
56
- currency_opts = {}
34
+ options = { null: options[:null], default: options[:default] }.compact
35
+ [column, config.merge(options)]
36
+ end
37
+
38
+ def parse_currency_args(accessor, options)
39
+ options ||= {}
40
+ limit = (options[:limit] || CURRENCY_DEFAULT_LIMIT).to_i
41
+ if limit < CURRENCY_MIN_LIMIT
42
+ raise ArgumentError,
43
+ "currency limit: #{limit} is too small to hold an ISO 4217 code and crypto popular codes" \
44
+ "(minimum #{CURRENCY_MIN_LIMIT}). Omit limit: to use the default of #{CURRENCY_DEFAULT_LIMIT}, " \
45
+ "or pass a value >= #{CURRENCY_MIN_LIMIT}."
57
46
  end
58
47
 
59
- [amount_col, currency_col, amount_opts, currency_opts]
48
+ column = currency_column_name(accessor, options[:column])
49
+ [column, { limit:, null: options[:null], default: options[:default] }.compact]
50
+ end
51
+
52
+ def currency_column_name(accessor, column_override)
53
+ return column_override.to_s if column_override
54
+
55
+ name = accessor.to_s
56
+ return 'currency' if name == 'amount'
57
+
58
+ radical = name.end_with?('_amount') ? name.sub(/_amount$/, '') : name
59
+ "#{radical}_currency"
60
+ end
61
+
62
+ def parse_money_args(accessor, options = {})
63
+ amount_column, amount_options = parse_money_amount_args(accessor, options[:amount])
64
+ currency_column, currency_options = parse_currency_args(accessor, options[:currency])
65
+
66
+ [amount_column, currency_column, amount_options, currency_options]
60
67
  end
61
68
  end
62
69
  end
@@ -4,21 +4,34 @@ require_relative 'helper'
4
4
 
5
5
  module MoneyAttribute
6
6
  module MigrationExtensions
7
+ # :nodoc:
7
8
  module SchemaStatements
8
9
  include Helper
9
10
 
10
11
  def add_money_attribute(table_name, accessor, options = {})
11
12
  amount_col, currency_col, amount_opts, currency_opts = parse_money_args(accessor, options)
12
13
 
13
- add_column(table_name, amount_col, amount_opts[:type], **amount_opts.except(:type))
14
- add_column(table_name, currency_col, :string, **currency_opts) if currency_col
14
+ type = amount_opts.delete(:type)
15
+ add_column(table_name, amount_col, type, **amount_opts)
16
+ add_column(table_name, currency_col, :string, **currency_opts)
15
17
  end
16
18
 
17
19
  def remove_money_attribute(table_name, accessor, options = {})
18
20
  amount_col, currency_col, = parse_money_args(accessor, options)
19
21
 
20
22
  remove_column(table_name, amount_col)
21
- remove_column(table_name, currency_col) if currency_col
23
+ remove_column(table_name, currency_col)
24
+ end
25
+
26
+ def add_money_amount(table_name, accessor, options = {})
27
+ amount_col, amount_opts = parse_money_amount_args(accessor, options)
28
+
29
+ type = amount_opts.delete(:type)
30
+ add_column(table_name, amount_col, type, **amount_opts)
31
+ end
32
+
33
+ def remove_money_amount(table_name, accessor, options = {})
34
+ remove_column(table_name, (options[:column] || accessor).to_s)
22
35
  end
23
36
  end
24
37
  end
@@ -4,6 +4,7 @@ require_relative 'helper'
4
4
 
5
5
  module MoneyAttribute
6
6
  module MigrationExtensions
7
+ # :nodoc:
7
8
  module TableDefinition
8
9
  include Helper
9
10
 
@@ -11,14 +12,26 @@ module MoneyAttribute
11
12
  amount_col, currency_col, amount_opts, currency_opts = parse_money_args(accessor, options)
12
13
 
13
14
  column(amount_col, amount_opts[:type], **amount_opts.except(:type))
14
- column(currency_col, :string, **currency_opts) if currency_col
15
+ column(currency_col, :string, **currency_opts)
15
16
  end
16
17
 
17
18
  def remove_money_attribute(accessor, options = {})
18
19
  amount_col, currency_col, = parse_money_args(accessor, options)
19
20
 
20
21
  remove_column(amount_col)
21
- remove_column(currency_col) if currency_col
22
+ remove_column(currency_col)
23
+ end
24
+
25
+ def money_amount(accessor, options = {})
26
+ amount_col, amount_opts = parse_money_amount_args(accessor, options)
27
+
28
+ column(amount_col, amount_opts[:type], **amount_opts.except(:type))
29
+ end
30
+
31
+ def remove_money_amount(accessor, options = {})
32
+ amount_col, = parse_money_amount_args(accessor, options)
33
+
34
+ remove_column(amount_col)
22
35
  end
23
36
  end
24
37
  end