minting-rails 0.7.0 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 205d9eb9b1d27048756b4f65f2e65781ae71f131d1d838a1834ea733349d3bf4
4
- data.tar.gz: ea2dfd05351ae59717737a0770480c1eeb6cf484264c9daea064c87ac80dd4a0
3
+ metadata.gz: dfed7ac7c21d15b7b983eac0bd391ec8f4eb4f1f89b1cb9ae0308701dd15ba76
4
+ data.tar.gz: da2eb9d34f56f26a71674a45a792b922adb57d0ff9d6a5098eb96d2e3cf5394a
5
5
  SHA512:
6
- metadata.gz: a3e434d199296b8d9a22db937b2b40af35eec147a7fdc1d4a1c1a387a3a0300b806381f2e98cb3a3dfc40371b1087f1529a3a81b399b91dff4d4f454034f39c4
7
- data.tar.gz: 8552a6d4d6c8f6567a4300c6540b04c00ca220b52b3deb0f0ba00af1fb40d0c974110eb97f04d3d0acb8d037f32852d61cd4a55a6b8d2dc17cbc398c9e58c6d4
6
+ metadata.gz: bca70a3dc33e70b65635e1dddf9a435ee718b5d6e06e26262fdc53b8d7a9c7e3b7534b88e4ee064cdc6015c2c2772d7941a9482787e96d84397c8a74ec8cd51e
7
+ data.tar.gz: 73ef1dcbedaa6cbbf832d37b450c08af1ebf41c12d47a76eb1a884a571ba869c1fbebb89be24a0422a855e53e245c53cd64811466b7a73ef0fa7de5878aa0791
data/README.md CHANGED
@@ -1,86 +1,104 @@
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
-
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.
13
-
14
- ## Requirements
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
15
13
 
16
- - Ruby 3.3 or newer.
17
- - Rails 7.1.3.2 or newer.
18
- - Minting 1.6.0 or newer.
14
+ Product.new(price: 12).price # => [USD 12.00]
15
+ ```
19
16
 
20
- ## Installation
17
+ ## Quick start
21
18
 
22
- Add the gem to your Rails application's `Gemfile`:
19
+ ```sh
20
+ bundle add minting-rails
21
+ bin/rails g mint:initializer
22
+ ```
23
23
 
24
24
  ```ruby
25
- gem 'minting-rails'
25
+ # app/models/product.rb
26
+ class Product < ApplicationRecord
27
+ money_attribute :price, currency: 'USD'
28
+ end
26
29
  ```
27
30
 
28
- Install it:
31
+ That's it. `Product.new(price: 12).price` is a `Mint::Money`.
29
32
 
30
- ```sh
31
- bundle install
32
- ```
33
+ ## Why Minting::Rails?
33
34
 
34
- Generate the initializer:
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.
35
41
 
36
- ```sh
37
- bin/rails g mint:initializer
38
- ```
42
+ ### At a glance — vs money-rails
39
43
 
40
- ## Configuration
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 | |
41
56
 
42
- Configure Minting in `config/initializers/minting.rb`:
57
+ ## Requirements
43
58
 
44
- ```ruby
45
- Mint.configure do |config|
46
- config.enabled_currencies = :all
47
- config.default_currency = 'USD'
48
- config.default_format = '%<symbol>s%<amount>f'
49
- end
50
- ```
59
+ - Ruby 3.3+
60
+ - Rails 7.1.3.2+
61
+ - [Minting](https://github.com/gferraz/minting) 1.6.0+
51
62
 
52
- You can limit the currencies that may be used:
63
+ ## Installation
53
64
 
54
65
  ```ruby
55
- Mint.configure do |config|
56
- config.enabled_currencies = %w[USD EUR BRL]
57
- config.default_currency = 'USD'
58
- end
66
+ # Gemfile
67
+ gem 'minting-rails'
68
+ ```
69
+
70
+ ```sh
71
+ bundle install
72
+ bin/rails g mint:initializer
59
73
  ```
60
74
 
61
- You can also register custom currencies before enabling or using them:
75
+ The generator creates `config/initializers/minting.rb`.
76
+
77
+ ## Configuration
62
78
 
63
79
  ```ruby
