Gringotts: A complete payment library for Elixir and Phoenix Framework

I had worked on a fin-tech project for mutual funds, stocks, bonds etc. Even decimals were giving us nightmares, as digits at 9 decimal places were important. And ruby was not doing it correctly at those decimal places, or may be we were missing something. But we had to fix the decimal places later in the end .

1 Like

I hear you although Iā€™m no fintech guru. Its why in ex_money I never round implicitly, only ever explicitly.

I am pretty confident that the Decimal lib works correctly to arbitrary precision although of course when division is involved there can always be issues. Arguably support for Decimal and Rational numbers would be useful.

if you defined something like:

defprotocol Money.Access do
  @doc Access money components
  def currency(money)
  def amount(money)
end

defimpl Money.Access, for: Any do
  def currency(money), do: Map.get(money, :currency)
  def amount(money), do: Map.get(money, :amount)
end

that would give a reasonable default for many libs and easy implementation for other lib writers. And then as a byproduct we could end of with a community endorsed protocol that could, if required, be extended.

I thought a few times that it would be handy to have a central registration of protocols that could be used to aid integrations like this but thats also hard to manageā€¦

5 Likes

Yeah, seems like useful, will have to think/plan somewhat like this. Thanks

Following your request we have created this table of gateway feature matrix https://github.com/aviabird/gringotts/wiki/Gateway-feature-matrix

It lists all the gateways and the methods they support. Hope that helps!

3 Likes

Hey everyone! Iā€™m on the Gringotts core team too.
Thanks for your valuable suggestions on how to handle Money @kip @AstonJ @michalmuskala . Weā€™ve made an issue #62 on this and would like to move the discussion over there.

Eager to hear your thoughts on the proposed plan!

5 Likes

@oyeb @pkrawat1 Spreedly (co-maintainer of active merchant) team member here - while we donā€™t use Elixir for payments at this time, we do use Elixir throughout our stack - best wishes in your endeavor in creating an Elixir payments library. Looking forward to seeing your progress.

5 Likes

Hi all

We are working on an example app for gringotts deployed on Heroku.
Have a look at here https://gringottspay.herokuapp.com

Thanks

3 Likes

With some encouragement from @Schultzer, ex_money version 1.1.3 now makes it easier to work with money in integer form to help easy integration with APIā€™s that use that form like Stripe.

iex> m = Money.new(:USD, 200.012356)
#Money<:USD, 200.012356>

# Return the money amount as {currency, integer, exponent, remainder}
iex> Money.to_integer_exp(m)
{:USD, 20001, -2, #Money<:USD, 0.002356>}

# Convert from integer to money.  ex_money will look up
# how many implied decimal places are intended based
# upon the currency definition in CLDR
iex> Money.from_integer(20000, :USD)
#Money<:USD, 200.00>
iex> Money.from_integer(20000, :JPY)
#Money<:JPY, 20000>

Hopefully this makes it easier to work with other libraries that use the integer format even though the risk of miscalculating the exponent for the appropriate currency is still a risk with this representation.

1 Like

Uhh, is this safe?! You should never ever ever ever use floats for money, never ever ever EVERā€¦ o.O
Should not accept it as floating point, should not give it as floating point, should not display it as floating point, etcā€¦ Fixed point only. And never ever ever by using floating point calculations internally either, ever.
(See my many prior posts about floating point as to why.)

4 Likes

They have an open issue for moving away from using floats here so theyā€™re working on it.

2 Likes

We have already started working on Money Protocols here

1 Like

Money.new/2 does accept a float - and perhaps it should not. It accepts an integer, Decimal and String as well which are clearly preferable. It converts to a Decimal at the interface. The inspect output you reference above is just that. In #Money<:USD, 200.012356> the 200.012356 is not a float, its a Decimal.

So the only place a float gets in the picture is at the factory interface new/2. There is no internally usage of a float whatsoever. Even to the extent I had to build my Decimal math lib to support basic finance calculations.

Definitely open to community views on disallowing floats even at the factory interface.

A coda to the above: One reason that floats are supported in Money.new/2 is that services, like OpenExchangeRates, return data in json format which the Elixir decoders will understandably reduce to a float when given the following:

    rates: {
        AED: 3.672538,
        AFN: 66.809999,
        ALL: 125.716501,
        AMD: 484.902502,
        ANG: 1.788575,
        AOA: 135.295998,
        ARS: 9.750101,
        AUD: 1.390866,
        /* ... */
    }

So removing float support at in new/2 would create some challenges with the exisiting services that deliver floats. Not insurmountable, but not helpful either.

Absolutely disallow. Consuming or returning floats should be a hard error, absolutely not allowed as it will eventually break.

Such services should be disallowed. Anyone who uses a float to transfer monetary values is not just recklessness, but also malicious. Storing a number as a string allows lossless conversion and that is fine, same with fixed point, integers, ratioā€™s, etc, but storing it as a float should always be absolutely disallowed.

2 Likes

We would all easily agree that touching floating point arithmetic would be a very bad thing. But as best I can tell, simply converting from a float to a Decimal would not appear to introduce any additional precision errors.

That assumption is based upon my reading of IEE 754, consulting the Erlang and Decimal docs and running millions of empirical tests in the last hour or so round tripping Float -> Decimal -> Float (using :random.uniform/0 so not conclusive as yet). I also reviewed how Poison parses numbers and it ultimately uses String.to_float/1 which calls :erlang.binary_to_float/1 which is the same code path I used in my testing.

But Iā€™m not a mathematician therefore Iā€™m not claiming I have a definitive understanding whatsoever. However I definitely want to get to a clear and specific understanding here so I can make the best informed decisions on what I need to do in the lib given the goals I have expressed for it in the readme.

Iā€™d be very happy to be directed to the relevant literature on the simple ā€œconversionā€ path since all content I can find in the last hour have been about arithmetic about which we have no dispute.

Of course this doesnā€™t change your second challenge around some very popular external services that deliver exchange rates in json number format (and therefore reduced to a Float by json decoders).

2 Likes

I think the problem is not allowing the conversion per-se, but by allowing the conversion from floats then some application developers will not realize that they are dealing with an imprecise format and the potential for hard-to-detect errors. So in a way the library could be seen as encouraging bad practices. As an alternative new/2 could raise an error describing the issue when called with a float.

2 Likes

I think the problem is not allowing the conversion per-se, but by allowing the conversion from floats then some application developers will not realize that they are dealing with an imprecise format

Its good point of course.

And after writing a much longer answer with questions I realise all my issues are not about Money.new/2 but are about doing arithmetic (interest rate calcs, exchange rate calcs) which donā€™t involve Money.new/2, just conversion of float to Decimal in order to become a multiplicand.

Iā€™ll deprecate Money.new(currency, Float) in the next version and remove it in ex_money 2.0.

Great conversation, thanks to you all.

3 Likes

Yes, yes it would. :wink:

Say you get a number from json or so, not a string, a number, and it is like 8.10, well 8.10 is not perfectly representable so youā€™d probably get a 8.0999999999999999* decimal, and this is just one of the most simple examples.

Never even accept float!

For note, 8.099999999999999 will (with enough 9ā€™s) be rendered to the screen or so as 8.1, but will still internally have lots of 9ā€™s that will mess up calculations. You cannot perfectly represent Base 10 floating point in Base 2 floating point, it is impossible for many numbers.

3 Likes

While it isnā€™t on the topic of floats and their evil use within the realm of money, Iā€™d like to mention that Iā€™m the maintainer of the Braintree library for Elixir.

Iā€™ve personally never changed payment gateways during a project and donā€™t see a pressing need for an all encompassing payment gateway, but I know other people coming to the language will be looking for it. Best of luck!

Please reach out or feel free to lift the XML handling from the Braintree library.

4 Likes

CURRENT STATUS: we have merged the implementation in the dev branch; weā€™ll be releasing a new version in a day or two.

PR# MERGED: https://github.com/aviabird/gringotts/pull/71
ISSUE# https://github.com/aviabird/gringotts/issues/62

1 Like

Iā€™ve released ex_money version 2.0.0 which no longer supports a float as a parameter to the factory method Money.new/2 (it will return an error or raise on the ! method).

I have also added Money.from_float/2 which will accept a float parameter. The intention is to make it really clear to a developer that they are using something out of the ordinary. As a precaution to ensure that ex_money doesnā€™t introduce any additional imprecision it will return an error if the supplied float has more than 15 digits of precision.

(not really Gringotts related I know but I want to reflect the decisions I took based upon the solid feedback from the community)

8 Likes