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.
- checksums.yaml +4 -4
- data/README.md +166 -72
- data/Rakefile +31 -6
- data/lib/generators/money_attribute/initializer_generator.rb +10 -0
- data/lib/money_attribute/attribute_spec.rb +9 -3
- data/lib/money_attribute/attribute_spec_registry.rb +63 -5
- data/lib/money_attribute/column_type_validations.rb +47 -0
- data/lib/money_attribute/converter.rb +18 -3
- data/lib/money_attribute/core_ext/numeric.rb +10 -1
- data/lib/money_attribute/core_ext/string.rb +12 -2
- data/lib/money_attribute/form_builder_extension.rb +48 -3
- data/lib/money_attribute/macro.rb +96 -7
- data/lib/money_attribute/migration_extensions/helper.rb +61 -1
- data/lib/money_attribute/migration_extensions/schema_statements.rb +89 -9
- data/lib/money_attribute/migration_extensions/table_definition.rb +68 -11
- data/lib/money_attribute/money_amount.rb +30 -2
- data/lib/money_attribute/query/amount_condition.rb +139 -2
- data/lib/money_attribute/query/amount_order.rb +9 -3
- data/lib/money_attribute/query/currency_condition.rb +8 -2
- data/lib/money_attribute/query/helpers.rb +28 -5
- data/lib/money_attribute/query/pick.rb +11 -8
- data/lib/money_attribute/query/pluck.rb +11 -15
- data/lib/money_attribute/query/sum.rb +15 -3
- data/lib/money_attribute/query.rb +16 -11
- data/lib/money_attribute/railtie.rb +40 -0
- data/lib/money_attribute/type.rb +6 -2
- data/lib/money_attribute/version.rb +2 -1
- data/lib/money_attribute.rb +1 -0
- metadata +5 -4
|
@@ -4,34 +4,91 @@ require_relative 'helper'
|
|
|
4
4
|
|
|
5
5
|
module MoneyAttribute
|
|
6
6
|
module MigrationExtensions
|
|
7
|
-
#
|
|
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
|
-
|
|
42
|
+
amount_column, currency_column, amount_opts, currency_opts = parse_money_args(accessor, options)
|
|
13
43
|
|
|
14
|
-
column(
|
|
15
|
-
column(
|
|
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
|
-
|
|
56
|
+
amount_column, currency_column, = parse_money_args(accessor, options)
|
|
20
57
|
|
|
21
|
-
remove_column(
|
|
22
|
-
remove_column(
|
|
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
|
-
|
|
77
|
+
amount_column, amount_opts = parse_money_amount_args(accessor, options)
|
|
27
78
|
|
|
28
|
-
column(
|
|
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
|
-
|
|
89
|
+
amount_column, = parse_money_amount_args(accessor, options)
|
|
33
90
|
|
|
34
|
-
remove_column(
|
|
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
|
-
#
|
|
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,
|
|
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
|
-
#
|
|
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.
|
|
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
|
-
#
|
|
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.
|
|
22
|
+
order(spec.currency_column => :asc, spec.amount_column => direction)
|
|
17
23
|
else
|
|
18
|
-
order(spec.
|
|
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
|
-
#
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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.
|
|
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
|
-
|
|
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
|
-
#
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
#
|
|
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
|
|
46
|
-
def where_amount(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
#
|
|
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
|