whittaker_tech-midas 0.1.1 → 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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  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 +186 -172
  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/{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 +5 -11
  22. data/lib/whittaker_tech/midas/deprecation.rb +32 -0
  23. data/lib/whittaker_tech/midas/engine.rb +113 -115
  24. data/lib/whittaker_tech/midas/version.rb +4 -4
  25. data/lib/whittaker_tech/midas.rb +120 -3
  26. metadata +69 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 027c1179177e4e7a6c618a55e196fad98d2b617a91b7ef70a49fb8653bb983ae
4
- data.tar.gz: 54a5943974b9b93be6dfed049b87566afd9c952c560f5b286e62dba46ee18d1c
3
+ metadata.gz: 79ce2a5c7e59f15a9ca7af2ea759c514a32fcdc67915618d01d4f1cf2c00bd9a
4
+ data.tar.gz: 149f3fa609b856dffdef559835f668bb47758c00262d5cb2d4517ef21f3c1892
5
5
  SHA512:
6
- metadata.gz: e6bde8babeb983a210ebb849c01a407c620491be821260782b43977747d84b4c07b23e3878597d49e7e0a2421dd3aab10ffc5aa165403f35f56b152ecdcc8e4f
7
- data.tar.gz: dec832c4c97ae6155a9b67ed0dade3254ea06509446855ace527b6d574018bcafa162ad9d0a5849bf68636365ef38e5627bd16639acae290c6eb58f3890f6eed
6
+ metadata.gz: f446717fbf1acd4e4a3648eec3aeadbaa71e6263b60e77b68e3c960cfaa10a958783888d5bfa0df53c39cbff8e4849193a173aaa423fd4e92830bd7a954c22a5
7
+ data.tar.gz: 538185c60138515129e23ad762985a2bed02ebb369c6c02311107a06c876f481a66df71c4d4437ff99d39d43d67974e18f81b4c172c4d50dde16945dff0b1d34
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # WhittakerTech::Midas
2
2
 
3
3
  [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](MIT-LICENSE)
4
- [![Ruby 3.4](https://img.shields.io/badge/ruby-3.4+-red.svg)]()
5
- [![Rails 7.1](https://img.shields.io/badge/rails-7.1+-crimson.svg)]()
4
+ ![Ruby 3.4](https://img.shields.io/badge/ruby-3.4+-red.svg)
5
+ ![Rails 7.1](https://img.shields.io/badge/rails-7.1+-crimson.svg)
6
6
  [![Gem Version](https://badge.fury.io/rb/whittaker_tech-midas.svg)](https://badge.fury.io/rb/whittaker_tech-midas)
7
7
  [![CI](https://github.com/WhittakerTech/midas/actions/workflows/ci.yml/badge.svg)](https://github.com/WhittakerTech/midas/actions)
8
8
 
@@ -1,6 +1,2 @@
1
- module WhittakerTech
2
- module Midas
3
- class ApplicationController < ActionController::Base
4
- end
5
- end
1
+ class WhittakerTech::Midas::ApplicationController < ActionController::Base
6
2
  end
@@ -1,6 +1,2 @@
1
- module WhittakerTech
2
- module Midas
3
- module ApplicationHelper
4
- end
5
- end
1
+ module WhittakerTech::Midas::ApplicationHelper
6
2
  end
@@ -1,78 +1,74 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module WhittakerTech
4
- module Midas
5
- # Form helper for rendering currency input fields with bank-style typing behavior.
6
- #
7
- # This helper provides a headless currency input that the parent application
8
- # can style according to its design system. The helper only provides the
9
- # behavior (via Stimulus) and basic structure.
10
- #
11
- # @example Basic usage (unstyled)
12
- # <%= midas_currency_field f, :price, currency_code: 'USD' %>
13
- #
14
- # @example With Tailwind styling
15
- # <%= midas_currency_field f, :price,
16
- # currency_code: 'USD',
17
- # input_html: { class: 'rounded-lg border-gray-300 text-right' },
18
- # wrapper_html: { class: 'mb-4' },
19
- # label: 'Product Price' %>
20
- #
21
- # @example With Bootstrap styling
22
- # <%= midas_currency_field f, :price,
23
- # currency_code: 'EUR',
24
- # input_html: { class: 'form-control text-end' },
25
- # wrapper_html: { class: 'mb-3' },
26
- # label: 'Price (€)' %>
27
- module FormHelper
28
- # Renders a currency input field with bank-style typing behavior.
29
- #
30
- # The field consists of:
31
- # - A visible formatted input (displays dollars/euros)
32
- # - A hidden input storing minor units (cents)
33
- # - A hidden input storing the currency code
34
- #
35
- # @param form [ActionView::Helpers::FormBuilder] The form builder
36
- # @param attribute [Symbol] The coin attribute name (e.g., :price, :cost)
37
- # @param currency_code [String] ISO 4217 currency code (e.g., 'USD', 'EUR')
38
- # @param options [Hash] Additional options
39
- # @option options [Hash] :input_html HTML attributes for the display input
40
- # @option options [Hash] :wrapper_html HTML attributes for the wrapper div
41
- # @option options [Integer] :decimals Number of decimal places (default: 2)
42
- # @option options [String] :label Label text (optional, no label if nil)
43
- #
44
- # @return [String] Rendered HTML for the currency field
45
- #
46
- # @example
47
- # <%= midas_currency_field f, :price,
48
- # currency_code: 'USD',
49
- # decimals: 2,
50
- # label: 'Sale Price',
51
- # wrapper_html: { class: 'form-group' },
52
- # input_html: { class: 'form-control', placeholder: '0.00' } %>
53
- def midas_currency_field(form, attribute, currency_code:, **options)
54
- input_html = options.fetch(:input_html, {})
55
- wrapper_html = options.fetch(:wrapper_html, {})
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
- # Get current value if coin exists
60
- resource = form.object
61
- coin = resource.public_send(attribute) if resource.respond_to?(attribute)
62
- current_minor = coin&.currency_minor || 0
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
- render partial: 'whittaker_tech/midas/shared/currency_field',
65
- locals: {
66
- form: form,
67
- attribute: attribute,
68
- currency_code: currency_code,
69
- current_minor: current_minor,
70
- decimals: decimals,
71
- input_html: input_html,
72
- wrapper_html: wrapper_html,
73
- label_text: label_text
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,6 +1,2 @@
1
- module WhittakerTech
2
- module Midas
3
- class ApplicationJob < ActiveJob::Base
4
- end
5
- end
1
+ class WhittakerTech::Midas::ApplicationJob < ActiveJob::Base
6
2
  end
@@ -1,8 +1,4 @@
1
- module WhittakerTech
2
- module Midas
3
- class ApplicationMailer < ActionMailer::Base
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
@@ -1,189 +1,203 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module WhittakerTech
4
- module Midas
5
- # The Bankable module provides currency and monetary value management functionality
6
- # for Active Record models. It allows models to have associated monetary values
7
- # (coins) with different currencies and provides convenient methods for setting,
8
- # retrieving, and formatting these values.
9
- #
10
- # When included in a model, Bankable automatically sets up a polymorphic association
11
- # to the Midas::Coin model and provides class methods to define specific monetary
12
- # attributes.
13
- #
14
- # @example Basic usage
15
- # class Product < ApplicationRecord
16
- # include WhittakerTech::Midas::Bankable
17
- #
18
- # has_coin :price
19
- # end
20
- #
21
- # # Create and set a price
22
- # product = Product.create!
23
- # product.set_price(amount: 29.99, currency_code: 'USD')
24
- #
25
- # # Access the price
26
- # product.price # => Coin object
27
- # product.price_amount # => Money object
28
- # product.price_format # => "$29.99"
29
- # product.price_in(:eur) # => "€26.85" (if exchange rates available)
30
- #
31
- # @example Multiple coins
32
- # class Invoice < ApplicationRecord
33
- # include WhittakerTech::Midas::Bankable
34
- #
35
- # has_coins :subtotal, :tax, :total
36
- # end
37
- #
38
- # @example Custom dependency handling
39
- # class Order < ApplicationRecord
40
- # include WhittakerTech::Midas::Bankable
41
- #
42
- # has_coin :deposit, dependent: :nullify
43
- # end
44
- #
45
- # == Associations Created
46
- #
47
- # When included, the module automatically creates:
48
- # - `midas_coins`: A polymorphic has_many association to all Coin records
49
- # associated with this model instance
50
- #
51
- # == Methods Created by has_coin
52
- #
53
- # For each coin defined with `has_coin :name`, the following methods are created:
54
- #
55
- # - `name`: Returns the associated Coin object
56
- # - `name_amount`: Returns the Money object representing the amount
57
- # - `name_format`: Returns a formatted string representation of the amount
58
- # - `name_in(currency)`: Returns the amount converted to the specified currency
59
- # - `set_name(amount:, currency_code:)`: Sets the coin value with the given amount and currency
60
- #
61
- # == Supported Amount Types
62
- #
63
- # The `set_*` methods accept amounts in various formats:
64
- # - Money objects: Used directly for cents value
65
- # - Integer: Treated as cents/minor currency units
66
- # - Numeric: Converted to cents using currency-specific decimal places
3
+ # The Bankable module provides currency and monetary value management functionality
4
+ # for Active Record models. It allows models to have associated monetary values
5
+ # (coins) with different currencies and provides convenient methods for setting,
6
+ # retrieving, and formatting these values.
7
+ #
8
+ # When included in a model, Bankable automatically sets up a polymorphic association
9
+ # to the Midas::Coin model and provides class methods to define specific monetary
10
+ # attributes.
11
+ #
12
+ # @example Basic usage
13
+ # class Product < ApplicationRecord
14
+ # include WhittakerTech::Midas::Bankable
15
+ #
16
+ # has_coin :price
17
+ # end
18
+ #
19
+ # # Create and set a price
20
+ # product = Product.create!
21
+ # product.set_price(amount: 29.99, currency_code: 'USD')
22
+ #
23
+ # # Access the price
24
+ # product.price # => Coin object
25
+ # product.price_amount # => Money object
26
+ # product.price_format # => "$29.99"
27
+ #
28
+ # @example Multiple coins
29
+ # class Invoice < ApplicationRecord
30
+ # include WhittakerTech::Midas::Bankable
31
+ #
32
+ # has_coins :subtotal, :tax, :total
33
+ # end
34
+ #
35
+ # @example Custom dependency handling
36
+ # class Order < ApplicationRecord
37
+ # include WhittakerTech::Midas::Bankable
38
+ #
39
+ # has_coin :deposit, dependent: :nullify
40
+ # end
41
+ #
42
+ # == Associations Created
43
+ #
44
+ # When included, the module automatically creates:
45
+ # - `midas_coins`: A polymorphic has_many association to all Coin records
46
+ # associated with this model instance
47
+ #
48
+ # == Methods Created by has_coin
49
+ #
50
+ # For each coin defined with `has_coin :name`, the following methods are created:
51
+ #
52
+ # - `name`: Returns the associated Coin object
53
+ # - `name_amount`: Returns the Money object representing the amount
54
+ # - `name_format`: Returns a formatted string representation of the amount
55
+ # - `set_name(amount:, currency_code:)`: Sets the coin value with the given amount and currency
56
+ #
57
+ # == Supported Amount Types
58
+ #
59
+ # The `set_*` methods accept amounts in various formats:
60
+ # - Money objects: Used directly for cents value
61
+ # - Integer: Treated as cents/minor currency units
62
+ # - Numeric: Converted to cents using currency-specific decimal places
63
+ #
64
+ # == Currency Configuration
65
+ #
66
+ # The module uses I18n for currency-specific configuration:
67
+ # - `midas.ui.currencies.<ISO_CODE>.decimal_count`: Decimal places for specific currency
68
+ # - `midas.ui.defaults.decimal_count`: Default decimal places (defaults to 2)
69
+ #
70
+ # == Thread Safety
71
+ #
72
+ # This module is designed to be thread-safe when used with Rails' standard
73
+ # Active Record patterns.
74
+ #
75
+ # See also: `WhittakerTech::Midas::Coin`
76
+ # @since 0.1.0
77
+ module WhittakerTech::Midas::Bankable
78
+ extend ActiveSupport::Concern
79
+
80
+ included do
81
+ has_many :midas_coins,
82
+ as: :resource,
83
+ class_name: 'WhittakerTech::Midas::Coin',
84
+ dependent: :destroy
85
+ end
86
+
87
+ class_methods do
88
+ # Defines multiple coin attributes at once.
67
89
  #
68
- # == Currency Configuration
90
+ # @param names [Array<Symbol>] The names of the coin attributes to define
91
+ # @param dependent [Symbol] The dependency behavior when the parent record is destroyed
92
+ # (:destroy, :delete_all, :nullify, :restrict_with_exception, :restrict_with_error)
69
93
  #
70
- # The module uses I18n for currency-specific configuration:
71
- # - `midas.ui.currencies.{ISO_CODE}.decimal_count`: Decimal places for specific currency
72
- # - `midas.ui.defaults.decimal_count`: Default decimal places (defaults to 2)
94
+ # @example
95
+ # has_coins :price, :cost, :tax
96
+ # has_coins :deposit, :refund, dependent: :nullify
97
+ def has_coins(*names, dependent: :destroy)
98
+ names.each { |name| has_coin(name, dependent:) }
99
+ end
100
+
101
+ # Defines a single coin attribute with associated methods and database relationship.
73
102
  #
74
- # == Thread Safety
103
+ # This method creates:
104
+ # - A has_one association to the Coin model
105
+ # - Getter and setter methods for the coin
106
+ # - Helper methods for amount access and formatting
75
107
  #
76
- # This module is designed to be thread-safe when used with Rails' standard
77
- # Active Record patterns.
108
+ # @param name [Symbol] The name of the coin attribute
109
+ # @param dependent [Symbol] The dependency behavior when the parent record is destroyed
78
110
  #
79
- # @see WhittakerTech::Midas::Coin
80
- # @since 0.1.0
81
- module Bankable
82
- extend ActiveSupport::Concern
83
-
84
- included do
85
- has_many :midas_coins,
86
- as: :resource,
87
- class_name: 'WhittakerTech::Midas::Coin',
88
- dependent: :destroy
89
- end
111
+ # @example
112
+ # has_coin :price
113
+ # has_coin :refundable_deposit, dependent: :nullify
114
+ def has_coin(name, dependent: :destroy)
115
+ label = name.to_s
116
+ assoc_name = :"#{name}_coin"
90
117
 
91
- class_methods do
92
- # Defines multiple coin attributes at once.
93
- #
94
- # @param names [Array<Symbol>] The names of the coin attributes to define
95
- # @param dependent [Symbol] The dependency behavior when the parent record is destroyed
96
- # (:destroy, :delete_all, :nullify, :restrict_with_exception, :restrict_with_error)
97
- #
98
- # @example
99
- # has_coins :price, :cost, :tax
100
- # has_coins :deposit, :refund, dependent: :nullify
101
- def has_coins(*names, dependent: :destroy)
102
- names.each { |name| has_coin(name, dependent:) }
103
- end
104
-
105
- # Defines a single coin attribute with associated methods and database relationship.
106
- #
107
- # This method creates:
108
- # - A has_one association to the Coin model
109
- # - Getter and setter methods for the coin
110
- # - Helper methods for amount access and formatting
111
- #
112
- # @param name [Symbol] The name of the coin attribute
113
- # @param dependent [Symbol] The dependency behavior when the parent record is destroyed
114
- #
115
- # @example
116
- # has_coin :price
117
- # has_coin :refundable_deposit, dependent: :nullify
118
- def has_coin(name, dependent: :destroy)
119
- label = name.to_s
120
- assoc_name = :"#{name}_coin"
121
-
122
- has_one assoc_name,
123
- -> { where(resource_label: label) },
124
- as: :resource,
125
- class_name: 'WhittakerTech::Midas::Coin',
126
- dependent: dependent
127
-
128
- define_methods(name, label, assoc_name)
129
- end
130
-
131
- def define_methods(name, label, assoc_name)
132
- define_method(name) { public_send(assoc_name) }
133
-
134
- define_method("#{name}_amount") { public_send(name)&.amount }
135
- define_method("#{name}_format") { public_send(name)&.amount&.format }
136
- define_method("#{name}_in") { |to| public_send(name)&.exchange_to(to)&.format }
137
-
138
- # Sets the coin value with the specified amount and currency.
139
- #
140
- # @param amount [Money, Integer, Numeric] The amount to set
141
- # @param currency_code [String, Symbol] The ISO currency code (e.g., 'USD', 'EUR')
142
- # @return [Coin] The created or updated Coin object
143
- # @raise [ArgumentError] If the amount type is not supported
144
- #
145
- # @example
146
- # product.set_price(amount: 29.99, currency_code: 'USD')
147
- # product.set_price(amount: Money.new(2999, 'USD'), currency_code: 'USD')
148
- # product.set_price(amount: 2999, currency_code: 'USD') # 2999 cents
149
- define_method("set_#{name}") do |amount:, currency_code:|
150
- iso = currency_code.to_s.upcase
151
- coin = public_send(name) || public_send("build_#{assoc_name}", resource_label: label)
152
- coin.currency_code = iso
153
- coin.currency_minor = to_cents(name, amount, iso)
154
- coin.resource = self
155
- coin.save!
156
-
157
- coin
158
- end
159
- end
160
- end
118
+ has_one assoc_name,
119
+ -> { for_role(label) },
120
+ as: :resource,
121
+ class_name: 'WhittakerTech::Midas::Coin',
122
+ dependent: dependent
161
123
 
162
- private
163
-
164
- def to_cents(name, amount, iso)
165
- raise ArgumentError, "Invalid value for #{name}: #{amount.inspect}" unless is_valid_type?(amount)
124
+ define_methods(name, label, assoc_name)
125
+ end
166
126
 
167
- return amount.cents if amount.is_a? Money
168
- return amount if amount.is_a? Integer
127
+ def define_methods(name, label, assoc_name)
128
+ define_method(name) { public_send(assoc_name) }
169
129
 
170
- (BigDecimal(amount.to_s) * (10**decimals_for(iso))).round.to_i
171
- end
130
+ define_method("#{name}_amount") { public_send(name)&.amount }
131
+ define_method("#{name}_format") { public_send(name)&.amount&.format }
132
+ # define_method("#{name}_in") { |to| public_send(name)&.exchange_to(to)&.format }
172
133
 
173
- def is_valid_type?(amount)
174
- [Money, Integer, Numeric].any? { |klass| amount.is_a?(klass) }
175
- end
176
-
177
- # Determines the number of decimal places for a given currency.
134
+ # Sets the coin value with the specified amount and currency.
135
+ #
136
+ # @param amount [Money, Integer, Numeric] The amount to set
137
+ # @param currency_code [String, Symbol] The ISO currency code (e.g., 'USD', 'EUR')
138
+ # @return [Coin] The created or updated Coin object
139
+ # @raise [ArgumentError] If the amount type is not supported
178
140
  #
179
- # @param iso [String] The ISO currency code
180
- # @return [Integer] Number of decimal places (0-12)
181
- def decimals_for(iso)
182
- scope = 'midas.ui'
183
- per = I18n.t("#{scope}.currencies.#{iso}", default: {})
184
- default = I18n.t("#{scope}.defaults.decimal_count", default: 2)
185
- (per['decimal_count'] || default).to_i.clamp(0, 12)
141
+ # @example
142
+ # product.set_price(amount: 29.99, currency_code: 'USD')
143
+ # product.set_price(amount: Money.new(2999, 'USD'), currency_code: 'USD')
144
+ # product.set_price(amount: 2999, currency_code: 'USD') # 2999 cents
145
+ define_method("set_#{name}") do |amount:, currency_code:|
146
+ iso = currency_code.to_s.upcase
147
+ coin = public_send(name) || public_send("build_#{assoc_name}", resource_role: label)
148
+ coin.currency_code = iso
149
+ coin.currency_minor = to_cents(name, amount, iso)
150
+ coin.resource = self
151
+ coin.save!
152
+
153
+ coin
186
154
  end
187
155
  end
188
156
  end
157
+
158
+ private
159
+
160
+ # Converts an amount in various representations to an integer minor-unit count.
161
+ #
162
+ # Input conversion rules:
163
+ #
164
+ # - `Money`: Uses `money.cents` directly (already minor units)
165
+ # - `Integer`: Assumed to already be minor units; returned as-is
166
+ # - `Numeric`: Treated as major units; scaled by `10 ** decimals_for`
167
+ #
168
+ # @param name [Symbol] attribute name used in error messages
169
+ # @param amount [Money, Integer, Numeric] the amount to convert
170
+ # @param iso [String] uppercased ISO 4217 currency code
171
+ # @return [Integer] minor-unit count (e.g. cents)
172
+ # @raise [ArgumentError] if `amount` is not a supported type
173
+ def to_cents(name, amount, iso)
174
+ raise ArgumentError, "Invalid value for #{name}: #{amount.inspect}" unless is_valid_type?(amount)
175
+
176
+ return amount.cents if amount.is_a? Money
177
+ return amount if amount.is_a? Integer
178
+
179
+ (BigDecimal(amount.to_s) * (10**decimals_for(iso))).round.to_i
180
+ end
181
+
182
+ # Returns true if `amount` is one of the accepted input types.
183
+ # @param amount [Object]
184
+ # @return [Boolean]
185
+ def is_valid_type?(amount)
186
+ [Money, Integer, Numeric].any? { |klass| amount.is_a?(klass) }
187
+ end
188
+
189
+ # Determines the number of decimal places for a given currency.
190
+ #
191
+ # Reads from `midas.ui.currencies.<ISO>.decimal_count` in I18n, falling
192
+ # back to `midas.ui.defaults.decimal_count` (default: `2`). Clamped to
193
+ # `[0, 12]` to prevent pathological scaling.
194
+ #
195
+ # @param iso [String] The ISO currency code
196
+ # @return [Integer] Number of decimal places (0-12)
197
+ def decimals_for(iso)
198
+ scope = 'midas.ui'
199
+ per = I18n.t("#{scope}.currencies.#{iso}", default: {})
200
+ default = I18n.t("#{scope}.defaults.decimal_count", default: 2)
201
+ (per['decimal_count'] || default).to_i.clamp(0, 12)
202
+ end
189
203
  end
@@ -1,7 +1,3 @@
1
- module WhittakerTech
2
- module Midas
3
- class ApplicationRecord < ActiveRecord::Base
4
- self.abstract_class = true
5
- end
6
- end
1
+ class WhittakerTech::Midas::ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
7
3
  end