Class WhittakerTech::Midas::Coin ¶
Inherits: WhittakerTech::Midas::ApplicationRecord
Includes: Poly::Joins, Poly::Owners, Poly::Role, WhittakerTech::Midas::Coin::Arithmetic, WhittakerTech::Midas::Coin::Bidi, WhittakerTech::Midas::Coin::Converter, WhittakerTech::Midas::Coin::Presenter
Coin is the canonical, persisted representation of a monetary value.
Conceptual model¶
- A Coin represents payable money: an exact integer count of minor units (cents, pence, etc.) in a specific currency.
- A Coin is immutable by convention: arithmetic operations return new frozen Coins rather than mutating the receiver.
- A Coin owns arithmetic truth — not presentation, pricing interpretation, or currency conversion.
Storage¶
Every Coin is persisted in midas_coins and belongs polymorphically to any
domain object (resource_type / resource_id). The resource_role
distinguishes multiple coins on the same resource (e.g. "price", "cost",
"tax").
Modules¶
Behavior is composed via mixins:
+------------+----------------------------------------------+
| Module | Responsibility |
+------------+----------------------------------------------+
| Arithmetic | +, -, *, /, %, negate, equality |
| Bidi | Unicode bidirectional text isolation |
| Converter | Currency conversion (reserved, not yet live) |
| Presenter | Token-based formatting grammar |
+------------+----------------------------------------------+
Usage via Bankable¶
The typical entry point is the WhittakerTech::Midas::Bankable
concern; direct Coin construction is mostly used in service objects and tests.
product.set_price(amount: 29.99, currency_code: 'USD')
product.price # => #<WhittakerTech::Midas::Coin ...>
product.price_amount # => #<Money @fractional=2999 @currency="USD">
See also:
WhittakerTech::Midas::BankableWhittakerTech::Midas::Coin::Arithmetic-
WhittakerTech::Midas::Coin::Allocationrubocop:disable Metrics/ClassLength -
@since 0.1.0
Public Class Methods¶
for_label(label) ¶
- @deprecated Use
for_roleinstead. Will be removed in v0.3.0. - @since 0.1.0
parse(value, currency_code: = nil) ¶
Parses a heterogeneous input into a Coin using Coin::Parser.
Accepted input types: Coin, Money, Numeric, String.
- @param
value[Coin, Money, Numeric, String] the value to parse - @param
currency_code[String, nil] required for Numeric and bare String inputs - @raise [TypeError] if the input type cannot be converted
- @raise [ArgumentError] if a currency_code is required but not provided
- @return [Coin]
- @since 0.1.0
@example
Coin.parse(Money.new(2999, 'USD'))
Coin.parse(29.99, currency_code: 'USD')
Coin.parse('$29.99')
value(currency_minor, currency_code) ¶
Constructs a Coin directly from an integer minor-unit count and an ISO currency code.
This is the lowest-level factory. It enforces that currency_minor is an
Integer (fractional minor units are not permitted).
- @param
currency_minor[Integer] the amount in minor units (e.g., cents) - @param
currency_code[String] ISO 4217 currency code, e.g."USD" - @raise [TypeError] if
currency_minoris not an Integer - @return [Coin]
- @since 0.1.0
@example
Coin.value(2999, 'USD') # => $29.99
Coin.value(0, 'JPY') # => ¥0
zero(currency_code) ¶
Returns a zero-valued Coin in the given currency.
- @param
currency_code[String] ISO 4217 currency code - @return [Coin]
- @since 0.1.0
@example
Coin.zero('USD') # => $0.00
Public Instance Methods¶
%(other) ¶
Returns the remainder after dividing by an integer.
- @param
other[Integer] the divisor - @raise [TypeError] if
otheris not an Integer - @raise [ZeroDivisionError] if
otheris zero - @return [Coin] a new, frozen Coin representing the remainder
- @since 0.1.0
@example
Coin.value(1001, 'USD') % 3 # => Coin($0.02) (1001 % 3 == 2 cents)
*(other) ¶
Multiplies the Coin by an integer scalar.
- @param
other[Integer] the multiplier - @raise [TypeError] if
otheris not an Integer - @return [Coin] a new, frozen Coin
- @since 0.1.0
@example
Coin.value(500, 'USD') * 3 # => Coin($15.00)
+(other) ¶
Adds two Coins and returns the sum as a new Coin.
- @param
other[Coin] must have the same currency code as the receiver - @raise [TypeError] if
otheris not aCoin - @raise [ArgumentError] if the currencies do not match
- @return [Coin] a new, frozen Coin
- @since 0.1.0
@example
a = Coin.value(1000, 'USD')
b = Coin.value(500, 'USD')
a + b # => Coin($15.00)
-(other) ¶
Subtracts another Coin from the receiver and returns the difference.
- @param
other[Coin] must have the same currency code as the receiver - @raise [TypeError] if
otheris not aCoin - @raise [ArgumentError] if the currencies do not match
- @return [Coin] a new, frozen Coin
- @since 0.1.0
@example
a = Coin.value(1000, 'USD')
b = Coin.value(300, 'USD')
a - b # => Coin($7.00)
-@() ¶
Unary minus operator — shorthand for #negate.
- @return [Coin]
- @since 0.1.0
==(other) ¶
Tests value equality with another Coin.
Two Coins are equal when they have the same currency code and the same minor-unit amount. A Coin is never equal to a non-Coin object.
- @param
other[Object] the object to compare - @return [Boolean]
- @since 0.1.0
allocate(per:, rounding_policy: = WhittakerTech::Midas::DEFAULT_ROUNDING_POLICY) ¶
Constructs a Coin::Allocation that interprets this Coin as a
per-unit price.
The Coin itself is unchanged; Coin::Allocation encapsulates the
per-unit interpretation and rounding policy.
- @param
per[Numeric] number of units this Coin covers (the divisor) - @param
rounding_policy[Symbol] one ofWhittakerTech::Midas::ROUNDING_POLICIES - @return [Coin::Allocation]
- @since 0.1.0
@example Price per item from a bulk price
bulk = Coin.value(10_000, 'USD') # $100.00 for 6 units
alloc = bulk.allocate(per: 6, rounding_policy: :ceil)
alloc.value # => Coin($16.67)
alloc.price(qty: 3) # => Coin($50.00)
amount() ¶
Returns a Money object representing the stored monetary value.
This is a projection, not canonical value. Use
#currency_minor and #currency_code as the source of
truth.
Memoized for performance. The memo is cleared automatically when
#currency_minor= or #currency_code= are called.
- @return [Money]
- @since 0.1.0
amount=(value) ¶
Sets the coin's monetary value from a Money object or raw minor-unit integer.
- @param
value[Money, Integer, Numeric] the value to assign Money— copiescentsandcurrency.iso_codedirectly.-
Numeric— treated as already-scaled minor units; requires#currency_codeto already be set. -
@raise [ArgumentError] if a Numeric is given without a prior currency_code
- @raise [ArgumentError] if the value type is not supported
- @return [void]
- @since 0.1.0
bidi_currency_dir(currency_code) ¶
Resolves the configured display direction for a currency code.
Reads from WhittakerTech::Midas.currency_directions. Defaults to
:ltr for any currency not explicitly configured.
- @param
currency_code[String] ISO 4217 currency code - @return [Symbol]
:ltror:rtl - @since 0.1.0
bidi_isolate(text, dir:) ¶
Wraps text in a Unicode directional-isolate span.
- @param
text[String, nil] the text to isolate;nilreturns"" - @param
dir[Symbol, nil]:ltr,:rtl, ornil(no wrapping) - @raise [ArgumentError] if
diris not:ltr,:rtl, ornil - @return [String]
- @since 0.1.0
@example
bidi_isolate('$29.99', dir: :ltr) # => "\u2066$29.99\u2069"
bidi_isolate('29.99', dir: nil) # => "29.99"
bidi_isolate_number(text) ¶
Wraps a number string with LTR isolation.
Numbers are always rendered LTR. This prevents the bidi algorithm from reordering digit sequences when they appear in an RTL context.
- @param
text[String, Integer, BigDecimal] the numeric value to isolate - @return [String]
- @since 0.1.0
convert_to(currency_code, at: = Time.current, using: = nil) ¶
Converts this Coin to another currency.
- @param
currency_code[String] target ISO 4217 currency code - @param
at[Time] the rate timestamp (for historical conversions) - @param
using[Object, nil] exchange rate provider / bank override - @raise [NotImplementedError] — not yet implemented
- @return [Coin]
- @since 0.1.0
currency() ¶
- @return [String] the ISO currency code (alias for
#currency_code) - @since 0.1.0
currency_code=(value) ¶
Clears the memoized Money projection when the currency code changes.
- @param
value[String] - @return [void]
- @since 0.1.0
currency_minor=(value) ¶
Clears the memoized Money projection when the minor-unit value changes.
- @param
value[Integer] - @return [void]
- @since 0.1.0
decimals() ¶
Returns the number of decimal places defined by the currency specification.
For example, USD = 2, JPY = 0, BHD = 3 This is informational and does not imply rounding or formatting.
- @return [Integer]
- @since 0.1.0
divide(divisor, rounding_policy: = WhittakerTech::Midas::DEFAULT_ROUNDING_POLICY) ¶
Divides the Coin by a numeric divisor and returns the quotient.
Division is the only arithmetic operation that requires a rounding policy because dividing integer minor units can produce a non-integer result.
- @param
divisor[Numeric] the divisor; must be non-zero -
@param
rounding_policy[Symbol] one of the keys inWhittakerTech::Midas::ROUNDING_POLICIES(default:WhittakerTech::Midas::DEFAULT_ROUNDING_POLICY) -
@raise [TypeError] if
divisoris not Numeric - @raise [ZeroDivisionError] if
divisoris zero - @return [Coin] a new, frozen Coin
- @since 0.1.0
@example
Coin.value(1000, 'USD').divide(3, rounding_policy: :ceil) # => Coin($3.34)
Coin.value(1000, 'USD').divide(3, rounding_policy: :floor) # => Coin($3.33)
Coin.value(1000, 'USD').divide(3, rounding_policy: :bankers)# => Coin($3.33)
format(to: = nil) ¶
Formats the Coin for display, optionally converting to another currency first.
This is a convenience wrapper around the Money gem's #format. For
richer formatting use #present with a pattern string.
-
@param
to[String, nil] target ISO 4217 currency code for conversion, ornilto format in the native currency. -
@return [String] the formatted monetary string, e.g.
"$29.99" - @since 0.1.0
fractional() ¶
- @return [Integer] the raw minor-unit count (alias for
#currency_minor) - @since 0.1.0
hash() ¶
Returns a hash value consistent with #eql?.
Coins with the same currency code and minor-unit amount produce the same hash, making them interchangeable as Hash keys or Set members.
- @return [Integer]
- @since 0.1.0
major() ¶
Returns the major-unit value as a precise BigDecimal.
This is NOT payable money. It is intended for inspection and formatting only. No rounding policy is applied.
- @return [BigDecimal]
- @since 0.1.0
@example
Coin.value(2999, 'USD').major # => BigDecimal("29.99")
Coin.value(100, 'JPY').major # => BigDecimal("100")
minor() ¶
- @return [Integer] the raw minor-unit count (alias for
#currency_minor) - @since 0.1.0
negate() ¶
Returns a new Coin with the sign reversed.
- @return [Coin] a new, frozen Coin
- @since 0.1.0
@example
Coin.value(500, 'USD').negate # => Coin(-$5.00)
present(pattern) ¶
Renders this Coin using a format pattern.
- @param
pattern[String] the format pattern containing%xtokens - @param
opts[Hash] optional rendering context overrides (see module docs) - @raise [ArgumentError] if the pattern contains an unknown or unterminated token
- @return [String]
- @since 0.1.0
@example
coin.present('%s%M') # => "$29.99"
coin.present('%t (%c)') # => "$29.99 (USD)"
coin.present('~%t', approx: true) # => "≈$29.99"
resource_label() ¶
- @deprecated Use
#resource_roleinstead. Will be removed in v0.3.0. - @since 0.1.0
resource_label=(value) ¶
- @deprecated Use
#resource_role=instead. Will be removed in v0.3.0. - @since 0.1.0
scale() ¶
Returns the scaling factor used to convert major units to minor units.
Equivalent to 10 ** decimals. For USD this is 100; for JPY it is 1.
- @return [Numeric]
- @since 0.1.0