80
+ # config/initializers/minting.rb
64
81
  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
- ]
69
-
70
- config.enabled_currencies = :all
71
- config.default_currency = 'CRC'
82
+ config.default_currency = 'USD'
83
+ # enabled_currencies removed all registered currencies are valid
72
84
  end
73
85
  ```
74
86
 
75
- The default currency must be registered and included in `enabled_currencies`.
87
+ See the [Minting gem](https://github.com/gferraz/minting) for full configuration options (custom currencies, formatting, rounding).
76
88
 
77
- ## Usage
89
+ ## Usage — Two modes
78
90
 
79
- Declare money attributes in your Active Record models with `money_attribute`.
91
+ ### Decision table
80
92
 
81
- ### Single-column fixed currency
93
+ | | Fixed currency (single column) | Multi-currency (amount + currency) |
94
+ |---|---|---|
95
+ | **Migration** | `t.decimal :price` | `t.decimal :price_amount` + `t.string :price_currency` |
96
+ | **Model** | `money_attribute :price, currency: 'USD'` | `money_attribute :price` |
97
+ | **When to use** | Column always holds the same currency | Each row can hold a different currency |
98
+ | **Column type** | `decimal`, `integer`, or `bigint` | `decimal`, `integer`, or `bigint` for amount; `string` for currency |
99
+ | **Query** | `Product.where(price: 10.mint('USD'))` — full type support | `Offer.where(price: 10.mint('EUR'))` — equality only |
82
100
 
83
- Use this when a column always stores one currency, such as a `price` column that is always USD.
101
+ ### Fixed currency (single column)
84
102
 
85
103
  Migration:
86
104
 
@@ -90,7 +108,6 @@ class CreateProducts < ActiveRecord::Migration[7.1]
90
108
  create_table :products do |t|
91
109
  t.decimal :price
92
110
  t.decimal :discount
93
-
94
111
  t.timestamps
95
112
  end
96
113
  end
@@ -110,24 +127,18 @@ Assignments are normalized to `Mint::Money`:
110
127
 
111
128
  ```ruby
112
129
  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>
130
+ product.price # => [USD 12.00]
131
+ product.discount # => [USD 3.50]
119
132
  ```
120
133
 
121
- Assigning a `Mint::Money` with a different currency raises `ArgumentError`:
134
+ A currency mismatch raises `ArgumentError`:
122
135
 
123
136
  ```ruby
124
137
  Product.new(price: 12.to_money('EUR'))
125
- # raises ArgumentError because the attribute only accepts USD
138
+ # => ArgumentError: ... has different currency. Only USD allowed.
126
139
  ```
127
140
 
128
- ### Amount and currency columns
129
-
130
- Use this when each row can store a different currency per record.
141
+ ### Multi-currency (amount + currency columns)
131
142
 
132
143
  Migration:
133
144
 
@@ -136,8 +147,7 @@ class CreateOffers < ActiveRecord::Migration[7.1]
136
147
  def change
137
148
  create_table :offers do |t|
138
149
  t.decimal :price_amount
139
- t.string :price_currency
140
-
150
+ t.string :price_currency
141
151
  t.timestamps
142
152
  end
143
153
  end
@@ -156,166 +166,286 @@ The attribute is composed from `price_amount` and `price_currency`:
156
166
 
157
167
  ```ruby
158
168
  offer = Offer.new(price: 15.to_money('EUR'))
169
+ offer.price # => [EUR 15.00]
170
+ offer.price_amount # => 15.0
171
+ offer.price_currency # => "EUR"
172
+ ```
159
173
 
160
- offer.price
161
- # => #<Mint::Money ... EUR 15.00>
162
-
163
- offer.price_amount
164
- # => 15.0
174
+ When assigning a plain number or string, `Mint.default_currency` is used:
165
175
 
166
- offer.price_currency
167
- # => "EUR"
176
+ ```ruby
177
+ offer = Offer.new(price: '12')
178
+ offer.price.currency.code # => "USD"
168
179
  ```
169
180
 
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.
181
+ ## Column type detection
173
182
 
174
- Migration:
183
+ Declare the column as `decimal`, `integer`, or `bigint` — the gem adapts:
175
184
 
176
185
  ```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
