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

[PinPayments] purchase and refund function with test cases #146

Open
wants to merge 28 commits into
base: pinpay
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d43cdb5
Implemented and Optimised `authorize/3` function.
ravirocx Mar 21, 2018
b6958a9
Core Functions implemented
ravirocx Mar 21, 2018
1e353b2
Fixed guard clause bug
ravirocx Mar 22, 2018
32d6707
Optimised `purchase`, `capture`, 'refund`, `store` and their private …
ravirocx Mar 22, 2018
e3d7a28
Merge branch 'pinpay' of https://github.com/aviabird/gringotts into p…
ravirocx Mar 22, 2018
50962f1
bug fixes and optimisation of core functions
ravirocx Mar 22, 2018
a7a01d7
Bug fixes and Documentation completed
ravirocx Mar 23, 2018
c8907b7
Optimised
ravirocx Mar 28, 2018
f369200
bug fixed
ravirocx Mar 29, 2018
977ddda
Merge branch 'dev' into pinpay
ravirocx Mar 29, 2018
fe28bb9
bug fixes
ravirocx Mar 29, 2018
f584e75
after merge
ravirocx Mar 29, 2018
befe5d1
bug fixes
ravirocx Mar 29, 2018
6049aaf
formatted
ravirocx Mar 29, 2018
24e357b
formatted all files
ravirocx Mar 29, 2018
49a72fe
improved code readability
ravirocx Mar 30, 2018
ce625c8
improved code readability
ravirocx Mar 30, 2018
3a5bf76
bug fixed
ravirocx Mar 30, 2018
5242a1b
bug fixes
ravirocx Mar 30, 2018
f874e63
improved code readability
ravirocx Mar 30, 2018
ef13ca5
bug fixes
ravirocx Mar 31, 2018
5ab9ae7
bug fixes
ravirocx Mar 31, 2018
34ceb97
[PinPayments] function with test cases
ravirocx Mar 31, 2018
2e443f4
bug fixes
ravirocx Mar 31, 2018
2a7ee32
testing
ravirocx Apr 28, 2018
92a0317
test commit
ravirocx Apr 28, 2018
9f1bdfd
test commit
ravirocx Apr 28, 2018
0d09a81
rectified limitations
ravirocx May 1, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
369 changes: 369 additions & 0 deletions lib/gringotts/gateways/pin_payments.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,369 @@
defmodule Gringotts.Gateways.PinPayments do
@moduledoc """
[PinPayments][home] gateway implementation.

The following features of PinPayments are implemented:

| Action | Method |
| ------ | ------ |
| Authorize | `authorize/3` |
| Capture | `capture/3` |
| Purchase | `purchase/3` |
| Store | `store/2` |
| Refund | `refund/3` |

## The `opts` argument

Most `Gringotts` API calls accept an optional `keyword` list `opts` to supply
optional arguments for transactions with the PinPayments gateway. The following keys
are supported:

| Key | Type | Remark |
| ---- | ---- | --- |
| `address` | `Address.t`| The address of the customer |
| `email_id` | `String.t` | The email address of the purchaser. |
| `description` | `String.t` | A description of the item purchased (e.g. 500g of single origin beans) |
| `ip_address` | `String.t` | The IP address of the person submitting the payment(optional) |

> PinPayments supports more optional keys and you can raise [issues] if
this is important to you.

[issues]: https://github.com/aviabird/gringotts/issues/new

## Registering your PinPayments account at `Gringotts`

| Config parameter | PinPayments secret |
| ------- | ---- |
| `:api_key` | `**API_SECRET_KEY**` |

> Your Application config **must include the `:api_key`
> fields** and would look something like this:

config :gringotts, Gringotts.Gateways.Pinpay,
api_key: "your_secret_key",


* PinPayments **does** process money in cents.
* Although PinPayments supports payments various cards. This module only
accepts payments via `VISA`, `MASTER`, and `AMERICAN EXPRESS`.

## Supported countries
PinPayments supports the countries listed
* Australia
* New Zealand

## Supported currencies
PinPayments supports the currencies listed [here](https://pinPayments.com/developers/api-reference/currency-support)

## Following the examples

1. First, set up a sample application and configure it to work with PinPayments.
- You could do that from scratch by following our [Getting Started][gs] guide.
- 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-pinpayments-account-at-gringotts).

2. Run an `iex` session with `iex -S mix` and add some variable bindings and
aliases to it (to save some time):
```
iex> alias Gringotts.{Response, CreditCard, Gateways.Pinpay}
iex> card = %CreditCard{first_name: "Jo",
last_name: "Doe",
number: "4200000000000000",
year: 2099, month: 12,
verification_code: "123", brand: "VISA"}
```

We'll be using these in the examples below.

[gs]: https://github.com/aviabird/gringotts/wiki/
[home]: https://pinPayments.com
[docs]: https://pinPayments.com/developers/api-reference
[example]: https://github.com/aviabird/gringotts_example
"""

