minting-rails 0.8.2 → 0.8.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c6f9a0ca2ddc8795a648b6c6047728d626106e2afddbe63869ad3159fcd819e
4
- data.tar.gz: 863f9135a359162e053f988e991a9ac2a832e56e310cc1c7511410212e7c0427
3
+ metadata.gz: 7f128e6d9ea67e7b1f082a06c27bc7f9a332094bdfefcdf5c63ed101eafbb59b
4
+ data.tar.gz: a8d8a159f47e1a9fa906777072bcb9872db56367a5b2479b786968aaa43804fe
5
5
  SHA512:
6
- metadata.gz: 148ee6a06dde29d5685c47e5c3e284fdce46c7d75cd84ab265760087f1e3fcc43afb9f60e949bbe615cf311b576bb9a6180569f103c5665a137d10849604e42c
7
- data.tar.gz: 7c0eaca1345709f22bb446764b9220c5767cca2998e0570490871555cce3c2d692e5839eea9eedcae282cdbe032dca603d1a89bb8ba61d358aa725e83b26b883
6
+ metadata.gz: f1076b8630bc62f73a37c3b1f72121e5aee2afce3ebba8b6964dfe84f0cde55961081ea84b48e2514b2230c9ebbacd9a939aca796037401e667e7a4bb8849fde
7
+ data.tar.gz: a6346b842fb7c1337b13ffecf18967f72d581125ecdc9999e82dd358b4021d5ac2824ad4c1fdc8ae43085065343523b75c81796da6f6a5a03a60e536f5620e6e
data/README.md CHANGED
@@ -269,6 +269,46 @@ end
269
269
 
270
270
  The mapping keys are `:amount` and `:currency`; values are your database column names.
271
271
 
272
+ ## Column resolution
273
+
274
+ When you declare `money_attribute :name`, the gem resolves which database columns to use by checking the table schema in this order:
275
+
276
+ | Step | Condition | Columns used | Mode |
277
+ |---|---|---|---|
278
+ | 1 | `mapping:` provided | As specified | Explicit composite |
279
+ | 2 | `name_currency` column exists | `name` + `name_currency` | Composite (multi-currency) |
280
+ | 3 | `name == 'amount'` AND `currency` column exists | `amount` + `currency` | Composite (multi-currency) |
281
+ | 4 | `name_amount` + `name_currency` columns exist | `name_amount` + `name_currency` | Composite (multi-currency) |
282
+ | 5 | `name` column exists (no currency partner) | `name` alone | Single-column (fixed-currency) |
283
+
284
+ **Example**
285
+
286
+ ```ruby
287
+ create_table :financial_transactions do |t|
288
+ t.integer :amount
289
+ t.string :currency, limit: 3
290
+ t.integer :discount
291
+ t.string :discount_currency, limit: 3
292
+ t.decimal :price_amount
293
+ t.string :price_currency, limit: 3
294
+ t.bigint :surplus
295
+ t.bigint :tax
296
+ t.decimal :total_amount
297
+ t.string :currency_code, limit: 3
298
+ end
299
+ ```
300
+
301
+ ```ruby
302
+ class FinancialTransaction < ApplicationRecord
303
+ money_attribute :amount # step 3: amount(int) + currency
304
+ money_attribute :discount # step 2: discount(int) + discount_currency
305
+ money_attribute :price # step 4: price_amount(dec) + price_currency
306
+ money_attribute :surplus, currency: 'EUR' # step 5: surplus(int) (single-column, will use EUR)
307
+ money_attribute :tax # step 5: tax(int) (single-column, will use default currency)
308
+ money_attribute :total, mapping: { amount: :total_amount, currency: :currency_code } # step 1: explicit
309
+ end
310
+ ```
311
+
272
312
  ## Querying
273
313
 
274
314
  Fixed-currency attributes support Rails-native querying through the custom type:
@@ -10,8 +10,8 @@ Mint.configure do |config|
10
10
  # {currency: 'NGN', subunit: 3, symbol: '₦'}
11
11
  # ]
12
12
  config.added_currencies = [
13
- { currency: 'CRC', subunit: 2, symbol: '₡' },
14
- { currency: 'NGN', subunit: 3, symbol: '₦' }
13
+ { currency: 'XCRC', subunit: 2, symbol: '₡' },
14
+ { currency: 'XNGN', subunit: 3, symbol: '₦' }
15
15
  ]
16
16
 
17
17
  # To set the default currency
@@ -1,65 +1,91 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mint
4
- # MoneyAttribute
5
4
  module MoneyAttribute
6
5
  extend ActiveSupport::Concern
7
6
 
8
7
  class_methods do
9
- # Money attribute
10
8
  def money_attribute(name, currency: Mint.default_currency, mapping: nil)
9
+ columns = attribute_names
11
10
  currency = Currency.resolve!(currency)
11
+ name = name.to_s
12
12
  parser = Parser.new(currency)
13
- if attribute_names.include? name.to_s
14
- attribute(name, :mint_money, currency:)
15
- normalizes(name, with: parser)
13
+ resolved_mapping = mapping || resolve_mapping(name, columns)
14
+
15
+ if columns.include?(name) && resolved_mapping.nil?
16
+ define_single_column_money_attribute(name, currency, parser)
16
17
  else
17
- aggregated = find_money_attributes(name, mapping:)
18
- options = {
19
- allow_nil: true, class_name: 'Mint::Money',
20
- constructor: parser, converter: parser,
21
- mapping: {
22
- aggregated[:amount] => amount_extractor_for(aggregated[:amount]),
23
- aggregated[:currency] => :currency_code
24
- }
25
- }
26
- composed_of(name, options)
18
+ define_composite_money_attribute(name, resolved_mapping, parser)
27
19
  end
