whittaker_tech-midas 0.1.1 → 0.3.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 +75 -6
- data/app/controllers/whittaker_tech/midas/application_controller.rb +1 -5
- data/app/helpers/whittaker_tech/midas/application_helper.rb +1 -5
- data/app/helpers/whittaker_tech/midas/form_helper.rb +68 -72
- data/app/jobs/whittaker_tech/midas/application_job.rb +1 -5
- data/app/mailers/whittaker_tech/midas/application_mailer.rb +3 -7
- data/app/models/concerns/whittaker_tech/midas/bankable.rb +188 -172
- data/app/models/whittaker_tech/midas/application_record.rb +2 -6
- data/app/models/whittaker_tech/midas/coin/allocation.rb +124 -0
- data/app/models/whittaker_tech/midas/coin/arithmetic.rb +196 -0
- data/app/models/whittaker_tech/midas/coin/bidi.rb +87 -0
- data/app/models/whittaker_tech/midas/coin/converter/bank_provider.rb +35 -0
- data/app/models/whittaker_tech/midas/coin/converter.rb +79 -0
- data/app/models/whittaker_tech/midas/coin/parser.rb +104 -0
- data/app/models/whittaker_tech/midas/coin/presenter.rb +229 -0
- data/app/models/whittaker_tech/midas/coin.rb +285 -76
- data/app/models/whittaker_tech/midas/exchange.rb +37 -0
- data/db/migrate/20260101000000_create_whittaker_tech_schema.rb +26 -0
- data/db/migrate/{create_wt_midas_coins.rb → 20260101000001_create_midas_coins.rb} +7 -3
- data/db/migrate/20260219120000_rename_resource_label_to_resource_role_in_wt_midas_coins.rb +12 -0
- data/db/migrate/20260219150000_rename_wt_midas_coins_to_midas_coins.rb +13 -0
- data/db/migrate/20260707000000_create_midas_exchanges.rb +15 -0
- data/lib/generators/whittaker_tech/midas/install/install_generator.rb +5 -11
- data/lib/whittaker_tech/midas/deprecation.rb +32 -0
- data/lib/whittaker_tech/midas/engine.rb +113 -115
- data/lib/whittaker_tech/midas/version.rb +4 -4
- data/lib/whittaker_tech/midas.rb +120 -3
- metadata +72 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a3fd5f9cd2e7d3ffac65eaedd6353fca8f73ad4e786207f8402791b485454ff0
|
|
4
|
+
data.tar.gz: 30445ce5ed3a5ebf775f56c8a05afaf9eb401bac0f0a84a624cc9d4ddafbe9b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82e65cff44ba88558f2ee2563f107c88399ad41d65e008ae23b8d89846982c292be06c5dfbe3518810b11a1986359156b29119831614ac93d87d2ba5d28c99ef
|
|
7
|
+
data.tar.gz: b14740dd0c7ec5fc0d195fabf5bb81da564e456693a4cef8bca03f042a5964a03a7754776fa24cf9c2dca0b6bfb551ec075ef4da841cdfe674e1c50c9cb4b1c5
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# WhittakerTech::Midas
|
|
2
2
|
|
|
3
3
|
[](MIT-LICENSE)
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+

|
|
5
|
+

