minting-rails 0.7.1 → 0.8.2

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: f55a3c6c12d63ec3bcd7fe8aded08cefcb61d9fc21491ce49d3057064fcbe18c
4
- data.tar.gz: dc620d935db6c7d606286bbcbb786e75ccaec3e8437eaf7cdeb7b1cd94f5f433
3
+ metadata.gz: 6c6f9a0ca2ddc8795a648b6c6047728d626106e2afddbe63869ad3159fcd819e
4
+ data.tar.gz: 863f9135a359162e053f988e991a9ac2a832e56e310cc1c7511410212e7c0427
5
5
  SHA512:
6
- metadata.gz: c4683ac9e66301ef10168808c4e623dec3539066ba62492f20922ac7f117a6bc48b7270ba31ce3ee948e3f6ca61adff4808013352322f9817cd2638c19dd6e4b
7
- data.tar.gz: 31299dc881a94df769ec78cc9910668e278022f94fef389816758453f987b092136dda0ab039e50503fa61ba03d41d9f6a9528bc163b312127f212badc6b2759
6
+ metadata.gz: 148ee6a06dde29d5685c47e5c3e284fdce46c7d75cd84ab265760087f1e3fcc43afb9f60e949bbe615cf311b576bb9a6180569f103c5665a137d10849604e42c
7
+ data.tar.gz: 7c0eaca1345709f22bb446764b9220c5767cca2998e0570490871555cce3c2d692e5839eea9eedcae282cdbe032dca603d1a89bb8ba61d358aa725e83b26b883
data/README.md CHANGED
@@ -1,86 +1,149 @@
1
1
  # Minting::Rails
2
2
 
3
- Minting::Rails brings [Minting](https://github.com/gferraz/minting) money objects to Active Record models.
3
+ [![CI](https://github.com/gferraz/minting-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/gferraz/minting-rails/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/minting-rails.svg)](https://badge.fury.io/rb/minting-rails)
4
5
 
5
- It adds a `money_attribute` model helper, registers a `:mint_money` Active Record type, and includes small convenience methods such as `12.to_money('USD')`, `12.dollars`, and `'12.00'.mint('BRL')`.
6
+ Store and read Active Record attributes as `Mint::Money` objects with a single `money_attribute` declaration. No manual serialization, no boilerplate.
6
7
 
7
- ## What it does
8
+ ```ruby
9
+ class Product < ApplicationRecord
10
+ money_attribute :price, currency: 'USD' # fixed currency, single column
11
+ money_attribute :total # multi-currency, two columns
12
+ end
13
+
14
+ Product.new(price: 12).price # => [USD 12.00]
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```sh
20
+ bundle add minting-rails
21
+ bin/rails g mint:initializer
22
+ ```
23
+
24
+ ```ruby
25
+ # app/models/product.rb
26
+ class Product < ApplicationRecord
27
+ money_attribute :price, currency: 'USD'
28
+ end
29
+ ```
8
30
 
9
- - Stores and reads model attributes as `Mint::Money` objects.
10
- - Supports composed money attributes backed by amount and currency columns.
11
- - Normalizes numeric, string, and `Mint::Money` assignments.
12
- - Validates currencies against the currencies enabled in Minting.
31
+ That's it. `Product.new(price: 12).price` is a `Mint::Money`.
32
+
33
+ ## Why Minting::Rails?
34
+
35
+ - **No serialization boilerplate** — declare once, read/write `Mint::Money` everywhere.
36
+ - **Two storage modes** — single column for fixed-currency apps (simpler), amount+currency columns for multi-currency records (more flexible).
37
+ - **Integer or decimal columns** — auto-detects the column type and adjusts serialization (e.g. integer stores cents, decimal stores unit value).
38
+ - **Normalizes everything** — pass a number, string, or `Mint::Money`; always get a `Mint::Money` back.
39
+ - **Currency enforcement** — fixed-currency attributes reject wrong currencies at assignment time.
40
+ - **Built on Rails primitives** — uses `ActiveRecord::Type`, `composed_of`, and `normalizes` under the hood. No monkey-patching of core classes.
41
+
42
+ ### At a glance — vs money-rails
43
+
44
+ | Feature | minting-rails | money-rails |
45
+ |---|---|---|
46
+ | **Declaration** | `money_attribute :price` | `monetize :price_cents` |
47
+ | **Column types** | `integer`, `decimal`, `bigint` — auto-detected | `integer` cents only |
48
+ | **Storage modes** | Single column, composite (amount+currency)| Single cents column, composite (cents+currency) |
49
+ | **Decimal columns** | Native — `t.decimal :price` | Not supported — must convert to cents manually |
50
+ | **Multi-currency** | `money_attribute :price` (convention: `<name>_amount` + `<name>_currency`) | `monetize :price_cents, with_currency: :price_currency` |
51
+ | **Rails integration** | `ActiveRecord::Type` + `composed_of` — no monkey-patches | `monetize` overrides reader/writer methods |
52
+ | **Query (fixed)** | `Model.where(price: money)` — `=`, `IN`, `BETWEEN`, `ORDER`, `SUM` | Through cents column (`price_cents`) |
53
+ | **Query (multi)** | `Model.where(price: money)` | `Model.where(price_cents:, price_currency:)` |
54
+ | **Internal amount** | `Rational` | `BigDecimal` |
55
+ | **Performance** | See [BENCHMARKS.md](BENCHMARKS.md) — wins 9/11 cells | |
13
56
 
14
57
  ## Requirements
15
58
 
16
- - Ruby 3.3 or newer.
17
- - Rails 7.1.3.2 or newer.
18
- - Minting 1.6.0 or newer.
59
+ - Ruby 3.3+
60
+ - Rails 7.1.3.2+
61
+ - [Minting](https://github.com/gferraz/minting) 1.6.0+
19
62
 
20
63
  ## Installation
21
64
 
22
- Add the gem to your Rails application's `Gemfile`:
23
-
24
65
  ```ruby
66
+ # Gemfile
25
67
  gem 'minting-rails'
26
68
  ```
27
69
 
28
- Install it:
29
-
30
70
  ```sh
31
71
  bundle install
32
- ```
33
-
34
- Generate the initializer:
35
-
36
- ```sh
37
72
  bin/rails g mint:initializer
38
73
  ```
39
74
 
40
- ## Configuration
75
+ The generator creates `config/initializers/minting.rb`.
41
76
 
42
- Configure Minting in `config/initializers/minting.rb`:
77
+ ## Configuration
43
78
 
44
79
  ```ruby
80
+ # config/initializers/minting.rb
45
81
  Mint.configure do |config|
46
- config.enabled_currencies = :all
47
82
  config.default_currency = 'USD'
48
- config.default_format = '%<symbol>s%<amount>f'
83
+ # enabled_currencies removed — all registered currencies are valid
49
84
  end
50
85
  ```
51
86
 
52
- You can limit the currencies that may be used:
87
+ See the [Minting gem](https://github.com/gferraz/minting) for full configuration options (custom currencies, formatting, rounding).
53
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`:
54
94
  ```ruby
55
- Mint.configure do |config|
56
- config.enabled_currencies = %w[USD EUR BRL]
57
- config.default_currency = 'USD'
58
- end
95
+ Mint.money(1234.56, 'USD').to_s # => "$1,234.56"
59
96
  ```
60
97
 
61
- You can also register custom currencies before enabling or using them:
62
-
98
+ Switch to `:'pt-BR'` and the separators change automatically (requires [`rails-i18n`](https://github.com/svenfuchs/rails-i18n) or your own locale file):
63
99
  ```ruby
64
- Mint.configure do |config|
65
- config.added_currencies = [
66
- { currency: 'CRC', subunit: 2, symbol: 'CRC' },
67
- { currency: 'NGN', subunit: 2, symbol: 'NGN' } # subunit is the number of digits after the decimal; USD has 2, JPY has 0, BHD has 3
68
- ]
100
+ I18n.locale = :'pt-BR'
101
+ Mint.money(1234.56, 'USD').to_s # => "$1.234,56"
102
+ ```
69
103
 
70
- config.enabled_currencies = :all
71
- config.default_currency = 'CRC'
72
- end
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 # => "--"
73
128
  ```
74
129
 
75
- The default currency must be registered and included in `enabled_currencies`.
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.
76
133
 
77
- ## Usage
134
+ ## Usage — Two modes
78
135
 
79
- Declare money attributes in your Active Record models with `money_attribute`.
136
+ ### Decision table
80
137
 
81
- ### Single-column fixed currency
138
+ | | Fixed currency (single column) | Multi-currency (amount + currency) |
139
+ |---|---|---|
140
+ | **Migration** | `t.decimal :price` | `t.decimal :price_amount` + `t.string :price_currency` |
141
+ | **Model** | `money_attribute :price, currency: 'USD'` | `money_attribute :price` |
142
+ | **When to use** | Column always holds the same currency | Each row can hold a different currency |
143
+ | **Column type** | `decimal`, `integer`, or `bigint` | `decimal`, `integer`, or `bigint` for amount; `string` for currency |
144
+ | **Query** | `Product.where(price: 10.mint('USD'))` — full type support | `Offer.where(price: 10.mint('EUR'))` — equality only |
82
145
 
83
- Use this when a column always stores one currency, such as a `price` column that is always USD.
146
+ ### Fixed currency (single column)
84
147
 
85
148
  Migration:
86
149
 
@@ -90,7 +153,6 @@ class CreateProducts < ActiveRecord::Migration[7.1]
90
153
  create_table :products do |t|
91
154
  t.decimal :price
92
155
  t.decimal :discount
93
-
94
156
  t.timestamps
95
157
  end
96
158
  end
@@ -110,24 +172,18 @@ Assignments are normalized to `Mint::Money`:
110
172
 
111
173
  ```ruby
112
174
  product = Product.new(price: 12, discount: '3.50')
113
-
114
- product.price
115
- # => #<Mint::Money ... USD 12.00>
116
-
117
- product.discount
118
- # => #<Mint::Money ... USD 3.50>
175
+ product.price # => [USD 12.00]
176
+ product.discount # => [USD 3.50]
119
177
  ```
120
178
 
121
- Assigning a `Mint::Money` with a different currency raises `ArgumentError`:
179
+ A currency mismatch raises `ArgumentError`:
122
180
 
123
181
  ```ruby
124
182
  Product.new(price: 12.to_money('EUR'))
125
- # raises ArgumentError because the attribute only accepts USD
183
+ # => ArgumentError: ... has different currency. Only USD allowed.
126
184
  ```
127
185
 
128
- ### Amount and currency columns
129
-
130
- Use this when each row can store a different currency per record.
186
+ ### Multi-currency (amount + currency columns)
131
187
 
132
188
  Migration:
133
189
 
@@ -136,8 +192,7 @@ class CreateOffers < ActiveRecord::Migration[7.1]
136
192
  def change
137
193
  create_table :offers do |t|
138
194
  t.decimal :price_amount
139
- t.string :price_currency
140
-
195
+ t.string :price_currency
141
196
  t.timestamps
142
197
  end
143
198
  end
@@ -156,166 +211,285 @@ The attribute is composed from `price_amount` and `price_currency`:
156
211
 
157
212
  ```ruby
158
213
  offer = Offer.new(price: 15.to_money('EUR'))
214
+ offer.price # => [EUR 15.00]
215
+ offer.price_amount # => 15.0
216
+ offer.price_currency # => "EUR"
217
+ ```
159
218
 
160
- offer.price
161
- # => #<Mint::Money ... EUR 15.00>
162
-
163
- offer.price_amount
164
- # => 15.0
219
+ When assigning a plain number or string, `Mint.default_currency` is used:
165
220
 
166
- offer.price_currency
167
- # => "EUR"
221
+ ```ruby
222
+ offer = Offer.new(price: '12')
223
+ offer.price.currency.code # => "USD"
168
224
  ```
169
225
 
170
- ### Integer storage
171
-
172
- By default, money attributes are stored as `decimal` columns. If you prefer to store amounts as integer subunits (cents, pence, etc.), use a `bigint` or `integer` column instead. Minting::Rails detects the column type automatically and adapts serialization accordingly.
226
+ ## Column type detection
173
227
 
174
- Migration:
228
+ Declare the column as `decimal`, `integer`, or `bigint` — the gem adapts:
175
229
 
176
230
  ```ruby
177
- class CreateOrders < ActiveRecord::Migration[7.1]
178
- def change
179
- create_table :orders do |t|
180
- t.bigint :total_amount
181
- t.string :total_currency
182
-
183
- t.timestamps
184
- end
185
- end
231
+ # Migration
232
+ create_table :orders do |t|
233
+ t.bigint :total_amount # stored as cents (subunits)
234
+ t.string :total_currency
186
235
  end
187
- ```
188
-
189
- Model:
190
236
 
191
- ```ruby
237
+ # Model
192
238
  class Order < ApplicationRecord
193
239
  money_attribute :total
194
240
  end
241
+
242
+ Order.new(total: 19.99.to_money('USD')).total_amount # => 1999
195
243
  ```
196
244
 
197
- The amount is stored and retrieved in subunits:
245
+ Same for fixed-currency attributes:
198
246
 
199
247
  ```ruby
200
- order = Order.new(total: 19.99.to_money(:USD))
248
+ # Migration
249
+ t.bigint :price
201
250
 
202
- order.total
203
- # => #<Mint::Money ... USD 19.99>
251
+ # Model (no change needed)
252
+ money_attribute :price, currency: 'USD'
253
+ ```
204
254
 
205
- order.total_amount
206
- # => 1999
255
+ > Use `integer`/`bigint` for large tables (faster, smaller). Use `decimal` when SQL-level readability matters.
207
256
 
208
- order.total_currency
209
- # => "USD"
210
- ```
257
+ ## Custom column names
211
258
 
212
- The same applies to single-column fixed-currency attributes:
259
+ If your columns don't follow the `<name>_amount` / `<name>_currency` convention:
213
260
 
214
261
  ```ruby
215
- class Ticket < ApplicationRecord
216
- money_attribute :price, currency: 'USD'
262
+ class Invoice < ApplicationRecord
263
+ money_attribute :total, mapping: {
264
+ amount: :total_amount,
265
+ currency: :currency_code
266
+ }
217
267
  end
218
268
  ```
219
269
 
220
- Migration:
270
+ The mapping keys are `:amount` and `:currency`; values are your database column names.
271
+
272
+ ## Querying
273
+
274
+ Fixed-currency attributes support Rails-native querying through the custom type:
221
275
 
222
276
  ```ruby
223
- t.bigint :price
224
- ```
277
+ # Equality
278
+ Product.where(price: 10.mint('USD'))
279
+
280
+ # IN clause
281
+ Product.where(price: [10.mint('USD'), 20.mint('USD')])
225
282
 
226
- No model change is needed. The column type drives the behavior.
283
+ # BETWEEN
284
+ Product.where(price: 10.mint('USD')..20.mint('USD'))
227
285
 
228
- > **Note:** Integer storage is more efficient for large tables. Use Decimal when you need to stay close to SQL standards for interoperability with other systems
286
+ # Ordering
287
+ Product.order(price: :desc)
229
288
 
230
- ### Default Currency
289
+ # Aggregation
290
+ Product.where(price: 10.mint('USD')).sum(:price)
291
+ ```
231
292
 
232
- When you assign a plain number or string, Minting::Rails uses `Mint.default_currency`:
293
+ Multi-currency attributes support equality queries via `composed_of`:
233
294
 
234
295
  ```ruby
235
- offer = Offer.new(price: '12')
296
+ Offer.where(price: 10.mint('EUR'))
297
+ ```
298
+
299
+ For comparisons on multi-currency attributes, use the backing columns directly:
236
300
 
237
- offer.price.currency_code
238
- # => "USD"
301
+ ```ruby
302
+ Offer.where(price_amount: 10..20, price_currency: 'EUR')
303
+ Offer.where('price_amount > ? AND price_currency = ?', 10, 'EUR')
239
304
  ```
240
305
 
241
- ### Custom column names
306
+ ## Convenience methods
242
307
 
243
- If your amount and currency columns do not follow the `<name>_amount` and `<name>_currency` convention, pass a mapping:
308
+ Minting::Rails adds small helpers on `Numeric` and `String`:
244
309
 
245
310
  ```ruby
246
- class Invoice < ApplicationRecord
247
- money_attribute :total, mapping: {
248
- amount: :total_amount,
249
- currency: :currency_code
250
- }
251
- end
311
+ 12.to_money('USD') # => [USD 12.00]
312
+ 12.dollars # => [USD 12.00]
313
+ 12.euros # => [EUR 12.00]
314
+ '12.50'.mint('BRL') # => [BRL 12.50]
252
315
  ```
253
316
 
254
- The mapping keys are `:amount` and `:currency`. The values are your database columns.
317
+ > If you prefer not to extend core classes, use `Mint::Money.money(12, 'USD')` instead.
255
318
 
256
- ## Querying
319
+ ## vs money-rails
257
320
 
258
- Fixed-currency attributes can be queried with `Mint::Money` values:
321
+ [Money-rails](https://github.com/RubyMoney/money-rails) is the most popular money-in-Rails gem. Here's how they compare side-by-side.
322
+
323
+ ### Model declaration
259
324
 
260
325
  ```ruby
261
- Product.where(price: 15.to_money('USD'))
326
+ # minting-rails
327
+ class Product < ApplicationRecord
328
+ money_attribute :price, currency: 'USD' # single column, fixed currency
329
+ money_attribute :total # two columns, multi-currency
330
+ end
331
+
332
+ # money-rails
333
+ class Product < ApplicationRecord
334
+ monetize :price_cents # single cents column, fixed currency
335
+ monetize :total_cents, with_currency: :total_currency # two columns, multi-currency
336
+ end
262
337
  ```
263
338
 
264
- Composed attributes can also be queried with a money object:
339
+ ### Migration
265
340
 
266
341
  ```ruby
267
- Offer.where(price: 15.to_money('EUR'))
342
+ # minting-rails — any numeric column type
343
+ create_table :products do |t|
344
+ t.decimal :price # stores 12.34
345
+ t.integer :discount # stores 1234 (cents)
346
+ t.bigint :total_amount # stores 1999 (cents)
347
+ t.string :total_currency
348
+ end
349
+
350
+ # money-rails — integer cents only
351
+ create_table :products do |t|
352
+ t.integer :price_cents # stores 1234 (cents)
353
+ t.integer :discount_cents # stores 350 (cents)
354
+ t.integer :total_cents
355
+ t.string :total_currency
356
+ end
268
357
  ```
269
358
 
270
- You can still query the backing columns directly when that is clearer:
359
+ ### Reading & writing
271
360
 
272
361
  ```ruby
273
- Offer.where(price_amount: 15, price_currency: 'EUR')
362
+ # minting-rails — pass any type, always get Mint::Money
363
+ product.price = 12.34 # stores 12.34 in decimal column
364
+ product.price = 1234 # stores 1234 in integer column
365
+ product.price = '$12.34' # parses string
366
+ product.price # => [USD 12.34]
367
+
368
+ # money-rails — pass any type, always get Money
369
+ product.price_cents = 1234 # stores 1234
370
+ product.price = Money.new(1234, 'USD')
371
+ product.price # => #<Money fractional:1234 currency:USD>
274
372
  ```
275
373
 
276
- ## Convenience methods
374
+ ### Querying
277
375
 
278
- Minting::Rails adds a few small helpers:
376
+ ```ruby
377
+ # minting-rails (fixed-currency) — full type-aware querying
378
+ Product.where(price: 10.mint('USD'))
379
+ Product.where(price: [5.mint('USD'), 10.mint('USD')])
380
+ Product.where(price: 5.mint('USD')..15.mint('USD'))
381
+ Product.order(price: :desc)
382
+ Product.where(price: 10.mint('USD')).sum(:price)
383
+
384
+ # money-rails — query through cents column
385
+ Product.where(price_cents: 1000)
386
+ Product.where(price_cents: [500, 1000])
387
+ Product.where(price_cents: 500..1500)
388
+ Product.order(:price_cents)
389
+ ```
390
+
391
+ ### Decimal columns
279
392
 
280
393
  ```ruby
281
- 12.to_money('USD')
282
- 12.mint('BRL')
283
- 12.dollars
284
- 1.dollar
285
- 1.euro
286
- 12.euros
287
- '12.50'.to_money('USD')
288
- '12.50'.mint('BRL')
394
+ # minting-rails — works with decimal columns out of the box
395
+ # migration: t.decimal :price
396
+ money_attribute :price, currency: 'USD'
397
+
398
+ product.price = 12.34
399
+ product.price # => [USD 12.34]
400
+ product.read_attribute(:price) # => [USD 12.34]
401
+
402
+ # money-rails — no decimal column support
403
+ # migration: t.decimal :price ← not supported
404
+ # Must use integer cents:
405
+ # migration: t.integer :price_cents
406
+ monetize :price_cents
407
+ product.price_cents = 1234
408
+ product.price # => #<Money fractional:1234 currency:USD>
289
409
  ```
290
410
 
291
- These return `Mint::Money` instances.
411
+ ### Multi-currency
292
412
 
293
- ## Development
413
+ ```ruby
414
+ # minting-rails
415
+ money_attribute :price # expects price_amount + price_currency columns
294
416
 
295
- Clone the repository and install dependencies:
417
+ offer = Offer.new(price: 15.to_money('EUR'))
418
+ offer.price # => [EUR 15.00]
419
+ offer.price_amount # => 15.0
420
+ offer.price_currency # => "EUR"
296
421
 
297
- ```sh
298
- bundle install
422
+ # money-rails
423
+ monetize :price_cents, with_currency: :price_currency
424
+
425
+ offer = Offer.new(price: Money.new(1500, 'EUR'))
426
+ offer.price # => #<Money fractional:1500 currency:EUR>
427
+ offer.price_cents # => 1500
428
+ offer.price_currency # => "EUR"
299
429
  ```
300
430
 
301
- Run the test suite:
431
+ ### Column type auto-detection
302
432
 
303
- ```sh
304
- bundle exec rake test
433
+ ```ruby
434
+ # minting-rails same declaration works with any column type
435
+ money_attribute :price, currency: 'USD'
436
+
437
+ # t.decimal :price → stores human-readable value (12.34)
438
+ # t.integer :price → stores cents (1234)
439
+ # t.bigint :price → stores cents (1234)
440
+
441
+ # money-rails — must always match the column name
442
+ monetize :price_cents # column must be price_cents
443
+ monetize :price # column must be price — no support for other types
305
444
  ```
306
445
 
307
- The repository includes a dummy Rails application under `test/dummy` for exercising the engine in a Rails environment.
446
+ ### Performance
308
447
 
309
- ## Contributing
448
+ See [BENCHMARKS.md](BENCHMARKS.md) for detailed results across instantiation, persistence, reads, queries, arithmetic, and mass inserts. Minting-rails wins 9 of 11 benchmark cells, with the largest advantages in reads (up to 14× faster), arithmetic (6.6×), and mass inserts (1.6×).
449
+
450
+ ### What money-rails has (and minting-rails doesn't)
451
+
452
+ Minting-rails is intentionally minimal — it focuses on storing and reading money attributes with Rails primitives. Money-rails is a more mature gem (12+ years, 1.9k stars) with a broader feature set that minting-rails does not currently provide:
453
+
454
+ | Feature | money-rails | minting-rails |
455
+ |---|---|---|
456
+ | **Mongoid support** | Yes | ActiveRecord only |
457
+ | **Migration helpers** | `add_monetize :products, :price` | None |
458
+ | **View helpers** | `humanized_money`, `money_without_cents`, etc. | None |
459
+ | **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 |
460
+ | **Test matcher** | `monetize(:price_cents)` RSpec matcher | None |
461
+ | **Currency exchange** | `default_bank`, `add_rate`, EuCentralBank | None |
462
+ | **Custom currencies** | `register_currency` for non-ISO codes | Via `minting` gem config |
463
+ | **Validation integration** | `validates_numericality_of` auto-added | Must add manually |
464
+ | **Rounding mode** | Configurable `rounding_mode` | None |
465
+ | **Per-request currency** | Lambda-based for multi-tenant apps | Static default only |
466
+ | **Allow nil** | `monetize :x, allow_nil: true` | Must handle nil manually |
467
+ | **Parse error control** | `raise_error_on_money_parsing` option | Always raises |
468
+ | **Community** | 1.9k stars, 386 forks, 897 commits | New gem |
469
+
470
+ If you need any of these features today, money-rails may be a better fit. minting-rails fills a specific niche: a lightweight, performant money-in-Rails solution built on standard Rails primitives.
471
+
472
+ ## Roadmap
310
473
 
311
- Bug reports and pull requests are welcome on GitHub at [gferraz/minting-rails](https://github.com/gferraz/minting-rails).
474
+ 1. **Allow nil** `money_attribute :price, currency: 'USD', allow_nil: true`
475
+ 1. **Method-level currency** — lambda-based currency resolution for multi-tenant and instance-level scenarios
476
+ 1. **Migration helper**
312
477
 
313
- Before opening a pull request, please run:
478
+ Contributions and suggestions are welcome open an issue or PR at [gferraz/minting-rails](https://github.com/gferraz/minting-rails).
479
+
480
+ ## Development
314
481
 
315
482
  ```sh
483
+ bundle install
316
484
  bundle exec rake test
317
485
  ```
318
486
 
487
+ The dummy Rails app under `test/dummy` exercises the engine in a full Rails environment.
488
+
489
+ ## Contributing
490
+
491
+ Bug reports and pull requests welcome at [gferraz/minting-rails](https://github.com/gferraz/minting-rails).
492
+
319
493
  ## License
320
494
 
321
- The gem is available as open source under the terms of the [MIT License](MIT-LICENSE).
495
+ [MIT](MIT-LICENSE)
data/Rakefile CHANGED
@@ -4,8 +4,9 @@ require 'bundler/setup'
4
4
  require 'bundler/gem_tasks'
5
5
  require 'rake/testtask'
6
6
 
7
- task default: :test
7
+ task default: :test_run
8
8
 
9
+ desc 'Run tests'
9
10
  Rake::TestTask.new(:test_run) do |t|
10
11
  t.libs << 'test'
11
12
  t.libs << 'lib'
@@ -21,4 +22,9 @@ task :test_db_migrate do
21
22
  end
22
23
 
23
24
  desc 'Run tests (migrates test DB first)'
24
- task test: [:test_db_migrate, :test_run]
25
+ task test: %i[test_db_migrate test_run]
26
+
27
+ desc 'Run minting-rails vs money-rails benchmark'
28
+ task :bench do
29
+ sh({ 'RAILS_ENV' => 'test' }, 'bundle', 'exec', 'ruby', 'benchmark/comparison.rb')
30
+ end
@@ -14,13 +14,6 @@ Mint.configure do |config|
14
14
  { currency: 'NGN', subunit: 3, symbol: '₦' }
15
15
  ]
16
16
 
17
- # Enable currencies
18
- # Only these currencies amounts can be created
19
- # Example:
20
- # config.enabled_currencies = :all
21
-
22
- config.enabled_currencies = :all
23
-
24
17
  # To set the default currency
25
18
  #
26
19
  # It must be a registered currency
@@ -3,12 +3,11 @@
3
3
  module Mint
4
4
  module MoneyAttribute
5
5
  class Configuration
6
- attr_accessor :added_currencies, :enabled_currencies, :default_currency,
6
+ attr_accessor :added_currencies, :default_currency,
7
7
  :rounding_mode, :default_format
8
8
 
9
9
  def initialize
10
10
  @added_currencies = []
11
- @enabled_currencies = :all
12
11
  @default_currency = 'USD'
13
12
  @rounding_mode = nil
14
13
  @default_format = nil
@@ -26,19 +25,7 @@ module Mint
26
25
  config
27
26
  end
28
27
 
29
- def self.assert_valid_currency!(currency)
30
- currency = Mint.currency(currency)
31
- return currency if Mint.valid_currency?(currency)
32
-
33
- raise ArgumentError, "Invalid currency '#{currency}'. Please select a registered currency"
34
- end
35
-
36
28
  def self.default_currency
37
- @default_currency ||= Mint.assert_valid_currency!(config.default_currency)
38
- end
39
-
40
- def self.valid_currency?(currency)
41
- enabled = config.enabled_currencies
42
- currency && (enabled == :all || enabled.include?(currency.code))
29
+ @default_currency ||= Currency.resolve!(config.default_currency)
43
30
  end
44
31
  end
@@ -8,7 +8,7 @@ module Mint
8
8
  class_methods do
9
9
  # Money attribute
10
10
  def money_attribute(name, currency: Mint.default_currency, mapping: nil)
11
- currency = Mint.assert_valid_currency!(currency)
11
+ currency = Currency.resolve!(currency)
12
12
  parser = Parser.new(currency)
13
13
  if attribute_names.include? name.to_s
14
14
  attribute(name, :mint_money, currency:)
@@ -33,21 +33,29 @@ module Mint
33
33
  case col&.type
34
34
  when :bigint, :integer
35
35
  :fractional
36
- when :decimal, :numeric
37
- :to_d
38
36
  else
39
37
  :to_d # :decimal, :numeric, unknown
40
38
  end
41
39
  end
42
40
 
43
41
  def find_money_attributes(name, mapping:)
44
- composite = if mapping.present?
45
- { amount: mapping[:amount].to_s, currency: mapping[:currency].to_s }
46
- else
47
- { amount: "#{name}_amount", currency: "#{name}_currency" }
48
- end
49
- if (composite.values & attribute_names).size != 2
50
- raise ArgumentError, "Could not find attributes to map to #{name} money attribute"
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
52
+
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(', ')}"
51
59
  end
52
60
 
53
61
  composite
@@ -26,7 +26,7 @@ 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.multiplier, @currency)
29
+ Mint.money(value * @currency.fractional_multiplier, @currency)
30
30
  else
31
31
  Mint.money(value, @currency)
32
32
  end
@@ -49,6 +49,7 @@ module Mint
49
49
  end
50
50
 
51
51
  ActiveSupport.on_load(:active_record) do
52
- ActiveSupport.on_load(:active_record) { include Mint::MoneyAttribute }
52
+ include Mint::MoneyAttribute
53
+
53
54
  ActiveRecord::Type.register(:mint_money, Mint::MintMoneyType)
54
55
  end
@@ -9,11 +9,11 @@ module Mint
9
9
  end
10
10
 
11
11
  def parse(amount, currency = @default_currency)
12
- currency = Mint.assert_valid_currency!(currency)
12
+ currency = Currency.resolve!(currency)
13
13
  case amount
14
14
  when NilClass then nil
15
- when Numeric then Mint::Money.create(amount, currency)
16
- when String then Mint::Money.create(amount.to_r, currency)
15
+ when Numeric then Mint::Money.from(amount, currency)
16
+ when String then Mint::Money.from(amount.to_r, currency)
17
17
  when Mint::Money
18
18
  return amount if amount.currency == currency
19
19
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Mint
4
4
  module MoneyAttribute
5
- VERSION = '0.7.1'
5
+ VERSION = '0.8.2'
6
6
  end
7
7
  end
@@ -6,7 +6,31 @@ module Mint
6
6
  require 'generators/minting/initializer_generator'
7
7
  end
8
8
 
9
- config.to_prepare do
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
36
  code = currency_data[:currency] || currency_data['currency']
@@ -15,7 +39,7 @@ module Mint
15
39
  else
16
40
  code, subunit, symbol = *currency_data
17
41
  end
18
- Mint.register_currency(code:, subunit:, symbol:)
42
+ Currency.register(code:, subunit:, symbol:)
19
43
  end
20
44
  end
21
45
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minting-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gilson Ferraz
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-06-13 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: minting
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: 1.6.0
18
+ version: 1.8.1
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: 1.6.0
25
+ version: 1.8.1
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: rails
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -69,7 +68,6 @@ metadata:
69
68
  changelog_uri: https://github.com/gferraz/minting-rails/releases
70
69
  bug_tracker_uri: https://github.com/gferraz/minting-rails/issues
71
70
  rubygems_mfa_required: 'true'
72
- post_install_message:
73
71
  rdoc_options: []
74
72
  require_paths:
75
73
  - lib
@@ -84,8 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
82
  - !ruby/object:Gem::Version
85
83
  version: '0'
86
84
  requirements: []
87
- rubygems_version: 3.5.22
88
- signing_key:
85
+ rubygems_version: 4.0.10
89
86
  specification_version: 4
90
87
  summary: Money attributes to ActiveRecord
91
88
  test_files: []