money_attribute 1.1.0 → 1.2.1

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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +143 -41
  3. data/Rakefile +43 -2
  4. data/lib/generators/money_attribute/initializer_generator.rb +10 -0
  5. data/lib/generators/templates/money_attribute.rb +20 -12
  6. data/lib/money_attribute/attribute_spec.rb +84 -0
  7. data/lib/money_attribute/attribute_spec_registry.rb +78 -0
  8. data/lib/money_attribute/column_type_validations.rb +47 -0
  9. data/lib/money_attribute/configuration.rb +43 -15
  10. data/lib/money_attribute/converter.rb +30 -5
  11. data/lib/money_attribute/core_ext/numeric.rb +17 -0
  12. data/lib/money_attribute/core_ext/string.rb +11 -1
  13. data/lib/money_attribute/current.rb +13 -0
  14. data/lib/money_attribute/form_builder_extension.rb +48 -1
  15. data/lib/money_attribute/macro.rb +106 -72
  16. data/lib/money_attribute/migration_extensions/helper.rb +62 -3
  17. data/lib/money_attribute/migration_extensions/schema_statements.rb +89 -9
  18. data/lib/money_attribute/migration_extensions/table_definition.rb +68 -11
  19. data/lib/money_attribute/money_amount.rb +49 -25
  20. data/lib/money_attribute/query/amount_condition.rb +207 -0
  21. data/lib/money_attribute/query/amount_order.rb +28 -0
  22. data/lib/money_attribute/query/currency_condition.rb +29 -0
  23. data/lib/money_attribute/query/helpers.rb +53 -0
  24. data/lib/money_attribute/query/pick.rb +41 -0
  25. data/lib/money_attribute/query/pluck.rb +39 -0
  26. data/lib/money_attribute/query/sum.rb +52 -0
  27. data/lib/money_attribute/query.rb +125 -0
  28. data/lib/money_attribute/railtie.rb +47 -1
  29. data/lib/money_attribute/type.rb +41 -33
  30. data/lib/money_attribute/version.rb +2 -1
  31. data/lib/money_attribute.rb +16 -1
  32. metadata +17 -5
  33. data/lib/money_attribute/core_ext.rb +0 -8
@@ -0,0 +1,125 @@
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 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+.
41
+ #
42
+ # @param args [Array] a condition hash, or a SQL string with bind values
43
+ # @return [ActiveRecord::Relation]
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
53
+ end
54
+
55
+ # Orders by amount for one or more money attributes.
56
+ #
57
+ # Composite attributes sort by currency ASC first, then amount.
58
+ #
59
+ # @param conditions [Hash{Symbol => Symbol}] attribute name to +:asc+ or +:desc+
60
+ # @return [ActiveRecord::Relation]
61
+ # @raise [ArgumentError] if the attribute is not a registered money attribute
62
+ def order_by_amount(conditions)
63
+ scope = all
64
+ conditions.each { |attr, dir| scope = scope.resolve_amount_order(attr, dir || :asc) }
65
+ scope
66
+ end
67
+
68
+ # Plucks money-aware amounts from the current relation.
69
+ #
70
+ # Offer.pluck_amount(:price)
71
+ # # => [Mint::Money(10.0, 'EUR'), Mint::Money(20.0, 'USD')]
72
+ #
73
+ # @param attrs [Array<Symbol>] one or more registered money attribute names
74
+ # @return [Array<Mint::Money>] for a single attribute
75
+ # @return [Array<Array>] for multiple attributes, one row array per attribute
76
+ # @raise [ArgumentError] if any attribute is not a registered money attribute
77
+ def pluck_amount(*attrs)
78
+ all.pluck_amount(*attrs)
79
+ end
80
+
81
+ # Picks a single money-aware value from the current relation.
82
+ #
83
+ # Offer.pick_amount(:price)
84
+ # # => Mint::Money(10.0, 'EUR')
85
+ #
86
+ # @param attrs [Array<Symbol>] one or more registered money attribute names
87
+ # @return [Mint::Money, Array, nil] Money for a single attribute, row array for multiple, nil if empty
88
+ # @raise [ArgumentError] if any attribute is not a registered money attribute
89
+ def pick_amount(*attrs)
90
+ all.pick_amount(*attrs)
91
+ end
92
+
93
+ # Sums money-aware amounts, grouping by currency for composite attributes.
94
+ #
95
+ # Offer.sum_amount(:price)
96
+ # # => [Mint::Money(30.0, 'EUR'), Mint::Money(50.0, 'USD')]
97
+ #
98
+ # @param attr [Symbol] a registered money attribute name
99
+ # @return [Array<Mint::Money>] one Money per currency (or one for single-column attributes)
100
+ # @raise [ArgumentError] if the attribute is not a registered money attribute
101
+ def sum_amount(attr)
102
+ all.sum_amount(attr)
103
+ end
104
+ end
105
+ end
106
+
107
+ # Internal mixin combining all query sub-modules onto relations.
108
+ #
109
+ # @api private
110
+ module QueryMethods
111
+ include QueryHelpers
112
+ include CurrencyCondition
113
+ include AmountCondition
114
+ include AmountOrder
115
+ include PluckAmount
116
+ include PickAmount
117
+ include SumAmount
118
+ end
119
+ end
120
+
121
+ ActiveSupport.on_load(:active_record) do
122
+ include MoneyAttribute::AttributeSpecRegistry
123
+ include MoneyAttribute::Query
124
+ ActiveRecord::Relation.include(MoneyAttribute::QueryMethods)
125
+ end
@@ -1,6 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
+ # Rails engine integration.
5
+ #
6
+ # On boot, includes the migration and form-builder extensions into the
7
+ # corresponding Rails classes, wires Mint's locale backend to Rails i18n,
8
+ # and registers custom currencies declared in the initializer.
9
+ #
10
+ # @example Generated initializer
11
+ # MoneyAttribute.configure do |config|
12
+ # config.default_currency = 'BRL'
13
+ # config.added_currencies = [
14
+ # { currency: 'BTB', subunit: 8, symbol: '₿' }
15
+ # ]
16
+ # end
17
+ #
18
+ # @api private
4
19
  class Railtie < ::Rails::Railtie