186
+ # Migration
187
+ create_table :orders do |t|
188
+ t.bigint :total_amount # stored as cents (subunits)
189
+ t.string :total_currency
186
190
  end
187
- ```
188
-
189
- Model:
190
191
 
191
- ```ruby
192
+ # Model
192
193
  class Order < ApplicationRecord
193
194
  money_attribute :total
194
195
  end
196
+
197
+ Order.new(total: 19.99.to_money('USD')).total_amount # => 1999
195
198
  ```
196
199
 
197
- The amount is stored and retrieved in subunits:
200
+ Same for fixed-currency attributes:
198
201
 
199
202
  ```ruby
200
- order = Order.new(total: 19.99.to_money(:USD))
203
+ # Migration
204
+ t.bigint :price
201
205
 
202
- order.total
203
- # => #<Mint::Money ... USD 19.99>
206
+ # Model (no change needed)
207
+ money_attribute :price, currency: 'USD'
208
+ ```
204
209
 
205
- order.total_amount
206
- # => 1999
210
+ > Use `integer`/`bigint` for large tables (faster, smaller). Use `decimal` when SQL-level readability matters.
207
211
 
208
- order.total_currency
209
- # => "USD"
210
- ```
212
+ ## Custom column names
211
213
 
212
- The same applies to single-column fixed-currency attributes:
214
+ If your columns don't follow the `<name>_amount` / `<name>_currency` convention:
213
215
 
214
216
  ```ruby
215
- class Ticket < ApplicationRecord
216
- money_attribute :price, currency: 'USD'
217
+ class Invoice < ApplicationRecord
218
+ money_attribute :total, mapping: {
219
+ amount: :total_amount,
220
+ currency: :currency_code
221
+ }
217
222
  end
218
223
  ```
219
224
 
220
- Migration:
225
+ The mapping keys are `:amount` and `:currency`; values are your database column names.
226
+
227
+ ## Querying
228
+
229
+ Fixed-currency attributes support Rails-native querying through the custom type:
221
230
 
222
231
  ```ruby
223
- t.bigint :price
224
- ```
232
+ # Equality
233
+ Product.where(price: 10.mint('USD'))
234
+
235
+ # IN clause
236
+ Product.where(price: [10.mint('USD'), 20.mint('USD')])
225
237
 
226
- No model change is needed. The column type drives the behavior.
238
+ # BETWEEN
239
+ Product.where(price: 10.mint('USD')..20.mint('USD'))
227
240
 
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
241
+ # Ordering
242
+ Product.order(price: :desc)
229
243
 
230
- ### Default Currency
244
+ # Aggregation
245
+ Product.where(price: 10.mint('USD')).sum(:price)
246
+ ```
231
247
 
232
- When you assign a plain number or string, Minting::Rails uses `Mint.default_currency`:
248
+ Multi-currency attributes support equality queries via `composed_of`:
233
249
 
234
250
  ```ruby
235
- offer = Offer.new(price: '12')
251
+ Offer.where(price: 10.mint('EUR'))
252
+ ```
253
+
254
+ For comparisons on multi-currency attributes, use the backing columns directly:
236
255
 
237
- offer.price.currency_code
238
- # => "USD"
256
+ ```ruby
257
+ Offer.where(price_amount: 10..20, price_currency: 'EUR')
258
+ Offer.where('price_amount > ? AND price_currency = ?', 10, 'EUR')
239
259
  ```
240
260
 
241
- ### Custom column names
261
+ ## Convenience methods
242
262
 
243
- If your amount and currency columns do not follow the `<name>_amount` and `<name>_currency` convention, pass a mapping:
263
+ Minting::Rails adds small helpers on `Numeric` and `String`:
244
264
 
245
265
  ```ruby
