Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing the Money protocol #71

Merged
merged 10 commits into from
Jan 10, 2018
Merged

Introducing the Money protocol #71

merged 10 commits into from
Jan 10, 2018

Conversation

oyeb
Copy link
Contributor

@oyeb oyeb commented Jan 2, 2018

Building on the discussion in #62, this PR introduces a protocol to replace amount argument, and removes the need to specify currency in config or opts.
There's a fallback implementation for Any which enables us to support ex_money and monetized right out of the box.

Todo

Please push commits to resolve the following:

  • Fix examples in README
  • Possibly rename the protocol method amount to value... as an amount can be seen to have a value in a specific currency.
  • Add integration tests for a money lib.
  • Add ex_money as dependency and implement the protocol for it.

@@ -196,6 +197,12 @@ defmodule Gringotts.Gateways.MoneiTest do
then = Enum.map(all, &Gateway.respond({:ok, &1}))
assert Keyword.keys(then) == [:ok, :error, :error, :error]
end

defp amount(amount, currency \\ "USD") do
%{amount: Decimal.new(amount), currency: currency}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oyeb instead of using a simple map, use ex_money struct example here, put ex_money as only a dependency for test environment.

Or you also write 3 test cases for testing 3 existing libs by passing money structs of these libs showing how we accept all the libraries in elixir.

cc @kartikjagdale @arjun289 @SagarKarwande

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkrawat1 agree with you on this.

@@ -62,4 +62,5 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do
assert config[:adapter] == Gringotts.Gateways.Monei
end

defp amount(amount, currency \\ "EUR"), do: %{amount: Decimal.new(amount), currency: currency}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here as above comment, modify test cases to show it works with dependencies? what do you think?

Actually compatibility with external libs can be tested in a single file I am thinking.

Thoughts ? @kartikjagdale @arjun289 @oyeb

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not without adding them as dependencies that are available in test environment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oyeb agree here with you...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So do we agree to add ex_money and monteized to our dependencies?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, also keep the any fallback just in case. We need more documentation stating out of box support for these libs. @oyeb

