defprotocol Gringotts.Money do
@moduledoc """
Money protocol used by the Gringotts API.
The `amount` argument required for some of Gringotts' API methods must
implement this protocol.
If your application is already using a supported Money library, just pass in
the Money struct and things will work out of the box.
Otherwise, just wrap your `amount` with the `currency` together in a `Map`
like so,
price = %{value: Decimal.new("20.18"), currency: "USD"}
and the API will accept it (as long as the currency is valid [ISO 4217
currency code](https://www.iso.org/iso-4217-currency-codes.html)).
## Note on the `Any` implementation
Both `to_string/1` and `to_integer/1` assume that the precision for the `currency`
is 2 digits after decimal.
"""
@fallback_to_any true
@type t :: Gringotts.Money.t()
@doc """
Returns the ISO 4217 compliant currency code associated with this sum of money.
This must be an UPCASE `string`
"""
@spec currency(t) :: String.t()
def currency(money)
@doc """
Returns a `Decimal.t` representing the "worth" of this sum of money in the
associated `currency`.
"""
@spec value(t) :: Decimal.t()
def value(money)
@doc """
Returns the ISO4217 `currency` code as string and `value` as an integer.
Useful for gateways that require amount as integer (like cents instead of
dollars).
## Note
Conversion from `Decimal.t` to `integer` is potentially lossy and the rounding
(if required) is performed (automatically) by the Money library defining the
type, or in the implementation of this protocol method.
If you want to implement this method for your custom type, please ensure that
the rounding strategy (if any rounding is applied) must be
[`half_even`][wiki-half-even].
**To keep things predictable and transparent, merchants should round the
`amount` themselves**, perhaps by explicitly calling the relevant method of
the Money library in their application _before_ passing it to `Gringotts`'s
public API.
## Examples
# the money lib is aliased as "MoneyLib"
iex> usd_price = MoneyLib.new("4.1234", :USD)
#MoneyLib<4.1234, "USD">
iex> Gringotts.Money.to_integer(usd_price)
{"USD", 412, -2}
iex> bhd_price = MoneyLib.new("4.1234", :BHD)
#MoneyLib<4.1234, "BHD">
iex> Gringotts.Money.to_integer(bhd_price)
{"BHD", 4123, -3}
# the Bahraini dinar is divided into 1000 fils unlike the dollar which is
# divided in 100 cents
[wiki-half-even]: https://en.wikipedia.org/wiki/Rounding#Round_half_to_even
"""
@spec to_integer(Money.t()) ::
{currency :: String.t(), value :: integer, exponent :: neg_integer}
def to_integer(money)
@doc """
Returns a tuple of ISO4217 `currency` code and the `value` as strings.
The stringified `value` must match this regex: `~r/-?\\d+\\.\\d\\d{n}/` where
`n+1` should match the required precision for the `currency`. There should be
no place value separators except the decimal point (like commas).
> Gringotts will not (and cannot) validate this of course.
## Note
Conversion from `Decimal.t` to `string` is potentially lossy and the rounding
(if required) is performed (automatically) by the Money library defining the
type, or in the implementation of this protocol method.
If you want to implement this method for your custom type, please ensure that
the rounding strategy (if any rounding is applied) must be
[`half_even`][wiki-half-even].
**To keep things predictable and transparent, merchants should round the
`amount` themselves**, perhaps by explicitly calling the relevant method of
the Money library in their application _before_ passing it to `Gringotts`'s
public API.
## Examples
# the money lib is aliased as "MoneyLib"
iex> usd_price = MoneyLib.new("4.1234", :USD)
#MoneyLib<4.1234, "USD">
iex> Gringotts.Money.to_string(usd_price)
{"USD", "4.12"}
iex> bhd_price = MoneyLib.new("4.1234", :BHD)
#MoneyLib<4.1234, "BHD">
iex> Gringotts.Money.to_string(bhd_price)
{"BHD", "4.123"}
[wiki-half-even]: https://en.wikipedia.org/wiki/Rounding#Round_half_to_even
"""
@spec to_string(t) :: {currency :: String.t(), value :: String.t()}
def to_string(money)
end
# Assumes that the currency is subdivided into 100 units
defimpl Gringotts.Money, for: Any do
def currency(%{currency: currency}), do: currency
def value(%{value: %Decimal{} = value}), do: value
def to_integer(%{value: %Decimal{} = value, currency: currency}) do
{
currency,
value
|> Decimal.mult(Decimal.new(100))
|> Decimal.round(0)
|> Decimal.to_integer(),
-2
}
end
def to_string(%{value: %Decimal{} = value, currency: currency}) do
{
currency,
value |> Decimal.round(2) |> Decimal.to_string()
}
end
end