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 +4 -4
- data/README.md +280 -150
- data/Rakefile +18 -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 +7 -6
- data/lib/minting/money_attribute/version.rb +1 -1
- data/lib/minting/railties.rb +2 -2
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dfed7ac7c21d15b7b983eac0bd391ec8f4eb4f1f89b1cb9ae0308701dd15ba76
|
|
4
|
+
data.tar.gz: da2eb9d34f56f26a71674a45a792b922adb57d0ff9d6a5098eb96d2e3cf5394a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bca70a3dc33e70b65635e1dddf9a435ee718b5d6e06e26262fdc53b8d7a9c7e3b7534b88e4ee064cdc6015c2c2772d7941a9482787e96d84397c8a74ec8cd51e
|
|
7
|
+
data.tar.gz: 73ef1dcbedaa6cbbf832d37b450c08af1ebf41c12d47a76eb1a884a571ba869c1fbebb89be24a0422a855e53e245c53cd64811466b7a73ef0fa7de5878aa0791
|
data/README.md
CHANGED
|
@@ -1,86 +1,104 @@
|
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
- Minting 1.6.0 or newer.
|
|
14
|
+
Product.new(price: 12).price # => [USD 12.00]
|
|
15
|
+
```
|
|
19
16
|
|
|
20
|
-
##
|
|
17
|
+
## Quick start
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
```sh
|
|
20
|
+
bundle add minting-rails
|
|
21
|
+
bin/rails g mint:initializer
|
|
22
|
+
```
|
|
23
23
|
|
|
24
24
|
```ruby
|
|
25
|
-
|
|
25
|
+
# app/models/product.rb
|
|
26
|
+
class Product < ApplicationRecord
|
|
27
|
+
money_attribute :price, currency: 'USD'
|
|
28
|
+
end
|
|
26
29
|
```
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
That's it. `Product.new(price: 12).price` is a `Mint::Money`.
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
bundle install
|
|
32
|
-
```
|
|
33
|
+
## Why Minting::Rails?
|
|
33
34
|
|
|
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.
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
bin/rails g mint:initializer
|
|
38
|
-
```
|
|
42
|
+
### At a glance — vs money-rails
|
|
39
43
|
|
|
40
|
-
|
|
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
|
-
|
|
57
|
+
## Requirements
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
63
|
+
## Installation
|
|
53
64
|
|
|
54
65
|
```ruby
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
# Gemfile
|
|
67
|
+
gem 'minting-rails'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
bundle install
|
|
72
|
+
bin/rails g mint:initializer
|
|
59
73
|
```
|
|
60
74
|
|
|
61
|
-
|
|
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.
|
|
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
|
-
]
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
### Decision table
|
|
80
92
|
|
|
81
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
134
|
+
A currency mismatch raises `ArgumentError`:
|
|
122
135
|
|
|
123
136
|
```ruby
|
|
124
137
|
Product.new(price: 12.to_money('EUR'))
|
|
125
|
-
#
|
|
138
|
+
# => ArgumentError: ... has different currency. Only USD allowed.
|
|
126
139
|
```
|
|
127
140
|
|
|
128
|
-
###
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
176
|
+
```ruby
|
|
177
|
+
offer = Offer.new(price: '12')
|
|
178
|
+
offer.price.currency.code # => "USD"
|
|
168
179
|
```
|
|
169
180
|
|
|
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.
|
|
181
|
+
## Column type detection
|
|
173
182
|
|
|
174
|
-
|
|
183
|
+
Declare the column as `decimal`, `integer`, or `bigint` — the gem adapts:
|
|
175
184
|
|
|
176
185
|
```ruby
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
Same for fixed-currency attributes:
|
|
198
201
|
|
|
199
202
|
```ruby
|
|
200
|
-
|
|
203
|
+
# Migration
|
|
204
|
+
t.bigint :price
|
|
201
205
|
|
|
202
|
-
|
|
203
|
-
|
|
206
|
+
# Model (no change needed)
|
|
207
|
+
money_attribute :price, currency: 'USD'
|
|
208
|
+
```
|
|
204
209
|
|
|
205
|
-
|
|
206
|
-
# => 1999
|
|
210
|
+
> Use `integer`/`bigint` for large tables (faster, smaller). Use `decimal` when SQL-level readability matters.
|
|
207
211
|
|
|
208
|
-
|
|
209
|
-
# => "USD"
|
|
210
|
-
```
|
|
212
|
+
## Custom column names
|
|
211
213
|
|
|
212
|
-
|
|
214
|
+
If your columns don't follow the `<name>_amount` / `<name>_currency` convention:
|
|
213
215
|
|
|
214
216
|
```ruby
|
|
215
|
-
class
|
|
216
|
-
money_attribute :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
+
# BETWEEN
|
|
239
|
+
Product.where(price: 10.mint('USD')..20.mint('USD'))
|
|
227
240
|
|
|
228
|
-
|
|
241
|
+
# Ordering
|
|
242
|
+
Product.order(price: :desc)
|
|
229
243
|
|
|
230
|
-
|
|
244
|
+
# Aggregation
|
|
245
|
+
Product.where(price: 10.mint('USD')).sum(:price)
|
|
246
|
+
```
|
|
231
247
|
|
|
232
|
-
|
|
248
|
+
Multi-currency attributes support equality queries via `composed_of`:
|
|
233
249
|
|
|
234
250
|
```ruby
|
|
235
|
-
|
|
251
|
+
Offer.where(price: 10.mint('EUR'))
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
For comparisons on multi-currency attributes, use the backing columns directly:
|
|
236
255
|
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
261
|
+
## Convenience methods
|
|
242
262
|
|
|
243
|
-
|
|
263
|
+
Minting::Rails adds small helpers on `Numeric` and `String`:
|
|
244
264
|
|
|
245
265
|
```ruby
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
272
|
+
> If you prefer not to extend core classes, use `Mint::Money.money(12, 'USD')` instead.
|
|
255
273
|
|
|
256
|
-
##
|
|
274
|
+
## vs money-rails
|
|
257
275
|
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
294
|
+
### Migration
|
|
265
295
|
|
|
266
296
|
```ruby
|
|
267
|
-
|
|
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
|
-
|
|
314
|
+
### Reading & writing
|
|
271
315
|
|
|
272
316
|
```ruby
|
|
273
|
-
|
|
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
|
-
|
|
329
|
+
### Querying
|
|
277
330
|
|
|
278
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
12.
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
366
|
+
### Multi-currency
|
|
292
367
|
|
|
293
|
-
|
|
368
|
+
```ruby
|
|
369
|
+
# minting-rails
|
|
370
|
+
money_attribute :price # expects price_amount + price_currency columns
|
|
294
371
|
|
|
295
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
386
|
+
### Column type auto-detection
|
|
302
387
|
|
|
303
|
-
```
|
|
304
|
-
|
|
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
|
-
|
|
401
|
+
### Performance
|
|
308
402
|
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: :
|
|
7
|
+
task default: :test_run
|
|
8
8
|
|
|
9
|
-
|
|
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, :
|
|
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,19 +9,20 @@ 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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
25
|
+
alias call parse
|
|
25
26
|
end
|
|
26
27
|
end
|
|
27
28
|
end
|
data/lib/minting/railties.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Mint
|
|
|
6
6
|
require 'generators/minting/initializer_generator'
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
config.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
85
|
+
rubygems_version: 4.0.10
|
|
86
86
|
specification_version: 4
|
|
87
87
|
summary: Money attributes to ActiveRecord
|
|
88
88
|
test_files: []
|