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] store function with test cases #148

Open
wants to merge 23 commits into
base: pinpay
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
277 changes: 277 additions & 0 deletions lib/gringotts/gateways/pin_payments.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
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)

card_token_response =
:post
|> commit("cards", card_for_token(card, opts) ++ Keyword.delete(opts, :address))
|> extract_card_token

case card_token_response do
{:error, error} ->
{:error, error}

{:ok, token} ->
params =
[
amount: value,
capture: false,
card_token: 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 """
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

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

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({:ok, %{token: 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"]["token"]
message = parsed["response"]["status_message"]

{
:ok,
Response.success(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
91 changes: 91 additions & 0 deletions test/integration/gateways/pin_payments_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
defmodule Gringotts.Integration.Gateways.PinPaymentsTest do
# Integration tests for the PinPayments
use ExUnit.Case, async: false
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney

alias Gringotts.{
CreditCard,
Address
}

alias Gringotts.Gateways.PinPayments, as: Gateway

# @moduletag :integration

@amount Money.new(420, :AUD)

@bad_card1 %CreditCard{
first_name: "Harry",
last_name: "Potter",
number: "4100000000000001",
year: 2019,
month: 12,
verification_code: "123",
brand: "VISA"
}

@bad_card2 %CreditCard{
first_name: "Harry",
last_name: "Potter",
number: "4600000000000006",
year: 2019,
month: 12,
verification_code: "123",
brand: "VISA"
}

@bad_card3 %CreditCard{
first_name: "Harry",
last_name: "Potter",
number: "4600000000000006",
year: 2009,
month: 12,
verification_code: "123",
brand: "VISA"
}

@good_card %CreditCard{
first_name: "Harry",
last_name: "Potter",
number: "4200000000000000",
year: 2019,
month: 12,
verification_code: "123",
brand: "VISA"
}

@add %Address{
street1: "OBH",
street2: "AIT",
city: "PUNE",
region: "Maharashtra",
country: "IN",
postal_code: "411015",
phone: "8007810916"
}

@opts [
description: "hello",
email: "[email protected]",
ip_address: "1.1.1.1",
config: %{apiKey: "c4nxgznanW4XZUaEQhxS6g", pass: ""}
] ++ [address: @add]

describe "store" do
test "[Store] with CreditCard" do
use_cassette "pin_pay/store_with_valid_card" do
assert {:ok, response} = Gateway.store(@good_card, @opts)
assert response.success == true
assert response.status_code == 201
end
end

test "[Store] with bad CreditCard" do
use_cassette "pin_pay/store_with_invalid_card" do
assert {:error, response} = Gateway.store(@bad_card3, @opts)
assert response.success == false
assert response.status_code == 422
end
end
end
end