Class: WhittakerTech::Midas::Coin

Inherits:
ApplicationRecord show all
Includes:
Poly::Joins, Poly::Owners, Poly::Role, Arithmetic, Bidi, Converter, Presenter
Defined in:
app/models/whittaker_tech/midas/coin.rb

Overview

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 (live, audited via Exchange) |
| 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::Bankable
  • WhittakerTech::Midas::Coin::Arithmetic
  • WhittakerTech::Midas::Coin::Allocation

Since:

  • 0.1.0

Defined Under Namespace

Modules: Arithmetic, Bidi, Converter, Presenter Classes: Allocation, Parser

Constant Summary

Constants included from Presenter

Presenter::TOKEN_MAP

Constants included from Bidi

Bidi::LRI, Bidi::PDI, Bidi::RLI

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Presenter

build_context, dispatch, format, #present, scan, token_approx, token_currency_code, token_currency_symbol, token_major, token_minor, token_number_only, token_per_exact, token_percent, token_total, token_units

Methods included from Converter

#convert_to

Methods included from Bidi

#bidi_currency_dir, #bidi_isolate, #bidi_isolate_number

Methods included from Arithmetic

#%, #*, #+, #-, #-@, #==, #divide, #hash, #negate

Class Method Details

.parse(value, currency_code: nil) ⇒ Coin

Parses a heterogeneous input into a Coin using Coin::Parser.

Accepted input types: Coin, Money, Numeric, String.

Examples:

Coin.parse(Money.new(2999, 'USD'))
Coin.parse(29.99, currency_code: 'USD')
Coin.parse('$29.99')

Parameters:

  • value (Coin, Money, Numeric, String)

    the value to parse

  • currency_code (String, nil) (defaults to: nil)

    required for Numeric and bare String inputs

Returns:

Raises:

  • (TypeError)

    if the input type cannot be converted

  • (ArgumentError)

    if a currency_code is required but not provided

Since:

  • 0.1.0



275
276
277
# File 'app/models/whittaker_tech/midas/coin.rb', line 275

def parse(value, currency_code: nil)
  Parser.parse(value, currency_code:)
end

.value(currency_minor, currency_code) ⇒ Coin

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).

Examples:

Coin.value(2999, 'USD')  # => $29.99
Coin.value(0, 'JPY')     # => ¥0

Parameters:

  • currency_minor (Integer)

    the amount in minor units (e.g., cents)

  • currency_code (String)

    ISO 4217 currency code, e.g. "USD"

Returns:

Raises:

  • (TypeError)

    if currency_minor is not an Integer

Since:

  • 0.1.0



244
245
246
247
248
# File 'app/models/whittaker_tech/midas/coin.rb', line 244

def value(currency_minor, currency_code)
  raise TypeError unless currency_minor.is_a?(Integer)

  new(currency_minor:, currency_code:)
end

.zero(currency_code) ⇒ Coin

Returns a zero-valued Coin in the given currency.

Examples:

Coin.zero('USD')  # => $0.00

Parameters:

  • currency_code (String)

    ISO 4217 currency code

Returns:

Since:

  • 0.1.0



257
258
259
# File 'app/models/whittaker_tech/midas/coin.rb', line 257

def zero(currency_code)
  new(currency_minor: 0, currency_code:)
end

Instance Method Details

#allocate(per:, rounding_policy: WhittakerTech::Midas::DEFAULT_ROUNDING_POLICY) ⇒ Coin::Allocation

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.

Examples:

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)

Parameters:

  • per (Numeric)

    number of units this Coin covers (the divisor)

  • rounding_policy (Symbol) (defaults to: WhittakerTech::Midas::DEFAULT_ROUNDING_POLICY)

    one of WhittakerTech::Midas::ROUNDING_POLICIES

Returns:

Since:

  • 0.1.0



188
189
190
191
192
193
194
# File 'app/models/whittaker_tech/midas/coin.rb', line 188

def allocate(per:, rounding_policy: WhittakerTech::Midas::DEFAULT_ROUNDING_POLICY)
  WhittakerTech::Midas::Coin::Allocation.new(
    coin: self,
    divisor: per,
    rounding_policy:
  )
end

#amountMoney

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.

Returns:

  • (Money)

Since:

  • 0.1.0



97
98
99
# File 'app/models/whittaker_tech/midas/coin.rb', line 97

def amount
  @amount ||= Money.new(currency_minor, currency_code)
end

#amount=(value) ⇒ void

This method returns an undefined value.

Sets the coin's monetary value from a Money object or raw minor-unit integer.

Parameters:

  • value (Money, Integer, Numeric)

    the value to assign

    • Money — copies cents and currency.iso_code directly.
    • Numeric — treated as already-scaled minor units; requires #currency_code to already be set.

Raises:

  • (ArgumentError)

    if a Numeric is given without a prior currency_code

  • (ArgumentError)

    if the value type is not supported

Since:

  • 0.1.0



110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'app/models/whittaker_tech/midas/coin.rb', line 110

def amount=(value)
  case value
  when Money
    self.currency_minor = value.cents
    self.currency_code = value.currency.iso_code
  when Numeric
    raise ArgumentError, 'currency_code required before setting numeric amount' if currency_code.blank?

    self.currency_minor = Integer(value)
  else
    raise ArgumentError, "Invalid value for Coin#amount: #{value.inspect}"
  end
end

#currencyString

Returns the ISO currency code (alias for #currency_code).

Returns:

  • (String)

    the ISO currency code (alias for #currency_code)

Since:

  • 0.1.0



149
150
151
# File 'app/models/whittaker_tech/midas/coin.rb', line 149

def currency
  currency_code
end

#currency_code=(value) ⇒ void

This method returns an undefined value.

Clears the memoized Money projection when the currency code changes.

Parameters:

  • value (String)

Since:

  • 0.1.0



169
170
171
172
# File 'app/models/whittaker_tech/midas/coin.rb', line 169

def currency_code=(value)
  @amount = nil
  super
end

#currency_minor=(value) ⇒ void

This method returns an undefined value.

Clears the memoized Money projection when the minor-unit value changes.

Parameters:

  • value (Integer)

Since:

  • 0.1.0



161
162
163
164
# File 'app/models/whittaker_tech/midas/coin.rb', line 161

def currency_minor=(value)
  @amount = nil
  super
end

#decimalsInteger

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.

Returns:

  • (Integer)

Since:

  • 0.1.0



202
203
204
# File 'app/models/whittaker_tech/midas/coin.rb', line 202

def decimals
  Money::Currency.new(currency_code).decimal_places
end

#format(to: nil) ⇒ String

Note:

When to is given, this performs a live conversion (via #convert_to) and writes an Exchange audit row on every call. If formatting the same converted value repeatedly (e.g. in a view loop), convert once and reuse the result's #amount.format instead.

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.

Parameters:

  • to (String, nil) (defaults to: nil)

    target ISO 4217 currency code for conversion, or nil to format in the native currency.

Returns:

  • (String)

    the formatted monetary string, e.g. "$29.99"

Since:

  • 0.1.0



137
138
139
140
141
# File 'app/models/whittaker_tech/midas/coin.rb', line 137

def format(to: nil)
  return amount.format unless to

  convert_to(to).amount.format
end

#fractionalInteger

Returns the raw minor-unit count (alias for #currency_minor).

Returns:

  • (Integer)

    the raw minor-unit count (alias for #currency_minor)

Since:

  • 0.1.0



154
155
156
# File 'app/models/whittaker_tech/midas/coin.rb', line 154

def fractional
  currency_minor
end

#majorBigDecimal

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.

Examples:

Coin.value(2999, 'USD').major  # => BigDecimal("29.99")
Coin.value(100, 'JPY').major   # => BigDecimal("100")

Returns:

  • (BigDecimal)

Since:

  • 0.1.0



225
226
227
# File 'app/models/whittaker_tech/midas/coin.rb', line 225

def major
  BigDecimal(currency_minor) / scale
end

#minorInteger

Returns the raw minor-unit count (alias for #currency_minor).

Returns:

  • (Integer)

    the raw minor-unit count (alias for #currency_minor)

Since:

  • 0.1.0



144
145
146
# File 'app/models/whittaker_tech/midas/coin.rb', line 144

def minor
  currency_minor
end

#scaleNumeric

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.

Returns:

  • (Numeric)

Since:

  • 0.1.0



211
212
213
# File 'app/models/whittaker_tech/midas/coin.rb', line 211

def scale
  10**decimals
end