Skip to content

Commit

Permalink
[money-protocol] Expansion: to_string and to_integer (#86)
Browse files Browse the repository at this point in the history
* Adds `to_string` and `to_integer` to the protocol
* Fixes #62 and #85
* Implements all protocol methods for `ex_money`
* Adds integration test with ex_money
* Adapted Monei with protocol updates
* Adds test for `Any`
* The default rounding strategy for implementations of Gringotts.Money
protocol is HALF-EVEN.
* Updated public API docs with "perils of rounding".
  • Loading branch information
oyeb authored and pkrawat1 committed Jan 25, 2018
1 parent 798539c commit f3137a3
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 60 deletions.
72 changes: 44 additions & 28 deletions lib/gringotts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,53 @@ defmodule Gringotts do
This argument represents the "amount", annotated with the currency unit for
the transaction. `amount` is polymorphic thanks to the `Gringotts.Money`
protocol which can be implemented by your custom Money type.
protocol which can even be implemented by your very own custom Money type!
#### Note
We support [`ex_money`][ex_money] and [`monetized`][monetized] out of the
box, and you can drop their types in this argument and everything will work
as expected.
Otherwise, just wrap your `amount` with the `currency` together in a `Map` like so,
money = %{amount: Decimal.new(100.50), currency: "USD"}
Gringotts supports [`ex_money`][ex_money] out of the box, just drop `ex_money`
types in this argument and everything will work as expected.
> Support for [`monetized`][monetized] and [`money`][money] is on the
> way, track it [here][iss-money-lib-support]!
Otherwise, just wrap your `amount` with the `currency` together in a `Map` like so,
money = %{value: Decimal.new("100.50"), currency: "USD"}
> When this highly precise `amount` is serialized into the network request, we
> use a potentially lossy `Gringotts.Money.to_string/1` or
> `Gringotts.Money.to_integer/1` to perform rounding (if required) using the
> [`half-even`][wiki-half-even] strategy.
>
> **Hence, to ensure transparency, protect sanity and save _real_ money, we
> STRONGLY RECOMMEND that merchants perform any required rounding and handle
> remainders in their application logic -- before passing the `amount` to
> Gringotts's API.**
#### Example
If you use `ex_money` in your project, and want to make an authorization for
$2.99 to MONEI, you'll do the following:
$2.99 to the `XYZ` Gateway, you'll do the following:
# the money lib is aliased as "MoneyLib"
amount = Money.new(2.99, :USD)
Gringotts.authorize(Gringotts.Gateways.Monei, amount, some_card, extra_options)
amount = MoneyLib.new("2.99", :USD)
Gringotts.authorize(Gringotts.Gateways.XYZ, amount, some_card, extra_options)
[ex_money]: https://hexdocs.pm/ex_money/readme.html
[monetized]: https://hexdocs.pm/monetized/
[money]: https://hexdocs.pm/money/Money.html
[iss-money-lib-support]: https://github.com/aviabird/gringotts/projects/3#card-6801146
[wiki-half-even]: https://en.wikipedia.org/wiki/Rounding#Round_half_to_even
### `card`, a payment source
Gringotts provides a `Gringotts.CreditCard` type to hold card parameters
which merchants fetch from their clients. The same type can also hold Debit
card details.
#### Note
Gringotts only supports payment by debit or credit card, even though the
gateways might support payment via other instruments such as e-wallets,
vouchers, bitcoins or banks. Support for these instruments is planned in
Expand All @@ -71,10 +93,7 @@ defmodule Gringotts do
`opts` is a `keyword` list of other options/information accepted by the
gateway. The format, use and structure is gateway specific and documented in
the Gateway's docs.
[ex_money]: https://hexdocs.pm/ex_money/readme.html
[monetized]: https://hexdocs.pm/monetized/
## Configuration
Merchants must provide Gateway specific configuration in their application
Expand Down Expand Up @@ -140,9 +159,9 @@ defmodule Gringotts do
To (pre) authorize a payment of $4.20 on a sample `card` with the `XYZ`
gateway,
amount = Money.new(4.2, :USD)
# IF YOU DON'T USE ex_money OR monetized
# amount = %{value: Decimal.new(4.2), currency: "EUR"}
amount = Money.new("4.2", :USD)
# IF YOU DON'T USE ex_money
# amount = %{value: Decimal.new("4.2"), currency: "EUR"}
card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
{:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.XYZ, amount, card, opts)
"""
Expand All @@ -163,9 +182,9 @@ defmodule Gringotts do
To capture $4.20 on a previously authorized payment worth $4.20 by referencing
the obtained authorization `id` with the `XYZ` gateway,
amount = Money.new(4.2, :USD)
# IF YOU DON'T USE ex_money OR monetized
# amount = %{value: Decimal.new(4.2), currency: "EUR"}
amount = Money.new("4.2", :USD)
# IF YOU DON'T USE ex_money
# amount = %{value: Decimal.new("4.2"), currency: "EUR"}
card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
Gringotts.capture(Gringotts.Gateways.XYZ, amount, auth_result.id, opts)
"""
Expand All @@ -191,9 +210,9 @@ defmodule Gringotts do
To process a purchase worth $4.2, with the `XYZ` gateway,
amount = Money.new(4.2, :USD)
# IF YOU DON'T USE ex_money OR monetized
# amount = %{value: Decimal.new(4.2), currency: "EUR"}
amount = Money.new("4.2", :USD)
# IF YOU DON'T USE ex_money
# amount = %{value: Decimal.new("4.2"), currency: "EUR"}
card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
Gringotts.purchase(Gringotts.Gateways.XYZ, amount, card, opts)
"""
Expand All @@ -212,9 +231,9 @@ defmodule Gringotts do
To refund a previous purchase worth $4.20 referenced by `id`, with the `XYZ`
gateway,
amount = Money.new(4.2, :USD)
# IF YOU DON'T USE ex_money OR monetized
# amount = %{value: Decimal.new(4.2), currency: "EUR"}
amount = Money.new("4.2", :USD)
# IF YOU DON'T USE ex_money
# amount = %{value: Decimal.new("4.2"), currency: "EUR"}
Gringotts.purchase(Gringotts.Gateways.XYZ, amount, id, opts)
"""
def refund(gateway, amount, id, opts \\ []) do
Expand Down Expand Up @@ -282,9 +301,6 @@ defmodule Gringotts do
call(:payment_worker, {:void, gateway, id, opts})
end


# TODO: This is runtime error reporting fix this so that it does compile
# time error reporting.
defp validate_config(gateway) do
# Keep the key name and adapter the same in the config in application
config = Application.get_env(:gringotts, gateway)
Expand Down
46 changes: 27 additions & 19 deletions lib/gringotts/gateways/monei.ex
Original file line number Diff line number Diff line change
Expand Up @@ -178,17 +178,19 @@ defmodule Gringotts.Gateways.Monei do
The following session shows how one would (pre) authorize a payment of $40 on a sample `card`.
iex> amount = %{value: Decimal.new(42), currency: "EUR"}
iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> auth_result = Gringotts.authorize(Gringotts.Gateways.Monei, amount, card, opts)
iex> auth_result.id # This is the authorization ID
"""
@spec authorize(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response}
@spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response}
def authorize(amount, card = %CreditCard{}, opts) do
{currency, value} = Money.to_string(amount)

params =
[
paymentType: "PA",
amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2),
currency: Money.currency(amount)
amount: value,
currency: currency
] ++ card_params(card)

auth_info = Keyword.fetch!(opts, :config)
Expand All @@ -215,17 +217,19 @@ defmodule Gringotts.Gateways.Monei do
authorized a payment worth $35 by referencing the obtained authorization `id`.
iex> amount = %{value: Decimal.new(42), currency: "EUR"}
iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> capture_result = Gringotts.capture(Gringotts.Gateways.Monei, 35, auth_result.id, opts)
"""
@spec capture(Money.t, String.t(), keyword) :: {:ok | :error, Response}
@spec capture(Money.t(), String.t(), keyword) :: {:ok | :error, Response}
def capture(amount, payment_id, opts)

def capture(amount, <<payment_id::bytes-size(32)>>, opts) do
{currency, value} = Money.to_string(amount)

params = [
paymentType: "CP",
amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2),
currency: Money.currency(amount)
amount: value,
currency: currency
]

