money_attribute 0.14.5 → 1.1.0
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 +157 -104
- data/Rakefile +21 -5
- data/lib/money_attribute/configuration.rb +10 -11
- data/lib/money_attribute/converter.rb +6 -4
- data/lib/money_attribute/core_ext/string.rb +8 -0
- data/lib/money_attribute/core_ext.rb +2 -9
- data/lib/money_attribute/form_builder_extension.rb +2 -1
- data/lib/money_attribute/macro.rb +41 -41
- data/lib/money_attribute/migration_extensions/helper.rb +53 -45
- data/lib/money_attribute/migration_extensions/schema_statements.rb +16 -3
- data/lib/money_attribute/migration_extensions/table_definition.rb +15 -2
- data/lib/money_attribute/money_amount.rb +37 -0
- data/lib/money_attribute/railtie.rb +25 -15
- data/lib/money_attribute/type.rb +5 -7
- data/lib/money_attribute/version.rb +1 -1
- data/lib/money_attribute.rb +2 -0
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bcfa5f42f59d3a7f372ddbff422a4e2aa45faccf4f5bd912fc2509e974bda609
|
|
4
|
+
data.tar.gz: 173509ac929dc32881a8bae54be2c3a7c0f92632ab8fae0192d84ae9b42433eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18598feb924b703ae70e0fe68a76dd93d3ec9f22753d8556616d8c42ebae8460512e9451ed2e80b96feb44d380e8c03e1d2c0a23f22cf956773d001f5e27d83b
|
|
7
|
+
data.tar.gz: 1178b4b944cbd54b93d8d96f03a55403141086573d5f15f6225a820e65fb9a3e4fae7f0277518563274fa91a7dc7fd6a4ae889e1391dfb69efe72c07de0bb53a
|
data/README.md
CHANGED
|
@@ -3,17 +3,38 @@
|
|
|
3
3
|
[](https://github.com/gferraz/money-attribute/actions/workflows/ci.yml)
|
|
4
4
|
[](https://badge.fury.io/rb/money_attribute)
|
|
5
5
|
|
|
6
|
-
Store and read Active Record attributes as `Mint::Money` objects with
|
|
6
|
+
Store and read Active Record attributes as `Mint::Money` objects with no manual serialization.
|
|
7
|
+
|
|
8
|
+
`money_attribute` uses two DB columns (amount + currency) for per-row multi-currency data. A simpler `money_amount` variant is also available for fixed-currency models (see [note](#single-column-mode--money_amount-fixed-currency)).
|
|
7
9
|
|
|
8
10
|
```ruby
|
|
9
11
|
class Product < ApplicationRecord
|
|
10
|
-
money_attribute :price
|
|
11
|
-
money_attribute :total # multi-currency, two columns
|
|
12
|
+
money_attribute :price
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
Product.new(price: 12).price # => [USD 12.00]
|
|
15
|
+
p = Product.new(price: 12.44.dollars).price # => [USD 12.00]
|
|
16
|
+
p.price * 2 # => [USD 24.88]
|
|
15
17
|
```
|
|
16
18
|
|
|
19
|
+
## Table of contents
|
|
20
|
+
|
|
21
|
+
- [Quick start](#quick-start)
|
|
22
|
+
- [Why MoneyAttribute](#why-moneyattribute)
|
|
23
|
+
- [Requirements](#requirements)
|
|
24
|
+
- [Installation](#installation)
|
|
25
|
+
- [Migration helpers](#migration-helpers)
|
|
26
|
+
- [Configuration](#configuration)
|
|
27
|
+
- [Usage](#usage)
|
|
28
|
+
- [Column type detection](#column-type-detection)
|
|
29
|
+
- [Custom column names](#custom-column-names)
|
|
30
|
+
- [Column resolution](#column-resolution)
|
|
31
|
+
- [Querying](#querying)
|
|
32
|
+
- [Convenience methods](#convenience-methods)
|
|
33
|
+
- [Form helpers](#form-helpers)
|
|
34
|
+
- [Roadmap](#roadmap)
|
|
35
|
+
- [Development & Contributing](#development)
|
|
36
|
+
- [License](#license)
|
|
37
|
+
|
|
17
38
|
## Quick start
|
|
18
39
|
|
|
19
40
|
```sh
|
|
@@ -27,7 +48,7 @@ class CreateProducts < ActiveRecord::Migration[8.1]
|
|
|
27
48
|
def change
|
|
28
49
|
create_table :products do |t|
|
|
29
50
|
t.string :name
|
|
30
|
-
t.money_attribute :price
|
|
51
|
+
t.money_attribute :price # price: decimal(20,4), price_currency: string
|
|
31
52
|
t.timestamps
|
|
32
53
|
end
|
|
33
54
|
end
|
|
@@ -37,16 +58,15 @@ end
|
|
|
37
58
|
```ruby
|
|
38
59
|
# app/models/product.rb
|
|
39
60
|
class Product < ApplicationRecord
|
|
40
|
-
money_attribute :price
|
|
61
|
+
money_attribute :price
|
|
41
62
|
end
|
|
42
63
|
```
|
|
43
64
|
|
|
44
|
-
That's it. `Product.new(price: 12).price` is a `Mint::Money`.
|
|
65
|
+
That's it. `Product.new(price: 12.dollars).price` is a `Mint::Money`.
|
|
45
66
|
|
|
46
67
|
## Why MoneyAttribute?
|
|
47
68
|
|
|
48
69
|
- **No serialization boilerplate** — declare once, read/write `Mint::Money` everywhere.
|
|
49
|
-
- **Two storage modes** — single column for fixed-currency apps (simpler), amount+currency columns for multi-currency records (more flexible).
|
|
50
70
|
- **Integer or decimal columns** — auto-detects the column type and adjusts serialization (e.g. integer stores cents, decimal stores unit value).
|
|
51
71
|
- **Normalizes everything** — pass a number, string, or `Mint::Money`; always get a `Mint::Money` back.
|
|
52
72
|
- **Currency enforcement** — fixed-currency attributes reject wrong currencies at assignment time.
|
|
@@ -56,16 +76,16 @@ That's it. `Product.new(price: 12).price` is a `Mint::Money`.
|
|
|
56
76
|
|
|
57
77
|
| Feature | MoneyAttribute | money-rails |
|
|
58
78
|
|---|---|---|
|
|
59
|
-
| **
|
|
79
|
+
| **Declare** | `t.money_attribute :price` / `money_attribute :price` or `t.money_amount :price` / `money_amount :price` | `monetize :price_cents` |
|
|
60
80
|
| **Column types** | `integer`, `decimal`, `bigint` — auto-detected | `integer` cents only |
|
|
61
|
-
| **Storage modes** |
|
|
81
|
+
| **Storage modes** | Composite (amount+currency), single column | Single cents column, composite (cents+currency) |
|
|
62
82
|
| **Decimal columns** | Native — `t.decimal :price` | Not supported — must convert to cents manually |
|
|
63
83
|
| **Multi-currency** | `money_attribute :price` (convention: `<name>_amount` + `<name>_currency`) | `monetize :price_cents, with_currency: :price_currency` |
|
|
64
84
|
| **Rails integration** | `ActiveRecord::Type` + `composed_of` — no monkey-patches | `monetize` overrides reader/writer methods |
|
|
65
85
|
| **Query (fixed)** | `Model.where(price: money)` — `=`, `IN`, `BETWEEN`, `ORDER`, `SUM` | Through cents column (`price_cents`) |
|
|
66
86
|
| **Query (multi)** | `Model.where(price: money)` | `Model.where(price_cents:, price_currency:)` |
|
|
67
|
-
| **Internal amount** | `Rational`
|
|
68
|
-
| **Performance** | See [BENCHMARKS.md](BENCHMARKS.md) — wins 9/11 cells |
|
|
87
|
+
| **Internal amount** | `Rational` | `BigDecimal` |
|
|
88
|
+
| **Performance** | See [BENCHMARKS.md](BENCHMARKS.md) — wins 9/11 cells | — |
|
|
69
89
|
|
|
70
90
|
For a detailed side-by-side comparison, see [COMPARISON.md](COMPARISON.md).
|
|
71
91
|
|
|
@@ -73,7 +93,7 @@ For a detailed side-by-side comparison, see [COMPARISON.md](COMPARISON.md).
|
|
|
73
93
|
|
|
74
94
|
- Ruby 3.3+
|
|
75
95
|
- Rails 7.1.3.2+
|
|
76
|
-
- [Minting](https://github.com/gferraz/minting)
|
|
96
|
+
- [Minting](https://github.com/gferraz/minting) 2.0+
|
|
77
97
|
|
|
78
98
|
## Installation
|
|
79
99
|
|
|
@@ -91,19 +111,32 @@ The generator creates `config/initializers/money_attribute.rb`.
|
|
|
91
111
|
|
|
92
112
|
## Migration helpers
|
|
93
113
|
|
|
94
|
-
|
|
114
|
+
### `money_attribute` (composite — amount + currency)
|
|
115
|
+
|
|
116
|
+
Primary migration helper for multi-currency attributes. Creates two columns — amount and currency.
|
|
117
|
+
|
|
118
|
+
| Method | Action |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `add_money_attribute` / `t.money_attribute` | Amount column + currency column |
|
|
121
|
+
| `remove_money_attribute` / `t.remove_money_attribute` | Drops both columns |
|
|
95
122
|
|
|
96
|
-
|
|
123
|
+
Default columns: `decimal(20,4)` for amount + `string(16)` for currency.
|
|
124
|
+
|
|
125
|
+
### Column types
|
|
126
|
+
|
|
127
|
+
| Amount type | Column type | Precision/Scale | Maximum | Integer digits |
|
|
128
|
+
|---|---|---|---|---|
|
|
129
|
+
| `:crypto_decimal` | `decimal` | `36/18` | ~1 quintillion | 18 |
|
|
130
|
+
| `:fiat_decimal` | `decimal` | `20/4` | ~10 quadrillion | 16 |
|
|
131
|
+
| `:fiat_integer` | `bigint` | — | ~922 trillion | ~15 |
|
|
97
132
|
|
|
98
133
|
```ruby
|
|
99
134
|
class CreateProducts < ActiveRecord::Migration[8.1]
|
|
100
135
|
def change
|
|
101
136
|
create_table :products do |t|
|
|
102
137
|
t.string :name
|
|
103
|
-
t.money_attribute :
|
|
104
|
-
t.money_attribute :
|
|
105
|
-
t.money_attribute :fee, currency: false # single column, no currency
|
|
106
|
-
t.money_attribute :tax, amount: { type: :bigint } # bigint amount + currency
|
|
138
|
+
t.money_attribute :multi # decimal(20,4) + currency
|
|
139
|
+
t.money_attribute :tax, amount: { type: :fiat_integer } # bigint + currency
|
|
107
140
|
t.timestamps
|
|
108
141
|
end
|
|
109
142
|
end
|
|
@@ -111,24 +144,23 @@ end
|
|
|
111
144
|
|
|
112
145
|
class AddPriceToProducts < ActiveRecord::Migration[8.1]
|
|
113
146
|
def change
|
|
114
|
-
add_money_attribute :products, :price
|
|
115
|
-
|
|
116
|
-
remove_money_attribute :products, :obsolete_fee # reversible in change
|
|
147
|
+
add_money_attribute :products, :price # price + price_currency
|
|
148
|
+
remove_money_attribute :products, :obsolete_fee # reversible in change
|
|
117
149
|
end
|
|
118
150
|
end
|
|
119
151
|
```
|
|
120
152
|
|
|
121
153
|
### Naming
|
|
122
154
|
|
|
155
|
+
**`money_attribute` (composite):**
|
|
156
|
+
|
|
123
157
|
| Migration call | Columns created | Model declaration |
|
|
124
158
|
|---|---|---|
|
|
125
|
-
| `t.money_attribute :price` | `price` decimal + `price_currency` string | `money_attribute :price` |
|
|
126
|
-
| `t.money_attribute :price_amount` | `price_amount` decimal + `price_currency` string | `money_attribute :price` |
|
|
127
|
-
| `t.money_attribute :price,
|
|
128
|
-
| `t.money_attribute :price, amount: { type: :integer }` | `price` integer + `price_currency` string | `money_attribute :price` |
|
|
159
|
+
| `t.money_attribute :price` | `price` decimal(20,4) + `price_currency` string(16) | `money_attribute :price` |
|
|
160
|
+
| `t.money_attribute :price_amount` | `price_amount` decimal(20,4) + `price_currency` string(16) | `money_attribute :price` |
|
|
161
|
+
| `t.money_attribute :price, amount: { type: :fiat_integer }` | `price` bigint + `price_currency` string(16) | `money_attribute :price` |
|
|
129
162
|
| `t.money_attribute :price, amount: { column: :a }, currency: { column: :c }` | `a` + `c` | `money_attribute :price, mapping: { amount: :a, currency: :c }` |
|
|
130
|
-
| `t.money_attribute :price, currency: { limit:
|
|
131
|
-
| `t.money_attribute :price, amount: { precision: 14, scale: 2, null: false }, currency: { limit: 3, default: 'USD' }` | `price` decimal(14,2) NOT NULL + `price_currency` string(3) DEFAULT 'USD' | `money_attribute :price` |
|
|
163
|
+
| `t.money_attribute :price, currency: { limit: 5 }` | `price` decimal(20,4) + `price_currency` string(5) | `money_attribute :price` |
|
|
132
164
|
| `t.remove_money_attribute :price` | Removes `price` + `price_currency` | `money_attribute :price` |
|
|
133
165
|
|
|
134
166
|
Inside `change_table`:
|
|
@@ -195,33 +227,7 @@ If none of those keys are set, `format` is used as a plain string (simple format
|
|
|
195
227
|
|
|
196
228
|
> 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.
|
|
197
229
|
|
|
198
|
-
## Usage
|
|
199
|
-
|
|
200
|
-
### Decision table
|
|
201
|
-
|
|
202
|
-
| | Fixed currency (single column) | Multi-currency (amount + currency) |
|
|
203
|
-
|---|---|---|
|
|
204
|
-
| **Migration** | `t.money_attribute :price` (or `t.decimal :price`) | `t.money_attribute :price` (or `t.decimal :price_amount` + `t.string :price_currency`) |
|
|
205
|
-
| **Model** | `money_attribute :price, currency: 'USD'` | `money_attribute :price` |
|
|
206
|
-
| **When to use** | Column always holds the same currency | Each row can hold a different currency |
|
|
207
|
-
| **Column type** | `decimal`, `integer`, or `bigint` | `decimal`, `integer`, or `bigint` for amount; `string` for currency |
|
|
208
|
-
| **Query** | `Product.where(price: 10.to_money('USD'))` — full type support | `Offer.where(price: 10.to_money('EUR'))` — equality only |
|
|
209
|
-
|
|
210
|
-
### Fixed currency
|
|
211
|
-
|
|
212
|
-
```ruby
|
|
213
|
-
class Product < ApplicationRecord
|
|
214
|
-
money_attribute :price, currency: 'USD'
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
product = Product.new(price: 12)
|
|
218
|
-
product.price # => [USD 12.00]
|
|
219
|
-
|
|
220
|
-
Product.new(price: 12.to_money('EUR'))
|
|
221
|
-
# => ArgumentError: ... has different currency. Only USD allowed.
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### Multi-currency
|
|
230
|
+
## Usage
|
|
225
231
|
|
|
226
232
|
```ruby
|
|
227
233
|
class Offer < ApplicationRecord
|
|
@@ -239,15 +245,29 @@ offer.price.currency.code # => "USD"
|
|
|
239
245
|
|
|
240
246
|
Unlike fixed-currency attributes, composite mode does not enforce a specific currency — any registered currency is accepted at assignment.
|
|
241
247
|
|
|
248
|
+
### Invalid currencies in the database
|
|
249
|
+
|
|
250
|
+
If the currency column contains a value that is not a registered currency (e.g. a legacy code that was removed, or data corruption), `money_attribute` does not crash. The currency resolves to **XXX** (ISO 4217 "No Currency") and the monetary amount is preserved:
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
offer = Offer.find(42)
|
|
254
|
+
offer.price # => [XXX 10.00] # amount preserved, currency flagged
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Records with XXX currency are easily queryable for cleanup:
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
Offer.where(price_currency: 'XXX')
|
|
261
|
+
```
|
|
262
|
+
|
|
242
263
|
## Column type detection
|
|
243
264
|
|
|
244
|
-
|
|
265
|
+
Select the amount column type via the `type:` option. The gem adapts serialization accordingly:
|
|
245
266
|
|
|
246
267
|
```ruby
|
|
247
268
|
# Migration
|
|
248
269
|
create_table :orders do |t|
|
|
249
|
-
t.
|
|
250
|
-
t.string :total_currency
|
|
270
|
+
t.money_attribute :total, amount: { type: :fiat_integer } # bigint — stored as subunits
|
|
251
271
|
end
|
|
252
272
|
|
|
253
273
|
# Model
|
|
@@ -258,17 +278,7 @@ end
|
|
|
258
278
|
Order.new(total: 19.99.to_money('USD')).total_amount # => 1999
|
|
259
279
|
```
|
|
260
280
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
```ruby
|
|
264
|
-
# Migration
|
|
265
|
-
t.bigint :price
|
|
266
|
-
|
|
267
|
-
# Model (no change needed)
|
|
268
|
-
money_attribute :price, currency: 'USD'
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
> Use `integer`/`bigint` for large tables (faster, smaller). Use `decimal` when SQL-level readability matters.
|
|
281
|
+
> Use `:fiat_integer` (bigint) for large tables — smaller and sufficient for most fiat use cases (~922 trillion max). Use `:fiat_decimal` (decimal) when SQL-level readability matters. For cryto currencies support, `:crypto_decimal` is mandatory.
|
|
272
282
|
|
|
273
283
|
## Custom column names
|
|
274
284
|
|
|
@@ -294,17 +304,16 @@ end
|
|
|
294
304
|
|
|
295
305
|
## Column resolution
|
|
296
306
|
|
|
297
|
-
|
|
307
|
+
`money_attribute :name` is always composite. It resolves columns in this order:
|
|
298
308
|
|
|
299
|
-
| Step | Condition | Columns used |
|
|
300
|
-
|
|
301
|
-
| 1 | `mapping:` provided | As specified
|
|
302
|
-
| 2 | `name_currency` column exists | `name` + `name_currency` |
|
|
303
|
-
| 3 | `name == 'amount'` AND `currency` column exists | `amount` + `currency` |
|
|
304
|
-
| 4 |
|
|
305
|
-
| 5 | None of the above (name column missing) | `name_amount` + `name_currency` (convention) | Composite (multi-currency) |
|
|
309
|
+
| Step | Condition | Columns used |
|
|
310
|
+
|---|---|---|
|
|
311
|
+
| 1 | `mapping:` provided | As specified (missing keys fall back to `<name>_amount` / `<name>_currency`) |
|
|
312
|
+
| 2 | `name_currency` column exists AND `name` column exists | `name` + `name_currency` |
|
|
313
|
+
| 3 | `name == 'amount'` AND `currency` column exists | `amount` + `currency` |
|
|
314
|
+
| 4 | None of the above | `<name>_amount` + `<name>_currency` (convention) |
|
|
306
315
|
|
|
307
|
-
Step
|
|
316
|
+
Step 4 raises `ArgumentError` if the convention columns don't exist. For single-column fixed-currency attributes, see [`money_amount`](#single-column-mode--money_amount).
|
|
308
317
|
|
|
309
318
|
**Example**
|
|
310
319
|
|
|
@@ -316,7 +325,6 @@ create_table :financial_transactions do |t|
|
|
|
316
325
|
t.string :discount_currency, limit: 3
|
|
317
326
|
t.decimal :price_amount
|
|
318
327
|
t.string :price_currency, limit: 3
|
|
319
|
-
t.bigint :surplus
|
|
320
328
|
t.bigint :tax
|
|
321
329
|
t.decimal :total_amount
|
|
322
330
|
t.string :currency_code, limit: 3
|
|
@@ -327,47 +335,29 @@ end
|
|
|
327
335
|
class FinancialTransaction < ApplicationRecord
|
|
328
336
|
money_attribute :amount # step 3: amount(int) + currency
|
|
329
337
|
money_attribute :discount # step 2: discount(int) + discount_currency
|
|
330
|
-
money_attribute :price # step 4: price_amount
|
|
331
|
-
money_attribute :surplus, currency: 'EUR' # step 5: surplus(int) (single-column, will use EUR)
|
|
332
|
-
money_attribute :tax # step 5: tax(int) (single-column, will use default currency)
|
|
338
|
+
money_attribute :price # step 4: price_amount + price_currency
|
|
333
339
|
money_attribute :total, mapping: { amount: :total_amount, currency: :currency_code } # step 1: explicit
|
|
340
|
+
money_amount :tax # single-column, fixed-currency (uses default currency)
|
|
334
341
|
end
|
|
335
342
|
```
|
|
336
343
|
|
|
337
344
|
## Querying
|
|
338
345
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
```ruby
|
|
342
|
-
# Equality
|
|
343
|
-
Product.where(price: 10.to_money('USD'))
|
|
344
|
-
|
|
345
|
-
# IN clause
|
|
346
|
-
Product.where(price: [10.to_money('USD'), 20.to_money('USD')])
|
|
347
|
-
|
|
348
|
-
# BETWEEN
|
|
349
|
-
Product.where(price: 10.to_money('USD')..20.to_money('USD'))
|
|
350
|
-
|
|
351
|
-
# Ordering
|
|
352
|
-
Product.order(price: :desc)
|
|
353
|
-
|
|
354
|
-
# Aggregation
|
|
355
|
-
Product.where(price: 10.to_money('USD')).sum(:price)
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
Multi-currency attributes support equality queries via `composed_of`:
|
|
346
|
+
Multi-currency (`money_attribute`) attributes support equality queries via `composed_of`:
|
|
359
347
|
|
|
360
348
|
```ruby
|
|
361
349
|
Offer.where(price: 10.to_money('EUR'))
|
|
362
350
|
```
|
|
363
351
|
|
|
364
|
-
For comparisons
|
|
352
|
+
For comparisons, use the backing columns directly:
|
|
365
353
|
|
|
366
354
|
```ruby
|
|
367
355
|
Offer.where(price_amount: 10..20, price_currency: 'EUR')
|
|
368
356
|
Offer.where('price_amount > ? AND price_currency = ?', 10, 'EUR')
|
|
369
357
|
```
|
|
370
358
|
|
|
359
|
+
For fixed-currency (`money_amount`) attributes, see the [single-column section](#single-column-mode--money_amount).
|
|
360
|
+
|
|
371
361
|
## Convenience methods
|
|
372
362
|
|
|
373
363
|
MoneyAttribute adds small helpers on `Numeric` and `String`:
|
|
@@ -394,10 +384,73 @@ MoneyAttribute adds `money_field` and `money_amount_field` to Rails form builder
|
|
|
394
384
|
<% end %>
|
|
395
385
|
```
|
|
396
386
|
|
|
387
|
+
### Single-column mode — `money_amount` (fixed-currency)
|
|
388
|
+
|
|
389
|
+
`money_amount` wraps a numeric column as `Mint::Money` using the application's default currency. No per-row currency. A lighter alternative when you don't need multi-currency support.
|
|
390
|
+
|
|
391
|
+
#### Migration helpers
|
|
392
|
+
|
|
393
|
+
| Method | Action |
|
|
394
|
+
|---|---|
|
|
395
|
+
| `add_money_amount` / `t.money_amount` | Amount column only |
|
|
396
|
+
| `remove_money_amount` / `t.remove_money_amount` | Drops the column |
|
|
397
|
+
|
|
398
|
+
Default column: `decimal(20,4)`. The top-level `type:` shortcut selects the column type:
|
|
399
|
+
|
|
400
|
+
```ruby
|
|
401
|
+
t.money_amount :price # decimal(20,4)
|
|
402
|
+
t.money_amount :btc_balance, type: :crypto_decimal # decimal(36,18)
|
|
403
|
+
t.money_amount :qty, type: :fiat_integer # bigint
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
#### Naming
|
|
407
|
+
|
|
408
|
+
| Migration call | Columns created | Model declaration |
|
|
409
|
+
|---|---|---|
|
|
410
|
+
| `t.money_amount :price` | `price` decimal(20,4) | `money_amount :price` |
|
|
411
|
+
| `t.money_amount :btc, type: :crypto_decimal` | `btc` decimal(36,18) | `money_amount :btc` |
|
|
412
|
+
| `t.money_amount :price, type: :fiat_integer` | `price` bigint | `money_amount :price` |
|
|
413
|
+
| `t.money_amount :price, type: :fiat_decimal` | `price` decimal(20,4) | `money_amount :price` |
|
|
414
|
+
|
|
415
|
+
#### Usage
|
|
416
|
+
|
|
417
|
+
```ruby
|
|
418
|
+
class Product < ApplicationRecord
|
|
419
|
+
money_amount :price
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
product = Product.new(price: 12)
|
|
423
|
+
product.price # => [USD 12.00]
|
|
424
|
+
|
|
425
|
+
Product.new(price: 12.to_money('EUR'))
|
|
426
|
+
# => ArgumentError: ... has different currency. Only USD allowed.
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
#### Column type shortcut
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
# Migration
|
|
433
|
+
t.money_amount :price, type: :fiat_integer # bigint column
|
|
434
|
+
|
|
435
|
+
# Model
|
|
436
|
+
money_amount :price
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
#### Querying
|
|
440
|
+
|
|
441
|
+
Fixed-currency attributes support Rails-native querying through the custom type:
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
Product.where(price: 10.to_money('USD')) # equality
|
|
445
|
+
Product.where(price: [10.to_money('USD'), 20.to_money('USD')]) # IN
|
|
446
|
+
Product.where(price: 10.to_money('USD')..20.to_money('USD')) # BETWEEN
|
|
447
|
+
Product.order(price: :desc) # ordering
|
|
448
|
+
Product.where(price: 10.to_money('USD')).sum(:price) # aggregation
|
|
449
|
+
```
|
|
450
|
+
|
|
397
451
|
## Roadmap
|
|
398
452
|
|
|
399
453
|
1. **Method-level currency** — lambda-based currency resolution for multi-tenant and instance-level scenarios
|
|
400
|
-
2. Prepare to official 1.0 launh
|
|
401
454
|
|
|
402
455
|
Contributions and suggestions are welcome — open an issue or PR at [gferraz/money-attribute](https://github.com/gferraz/money-attribute).
|
|
403
456
|
|
data/Rakefile
CHANGED
|
@@ -16,15 +16,31 @@ end
|
|
|
16
16
|
|
|
17
17
|
desc 'Migrate test database'
|
|
18
18
|
task :test_db_migrate do
|
|
19
|
-
|
|
20
|
-
sh({ 'RAILS_ENV' => 'test' }, 'bin/rails', 'db:migrate')
|
|
21
|
-
end
|
|
19
|
+
sh({ 'RAILS_ENV' => 'test' }, 'bin/rails', 'db:migrate', chdir: 'test/dummy')
|
|
22
20
|
end
|
|
23
21
|
|
|
24
22
|
desc 'Run tests (migrates test DB first)'
|
|
25
23
|
task test: %i[test_db_migrate test_run]
|
|
26
24
|
|
|
27
25
|
desc 'Run money_attribute vs money-rails benchmark'
|
|
28
|
-
task :
|
|
29
|
-
|
|
26
|
+
task bench: :test_db_migrate do
|
|
27
|
+
puts
|
|
28
|
+
puts '=' * 80
|
|
29
|
+
puts 'money_attribute (minting gem)'
|
|
30
|
+
puts '=' * 80
|
|
31
|
+
sh({ 'RAILS_ENV' => 'test', 'BENCH_SIDE' => 'minting' },
|
|
32
|
+
'bundle', 'exec', 'ruby', 'benchmark/comparison.rb')
|
|
33
|
+
|
|
34
|
+
puts
|
|
35
|
+
puts '=' * 80
|
|
36
|
+
puts 'money-rails (money gem)'
|
|
37
|
+
puts '=' * 80
|
|
38
|
+
sh({ 'RAILS_ENV' => 'test', 'BENCH_SIDE' => 'money_rails',
|
|
39
|
+
'BUNDLE_GEMFILE' => 'Gemfile.benchmark' },
|
|
40
|
+
'bundle', 'exec', 'ruby', 'benchmark/comparison.rb')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
desc 'Generate consolidated benchmark report (markdown)'
|
|
44
|
+
task 'bench:report' do
|
|
45
|
+
ruby 'benchmark/report.rb'
|
|
30
46
|
end
|
|
@@ -10,19 +10,18 @@ module MoneyAttribute
|
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
end
|
|
13
|
+
cfg = Configuration.new
|
|
14
|
+
cached_default = nil
|
|
15
|
+
|
|
16
|
+
define_singleton_method(:config) { cfg }
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
define_singleton_method(:configure) do |&block|
|
|
19
|
+
block&.call(cfg)
|
|
20
|
+
cached_default = nil
|
|
21
|
+
cfg
|
|
23
22
|
end
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
define_singleton_method(:default_currency) do
|
|
25
|
+
cached_default ||= ::Mint::Currency.resolve!(cfg.default_currency)
|
|
27
26
|
end
|
|
28
27
|
end
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MoneyAttribute
|
|
4
|
+
# :nodoc:
|
|
4
5
|
class Converter
|
|
5
6
|
def initialize(currency = MoneyAttribute.default_currency)
|
|
6
7
|
@default_currency = currency
|
|
7
8
|
end
|
|
8
9
|
|
|
9
|
-
def parse(amount
|
|
10
|
+
def parse(amount)
|
|
10
11
|
case amount
|
|
11
|
-
when
|
|
12
|
-
when Numeric
|
|
13
|
-
when String
|
|
12
|
+
when Money, NilClass then amount
|
|
13
|
+
when Numeric then Money.from(amount, @default_currency)
|
|
14
|
+
when String then Money.parse(amount, @default_currency)
|
|
14
15
|
else raise ArgumentError, "Cannot convert #{amount.inspect} (#{amount.class}) to Money"
|
|
15
16
|
end
|
|
16
17
|
end
|
|
18
|
+
|
|
17
19
|
alias call parse
|
|
18
20
|
end
|
|
19
21
|
end
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# :nodoc
|
|
3
|
+
# :nodoc:
|
|
4
4
|
class Numeric
|
|
5
|
-
remove_method :to_money
|
|
5
|
+
remove_method :to_money if method_defined?(:to_money)
|
|
6
6
|
|
|
7
7
|
def to_money(currency = MoneyAttribute.default_currency) = Mint.money(self, currency)
|
|
8
8
|
end
|
|
9
|
-
|
|
10
|
-
# :nodoc
|
|
11
|
-
class String
|
|
12
|
-
remove_method :to_money
|
|
13
|
-
|
|
14
|
-
def to_money(currency = MoneyAttribute.default_currency) = Mint.parse(self, currency)
|
|
15
|
-
end
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MoneyAttribute
|
|
4
|
+
# :nodoc:
|
|
4
5
|
module FormBuilderExtension
|
|
5
6
|
def money_field(method, options = {})
|
|
6
7
|
money = object.public_send(method)
|
|
7
|
-
value = money&.to_fs
|
|
8
|
+
value = money&.to_fs
|
|
8
9
|
|
|
9
10
|
@template.text_field_tag(field_name(method), value,
|
|
10
11
|
{ id: field_id(method) }.merge(options))
|
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MoneyAttribute
|
|
4
|
+
# :nodoc:
|
|
4
5
|
module Macro
|
|
5
6
|
extend ActiveSupport::Concern
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
# :nodoc:
|
|
9
|
+
module CompositeClassMethods
|
|
10
|
+
def resolve_composite_mapping(name)
|
|
9
11
|
columns = attribute_names
|
|
10
|
-
currency = ::Mint::Currency.resolve!(currency)
|
|
11
|
-
name = name.to_s
|
|
12
|
-
resolved_mapping = mapping || resolve_mapping(name, columns)
|
|
13
|
-
|
|
14
|
-
if columns.include?(name) && resolved_mapping.nil?
|
|
15
|
-
define_single_column_money_attribute(name, currency)
|
|
16
|
-
else
|
|
17
|
-
define_composite_money_attribute(name, resolved_mapping, currency)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
# --- Preparation (no side effects) ---
|
|
24
|
-
|
|
25
|
-
def resolve_mapping(name, columns)
|
|
26
|
-
return nil unless columns.include?(name)
|
|
27
|
-
|
|
28
12
|
if columns.include?("#{name}_currency")
|
|
29
|
-
{ amount: name, currency: :"#{name}_currency" }
|
|
30
|
-
|
|
13
|
+
return { amount: name, currency: :"#{name}_currency" } if columns.include?(name)
|
|
14
|
+
|
|
15
|
+
nil
|
|
16
|
+
elsif name == 'amount' && columns.include?('currency')
|
|
31
17
|
{ amount: name, currency: :currency }
|
|
32
18
|
end
|
|
33
19
|
end
|
|
@@ -35,8 +21,8 @@ module MoneyAttribute
|
|
|
35
21
|
def resolve_composite_for(name, mapping:)
|
|
36
22
|
composite = { amount: "#{name}_amount", currency: "#{name}_currency" }
|
|
37
23
|
|
|
38
|
-
composite[:amount]
|
|
39
|
-
composite[:currency]
|
|
24
|
+
composite[:amount] = mapping[:amount].to_s if mapping&.key?(:amount)
|
|
25
|
+
composite[:currency] = mapping[:currency].to_s if mapping&.key?(:currency)
|
|
40
26
|
|
|
41
27
|
assert_columns_exist!(name, composite)
|
|
42
28
|
composite
|
|
@@ -57,17 +43,18 @@ module MoneyAttribute
|
|
|
57
43
|
def money_constructor_for(amount_column)
|
|
58
44
|
default = MoneyAttribute.default_currency
|
|
59
45
|
if integer_column?(amount_column)
|
|
60
|
-
|
|
61
|
-
return nil if amount.nil?
|
|
62
|
-
|
|
63
|
-
Mint::Money.from_subunits(amount, currency.presence || default)
|
|
64
|
-
}
|
|
46
|
+
build_money_constructor(:from_subunits, default)
|
|
65
47
|
else
|
|
66
|
-
|
|
67
|
-
|
|
48
|
+
build_money_constructor(:from, default)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
68
51
|
|
|
69
|
-
|
|
70
|
-
|
|
52
|
+
def build_money_constructor(method, default)
|
|
53
|
+
lambda do |amount, currency|
|
|
54
|
+
next nil if amount.nil?
|
|
55
|
+
|
|
56
|
+
resolved = Mint::Currency.resolve(currency.presence || default) || 'XXX'
|
|
57
|
+
Mint::Money.public_send(method, amount, resolved)
|
|
71
58
|
end
|
|
72
59
|
end
|
|
73
60
|
|
|
@@ -76,14 +63,6 @@ module MoneyAttribute
|
|
|
76
63
|
%i[integer bigint].include?(col&.type)
|
|
77
64
|
end
|
|
78
65
|
|
|
79
|
-
# --- Configuration (registers types, normalizers, composed_of) ---
|
|
80
|
-
|
|
81
|
-
def define_single_column_money_attribute(name, currency)
|
|
82
|
-
column_type = integer_column?(name) ? ActiveRecord::Type::Integer.new : ActiveRecord::Type::Decimal.new
|
|
83
|
-
attribute(name.to_sym, :mint_money, currency:, column_type: column_type)
|
|
84
|
-
normalizes(name.to_sym, with: Converter.new(currency))
|
|
85
|
-
end
|
|
86
|
-
|
|
87
66
|
def define_composite_money_attribute(name, mapping, currency)
|
|
88
67
|
aggregated = resolve_composite_for(name, mapping:)
|
|
89
68
|
|
|
@@ -99,5 +78,26 @@ module MoneyAttribute
|
|
|
99
78
|
})
|
|
100
79
|
end
|
|
101
80
|
end
|
|
81
|
+
|
|
82
|
+
class_methods do
|
|
83
|
+
def money_attribute(name, currency: MoneyAttribute.default_currency, mapping: nil)
|
|
84
|
+
name = name.to_s
|
|
85
|
+
currency = ::Mint::Currency.resolve!(currency)
|
|
86
|
+
resolved_mapping = mapping || resolve_composite_mapping(name)
|
|
87
|
+
|
|
88
|
+
if resolved_mapping.nil? && attribute_names.include?(name)
|
|
89
|
+
raise ArgumentError,
|
|
90
|
+
"Column '#{name}' exists but no '#{name}_currency' column was found. " \
|
|
91
|
+
'For single-column fixed-currency attributes, use `money_amount` ' \
|
|
92
|
+
'instead of `money_attribute`.'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
define_composite_money_attribute(name, resolved_mapping || {}, currency)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
included do
|
|
100
|
+
extend CompositeClassMethods
|
|
101
|
+
end
|
|
102
102
|
end
|
|
103
103
|
end
|
|
@@ -2,61 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
module MoneyAttribute
|
|
4
4
|
module MigrationExtensions
|
|
5
|
+
# :nodoc:
|
|
5
6
|
module Helper
|
|
6
|
-
|
|
7
|
+
AMOUNT_CONFIG = {
|
|
8
|
+
crypto_decimal: { type: :decimal, precision: 36, scale: 18 },
|
|
9
|
+
fiat_decimal: { type: :decimal, precision: 20, scale: 4 },
|
|
10
|
+
fiat_integer: { type: :bigint }
|
|
11
|
+
}.freeze
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
CURRENCY_MIN_LIMIT = 8
|
|
14
|
+
CURRENCY_DEFAULT_LIMIT = 20
|
|
15
|
+
|
|
16
|
+
private
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
precision: opts[:precision],
|
|
19
|
-
scale: opts[:scale]
|
|
20
|
-
}.compact
|
|
21
|
-
else
|
|
22
|
-
amount_col = name
|
|
23
|
-
amount_opts = {}
|
|
18
|
+
def parse_money_amount_args(accessor, options)
|
|
19
|
+
options ||= {}
|
|
20
|
+
if options.key?(:precision) || options.key?(:scale)
|
|
21
|
+
raise ArgumentError,
|
|
22
|
+
'precision:/scale: are not configurable — money_attribute uses fixed, ' \
|
|
23
|
+
'vetted values per type (:crypto_decimal, :fiat_decimal, :fiat_integer) ' \
|
|
24
|
+
'to prevent under-precision bugs, particularly for crypto amounts.'
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
column = (options[:column] || accessor).to_s
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
amount_opts.delete(:precision)
|
|
33
|
-
amount_opts.delete(:scale)
|
|
29
|
+
config = AMOUNT_CONFIG[options[:type] || :fiat_decimal]
|
|
30
|
+
unless config
|
|
31
|
+
raise ArgumentError,
|
|
32
|
+
"Invalid money amount type #{options[:type]}. Use :crypto_decimal, :fiat_decimal or :fiat_integer"
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
null: opts[:null],
|
|
49
|
-
default: opts[:default]
|
|
50
|
-
}.compact
|
|
51
|
-
elsif options[:currency] == false
|
|
52
|
-
currency_col = nil
|
|
53
|
-
currency_opts = {}
|
|
54
|
-
else
|
|
55
|
-
currency_col = default_currency_col
|
|
56
|
-
currency_opts = {}
|
|
35
|
+
options = { null: options[:null], default: options[:default] }.compact
|
|
36
|
+
[column, config.merge(options)]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse_currency_args(accessor, options)
|
|
40
|
+
options ||= {}
|
|
41
|
+
limit = (options[:limit] || CURRENCY_DEFAULT_LIMIT).to_i
|
|
42
|
+
if limit < CURRENCY_MIN_LIMIT
|
|
43
|
+
raise ArgumentError,
|
|
44
|
+
"currency limit: #{limit} is too small to hold an ISO 4217 code and crypto popular codes" \
|
|
45
|
+
"(minimum #{CURRENCY_MIN_LIMIT}). Omit limit: to use the default of #{CURRENCY_DEFAULT_LIMIT}, " \
|
|
46
|
+
"or pass a value >= #{CURRENCY_MIN_LIMIT}."
|
|
57
47
|
end
|
|
58
48
|
|
|
59
|
-
|
|
49
|
+
column = currency_column_name(accessor, options[:column])
|
|
50
|
+
[column, { limit:, null: options[:null], default: options[:default] }.compact]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def currency_column_name(accessor, column_override)
|
|
54
|
+
return column_override.to_s if column_override
|
|
55
|
+
|
|
56
|
+
name = accessor.to_s
|
|
57
|
+
return 'currency' if name == 'amount'
|
|
58
|
+
|
|
59
|
+
radical = name.end_with?('_amount') ? name.sub(/_amount$/, '') : name
|
|
60
|
+
"#{radical}_currency"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_money_args(accessor, options = {})
|
|
64
|
+
amount_column, amount_options = parse_money_amount_args(accessor, options[:amount])
|
|
65
|
+
currency_column, currency_options = parse_currency_args(accessor, options[:currency])
|
|
66
|
+
|
|
67
|
+
[amount_column, currency_column, amount_options, currency_options]
|
|
60
68
|
end
|
|
61
69
|
end
|
|
62
70
|
end
|
|
@@ -4,21 +4,34 @@ require_relative 'helper'
|
|
|
4
4
|
|
|
5
5
|
module MoneyAttribute
|
|
6
6
|
module MigrationExtensions
|
|
7
|
+
# :nodoc:
|
|
7
8
|
module SchemaStatements
|
|
8
9
|
include Helper
|
|
9
10
|
|
|
10
11
|
def add_money_attribute(table_name, accessor, options = {})
|
|
11
12
|
amount_col, currency_col, amount_opts, currency_opts = parse_money_args(accessor, options)
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
add_column(table_name,
|
|
14
|
+
type = amount_opts.delete(:type)
|
|
15
|
+
add_column(table_name, amount_col, type, **amount_opts)
|
|
16
|
+
add_column(table_name, currency_col, :string, **currency_opts)
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def remove_money_attribute(table_name, accessor, options = {})
|
|
18
20
|
amount_col, currency_col, = parse_money_args(accessor, options)
|
|
19
21
|
|
|
20
22
|
remove_column(table_name, amount_col)
|
|
21
|
-
remove_column(table_name, currency_col)
|
|
23
|
+
remove_column(table_name, currency_col)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def add_money_amount(table_name, accessor, options = {})
|
|
27
|
+
amount_col, amount_opts = parse_money_amount_args(accessor, options)
|
|
28
|
+
|
|
29
|
+
type = amount_opts.delete(:type)
|
|
30
|
+
add_column(table_name, amount_col, type, **amount_opts)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def remove_money_amount(table_name, accessor, options = {})
|
|
34
|
+
remove_column(table_name, (options[:column] || accessor).to_s)
|
|
22
35
|
end
|
|
23
36
|
end
|
|
24
37
|
end
|
|
@@ -4,6 +4,7 @@ require_relative 'helper'
|
|
|
4
4
|
|
|
5
5
|
module MoneyAttribute
|
|
6
6
|
module MigrationExtensions
|
|
7
|
+
# :nodoc:
|
|
7
8
|
module TableDefinition
|
|
8
9
|
include Helper
|
|
9
10
|
|
|
@@ -11,14 +12,26 @@ module MoneyAttribute
|
|
|
11
12
|
amount_col, currency_col, amount_opts, currency_opts = parse_money_args(accessor, options)
|
|
12
13
|
|
|
13
14
|
column(amount_col, amount_opts[:type], **amount_opts.except(:type))
|
|
14
|
-
column(currency_col, :string, **currency_opts)
|
|
15
|
+
column(currency_col, :string, **currency_opts)
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def remove_money_attribute(accessor, options = {})
|
|
18
19
|
amount_col, currency_col, = parse_money_args(accessor, options)
|
|
19
20
|
|
|
20
21
|
remove_column(amount_col)
|
|
21
|
-
remove_column(currency_col)
|
|
22
|
+
remove_column(currency_col)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def money_amount(accessor, options = {})
|
|
26
|
+
amount_col, amount_opts = parse_money_amount_args(accessor, options)
|
|
27
|
+
|
|
28
|
+
column(amount_col, amount_opts[:type], **amount_opts.except(:type))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def remove_money_amount(accessor, options = {})
|
|
32
|
+
amount_col, = parse_money_amount_args(accessor, options)
|
|
33
|
+
|
|
34
|
+
remove_column(amount_col)
|
|
22
35
|
end
|
|
23
36
|
end
|
|
24
37
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MoneyAttribute
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module MoneyAmount
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def money_amount(name)
|
|
10
|
+
name = name.to_s
|
|
11
|
+
|
|
12
|
+
assert_column_exists!(name)
|
|
13
|
+
|
|
14
|
+
currency = ::Mint::Currency.resolve!(MoneyAttribute.default_currency)
|
|
15
|
+
column_type = detect_column_type(name)
|
|
16
|
+
|
|
17
|
+
attribute(name.to_sym, MoneyAttribute::Type.new(currency:, column_type:))
|
|
18
|
+
normalizes(name.to_sym, with: Converter.new)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def assert_column_exists!(name)
|
|
24
|
+
return if attribute_names.include?(name)
|
|
25
|
+
|
|
26
|
+
raise ArgumentError,
|
|
27
|
+
"Column '#{name}' does not exist on this table. " \
|
|
28
|
+
"Add a column named '#{name}' or use a different accessor name."
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def detect_column_type(name)
|
|
32
|
+
col = columns.find { |c| c.name == name }
|
|
33
|
+
%i[integer bigint].include?(col&.type) ? ActiveRecord::Type::Integer.new : ActiveRecord::Type::Decimal.new
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -21,24 +21,34 @@ module MoneyAttribute
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def self.setup_locale_backend!
|
|
24
|
-
::Mint.locale_backend =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
::Mint.locale_backend = method(:build_locale_format).to_proc
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.build_locale_format
|
|
28
|
+
fmt = I18n.t('number.currency.format', default: {})
|
|
29
|
+
{ decimal: fmt[:separator], thousand: fmt[:delimiter], format: build_format(fmt) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.build_format(fmt)
|
|
33
|
+
if %i[positive negative zero].any? { |k| fmt.key?(k) }
|
|
34
|
+
build_hash_format(fmt)
|
|
35
|
+
else
|
|
36
|
+
translate_format(fmt[:format])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.build_hash_format(fmt)
|
|
41
|
+
{
|
|
42
|
+
positive: translate_format(fmt[:positive] || fmt[:format]),
|
|
43
|
+
negative: translate_format(fmt[:negative] || fmt[:format]),
|
|
44
|
+
zero: translate_format(fmt[:zero] || fmt[:format])
|
|
39
45
|
}
|
|
40
46
|
end
|
|
41
47
|
|
|
48
|
+
def self.translate_format(str)
|
|
49
|
+
str.to_s.gsub('%n', '%<amount>f').gsub('%u', '%<symbol>s')
|
|
50
|
+
end
|
|
51
|
+
|
|
42
52
|
def self.register_custom_currencies!
|
|
43
53
|
Array(MoneyAttribute.config.added_currencies).each do |currency_data|
|
|
44
54
|
if currency_data.respond_to?(:values_at)
|
data/lib/money_attribute/type.rb
CHANGED
|
@@ -10,9 +10,10 @@ module MoneyAttribute
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def cast(value)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
else
|
|
13
|
+
if value.is_a?(String)
|
|
14
|
+
Mint::Money.parse(value, @currency)
|
|
15
|
+
else
|
|
16
|
+
super
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
19
|
|
|
@@ -48,13 +49,10 @@ module MoneyAttribute
|
|
|
48
49
|
value.to_d
|
|
49
50
|
end
|
|
50
51
|
end
|
|
51
|
-
|
|
52
|
-
def self.type = :mint_money
|
|
53
52
|
end
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
ActiveSupport.on_load(:active_record) do
|
|
57
56
|
include MoneyAttribute::Macro
|
|
58
|
-
|
|
59
|
-
ActiveRecord::Type.register(:mint_money, MoneyAttribute::Type)
|
|
57
|
+
include MoneyAttribute::MoneyAmount
|
|
60
58
|
end
|
data/lib/money_attribute.rb
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require 'minting'
|
|
4
4
|
require 'money_attribute/core_ext'
|
|
5
|
+
require 'money_attribute/core_ext/string'
|
|
5
6
|
require 'money_attribute/configuration'
|
|
6
7
|
require 'money_attribute/macro'
|
|
8
|
+
require 'money_attribute/money_amount'
|
|
7
9
|
require 'money_attribute/converter'
|
|
8
10
|
require 'money_attribute/type'
|
|
9
11
|
require 'money_attribute/form_builder_extension'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: money_attribute
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gilson Ferraz
|
|
@@ -15,28 +15,28 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: '2.0'
|
|
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:
|
|
25
|
+
version: '2.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: rails
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 7.1
|
|
32
|
+
version: '7.1'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 7.1
|
|
39
|
+
version: '7.1'
|
|
40
40
|
description: Easily add money attributes to your Rails models
|
|
41
41
|
email:
|
|
42
42
|
- gilson@cesar.etc.br
|
|
@@ -53,11 +53,13 @@ files:
|
|
|
53
53
|
- lib/money_attribute/configuration.rb
|
|
54
54
|
- lib/money_attribute/converter.rb
|
|
55
55
|
- lib/money_attribute/core_ext.rb
|
|
56
|
+
- lib/money_attribute/core_ext/string.rb
|
|
56
57
|
- lib/money_attribute/form_builder_extension.rb
|
|
57
58
|
- lib/money_attribute/macro.rb
|
|
58
59
|
- lib/money_attribute/migration_extensions/helper.rb
|
|
59
60
|
- lib/money_attribute/migration_extensions/schema_statements.rb
|
|
60
61
|
- lib/money_attribute/migration_extensions/table_definition.rb
|
|
62
|
+
- lib/money_attribute/money_amount.rb
|
|
61
63
|
- lib/money_attribute/railtie.rb
|
|
62
64
|
- lib/money_attribute/type.rb
|
|
63
65
|
- lib/money_attribute/version.rb
|