whittaker_tech-midas 0.1.0 → 0.2.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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/app/controllers/whittaker_tech/midas/application_controller.rb +1 -5
  4. data/app/helpers/whittaker_tech/midas/application_helper.rb +1 -5
  5. data/app/helpers/whittaker_tech/midas/form_helper.rb +68 -72
  6. data/app/jobs/whittaker_tech/midas/application_job.rb +1 -5
  7. data/app/mailers/whittaker_tech/midas/application_mailer.rb +3 -7
  8. data/app/models/concerns/whittaker_tech/midas/bankable.rb +193 -174
  9. data/app/models/whittaker_tech/midas/application_record.rb +2 -6
  10. data/app/models/whittaker_tech/midas/coin/allocation.rb +124 -0
  11. data/app/models/whittaker_tech/midas/coin/arithmetic.rb +196 -0
  12. data/app/models/whittaker_tech/midas/coin/bidi.rb +87 -0
  13. data/app/models/whittaker_tech/midas/coin/converter.rb +32 -0
  14. data/app/models/whittaker_tech/midas/coin/parser.rb +104 -0
  15. data/app/models/whittaker_tech/midas/coin/presenter.rb +229 -0
  16. data/app/models/whittaker_tech/midas/coin.rb +314 -76
  17. data/db/migrate/20260101000000_create_whittaker_tech_schema.rb +26 -0
  18. data/db/migrate/{20251015160523_create_wt_midas_coins.rb → 20260101000001_create_midas_coins.rb} +7 -3
  19. data/db/migrate/20260219120000_rename_resource_label_to_resource_role_in_wt_midas_coins.rb +12 -0
  20. data/db/migrate/20260219150000_rename_wt_midas_coins_to_midas_coins.rb +13 -0
  21. data/lib/generators/whittaker_tech/midas/install/install_generator.rb +10 -0
  22. data/lib/tasks/whittaker_tech/midas_tasks.rake +27 -4
  23. data/lib/whittaker_tech/midas/deprecation.rb +32 -0
  24. data/lib/whittaker_tech/midas/engine.rb +113 -110
  25. data/lib/whittaker_tech/midas/version.rb +4 -4
  26. data/lib/whittaker_tech/midas.rb +120 -3
  27. metadata +71 -19
  28. data/config/routes.rb +0 -2
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Presenter provides a strftime-like token grammar for rendering `Coin` values.
4
+ #
5
+ # ## Design principles
6
+ #
7
+ # - **Declarative** — tokens map directly to named handler methods.
8
+ # - **Pure** — no mutation, rounding, or currency conversion.
9
+ # - **Direction-safe** — all text is wrapped with Unicode bidirectional
10
+ # isolation markers via `Coin::Bidi` to prevent display corruption in
11
+ # mixed LTR/RTL contexts.
12
+ #
13
+ # ## Token reference
14
+ #
15
+ # Patterns are strings containing `%x` tokens (similar to `strftime`).
16
+ #
17
+ # Tokens:
18
+ #
19
+ # - `%t`: Formatted total (symbol + amount), e.g. `$29.99`
20
+ # - `%m`: Minor units (raw integer), e.g. `2999`
21
+ # - `%M`: Major units (decimal string), e.g. `29.99`
22
+ # - `%c`: Currency code, e.g. `USD`
23
+ # - `%s`: Currency symbol, e.g. `$`
24
+ # - `%n`: Number only (no symbol), e.g. `29.99`
25
+ # - `%u`: Custom units label (see opts), e.g. `per kg`
26
+ # - `%p`: Custom per-exact label (see opts), e.g. `0.2997`
27
+ # - `%~`: Approximate marker (`≈` or empty)
28
+ # - `%%`: Literal percent sign
29
+ #
30
+ # ## Usage
31
+ #
32
+ # coin = Coin.value(2999, 'USD')
33
+ # coin.present('%s%M %c') # => "$29.99 USD"
34
+ # coin.present('%~%t', approx: true) # => "≈$29.99"
35
+ #
36
+ # ## Options
37
+ #
38
+ # Pass keyword options to `present` / `format` to populate optional tokens:
39
+ #
40
+ # - `approx:` [Boolean] — if true, `~` expands to `≈`; otherwise empty string.
41
+ # - `units:` [String] — value for `%u` token.
42
+ # - `per_exact:` [String] — value for `%p` token.
43
+ # - `currency_dir:` [Symbol] — override the currency display direction
44
+ # (`:ltr` or `:rtl`); defaults to the value from
45
+ # `WhittakerTech::Midas.currency_direction_for`.
46
+ #
47
+ # @since 0.1.0
48
+ module WhittakerTech::Midas::Coin::Presenter
49
+ # Internal rendering context. Populated from the Coin and caller-supplied options.
50
+ # @!visibility private
51
+ Context = Struct.new(
52
+ :coin,
53
+ :currency_dir,
54
+ :approx,
55
+ :units,
56
+ :per_exact,
57
+ keyword_init: true
58
+ )
59
+
60
+ # Renders this Coin using a format pattern.
61
+ #
62
+ # @param pattern [String] the format pattern containing `%x` tokens
63
+ # @param opts [Hash] optional rendering context overrides (see module docs)
64
+ # @return [String]
65
+ # @raise [ArgumentError] if the pattern contains an unknown or unterminated token
66
+ #
67
+ # @example
68
+ # coin.present('%s%M') # => "$29.99"
69
+ # coin.present('%t (%c)') # => "$29.99 (USD)"
70
+ # coin.present('~%t', approx: true) # => "≈$29.99"
71
+ def present(pattern, **)
72
+ WhittakerTech::Midas::Coin::Presenter.format(self, pattern, **)
73
+ end
74
+
75
+ class << self
76
+ # Registry of recognised tokens and their handler method names.
77
+ #
78
+ # @return [Hash{String => Symbol}]
79
+ TOKEN_MAP = {
80
+ '%' => :token_percent,
81
+ 't' => :token_total,
82
+ 'm' => :token_minor,
83
+ 'M' => :token_major,
84
+ 'c' => :token_currency_code,
85
+ 's' => :token_currency_symbol,
86
+ 'n' => :token_number_only,
87
+ 'u' => :token_units,
88
+ 'p' => :token_per_exact,
89
+ '~' => :token_approx
90
+ }.freeze
91
+
92
+ # Formats a Coin using a pattern string.
93
+ #
94
+ # This is the class-level entry point; instance-level access is via
95
+ # `Coin#present`.
96
+ #
97
+ # @param coin [Coin] the value to format
98
+ # @param pattern [String] the format pattern
99
+ # @param opts [Hash] optional context overrides
100
+ # @return [String]
101
+ # @raise [ArgumentError] if `pattern` is nil, contains an unknown token,
102
+ # or has an unterminated `%` escape
103
+ def format(coin, pattern, **)
104
+ raise ArgumentError, 'pattern required' if pattern.nil?
105
+
106
+ ctx = build_context(coin, **)
107
+ scan(pattern, ctx)
108
+ end
109
+
110
+ # Builds a `Context` struct from a Coin and caller-supplied options.
111
+ #
112
+ # @param coin [Coin]
113
+ # @param opts [Hash]
114
+ # @option opts [Symbol] :currency_dir (:ltr or :rtl) override direction
115
+ # @option opts [Boolean] :approx whether to render `~` as `≈`
116
+ # @option opts [String, nil] :units value for `%u` token
117
+ # @option opts [String, nil] :per_exact value for `%p` token
118
+ # @return [Context]
119
+ def build_context(coin, **opts)
120
+ Context.new(coin:,
121
+ currency_dir: opts[:currency_dir] || coin.bidi_currency_dir(coin.currency_code),
122
+ approx: opts[:approx] || false,
123
+ units: opts[:units] || nil,
124
+ per_exact: opts[:per_exact] || nil)
125
+ end
126
+
127
+ # Scans a pattern string, expanding `%x` tokens into rendered values.
128
+ #
129
+ # @param pattern [String]
130
+ # @param ctx [Context]
131
+ # @return [String]
132
+ # @raise [ArgumentError] on unknown or unterminated tokens
133
+ def scan(pattern, ctx)
134
+ is_token = false
135
+ out = +'' # output buffer
136
+
137
+ pattern.to_s.each_char do |char|
138
+ if is_token
139
+ out << dispatch(char, ctx)
140
+ is_token = false
141
+ elsif char == '%'
142
+ is_token = true
143
+ else
144
+ out << char
145
+ end
146
+ end
147
+
148
+ raise ArgumentError, "Unterminated token in pattern: #{pattern}" if is_token
149
+
150
+ out
151
+ end
152
+
153
+ # Dispatches a single token character to its handler.
154
+ #
155
+ # @param token [String] single character following `%`
156
+ # @param ctx [Context]
157
+ # @return [String]
158
+ # @raise [ArgumentError] if the token is not in `TOKEN_MAP`
159
+ def dispatch(token, ctx)
160
+ handler = TOKEN_MAP[token]
161
+ raise ArgumentError, "Unknown presenter token: %#{token}" unless handler
162
+
163
+ send(handler, **ctx.to_h)
164
+ end
165
+
166
+ # -------------------------
167
+ # Token implementations
168
+ # -------------------------
169
+
170
+ # @api private
171
+ def token_percent(**)
172
+ '%'
173
+ end
174
+
175
+ # @api private
176
+ def token_total(coin:, currency_dir:, **)
177
+ coin.bidi_isolate(coin.amount.format, dir: currency_dir)
178
+ end
179
+
180
+ # @api private
181
+ def token_minor(coin:, **)
182
+ coin.bidi_isolate_number(coin.currency_minor)
183
+ end
184
+
185
+ # @api private
186
+ def token_major(coin:, **)
187
+ coin.bidi_isolate_number(coin.major.to_s('F'))
188
+ end
189
+
190
+ # @api private
191
+ def token_currency_code(coin:, **)
192
+ # Currency codes are neutral; isolate as LTR for stability
193
+ coin.bidi_isolate(coin.currency_code, dir: :ltr)
194
+ end
195
+
196
+ # @api private
197
+ def token_currency_symbol(coin:, currency_dir:, **)
198
+ symbol = Money::Currency.new(coin.currency_code).symbol
199
+ coin.bidi_isolate(symbol, dir: currency_dir)
200
+ end
201
+
202
+ # @api private
203
+ def token_number_only(coin:, **)
204
+ # Best-effort extraction of the numeric portion
205
+ formatted = coin.amount.format
206
+ symbol = Money::Currency.new(coin.currency_code).symbol.to_s
207
+
208
+ numberish =
209
+ symbol.empty? ? formatted : formatted.gsub(symbol, '').strip
210
+
211
+ coin.bidi_isolate_number(numberish)
212
+ end
213
+
214
+ # @api private
215
+ def token_units(units:, **)
216
+ units.nil? ? '' : units.to_s
217
+ end
218
+
219
+ # @api private
220
+ def token_per_exact(per_exact:, **)
221
+ per_exact.nil? ? '' : per_exact.to_s
222
+ end
223
+
224
+ # @api private
225
+ def token_approx(approx:, **)
226
+ approx ? '≈' : ''
227
+ end
228
+ end
229
+ end
@@ -1,81 +1,319 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module WhittakerTech
4
- module Midas
5
- class Coin < ApplicationRecord
6
- self.table_name = 'wt_midas_coins'
7
-
8
- belongs_to :resource, polymorphic: true
9
-
10
- before_validation :normalize_fields
11
-
12
- validates :resource_label,
13
- presence: true,
14
- format: { with: /\A[a-z0-9_]+\z/ },
15
- length: { maximum: 64 },
16
- uniqueness: { scope: %i[resource_type resource_id], case_sensitive: false }
17
-
18
- validates :currency_code, presence: true, length: { is: 3 }
19
- validates :currency_minor, presence: true, numericality: { only_integer: true }
20
-
21
- # Returns a Money object representing the stored monetary value.
22
- # Memoized for performance in hot paths.
23
- def amount
24
- @amount ||= Money.new(currency_minor, currency_code)
25
- end
26
-
27
- # Sets the coin's monetary value from various input types.
28
- def amount=(value)
29
- case value
30
- when Money
31
- self.currency_minor = value.cents
32
- self.currency_code = value.currency.iso_code
33
- when Numeric
34
- raise ArgumentError, 'currency_code required before setting numeric amount' if currency_code.blank?
35
-
36
- self.currency_minor = Integer(value)
37
- else
38
- raise ArgumentError, "Invalid value for Coin#amount: #{value.inspect}"
39
- end
40
- end
41
-
42
- delegate :exchange_to, to: :amount
43
-
44
- def format(to: nil)
45
- (to ? exchange_to(to) : amount).format
46
- end
47
-
48
- # Convenient aliases for form helpers
49
- def minor
50
- currency_minor
51
- end
52
-
53
- def currency
54
- currency_code
55
- end
56
-
57
- def fractional
58
- currency_minor
59
- end
60
-
61
- # Override setters to clear memoization
62
- def currency_minor=(value)
63
- @amount = nil
64
- super
65
- end
66
-
67
- def currency_code=(value)
68
- @amount = nil
69
- super
70
- end
71
-
72
- private
73
-
74
- # Normalize inputs for consistency and case-insensitive uniqueness
75
- def normalize_fields
76
- self.resource_label = resource_label.to_s.strip.downcase.presence if resource_label
77
- self.currency_code = currency_code.to_s.strip.upcase.presence if currency_code
78
- end
3
+ # Coin is the canonical, persisted representation of a monetary value.
4
+ #
5
+ # ## Conceptual model
6
+ #
7
+ # - A Coin represents *payable money*: an exact integer count of minor units
8
+ # (cents, pence, etc.) in a specific currency.
9
+ # - A Coin is **immutable by convention**: arithmetic operations return new
10
+ # frozen Coins rather than mutating the receiver.
11
+ # - A Coin owns **arithmetic truth** — not presentation, pricing
12
+ # interpretation, or currency conversion.
13
+ # ## Storage
14
+ #
15
+ #
16
+ # Every Coin is persisted in `midas_coins` and belongs polymorphically to
17
+ # any domain object (`resource_type` / `resource_id`). The `resource_role`
18
+ # distinguishes multiple coins on the same resource (e.g. `"price"`,
19
+ # `"cost"`, `"tax"`).
20
+ #
21
+ # ## Modules
22
+ #
23
+ # Behavior is composed via mixins:
24
+ #
25
+ # +------------+----------------------------------------------+
26
+ # | Module | Responsibility |
27
+ # +------------+----------------------------------------------+
28
+ # | Arithmetic | +, -, *, /, %, negate, equality |
29
+ # | Bidi | Unicode bidirectional text isolation |
30
+ # | Converter | Currency conversion (reserved, not yet live) |
31
+ # | Presenter | Token-based formatting grammar |
32
+ # +------------+----------------------------------------------+
33
+ #
34
+ # ## Usage via Bankable
35
+ #
36
+ # The typical entry point is the `WhittakerTech::Midas::Bankable` concern; direct Coin construction
37
+ # is mostly used in service objects and tests.
38
+ #
39
+ # product.set_price(amount: 29.99, currency_code: 'USD')
40
+ # product.price # => #<WhittakerTech::Midas::Coin ...>
41
+ # product.price_amount # => #<Money @fractional=2999 @currency="USD">
42
+ #
43
+ # See also:
44
+ #
45
+ # - `WhittakerTech::Midas::Bankable`
46
+ # - `WhittakerTech::Midas::Coin::Arithmetic`
47
+ # - `WhittakerTech::Midas::Coin::Allocation`
48
+ # @since 0.1.0
49
+ # rubocop:disable Metrics/ClassLength
50
+ class WhittakerTech::Midas::Coin < WhittakerTech::Midas::ApplicationRecord
51
+ # Arithmetic: exact arithmetic and equality semantics
52
+ include Arithmetic
53
+ # Bidi: bidirectional currency conversion
54
+ include Bidi
55
+ # Converter: future currency conversion logic
56
+ include Converter
57
+ # Presenter: formatting and presentation logic
58
+ include Presenter
59
+
60
+ self.table_name = WhittakerTech::Midas.table_name('coins')
61
+
62
+ # Coins belong polymorphically to a domain object (ledger, entry, product, etc.)
63
+ belongs_to :resource, polymorphic: true
64
+
65
+ # Poly::Joins defines joins_resource(klass) for typed polymorphic joins
66
+ include Poly::Joins
67
+ # Poly::Role validates resource_role, normalizes it, and provides for_role scope
68
+ include Poly::Role
69
+ # Poly::Owners defines owner_resource(klass) for typed polymorphic ownership
70
+ include Poly::Owners
71
+
72
+ # Normalize user input before validation to ensure consistent storage
73
+ before_validation :normalize_currency_code
74
+
75
+ # Delegates presence, format `/\A[a-z0-9_]+\z/`, length (max 64), and
76
+ # before_validation normalization to Poly::Role
77
+ poly_role :resource
78
+
79
+ # Each resource may only have one Coin per resource_role
80
+ validates :resource_role,
81
+ uniqueness: { scope: %i[resource_type resource_id], case_sensitive: false }
82
+
83
+ # Currency code is stored as a 3-letter ISO string (e.g. "USD")
84
+ validates :currency_code, presence: true, length: { is: 3 }
85
+
86
+ # Minor units are always stored as integers
87
+ validates :currency_minor, presence: true, numericality: { only_integer: true }
88
+
89
+ # Returns a Money object representing the stored monetary value.
90
+ #
91
+ # This is a *projection*, not canonical value. Use `#currency_minor`
92
+ # and `#currency_code` as the source of truth.
93
+ #
94
+ # Memoized for performance. The memo is cleared automatically when
95
+ # `#currency_minor=` or `#currency_code=` are called.
96
+ #
97
+ # @return [Money]
98
+ def amount
99
+ @amount ||= Money.new(currency_minor, currency_code)
100
+ end
101
+
102
+ # Sets the coin's monetary value from a Money object or raw minor-unit integer.
103
+ #
104
+ # @param value [Money, Integer, Numeric] the value to assign
105
+ # - `Money` — copies `cents` and `currency.iso_code` directly.
106
+ # - `Numeric` — treated as already-scaled minor units; requires
107
+ # `#currency_code` to already be set.
108
+ # @raise [ArgumentError] if a Numeric is given without a prior currency_code
109
+ # @raise [ArgumentError] if the value type is not supported
110
+ # @return [void]
111
+ def amount=(value)
112
+ case value
113
+ when Money
114
+ self.currency_minor = value.cents
115
+ self.currency_code = value.currency.iso_code
116
+ when Numeric
117
+ raise ArgumentError, 'currency_code required before setting numeric amount' if currency_code.blank?
118
+
119
+ self.currency_minor = Integer(value)
120
+ else
121
+ raise ArgumentError, "Invalid value for Coin#amount: #{value.inspect}"
122
+ end
123
+ end
124
+
125
+ # Formats the Coin for display, optionally converting to another currency first.
126
+ #
127
+ # This is a convenience wrapper around the Money gem's `#format`. For
128
+ # richer formatting use `#present` with a pattern string.
129
+ #
130
+ # @param to [String, nil] target ISO 4217 currency code for conversion,
131
+ # or `nil` to format in the native currency.
132
+ # @return [String] the formatted monetary string, e.g. `"$29.99"`
133
+ def format(to: nil)
134
+ if to
135
+ raise NotImplementedError,
136
+ 'Currency conversion is not yet implemented. Use #amount.format for native formatting.'
137
+ end
138
+
139
+ amount.format
140
+ end
141
+
142
+ # @return [Integer] the raw minor-unit count (alias for `#currency_minor`)
143
+ def minor
144
+ currency_minor
145
+ end
146
+
147
+ # @return [String] the ISO currency code (alias for `#currency_code`)
148
+ def currency
149
+ currency_code
150
+ end
151
+
152
+ # @return [Integer] the raw minor-unit count (alias for `#currency_minor`)
153
+ def fractional
154
+ currency_minor
155
+ end
156
+
157
+ # Clears the memoized Money projection when the minor-unit value changes.
158
+ # @param value [Integer]
159
+ # @return [void]
160
+ def currency_minor=(value)
161
+ @amount = nil
162
+ super
163
+ end
164
+
165
+ # Clears the memoized Money projection when the currency code changes.
166
+ # @param value [String]
167
+ # @return [void]
168
+ def currency_code=(value)
169
+ @amount = nil
170
+ super
171
+ end
172
+
173
+ # Constructs a `Coin::Allocation` that interprets this Coin as a per-unit price.
174
+ #
175
+ # The Coin itself is unchanged; `Coin::Allocation` encapsulates the
176
+ # per-unit interpretation and rounding policy.
177
+ #
178
+ # @param per [Numeric] number of units this Coin covers (the divisor)
179
+ # @param rounding_policy [Symbol] one of `WhittakerTech::Midas::ROUNDING_POLICIES`
180
+ # @return [Coin::Allocation]
181
+ #
182
+ # @example Price per item from a bulk price
183
+ # bulk = Coin.value(10_000, 'USD') # $100.00 for 6 units
184
+ # alloc = bulk.allocate(per: 6, rounding_policy: :ceil)
185
+ # alloc.value # => Coin($16.67)
186
+ # alloc.price(qty: 3) # => Coin($50.00)
187
+ def allocate(per:, rounding_policy: WhittakerTech::Midas::DEFAULT_ROUNDING_POLICY)
188
+ WhittakerTech::Midas::Coin::Allocation.new(
189
+ coin: self,
190
+ divisor: per,
191
+ rounding_policy:
192
+ )
193
+ end
194
+
195
+ # Returns the number of decimal places defined by the currency specification.
196
+ #
197
+ # For example, USD = 2, JPY = 0, BHD = 3
198
+ # This is informational and does not imply rounding or formatting.
199
+ #
200
+ # @return [Integer]
201
+ def decimals
202
+ Money::Currency.new(currency_code).decimal_places
203
+ end
204
+
205
+ # Returns the scaling factor used to convert major units to minor units.
206
+ #
207
+ # Equivalent to `10 ** decimals`. For USD this is `100`; for JPY it is `1`.
208
+ #
209
+ # @return [Numeric]
210
+ def scale
211
+ 10**decimals
212
+ end
213
+
214
+ # Returns the major-unit value as a precise BigDecimal.
215
+ #
216
+ # **This is NOT payable money.** It is intended for inspection and
217
+ # formatting only. No rounding policy is applied.
218
+ #
219
+ # @return [BigDecimal]
220
+ #
221
+ # @example
222
+ # Coin.value(2999, 'USD').major # => BigDecimal("29.99")
223
+ # Coin.value(100, 'JPY').major # => BigDecimal("100")
224
+ def major
225
+ BigDecimal(currency_minor) / scale
226
+ end
227
+
228
+ class << self
229
+ # Constructs a Coin directly from an integer minor-unit count and an ISO
230
+ # currency code.
231
+ #
232
+ # This is the lowest-level factory. It enforces that `currency_minor` is
233
+ # an Integer (fractional minor units are not permitted).
234
+ #
235
+ # @param currency_minor [Integer] the amount in minor units (e.g., cents)
236
+ # @param currency_code [String] ISO 4217 currency code, e.g. `"USD"`
237
+ # @return [Coin]
238
+ # @raise [TypeError] if `currency_minor` is not an Integer
239
+ #
240
+ # @example
241
+ # Coin.value(2999, 'USD') # => $29.99
242
+ # Coin.value(0, 'JPY') # => ¥0
243
+ def value(currency_minor, currency_code)
244
+ raise TypeError unless currency_minor.is_a?(Integer)
245
+
246
+ new(currency_minor:, currency_code:)
247
+ end
248
+
249
+ # Returns a zero-valued Coin in the given currency.
250
+ #
251
+ # @param currency_code [String] ISO 4217 currency code
252
+ # @return [Coin]
253
+ #
254
+ # @example
255
+ # Coin.zero('USD') # => $0.00
256
+ def zero(currency_code)
257
+ new(currency_minor: 0, currency_code:)
258
+ end
259
+
260
+ # Parses a heterogeneous input into a Coin using `Coin::Parser`.
261
+ #
262
+ # Accepted input types: `Coin`, `Money`, `Numeric`, `String`.
263
+ #
264
+ # @param value [Coin, Money, Numeric, String] the value to parse
265
+ # @param currency_code [String, nil] required for Numeric and bare String inputs
266
+ # @return [Coin]
267
+ # @raise [TypeError] if the input type cannot be converted
268
+ # @raise [ArgumentError] if a currency_code is required but not provided
269
+ #
270
+ # @example
271
+ # Coin.parse(Money.new(2999, 'USD'))
272
+ # Coin.parse(29.99, currency_code: 'USD')
273
+ # Coin.parse('$29.99')
274
+ def parse(value, currency_code: nil)
275
+ Parser.parse(value, currency_code:)
79
276
  end