5
20
  generators do
6
21
  require 'generators/money_attribute/initializer_generator'
@@ -20,15 +35,28 @@ module MoneyAttribute
20
35
  register_custom_currencies!
21
36
  end
22
37
 
38
+ # Configures Mint to use the Rails locale currency format.
39
+ #
40
+ # @return [void]
41
+ # @api private
23
42
  def self.setup_locale_backend!
24
43
  ::Mint.locale_backend = method(:build_locale_format).to_proc
25
44
  end
26
45
 
46
+ # Builds the locale-aware currency formatting hash.
47
+ #
48
+ # @return [Hash] the +:decimal+, +:thousand+, and +:format+ keys for Mint
49
+ # @api private
27
50
  def self.build_locale_format
28
51
  fmt = I18n.t('number.currency.format', default: {})
29
52
  { decimal: fmt[:separator], thousand: fmt[:delimiter], format: build_format(fmt) }
30
53
  end
31
54
 
55
+ # Builds the final currency format string or hash for Mint.
56
+ #
57
+ # @param fmt [Hash] the Rails currency format settings
58
+ # @return [String, Hash] a single format string or a per-sign format hash
59
+ # @api private
32
60
  def self.build_format(fmt)
33
61
  if %i[positive negative zero].any? { |k| fmt.key?(k) }
34
62
  build_hash_format(fmt)
@@ -37,6 +65,11 @@ module MoneyAttribute
37
65
  end
38
66
  end
39
67
 
68
+ # Builds a per-sign currency format hash.
69
+ #
70
+ # @param fmt [Hash] the Rails currency format settings
71
+ # @return [Hash] the +:positive+, +:negative+, and +:zero+ format strings
72
+ # @api private
40
73
  def self.build_hash_format(fmt)
41
74
  {
42
75
  positive: translate_format(fmt[:positive] || fmt[:format]),
@@ -45,10 +78,23 @@ module MoneyAttribute
45
78
  }
46
79
  end
47
80
 
81
+ # Translates Rails currency placeholders into Mint placeholders.
82
+ #
83
+ # @param str [String, nil] the Rails format string
84
+ # @return [String] the translated format string
85
+ # @api private
48
86
  def self.translate_format(str)
49
87
  str.to_s.gsub('%n', '%<amount>f').gsub('%u', '%<symbol>s')
50
88
  end
51
89
 
90
+ # Registers custom currencies configured by the application.
91
+ #
92
+ # Accepts hashes with +:currency+, +:subunit+, +:symbol+ keys or the
93
+ # matching positional array form. Already-registered currencies are skipped.
94
+ #
95
+ # @return [void]
96
+ # @raise [ArgumentError] if a currency hash is missing a required key
97
+ # @api private
52
98
  def self.register_custom_currencies!
53
99
  Array(MoneyAttribute.config.added_currencies).each do |currency_data|
54
100
  if currency_data.respond_to?(:values_at)
@@ -58,7 +104,7 @@ module MoneyAttribute
58
104
  else
59
105
  code, subunit, symbol = *currency_data
60
106
  end
61
- ::Mint::Currency.register(code:, subunit:, symbol:)
107
+ Money::Currency.register(code:, subunit:, symbol:)
62
108
  rescue KeyError => e
63
109
  unless e.message.include?('already registered')
64
110
  raise ArgumentError,
