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

[Mercadopago] Implemented authorize/3 #133

Closed
wants to merge 21 commits into from
Closed

[Mercadopago] Implemented authorize/3 #133

wants to merge 21 commits into from

Conversation

subpal
Copy link
Contributor

@subpal subpal commented Mar 21, 2018

  1. Implemented authorize
  2. Added docs
  3. Used Money and CreditCard datatype.

@xC-Stony xC-Stony self-requested a review March 21, 2018 10:50
## The `opts` argument

Most `Gringotts` API calls accept an optional `keyword` list `opts` to supply
optional arguments for transactions with the MERCADOAPGO

Choose a reason for hiding this comment

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

typo "MERCADOAPGO"

* `capture/3` _an_ amount.
* `void/2` a pre-authorization.

## Note

Choose a reason for hiding this comment

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

Not needed as you haven't written anything under it


@spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response}
def authorize(amount , %CreditCard{} = card, opts) do
# commit(args, ...)

Choose a reason for hiding this comment

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

Insignificant comment

defp get_customer_id(opts) do
body = %{"email": opts[:email]} |> Poison.encode!
headers = [{"content-type", "application/json"}, {"accept", "application/json"}]
response = HTTPoison.post!("#{@base_url}/v1/customers?access_token=#{opts[:config][:access_token]}", body, headers, [])

Choose a reason for hiding this comment

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

  • I think you should use

HTTPoison.post

rather than

HTTPoison.post!

as the latter will raise an exception in case it fails.

  • Also I would suggest you to handle all request via the commit function

"expirationMonth": card.month,
"cardNumber": card.number,
"securityCode": card.verification_code,
"cardholder": %{

Choose a reason for hiding this comment

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

Can be rewritten as `

"cardholder": CreditCard.full_name(card)

`

Copy link

@xC-Stony xC-Stony left a comment

Choose a reason for hiding this comment

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

Some minor changes are needed.

import Poison, only: [decode: 1]

alias Gringotts.{CreditCard,
Response}

Choose a reason for hiding this comment

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

...can be in single line...

def authorize(amount , %CreditCard{} = card, opts) do
# commit(args, ...)
{_, value, _, _} = Money.to_integer_exp(amount)
if(is_nil(opts[:customer_id])) do
Copy link
Contributor

Choose a reason for hiding this comment

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

Use Keyword.has_key? instead.

"type": "mercadopago",
"id": opts[:order_id]
},
"installments": 1,
Copy link
Contributor

Choose a reason for hiding this comment

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

I haven't read the docs, but I don't think this should be hard-coded.