# The Base module has the (abstract) public API, and some utility
# implementations.
use Gringotts.Gateways.Base

# The Adapter module provides the `validate_config/1`
# Add the keys that must be present in the Application config in the
# `required_config` list
use Gringotts.Adapter, required_config: [:api_key]

import Poison, only: [decode: 1]

alias Gringotts.{Money, CreditCard, Response}

@test_url "https://test-api.pinPayments.com/1/"

@doc """
Creates a new charge and returns its details.

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

PinPayments returns a **Token Id** which can be used later to:
* `capture/3` an amount.

## Example

The following example shows how one would (pre) authorize a payment of $20 on
a sample `card`.
```
iex> card = %CreditCard{first_name: "Jo",
last_name: "Doe",
number: "4200000000000000",
year: 2099, month: 12,
verification_code: "123", brand: "VISA"}
iex> money = Money.new(20, :USD)
iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.PinPayments, amount, card, opts)
```
"""

@spec authorize(Money.t(), CreditCard.t() | String.t(), keyword) :: {:ok | :error, Response}
def authorize(amount, %CreditCard{} = card, opts) do
{currency, value, _} = Money.to_integer(amount)

with {:ok, card_token_response} <-
commit(:post, "cards", card_for_token(card, opts) ++ Keyword.delete(opts, :address)),
{:ok, card_token} <- extract_card_token(card_token_response) do
params =
[
amount: value,
capture: false,
card_token: card_token,
currency: currency
] ++ Keyword.delete(opts, :address)

commit(:post, "charges", params)
end
end

def authorize(amount, card_token, opts) when is_binary(card_token) do
{currency, value, _} = Money.to_integer(amount)

params =
[
amount: value,
capture: false,
currency: currency,
card_token: card_token
] ++ Keyword.delete(opts, :address)

commit(:post, "charges", params)
end

@doc """
Transfers `amount` from the customer to the merchant.

PinPayments attempts to process a purchase on behalf of the customer, by
debiting `amount` from the customer's account by charging the customer's
`card`.

PinPayments returns a **Payment Id** which can be used later to:
* `refund/3` the amount.

## Examples

The following example shows how one would process a payment worth $20 in
one-shot, without (pre) authorization.
```
iex> card = %CreditCard{first_name: "Harry",
last_name: "Potter",
number: "4200000000000000",
year: 2099,
month: 12,
verification_code: "999",
brand: "VISA"}
iex> money = Money.new(20, :USD)
iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.PinPayments, amount, card, opts)
```
"""

@spec purchase(Money.t(), CreditCard.t() | String.t(), keyword) :: {:ok | :error, Response}
def purchase(amount, %CreditCard{} = card, opts) do
{currency, value, _} = Money.to_integer(amount)

with {:ok, card_token_response} <-
commit(:post, "cards", card_for_token(card, opts) ++ Keyword.delete(opts, :address)),
{:ok, card_token} <- extract_card_token(card_token_response) do
params =
[
amount: value,
card_token: card_token,
currency: currency
] ++ Keyword.delete(opts, :address)

commit(:post, "charges", params)
end
end

def purchase(amount, card_token, opts) when is_binary(card_token) do
{currency, value, _} = Money.to_integer(amount)

params =
[
amount: value,
card_token: card_token,
currency: currency
] ++ Keyword.delete(opts, :address)

commit(:post, "charges", params)
end

@doc """
Refunds the `amount` to the customer's account with reference to a prior transfer.

PinPayments processes a full refund worth `amount`, referencing a
previous `purchase/3` or `capture/3`.

The end customer will usually see two bookings/records on his statement.

## Example
```
iex> money = Money.new(20, :USD)
iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.PinPayments, payment_id, opts)
```
"""
@spec refund(String.t(), keyword) :: {:ok | :error, Response}
def refund(payment_id, opts) do
url = @test_url <> "charges/#{payment_id}/refunds"

commit_short(:post, url, opts)
end