28
20
  end
29
21
 
30
- def amount_extractor_for(column_name)
31
- col = columns.find { |c| c.name == column_name.to_s }
22
+ private
32
23
 
33
- case col&.type
34
- when :bigint, :integer
35
- :fractional
36
- else
37
- :to_d # :decimal, :numeric, unknown
24
+ # --- Preparation (no side effects) ---
25
+
26
+ def resolve_mapping(name, columns)
27
+ return nil unless columns.include?(name)
28
+
29
+ if columns.include?("#{name}_currency")
30
+ { amount: name, currency: :"#{name}_currency" }
31
+ elsif columns.include?('currency') && name == 'amount'
32
+ { amount: name, currency: :currency }
38
33
  end
39
34
  end
40
35
 
41
- def find_money_attributes(name, mapping:)
42
- if mapping.present?
43
- missing_keys = (%i[amount currency] - mapping.keys).join(', ')
44
- if missing_keys.present?
45
- raise ArgumentError,
46
- "Mapping for :#{name} money attribute is missing required keys: #{missing_keys}"
47
- end
48
- composite = { amount: mapping[:amount].to_s, currency: mapping[:currency].to_s }
49
- else
50
- composite = { amount: "#{name}_amount", currency: "#{name}_currency" }
51
- end
36
+ def resolve_composite_for(name, mapping:)
37
+ composite = { amount: "#{name}_amount", currency: "#{name}_currency" }
52
38
 
53
- missing = composite.values - attribute_names
54
- if missing.any?
55
- raise ArgumentError,
56
- "Could not find columns for :#{name} money attribute. " \
57
- "Expected: #{composite.values.join(', ')}, " \
58
- "Found: #{attribute_names.join(', ')}"
59
- end
39
+ composite[:amount] = mapping[:amount].to_s if mapping&.key?(:amount)
40
+ composite[:currency] = mapping[:currency].to_s if mapping&.key?(:currency)
60
41
 
42
+ assert_columns_exist!(name, composite)
61
43
  composite
62
44
  end
45
+
46
+ def assert_columns_exist!(name, composite)
47
+ missing = composite.values - attribute_names
48
+ return if missing.empty?
49
+
50
+ raise ArgumentError,
51
+ "Could not find columns for :#{name} money attribute. " \
52
+ "Expected: #{composite.values.join(', ')}, " \
53
+ "Found: #{attribute_names.join(', ')}"
54
+ end
55
+
56
+ def amount_extractor_for(column_name)
57
+ integer_column?(column_name) ? :fractional : :to_d
58
+ end
59
+
60
+ def money_constructor_for(amount_column) = integer_column?(amount_column) ? :from_fractional : :from
61
+
62
+ def integer_column?(column_name)
63
+ col = columns.find { |c| c.name == column_name }
64
+ %i[integer bigint].include?(col&.type)
65
+ end
66
+
67
+ # --- Configuration (registers types, normalizers, composed_of) ---
68
+
69
+ def define_single_column_money_attribute(name, currency, parser)
70
+ column_type = integer_column?(name) ? ActiveRecord::Type::Integer.new : ActiveRecord::Type::Decimal.new
71
+ attribute(name.to_sym, :mint_money, currency:, column_type: column_type)
72
+ normalizes(name.to_sym, with: parser)
73
+ end
74
+
75
+ def define_composite_money_attribute(name, mapping, parser)
76
+ aggregated = resolve_composite_for(name, mapping:)
77
+
78
+ composed_of(name.to_sym, {
79
+ allow_nil: true,
80
+ class_name: 'Mint::Money',
81
+ constructor: money_constructor_for(aggregated[:amount]),
82
+ converter: parser,
83
+ mapping: {
84
+ aggregated[:amount] => amount_extractor_for(aggregated[:amount]),
85
+ aggregated[:currency] => :currency_code
86
+ }
87
+ })
88
+ end
63
89
  end
64
90
  end
65
91
  end
@@ -26,9 +26,9 @@ module Mint
26
26
  return nil unless value
27
27
 
28
28
  if @column_type.is_a?(ActiveRecord::Type::Integer)
29
- Mint.money(value * @currency.fractional_multiplier, @currency)
29
+ Mint::Money.from_fractional(value, @currency)
30
30
  else
31
- Mint.money(value, @currency)
31
+ Mint::Money.from(value, @currency)
32
32
  end
33
33
  end
34
34
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Mint
4
4
  module MoneyAttribute
5
- VERSION = '0.8.2'
5
+ VERSION = '0.8.3'
6
6
  end
7
7
  end
@@ -33,13 +33,15 @@ module Mint
33
33
  def self.register_custom_currencies!
34
34
  Array(Mint.config.added_currencies).each do |currency_data|
35
35
  if currency_data.respond_to?(:values_at)
36
- code = currency_data[:currency] || currency_data['currency']
37
- subunit = currency_data[:subunit] || currency_data['subunit']
38
- symbol = currency_data[:symbol] || currency_data['symbol']
36
+ code = currency_data[:currency]
37
+ subunit = currency_data[:subunit]
38
+ symbol = currency_data[:symbol]
39
39
  else
40
40
  code, subunit, symbol = *currency_data
41
41
  end
42
42
  Currency.register(code:, subunit:, symbol:)
43
+ rescue KeyError
44
+ nil
43
45
  end
44
46
  end
45
47
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minting-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gilson Ferraz