@@ -1,54 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
- # Type
5
- class Type < ActiveRecord::Type::Value
6
- def initialize(currency:, column_type: ActiveRecord::Type::Decimal.new)
7
- @currency = currency
8
- @column_type = column_type
9
- super()
10
- end
11
-
12
- def cast(value)
13
- if value.is_a?(String)
14
- Mint::Money.parse(value, @currency)
15
- else
16
- super
17
- end
18
- 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
19
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
20
17
  def assert_valid_value(value)
21
18
  case value
22
19
  when NilClass, Numeric, String then return
23
20
  when Mint::Money
24
- return if value.currency == @currency
21
+ currency = MoneyAttribute.default_currency
22
+ return if value.currency == currency
25
23
 
26
- message = "'#{value.inspect}' has different currency. Only #{@currency.code} allowed."
24
+ message = "'#{value.inspect}' has different currency. Only #{currency.code} allowed."
27
25
  else
28
26
  message = "'#{value.inspect}' is not a valid type for the attribute."
29
27
  end
30
28
  raise ArgumentError, message
31
29
  end
30
+ end
32
31
 
33
- def deserialize(value)
34
- return nil unless value
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)
35
39
 
36
- if @column_type.is_a?(ActiveRecord::Type::Integer)
37
- Mint::Money.from_subunits(value, @currency)
38
- else
39
- Mint::Money.from(value, @currency)
40
- end
41
- end
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
42
46
 
43
- def serialize(value)
44
- return nil unless value
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&.to_money(MoneyAttribute.default_currency)
45
54
 
46
- if @column_type.is_a?(ActiveRecord::Type::Integer)
47
- value.subunits
48
- else
49
- value.to_d
50
- end
51
- end
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
52
60
  end
53
61
  end
54
62
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
- VERSION = '1.1.0'
4
+ # The current gem version, following semantic versioning.
5
+ VERSION = '1.2.1'
5
6
  end
@@ -1,13 +1,28 @@
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'
5
15
  require 'money_attribute/core_ext/string'
6
16
  require 'money_attribute/configuration'
17
+ require 'money_attribute/current'
18
+ require 'money_attribute/column_type_validations'
19
+ require 'money_attribute/attribute_spec'
20
+ require 'money_attribute/attribute_spec_registry'
7
21
  require 'money_attribute/macro'
8
22
  require 'money_attribute/money_amount'
9
23
  require 'money_attribute/converter'
10
24
  require 'money_attribute/type'
11
25
  require 'money_attribute/form_builder_extension'
26
+ require 'money_attribute/query'
12
27
  require 'money_attribute/railtie'
13
28
  require 'money_attribute/version'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: money_attribute
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gilson Ferraz
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '2.0'
18
+ version: '2.1'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '2.0'
25
+ version: '2.1'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rails
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -50,16 +50,28 @@ files:
50
50
  - lib/generators/money_attribute/initializer_generator.rb
51
51
  - lib/generators/templates/money_attribute.rb
52
52
  - lib/money_attribute.rb
53
+ - lib/money_attribute/attribute_spec.rb
54
+ - lib/money_attribute/attribute_spec_registry.rb
55
+ - lib/money_attribute/column_type_validations.rb
53
56
  - lib/money_attribute/configuration.rb
54
57
  - lib/money_attribute/converter.rb
55
- - lib/money_attribute/core_ext.rb
58
+ - lib/money_attribute/core_ext/numeric.rb
56
59
  - lib/money_attribute/core_ext/string.rb
60
+ - lib/money_attribute/current.rb
57
61
  - lib/money_attribute/form_builder_extension.rb
58
62
  - lib/money_attribute/macro.rb
59
63
  - lib/money_attribute/migration_extensions/helper.rb
60
64
  - lib/money_attribute/migration_extensions/schema_statements.rb
61
65
  - lib/money_attribute/migration_extensions/table_definition.rb
62
66
  - lib/money_attribute/money_amount.rb
67
+ - lib/money_attribute/query.rb
68
+ - lib/money_attribute/query/amount_condition.rb
69
+ - lib/money_attribute/query/amount_order.rb
70
+ - lib/money_attribute/query/currency_condition.rb
71
+ - lib/money_attribute/query/helpers.rb
72
+ - lib/money_attribute/query/pick.rb
73
+ - lib/money_attribute/query/pluck.rb
74
+ - lib/money_attribute/query/sum.rb
63
75
  - lib/money_attribute/railtie.rb
64
76
  - lib/money_attribute/type.rb
65
77
  - lib/money_attribute/version.rb
@@ -88,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
100
  - !ruby/object:Gem::Version
89
101
  version: '0'
90
102
  requirements: []
91
- rubygems_version: 4.0.10
103
+ rubygems_version: 4.0.16
92
104
  specification_version: 4
93
105
  summary: Money attributes for ActiveRecord
94
106
  test_files: []
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # :nodoc:
4
- class Numeric
5
- remove_method :to_money if method_defined?(:to_money)
6
-
7
- def to_money(currency = MoneyAttribute.default_currency) = Mint.money(self, currency)
8
- end