whittaker_tech-midas 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +75 -6
- data/app/controllers/whittaker_tech/midas/application_controller.rb +1 -5
- data/app/helpers/whittaker_tech/midas/application_helper.rb +1 -5
- data/app/helpers/whittaker_tech/midas/form_helper.rb +68 -72
- data/app/jobs/whittaker_tech/midas/application_job.rb +1 -5
- data/app/mailers/whittaker_tech/midas/application_mailer.rb +3 -7
- data/app/models/concerns/whittaker_tech/midas/bankable.rb +188 -172
- data/app/models/whittaker_tech/midas/application_record.rb +2 -6
- data/app/models/whittaker_tech/midas/coin/allocation.rb +124 -0
- data/app/models/whittaker_tech/midas/coin/arithmetic.rb +196 -0
- data/app/models/whittaker_tech/midas/coin/bidi.rb +87 -0
- data/app/models/whittaker_tech/midas/coin/converter/bank_provider.rb +35 -0
- data/app/models/whittaker_tech/midas/coin/converter.rb +79 -0
- data/app/models/whittaker_tech/midas/coin/parser.rb +104 -0
- data/app/models/whittaker_tech/midas/coin/presenter.rb +229 -0
- data/app/models/whittaker_tech/midas/coin.rb +285 -76
- data/app/models/whittaker_tech/midas/exchange.rb +37 -0
- data/db/migrate/20260101000000_create_whittaker_tech_schema.rb +26 -0
- data/db/migrate/{create_wt_midas_coins.rb → 20260101000001_create_midas_coins.rb} +7 -3
- data/db/migrate/20260219120000_rename_resource_label_to_resource_role_in_wt_midas_coins.rb +12 -0
- data/db/migrate/20260219150000_rename_wt_midas_coins_to_midas_coins.rb +13 -0
- data/db/migrate/20260707000000_create_midas_exchanges.rb +15 -0
- data/lib/generators/whittaker_tech/midas/install/install_generator.rb +5 -11
- data/lib/whittaker_tech/midas/deprecation.rb +32 -0
- data/lib/whittaker_tech/midas/engine.rb +113 -115
- data/lib/whittaker_tech/midas/version.rb +4 -4
- data/lib/whittaker_tech/midas.rb +120 -3
- metadata +72 -3
|
@@ -1,189 +1,205 @@
|
|
|
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
|
+
# - `name_in(currency_code)`: Converts and formats the coin in another currency
|
|
56
|
+
# (writes an Exchange audit row on every call — see `Coin::Converter`)
|
|
57
|
+
# - `set_name(amount:, currency_code:)`: Sets the coin value with the given amount and currency
|
|
58
|
+
#
|
|
59
|
+
# == Supported Amount Types
|
|
60
|
+
#
|
|
61
|
+
# The `set_*` methods accept amounts in various formats:
|
|
62
|
+
# - Money objects: Used directly for cents value
|
|
63
|
+
# - Integer: Treated as cents/minor currency units
|
|
64
|
+
# - Numeric: Converted to cents using currency-specific decimal places
|
|
65
|
+
#
|
|
66
|
+
# == Currency Configuration
|
|
67
|
+
#
|
|
68
|
+
# The module uses I18n for currency-specific configuration:
|
|
69
|
+
# - `midas.ui.currencies.<ISO_CODE>.decimal_count`: Decimal places for specific currency
|
|
70
|
+
# - `midas.ui.defaults.decimal_count`: Default decimal places (defaults to 2)
|
|
71
|
+
#
|
|
72
|
+
# == Thread Safety
|
|
73
|
+
#
|
|
74
|
+
# This module is designed to be thread-safe when used with Rails' standard
|
|
75
|
+
# Active Record patterns.
|
|
76
|
+
#
|
|
77
|
+
# See also: `WhittakerTech::Midas::Coin`
|
|
78
|
+
# @since 0.1.0
|
|
79
|
+
module WhittakerTech::Midas::Bankable
|
|
80
|
+
extend ActiveSupport::Concern
|
|
81
|
+
|
|
82
|
+
included do
|
|
83
|
+
has_many :midas_coins,
|
|
84
|
+
as: :resource,
|
|
85
|
+
class_name: 'WhittakerTech::Midas::Coin',
|
|
86
|
+
dependent: :destroy
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class_methods do
|
|
90
|
+
# Defines multiple coin attributes at once.
|
|
67
91
|
#
|
|
68
|
-
#
|
|
92
|
+
# @param names [Array<Symbol>] The names of the coin attributes to define
|
|
93
|
+
# @param dependent [Symbol] The dependency behavior when the parent record is destroyed
|
|
94
|
+
# (:destroy, :delete_all, :nullify, :restrict_with_exception, :restrict_with_error)
|
|
69
95
|
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
96
|
+
# @example
|
|
97
|
+
# has_coins :price, :cost, :tax
|
|
98
|
+
# has_coins :deposit, :refund, dependent: :nullify
|
|
99
|
+
def has_coins(*names, dependent: :destroy)
|
|
100
|
+
names.each { |name| has_coin(name, dependent:) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Defines a single coin attribute with associated methods and database relationship.
|
|
73
104
|
#
|
|
74
|
-
#
|
|
105
|
+
# This method creates:
|
|
106
|
+
# - A has_one association to the Coin model
|
|
107
|
+
# - Getter and setter methods for the coin
|
|
108
|
+
# - Helper methods for amount access and formatting
|
|
75
109
|
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
110
|
+
# @param name [Symbol] The name of the coin attribute
|
|
111
|
+
# @param dependent [Symbol] The dependency behavior when the parent record is destroyed
|
|
78
112
|
#
|
|
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
|
|
113
|
+
# @example
|
|
114
|
+
# has_coin :price
|
|
115
|
+
# has_coin :refundable_deposit, dependent: :nullify
|
|
116
|
+
def has_coin(name, dependent: :destroy)
|
|
117
|
+
label = name.to_s
|
|
118
|
+
assoc_name = :"#{name}_coin"
|
|
90
119
|
|
|
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
|
|
120
|
+
has_one assoc_name,
|
|
121
|
+
-> { for_role(label) },
|
|
122
|
+
as: :resource,
|
|
123
|
+
class_name: 'WhittakerTech::Midas::Coin',
|
|
124
|
+
dependent: dependent
|
|
161
125
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def to_cents(name, amount, iso)
|
|
165
|
-
raise ArgumentError, "Invalid value for #{name}: #{amount.inspect}" unless is_valid_type?(amount)
|
|
126
|
+
define_methods(name, label, assoc_name)
|
|
127
|
+
end
|
|
166
128
|
|
|
167
|
-
|
|
168
|
-
|
|
129
|
+
def define_methods(name, label, assoc_name)
|
|
130
|
+
define_method(name) { public_send(assoc_name) }
|
|
169
131
|
|
|
170
|
-
|
|
171
|
-
|
|
132
|
+
define_method("#{name}_amount") { public_send(name)&.amount }
|
|
133
|
+
define_method("#{name}_format") { public_send(name)&.amount&.format }
|
|
134
|
+
define_method("#{name}_in") { |to| public_send(name)&.exchange_to(to)&.format }
|
|
172
135
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
#
|
|
136
|
+
# Sets the coin value with the specified amount and currency.
|
|
137
|
+
#
|
|
138
|
+
# @param amount [Money, Integer, Numeric] The amount to set
|
|
139
|
+
# @param currency_code [String, Symbol] The ISO currency code (e.g., 'USD', 'EUR')
|
|
140
|
+
# @return [Coin] The created or updated Coin object
|
|
141
|
+
# @raise [ArgumentError] If the amount type is not supported
|
|
178
142
|
#
|
|
179
|
-
# @
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
(
|
|
143
|
+
# @example
|
|
144
|
+
# product.set_price(amount: 29.99, currency_code: 'USD')
|
|
145
|
+
# product.set_price(amount: Money.new(2999, 'USD'), currency_code: 'USD')
|
|
146
|
+
# product.set_price(amount: 2999, currency_code: 'USD') # 2999 cents
|
|
147
|
+
define_method("set_#{name}") do |amount:, currency_code:|
|
|
148
|
+
iso = currency_code.to_s.upcase
|
|
149
|
+
coin = public_send(name) || public_send("build_#{assoc_name}", resource_role: label)
|
|
150
|
+
coin.currency_code = iso
|
|
151
|
+
coin.currency_minor = to_cents(name, amount, iso)
|
|
152
|
+
coin.resource = self
|
|
153
|
+
coin.save!
|
|
154
|
+
|
|
155
|
+
coin
|
|
186
156
|
end
|
|
187
157
|
end
|
|
188
158
|
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
# Converts an amount in various representations to an integer minor-unit count.
|
|
163
|
+
#
|
|
164
|
+
# Input conversion rules:
|
|
165
|
+
#
|
|
166
|
+
# - `Money`: Uses `money.cents` directly (already minor units)
|
|
167
|
+
# - `Integer`: Assumed to already be minor units; returned as-is
|
|
168
|
+
# - `Numeric`: Treated as major units; scaled by `10 ** decimals_for`
|
|
169
|
+
#
|
|
170
|
+
# @param name [Symbol] attribute name used in error messages
|
|
171
|
+
# @param amount [Money, Integer, Numeric] the amount to convert
|
|
172
|
+
# @param iso [String] uppercased ISO 4217 currency code
|
|
173
|
+
# @return [Integer] minor-unit count (e.g. cents)
|
|
174
|
+
# @raise [ArgumentError] if `amount` is not a supported type
|
|
175
|
+
def to_cents(name, amount, iso)
|
|
176
|
+
raise ArgumentError, "Invalid value for #{name}: #{amount.inspect}" unless is_valid_type?(amount)
|
|
177
|
+
|
|
178
|
+
return amount.cents if amount.is_a? Money
|
|
179
|
+
return amount if amount.is_a? Integer
|
|
180
|
+
|
|
181
|
+
(BigDecimal(amount.to_s) * (10**decimals_for(iso))).round.to_i
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Returns true if `amount` is one of the accepted input types.
|
|
185
|
+
# @param amount [Object]
|
|
186
|
+
# @return [Boolean]
|
|
187
|
+
def is_valid_type?(amount)
|
|
188
|
+
[Money, Integer, Numeric].any? { |klass| amount.is_a?(klass) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Determines the number of decimal places for a given currency.
|
|
192
|
+
#
|
|
193
|
+
# Reads from `midas.ui.currencies.<ISO>.decimal_count` in I18n, falling
|
|
194
|
+
# back to `midas.ui.defaults.decimal_count` (default: `2`). Clamped to
|
|
195
|
+
# `[0, 12]` to prevent pathological scaling.
|
|
196
|
+
#
|
|
197
|
+
# @param iso [String] The ISO currency code
|
|
198
|
+
# @return [Integer] Number of decimal places (0-12)
|
|
199
|
+
def decimals_for(iso)
|
|
200
|
+
scope = 'midas.ui'
|
|
201
|
+
per = I18n.t("#{scope}.currencies.#{iso}", default: {})
|
|
202
|
+
default = I18n.t("#{scope}.defaults.decimal_count", default: 2)
|
|
203
|
+
(per['decimal_count'] || default).to_i.clamp(0, 12)
|
|
204
|
+
end
|
|
189
205
|
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Allocation represents a per-unit interpretation of a `Coin`.
|
|
4
|
+
#
|
|
5
|
+
# ## Conceptual model
|
|
6
|
+
#
|
|
7
|
+
# ```
|
|
8
|
+
# Coin -> canonical, payable money (what exists)
|
|
9
|
+
# Allocation -> "this Coin, spread across N units, using policy X"
|
|
10
|
+
# ```
|
|
11
|
+
#
|
|
12
|
+
# An Allocation answers two questions:
|
|
13
|
+
#
|
|
14
|
+
# 1. **What does one unit cost?** — `#value`
|
|
15
|
+
# 2. **What does a given quantity cost?** — `#price`
|
|
16
|
+
#
|
|
17
|
+
# ## Immutability
|
|
18
|
+
#
|
|
19
|
+
# Allocation is:
|
|
20
|
+
# - **immutable** — `@coin`, `@divisor`, and `@rounding_policy` are frozen on
|
|
21
|
+
# construction and never mutated.
|
|
22
|
+
# - **not persisted** — Allocation is a pure value object.
|
|
23
|
+
# - **minor-unit safe** — it never stores sub-minor-unit amounts; rounding is
|
|
24
|
+
# applied before returning a Coin.
|
|
25
|
+
#
|
|
26
|
+
# @example Per-unit price from a bulk price
|
|
27
|
+
# bulk = Coin.value(10_000, 'USD') # $100.00 for a pack of 6
|
|
28
|
+
# alloc = bulk.allocate(per: 6, rounding_policy: :ceil)
|
|
29
|
+
# alloc.value # => Coin($16.67)
|
|
30
|
+
# alloc.price(qty: 2) # => Coin($33.34)
|
|
31
|
+
#
|
|
32
|
+
# See also: `Coin#allocate`
|
|
33
|
+
# @since 0.1.0
|
|
34
|
+
class WhittakerTech::Midas::Coin::Allocation
|
|
35
|
+
# @return [Coin] the total monetary amount this Allocation is based on
|
|
36
|
+
attr_reader :coin
|
|
37
|
+
|
|
38
|
+
# @return [Numeric] the number of units the coin covers (the divisor)
|
|
39
|
+
attr_reader :divisor
|
|
40
|
+
|
|
41
|
+
# @return [Symbol] the rounding policy applied to per-unit calculations
|
|
42
|
+
attr_reader :rounding_policy
|
|
43
|
+
|
|
44
|
+
# Alias for `#divisor`. Reads as "this coin, per N units".
|
|
45
|
+
alias per divisor
|
|
46
|
+
|
|
47
|
+
# Delegate invariant facts from the underlying Coin.
|
|
48
|
+
# These values do not change under scaling.
|
|
49
|
+
delegate :currency_code,
|
|
50
|
+
:currency,
|
|
51
|
+
:decimals,
|
|
52
|
+
:scale,
|
|
53
|
+
:zero?,
|
|
54
|
+
:positive?,
|
|
55
|
+
:negative?,
|
|
56
|
+
to: :coin
|
|
57
|
+
|
|
58
|
+
# @param coin [Coin] the total monetary value
|
|
59
|
+
# @param divisor [Numeric] the number of units; must be positive
|
|
60
|
+
# @param rounding_policy [Symbol] one of `WhittakerTech::Midas::ROUNDING_POLICIES`
|
|
61
|
+
# @raise [TypeError] if `coin` is not a `Coin`
|
|
62
|
+
# @raise [TypeError] if `divisor` is not a positive Numeric
|
|
63
|
+
# @raise [ArgumentError] if `rounding_policy` is not a recognised policy key
|
|
64
|
+
def initialize(coin:, divisor:, rounding_policy:)
|
|
65
|
+
raise TypeError unless coin.is_a?(WhittakerTech::Midas::Coin)
|
|
66
|
+
raise TypeError unless divisor.is_a?(Numeric) && divisor.positive?
|
|
67
|
+
raise ArgumentError unless WhittakerTech::Midas::ROUNDING_POLICIES.key?(rounding_policy)
|
|
68
|
+
|
|
69
|
+
@coin = coin.freeze
|
|
70
|
+
@divisor = divisor
|
|
71
|
+
@rounding_policy = rounding_policy.freeze
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns the per-unit Coin after dividing by `#divisor` and applying the
|
|
75
|
+
# `#rounding_policy`.
|
|
76
|
+
#
|
|
77
|
+
# The result is still a `Coin` — payable money representing a single unit.
|
|
78
|
+
#
|
|
79
|
+
# @return [Coin]
|
|
80
|
+
def value
|
|
81
|
+
coin.divide(divisor, rounding_policy: rounding_policy)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns the total price for a given quantity of units.
|
|
85
|
+
#
|
|
86
|
+
# Uses `(total_minor * qty) / divisor` rounded via `#rounding_policy`.
|
|
87
|
+
# Equivalent to `qty` units at the per-unit rate, but computed from the
|
|
88
|
+
# original total to minimise accumulated rounding error.
|
|
89
|
+
#
|
|
90
|
+
# @param qty [Numeric] the number of units; must be >= 0
|
|
91
|
+
# @return [Coin]
|
|
92
|
+
# @raise [ArgumentError] if `qty` is negative or not Numeric
|
|
93
|
+
#
|
|
94
|
+
# @example
|
|
95
|
+
# alloc = Coin.value(10_000, 'USD').allocate(per: 6)
|
|
96
|
+
# alloc.price(qty: 3) # => Coin($50.00) (10000 * 3 / 6 = 5000 cents)
|
|
97
|
+
def price(qty: 1)
|
|
98
|
+
raise ArgumentError unless qty.is_a?(Numeric) && qty >= 0
|
|
99
|
+
|
|
100
|
+
WhittakerTech::Midas::Coin.value(
|
|
101
|
+
round((coin.currency_minor * qty) / divisor),
|
|
102
|
+
coin.currency_code
|
|
103
|
+
).freeze
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Human-readable representation for logs and console output.
|
|
107
|
+
# @return [String]
|
|
108
|
+
def inspect
|
|
109
|
+
"#<#{self.class.name} coin=#{coin.inspect} divisor=#{divisor} rounding_policy=#{rounding_policy}>"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# Applies the configured `#rounding_policy` to a raw Numeric amount and
|
|
115
|
+
# returns an Integer suitable for Coin construction.
|
|
116
|
+
# @param amount [Numeric]
|
|
117
|
+
# @return [Integer]
|
|
118
|
+
def round(amount)
|
|
119
|
+
WhittakerTech::Midas::ROUNDING_POLICIES
|
|
120
|
+
.fetch(rounding_policy)
|
|
121
|
+
.call(amount)
|
|
122
|
+
.to_i
|
|
123
|
+
end
|
|
124
|
+
end
|