minting-rails 0.8.1 → 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 +4 -4
- data/README.md +86 -2
- data/lib/generators/templates/minting.rb +2 -2
- data/lib/minting/money_attribute/money_attribute.rb +66 -40
- data/lib/minting/money_attribute/money_type.rb +2 -2
- data/lib/minting/money_attribute/version.rb +1 -1
- data/lib/minting/railties.rb +29 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f128e6d9ea67e7b1f082a06c27bc7f9a332094bdfefcdf5c63ed101eafbb59b
|
|
4
|
+
data.tar.gz: a8d8a159f47e1a9fa906777072bcb9872db56367a5b2479b786968aaa43804fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1076b8630bc62f73a37c3b1f72121e5aee2afce3ebba8b6964dfe84f0cde55961081ea84b48e2514b2230c9ebbacd9a939aca796037401e667e7a4bb8849fde
|
|
7
|
+
data.tar.gz: a6346b842fb7c1337b13ffecf18967f72d581125ecdc9999e82dd358b4021d5ac2824ad4c1fdc8ae43085065343523b75c81796da6f6a5a03a60e536f5620e6e
|
data/README.md
CHANGED
|
@@ -86,6 +86,51 @@ end
|
|
|
86
86
|
|
|
87
87
|
See the [Minting gem](https://github.com/gferraz/minting) for full configuration options (custom currencies, formatting, rounding).
|
|
88
88
|
|
|
89
|
+
### I18n / Locale-aware formatting
|
|
90
|
+
|
|
91
|
+
Minting-rails integrates with Rails I18n to automatically format money amounts according to the current locale.
|
|
92
|
+
|
|
93
|
+
With `I18n.locale` set to `:en`:
|
|
94
|
+
```ruby
|
|
95
|
+
Mint.money(1234.56, 'USD').to_s # => "$1,234.56"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Switch to `:'pt-BR'` and the separators change automatically (requires [`rails-i18n`](https://github.com/svenfuchs/rails-i18n) or your own locale file):
|
|
99
|
+
```ruby
|
|
100
|
+
I18n.locale = :'pt-BR'
|
|
101
|
+
Mint.money(1234.56, 'USD').to_s # => "$1.234,56"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The locale backend reads `number.currency.format` from your I18n translations and maps Rails format syntax (`%n` for amount, `%u` for unit) to `Mint::Money#to_s`. If the translation key is missing (no locale file for that language), it falls back to hardcoded defaults (`.` decimal, `,` thousand, `%<symbol>s%<amount>f` format).
|
|
105
|
+
|
|
106
|
+
You can configure per-sign formatting by adding `positive`, `negative`, and `zero` keys to your locale:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
# config/locales/minting-rails.en.yml
|
|
110
|
+
en:
|
|
111
|
+
number:
|
|
112
|
+
currency:
|
|
113
|
+
format:
|
|
114
|
+
format: "%u%n" # fallback when no per-sign key matches
|
|
115
|
+
positive: "%u%n" # "$1,234.56"
|
|
116
|
+
negative: "(%u%n)" # "($1,234.56)"
|
|
117
|
+
zero: "--" # "--"
|
|
118
|
+
separator: "."
|
|
119
|
+
delimiter: ","
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
When any of `positive`, `negative`, or `zero` is present, a Hash format is built. Missing keys fall back to `format`:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
Mint.money(1234.56, 'USD').to_s # => "$1,234.56"
|
|
126
|
+
Mint.money(-1234.56, 'USD').to_s # => "($1,234.56)"
|
|
127
|
+
Mint.money(0, 'USD').to_s # => "--"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
If none of those keys are set, `format` is used as a plain string (simple formatting).
|
|
131
|
+
|
|
132
|
+
> Formatting respects the currency's own `subunit` for decimal precision — `I18n` locale settings for `precision` are ignored since that is a currency property, not a locale one.
|
|
133
|
+
|
|
89
134
|
## Usage — Two modes
|
|
90
135
|
|
|
91
136
|
### Decision table
|
|
@@ -224,6 +269,46 @@ end
|
|
|
224
269
|
|
|
225
270
|
The mapping keys are `:amount` and `:currency`; values are your database column names.
|
|
226
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
|
+
|
|
227
312
|
## Querying
|
|
228
313
|
|
|
229
314
|
Fixed-currency attributes support Rails-native querying through the custom type:
|
|
@@ -411,7 +496,7 @@ Minting-rails is intentionally minimal — it focuses on storing and reading mon
|
|
|
411
496
|
| **Mongoid support** | Yes | ActiveRecord only |
|
|
412
497
|
| **Migration helpers** | `add_monetize :products, :price` | None |
|
|
413
498
|
| **View helpers** | `humanized_money`, `money_without_cents`, etc. | None |
|
|
414
|
-
| **I18n / locale files** | Built-in locale-aware formatting
|
|
499
|
+
| **I18n / locale files** | Locale-aware formatting via I18n `number.currency.format` — reads your existing translations, no extra setup | Built-in locale-aware formatting with bundled translations |
|
|
415
500
|
| **Test matcher** | `monetize(:price_cents)` RSpec matcher | None |
|
|
416
501
|
| **Currency exchange** | `default_bank`, `add_rate`, EuCentralBank | None |
|
|
417
502
|
| **Custom currencies** | `register_currency` for non-ISO codes | Via `minting` gem config |
|
|
@@ -429,7 +514,6 @@ If you need any of these features today, money-rails may be a better fit. mintin
|
|
|
429
514
|
1. **Allow nil** — `money_attribute :price, currency: 'USD', allow_nil: true`
|
|
430
515
|
1. **Method-level currency** — lambda-based currency resolution for multi-tenant and instance-level scenarios
|
|
431
516
|
1. **Migration helper**
|
|
432
|
-
1. **Internationalization**
|
|
433
517
|
|
|
434
518
|
Contributions and suggestions are welcome — open an issue or PR at [gferraz/minting-rails](https://github.com/gferraz/minting-rails).
|
|
435
519
|
|
|
@@ -10,8 +10,8 @@ Mint.configure do |config|
|
|
|
10
10
|
# {currency: 'NGN', subunit: 3, symbol: '₦'}
|
|
11
11
|
# ]
|
|
12
12
|
config.added_currencies = [
|
|
13
|
-
{ currency: '
|
|
14
|
-
{ currency: '
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
col = columns.find { |c| c.name == column_name.to_s }
|
|
22
|
+
private
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
42
|
-
|
|
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
|
-
|
|
54
|
-
if
|
|
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.
|
|
29
|
+
Mint::Money.from_fractional(value, @currency)
|
|
30
30
|
else
|
|
31
|
-
Mint.
|
|
31
|
+
Mint::Money.from(value, @currency)
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
data/lib/minting/railties.rb
CHANGED
|
@@ -7,15 +7,41 @@ module Mint
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
config.after_initialize do
|
|
10
|
+
setup_locale_backend!
|
|
11
|
+
register_custom_currencies!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.setup_locale_backend!
|
|
15
|
+
Mint.locale_backend = lambda {
|
|
16
|
+
fmt = I18n.t('number.currency.format', default: {})
|
|
17
|
+
translator = ->(s) { s&.gsub('%n', '%<amount>f')&.gsub('%u', '%<symbol>s') }
|
|
18
|
+
|
|
19
|
+
format = if fmt.key?(:positive) || fmt.key?(:negative) || fmt.key?(:zero)
|
|
20
|
+
{
|
|
21
|
+
positive: translator.call(fmt[:positive] || fmt[:format]),
|
|
22
|
+
negative: translator.call(fmt[:negative] || fmt[:format]),
|
|
23
|
+
zero: translator.call(fmt[:zero] || fmt[:format])
|
|
24
|
+
}
|
|
25
|
+
else
|
|
26
|
+
translator.call(fmt[:format])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
{ decimal: fmt[:separator], thousand: fmt[:delimiter], format: format }
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.register_custom_currencies!
|
|
10
34
|
Array(Mint.config.added_currencies).each do |currency_data|
|
|
11
35
|
if currency_data.respond_to?(:values_at)
|
|
12
|
-
code = currency_data[:currency]
|
|
13
|
-
subunit = currency_data[:subunit]
|
|
14
|
-
symbol = currency_data[:symbol]
|
|
36
|
+
code = currency_data[:currency]
|
|
37
|
+
subunit = currency_data[:subunit]
|
|
38
|
+
symbol = currency_data[:symbol]
|
|
15
39
|
else
|
|
16
40
|
code, subunit, symbol = *currency_data
|
|
17
41
|
end
|
|
18
42
|
Currency.register(code:, subunit:, symbol:)
|
|
43
|
+
rescue KeyError
|
|
44
|
+
nil
|
|
19
45
|
end
|
|
20
46
|
end
|
|
21
47
|
end
|