-
Notifications
You must be signed in to change notification settings - Fork 53
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
Conversation
subpal
commented
Mar 21, 2018
- Implemented authorize
- Added docs
- Used Money and CreditCard datatype.
## The `opts` argument | ||
|
||
Most `Gringotts` API calls accept an optional `keyword` list `opts` to supply | ||
optional arguments for transactions with the MERCADOAPGO |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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, ...) |
There was a problem hiding this comment.
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, []) |
There was a problem hiding this comment.
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": %{ |
There was a problem hiding this comment.
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)
`
There was a problem hiding this 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} |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
monei??
There was a problem hiding this comment.
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!() |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor changes needed !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Please rename your functions, many of them start with
get_
.get_authorize_body
->authorize_params
get_token_id
->create_customer_token
orcreate_customer
get_token_body
->create_card_token
orcreate_card
- and so on...
- 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 : |
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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]}" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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]}" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- This function doesn't actually use
opts
as of now. If you passopts
to therequest
, there's some benefit. - 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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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 thewith/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 |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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}] |
There was a problem hiding this comment.
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 Report
@@ 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
Continue to review full report at Codecov.
|
2. Fetched order_type from opts. 3. Removed config from opts in examples.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ---- | --- | | ||
| `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. | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
payment_method_id
is no longer required inopts
customer_id
need not benil
in case of a new customer, it can simply be skipped. Please re-word the sentence.order_type
andinstallments
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) : |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Is it necessary to pass
:email
even though we have the customer's ID? - 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
Closing as this PR has been approved! 🎉 |
Although this PR was approved, it was never merged to |