246
- class Invoice < ApplicationRecord
247
- money_attribute :total, mapping: {
248
- amount: :total_amount,
249
- currency: :currency_code
250
- }
251
- end
266
+ 12.to_money('USD') # => [USD 12.00]
267
+ 12.dollars # => [USD 12.00]
268
+ 12.euros # => [EUR 12.00]
269
+ '12.50'.mint('BRL') # => [BRL 12.50]
252
270
  ```
253
271
 
254
- The mapping keys are `:amount` and `:currency`. The values are your database columns.
272
+ > If you prefer not to extend core classes, use `Mint::Money.money(12, 'USD')` instead.
255
273
 
256
- ## Querying
274
+ ## vs money-rails
257
275
 
258
- Fixed-currency attributes can be queried with `Mint::Money` values:
276
+ [Money-rails](https://github.com/RubyMoney/money-rails) is the most popular money-in-Rails gem. Here's how they compare side-by-side.
277
+
278
+ ### Model declaration
259
279
 
260
280
  ```ruby
261
- Product.where(price: 15.to_money('USD'))
281
+ # minting-rails
282
+ class Product < ApplicationRecord
283
+ money_attribute :price, currency: 'USD' # single column, fixed currency
284
+ money_attribute :total # two columns, multi-currency
285
+ end
286
+
287
+ # money-rails
288
+ class Product < ApplicationRecord
289
+ monetize :price_cents # single cents column, fixed currency
290
+ monetize :total_cents, with_currency: :total_currency # two columns, multi-currency
291
+ end
262
292
  ```
263
293
 
264
- Composed attributes can also be queried with a money object:
294
+ ### Migration
265
295
 
266
296
  ```ruby
267
- Offer.where(price: 15.to_money('EUR'))
297
+ # minting-rails — any numeric column type
298
+ create_table :products do |t|
299
+ t.decimal :price # stores 12.34
300
+ t.integer :discount # stores 1234 (cents)
301
+ t.bigint :total_amount # stores 1999 (cents)
302
+ t.string :total_currency
303
+ end
304
+
305
+ # money-rails — integer cents only
306
+ create_table :products do |t|
307
+ t.integer :price_cents # stores 1234 (cents)
308
+ t.integer :discount_cents # stores 350 (cents)
309
+ t.integer :total_cents
310
+ t.string :total_currency
311
+ end
268
312
  ```
269
313
 
270
- You can still query the backing columns directly when that is clearer:
314
+ ### Reading & writing
271
315
 
272
316
  ```ruby
273
- Offer.where(price_amount: 15, price_currency: 'EUR')
317
+ # minting-rails — pass any type, always get Mint::Money
318
+ product.price = 12.34 # stores 12.34 in decimal column
319
+ product.price = 1234 # stores 1234 in integer column
320
+ product.price = '$12.34' # parses string
321
+ product.price # => [USD 12.34]
322
+
323
+ # money-rails — pass any type, always get Money
324
+ product.price_cents = 1234 # stores 1234
325
+ product.price = Money.new(1234, 'USD')
326
+ product.price # => #<Money fractional:1234 currency:USD>
274
327
  ```
275
328
 
276
- ## Convenience methods
329
+ ### Querying
277
330
 
278
- Minting::Rails adds a few small helpers:
331
+ ```ruby
332
+ # minting-rails (fixed-currency) — full type-aware querying
333
+ Product.where(price: 10.mint('USD'))
334
+ Product.where(price: [5.mint('USD'), 10.mint('USD')])
335
+ Product.where(price: 5.mint('USD')..15.mint('USD'))
336
+ Product.order(price: :desc)
337
+ Product.where(price: 10.mint('USD')).sum(:price)
338
+
339
+ # money-rails — query through cents column
340
+ Product.where(price_cents: 1000)
341
+ Product.where(price_cents: [500, 1000])
342
+ Product.where(price_cents: 500..1500)
343
+ Product.order(:price_cents)
344
+ ```
345
+
346
+ ### Decimal columns
279
347
 
280
348
  ```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')