@doc """
Stores the payment-source data for later use.

PinPayments can store the payment-source details, for example card or bank details
which can be used to effectively process _One-Click_ and _Recurring_ payments,
and return a card token for reference.

## Note

* _One-Click_ and _Recurring_ payments are currently not implemented.
* Payment details can be saved during a `purchase/3` or `capture/3`.

## Example

The following example shows how one would store a card (a payment-source) for
future use.
```
iex> card = %CreditCard{first_name: "Harry",
last_name: "Potter",
number: "4200000000000000",
year: 2099,
month: 12,
verification_code: "999",
brand: "VISA"}
iex> {:ok, store_result} = Gringotts.store(Gringotts.Gateways.PinPayments, card, opts)
```
"""

@spec store(CreditCard.t(), keyword) :: {:ok | :error, Response}
def store(%CreditCard{} = card, opts) do
commit(:post, "cards", card_for_token(card, opts) ++ Keyword.delete(opts, :address))
end

###############################################################################
# PRIVATE METHODS #
###############################################################################

# Makes the request to PinPay's network.
# For consistency with other gateway implementations, make your (final)
# network request in here, and parse it using another private method called
# `respond`.

defp card_for_token(card, opts) do
[
number: card.number,
name: CreditCard.full_name(card),
expiry_month: card.month |> Integer.to_string() |> String.pad_leading(2, "0"),
expiry_year: card.year |> Integer.to_string(),
cvc: card.verification_code,
address_line1: opts[:address].street1,
address_city: opts[:address].city,
address_country: opts[:address].country,
address_line2: opts[:address].street2,
address_postcode: opts[:address].postal_code,
address_state: opts[:address].region
]
end

defp commit_short(method, url, opts) do
auth_token = encoded_credentials(opts[:config][:api_key])

headers = [
{"Content-Type", "application/x-www-form-urlencoded"},
{"Authorization", auth_token}
]

HTTPoison.request(method, url, [], headers)
|> respond
end

@spec commit(atom, String.t(), keyword) :: {:ok | :error, Response}
defp commit(:post, endpoint, param) do
auth_token = encoded_credentials(param[:config][:api_key])

headers = [
{"Content-Type", "application/x-www-form-urlencoded"},
{"Authorization", auth_token}
]

url = @test_url <> "#{endpoint}"
param = Keyword.delete(param, :config)

url
|> HTTPoison.post({:form, param}, headers)
|> respond
end

defp encoded_credentials(login) do
hash = Base.encode64("#{login}:")
"Basic #{hash}"
end

defp extract_card_token(%{id: token}) do
{:ok, token}
end

defp extract_card_token({:error, error_response}) do
{:error, error_response}
end

# Parses PinPay's response and returns a `Gringotts.Response` struct
# in a `:ok`, `:error` tuple.
@spec respond(term) :: {:ok | :error, Response}

defp respond({:ok, %{status_code: code, body: body}}) when code in 200..299 do
{:ok, parsed} = decode(body)

token = parsed["response"]["card"]["token"]
id = parsed["response"]["token"]
message = parsed["response"]["status_message"]

{
:ok,
Response.success(
id: id,
token: token,
message: message,
raw: parsed,
status_code: code
)
}
end

defp respond({:ok, %{status_code: status_code, body: body}}) do
{:ok, parsed} = decode(body)
detail = parsed["detail"]
{:error, Response.error(status_code: status_code, message: detail, raw: parsed)}
end

defp respond({:error, %HTTPoison.Error{} = error}) do
{:error, Response.error(code: error.id, message: "HTTPoison says '#{error.reason}'")}
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ defmodule Gringotts.Mixfile do
{:mock, "~> 0.3.0", only: :test},
{:bypass, "~> 0.8", only: :test},
{:excoveralls, "~> 0.8", only: :test},
{:exvcr, "~> 0.10", only: :test},

# various analyses tools
{:credo, "~> 0.3", only: [:dev, :test]},
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.3.1", "50a117654dff8f8ee6958e68a65d0c2835a7e2f1aff94c1ea8f582c04fdf0bd4", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 1.4.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
"ex_money": {:hex, :ex_money, "1.1.3", "843eed0a5673206de33be47cdc06574401abc3e2d33cbcf6d74e160226791ae4", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}], "hexpm"},
"exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.8.1", "0bbf67f22c7dbf7503981d21a5eef5db8bbc3cb86e70d3798e8c802c74fa5e27", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
"exvcr": {:hex, :exvcr, "0.10.0", "5150808404d9f48dbda636f70f7f8fefd93e2433cd39f695f810e73b3a9d1736", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 0.13", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.0", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
Expand Down
Loading