80
277
  end
278
+
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
+ private
309
+
310
+ # Normalizes currency_code before validation.
311
+ #
312
+ # - `currency_code` is stripped and capitalized
313
+ # - `resource_role` normalisation is handled by `Poly::Role`
314
+ # @return [void]
315
+ def normalize_currency_code
316
+ self.currency_code = currency_code.to_s.strip.upcase.presence if currency_code
317
+ end
81
318
  end
319
+ # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'whittaker_tech/midas'
4
+
5
+ class CreateWhittakerTechSchema < ActiveRecord::Migration[8.0]
6
+ SCHEMA_NAME = (WhittakerTech::Midas.table_namespace || 'midas').freeze
7
+ raise 'Invalid schema name' unless SCHEMA_NAME =~ /\A[a-z_][a-z0-9_]*\z/
8
+
9
+ def up
10
+ return unless postgresql?
11
+
12
+ execute "CREATE SCHEMA IF NOT EXISTS #{SCHEMA_NAME}"
13
+ end
14
+
15
+ def down
16
+ return unless postgresql?
17
+
18
+ execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE"
19
+ end
20
+
21
+ private
22
+
23
+ def postgresql?
24
+ connection.adapter_name == 'PostgreSQL'
25
+ end
26
+ end
@@ -1,6 +1,10 @@
1
- class CreateWtMidasCoins < ActiveRecord::Migration[8.0]
1
+ # frozen_string_literal: true
2
+
3
+ require 'whittaker_tech/midas'
4
+
5
+ class CreateMidasCoins < ActiveRecord::Migration[8.0]
2
6
  def change
3
- create_table :wt_midas_coins do |t|
7
+ create_table WhittakerTech::Midas.table_name('coins') do |t|
4
8
  t.references :resource, polymorphic: true, null: false, index: true
5
9
  t.string :resource_label, null: false, limit: 64
6
10
  t.string :currency_code, null: false, limit: 3
@@ -10,7 +14,7 @@ class CreateWtMidasCoins < ActiveRecord::Migration[8.0]
10
14
 
11
15
  t.index %i[resource_id resource_type resource_label],
12
16
  unique: true,
13
- name: 'index_wt_midas_coins_on_owner_and_label'
17
+ name: 'index_midas_coins_on_owner_and_label'
14
18
  end
15
19
  end
16
20
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'whittaker_tech/midas'
4
+
5
+ class RenameResourceLabelToResourceRoleInWtMidasCoins < ActiveRecord::Migration[8.0]
6
+ def change
7
+ rename_column WhittakerTech::Midas.table_name('coins'), :resource_label, :resource_role
8
+ rename_index WhittakerTech::Midas.table_name('coins'),
9
+ 'index_midas_coins_on_owner_and_label',
10
+ 'index_midas_coins_on_resource_and_role'
11
+ end
12
+ end