|
|
6
6
|
[](https://badge.fury.io/rb/whittaker_tech-midas)
|
|
7
7
|
[](https://github.com/WhittakerTech/midas/actions)
|
|
8
8
|
|
|
@@ -77,7 +77,7 @@ product.set_price(amount: 2999, currency_code: 'USD')
|
|
|
77
77
|
product.price # => Coin object
|
|
78
78
|
product.price_amount # => Money object (#<Money @cents=2999 @currency="USD">)
|
|
79
79
|
product.price_format # => "$29.99"
|
|
80
|
-
product.price_in('EUR') # => "€26.85"
|
|
80
|
+
product.price_in('EUR') # => "€26.85"
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
## Usage Guide
|
|
@@ -196,7 +196,7 @@ Money.default_formatting_rules = {
|
|
|
196
196
|
|
|
197
197
|
### Exchange Rates
|
|
198
198
|
|
|
199
|
-
Set up exchange rates
|
|
199
|
+
Set up exchange rates on `Money.default_bank` as usual:
|
|
200
200
|
```ruby
|
|
201
201
|
# In your app
|
|
202
202
|
Money.default_bank.add_rate('USD', 'EUR', 0.85)
|
|
@@ -211,6 +211,68 @@ For production, integrate with an exchange rate API:
|
|
|
211
211
|
- [money-open-exchange-rates](https://github.com/spk/money-open-exchange-rates)
|
|
212
212
|
- [google_currency](https://github.com/RubyMoney/google_currency)
|
|
213
213
|
|
|
214
|
+
#### How conversion works
|
|
215
|
+
|
|
216
|
+
`Coin` conversion is provider-agnostic. By default it wraps whatever bank is
|
|
217
|
+
set on `Money.default_bank` (via `Coin::Converter::BankProvider`), so any of
|
|
218
|
+
the gems above work unmodified. You can also convert directly:
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
coin = product.price
|
|
222
|
+
coin.convert_to('EUR') # => new, persisted Coin in EUR
|
|
223
|
+
coin.exchange_to('EUR') # => alias for convert_to
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Every conversion — whether via `convert_to`, `exchange_to`, `#{name}_in`, or
|
|
227
|
+
`Coin#format(to:)` — writes an immutable `WhittakerTech::Midas::Exchange`
|
|
228
|
+
audit row recording the `from`/`to` coins, the `rate` used, the provider
|
|
229
|
+
`source`, and the timestamp (`at`). This is a write-only audit log, not a
|
|
230
|
+
rate cache: `convert_to` never reads past `Exchange` rows back to resolve a
|
|
231
|
+
rate, it always asks the provider fresh.
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
result = coin.convert_to('EUR')
|
|
235
|
+
exchange = WhittakerTech::Midas::Exchange.last
|
|
236
|
+
exchange.from # => Coin copy of the original value (USD)
|
|
237
|
+
exchange.to # => the converted result (== `result`)
|
|
238
|
+
exchange.rate # => BigDecimal rate used
|
|
239
|
+
exchange.source # => "money:Money::Bank::VariableExchange"
|
|
240
|
+
exchange.at # => Time the conversion was made
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**`format(to:)` converts on every call.** If you need the same converted
|
|
244
|
+
value multiple times, convert once and reuse the result instead of calling
|
|
245
|
+
`format(to:)` repeatedly — each call performs a live conversion and writes a
|
|
246
|
+
new `Exchange` row:
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
# Avoid — converts and audits twice
|
|
250
|
+
coin.format(to: 'EUR')
|
|
251
|
+
coin.format(to: 'EUR')
|
|
252
|
+
|
|
253
|
+
# Prefer — convert once, format many times
|
|
254
|
+
converted = coin.convert_to('EUR')
|
|
255
|
+
converted.amount.format
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Historical rates.** Passing `at:` requires a provider that implements
|
|
259
|
+
`#exchange_at(money, currency_code, at:)` — the default `BankProvider` does
|
|
260
|
+
not, since `Money::Bank::VariableExchange` has no historical capability.
|
|
261
|
+
Passing `at:` against the default provider raises `ArgumentError`. Supply a
|
|
262
|
+
custom provider via `using:` for historical support:
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
coin.convert_to('EUR', at: 3.months.ago, using: my_historical_provider)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Custom providers.** Any object responding to `#exchange(money, currency_code)`
|
|
269
|
+
and `#name` can be passed as `using:` to override the default bank-backed
|
|
270
|
+
provider — useful for testing or wiring in a rate API directly:
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
coin.convert_to('EUR', using: my_provider)
|
|
274
|
+
```
|
|
275
|
+
|
|
214
276
|
## Advanced Usage
|
|
215
277
|
|
|
216
278
|
### Multiple Coins on One Resource
|
|
@@ -230,7 +292,7 @@ order.set_shipping(amount: 850, currency_code: 'EUR')
|
|
|
230
292
|
|
|
231
293
|
order.subtotal_format # => "$100.00"
|
|
232
294
|
order.shipping_format # => "€8.50"
|
|
233
|
-
order.shipping_in('USD') # => "$10.00"
|
|
295
|
+
order.shipping_in('USD') # => "$10.00"
|
|
234
296
|
```
|
|
235
297
|
|
|
236
298
|
### Working with Coin Objects Directly
|
|
@@ -366,11 +428,18 @@ bin/rails server
|
|
|
366
428
|
|
|
367
429
|
### Exchange rates not working
|
|
368
430
|
|
|
369
|
-
Make sure you've configured exchange rates
|
|
431
|
+
Make sure you've configured exchange rates on `Money.default_bank` (the
|
|
432
|
+
default provider raises whatever error the underlying bank raises, e.g.
|
|
433
|
+
`Money::Bank::UnknownRate`, if a rate is missing):
|
|
370
434
|
```ruby
|
|
371
435
|
Money.default_bank.add_rate('USD', 'EUR', 0.85)
|
|
372
436
|
```
|
|
373
437
|
|
|
438
|
+
### `ArgumentError` mentioning "historical" from `convert_to`
|
|
439
|
+
|
|
440
|
+
You passed `at:` without a provider that supports it. Either omit `at:` or
|
|
441
|
+
supply `using:` with a provider implementing `#exchange_at`.
|
|
442
|
+
|
|
374
443
|
### Input field not formatting
|
|
375
444
|
|
|
376
445
|
Check that Stimulus is loaded and the controller is registered:
|
|
@@ -1,78 +1,74 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
decimals = options.fetch(:decimals, 2)
|
|
57
|
-
label_text = options.fetch(:label, nil)
|
|
3
|
+
# Form helper for rendering currency input fields with bank-style typing behavior.
|
|
4
|
+
#
|
|
5
|
+
# This helper provides a headless currency input that the parent application
|
|
6
|
+
# can style according to its design system. The helper only provides the
|
|
7
|
+
# behavior (via Stimulus) and basic structure.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage (unstyled)
|
|
10
|
+
# <%= midas_currency_field f, :price, currency_code: 'USD' %>
|
|
11
|
+
#
|
|
12
|
+
# @example With Tailwind styling
|
|
13
|
+
# <%= midas_currency_field f, :price,
|
|
14
|
+
# currency_code: 'USD',
|
|
15
|
+
# input_html: { class: 'rounded-lg border-gray-300 text-right' },
|
|
16
|
+
# wrapper_html: { class: 'mb-4' },
|
|
17
|
+
# label: 'Product Price' %>
|
|
18
|
+
#
|
|
19
|
+
# @example With Bootstrap styling
|
|
20
|
+
# <%= midas_currency_field f, :price,
|
|
21
|
+
# currency_code: 'EUR',
|
|
22
|
+
# input_html: { class: 'form-control text-end' },
|
|
23
|
+
# wrapper_html: { class: 'mb-3' },
|
|
24
|
+
# label: 'Price (€)' %>
|
|
25
|
+
module WhittakerTech::Midas::FormHelper
|
|
26
|
+
# Renders a currency input field with bank-style typing behavior.
|
|
27
|
+
#
|
|
28
|
+
# The field consists of:
|
|
29
|
+
# - A visible formatted input (displays dollars/euros)
|
|
30
|
+
# - A hidden input storing minor units (cents)
|
|
31
|
+
# - A hidden input storing the currency code
|
|
32
|
+
#
|
|
33
|
+
# @param form [ActionView::Helpers::FormBuilder] The form builder
|
|
34
|
+
# @param attribute [Symbol] The coin attribute name (e.g., :price, :cost)
|
|
35
|
+
# @param currency_code [String] ISO 4217 currency code (e.g., 'USD', 'EUR')
|
|
36
|
+
# @param options [Hash] Additional options
|
|
37
|
+
# @option options [Hash] :input_html HTML attributes for the display input
|
|
38
|
+
# @option options [Hash] :wrapper_html HTML attributes for the wrapper div
|
|
39
|
+
# @option options [Integer] :decimals Number of decimal places (default: 2)
|
|
40
|
+
# @option options [String] :label Label text (optional, no label if nil)
|
|
41
|
+
#
|
|
42
|
+
# @return [String] Rendered HTML for the currency field
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# <%= midas_currency_field f, :price,
|
|
46
|
+
# currency_code: 'USD',
|
|
47
|
+
# decimals: 2,
|
|
48
|
+
# label: 'Sale Price',
|
|
49
|
+
# wrapper_html: { class: 'form-group' },
|
|
50
|
+
# input_html: { class: 'form-control', placeholder: '0.00' } %>
|
|
51
|
+
def midas_currency_field(form, attribute, currency_code:, **options)
|
|
52
|
+
input_html = options.fetch(:input_html, {})
|
|
53
|
+
wrapper_html = options.fetch(:wrapper_html, {})
|
|
54
|
+
decimals = options.fetch(:decimals, 2)
|
|
55
|
+
label_text = options.fetch(:label, nil)
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
# Get current value if coin exists
|
|
58
|
+
resource = form.object
|
|
59
|
+
coin = resource.public_send(attribute) if resource.respond_to?(attribute)
|
|
60
|
+
current_minor = coin&.currency_minor || 0
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
end
|
|
76
|
-
end
|
|
62
|
+
render partial: 'whittaker_tech/midas/shared/currency_field',
|
|
63
|
+
locals: {
|
|
64
|
+
form: form,
|
|
65
|
+
attribute: attribute,
|
|
66
|
+
currency_code: currency_code,
|
|
67
|
+
current_minor: current_minor,
|
|
68
|
+
decimals: decimals,
|
|
69
|
+
input_html: input_html,
|
|
70
|
+
wrapper_html: wrapper_html,
|
|
71
|
+
label_text: label_text
|
|
72
|
+
}
|
|
77
73
|
end
|
|
78
74
|
end
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
default from: 'from@example.com'
|
|
5
|
-
layout 'mailer'
|
|
6
|
-
end
|
|
7
|
-
end
|
|
1
|
+
class WhittakerTech::Midas::ApplicationMailer < ActionMailer::Base
|
|
2
|
+
default from: 'from@example.com'
|
|
3
|
+
layout 'mailer'
|
|
8
4
|
end
|