@@ -0,0 +1,376 @@
defmodule Gringotts.Gateways.Mercadopago do
@moduledoc """
[MERCADOPAGO][home] gateway implementation.
Copy link
Contributor

Choose a reason for hiding this comment

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

Use the gateway branding style when you write docs, to my best knowledge they've named their product as mercadopago.

- To save you time, we recommend [cloning our example
repo][example] that gives you a pre-configured sample app ready-to-go.
+ You could use the same config or update it the with your "secrets"
as described [above](#module-registering-your-monei-account-at-mercadopago).

Choose a reason for hiding this comment

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

monei??

Copy link
Contributor

Choose a reason for hiding this comment

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

This is because of a bug in the template file (templates/gateway.eex) used by gringotts.new. Could I interest you in submitting a tiny PR with the fix for #136?

token_id = get_token_id(card, opts)
opts = opts ++ [token_id: token_id]

body = get_authorize_body(value, card, opts, opts[:token_id], opts[:customer_id]) |> Poison.encode!()

Choose a reason for hiding this comment

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

If you are sending opts as an argument, what is the need to send opts[:token_id] and opts[:customer_id] separately along with it?

body = get_token_body(card) |> Poison.encode!()
headers = [{"content-type", "application/json"}, {"accept", "application/json"}]
token = HTTPoison.post!("#{@base_url}/v1/card_tokens/#{opts[:customer_id]}?public_key=#{opts[:config][:public_key]}", body, headers, [])
%HTTPoison.Response{body: body} = token

Choose a reason for hiding this comment

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

IMO, no need to use token variable. You can directly match them

Copy link

@RajeetKaushal RajeetKaushal left a comment

Choose a reason for hiding this comment

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

Minor changes needed !

Copy link
Contributor

@oyeb oyeb left a comment

Choose a reason for hiding this comment

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

  1. Please rename your functions, many of them start with get_.
    • get_authorize_body -> authorize_params
    • get_token_id -> create_customer_token or create_customer
    • get_token_body -> create_card_token or create_card
    • and so on...
  2. Please remove implementation of capture


[credentials]: https://www.mercadopago.com/mlb/account/credentials?type=basic

* mercadopago does not process money in cents, and the `amount` is rounded to 2
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be under a new section, perhaps ## Note

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was mistake. For mercadopago nothing is given how payments are processed.

Copy link
Contributor

@oyeb oyeb Mar 28, 2018

Choose a reason for hiding this comment

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

I understand nothing may be specified, but you'll have to figure it out yourselves. Look at the implementation in activemerchant, see if they have converted the amount argument into cents or used as is. Take hint from the various SDKs they (mercadopago) have implemented.


## Supported currencies and countries

> mercadoapgo supports the currencies listed here :
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this under a quote block >? Please add a link to the docs where you found this info, and remove the colons.

- To save you time, we recommend [cloning our example
repo][example] that gives you a pre-configured sample app ready-to-go.
+ You could use the same config or update it the with your "secrets"
as described [above](#module-registering-your-mercadopago-account-at-mercadopago).
Copy link
Contributor

Choose a reason for hiding this comment

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

#module-registering-your-mercadopago-account-at-gringotts
If you build the docs using mix docs, you'll see that this turns into an internal link. It should link to the section at line 33.

* `void/2` a pre-authorization.
## Note

> For a new customer, `customer_id` field should be ignored. Otherwise it should be provided.
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to keep this in a quote block. The quote blocks generated by gringotts.new are meant to be replaced, they were added as hints for the module author.

The authorization validates the `card` details with the banking network,
places a hold on the transaction `amount` in the customer’s issuing bank.

mercadoapgo's `authorize` returns a map containing authorization ID string which can be used to:
Copy link
Contributor

Choose a reason for hiding this comment

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

... returns an authorization ID (available in the Response.id field)...

end
end

defp respond({:error, %HTTPoison.Error{id: _, reason: _}}, opts) do
Copy link
Contributor

Choose a reason for hiding this comment

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

Use the reason, see how it's done in other gateways.

"type": "mercadopago",
"id": opts[:order_id]
},
"installments": 1,
Copy link
Contributor

Choose a reason for hiding this comment

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

opts[:installments] || 1

## Note
mercadopago allows partial captures also. However, you can make a partial capture to a payment only **once**.

> The authorization will be valid for 7 days. If you do not capture it by that time, it will be cancelled.
Copy link
Contributor

Choose a reason for hiding this comment

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

No blockquotes pls, maybe use bullets.

defp auth(amount, %CreditCard{} = card, opts, token_id, capture) do
{_, value, _, _} = Money.to_integer_exp(amount)
opts = opts ++ [token_id: token_id]
url = "#{@base_url}/v1/payments?access_token=#{opts[:config][:access_token]}"
Copy link
Contributor

Choose a reason for hiding this comment

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

Notice that URL and header construction is duplicated atleast 3 times. It's better to move this to commit along with the respond(opts). By moving both of these to commit, you'll realize this function looks very empty. That's good, because you can now move the logic up in authorize -- big gains in readability.

Don't eagerly split a function, that doesn't necessarily increase "modularity". If your private/helper function is getting used by ONLY one method, then you should consider moving that logic into the caller.

You'll have to figure out what to do about that capture argument.

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 cannot move url into commit, because it if different for different cases. We have moved the headers and respond function call in commit itself.

Copy link
Contributor

Choose a reason for hiding this comment

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

You can (partially)! Notice that HTTPoison.request/5 takes many options, one of them being the key :params.

auth(amount, %CreditCard{} = card, opts, token_id, capture) do
  {_, value, _, _} = Money.to_integer_exp(amount)
  opts = opts ++ [token_id: token_id]
  url = "/v1/payments"
  url_params = [access_token: opts[:config][:access_token]]
  body = authorize_params(value, card, opts, opts[:token_id], opts[:customer_id], capture) |> Poison.encode!
  commit(:post, url, body, params: url_params)
end

defp commit(method, path, body, opts) do
  headers = [{"content-type", "application/json"}, {"accept", "application/json"}]
  HTTPoison.request(method, path, body, headers, opts) |> respond()
end

url = "#{@base_url}/v1/customers?access_token=#{opts[:config][:access_token]}"
body = %{"email": opts[:email]} |> Poison.encode!
headers = [{"content-type", "application/json"}, {"accept", "application/json"}]
{state, res} = commit(:post, url, body, headers) |> respond(opts)
Copy link
Contributor

Choose a reason for hiding this comment

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

Use a case here.

@@ -258,8 +257,14 @@ defmodule Gringotts.Gateways.Mercadopago do
end
end

defp respond({:error, %HTTPoison.Error{id: _, reason: _}}, opts) do
{:error, "Network Error"}
defp respond({:error, %HTTPoison.Error{} = error}, opts) do
Copy link
Contributor

Choose a reason for hiding this comment

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

opts is never used.

defp auth(amount, %CreditCard{} = card, opts, token_id, capture) do
{_, value, _, _} = Money.to_integer_exp(amount)
opts = opts ++ [token_id: token_id]
url = "#{@base_url}/v1/payments?access_token=#{opts[:config][:access_token]}"
Copy link
Contributor

Choose a reason for hiding this comment

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

You can (partially)! Notice that HTTPoison.request/5 takes many options, one of them being the key :params.

auth(amount, %CreditCard{} = card, opts, token_id, capture) do
  {_, value, _, _} = Money.to_integer_exp(amount)
  opts = opts ++ [token_id: token_id]
  url = "/v1/payments"
  url_params = [access_token: opts[:config][:access_token]]
  body = authorize_params(value, card, opts, opts[:token_id], opts[:customer_id], capture) |> Poison.encode!
  commit(:post, url, body, params: url_params)
end

defp commit(method, path, body, opts) do
  headers = [{"content-type", "application/json"}, {"accept", "application/json"}]
  HTTPoison.request(method, path, body, headers, opts) |> respond()
end

@@ -151,8 +153,9 @@ defmodule Gringotts.Gateways.Mercadopago do
# network request in here, and parse it using another private method called
# `respond`.
#@spec commit(_, _, _, _) :: {:ok | :error, Response}
defp commit(method, path, body, headers) do
HTTPoison.request(method, path, body, headers, [])
defp commit(method, path, body, opts) do
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. This function doesn't actually use opts as of now. If you pass opts to the request, there's some benefit.
  2. Could you please add the right @spec?

Created all the helper functions, body for HTTPoison requests, response methods.
1. Completed the docs
2. Used predefined CreditCard and Money data type
3. Added brand in CreditCard
1. Implemented authorize
2. Completed docs
3. Used CreditCard and Money datatype
1. Used commit for HTTPoison requests.
2. Handled network and arguement errors.
3. Updated docs
1. Used commit for HTTPoison requests.
2. Handled network and arguement errors.
3. Updated docs
1. Used commit for HTTPoison requests.
2. Handled network and arguement errors.
3. Updated docs
1. Used commit for HTTPoison requests.
2. Handled network and arguement errors.
3. Updated docs
1. functions are renamed
2. if else replaced with case
3. docs updated
1. changed if else to case
2. changed function name
3. updated docs
1. Added default value of `installments` in function `authorize_params/6` as 1.
2. Added `headers` and `respond`function call in commit function itself.
3. Used `reason` in `respond` function, in case of network error.
4. Removed block quotes '>'.
5. Added money processing note.
1. Removed unnecessary `opts` from arguements of error `respond` function.
2. Used fifth arguement in `HTTPoison.Request` to send part of url as keyword list.
3. Added @SPEC for `commit` function.
Copy link
Contributor

@oyeb oyeb left a comment

Choose a reason for hiding this comment

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

Please remove the test files from this branch.


@spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response}
def authorize(amount , %CreditCard{} = card, opts) do
if Keyword.has_key?(opts, :customer_id) do
Copy link
Contributor

Choose a reason for hiding this comment

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

We can move this check inside create_customer, since create_customer recieves the same opts.

def authorize(amount , %CreditCard{} = card, opts) do
  with {:ok, customer_id} <- create_customer(opts),
     {:ok, card_token} <- create_token(card, opts) do
    {_, value, _, _} = Money.to_integer_exp(amount)
    url_params = [access_token: opts[:config][:access_token]]
    body = authorize_params(value, card, opts, card_token, customer_id, false) |> Poison.encode!
    commit(:post, "/v1/payments", body, opts, params: url_params )
  end
end

If you wish you could even write an else block. Here are the with/1 docs.

This way we can get rid of auth_token/4 as well as auth/5. Please refrain from writing so many helper functions.

commit(:post, "/v1/payments", body, opts, params: url_params )
end

defp create_customer(opts) do
Copy link
Contributor

Choose a reason for hiding this comment

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

You can move that if here to check if the caller supplied a customer_id in opts

"last_name": card.last_name
},
"order": %{
"type": "mercadopago",
Copy link
Contributor

Choose a reason for hiding this comment

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

What are other options for the order.type? Is it always "mercadopago"?

Mention `customer_id`.
iex> amount = Money.new(42, :BRL)
iex> card = %Gringotts.CreditCard{first_name: "Hermione", last_name: "Granger", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> opts = [email: "hermione@granger.com", order_id: 123125, customer_id: "308537342-HStv9cJCgK0dWU", payment_method_id: "visa", config: %{access_token: YOUR_ACCESS_TOKEN, public_key: YOUR_PUBLIC_KEY}]
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove the config key from the examples. Sorry for not making that clear in an earlier comment 😓

@codecov-io
Copy link

codecov-io commented Apr 18, 2018

Codecov Report

Merging #133 into dev will decrease coverage by 8.4%.
The diff coverage is 0%.

Impacted file tree graph

@@            Coverage Diff             @@
##              dev     #133      +/-   ##
==========================================
- Coverage   64.16%   55.75%   -8.41%     
==========================================
  Files          14       15       +1     
  Lines         466      504      +38     
==========================================
- Hits          299      281      -18     
- Misses        167      223      +56
Impacted Files Coverage Δ
lib/gringotts/gateways/mercadopago.ex 0% <0%> (ø)
lib/gringotts.ex 0% <0%> (-100%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a4082cb...9f9d3d7. Read the comment docs.

Copy link
Contributor

@oyeb oyeb left a comment

Choose a reason for hiding this comment

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

Please use credo to analyze your module, just run mix credo --strict in the project root and fix all mercadopago related issues.
Remeber that credo has the explain command which is extremely useful.

| ---- | --- |
| `email` | Email of the customer. Type - string . |
| `order_id` | Order id issued by the merchant. Type- integer. |
| `payment_method_id` | Payment network operators, eg. `visa`, `mastercard`. Type- string. |
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. payment_method_id is no longer required in opts
  2. customer_id need not be nil in case of a new customer, it can simply be skipped. Please re-word the sentence.
  3. order_type and installments are undocumented.

The authorization validates the `card` details with the banking network,
places a hold on the transaction `amount` in the customer’s issuing bank.

mercadoapgo's `authorize` returns authorization ID(available in the `Response.id` field) :
Copy link
Contributor

Choose a reason for hiding this comment

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

We must document what's returned in Response.token here.

# `required_config` list
use Gringotts.Adapter, required_config: [:public_key, :access_token]

import Poison, only: [decode: 1]
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an unused import, remove it. It shows up as a compile-time warning.

Mention `customer_id`.
iex> amount = Money.new(42, :BRL)
iex> card = %Gringotts.CreditCard{first_name: "Hermione", last_name: "Granger", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"}
iex> opts = [email: "hermione@granger.com", order_id: 123125, customer_id: "308537342-HStv9cJCgK0dWU", payment_method_id: "visa"]
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. Is it necessary to pass :email even though we have the customer's ID?
  2. Please mask the :customer_id with some string like "hermione's_customer_id".

2. Added docs for Response.token.
3. Removed unused import.
4. Masked customer id in example.
5. Reduced the arity of authorize_params
@oyeb
Copy link
Contributor

oyeb commented Apr 22, 2018

Closing as this PR has been approved! 🎉

@oyeb oyeb closed this Apr 22, 2018
@mandarvaze
Copy link
Collaborator

Although this PR was approved, it was never merged to dev
Superceded by #185

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants