money_attribute 1.2.0 → 1.3.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.
@@ -4,34 +4,91 @@ require_relative 'helper'
4
4
 
5
5
  module MoneyAttribute
6
6
  module MigrationExtensions
7
- # :nodoc:
7
+ # Migration DSL methods for use inside +create_table+ and +change_table+
8
+ # blocks.
9
+ #
10
+ # Included into both +ActiveRecord::ConnectionAdapters::TableDefinition+
11
+ # and +ActiveRecord::ConnectionAdapters::Table+.
12
+ #
13
+ # @example Inside a +create_table+ block
14
+ # create_table :products do |t|
15
+ # t.string :name
16
+ # t.money_attribute :price
17
+ # t.money_amount :discount, type: :fiat_integer
18
+ # end
19
+ #
20
+ # @example Inside a +change_table+ block
21
+ # change_table :products do |t|
22
+ # t.money_attribute :price, amount: { type: :fiat_integer }
23
+ # t.remove_money_attribute :old_price
24
+ # end
8
25
  module TableDefinition
9
26
  include Helper
10
27
 
28
+ # Adds amount and currency columns within a table definition.
29
+ #
30
+ # @param accessor [Symbol, String] the money attribute accessor name
31
+ # @param options [Hash] migration options
32
+ # @option options [Hash] :amount amount column options
33
+ # (+:column+, +:type+, +:null+, +:default+)
34
+ # @option options [Hash] :currency currency column options
35
+ # (+:column+, +:limit+, +:null+, +:default+)
36
+ # @return [void]
37
+ #
38
+ # @example
39
+ # t.money_attribute :price
40
+ # t.money_attribute :price, amount: { type: :fiat_integer }
11
41
  def money_attribute(accessor, options = {})
12
- amount_col, currency_col, amount_opts, currency_opts = parse_money_args(accessor, options)
42
+ amount_column, currency_column, amount_opts, currency_opts = parse_money_args(accessor, options)
13
43
 
14
- column(amount_col, amount_opts[:type], **amount_opts.except(:type))
15
- column(currency_col, :string, **currency_opts)
44
+ column(amount_column, amount_opts[:type], **amount_opts.except(:type))
45
+ column(currency_column, :string, **currency_opts)
16
46
  end
17
47
 
48
+ # Removes amount and currency columns within a table definition.
49
+ #
50
+ # @param accessor [Symbol, String] the money attribute accessor name
51
+ # @param options [Hash] migration options
52
+ # @option options [Hash] :amount amount column options (+:column+)
53
+ # @option options [Hash] :currency currency column options (+:column+)
54
+ # @return [void]
18
55
  def remove_money_attribute(accessor, options = {})
19
- amount_col, currency_col, = parse_money_args(accessor, options)
56
+ amount_column, currency_column, = parse_money_args(accessor, options)
20
57
 
21
- remove_column(amount_col)
22
- remove_column(currency_col)
58
+ remove_column(amount_column)
59
+ remove_column(currency_column)
23
60
  end
24
61
 
62
+ # Adds a single amount column within a table definition.
63
+ #
64
+ # @param accessor [Symbol, String] the money attribute accessor name
65
+ # @param options [Hash] column options
66
+ # @option options [Symbol] :column explicit column name override
67
+ # @option options [Symbol] :type amount type (+:fiat_decimal+,
68
+ # +:crypto_decimal+, +:fiat_integer+)
69
+ # @option options [Boolean] :null whether the column allows NULL
70
+ # @option options [Object] :default default value for the column
71
+ # @return [void]
72
+ #
73
+ # @example
74
+ # t.money_amount :discount
75
+ # t.money_amount :bonus, column: :bonus_cents, type: :fiat_integer
25
76
  def money_amount(accessor, options = {})
26
- amount_col, amount_opts = parse_money_amount_args(accessor, options)
77
+ amount_column, amount_opts = parse_money_amount_args(accessor, options)
27
78
 
28
- column(amount_col, amount_opts[:type], **amount_opts.except(:type))
79
+ column(amount_column, amount_opts[:type], **amount_opts.except(:type))
29
80
  end
30
81
 
82
+ # Removes a single amount column within a table definition.
83
+ #
84
+ # @param accessor [Symbol, String] the money attribute accessor name
85
+ # @param options [Hash] column options
86
+ # @option options [Symbol] :column explicit column name override
87
+ # @return [void]
31
88
  def remove_money_amount(accessor, options = {})
32
- amount_col, = parse_money_amount_args(accessor, options)
89
+ amount_column, = parse_money_amount_args(accessor, options)
33
90
 
34
- remove_column(amount_col)
91
+ remove_column(amount_column)
35
92
  end
36
93
  end
37
94
  end
@@ -1,12 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
- # :nodoc:
4
+ # Declares fixed-currency money attributes on Active Record models.
5
+ #
6
+ # Provides the +money_amount+ class method which wires a single backing
7
+ # column to a +Mint::Money+ value object using a custom attribute type and a
8
+ # normalizer. The application default currency (or {Current} per-request
9
+ # override) applies to all rows.
10
+ #
11
+ # @example
12
+ # class SimpleOffer < ApplicationRecord
13
+ # money_amount :price
14
+ # end
5
15
  module MoneyAmount
6
16
  extend ActiveSupport::Concern
7
17
 
8
18
  class_methods do
19
+ include ColumnTypeValidations
20
+
9
21
  # Declares a fixed-currency money attribute backed by a single column.
22
+ #
23
+ # The column type determines the storage unit: integer/bigint stores
24
+ # subunits, decimal stores the unit value. No currency column is created —
25
+ # the application default currency applies to every row.
26
+ #
27
+ # @param name [Symbol, String] the money attribute accessor name
28
+ # @return [void]
29
+ # @raise [ArgumentError] if the column does not exist or has an
30
+ # unsupported type
31
+ #
32
+ # @example
33
+ # class SimpleOffer < ApplicationRecord
34
+ # money_amount :price
35
+ # end
10
36
  def money_amount(name)
11
37
  column = column_for_attribute(name)
12
38
 
@@ -16,6 +42,8 @@ module MoneyAttribute
16
42
  "Add a column named '#{name}' or use a different accessor name."
17
43
  end
18
44
 
45
+ assert_valid_amount_column!(name, name, column)
46
+
19
47
  if %i[integer bigint].include?(column.type)
20
48
  amount_type = :integer
21
49
  type_class = IntegerAmountType
@@ -26,7 +54,7 @@ module MoneyAttribute
26
54
 
27
55
  attribute(name, type_class.new)
28
56
  normalizes(name, with: Converter.default)
29
- register_money_attribute_spec(name, kind: :single, amount_col: name, amount_type: amount_type)
57
+ register_money_attribute_spec(name, kind: :single, amount_column: name, amount_type: amount_type)
30
58
  end
31
59
  end
32
60
  end
@@ -1,24 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
- # :nodoc:
4
+ # Internal amount-filter resolution for the +where_amount+ query helper.
5
+ #
6
+ # Supports two input forms: a hash of attribute/value pairs resolved to Arel
7
+ # predicates, and a SQL string with +?+ placeholders where attribute names
8
+ # are substituted for backing columns and +Mint::Money+ binds are decomposed.
9
+ #
10
+ # @api private
5
11
  module AmountCondition
12
+ ALLOWED_KEYWORDS = %w[and or not is null].to_set.freeze
13
+ QueryPlan = Struct.new(:sql, :value_specs, keyword_init: true)
14
+
6
15
  # Builds an amount filter for the registered money attribute.
7
16
  #
8
17
  # @param attr [Symbol] the money attribute name
9
18
  # @param value [Mint::Money, Numeric, Range, Array] the filter value
10
19
  # @return [ActiveRecord::Relation]
11
20
  # @raise [ArgumentError] if the attribute is not a registered money attribute
21
+ # @api private
12
22
  def resolve_amount_condition(attr, value)
13
23
  spec = money_attribute_spec!(attr)
14
- col = arel_table[spec.amount_col]
24
+ col = arel_table[spec.amount_column]
15
25
 
16
26
  where(build_amount_predicate(col, spec, value))
17
27
  end
18
28
 
29
+ # Builds an amount filter using a SQL string with +?+ placeholders.
30
+ #
31
+ # Only money attribute names, +and+, +or+, +not+, +is+, and +null+ are
32
+ # allowed as identifiers. +Mint::Money+ bind values are decomposed to
33
+ # raw storage values automatically.
34
+ #
35
+ # @param sql [String] SQL fragment using attribute names and +?+ placeholders
36
+ # @param values [Array] bind values
37
+ # @return [ActiveRecord::Relation]
38
+ # @raise [ArgumentError] on unknown identifiers or placeholder mismatch
39
+ # @api private
40
+ def resolve_amount_condition_from_sql(sql, *values)
41
+ plan = klass.money_attribute_query_plan_cache.fetch_or_store(sql) do
42
+ compile_query_plan(sql)
43
+ end
44
+ decomposed = decompose_values(values, plan.value_specs)
45
+
46
+ where(plan.sql, *decomposed)
47
+ end
48
+
19
49
  private
20
50
 
51
+ # Compiles a string query into substituted SQL and bind metadata.
52
+ #
53
+ # @param sql [String] the SQL fragment
54
+ # @return [QueryPlan] the compiled query plan
55
+ # @api private
56
+ def compile_query_plan(sql)
57
+ specs = klass.money_attribute_specs
58
+ value_specs = parse_sql_value_specs(sql, specs)
59
+
60
+ QueryPlan.new(
61
+ sql: substitute_attribute_names(sql, specs),
62
+ value_specs: value_specs.freeze
63
+ ).freeze
64
+ end
65
+
21
66
  # Builds an Arel predicate for the given amount value.
67
+ #
68
+ # @param col [Arel::Attributes::Attribute] the amount column node
69
+ # @param spec [AttributeSpec] the money attribute spec
70
+ # @param value [Mint::Money, Numeric, Range, Array] the filter value
71
+ # @return [Arel::Nodes::Node] the predicate
72
+ # @api private
22
73
  def build_amount_predicate(col, spec, value)
23
74
  case value
24
75
  when Range
@@ -39,10 +90,96 @@ module MoneyAttribute
39
90
  # so we must pre-normalize Money to the raw storage value (subunits or decimal).
40
91
  # Single-column attributes: the column has a registered Type that handles
41
92
  # serialization, so we pass Money objects through directly to avoid double conversion.
93
+ #
94
+ # @param spec [AttributeSpec] the money attribute spec
95
+ # @param value [Object] the value to normalize
96
+ # @return [Object] the normalized value
97
+ # @api private
42
98
  def normalize_amount_value(spec, value)
43
99
  return value unless spec.composite?
44
100
 
45
101
  spec.normalize_query_value(value)
46
102
  end
103
+
104
+ # Validates identifiers and associates placeholders with the nearest
105
+ # preceding money attribute in one left-to-right pass.
106
+ #
107
+ # @param sql [String] the SQL fragment
108
+ # @param specs [Hash{String => AttributeSpec}] the money attribute specs
109
+ # @return [Array<AttributeSpec>] one spec per bind value
110
+ # @raise [ArgumentError] on an unknown identifier or unassociated placeholder
111
+ # @api private
112
+ def parse_sql_value_specs(sql, specs)
113
+ current_spec = nil
114
+ value_specs = []
115
+
116
+ sql.scan(/[a-z_]\w*|\?/i) do |token|
117
+ if token == '?'
118
+ raise ArgumentError, "No money attribute found before '?' in: #{sql.inspect}" unless current_spec
119
+
120
+ value_specs << current_spec
121
+ next
122
+ end
123
+
124
+ word = token.downcase
125
+ next if ALLOWED_KEYWORDS.include?(word)
126
+
127
+ current_spec = specs[word]
128
+ raise ArgumentError, "'#{token}' is not a money attribute on #{klass.name}" unless current_spec
129
+ end
130
+
131
+ value_specs
132
+ end
133
+
134
+ # Decomposes +Mint::Money+ bind values to raw storage values using their
135
+ # positional specs. Unlike +normalize_query_value+ (which relies on the
136
+ # custom type for single-column attributes), this always decomposes since
137
+ # raw SQL bind parameters don't resolve custom types.
138
+ #
139
+ # @param values [Array] the bind values
140
+ # @param value_specs [Array<AttributeSpec>] one spec per bind value
141
+ # @return [Array] decomposed bind values
142
+ # @raise [ArgumentError] if the number of values and specs differs
143
+ # @api private
144
+ def decompose_values(values, value_specs)
145
+ if values.size != value_specs.size
146
+ raise ArgumentError, "Expected #{value_specs.size} bind value(s), got #{values.size}"
147
+ end
148
+
149
+ values.zip(value_specs).map do |val, spec|
150
+ if val.is_a?(Mint::Money)
151
+ spec.integer_amount? ? val.subunits : val.to_d
152
+ else
153
+ val
154
+ end
155
+ end
156
+ end
157
+
158
+ # Replaces attribute names with their backing amount column names in the SQL.
159
+ #
160
+ # Only attributes whose name differs from their amount column are
161
+ # substituted; the rest are already valid column references.
162
+ #
163
+ # @param sql [String] the SQL fragment
164
+ # @param specs [Hash{String => AttributeSpec}] the money attribute specs
165
+ # @return [String] the SQL with attribute names replaced by column names
166
+ # @api private
167
+ def substitute_attribute_names(sql, specs)
168
+ to_sub = specs_to_substitute(specs)
169
+ return sql if to_sub.empty?
170
+
171
+ lookup = to_sub.to_h { |s| [s.name.downcase, s.amount_column] }
172
+ pattern = /\b(#{to_sub.map { |s| Regexp.escape(s.name) }.join('|')})\b/i
173
+ sql.gsub(pattern) { |match| lookup[match.downcase] }
174
+ end
175
+
176
+ # Returns the specs whose attribute name differs from their amount column.
177
+ #
178
+ # @param specs [Hash{String => AttributeSpec}] the money attribute specs
179
+ # @return [Array<AttributeSpec>] specs needing SQL substitution
180
+ # @api private
181
+ def specs_to_substitute(specs)
182
+ specs.values.reject { |s| s.name == s.amount_column }
183
+ end
47
184
  end
48
185
  end
@@ -1,21 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
- # :nodoc:
4
+ # Internal ordering resolution for the +order_by_amount+ query helper.
5
+ #
6
+ # @api private
5
7
  module AmountOrder
6
8
  # Builds an amount ordering for the registered money attribute.
7
9
  #
10
+ # Composite attributes order by currency ASC first, then amount in the
11
+ # requested direction. Single-column attributes order by amount only.
12
+ #
8
13
  # @param attr [Symbol] the money attribute name
9
14
  # @param direction [Symbol] +:asc+ or +:desc+
10
15
  # @return [ActiveRecord::Relation]
11
16
  # @raise [ArgumentError] if the attribute is not a registered money attribute
17
+ # @api private
12
18
  def resolve_amount_order(attr, direction)
13
19
  spec = money_attribute_spec!(attr)
14
20
 
15
21
  if spec.composite?
16
- order(spec.currency_col => :asc, spec.amount_col => direction)
22
+ order(spec.currency_column => :asc, spec.amount_column => direction)
17
23
  else
18
- order(spec.amount_col => direction)
24
+ order(spec.amount_column => direction)
19
25
  end
20
26
  end
21
27
  end
@@ -1,14 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
- # :nodoc:
4
+ # Internal currency-filter resolution for the +where_currency+ query helper.
5
+ #
6
+ # @api private
5
7
  module CurrencyCondition
6
8
  # Builds a currency filter for the registered money attribute.
7
9
  #
10
+ # Only composite attributes have a currency column, so single-column
11
+ # attributes raise.
12
+ #
8
13
  # @param attr [Symbol] the money attribute name
9
14
  # @param currency [String, Mint::Currency] the currency code or object
10
15
  # @return [ActiveRecord::Relation]
11
16
  # @raise [ArgumentError] if the attribute is not a composite money attribute
17
+ # @api private
12
18
  def resolve_currency_condition(attr, currency)
13
19
  spec = money_attribute_spec!(attr)
14
20
 
@@ -17,7 +23,7 @@ module MoneyAttribute
17
23
  end
18
24
 
19
25
  code = currency.is_a?(Mint::Currency) ? currency.code : currency.to_s
20
- where(spec.currency_col => code)
26
+ where(spec.currency_column => code)
21
27
  end
22
28
  end
23
29
  end
@@ -1,13 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
- # :nodoc:
4
+ # Internal helpers shared by the query sub-modules.
5
+ #
6
+ # @api private
5
7
  module QueryHelpers
6
8
  # Returns the registered money attribute spec or raises when missing.
7
9
  #
8
10
  # @param attr [Symbol, String] the money attribute name
9
11
  # @return [AttributeSpec]
10
12
  # @raise [ArgumentError] if the attribute is not registered
13
+ # @api private
11
14
  def money_attribute_spec!(attr)
12
15
  spec = klass.money_attribute_spec(attr)
13
16
  raise ArgumentError, "#{attr} is not a money attribute on #{klass.name}" unless spec
@@ -15,16 +18,36 @@ module MoneyAttribute
15
18
  spec
16
19
  end
17
20
 
21
+ # Extracts money values for all specs from a single result row.
22
+ #
23
+ # @param row [Array] the flat row from +pluck+ or +pick+
24
+ # @param specs [Array<AttributeSpec>] the money attribute specs
25
+ # @return [Array] the extracted money values
26
+ # @api private
27
+ def extract_money_row(row, specs)
28
+ cursor = 0
29
+
30
+ specs.map do |spec|
31
+ value, cursor = extract_attribute_value(row, spec, cursor)
32
+ value
33
+ end
34
+ end
35
+
36
+ private
37
+
18
38
  # Extracts a single value from a flat row at the given cursor position.
19
39
  #
20
40
  # @param row [Array] the flat row from +pluck+ or +pick+
21
41
  # @param spec [AttributeSpec] the money attribute spec
22
42
  # @param cursor [Integer] current position in the row array
23
43
  # @return [Array(Object, Integer)] the extracted value and updated cursor
24
- def extract_pick_value(row, spec, cursor)
25
- return [row[cursor], cursor + 1] if spec.single?
26
-
27
- [spec.build_money(row[cursor], row[cursor + 1]), cursor + 2]
44
+ # @api private
45
+ def extract_attribute_value(row, spec, cursor)
46
+ if spec.single?
47
+ [row[cursor], cursor + 1]
48
+ else
49
+ [spec.build_money(row[cursor], row[cursor + 1]), cursor + 2]
50
+ end
28
51
  end
29
52
  end
30
53
  end
@@ -1,13 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
- # :nodoc:
4
+ # Internal pick resolution for the +pick_amount+ query helper.
5
+ #
6
+ # @api private
5
7
  module PickAmount
6
8
  # Picks money-aware amounts for one or more attributes.
7
9
  #
8
10
  # @param attrs [Array<Symbol>] one or more registered money attribute names
9
11
  # @return [Mint::Money, Array, nil] Money for a single attribute, row array for multiple, nil if empty
10
12
  # @raise [ArgumentError] if any attribute is not a registered money attribute
13
+ # @api private
11
14
  def pick_amount(*attrs)
12
15
  raise ArgumentError, 'No attribute specified' if attrs.empty?
13
16
 
@@ -17,22 +20,22 @@ module MoneyAttribute
17
20
  raw = pick(*specs.flat_map(&:columns))
18
21
  return unless raw
19
22
 
20
- cursor = 0
21
- specs.map do |spec|
22
- value, cursor = extract_pick_value(raw, spec, cursor)
23
- value
24
- end
23
+ extract_money_row(raw, specs)
25
24
  end
26
25
 
27
26
  private
28
27
 
29
28
  # Picks a single money-aware attribute and returns a single value.
29
+ #
30
+ # @param spec [AttributeSpec] the money attribute spec
31
+ # @return [Mint::Money, Object, nil] the composed Money, the raw value for
32
+ # single-column attributes, or nil when the relation is empty
33
+ # @api private
30
34
  def pick_single_amount(spec)
31
35
  raw = pick(*spec.columns)
32
36
  return unless raw
33
- return raw if spec.single?
34
37
 
35
- spec.build_money(raw[0], raw[1])
38
+ spec.single? ? raw : spec.build_money(raw[0], raw[1])
36
39
  end
37
40
  end
38
41
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
- # :nodoc:
4
+ # Internal pluck resolution for the +pluck_amount+ query helper.
5
+ #
6
+ # @api private
5
7
  module PluckAmount
6
8
  # Plucks money-aware amounts for one or more attributes.
7
9
  #
@@ -9,6 +11,7 @@ module MoneyAttribute
9
11
  # @return [Array<Mint::Money>] for a single attribute
10
12
  # @return [Array<Array>] for multiple attributes, one row array per attribute
11
13
  # @raise [ArgumentError] if any attribute is not a registered money attribute
14
+ # @api private
12
15
  def pluck_amount(*attrs)
13
16
  raise ArgumentError, 'No attribute specified' if attrs.empty?
14
17
 
@@ -22,22 +25,15 @@ module MoneyAttribute
22
25
  private
23
26
 
24
27
  # Plucks a single money-aware attribute and returns money values.
28
+ #
29
+ # @param spec [AttributeSpec] the money attribute spec
30
+ # @return [Array] raw values for single-column attributes, composed
31
+ # +Mint::Money+ values for composite attributes
32
+ # @api private
25
33
  def pluck_single_amount(spec)
26
- return pluck(spec.amount_col) if spec.single?
27
-
28
- pluck(spec.amount_col, spec.currency_col).map do |amount, currency|
29
- spec.build_money(amount, currency)
30
- end
31
- end
32
-
33
- # Rebuilds a result row for multi-attribute plucks.
34
- def extract_money_row(row, specs)
35
- cursor = 0
34
+ return pluck(spec.amount_column) if spec.single?
36
35
 
37
- specs.map do |spec|
38
- value, cursor = extract_pick_value(row, spec, cursor)
39
- value
40
- end
36
+ pluck(spec.amount_column, spec.currency_column).map { |amount, currency| spec.build_money(amount, currency) }
41
37
  end
42
38
  end
43
39
  end
@@ -1,13 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
- # :nodoc:
4
+ # Internal sum resolution for the +sum_amount+ query helper.
5
+ #
6
+ # @api private
5
7
  module SumAmount
6
8
  # Sums money-aware amounts for a single attribute.
7
9
  #
8
10
  # @param attr [Symbol] a registered money attribute name
9
11
  # @return [Array<Mint::Money>] one Money per currency (or one for single-column attributes)
10
12
  # @raise [ArgumentError] if the attribute is not a registered money attribute
13
+ # @api private
11
14
  def sum_amount(attr)
12
15
  raise ArgumentError, 'No attribute specified' if attr.nil?
13
16
 
@@ -22,8 +25,13 @@ module MoneyAttribute
22
25
  private
23
26
 
24
27
  # Sums a composite attribute grouped by currency.
28
+ #
29
+ # @param spec [AttributeSpec] the money attribute spec
30
+ # @return [Array<Mint::Money>] one +Mint::Money+ per currency, sorted by
31
+ # currency code, or a single zero-value Money when no rows match
32
+ # @api private
25
33
  def resolve_composite_sum(spec)
26
- totals = group(spec.currency_col).sum(spec.amount_col)
34
+ totals = group(spec.currency_column).sum(spec.amount_column)
27
35
  return [spec.build_money(0, MoneyAttribute.default_currency)] if totals.empty?
28
36
 
29
37
  totals.map { |code, amount| spec.build_money(amount, code) }
@@ -31,8 +39,12 @@ module MoneyAttribute
31
39
  end
32
40
 
33
41
  # Sums a fixed-currency single-column attribute.
42
+ #
43
+ # @param spec [AttributeSpec] the money attribute spec
44
+ # @return [Array<Mint::Money>] a single +Mint::Money+ in the default currency
45
+ # @api private
34
46
  def resolve_single_sum(spec)
35
- total = sum(spec.amount_col)
47
+ total = sum(spec.amount_column)
36
48
 
37
49
  [spec.build_money(total, MoneyAttribute.default_currency)]
38
50
  end
@@ -35,18 +35,21 @@ module MoneyAttribute
35
35
 
36
36
  # Filters by amount for one or more money attributes.
37
37
  #
38
- # Accepts +Mint::Money+ objects, numeric values (decimal columns), Ranges, or Arrays.
38
+ # Accepts a hash of conditions or a SQL string with +?+ placeholders.
39
+ # Hash: +{ attr: value }+, supports +Mint::Money+, +Range+, +Array+.
40
+ # SQL: only money attribute names, +and+, +or+, +not+, +is+, +null+.
39
41
  #
40
- # Offer.where_amount(price: 10.dollars..100.dollars)
41
- # Offer.where_amount(total: [10, 20, 30])
42
- #
43
- # @param conditions [Hash{Symbol => Numeric, Range, Array}] attribute name to filter value
42
+ # @param args [Array] a condition hash, or a SQL string with bind values
44
43
  # @return [ActiveRecord::Relation]
45
- # @raise [ArgumentError] if the attribute is not a registered money attribute
46
- def where_amount(conditions)
47
- scope = all
48
- conditions.each { |attr, value| scope = scope.resolve_amount_condition(attr, value) }
49
- scope
44
+ # @raise [ArgumentError] if an identifier is not a registered money attribute
45
+ def where_amount(*args)
46
+ if args.first.is_a?(Hash)
47
+ scope = all
48
+ args.first.each { |attr, value| scope = scope.resolve_amount_condition(attr, value) }
49
+ scope
50
+ else
51
+ all.resolve_amount_condition_from_sql(*args)
52
+ end
50
53
  end
51
54
 
52
55
  # Orders by amount for one or more money attributes.
@@ -101,7 +104,9 @@ module MoneyAttribute
101
104
  end
102
105
  end
103
106
 
104
- # :nodoc:
107
+ # Internal mixin combining all query sub-modules onto relations.
108
+ #
109
+ # @api private
105
110
  module QueryMethods
106
111
  include QueryHelpers
107
112
  include CurrencyCondition