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,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
|
-
|
|
5
|
-
|
|
4
|
+
# @private
|
|
5
|
+
CONFIG_MUTEX = Mutex.new
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
12
|
-
when Numeric
|
|
13
|
-
when String
|
|
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,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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
assert_columns_exist!(name, mapping)
|
|
19
|
+
mapping
|
|
20
|
+
end
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
29
|
-
{ amount: name, currency:
|
|
30
|
-
elsif
|
|
31
|
-
{ amount: name, 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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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: #{
|
|
55
|
+
"Expected: #{mapping.values.join(', ')}, " \
|
|
52
56
|
"Found: #{attribute_names.join(', ')}"
|
|
53
57
|
end
|
|
58
|
+
end
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
66
|
+
composed_of(name, {
|
|
91
67
|
allow_nil: true,
|
|
92
68
|
class_name: 'Mint::Money',
|
|
93
|
-
constructor:
|
|
94
|
-
converter: Converter.
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
13
|
+
CURRENCY_MIN_LIMIT = 8
|
|
14
|
+
CURRENCY_DEFAULT_LIMIT = 20
|
|
15
|
+
|
|
16
|
+
private
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
27
|
+
column = (options[:column] || accessor).to_s
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
add_column(table_name,
|
|
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)
|
|
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)
|
|
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)
|
|
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
|