@@ -74,8 +75,14 @@ defmodule Gringotts.Mixfile do
[
main: "Gringotts",
logo: "images/lg.png",
source_url: "https://github.com/aviabird/gringotts"
source_url: "https://github.com/aviabird/gringotts",
groups_for_modules: groups_for_modules()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cheers for this! Nothing to be done here 👍

@@ -252,22 +242,18 @@ defmodule Gringotts.Gateways.Monei do
The following session shows how one would process a payment in one-shot,
without (pre) authorization.

iex> opts = [currency: "EUR"] # The default currency is EUR, and this is just for an example.
iex> amount = %{amount: 40, currency: "EUR"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand why were we passing opts = [currency: "EUR"] in the first place? It was available all along in the gateway configs and hence in the gateway files in the lib.

Thoughts: @oyeb @kartikjagdale @arjun289 @Zeus999 ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ashish173 in gateway configs, it was the default_currency, to fallback to this currency if someone doesn't specify currency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had implemented just 1 optional param, currency. So it was the only thing that I could show an example for 😄
I've always been against having default_currency in gateway/lib-global configs. Thankfully, with this PR all that will go away.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oyeb why u r against with default_currency? what problems you find in that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkrawat1 agree with @ashish173 here that user should be able to pass a default currency through options.

Copy link
Member

@chandradot99 chandradot99 Jan 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kartikjagdale I don't think there is a need for that, if the user is passing a currency in Money struct, then what is the use of of passing default currency in options. it should be handled by our application only.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All amounts should necessarily, explicitly have an associated currency. Pls explain the need for a default on #62.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, now we don't have the need for the default currency.

Copy link
Member

@pkrawat1 pkrawat1 Jan 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I've read all the comments above and for the sake of organisation I am listing some points which would benefit from having default_currency.

  • In the application Instead of using "USD"strings in all the requests we can have a single source of truth for currency per gateway(assuming we have multiple gateways) or a global setting for default_currency this way we will be able to use that to create our request params for lib.

This lead to better organisation per gateway. Please note that we don't use this config directly in the library. We rely on what is send in the request.

cc: @oyeb @kartikjagdale @Zeus999 @arjun289

@@ -30,7 +30,6 @@ defmodule Gringotts.Gateways.Monei do
| ---- | --- | ---- |
| `billing_address` | | Not implemented |
| `cart` | | Not implemented |
| `currency` | | **Implemented** |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oyeb why did we remove this one? these params show what goes in the payments gateway and this PR should not have any change in the effect? Am I missing something here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currency is no longer an optional arg. It's explicitly available with the amount.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, I think I misunderstood that! 👍

@@ -107,7 +106,7 @@ defmodule Gringotts.Gateways.Monei do
aliases to it (to save some time):
```
iex> alias Gringotts.{Response, CreditCard, Gateways.Monei}
iex> opts = [currency: "EUR"] # The default currency is EUR, and this is just for an example.
iex> amount = %{amount: 40, currency: "EUR"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this is okay, for testing purpose developer can use a Map but in case of code follow my comments in this PR it would awesome if we can somehow make it explicit that we support 2-3 money libs out there, maybe a bunch of grouped test cases just thinking?

Thoughts? @oyeb @kartikjagdale @arjun289 @SagarKarwande @Zeus999

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to do that! And I'd love to add integration tests that make sure ex_money and monetized are compliant.
All that's stopping me is that I'll have to add them as an optional or only: :test dependency.

Copy link
Contributor Author

@oyeb oyeb Jan 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if we are adding them as dependency, I suggest we get them as optional: :true instead of only: :test as that will let us explicitly implement the protocol for them (which is currently happening via the fall back to any)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oyeb it will awesome if we have integration test for ex_money and monetized scenarios.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oyeb Yes, let's add them as optional and implement the protocol, make sure we have a fallback in any case.

lib/gringotts.ex Outdated
@@ -92,10 +101,8 @@ defmodule Gringotts do
country: "US",
postal_code: "11111"
}
Copy link
Member

@chandradot99 chandradot99 Jan 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

payment map is been divided into CreditCard and Address struct. so @oyeb this example needs to be change accordingly and now we are passing Address into the opts keyword.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a payment map? I'm not familiar with the recent changes in stripe (and also AFK), so I'll appreciate it if you can push a commit to this branch with the corrections.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oyeb ok I will make the required changes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Zeus999 I don't understand how is this change flowing backwards from gateway to gringotts.ex, care to give more info.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkrawat1 I think, the one who added the docs in gringotts.ex copied the docs from stripe.ex.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Zeus999 I added that 🥇 anyways @oyeb please make sure this is fixed in a way such that it does not depend on the gateway! @oyeb @Zeus999 collaborate on this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkrawat1 I am fixing this.

Copy link
Member

@kartikjagdale kartikjagdale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

params =
[
paymentType: "PA",
amount: :erlang.float_to_binary(amount, decimals: 2),
currency: currency(opts)
amount: amount |> Money.amount |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could rename the protocol method Money.amount to Money.value perhaps, then this would read:

amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2)

Writing Money.value(amount) is better than writing Money.amount(amount), no?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@oyeb
Copy link
Contributor Author

oyeb commented Jan 7, 2018

Task 3

I've hit a wall with writing integration tests for the money libs. The problem is that all these libs have a "naked" Money module/type, so if I add them to our deps(), the module gets redefined, as expected!

Moreover, there's no way for me to refer to the Money type from ex_money differently from the Money type from money (note that there's always only 1 Money type at the end (of compilation)).

@oyeb
Copy link
Contributor Author

oyeb commented Jan 7, 2018

Task 4

Because the module is naked, I can't implement the protocol for them in Gringotts. But we can send a PR to their projects, where they can implement our protocol. They won't have to add our lib as a dependency. It'll look something like:

# Add this in their money.ex
if Code.ensure_compiled?(Gringotts.Money) do
 defimpl value(money) do
    ...
 end
 defimpl currency(money) do
    ...
 end
end

If we ever change our protocol, we send PRs. If they make a breaking change later, it's their responsibility to mend it.

* Since amounts are Decimal.t, we depend on the decimal lib.
  - but it's marked as optional, so our users will have to explicitly
add it to their deps
* Added a group for gateways in the docs, for more features of ex_docs
see how other use it: https://github.com/kipcole9/money/blob/v1.1.0/mix.exs
* no need of currency in opts, removed @default_currency
* updates tests (even integration) to use a map for amount
There's much t0d0 in here.

* Documented the amount section
* Changed `amount` -> `value`
* We'll just be keeping ex_money in tests, as all Money libs keep their
modules "naked". See this thread: https://elixirforum.com/t/working-with-modules-with-same-name/11364
* Re-ordered methods, they are now alphabetic.
* docs are not tied to any specific gateway.
@pkrawat1 pkrawat1 merged commit d5fda24 into dev Jan 10, 2018
@pkrawat1 pkrawat1 deleted the money-protocol branch January 10, 2018 19:12
end

defimpl Gringotts.Money, for: Any do
def currency(money), do: Map.get(money, :currency)
Copy link
Contributor

@kipcole9 kipcole9 Jan 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I suggest calling to_string on the map result which would then normalise from libs (like ex_money) that use an atom for currency code?

def currency(money), do: Map.get(money, :currency) |> to_string

or to ensure uppercase per your definitions:

def currency(money), do: Map.get(money, :currency) |> to_string |> String.upcase

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is covered in 1076a9f (in this PR)
Both money and ex_money use upcase atoms, so the implementation for Money (not Any) will fire and give us a string. monetized uses upcase strings, so there's no need to do an upcase.


defimpl Gringotts.Money, for: Any do
def currency(money), do: Map.get(money, :currency)
def amount(money), do: Map.get(money, :amount)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps to enforce the use of Decimals this might be:

def amount(%{amount: %Decimal{} = amount}), do: amount

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll definitely introduce this when we add the to_float_string and to_integer methods to the protocol.

pkrawat1 pushed a commit that referenced this pull request Jan 25, 2018
Building on the discussion in #62, this PR introduces a protocol to replace amount argument, and removes the need to specify currency in config or opts.

Supporting `ex_money` and `monetized` money libs out of the box.

Usage:-
Money can now be passed like this while calling the public API methods.

money = %{amount: Decimal.new(2017.18), currency: "USD"}

Conversation for this happened here on elixir forum and https://elixirforum.com/t/gringotts-a-complete-payment-library-for-elixir-and-phoenix-framework/11054
@ashish173 ashish173 mentioned this pull request Apr 22, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants