whittaker_tech-midas 0.2.0 → 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 +74 -5
- data/app/models/concerns/whittaker_tech/midas/bankable.rb +3 -1
- data/app/models/whittaker_tech/midas/coin/converter/bank_provider.rb +35 -0
- data/app/models/whittaker_tech/midas/coin/converter.rb +70 -23
- data/app/models/whittaker_tech/midas/coin.rb +9 -38
- data/app/models/whittaker_tech/midas/exchange.rb +37 -0
- data/db/migrate/20260707000000_create_midas_exchanges.rb +15 -0
- data/lib/whittaker_tech/midas/version.rb +1 -1
- metadata +4 -1
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,7 +1,7 @@
|
|
|
1
1
|
# WhittakerTech::Midas
|
|
2
2
|
|
|
3
3
|
[](MIT-LICENSE)
|
|
4
|
-

|
|
5
5
|

|
|
6
6
|
[](https://badge.fury.io/rb/whittaker_tech-midas)
|
|
7
7
|
[](https://github.com/WhittakerTech/midas/actions)
|
|
@@ -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:
|
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
# - `name`: Returns the associated Coin object
|
|
53
53
|
# - `name_amount`: Returns the Money object representing the amount
|
|
54
54
|
# - `name_format`: Returns a formatted string representation of the amount
|
|
55
|
+
# - `name_in(currency_code)`: Converts and formats the coin in another currency
|
|
56
|
+
# (writes an Exchange audit row on every call — see `Coin::Converter`)
|
|
55
57
|
# - `set_name(amount:, currency_code:)`: Sets the coin value with the given amount and currency
|
|
56
58
|
#
|
|
57
59
|
# == Supported Amount Types
|
|
@@ -129,7 +131,7 @@ module WhittakerTech::Midas::Bankable
|
|
|
129
131
|
|
|
130
132
|
define_method("#{name}_amount") { public_send(name)&.amount }
|
|
131
133
|
define_method("#{name}_format") { public_send(name)&.amount&.format }
|
|
132
|
-
|
|
134
|
+
define_method("#{name}_in") { |to| public_send(name)&.exchange_to(to)&.format }
|
|
133
135
|
|
|
134
136
|
# Sets the coin value with the specified amount and currency.
|
|
135
137
|
#
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Default currency-conversion provider: wraps Money.default_bank (or any
|
|
4
|
+
# explicit Money::Bank::* instance) behind the adapter interface
|
|
5
|
+
# WhittakerTech::Midas::Coin::Converter expects.
|
|
6
|
+
#
|
|
7
|
+
# Adapter interface (duck-typed):
|
|
8
|
+
# #exchange(money, currency_code) -> Money
|
|
9
|
+
# #name -> String (recorded as Exchange#source)
|
|
10
|
+
# #exchange_at(money, currency_code, at:) -> Money (optional; only
|
|
11
|
+
# needed to support a non-default at:. This provider does not define
|
|
12
|
+
# it — the default Money::Bank::VariableExchange has no historical
|
|
13
|
+
# capability at all.)
|
|
14
|
+
#
|
|
15
|
+
# @since 0.3.0
|
|
16
|
+
class WhittakerTech::Midas::Coin::Converter::BankProvider
|
|
17
|
+
def initialize(bank = Money.default_bank)
|
|
18
|
+
@bank = bank
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param money [Money] the value to convert (native currency)
|
|
22
|
+
# @param currency_code [String] target ISO 4217 code
|
|
23
|
+
# @return [Money]
|
|
24
|
+
def exchange(money, currency_code)
|
|
25
|
+
# NOTE: deliberately NOT `money.exchange_to(currency_code)` — that
|
|
26
|
+
# method hardcodes Money.default_bank internally (money gem 6.x) and
|
|
27
|
+
# would silently ignore a `using:` override. Call the bank directly.
|
|
28
|
+
@bank.exchange_with(money, Money::Currency.new(currency_code))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [String] recorded on Exchange#source
|
|
32
|
+
def name
|
|
33
|
+
"money:#{@bank.class.name}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -1,32 +1,79 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Converter
|
|
3
|
+
# Converter implements cross-currency conversion for Coin, backed by a
|
|
4
|
+
# provider-agnostic adapter (default: Coin::Converter::BankProvider wrapping
|
|
5
|
+
# Money.default_bank). Every successful conversion writes an immutable
|
|
6
|
+
# WhittakerTech::Midas::Exchange audit row, which owns both a copy of the
|
|
7
|
+
# source value (`from`) and the converted result (`to`).
|
|
4
8
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Related modules:
|
|
8
|
-
#
|
|
9
|
-
# - `Arithmetic`: Integer arithmetic on minor units
|
|
10
|
-
# - `Allocation`: Per-unit pricing interpretation
|
|
11
|
-
# - `Converter`: Cross-currency rate conversion (planned)
|
|
12
|
-
#
|
|
13
|
-
# When implemented, Converter will handle:
|
|
14
|
-
# - Exchange rate sources (live API, snapshot, historical)
|
|
15
|
-
# - Historical conversions with a specific timestamp
|
|
16
|
-
# - Regulatory rounding rules per jurisdiction
|
|
17
|
-
#
|
|
18
|
-
# @note All methods in this module raise `NotImplementedError` intentionally.
|
|
19
|
-
# Use the Money gem's exchange rate infrastructure directly for now.
|
|
20
|
-
# @since 0.1.0
|
|
9
|
+
# @since 0.3.0
|
|
21
10
|
module WhittakerTech::Midas::Coin::Converter
|
|
22
11
|
# Converts this Coin to another currency.
|
|
23
12
|
#
|
|
13
|
+
# The receiver does not need to be persisted — it's only ever read, never
|
|
14
|
+
# mutated or reassigned. The result is a brand-new, persisted Coin owned
|
|
15
|
+
# by a newly created Exchange audit row (not linked back to this Coin's
|
|
16
|
+
# own resource).
|
|
17
|
+
#
|
|
24
18
|
# @param currency_code [String] target ISO 4217 currency code
|
|
25
|
-
# @param at [Time]
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# @
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
# @param at [Time, nil] rate timestamp; nil (default) means "now". A
|
|
20
|
+
# non-nil value requires `using:` to supply a provider implementing
|
|
21
|
+
# `#exchange_at`.
|
|
22
|
+
# @param using [Object, Money::Bank::Base, nil] provider override; any
|
|
23
|
+
# object responding to `#exchange(money, currency_code)` and `#name`,
|
|
24
|
+
# or a raw `Money::Bank::*` instance (auto-wrapped in BankProvider)
|
|
25
|
+
# @return [Coin] the persisted `to` Coin (the converted result), owned by
|
|
26
|
+
# the newly created Exchange audit row
|
|
27
|
+
# @raise [ArgumentError] if `at` is given and the resolved provider has
|
|
28
|
+
# no `#exchange_at`
|
|
29
|
+
def convert_to(currency_code, at: nil, using: nil)
|
|
30
|
+
provider = resolve_provider(using)
|
|
31
|
+
iso = currency_code.to_s.strip.upcase
|
|
32
|
+
|
|
33
|
+
if currency_minor.zero?
|
|
34
|
+
converted_cents = 0
|
|
35
|
+
rate = BigDecimal(0)
|
|
36
|
+
else
|
|
37
|
+
converted = fetch_rate(provider, iso, at)
|
|
38
|
+
converted_cents = converted.cents
|
|
39
|
+
rate = BigDecimal(converted_cents) / BigDecimal(currency_minor)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
persist_conversion(provider, iso, at, converted_cents, rate)
|
|
43
|
+
end
|
|
44
|
+
alias exchange_to convert_to
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def resolve_provider(using)
|
|
49
|
+
case using
|
|
50
|
+
when nil then WhittakerTech::Midas::Coin::Converter::BankProvider.new
|
|
51
|
+
when Money::Bank::Base then WhittakerTech::Midas::Coin::Converter::BankProvider.new(using)
|
|
52
|
+
else using
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def fetch_rate(provider, iso, at)
|
|
57
|
+
return provider.exchange(amount, iso) if at.nil?
|
|
58
|
+
|
|
59
|
+
unless provider.respond_to?(:exchange_at)
|
|
60
|
+
raise ArgumentError,
|
|
61
|
+
"#{provider.respond_to?(:name) ? provider.name : provider.class.name} does not support " \
|
|
62
|
+
'historical rates (at:); omit at: or supply a provider implementing #exchange_at'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
provider.exchange_at(amount, iso, at:)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def persist_conversion(provider, iso, at, minor, rate)
|
|
69
|
+
source = provider.respond_to?(:name) ? provider.name : provider.class.name
|
|
70
|
+
stamp = at || Time.current
|
|
71
|
+
|
|
72
|
+
WhittakerTech::Midas::Coin.transaction do
|
|
73
|
+
exchange = WhittakerTech::Midas::Exchange.create!(rate:, source:, at: stamp)
|
|
74
|
+
exchange.set_from(amount: currency_minor, currency_code: currency_code)
|
|
75
|
+
exchange.set_to(amount: minor, currency_code: iso)
|
|
76
|
+
exchange.to
|
|
77
|
+
end
|
|
31
78
|
end
|
|
32
79
|
end
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
# +------------+----------------------------------------------+
|
|
28
28
|
# | Arithmetic | +, -, *, /, %, negate, equality |
|
|
29
29
|
# | Bidi | Unicode bidirectional text isolation |
|
|
30
|
-
# | Converter | Currency conversion (
|
|
30
|
+
# | Converter | Currency conversion (live, audited via Exchange) |
|
|
31
31
|
# | Presenter | Token-based formatting grammar |
|
|
32
32
|
# +------------+----------------------------------------------+
|
|
33
33
|
#
|
|
@@ -46,13 +46,12 @@
|
|
|
46
46
|
# - `WhittakerTech::Midas::Coin::Arithmetic`
|
|
47
47
|
# - `WhittakerTech::Midas::Coin::Allocation`
|
|
48
48
|
# @since 0.1.0
|
|
49
|
-
# rubocop:disable Metrics/ClassLength
|
|
50
49
|
class WhittakerTech::Midas::Coin < WhittakerTech::Midas::ApplicationRecord
|
|
51
50
|
# Arithmetic: exact arithmetic and equality semantics
|
|
52
51
|
include Arithmetic
|
|
53
52
|
# Bidi: bidirectional currency conversion
|
|
54
53
|
include Bidi
|
|
55
|
-
# Converter:
|
|
54
|
+
# Converter: live currency conversion + Exchange audit trail
|
|
56
55
|
include Converter
|
|
57
56
|
# Presenter: formatting and presentation logic
|
|
58
57
|
include Presenter
|
|
@@ -127,16 +126,18 @@ class WhittakerTech::Midas::Coin < WhittakerTech::Midas::ApplicationRecord
|
|
|
127
126
|
# This is a convenience wrapper around the Money gem's `#format`. For
|
|
128
127
|
# richer formatting use `#present` with a pattern string.
|
|
129
128
|
#
|
|
129
|
+
# @note When `to` is given, this performs a live conversion (via
|
|
130
|
+
# `#convert_to`) and writes an Exchange audit row on every call. If
|
|
131
|
+
# formatting the same converted value repeatedly (e.g. in a view loop),
|
|
132
|
+
# convert once and reuse the result's `#amount.format` instead.
|
|
133
|
+
#
|
|
130
134
|
# @param to [String, nil] target ISO 4217 currency code for conversion,
|
|
131
135
|
# or `nil` to format in the native currency.
|
|
132
136
|
# @return [String] the formatted monetary string, e.g. `"$29.99"`
|
|
133
137
|
def format(to: nil)
|
|
134
|
-
|
|
135
|
-
raise NotImplementedError,
|
|
136
|
-
'Currency conversion is not yet implemented. Use #amount.format for native formatting.'
|
|
137
|
-
end
|
|
138
|
+
return amount.format unless to
|
|
138
139
|
|
|
139
|
-
amount.format
|
|
140
|
+
convert_to(to).amount.format
|
|
140
141
|
end
|
|
141
142
|
|
|
142
143
|
# @return [Integer] the raw minor-unit count (alias for `#currency_minor`)
|
|
@@ -276,35 +277,6 @@ class WhittakerTech::Midas::Coin < WhittakerTech::Midas::ApplicationRecord
|
|
|
276
277
|
end
|
|
277
278
|
end
|
|
278
279
|
|
|
279
|
-
# ── Deprecated ────────────────────────────────────────────────────────── #
|
|
280
|
-
|
|
281
|
-
# @deprecated Use `#resource_role` instead. Will be removed in v0.3.0.
|
|
282
|
-
def resource_label
|
|
283
|
-
WhittakerTech::Midas::Deprecation.warn(
|
|
284
|
-
'Coin#resource_label is deprecated. Use Coin#resource_role instead.',
|
|
285
|
-
caller_locations(1, 1)&.first
|
|
286
|
-
)
|
|
287
|
-
resource_role
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
# @deprecated Use `#resource_role=` instead. Will be removed in v0.3.0.
|
|
291
|
-
def resource_label=(value)
|
|
292
|
-
WhittakerTech::Midas::Deprecation.warn(
|
|
293
|
-
'Coin#resource_label= is deprecated. Use Coin#resource_role= instead.',
|
|
294
|
-
caller_locations(1, 1)&.first
|
|
295
|
-
)
|
|
296
|
-
self.resource_role = value
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
# @deprecated Use `for_role` instead. Will be removed in v0.3.0.
|
|
300
|
-
def self.for_label(label)
|
|
301
|
-
WhittakerTech::Midas::Deprecation.warn(
|
|
302
|
-
'Coin.for_label is deprecated. Use Coin.for_role instead.',
|
|
303
|
-
caller_locations(1, 1)&.first
|
|
304
|
-
)
|
|
305
|
-
for_role(label)
|
|
306
|
-
end
|
|
307
|
-
|
|
308
280
|
private
|
|
309
281
|
|
|
310
282
|
# Normalizes currency_code before validation.
|
|
@@ -316,4 +288,3 @@ class WhittakerTech::Midas::Coin < WhittakerTech::Midas::ApplicationRecord
|
|
|
316
288
|
self.currency_code = currency_code.to_s.strip.upcase.presence if currency_code
|
|
317
289
|
end
|
|
318
290
|
end
|
|
319
|
-
# rubocop:enable Metrics/ClassLength
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Exchange is an immutable audit record of a single currency conversion.
|
|
4
|
+
#
|
|
5
|
+
# It owns two Coins via the standard Bankable DSL — `from` (a copy of the
|
|
6
|
+
# value being converted) and `to` (the converted result) — plus the rate,
|
|
7
|
+
# provider, and timestamp used.
|
|
8
|
+
#
|
|
9
|
+
# Exchange is write-only: nothing reads it back to resolve future
|
|
10
|
+
# conversions. It exists purely as an auditable record of what was
|
|
11
|
+
# converted, at what rate, using which provider, and when.
|
|
12
|
+
#
|
|
13
|
+
# @since 0.3.0
|
|
14
|
+
class WhittakerTech::Midas::Exchange < WhittakerTech::Midas::ApplicationRecord
|
|
15
|
+
include WhittakerTech::Midas::Bankable
|
|
16
|
+
|
|
17
|
+
self.table_name = WhittakerTech::Midas.table_name('exchanges')
|
|
18
|
+
|
|
19
|
+
# Gives #from/#to (Coin readers), #from_amount/#to_amount,
|
|
20
|
+
# #from_format/#to_format, and #set_from/#set_to.
|
|
21
|
+
has_coins :from, :to
|
|
22
|
+
|
|
23
|
+
validates :rate, presence: true
|
|
24
|
+
validates :source, presence: true
|
|
25
|
+
validates :at, presence: true
|
|
26
|
+
|
|
27
|
+
before_update :block_updates
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Exchange rows are set once at creation and never touched again — the
|
|
32
|
+
# coins it owns attach separately via Bankable and don't trigger this
|
|
33
|
+
# callback. Does not block #destroy.
|
|
34
|
+
def block_updates
|
|
35
|
+
raise ActiveRecord::ReadOnlyRecord, 'WhittakerTech::Midas::Exchange records are immutable after creation'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'whittaker_tech/midas'
|
|
4
|
+
|
|
5
|
+
class CreateMidasExchanges < ActiveRecord::Migration[8.0]
|
|
6
|
+
def change
|
|
7
|
+
create_table WhittakerTech::Midas.table_name('exchanges') do |t|
|
|
8
|
+
t.decimal :rate, precision: 24, scale: 12, null: false
|
|
9
|
+
t.string :source, null: false
|
|
10
|
+
t.datetime :at, null: false
|
|
11
|
+
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: whittaker_tech-midas
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lee Whittaker
|
|
@@ -220,8 +220,10 @@ files:
|
|
|
220
220
|
- app/models/whittaker_tech/midas/coin/arithmetic.rb
|
|
221
221
|
- app/models/whittaker_tech/midas/coin/bidi.rb
|
|
222
222
|
- app/models/whittaker_tech/midas/coin/converter.rb
|
|
223
|
+
- app/models/whittaker_tech/midas/coin/converter/bank_provider.rb
|
|
223
224
|
- app/models/whittaker_tech/midas/coin/parser.rb
|
|
224
225
|
- app/models/whittaker_tech/midas/coin/presenter.rb
|
|
226
|
+
- app/models/whittaker_tech/midas/exchange.rb
|
|
225
227
|
- app/views/layouts/whittaker_tech/midas/application.html.erb
|
|
226
228
|
- app/views/layouts/whittaker_tech/midas/shared/_currency_field.html.erb
|
|
227
229
|
- config/locales/midas.en.yml
|
|
@@ -229,6 +231,7 @@ files:
|
|
|
229
231
|
- db/migrate/20260101000001_create_midas_coins.rb
|
|
230
232
|
- db/migrate/20260219120000_rename_resource_label_to_resource_role_in_wt_midas_coins.rb
|
|
231
233
|
- db/migrate/20260219150000_rename_wt_midas_coins_to_midas_coins.rb
|
|
234
|
+
- db/migrate/20260707000000_create_midas_exchanges.rb
|
|
232
235
|
- lib/generators/whittaker_tech/midas/install/install_generator.rb
|
|
233
236
|
- lib/tasks/whittaker_tech/midas_tasks.rake
|
|
234
237
|
- lib/whittaker_tech/midas.rb
|