349
+ # minting-rails — works with decimal columns out of the box
350
+ # migration: t.decimal :price
351
+ money_attribute :price, currency: 'USD'
352
+
353
+ product.price = 12.34
354
+ product.price # => [USD 12.34]
355
+ product.read_attribute(:price) # => [USD 12.34]
356
+
357
+ # money-rails — no decimal column support
358
+ # migration: t.decimal :price ← not supported
359
+ # Must use integer cents:
360
+ # migration: t.integer :price_cents
361
+ monetize :price_cents
362
+ product.price_cents = 1234
363
+ product.price # => #<Money fractional:1234 currency:USD>
289
364
  ```
290
365
 
291
- These return `Mint::Money` instances.
366
+ ### Multi-currency
292
367
 
293
- ## Development
368
+ ```ruby
369
+ # minting-rails
370
+ money_attribute :price # expects price_amount + price_currency columns
294
371
 
295
- Clone the repository and install dependencies:
372
+ offer = Offer.new(price: 15.to_money('EUR'))
373
+ offer.price # => [EUR 15.00]
374
+ offer.price_amount # => 15.0
375
+ offer.price_currency # => "EUR"
296
376
 
297
- ```sh
298
- bundle install
377
+ # money-rails
378
+ monetize :price_cents, with_currency: :price_currency
379
+
380
+ offer = Offer.new(price: Money.new(1500, 'EUR'))
381
+ offer.price # => #<Money fractional:1500 currency:EUR>
382
+ offer.price_cents # => 1500
383
+ offer.price_currency # => "EUR"
299
384
  ```
300
385
 
301
- Run the test suite:
386
+ ### Column type auto-detection
302
387
 
303
- ```sh
304
- bundle exec rake test
388
+ ```ruby
389
+ # minting-rails same declaration works with any column type
390
+ money_attribute :price, currency: 'USD'
391
+
392
+ # t.decimal :price → stores human-readable value (12.34)
393
+ # t.integer :price → stores cents (1234)
394
+ # t.bigint :price → stores cents (1234)
395
+
396
+ # money-rails — must always match the column name
397
+ monetize :price_cents # column must be price_cents
398
+ monetize :price # column must be price — no support for other types
305
399
  ```
306
400
 
307
- The repository includes a dummy Rails application under `test/dummy` for exercising the engine in a Rails environment.
401
+ ### Performance
308
402
 
309
- ## Contributing
403
+ 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×).
404
+
405
+ ### What money-rails has (and minting-rails doesn't)
406
+
407
+ 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:
408
+
409
+ | Feature | money-rails | minting-rails |
410
+ |---|---|---|
411
+ | **Mongoid support** | Yes | ActiveRecord only |
412
+ | **Migration helpers** | `add_monetize :products, :price` | None |
413
+ | **View helpers** | `humanized_money`, `money_without_cents`, etc. | None |
414
+ | **I18n / locale files** | Built-in locale-aware formatting | None |
415
+ | **Test matcher** | `monetize(:price_cents)` RSpec matcher | None |
416
+ | **Currency exchange** | `default_bank`, `add_rate`, EuCentralBank | None |
417
+ | **Custom currencies** | `register_currency` for non-ISO codes | Via `minting` gem config |
418
+ | **Validation integration** | `validates_numericality_of` auto-added | Must add manually |
419
+ | **Rounding mode** | Configurable `rounding_mode` | None |
420
+ | **Per-request currency** | Lambda-based for multi-tenant apps | Static default only |
421
+ | **Allow nil** | `monetize :x, allow_nil: true` | Must handle nil manually |
422
+ | **Parse error control** | `raise_error_on_money_parsing` option | Always raises |
423
+ | **Community** | 1.9k stars, 386 forks, 897 commits | New gem |
424
+
425
+ 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.
426
+
427
+ ## Roadmap
310
428
 
311
- Bug reports and pull requests are welcome on GitHub at [gferraz/minting-rails](https://github.com/gferraz/minting-rails).
429
+ 1. **Allow nil** `money_attribute :price, currency: 'USD', allow_nil: true`
430
+ 1. **Method-level currency** — lambda-based currency resolution for multi-tenant and instance-level scenarios
431
+ 1. **Migration helper**
432
+ 1. **Internationalization**
312
433
 
313
- Before opening a pull request, please run:
434
+ Contributions and suggestions are welcome open an issue or PR at [gferraz/minting-rails](https://github.com/gferraz/minting-rails).
435
+
436
+ ## Development
314
437
 
315
438
  ```sh