auth_info = Keyword.fetch!(opts, :config)
Expand All @@ -243,18 +247,20 @@ defmodule Gringotts.Gateways.Monei do
The following session shows how one would process a payment in one-shot,
without (pre) authorization.
iex> amount = %{value: Decimal.new(42), currency: "EUR"}
iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> amount = %{value: Decimal.new(42), currency: "EUR"}
iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> purchase_result = Gringotts.purchase(Gringotts.Gateways.Monei, amount, card, opts)
"""
@spec purchase(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response}
@spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response}
def purchase(amount, card = %CreditCard{}, opts) do
{currency, value} = Money.to_string(amount)

params =
card_params(card) ++
[
paymentType: "DB",
amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2),
currency: Money.currency(amount)
amount: value,
currency: currency
]

auth_info = Keyword.fetch!(opts, :config)
Expand Down Expand Up @@ -290,7 +296,7 @@ defmodule Gringotts.Gateways.Monei do
authorization. Remember that our `capture/3` example only did a partial
capture.
iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> void_result = Gringotts.void(Gringotts.Gateways.Monei, auth_result.id, opts)
"""
@spec void(String.t(), keyword) :: {:ok | :error, Response}
Expand Down Expand Up @@ -319,15 +325,17 @@ defmodule Gringotts.Gateways.Monei do
similarily for captures).
iex> amount = %{value: Decimal.new(42), currency: "EUR"}
iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> refund_result = Gringotts.refund(Gringotts.Gateways.Monei, purchase_result.id, amount)
"""
@spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response}
@spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response}
def refund(amount, <<payment_id::bytes-size(32)>>, opts) do
{currency, value} = Money.to_string(amount)

params = [
paymentType: "RF",
amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2),
currency: Money.currency(amount)
amount: value,
currency: currency
]

auth_info = Keyword.fetch!(opts, :config)
Expand All @@ -354,7 +362,7 @@ defmodule Gringotts.Gateways.Monei do
The following session shows how one would store a card (a payment-source) for
future use.
iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> store_result = Gringotts.store(Gringotts.Gateways.Monei, card, [])
"""
@spec store(CreditCard.t(), keyword) :: {:ok | :error, Response}
Expand Down

0 comments on commit f3137a3

Please sign in to comment.