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 +4 -4
- data/README.md +316 -142
- data/Rakefile +8 -2
- data/lib/generators/templates/minting.rb +0 -7
- data/lib/minting/money_attribute/configuration.rb +2 -15
- data/lib/minting/money_attribute/money_attribute.rb +18 -10
- data/lib/minting/money_attribute/money_type.rb +3 -2
- data/lib/minting/money_attribute/parser.rb +3 -3
- data/lib/minting/money_attribute/version.rb +1 -1
- data/lib/minting/railties.rb +26 -2
- metadata +5 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c6f9a0ca2ddc8795a648b6c6047728d626106e2afddbe63869ad3159fcd819e
|
|
4
|
+
data.tar.gz: 863f9135a359162e053f988e991a9ac2a832e56e310cc1c7511410212e7c0427
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 148ee6a06dde29d5685c47e5c3e284fdce46c7d75cd84ab265760087f1e3fcc43afb9f60e949bbe615cf311b576bb9a6180569f103c5665a137d10849604e42c
|
|
7
|
+
data.tar.gz: 7c0eaca1345709f22bb446764b9220c5767cca2998e0570490871555cce3c2d692e5839eea9eedcae282cdbe032dca603d1a89bb8ba61d358aa725e83b26b883
|
data/README.md
CHANGED
|
@@ -1,86 +1,149 @@
|
|
|
1
1
|
# Minting::Rails
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/gferraz/minting-rails/actions/workflows/ci.yml)
|
|
4
|
+
[](https://badge.fury.io/rb/minting-rails)
|
|
4
5
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
17
|
-
- Rails 7.1.3.2
|
|
18
|
-
- Minting 1.6.0
|
|
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
|
-
|
|
75
|
+
The generator creates `config/initializers/minting.rb`.
|
|
41
76
|
|
|
42
|
-
|
|
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
|
-
|
|
83
|
+
# enabled_currencies removed — all registered currencies are valid
|
|
49
84
|
end
|
|
50
85
|
```
|
|
51
86
|
|
|
52
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
### Decision table
|
|
80
137
|
|
|
81
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
179
|
+
A currency mismatch raises `ArgumentError`:
|
|
122
180
|
|
|
123
181
|
```ruby
|
|
124
182
|
Product.new(price: 12.to_money('EUR'))
|
|
125
|
-
#
|
|
183
|
+
# => ArgumentError: ... has different currency. Only USD allowed.
|
|
126
184
|
```
|
|
127
185
|
|
|
128
|
-
###
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
221
|
+
```ruby
|
|
222
|
+
offer = Offer.new(price: '12')
|
|
223
|
+
offer.price.currency.code # => "USD"
|
|
168
224
|
```
|
|
169
225
|
|
|
170
|
-
|
|
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
|
-
|
|
228
|
+
Declare the column as `decimal`, `integer`, or `bigint` — the gem adapts:
|
|
175
229
|
|
|
176
230
|
```ruby
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
+
Same for fixed-currency attributes:
|
|
198
246
|
|
|
199
247
|
```ruby
|
|
200
|
-
|
|
248
|
+
# Migration
|
|
249
|
+
t.bigint :price
|
|
201
250
|
|
|
202
|
-
|
|
203
|
-
|
|
251
|
+
# Model (no change needed)
|
|
252
|
+
money_attribute :price, currency: 'USD'
|
|
253
|
+
```
|
|
204
254
|
|
|
205
|
-
|
|
206
|
-
# => 1999
|
|
255
|
+
> Use `integer`/`bigint` for large tables (faster, smaller). Use `decimal` when SQL-level readability matters.
|
|
207
256
|
|
|
208
|
-
|
|
209
|
-
# => "USD"
|
|
210
|
-
```
|
|
257
|
+
## Custom column names
|
|
211
258
|
|
|
212
|
-
|
|
259
|
+
If your columns don't follow the `<name>_amount` / `<name>_currency` convention:
|
|
213
260
|
|
|
214
261
|
```ruby
|
|
215
|
-
class
|
|
216
|
-
money_attribute :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
+
# BETWEEN
|
|
284
|
+
Product.where(price: 10.mint('USD')..20.mint('USD'))
|
|
227
285
|
|
|
228
|
-
|
|
286
|
+
# Ordering
|
|
287
|
+
Product.order(price: :desc)
|
|
229
288
|
|
|
230
|
-
|
|
289
|
+
# Aggregation
|
|
290
|
+
Product.where(price: 10.mint('USD')).sum(:price)
|
|
291
|
+
```
|
|
231
292
|
|
|
232
|
-
|
|
293
|
+
Multi-currency attributes support equality queries via `composed_of`:
|
|
233
294
|
|
|
234
295
|
```ruby
|
|
235
|
-
|
|
296
|
+
Offer.where(price: 10.mint('EUR'))
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
For comparisons on multi-currency attributes, use the backing columns directly:
|
|
236
300
|
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
306
|
+
## Convenience methods
|
|
242
307
|
|
|
243
|
-
|
|
308
|
+
Minting::Rails adds small helpers on `Numeric` and `String`:
|
|
244
309
|
|
|
245
310
|
```ruby
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
317
|
+
> If you prefer not to extend core classes, use `Mint::Money.money(12, 'USD')` instead.
|
|
255
318
|
|
|
256
|
-
##
|
|
319
|
+
## vs money-rails
|
|
257
320
|
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
339
|
+
### Migration
|
|
265
340
|
|
|
266
341
|
```ruby
|
|
267
|
-
|
|
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
|
-
|
|
359
|
+
### Reading & writing
|
|
271
360
|
|
|
272
361
|
```ruby
|
|
273
|
-
|
|
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
|
-
|
|
374
|
+
### Querying
|
|
277
375
|
|
|
278
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
12.
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
411
|
+
### Multi-currency
|
|
292
412
|
|
|
293
|
-
|
|
413
|
+
```ruby
|
|
414
|
+
# minting-rails
|
|
415
|
+
money_attribute :price # expects price_amount + price_currency columns
|
|
294
416
|
|
|
295
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
431
|
+
### Column type auto-detection
|
|
302
432
|
|
|
303
|
-
```
|
|
304
|
-
|
|
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
|
-
|
|
446
|
+
### Performance
|
|
308
447
|
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: :
|
|
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: [
|
|
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, :
|
|
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 ||=
|
|
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 =
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
12
|
+
currency = Currency.resolve!(currency)
|
|
13
13
|
case amount
|
|
14
14
|
when NilClass then nil
|
|
15
|
-
when Numeric then Mint::Money.
|
|
16
|
-
when String then Mint::Money.
|
|
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
|
|
data/lib/minting/railties.rb
CHANGED
|
@@ -6,7 +6,31 @@ module Mint
|
|
|
6
6
|
require 'generators/minting/initializer_generator'
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
config.
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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:
|
|
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: []
|