439
+ bundle install
316
440
  bundle exec rake test
317
441
  ```
318
442
 
443
+ The dummy Rails app under `test/dummy` exercises the engine in a full Rails environment.
444
+
445
+ ## Contributing
446
+
447
+ Bug reports and pull requests welcome at [gferraz/minting-rails](https://github.com/gferraz/minting-rails).
448
+
319
449
  ## License
320
450
 
321
- The gem is available as open source under the terms of the [MIT License](MIT-LICENSE).
451
+ [MIT](MIT-LICENSE)
data/Rakefile CHANGED
@@ -4,11 +4,27 @@ 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
- Rake::TestTask.new(:test) do |t|
9
+ desc 'Run tests'
10
+ Rake::TestTask.new(:test_run) do |t|
10
11
  t.libs << 'test'
11
12
  t.libs << 'lib'
12
13
  t.test_files = FileList['test/**/*_test.rb']
13
14
  t.ruby_opts << '-rtest_helper.rb'
14
15
  end
16
+
17
+ desc 'Migrate test database'
18
+ task :test_db_migrate do
19
+ Dir.chdir('test/dummy') do
20
+ sh({ 'RAILS_ENV' => 'test' }, 'bin/rails', 'db:migrate')
21
+ end
22
+ end
23
+
24
+ desc 'Run tests (migrates test DB first)'
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
- ActiveRecord::Base.include Mint::MoneyAttribute
52
+ include Mint::MoneyAttribute
53
+
53
54
  ActiveRecord::Type.register(:mint_money, Mint::MintMoneyType)
54
55
  end
@@ -9,19 +9,20 @@ 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
- return amount if amount.currency == currency
19
- raise TypeError, "Cannot automatically convert #{amount} to #{currency.code}"
18
+ return amount if amount.currency == currency
19
+
20
+ raise TypeError, "Cannot automatically convert #{amount} to #{currency.code}"
20
21
  else
21
22
  Mint.parse(amount, currency)
22
23
  end
23
24
  end
24
- alias_method :call, :parse
25
+ alias call parse
25
26
  end
26
27
  end
27
28
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Mint
4
4
  module MoneyAttribute
5
- VERSION = '0.7.0'
5
+ VERSION = '0.8.1'
6
6
  end
7
7
  end
@@ -6,7 +6,7 @@ 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
10
  Array(Mint.config.added_currencies).each do |currency_data|
11
11
  if currency_data.respond_to?(:values_at)
12
12
  code = currency_data[:currency] || currency_data['currency']
@@ -15,7 +15,7 @@ module Mint
15
15
  else
16
16
  code, subunit, symbol = *currency_data
17
17
  end
18
- Mint.register_currency(code:, subunit:, symbol:)
18
+ Currency.register(code:, subunit:, symbol:)
19
19
  end
20
20
  end
21
21
  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.7.0
4
+ version: 0.8.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: 1.6.0
18
+ version: 1.8.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: 1.6.0
25
+ version: 1.8.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rails
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -82,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
82
82
  - !ruby/object:Gem::Version
83
83
  version: '0'
84
84
  requirements: []
85
- rubygems_version: 4.0.9
85
+ rubygems_version: 4.0.10
86
86
  specification_version: 4
87
87
  summary: Money attributes to ActiveRecord
88
88
  test_files: []