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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- 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 +186 -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.rb +32 -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 +314 -76
- 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/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 +69 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79ce2a5c7e59f15a9ca7af2ea759c514a32fcdc67915618d01d4f1cf2c00bd9a
|
|
4
|
+
data.tar.gz: 149f3fa609b856dffdef559835f668bb47758c00262d5cb2d4517ef21f3c1892
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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)
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+

|
|
5
|
+

|
|
6
6
|
[](https://badge.fury.io/rb/whittaker_tech-midas)
|
|
7
7
|
[](https://github.com/WhittakerTech/midas/actions)
|
|
8
8
|
|
|
@@ -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
|
|
@@ -1,189 +1,203 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
77
|
-
#
|
|
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
|
-
# @
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
127
|
+
def define_methods(name, label, assoc_name)
|
|
128
|
+
define_method(name) { public_send(assoc_name) }
|
|
169
129
|
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
#
|
|
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
|
-
# @
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
(
|
|
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
|