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.
- checksums.yaml +4 -4
- data/README.md +257 -105
- data/Rakefile +50 -6
- data/lib/generators/templates/money_attribute.rb +20 -12
- data/lib/money_attribute/attribute_spec.rb +78 -0
- data/lib/money_attribute/attribute_spec_registry.rb +50 -0
- data/lib/money_attribute/configuration.rb +44 -17
- data/lib/money_attribute/converter.rb +18 -6
- data/lib/money_attribute/core_ext/numeric.rb +8 -0
- data/lib/money_attribute/core_ext/string.rb +8 -0
- data/lib/money_attribute/current.rb +13 -0
- data/lib/money_attribute/form_builder_extension.rb +4 -1
- data/lib/money_attribute/macro.rb +50 -73
- data/lib/money_attribute/migration_extensions/helper.rb +52 -45
- data/lib/money_attribute/migration_extensions/schema_statements.rb +16 -3
- data/lib/money_attribute/migration_extensions/table_definition.rb +15 -2
- data/lib/money_attribute/money_amount.rb +33 -0
- data/lib/money_attribute/query/amount_condition.rb +48 -0
- data/lib/money_attribute/query/amount_order.rb +22 -0
- data/lib/money_attribute/query/currency_condition.rb +23 -0
- data/lib/money_attribute/query/helpers.rb +30 -0
- data/lib/money_attribute/query/pick.rb +38 -0
- data/lib/money_attribute/query/pluck.rb +43 -0
- data/lib/money_attribute/query/sum.rb +40 -0
- data/lib/money_attribute/query.rb +120 -0
- data/lib/money_attribute/railtie.rb +32 -16
- data/lib/money_attribute/type.rb +42 -36
- data/lib/money_attribute/version.rb +1 -1
- data/lib/money_attribute.rb +17 -1
- metadata +20 -7
- data/lib/money_attribute/core_ext.rb +0 -15
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MoneyAttribute
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module MoneyAmount
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
# Declares a fixed-currency money attribute backed by a single column.
|
|
10
|
+
def money_amount(name)
|
|
11
|
+
column = column_for_attribute(name)
|
|
12
|
+
|
|
13
|
+
unless column
|
|
14
|
+
raise ArgumentError,
|
|
15
|
+
"Column '#{name}' does not exist on this table. " \
|
|
16
|
+
"Add a column named '#{name}' or use a different accessor name."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if %i[integer bigint].include?(column.type)
|
|
20
|
+
amount_type = :integer
|
|
21
|
+
type_class = IntegerAmountType
|
|
22
|
+
else
|
|
23
|
+
amount_type = :decimal
|
|
24
|
+
type_class = DecimalAmountType
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attribute(name, type_class.new)
|
|
28
|
+
normalizes(name, with: Converter.default)
|
|
29
|
+
register_money_attribute_spec(name, kind: :single, amount_col: name, amount_type: amount_type)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MoneyAttribute
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module AmountCondition
|
|
6
|
+
# Builds an amount filter for the registered money attribute.
|
|
7
|
+
#
|
|
8
|
+
# @param attr [Symbol] the money attribute name
|
|
9
|
+
# @param value [Mint::Money, Numeric, Range, Array] the filter value
|
|
10
|
+
# @return [ActiveRecord::Relation]
|
|
11
|
+
# @raise [ArgumentError] if the attribute is not a registered money attribute
|
|
12
|
+
def resolve_amount_condition(attr, value)
|
|
13
|
+
spec = money_attribute_spec!(attr)
|
|
14
|
+
col = arel_table[spec.amount_col]
|
|
15
|
+
|
|
16
|
+
where(build_amount_predicate(col, spec, value))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# Builds an Arel predicate for the given amount value.
|
|
22
|
+
def build_amount_predicate(col, spec, value)
|
|
23
|
+
case value
|
|
24
|
+
when Range
|
|
25
|
+
low = normalize_amount_value(spec, value.begin)
|
|
26
|
+
high = normalize_amount_value(spec, value.end)
|
|
27
|
+
pred = col.gteq(low)
|
|
28
|
+
value.exclude_end? ? pred.and(col.lt(high)) : pred.and(col.lteq(high))
|
|
29
|
+
when Array
|
|
30
|
+
col.in(value.map { |v| normalize_amount_value(spec, v) })
|
|
31
|
+
else
|
|
32
|
+
col.eq(normalize_amount_value(spec, value))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Normalizes a scalar value for Arel comparison.
|
|
37
|
+
#
|
|
38
|
+
# Composite attributes: the amount column is a plain column with no custom Type,
|
|
39
|
+
# so we must pre-normalize Money to the raw storage value (subunits or decimal).
|
|
40
|
+
# Single-column attributes: the column has a registered Type that handles
|
|
41
|
+
# serialization, so we pass Money objects through directly to avoid double conversion.
|
|
42
|
+
def normalize_amount_value(spec, value)
|
|
43
|
+
return value unless spec.composite?
|
|
44
|
+
|
|
45
|
+
spec.normalize_query_value(value)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MoneyAttribute
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module AmountOrder
|
|
6
|
+
# Builds an amount ordering for the registered money attribute.
|
|
7
|
+
#
|
|
8
|
+
# @param attr [Symbol] the money attribute name
|
|
9
|
+
# @param direction [Symbol] +:asc+ or +:desc+
|
|
10
|
+
# @return [ActiveRecord::Relation]
|
|
11
|
+
# @raise [ArgumentError] if the attribute is not a registered money attribute
|
|
12
|
+
def resolve_amount_order(attr, direction)
|
|
13
|
+
spec = money_attribute_spec!(attr)
|
|
14
|
+
|
|
15
|
+
if spec.composite?
|
|
16
|
+
order(spec.currency_col => :asc, spec.amount_col => direction)
|
|
17
|
+
else
|
|
18
|
+
order(spec.amount_col => direction)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MoneyAttribute
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module CurrencyCondition
|
|
6
|
+
# Builds a currency filter for the registered money attribute.
|
|
7
|
+
#
|
|
8
|
+
# @param attr [Symbol] the money attribute name
|
|
9
|
+
# @param currency [String, Mint::Currency] the currency code or object
|
|
10
|
+
# @return [ActiveRecord::Relation]
|
|
11
|
+
# @raise [ArgumentError] if the attribute is not a composite money attribute
|
|
12
|
+
def resolve_currency_condition(attr, currency)
|
|
13
|
+
spec = money_attribute_spec!(attr)
|
|
14
|
+
|
|
15
|
+
unless spec.composite?
|
|
16
|
+
raise ArgumentError, "#{klass.name}.#{attr} is a money_amount attribute with no currency column"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
code = currency.is_a?(Mint::Currency) ? currency.code : currency.to_s
|
|
20
|
+
where(spec.currency_col => code)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MoneyAttribute
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module QueryHelpers
|
|
6
|
+
# Returns the registered money attribute spec or raises when missing.
|
|
7
|
+
#
|
|
8
|
+
# @param attr [Symbol, String] the money attribute name
|
|
9
|
+
# @return [AttributeSpec]
|
|
10
|
+
# @raise [ArgumentError] if the attribute is not registered
|
|
11
|
+
def money_attribute_spec!(attr)
|
|
12
|
+
spec = klass.money_attribute_spec(attr)
|
|
13
|
+
raise ArgumentError, "#{attr} is not a money attribute on #{klass.name}" unless spec
|
|
14
|
+
|
|
15
|
+
spec
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Extracts a single value from a flat row at the given cursor position.
|
|
19
|
+
#
|
|
20
|
+
# @param row [Array] the flat row from +pluck+ or +pick+
|
|
21
|
+
# @param spec [AttributeSpec] the money attribute spec
|
|
22
|
+
# @param cursor [Integer] current position in the row array
|
|
23
|
+
# @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]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MoneyAttribute
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module PickAmount
|
|
6
|
+
# Picks money-aware amounts for one or more attributes.
|
|
7
|
+
#
|
|
8
|
+
# @param attrs [Array<Symbol>] one or more registered money attribute names
|
|
9
|
+
# @return [Mint::Money, Array, nil] Money for a single attribute, row array for multiple, nil if empty
|
|
10
|
+
# @raise [ArgumentError] if any attribute is not a registered money attribute
|
|
11
|
+
def pick_amount(*attrs)
|
|
12
|
+
raise ArgumentError, 'No attribute specified' if attrs.empty?
|
|
13
|
+
|
|
14
|
+
specs = attrs.map { |attr| money_attribute_spec!(attr) }
|
|
15
|
+
return pick_single_amount(specs.first) if specs.length == 1
|
|
16
|
+
|
|
17
|
+
raw = pick(*specs.flat_map(&:columns))
|
|
18
|
+
return unless raw
|
|
19
|
+
|
|
20
|
+
cursor = 0
|
|
21
|
+
specs.map do |spec|
|
|
22
|
+
value, cursor = extract_pick_value(raw, spec, cursor)
|
|
23
|
+
value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Picks a single money-aware attribute and returns a single value.
|
|
30
|
+
def pick_single_amount(spec)
|
|
31
|
+
raw = pick(*spec.columns)
|
|
32
|
+
return unless raw
|
|
33
|
+
return raw if spec.single?
|
|
34
|
+
|
|
35
|
+
spec.build_money(raw[0], raw[1])
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MoneyAttribute
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module PluckAmount
|
|
6
|
+
# Plucks money-aware amounts for one or more attributes.
|
|
7
|
+
#
|
|
8
|
+
# @param attrs [Array<Symbol>] one or more registered money attribute names
|
|
9
|
+
# @return [Array<Mint::Money>] for a single attribute
|
|
10
|
+
# @return [Array<Array>] for multiple attributes, one row array per attribute
|
|
11
|
+
# @raise [ArgumentError] if any attribute is not a registered money attribute
|
|
12
|
+
def pluck_amount(*attrs)
|
|
13
|
+
raise ArgumentError, 'No attribute specified' if attrs.empty?
|
|
14
|
+
|
|
15
|
+
specs = attrs.map { |attr| money_attribute_spec!(attr) }
|
|
16
|
+
return pluck_single_amount(specs.first) if specs.length == 1
|
|
17
|
+
|
|
18
|
+
raw = pluck(*specs.flat_map(&:columns))
|
|
19
|
+
raw.map { |row| extract_money_row(row, specs) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Plucks a single money-aware attribute and returns money values.
|
|
25
|
+
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
|
|
36
|
+
|
|
37
|
+
specs.map do |spec|
|
|
38
|
+
value, cursor = extract_pick_value(row, spec, cursor)
|
|
39
|
+
value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MoneyAttribute
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module SumAmount
|
|
6
|
+
# Sums money-aware amounts for a single attribute.
|
|
7
|
+
#
|
|
8
|
+
# @param attr [Symbol] a registered money attribute name
|
|
9
|
+
# @return [Array<Mint::Money>] one Money per currency (or one for single-column attributes)
|
|
10
|
+
# @raise [ArgumentError] if the attribute is not a registered money attribute
|
|
11
|
+
def sum_amount(attr)
|
|
12
|
+
raise ArgumentError, 'No attribute specified' if attr.nil?
|
|
13
|
+
|
|
14
|
+
spec = money_attribute_spec!(attr)
|
|
15
|
+
if spec.composite?
|
|
16
|
+
resolve_composite_sum(spec)
|
|
17
|
+
else
|
|
18
|
+
resolve_single_sum(spec)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Sums a composite attribute grouped by currency.
|
|
25
|
+
def resolve_composite_sum(spec)
|
|
26
|
+
totals = group(spec.currency_col).sum(spec.amount_col)
|
|
27
|
+
return [spec.build_money(0, MoneyAttribute.default_currency)] if totals.empty?
|
|
28
|
+
|
|
29
|
+
totals.map { |code, amount| spec.build_money(amount, code) }
|
|
30
|
+
.sort_by(&:currency_code)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Sums a fixed-currency single-column attribute.
|
|
34
|
+
def resolve_single_sum(spec)
|
|
35
|
+
total = sum(spec.amount_col)
|
|
36
|
+
|
|
37
|
+
[spec.build_money(total, MoneyAttribute.default_currency)]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'query/helpers'
|
|
4
|
+
require_relative 'query/currency_condition'
|
|
5
|
+
require_relative 'query/amount_condition'
|
|
6
|
+
require_relative 'query/amount_order'
|
|
7
|
+
require_relative 'query/pluck'
|
|
8
|
+
require_relative 'query/pick'
|
|
9
|
+
require_relative 'query/sum'
|
|
10
|
+
|
|
11
|
+
module MoneyAttribute
|
|
12
|
+
# Money-aware query helpers for ActiveRecord.
|
|
13
|
+
#
|
|
14
|
+
# Offer.where_currency(price: 'EUR')
|
|
15
|
+
# Offer.where_amount(price: 10..100)
|
|
16
|
+
# Offer.order_by_amount(price: :desc)
|
|
17
|
+
# Offer.pluck_amount(:price)
|
|
18
|
+
# Offer.pick_amount(:price)
|
|
19
|
+
# Offer.sum_amount(:price)
|
|
20
|
+
#
|
|
21
|
+
module Query
|
|
22
|
+
extend ActiveSupport::Concern
|
|
23
|
+
|
|
24
|
+
class_methods do
|
|
25
|
+
# Filters by currency for one or more money attributes.
|
|
26
|
+
#
|
|
27
|
+
# @param conditions [Hash{Symbol => String}] attribute name to currency code
|
|
28
|
+
# @return [ActiveRecord::Relation]
|
|
29
|
+
# @raise [ArgumentError] if the attribute is not a composite money attribute
|
|
30
|
+
def where_currency(conditions)
|
|
31
|
+
scope = all
|
|
32
|
+
conditions.each { |attr, value| scope = scope.resolve_currency_condition(attr, value) }
|
|
33
|
+
scope
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Filters by amount for one or more money attributes.
|
|
37
|
+
#
|
|
38
|
+
# Accepts +Mint::Money+ objects, numeric values (decimal columns), Ranges, or Arrays.
|
|
39
|
+
#
|
|
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
|
|
44
|
+
# @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
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Orders by amount for one or more money attributes.
|
|
53
|
+
#
|
|
54
|
+
# Composite attributes sort by currency ASC first, then amount.
|
|
55
|
+
#
|
|
56
|
+
# @param conditions [Hash{Symbol => Symbol}] attribute name to +:asc+ or +:desc+
|
|
57
|
+
# @return [ActiveRecord::Relation]
|
|
58
|
+
# @raise [ArgumentError] if the attribute is not a registered money attribute
|
|
59
|
+
def order_by_amount(conditions)
|
|
60
|
+
scope = all
|
|
61
|
+
conditions.each { |attr, dir| scope = scope.resolve_amount_order(attr, dir || :asc) }
|
|
62
|
+
scope
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Plucks money-aware amounts from the current relation.
|
|
66
|
+
#
|
|
67
|
+
# Offer.pluck_amount(:price)
|
|
68
|
+
# # => [Mint::Money(10.0, 'EUR'), Mint::Money(20.0, 'USD')]
|
|
69
|
+
#
|
|
70
|
+
# @param attrs [Array<Symbol>] one or more registered money attribute names
|
|
71
|
+
# @return [Array<Mint::Money>] for a single attribute
|
|
72
|
+
# @return [Array<Array>] for multiple attributes, one row array per attribute
|
|
73
|
+
# @raise [ArgumentError] if any attribute is not a registered money attribute
|
|
74
|
+
def pluck_amount(*attrs)
|
|
75
|
+
all.pluck_amount(*attrs)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Picks a single money-aware value from the current relation.
|
|
79
|
+
#
|
|
80
|
+
# Offer.pick_amount(:price)
|
|
81
|
+
# # => Mint::Money(10.0, 'EUR')
|
|
82
|
+
#
|
|
83
|
+
# @param attrs [Array<Symbol>] one or more registered money attribute names
|
|
84
|
+
# @return [Mint::Money, Array, nil] Money for a single attribute, row array for multiple, nil if empty
|
|
85
|
+
# @raise [ArgumentError] if any attribute is not a registered money attribute
|
|
86
|
+
def pick_amount(*attrs)
|
|
87
|
+
all.pick_amount(*attrs)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Sums money-aware amounts, grouping by currency for composite attributes.
|
|
91
|
+
#
|
|
92
|
+
# Offer.sum_amount(:price)
|
|
93
|
+
# # => [Mint::Money(30.0, 'EUR'), Mint::Money(50.0, 'USD')]
|
|
94
|
+
#
|
|
95
|
+
# @param attr [Symbol] a registered money attribute name
|
|
96
|
+
# @return [Array<Mint::Money>] one Money per currency (or one for single-column attributes)
|
|
97
|
+
# @raise [ArgumentError] if the attribute is not a registered money attribute
|
|
98
|
+
def sum_amount(attr)
|
|
99
|
+
all.sum_amount(attr)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# :nodoc:
|
|
105
|
+
module QueryMethods
|
|
106
|
+
include QueryHelpers
|
|
107
|
+
include CurrencyCondition
|
|
108
|
+
include AmountCondition
|
|
109
|
+
include AmountOrder
|
|
110
|
+
include PluckAmount
|
|
111
|
+
include PickAmount
|
|
112
|
+
include SumAmount
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
ActiveSupport.on_load(:active_record) do
|
|
117
|
+
include MoneyAttribute::AttributeSpecRegistry
|
|
118
|
+
include MoneyAttribute::Query
|
|
119
|
+
ActiveRecord::Relation.include(MoneyAttribute::QueryMethods)
|
|
120
|
+
end
|
|
@@ -20,25 +20,41 @@ module MoneyAttribute
|
|
|
20
20
|
register_custom_currencies!
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# Configures Mint to use the Rails locale currency format.
|
|
23
24
|
def self.setup_locale_backend!
|
|
24
|
-
::Mint.locale_backend =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
::Mint.locale_backend = method(:build_locale_format).to_proc
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Builds the locale-aware currency formatting hash.
|
|
29
|
+
def self.build_locale_format
|
|
30
|
+
fmt = I18n.t('number.currency.format', default: {})
|
|
31
|
+
{ decimal: fmt[:separator], thousand: fmt[:delimiter], format: build_format(fmt) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Builds the final currency format string or hash for Mint.
|
|
35
|
+
def self.build_format(fmt)
|
|
36
|
+
if %i[positive negative zero].any? { |k| fmt.key?(k) }
|
|
37
|
+
build_hash_format(fmt)
|
|
38
|
+
else
|
|
39
|
+
translate_format(fmt[:format])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Builds a per-sign currency format hash.
|
|
44
|
+
def self.build_hash_format(fmt)
|
|
45
|
+
{
|
|
46
|
+
positive: translate_format(fmt[:positive] || fmt[:format]),
|
|
47
|
+
negative: translate_format(fmt[:negative] || fmt[:format]),
|
|
48
|
+
zero: translate_format(fmt[:zero] || fmt[:format])
|
|
39
49
|
}
|
|
40
50
|
end
|
|
41
51
|
|
|
52
|
+
# Translates Rails currency placeholders into Mint placeholders.
|
|
53
|
+
def self.translate_format(str)
|
|
54
|
+
str.to_s.gsub('%n', '%<amount>f').gsub('%u', '%<symbol>s')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Registers custom currencies configured by the application.
|
|
42
58
|
def self.register_custom_currencies!
|
|
43
59
|
Array(MoneyAttribute.config.added_currencies).each do |currency_data|
|
|
44
60
|
if currency_data.respond_to?(:values_at)
|
|
@@ -48,7 +64,7 @@ module MoneyAttribute
|
|
|
48
64
|
else
|
|
49
65
|
code, subunit, symbol = *currency_data
|
|
50
66
|
end
|
|
51
|
-
::
|
|
67
|
+
Money::Currency.register(code:, subunit:, symbol:)
|
|
52
68
|
rescue KeyError => e
|
|
53
69
|
unless e.message.include?('already registered')
|
|
54
70
|
raise ArgumentError,
|
data/lib/money_attribute/type.rb
CHANGED
|
@@ -1,60 +1,66 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MoneyAttribute
|
|
4
|
-
#
|
|
5
|
-
class
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def cast(value)
|
|
13
|
-
case value
|
|
14
|
-
when String then Mint::Money.parse(value, @currency)
|
|
15
|
-
else super
|
|
16
|
-
end
|
|
17
|
-
end
|
|
4
|
+
# Base type for money amount attributes. Handles casting and validation.
|
|
5
|
+
class AmountType < ActiveRecord::Type::Value
|
|
6
|
+
# Casts string input into a +Mint::Money+ value.
|
|
7
|
+
#
|
|
8
|
+
# @param value [String, Numeric, Mint::Money, nil] the input value
|
|
9
|
+
# @return [Mint::Money, Numeric, nil] a Money value for strings, otherwise delegates to super
|
|
10
|
+
def cast(value) = value.is_a?(String) ? Money.parse(value, MoneyAttribute.default_currency) : super
|
|
18
11
|
|
|
12
|
+
# Validates that the value is compatible with the fixed currency type.
|
|
13
|
+
#
|
|
14
|
+
# @param value [Object] the value to validate
|
|
15
|
+
# @return [void]
|
|
16
|
+
# @raise [ArgumentError] when the value has a mismatched currency or is an unsupported type
|
|
19
17
|
def assert_valid_value(value)
|
|
20
18
|
case value
|
|
21
19
|
when NilClass, Numeric, String then return
|
|
22
20
|
when Mint::Money
|
|
23
|
-
|
|
21
|
+
currency = MoneyAttribute.default_currency
|
|
22
|
+
return if value.currency == currency
|
|
24
23
|
|
|
25
|
-
message = "'#{value.inspect}' has different currency. Only #{
|
|
24
|
+
message = "'#{value.inspect}' has different currency. Only #{currency.code} allowed."
|
|
26
25
|
else
|
|
27
26
|
message = "'#{value.inspect}' is not a valid type for the attribute."
|
|
28
27
|
end
|
|
29
28
|
raise ArgumentError, message
|
|
30
29
|
end
|
|
30
|
+
end
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
# Type for integer columns storing subunits (e.g. cents).
|
|
33
|
+
class IntegerAmountType < AmountType
|
|
34
|
+
# Deserializes a subunit integer into a +Mint::Money+ value.
|
|
35
|
+
#
|
|
36
|
+
# @param value [Integer, nil] the raw database value
|
|
37
|
+
# @return [Mint::Money, nil]
|
|
38
|
+
def deserialize(value) = value && Money.from_subunits(value.to_i, MoneyAttribute.default_currency)
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
# Serializes a +Mint::Money+ value into subunits.
|
|
41
|
+
#
|
|
42
|
+
# @param value [Mint::Money, nil]
|
|
43
|
+
# @return [Integer, nil]
|
|
44
|
+
def serialize(value) = value&.subunits
|
|
45
|
+
end
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
# Type for decimal columns storing unit values (e.g. 12.34).
|
|
48
|
+
class DecimalAmountType < AmountType
|
|
49
|
+
# Deserializes a decimal value into a +Mint::Money+ value.
|
|
50
|
+
#
|
|
51
|
+
# @param value [BigDecimal, nil] the raw database value
|
|
52
|
+
# @return [Mint::Money, nil]
|
|
53
|
+
def deserialize(value) = value && Money.from(value, MoneyAttribute.default_currency)
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def self.type = :mint_money
|
|
55
|
+
# Serializes a +Mint::Money+ value into a decimal.
|
|
56
|
+
#
|
|
57
|
+
# @param value [Mint::Money, nil]
|
|
58
|
+
# @return [BigDecimal, nil]
|
|
59
|
+
def serialize(value) = value&.to_d
|
|
53
60
|
end
|
|
54
61
|
end
|
|
55
62
|
|
|
56
63
|
ActiveSupport.on_load(:active_record) do
|
|
57
64
|
include MoneyAttribute::Macro
|
|
58
|
-
|
|
59
|
-
ActiveRecord::Type.register(:mint_money, MoneyAttribute::Type)
|
|
65
|
+
include MoneyAttribute::MoneyAmount
|
|
60
66
|
end
|
data/lib/money_attribute.rb
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# MoneyAttribute provides ActiveRecord integrations for the Minting money gem.
|
|
4
|
+
#
|
|
5
|
+
# Two storage modes:
|
|
6
|
+
#
|
|
7
|
+
# - +money_attribute :price+ — composite (amount + currency columns)
|
|
8
|
+
# - +money_amount :price+ — single column (fixed currency)
|
|
9
|
+
#
|
|
10
|
+
# @see MoneyAttribute::Macro
|
|
11
|
+
# @see MoneyAttribute::MoneyAmount
|
|
12
|
+
|
|
3
13
|
require 'minting'
|
|
4
|
-
require 'money_attribute/core_ext'
|
|
14
|
+
require 'money_attribute/core_ext/numeric'
|
|
15
|
+
require 'money_attribute/core_ext/string'
|
|
5
16
|
require 'money_attribute/configuration'
|
|
17
|
+
require 'money_attribute/current'
|
|
18
|
+
require 'money_attribute/attribute_spec'
|
|
19
|
+
require 'money_attribute/attribute_spec_registry'
|
|
6
20
|
require 'money_attribute/macro'
|
|
21
|
+
require 'money_attribute/money_amount'
|
|
7
22
|
require 'money_attribute/converter'
|
|
8
23
|
require 'money_attribute/type'
|
|
9
24
|
require 'money_attribute/form_builder_extension'
|
|
25
|
+
require 'money_attribute/query'
|
|
10
26
|
require 'money_attribute/railtie'
|
|
11
27
|
require 'money_attribute/version'
|