diff --git a/.credo.exs b/.credo.exs index df92ae85..9381d3f7 100644 --- a/.credo.exs +++ b/.credo.exs @@ -77,7 +77,7 @@ {Credo.Check.Readability.FunctionNames}, {Credo.Check.Readability.LargeNumbers}, - {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80}, + {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100}, {Credo.Check.Readability.ModuleAttributeNames}, {Credo.Check.Readability.ModuleDoc}, {Credo.Check.Readability.ModuleNames}, diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 00000000..aa758aff --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,7 @@ +[ + inputs: [ + "{lib,config}/**/*.{ex,exs}", # lib and config + "test/**/*.{ex,exs}", # tests + "mix.exs" + ] +] diff --git a/.scripts/inch_report.sh b/.scripts/inch_report.sh new file mode 100644 index 00000000..a352261d --- /dev/null +++ b/.scripts/inch_report.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e +bold=$(tput bold) +purple='\e[106m' +normal=$(tput sgr0) +allowed_branches="^(master)|(develop)$" + +echo -e "${bold}${purple}" +if [ $TRAVIS_PULL_REQUEST = false ]; then + if [[ $TRAVIS_BRANCH =~ $allowed_branches ]]; then + env MIX_ENV=docs mix deps.get + env MIX_ENV=docs mix inch.report + else + echo "Skipping Inch CI report because this branch does not match on /$allowed_branches/" + fi +else + echo "Skipping Inch CI report because this is a PR build" +fi +echo -e "${normal}" diff --git a/.scripts/post-commit b/.scripts/post-commit new file mode 100755 index 00000000..ada60194 --- /dev/null +++ b/.scripts/post-commit @@ -0,0 +1,12 @@ +#!/bin/sh +# +# Runs credo and the formatter on the staged files, after the commit is made +# This is purely for notification and will not halt/change your commit. + +RED='\033[1;31m' +LGRAY='\033[1;30m' +NC='\033[0m' # No Color + +printf "${RED}Running 'mix credo --strict --format=oneline' on project...${NC}\n" +mix credo --strict --format=oneline +echo diff --git a/.scripts/pre-commit b/.scripts/pre-commit new file mode 100755 index 00000000..1f473c8c --- /dev/null +++ b/.scripts/pre-commit @@ -0,0 +1,50 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# Also run the mix format task, just check though. +exec mix format --check-formatted + diff --git a/.travis.yml b/.travis.yml index 82c3f5a9..d12dcef4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,29 @@ language: elixir -elixir: - - 1.5.2 + otp_release: - - 20.1 + - 20.2 before_install: - mix local.hex --force - mix local.rebar --force - mix deps.get script: - - mix coveralls.travis --include integration + - set -e + - MIX_ENV=test mix format --check-formatted + - set +e + - mix coveralls.json after_script: - - MIX_ENV=docs mix deps.get - - MIX_ENV=docs mix inch.report + - bash <(curl -s https://codecov.io/bash) + - bash .scripts/inch_report.sh + +matrix: + include: + - elixir: "1.5.3" + script: + - mix coveralls.json + - elixir: "1.6.2" + +notifications: + email: + recipients: + - ananya95+travis@gmail.com + - ashish+travis@aviabird.com diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..f5b42e88 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +# Changelog + +## [`v1.1.0`][tag-1_1_0] (2018-04-22) + +### Added + +* **api:** Introduces a `Money` protocol ([#71][pr#71]) +* **core:** Introduces Response.t ([#119][pr#91]) +* **development:** Adds a useful mix task gringotts.new ([#78][pr#78]) +* **docs:** Adds changelog, contributing guide ([#117][pr#117]) + +### Changed + +* **api:** Deprecates use of `floats` for money amounts, check issue [#62][iss#62] ([#71][pr#71]) +* **core:** Removes payment worker, no application, no worker now after josevalim [pointed it][jose-feedback] ([#118][pr#118]) + +[iss#62]: https://github.com/aviabird/gringotts/issues/62 +[pr#71]: https://github.com/aviabird/gringotts/pulls/71 +[pr#118]: https://github.com/aviabird/gringotts/pulls/118 +[pr#91]: https://github.com/aviabird/gringotts/pulls/91 +[pr#117]: https://github.com/aviabird/gringotts/pulls/117 +[pr#78]:https://github.com/aviabird/gringotts/pulls/78 +[pr#86]:https://github.com/aviabird/gringotts/pulls/86 +[jose-feedback]:https://elixirforum.com/t/gringotts-a-complete-payment-library-for-elixir-and-phoenix-framework/11054/41 + + +## [`v1.0.2`][tag-1_0_2] (2017-12-27) + +### Added + +* New Gateway: **Trexle** + +### Changed + +* **api:** Reduced arity of public API calls by 1 + - No need to pass the name of the `worker` as argument. + +## [`v1.0.1`][tag-1_0_1] (2017-12-23) + +### Added + +* **docs:** Improved documentation - made consistent accross gateways +* **tests:** Improved test coverage + +## [`v1.0.0`][tag-1_0_0] (2017-12-20) + +### Added + +* **api:** Initial public API release. +* **core:** Single worker architecture, config fetched from `config.exs` +* **api:** Supported Gateways: + - Stripe + - MONEI + - Paymill + - WireCard + - CAMSa + +[tag-1_1_0]: https://github.com/aviabird/gringotts/compare/1.1.0...1.0.2 +[tag-1_0_2]: https://github.com/aviabird/gringotts/compare/1.0.2...1.0.1 +[tag-1_0_1]: https://github.com/aviabird/gringotts/compare/1.0.1...1.0.0 +[tag-1_0_0]: https://github.com/aviabird/gringotts/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f096ca9d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,126 @@ +# Contributing to [`gringotts`][gringotts] + +There are many ways to contribute to `gringotts`, + +1. [Integrate a new Payment Gateway][wiki-new-gateway]. +2. Expanding the feature coverage of (partially) supported gateways. +3. Moving forward on the [roadmap][roadmap] or on tasks being tracked in the + [milestones][milestones]. + +We manage our development using [milestones][milestones] and issues so if you're +a first time contributor, look out for the [`good first issue`][first-issues] +and the [`hotlist: community-help`][ch-issues] labels on the [issues][issues] +page. + +The docs are hosted on [hexdocs.pm][hexdocs] and are updated for each +release. **You must build the docs locally using `mix docs` to get the bleeding +edge developer docs.** + +The article on [Gringott's Architecture][wiki-arch] explains how API calls are +processed. + +:exclamation: ***Please base your work on the `dev` branch.*** + +[roadmap]: https://github.com/aviabird/gringotts/wiki/Roadmap +[wiki-arch]: https://github.com/aviabird/gringotts/wiki/Architecture + +# Style Guidelines + +We follow +[lexmag/elixir-style-guide](https://github.com/lexmag/elixir-style-guide) and +[rrrene/elixir-style-guide](https://github.com/rrrene/elixir-style-guide) (both +overlap a lot), and use the elixir formatter. + +To enforce these, and also to make it easier for new contributors to adhere to +our style, we've provided a collection of handy `git-hooks` under the `.scripts/` +directory. + +* `.scripts/pre-commit` Runs the `format --check-formatted` task. +* `.scripts/post-commit` Runs a customised `credo` check. + +While we do not force you to use these hooks, you could write your +very own by taking inspiration from ours :smile: + +To set the `git-hooks` as provided, go to the repo root, +```sh +cd path/to/gringotts/ +``` +and make these symbolic links: +```sh +ln -s .scripts/pre-commit .git/hooks/pre-commit +ln -s .scripts/post-commit .git/hooks/post-commit +``` + +> Note that our CI will fail your PR if you dont run `mix format` in the project +> root. + +## General Rules + +* Keep line length below 100 characters. +* Complex anonymous functions should be extracted into named functions. +* One line functions, should only take up one line! +* Pipes are great, but don't use them if they are less readable than brackets! + +## Writing documentation + +All our docs are inline and built using [`ExDocs`][exdocs]. Please take a look +at how the docs are structured for the [MONEI gateway][src-monei] for +inspiration. + +[exdocs]: https://github.com/elixir-lang/ex_doc +[src-monei]: https://github.com/aviabird/gringotts/blob/dev/lib/gringotts/gateways/monei.ex + +## Writing test cases + +> This is WIP. + +`gringotts` has mock and integration tests. We have currently used +[`bypass`][bypass] and [`mock`][mock] for mock tests, but we don't recommed +using `mock` as it constrains tests to run serially. Use [`mox`][mox] instead.\ +Take a look at [MONEI's mock tests][src-monei-tests] for inspiration. + +# PR submission checklist + +Each PR should introduce a *focussed set of changes*, and ideally not span over +unrelated modules. + +* [ ] Format the project with the Elixir formatter. + ```sh + cd path/to/gringotts/ + mix format + ``` +* [ ] Run the edited files through [credo][credo] with the `--strict` flag. + ```sh + cd path/to/gringotts/ + mix credo --strict + ``` +* [ ] Check the test coverage by running `mix coveralls`. 100% coverage is not + strictly required. +* [ ] If the PR introduces a new Gateway or just Gateway specific changes, + please format the title like so,\ + `[] ` + +> **Note** +> You can skip the first two steps if you have set up `git-hooks` as we have +> provided! + +[gringotts]: https://github.com/aviabird/gringotts +[milestones]: https://github.com/aviabird/gringotts/milestones +[issues]: https://github.com/aviabird/gringotts/issues +[first-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"good+first+issue" +[ch-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"hotfix%3A+community-help" +[hexdocs]: https://hexdocs.pm/gringotts +[credo]: https://github.com/rrrene/credo + +-------------------------------------------------------------------------------- + +> **Where to next?** +> Wanna add a new gateway? Head to our [guide][wiki-new-gateway] for that. + +[wiki-new-gateway]: https://github.com/aviabird/gringotts/wiki/Adding-a-new-Gateway +[bypass]: https://github.com/pspdfkit-labs/bypass +[mock]: https://github.com/jjh42/mock +[mox]: https://github.com/plataformatec/mox +[src-monei-tests]: https://github.com/aviabird/gringotts/blob/dev/test/gateways/monei_test.exs +[gringotts]: https://github.com/aviabird/gringotts +[docs]: https://hexdocs.pm/gringotts/Gringotts.html diff --git a/README.md b/README.md index 42e56c2f..8f4f4a82 100644 --- a/README.md +++ b/README.md @@ -5,24 +5,24 @@

- Gringotts is a payment processing library in Elixir integrating various payment gateways, this draws motivation for shopify's activemerchant gem. Checkout the Demo here. + Gringotts is a payment processing library in Elixir integrating various payment gateways, drawing motivation for Shopify's activemerchant gem. Checkout the Demo here.

Build Status Coverage Status Docs coverage Help Contribute to Open Source

-A simple and unified API to access dozens of different payment -gateways with very different internal APIs is what Gringotts has to offer you. +Gringotts offers a **simple and unified API** to access dozens of different payment +gateways with very different APIs, response schemas, documentation and jargon. ## Installation -### From hex.pm +### From [`hex.pm`][hexpm] -Make the following changes to the `mix.exs` file. - -Add gringotts to the list of dependencies. +Add `gringotts` to the list of dependencies of your application. ```elixir +# your mix.exs + def deps do [ {:gringotts, "~> 1.0"}, @@ -33,35 +33,33 @@ def deps do end ``` -Add gringotts to the list of applications to be started. -```elixir -def application do - [ - extra_applications: [:gringotts] - ] -end -``` - ## Usage -This simple example demonstrates how a purchase can be made using a person's credit card details. +This simple example demonstrates how a `purchase` can be made using a sample +credit card using the [MONEI][monei] gateway. -Add configs in `config/config.exs` file. +One must "register" their account with `gringotts` ie, put all the +authentication details in the Application config. Usually via +`config/config.exs` ```elixir +# config/config.exs + config :gringotts, Gringotts.Gateways.Monei, - adapter: Gringotts.Gateways.Monei, userId: "your_secret_user_id", password: "your_secret_password", entityId: "your_secret_channel_id" ``` -Copy and paste this code in your module +Copy and paste this code in a module or an `IEx` session ```elixir alias Gringotts.Gateways.Monei alias Gringotts.{CreditCard} +# a fake sample card that will work now because the Gateway is by default +# in "test" mode. + card = %CreditCard{ first_name: "Harry", last_name: "Potter", @@ -71,9 +69,10 @@ card = %CreditCard{ brand: "VISA" } +# a sum of $42 amount = Money.new(42, :USD) -case Gringotts.purchase(Monei, amount, card, opts) do +case Gringotts.purchase(Monei, amount, card) do {:ok, %{id: id}} -> IO.puts("Payment authorized, reference token: '#{id}'") @@ -82,6 +81,9 @@ case Gringotts.purchase(Monei, amount, card, opts) do end ``` +[hexpm]: https://hex.pm/packages/gringotts +[monei]: http://www.monei.net + ## Supported Gateways | Gateway | Supported countries | @@ -105,9 +107,32 @@ end ## Road Map -- Support more gateways on an on-going basis. -- Each gateway request is hosted in a worker process and supervised. +Apart from supporting more and more gateways, we also keep a somewhat detailed +plan for the future on our [wiki][roadmap]. + +## FAQ + +#### 1. What's with the name? "Gringotts"? + +Gringotts has a nice ring to it. Also [this][reason]. + +#### 2. What is the worker doing in the middle? + +We wanted to "supervise" our payments, and power utilities to process recurring +payments, subscriptions with it. But yes, as of now, it is a bottle neck and +unnecessary. + +It's slated to be removed in [`v2.0.0`][milestone-2_0_0_alpha] and any +supervised/async/parallel work can be explicitly managed via native elixir +constructs. + +**In fact, it's already been removed from our [dev](#) branch.** + +[milestone-2_0_0_alpha]: https://github.com/aviabird/gringotts/milestone/3 +[reason]: http://harrypotter.wikia.com/wiki/Gringotts ## License MIT + +[roadmap]: https://github.com/aviabird/gringotts/wiki/Roadmap diff --git a/lib/gringotts.ex b/lib/gringotts.ex index 13d0eb07..161d1ae8 100644 --- a/lib/gringotts.ex +++ b/lib/gringotts.ex @@ -6,21 +6,21 @@ defmodule Gringotts do easy for merchants to use multiple gateways. All gateways must conform to the API as described in this module, but can also support more gateway features than those required by Gringotts. - + ## Standard API arguments All requests to Gringotts are served by a supervised worker, this might be made optional in future releases. - + ### `gateway` (Module) Name - + The `gateway` to which this request is made. This is required in all API calls because Gringotts supports multiple Gateways. #### Example If you've configured Gringotts to work with Stripe, you'll do this to make an `authorization` request: - + Gringotts.authorize(Gingotts.Gateways.Stripe, other args ...) ### `amount` _and currency_ @@ -39,7 +39,7 @@ defmodule Gringotts do Otherwise, just wrap your `amount` with the `currency` together in a `Map` like so, money = %{value: Decimal.new("100.50"), currency: "USD"} - + > When this highly precise `amount` is serialized into the network request, we > use a potentially lossy `Gringotts.Money.to_string/1` or > `Gringotts.Money.to_integer/1` to perform rounding (if required) using the @@ -49,14 +49,14 @@ defmodule Gringotts do > STRONGLY RECOMMEND that merchants perform any required rounding and handle > remainders in their application logic -- before passing the `amount` to > Gringotts's API.** - + #### Example If you use `ex_money` in your project, and want to make an authorization for $2.99 to the `XYZ` Gateway, you'll do the following: # the money lib is aliased as "MoneyLib" - + amount = MoneyLib.new("2.99", :USD) Gringotts.authorize(Gringotts.Gateways.XYZ, amount, some_card, extra_options) @@ -65,7 +65,7 @@ defmodule Gringotts do [money]: https://hexdocs.pm/money/Money.html [iss-money-lib-support]: https://github.com/aviabird/gringotts/projects/3#card-6801146 [wiki-half-even]: https://en.wikipedia.org/wiki/Rounding#Round_half_to_even - + ### `card`, a payment source Gringotts provides a `Gringotts.CreditCard` type to hold card parameters @@ -78,7 +78,7 @@ defmodule Gringotts do gateways might support payment via other instruments such as e-wallets, vouchers, bitcoins or banks. Support for these instruments is planned in future releases. - + %CreditCard { first_name: "Harry", last_name: "Potter", @@ -93,18 +93,18 @@ defmodule Gringotts do `opts` is a `keyword` list of other options/information accepted by the gateway. The format, use and structure is gateway specific and documented in the Gateway's docs. - + ## Configuration - + Merchants must provide Gateway specific configuration in their application config in the usual elixir style. The required and optional fields are documented in every Gateway. - + > The required config keys are validated at runtime, as they include > authentication information. See `Gringotts.Adapter.validate_config/2`. - + ### Global config - + This is set using the `:global_config` key once in your application. #### `:mode` @@ -120,9 +120,9 @@ defmodule Gringotts do environments. * `:prod` -- for live environment, all requests will reach the financial and banking networks. Switch to this in your application's `:prod` environment. - + **Example** - + config :gringotts, :global_config, # for live environment mode: :prod @@ -133,16 +133,9 @@ defmodule Gringotts do following format: config :gringotts, Gringotts.Gateways.XYZ, - adapter: Gringotts.Gateways.XYZ, # some_documented_key: associated_value # some_other_key: another_value - - > ***Note!*** - > The config key matches the `:adapter`! Both ***must*** be the Gateway module - > name! """ - - import GenServer, only: [call: 2] @doc """ Performs a (pre) Authorize operation. @@ -166,8 +159,8 @@ defmodule Gringotts do {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.XYZ, amount, card, opts) """ def authorize(gateway, amount, card, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:authorize, gateway, amount, card, opts}) + config = get_and_validate_config(gateway) + gateway.authorize(amount, card, [{:config, config} | opts]) end @doc """ @@ -178,7 +171,7 @@ defmodule Gringotts do * multiple captures, per authorization ## Example - + To capture $4.20 on a previously authorized payment worth $4.20 by referencing the obtained authorization `id` with the `XYZ` gateway, @@ -188,9 +181,9 @@ defmodule Gringotts do card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} Gringotts.capture(Gringotts.Gateways.XYZ, amount, auth_result.id, opts) """ - def capture(gateway, id, amount, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:capture, gateway, id, amount, opts}) + def capture(gateway, id, amount, opts \\ []) do + config = get_and_validate_config(gateway) + gateway.capture(id, amount, [{:config, config} | opts]) end @doc """ @@ -202,14 +195,14 @@ defmodule Gringotts do This method _can_ be implemented as a chained call to `authorize/3` and `capture/3`. But it must be implemented as a single call to the Gateway if it provides a specific endpoint or action for this. - + > ***Note!** > All gateways must implement (atleast) this method. ## Example To process a purchase worth $4.2, with the `XYZ` gateway, - + amount = Money.new("4.2", :USD) # IF YOU DON'T USE ex_money # amount = %{value: Decimal.new("4.2"), currency: "EUR"} @@ -217,8 +210,8 @@ defmodule Gringotts do Gringotts.purchase(Gringotts.Gateways.XYZ, amount, card, opts) """ def purchase(gateway, amount, card, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:purchase, gateway, amount, card, opts}) + config = get_and_validate_config(gateway) + gateway.purchase(amount, card, [{:config, config} | opts]) end @doc """ @@ -236,16 +229,16 @@ defmodule Gringotts do # amount = %{value: Decimal.new("4.2"), currency: "EUR"} Gringotts.purchase(Gringotts.Gateways.XYZ, amount, id, opts) """ - def refund(gateway, amount, id, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:refund, gateway, amount, id, opts}) + def refund(gateway, amount, id, opts \\ []) do + config = get_and_validate_config(gateway) + gateway.refund(amount, id, [{:config, config} | opts]) end @doc """ Stores the payment-source data for later use, returns a `token`. > The token must be returned in the `Response.authorization` field. - + ## Note This usually enables _One-Click_ and _Recurring_ payments. @@ -257,9 +250,9 @@ defmodule Gringotts do card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} Gringotts.store(Gringotts.Gateways.XYZ, card, opts) """ - def store(gateway, card, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:store, gateway, card, opts}) + def store(gateway, card, opts \\ []) do + config = get_and_validate_config(gateway) + gateway.store(card, [{:config, config} | opts]) end @doc """ @@ -271,13 +264,13 @@ defmodule Gringotts do ## Example To unstore with the `XYZ` gateway, - + token = "some_privileged_customer" Gringotts.unstore(Gringotts.Gateways.XYZ, token) """ - def unstore(gateway, token, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:unstore, gateway, token, opts}) + def unstore(gateway, token, opts \\ []) do + config = get_and_validate_config(gateway) + gateway.unstore(token, [{:config, config} | opts]) end @doc """ @@ -296,14 +289,16 @@ defmodule Gringotts do id = "some_previously_obtained_token" Gringotts.void(Gringotts.Gateways.XYZ, id, opts) """ - def void(gateway, id, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:void, gateway, id, opts}) + def void(gateway, id, opts \\ []) do + config = get_and_validate_config(gateway) + gateway.void(id, [{:config, config} | opts]) end - defp validate_config(gateway) do - # Keep the key name and adapter the same in the config in application + defp get_and_validate_config(gateway) do config = Application.get_env(:gringotts, gateway) + # The following call to validate_config might raise an error gateway.validate_config(config) + global_config = Application.get_env(:gringotts, :global_config) || [mode: :test] + Keyword.merge(global_config, config) end end diff --git a/lib/gringotts/adapter.ex b/lib/gringotts/adapter.ex index d6e250b6..f4380b87 100644 --- a/lib/gringotts/adapter.ex +++ b/lib/gringotts/adapter.ex @@ -1,12 +1,42 @@ defmodule Gringotts.Adapter do - @moduledoc ~S""" - Adapter module is currently holding the validation part. + @moduledoc """ + Validates the "required" configuration. - This modules is being `used` by all the payment gateways and raises a run-time - error for the missing configurations which are passed by the gateways to - `validate_config` method. + All gateway modules must `use` this module, which provides a run-time + configuration validator. - Raises an exception `ArgumentError` if the config is not as per the `@required_config` + Gringotts picks up the merchant's Gateway authentication secrets from the + Application config. The configuration validator can be customized by providing + a list of `required_config` keys. The validator will check if these keys are + available at run-time, before each call to the Gateway. + + ## Example + + Say a merchant must provide his `secret_user_name` and `secret_password` to + some Gateway `XYZ`. Then, `Gringotts` expects that the `GatewayXYZ` module + would use `Adapter` in the following manner: + + ``` + defmodule Gringotts.Gateways.GatewayXYZ do + + use Gringotts.Adapter, required_config: [:secret_user_name, :secret_password] + use Gringotts.Gateways.Base + + # the rest of the implentation + end + ``` + + And, the merchant woud provide these secrets in the Application config, + possibly via `config/config.exs` like so, + ``` + # config/config.exs + + config :gringotts, Gringotts.Gateways.GatewayXYZ, + adapter: Gringotts.Gateways.GatewayXYZ, + secret_user_name: "some_really_secret_user_name", + secret_password: "some_really_secret_password" + + ``` """ defmacro __using__(opts) do @@ -14,21 +44,33 @@ defmodule Gringotts.Adapter do @required_config opts[:required_config] || [] @doc """ - Validates the config dynamically depending on what is the value of `required_config` + Catches gateway configuration errors. + + Raises a run-time `ArgumentError` if any of the `required_config` values + is not available or missing from the Application config. """ - def validate_config(config) do - missing_keys = Enum.reduce(@required_config, [], fn(key, missing_keys) -> - if config[key] in [nil, ""], do: [key | missing_keys], else: missing_keys - end) + def validate_config(config) when is_list(config) do + missing_keys = + Enum.reduce(@required_config, [], fn key, missing_keys -> + if config[key] in [nil, ""], do: [key | missing_keys], else: missing_keys + end) + raise_on_missing_config(missing_keys, config) end + def validate_config(config) when is_map(config) do + config + |> Enum.into([]) + |> validate_config + end + defp raise_on_missing_config([], _config), do: :ok + defp raise_on_missing_config(key, config) do raise ArgumentError, """ - expected #{inspect key} to be set, got: #{inspect config} + expected #{inspect(key)} to be set, got: #{inspect(config)} """ end end end -end \ No newline at end of file +end diff --git a/lib/gringotts/address.ex b/lib/gringotts/address.ex index e1c50c95..57c8dd5c 100644 --- a/lib/gringotts/address.ex +++ b/lib/gringotts/address.ex @@ -1,3 +1,5 @@ defmodule Gringotts.Address do + @moduledoc false + defstruct [:street1, :street2, :city, :region, :country, :postal_code, :phone] end diff --git a/lib/gringotts/application.ex b/lib/gringotts/application.ex deleted file mode 100644 index f852de3b..00000000 --- a/lib/gringotts/application.ex +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Gringotts.Application do - @moduledoc ~S""" - Has the supervision tree which monitors all the workers - that are handling the payments. - """ - use Application - - def start(_type, _args) do - import Supervisor.Spec, warn: false - app_config = Application.get_all_env(:gringotts) - adapters = Enum.filter(app_config, fn({_, klist}) -> klist != [] end) - |> Enum.map(fn({_, klist}) -> Keyword.get(klist, :adapter) end) - - children = [ - # Define workers and child supervisors to be supervised - # worker(Gringotts.Worker, [arg1, arg2, arg3]) - worker( - Gringotts.Worker, - [ - adapters, # gateways - app_config, # options(config from application) - # Since we just have one worker handling all the incoming - # requests so this name remains fixed - [name: :payment_worker] - ]) - ] - - # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html - # for other strategies and supported options - opts = [strategy: :one_for_one, name: Gringotts.Supervisor] - Supervisor.start_link(children, opts) - end -end diff --git a/lib/gringotts/credit_card.ex b/lib/gringotts/credit_card.ex index 32a2a50c..e98481a0 100644 --- a/lib/gringotts/credit_card.ex +++ b/lib/gringotts/credit_card.ex @@ -1,9 +1,10 @@ defmodule Gringotts.CreditCard do @moduledoc """ - Defines a `Struct` for (credit) cards and some utilities. + Defines a `struct` for (credit) cards and some utilities. """ defstruct [:number, :month, :year, :first_name, :last_name, :verification_code, :brand] + @typedoc """ Represents a Credit Card. @@ -29,23 +30,24 @@ defmodule Gringotts.CreditCard do [mo]: http://www.maestrocard.com/gateway/index.html [dc]: http://www.dinersclub.com/ """ - @type t :: %__MODULE__{number: String.t, - month: 1..12, - year: non_neg_integer, - first_name: String.t, - last_name: String.t, - verification_code: String.t, - brand: String.t} + @type t :: %__MODULE__{ + number: String.t(), + month: 1..12, + year: non_neg_integer, + first_name: String.t(), + last_name: String.t(), + verification_code: String.t(), + brand: String.t() + } @doc """ Returns the full name of the card holder. Joins `first_name` and `last_name` with a space in between. """ - @spec full_name(t) :: String.t + @spec full_name(t) :: String.t() def full_name(card) do name = "#{card.first_name} #{card.last_name}" - String.trim(name) + String.trim(name) end - end diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 04762db3..2612bb7a 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -47,11 +47,15 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Notes - Authorize.Net supports [multiple currencies][currencies] however, multiple - currencies in one account are not supported. A merchant would need multiple - Authorize.Net accounts, one for each chosen currency. - - > Currently, `Gringotts` supports single Authorize.Net account configuration. + 1. Though Authorize.Net supports [multiple currencies][currencies] however, + multiple currencies in one account are not supported in _this_ module. A + merchant would need multiple Authorize.Net accounts, one for each chosen + currency. + 2. The responses of this module include a non-standard field: `:cavv_result`. + - `:cavv_result` is the "cardholder authentication verification response + code". In case of Mastercard transactions, this field will always be + `nil`. Please refer the "Response Format" section in the [docs][docs] for + more details. [currencies]: https://community.developer.authorize.net/t5/The-Authorize-Net-Developer-Blog/Authorize-Net-UK-Europe-Update/ba-p/35957 @@ -65,7 +69,6 @@ defmodule Gringotts.Gateways.AuthorizeNet do fields** and would look something like this: config :gringotts, Gringotts.Gateways.AuthorizeNet, - adapter: Gringotts.Gateways.AuthorizeNet, name: "name_provided_by_authorize_net", transaction_key: "transactionKey_provided_by_authorize_net" @@ -86,23 +89,20 @@ defmodule Gringotts.Gateways.AuthorizeNet do that gives you a pre-configured sample app ready-to-go. + You could use the same config or update it the with your "secrets" [above](#module-configuring-your-authorizenet-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.AuthorizeNet} - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} - iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} - ``` + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in + [this gist][authorize_net.iex.exs] to introduce a set of handy bindings and + aliases. - We'll be using these in the examples below. + We'll be using these bindings in the examples below. [example-repo]: https://github.com/aviabird/gringotts_example + [iex-docs]: https://hexdocs.pm/iex/IEx.html#module-the-iex-exs-file + [authorize_net.iex.exs]: https://gist.github.com/oyeb/b1030058bda1fa9a3d81f1cf30723695 [gs]: https://github.com/aviabird/gringotts/wiki """ import XmlBuilder - import XmlToMap use Gringotts.Gateways.Base use Gringotts.Adapter, required_config: [:name, :transaction_key] @@ -110,7 +110,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @test_url "https://apitest.authorize.net/xml/v1/request.api" @production_url "https://api.authorize.net/xml/v1/request.api" - @header [{"Content-Type", "text/xml"}] + @headers [{"Content-Type", "text/xml"}] @transaction_type %{ purchase: "authCaptureTransaction", @@ -134,8 +134,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do Charges a credit `card` for the specified `amount`. It performs `authorize` and `capture` at the [same time][auth-cap-same-time]. - Authorize.Net returns `transId` (available in the `Response.authorization` - field) which can be used to: + Authorize.Net returns `transId` (available in the `Response.id` field) which + can be used to: * `refund/3` a settled transaction. * `void/2` a transaction. @@ -168,7 +168,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ] ## Example - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD) iex> opts = [ ref_id: "123456", order: %{invoice_number: "INV-12345", description: "Product Description"}, @@ -180,11 +180,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} iex> result = Gringotts.purchase(Gringotts.Gateways.AuthorizeNet, amount, card, opts) """ - @spec purchase(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} + @spec purchase(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response.t()} def purchase(amount, payment, opts) do request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:purchase]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -196,8 +195,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do To transfer the funds to merchant's account follow this up with a `capture/3`. - Authorize.Net returns a `transId` (available in the `Response.authorization` - field) which can be used for: + Authorize.Net returns a `transId` (available in the `Response.id` field) which + can be used for: * `capture/3` an authorized transaction. * `void/2` a transaction. @@ -227,9 +226,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do customer_ip: String ] - ## Example - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD) iex> opts = [ ref_id: "123456", order: %{invoice_number: "INV-12345", description: "Product Description"}, @@ -241,11 +239,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} iex> result = Gringotts.authorize(Gringotts.Gateways.AuthorizeNet, amount, card, opts) """ - @spec authorize(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} + @spec authorize(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response.t()} def authorize(amount, payment, opts) do request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:authorize]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -254,8 +251,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do `amount` is transferred to the merchant account by Authorize.Net when it is smaller or equal to the amount used in the pre-authorization referenced by `id`. - Authorize.Net returns a `transId` (available in the `Response.authorization` - field) which can be used to: + Authorize.Net returns a `transId` (available in the `Response.id` field) which + can be used to: * `refund/3` a settled transaction. * `void/2` a transaction. @@ -280,15 +277,14 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> opts = [ ref_id: "123456" ] - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD) iex> id = "123456" iex> result = Gringotts.capture(Gringotts.Gateways.AuthorizeNet, id, amount, opts) """ - @spec capture(String.t(), Money.t(), Keyword.t()) :: {:ok | :error, Response} + @spec capture(String.t(), Money.t(), Keyword.t()) :: {:ok | :error, Response.t()} def capture(id, amount, opts) do request_data = normal_capture(amount, id, opts, @transaction_type[:capture]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -311,14 +307,13 @@ defmodule Gringotts.Gateways.AuthorizeNet do ref_id: "123456" ] iex> id = "123456" - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD) iex> result = Gringotts.refund(Gringotts.Gateways.AuthorizeNet, amount, id, opts) """ - @spec refund(Money.t(), String.t(), Keyword.t()) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), Keyword.t()) :: {:ok | :error, Response.t()} def refund(amount, id, opts) do request_data = normal_refund(amount, id, opts, @transaction_type[:refund]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -340,11 +335,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> id = "123456" iex> result = Gringotts.void(Gringotts.Gateways.AuthorizeNet, id, opts) """ - @spec void(String.t(), Keyword.t()) :: {:ok | :error, Response} + @spec void(String.t(), Keyword.t()) :: {:ok | :error, Response.t()} def void(id, opts) do request_data = normal_void(id, opts, @transaction_type[:void]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -394,17 +388,16 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, card, opts) """ - @spec store(CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} + @spec store(CreditCard.t(), Keyword.t()) :: {:ok | :error, Response.t()} def store(card, opts) do request_data = if opts[:customer_profile_id] do - card |> create_customer_payment_profile(opts) |> generate + card |> create_customer_payment_profile(opts) |> generate(format: :none) else - card |> create_customer_profile(opts) |> generate + card |> create_customer_profile(opts) |> generate(format: :none) end - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -415,48 +408,37 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Example iex> id = "123456" - iex> opts = [] - iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, id, opts) + iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, id) """ - @spec unstore(String.t(), Keyword.t()) :: {:ok | :error, Response} + @spec unstore(String.t(), Keyword.t()) :: {:ok | :error, Response.t()} def unstore(customer_profile_id, opts) do - request_data = customer_profile_id |> delete_customer_profile(opts) |> generate - response_data = commit(:post, request_data, opts) - respond(response_data) + request_data = customer_profile_id |> delete_customer_profile(opts) |> generate(format: :none) + commit(request_data, opts) end - # method to make the api request with params - defp commit(method, payload, opts) do - path = base_url(opts) - headers = @header - HTTPoison.request(method, path, payload, headers) + # method to make the API request with params + defp commit(payload, opts) do + opts + |> base_url() + |> HTTPoison.post(payload, @headers) + |> respond() end - # Function to return a response - defp respond({:ok, %{body: body, status_code: 200}}) do - raw_response = naive_map(body) - response_type = ResponseHandler.check_response_type(raw_response) - response_check(raw_response[response_type], raw_response) - end + defp respond({:ok, %{body: body, status_code: 200}}), do: ResponseHandler.respond(body) defp respond({:ok, %{body: body, status_code: code}}) do - {:error, Response.error(params: body, error_code: code)} + {:error, %Response{raw: body, status_code: code}} end defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, Response.error(error_code: error.id, message: "HTTPoison says '#{error.reason}'")} - end - - # Functions to send successful and error responses depending on message received - # from gateway. - - defp response_check(%{"messages" => %{"resultCode" => "Ok"}}, raw_response) do - {:ok, ResponseHandler.parse_gateway_success(raw_response)} - end - - defp response_check(%{"messages" => %{"resultCode" => "Error"}}, raw_response) do - {:error, ResponseHandler.parse_gateway_error(raw_response)} + { + :error, + %Response{ + reason: "network related failure", + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]" + } + } end ############################################################################## @@ -471,7 +453,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_order_id(opts), add_purchase_transaction_request(amount, transaction_type, payment, opts) ]) - |> generate + |> generate(format: :none) end # function for formatting the request for normal capture @@ -480,9 +462,9 @@ defmodule Gringotts.Gateways.AuthorizeNet do |> element(%{xmlns: @aut_net_namespace}, [ add_merchant_auth(opts[:config]), add_order_id(opts), - add_capture_transaction_request(amount, id, transaction_type, opts) + add_capture_transaction_request(amount, id, transaction_type) ]) - |> generate + |> generate(format: :none) end # function to format the request for normal refund @@ -493,7 +475,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_order_id(opts), add_refund_transaction_request(amount, id, opts, transaction_type) ]) - |> generate + |> generate(format: :none) end # function to format the request for normal void operation @@ -507,7 +489,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_ref_trans_id(id) ]) ]) - |> generate + |> generate(format: :none) end defp create_customer_payment_profile(card, opts) do @@ -576,7 +558,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_transaction_type(transaction_type), add_amount(amount), add_payment_source(payment), - add_invoice(transaction_type, opts), + add_invoice(opts), add_tax_fields(opts), add_duty_fields(opts), add_shipping_fields(opts), @@ -585,7 +567,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ]) end - defp add_capture_transaction_request(amount, id, transaction_type, opts) do + defp add_capture_transaction_request(amount, id, transaction_type) do element(:transactionRequest, [ add_transaction_type(transaction_type), add_amount(amount), @@ -641,7 +623,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ]) end - defp add_invoice(transactionType, opts) do + defp add_invoice(opts) do element([ element(:order, [ element(:invoiceNumber, opts[:order][:invoice_number]), @@ -747,88 +729,176 @@ defmodule Gringotts.Gateways.AuthorizeNet do end end + ################################################################################## + # RESPONSE_HANDLER MODULE # + # # + ################################################################################## + defmodule ResponseHandler do @moduledoc false alias Gringotts.Response - @response_type %{ - auth_response: "authenticateTestResponse", - transaction_response: "createTransactionResponse", - error_response: "ErrorResponse", - customer_profile_response: "createCustomerProfileResponse", - customer_payment_profile_response: "createCustomerPaymentProfileResponse", - delete_customer_profile: "deleteCustomerProfileResponse" + @supported_response_types [ + "authenticateTestResponse", + "createTransactionResponse", + "ErrorResponse", + "createCustomerProfileResponse", + "createCustomerPaymentProfileResponse", + "deleteCustomerProfileResponse" + ] + + @avs_code_translator %{ + # The street address matched, but the postal code did not. + "A" => {"pass", "fail"}, + # No address information was provided. + "B" => {nil, nil}, + # The AVS check returned an error. + "E" => {"fail", nil}, + # The card was issued by a bank outside the U.S. and does not support AVS. + "G" => {nil, nil}, + # Neither the street address nor postal code matched. + "N" => {"fail", "fail"}, + # AVS is not applicable for this transaction. + "P" => {nil, nil}, + # Retry — AVS was unavailable or timed out. + "R" => {nil, nil}, + # AVS is not supported by card issuer. + "S" => {nil, nil}, + # Address information is unavailable. + "U" => {nil, nil}, + # The US ZIP+4 code matches, but the street address does not. + "W" => {"fail", "pass"}, + # Both the street address and the US ZIP+4 code matched. + "X" => {"pass", "pass"}, + # The street address and postal code matched. + "Y" => {"pass", "pass"}, + # The postal code matched, but the street address did not. + "Z" => {"fail", "pass"}, + # fallback in-case of absence + "" => {nil, nil}, + # fallback in-case of absence + nil => {nil, nil} } - def parse_gateway_success(raw_response) do - response_type = check_response_type(raw_response) - token = raw_response[response_type]["transactionResponse"]["transId"] - message = raw_response[response_type]["messages"]["message"]["text"] - avs_result = raw_response[response_type]["transactionResponse"]["avsResultCode"] - cvc_result = raw_response[response_type]["transactionResponse"]["cavvResultCode"] + @cvc_code_translator %{ + "M" => "CVV matched.", + "N" => "CVV did not match.", + "P" => "CVV was not processed.", + "S" => "CVV should have been present but was not indicated.", + "U" => "The issuer was unable to process the CVV check.", + # fallback in-case of absence + nil => nil + } + + @cavv_code_translator %{ + "" => "CAVV not validated.", + "0" => "CAVV was not validated because erroneous data was submitted.", + "1" => "CAVV failed validation.", + "2" => "CAVV passed validation.", + "3" => "CAVV validation could not be performed; issuer attempt incomplete.", + "4" => "CAVV validation could not be performed; issuer system error.", + "5" => "Reserved for future use.", + "6" => "Reserved for future use.", + "7" => + "CAVV failed validation, but the issuer is available. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "8" => + "CAVV passed validation and the issuer is available. Valid for U.S.-issued card submitted to non-U.S. acquirer.", + "9" => + "CAVV failed validation and the issuer is unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "A" => + "CAVV passed validation but the issuer unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "B" => "CAVV passed validation, information only, no liability shift.", + # fallback in-case of absence + nil => nil + } - [] - |> status_code(200) - |> set_token(token) + def respond(body) do + response_map = XmlToMap.naive_map(body) + + case extract_gateway_response(response_map) do + :undefined_response -> + { + :error, + %Response{ + reason: "Undefined response from AunthorizeNet", + raw: body, + message: "You might wish to open an issue with Gringotts." + } + } + + result -> + build_response(result, %Response{raw: body, status_code: 200}) + end + end + + def extract_gateway_response(response_map) do + # The type of the response should be supported + # Find the first non-nil from the above, if all are `nil`... + # We are in trouble! + @supported_response_types + |> Stream.map(&Map.get(response_map, &1, nil)) + |> Enum.find(:undefined_response, & &1) + end + + defp build_response(%{"messages" => %{"resultCode" => "Ok"}} = result, base_response) do + {:ok, ResponseHandler.parse_gateway_success(result, base_response)} + end + + defp build_response(%{"messages" => %{"resultCode" => "Error"}} = result, base_response) do + {:error, ResponseHandler.parse_gateway_error(result, base_response)} + end + + def parse_gateway_success(result, base_response) do + id = result["transactionResponse"]["transId"] + message = result["messages"]["message"]["text"] + avs_result = result["transactionResponse"]["avsResultCode"] + cvc_result = result["transactionResponse"]["cvvResultCode"] + cavv_result = result["transactionResponse"]["cavvResultCode"] + gateway_code = result["messages"]["message"]["code"] + + base_response + |> set_id(id) |> set_message(message) + |> set_gateway_code(gateway_code) |> set_avs_result(avs_result) |> set_cvc_result(cvc_result) - |> set_params(raw_response) - |> set_success(true) - |> handle_opts + |> set_cavv_result(cavv_result) end - def parse_gateway_error(raw_response) do - response_type = check_response_type(raw_response) + def parse_gateway_error(result, base_response) do + message = result["messages"]["message"]["text"] + gateway_code = result["messages"]["message"]["code"] - {message, error_code} = - if raw_response[response_type]["transactionResponse"]["errors"] do - { - raw_response[response_type]["messages"]["message"]["text"] <> - " " <> - raw_response[response_type]["transactionResponse"]["errors"]["error"]["errorText"], - raw_response[response_type]["transactionResponse"]["errors"]["error"]["errorCode"] - } - else - { - raw_response[response_type]["messages"]["message"]["text"], - raw_response[response_type]["messages"]["message"]["code"] - } - end + error_text = result["transactionResponse"]["errors"]["error"]["errorText"] + error_code = result["transactionResponse"]["errors"]["error"]["errorCode"] + reason = "#{error_text} [Error code (#{error_code})]" - [] - |> status_code(200) + base_response |> set_message(message) - |> set_error_code(error_code) - |> set_params(raw_response) - |> set_success(false) - |> handle_opts + |> set_gateway_code(gateway_code) + |> set_reason(reason) end - def check_response_type(raw_response) do - cond do - raw_response[@response_type[:transaction_response]] -> "createTransactionResponse" - raw_response[@response_type[:error_response]] -> "ErrorResponse" - raw_response[@response_type[:customer_profile_response]] -> "createCustomerProfileResponse" - raw_response[@response_type[:customer_payment_profile_response]] -> "createCustomerPaymentProfileResponse" - raw_response[@response_type[:delete_customer_profile]] -> "deleteCustomerProfileResponse" - end + ############################################################################ + # HELPERS # + ############################################################################ + + defp set_id(response, id), do: %{response | id: id} + defp set_message(response, message), do: %{response | message: message} + defp set_gateway_code(response, code), do: %{response | gateway_code: code} + defp set_reason(response, body), do: %{response | reason: body} + + defp set_avs_result(response, avs_code) do + {street, zip_code} = @avs_code_translator[avs_code] + %{response | avs_result: %{street: street, zip_code: zip_code}} end - defp set_token(opts, token), do: [{:authorization, token} | opts] - defp set_success(opts, value), do: [{:success, value} | opts] - defp status_code(opts, code), do: [{:status, code} | opts] - defp set_message(opts, message), do: [{:message, message} | opts] - defp set_avs_result(opts, result), do: [{:avs, result} | opts] - defp set_cvc_result(opts, result), do: [{:cvc, result} | opts] - defp set_params(opts, raw_response), do: [{:params, raw_response} | opts] - defp set_error_code(opts, code), do: [{:error, code} | opts] - - defp handle_opts(opts) do - case Keyword.fetch(opts, :success) do - {:ok, true} -> Response.success(opts) - {:ok, false} -> Response.error(opts) - end + defp set_cvc_result(response, cvv_code) do + %{response | cvc_result: @cvc_code_translator[cvv_code]} + end + + defp set_cavv_result(response, cavv_code) do + Map.put(response, :cavv_result, @cavv_code_translator[cavv_code]) end end end diff --git a/lib/gringotts/gateways/base.ex b/lib/gringotts/gateways/base.ex index 92f8cfd2..145b4b7a 100644 --- a/lib/gringotts/gateways/base.ex +++ b/lib/gringotts/gateways/base.ex @@ -1,15 +1,29 @@ defmodule Gringotts.Gateways.Base do + @moduledoc """ + Dummy implementation of the Gringotts API + + All gateway implementations must `use` this module as it provides (pseudo) + implementations for the all methods of the Gringotts API. + + In case `GatewayXYZ` does not implement `unstore`, the following call would + not raise an error: + ``` + Gringotts.unstore(GatewayXYZ, "some_registration_id") + ``` + because this module provides an implementation. + """ + alias Gringotts.Response defmacro __using__(_) do quote location: :keep do @doc false - def purchase(_amount, _card_or_id, _opts) do + def purchase(_amount, _card_or_id, _opts) do not_implemented() end @doc false - def authorize(_amount, _card_or_id, _opts) do + def authorize(_amount, _card_or_id, _opts) do not_implemented() end @@ -38,33 +52,18 @@ defmodule Gringotts.Gateways.Base do not_implemented() end - defp http(method, path, params \\ [], opts \\ []) do - credentials = Keyword.get(opts, :credentials) - headers = [{"Content-Type", "application/x-www-form-urlencoded"}] - data = params_to_string(params) - - HTTPoison.request(method, path, data, headers, [hackney: [basic_auth: credentials]]) - end - - defp money_to_cents(amount) when is_float(amount) do - trunc(amount * 100) - end - - defp money_to_cents(amount) do - amount * 100 - end - - defp params_to_string(params) do - params |> Enum.filter(fn {_k, v} -> v != nil end) - |> URI.encode_query - end - @doc false defp not_implemented do {:error, Response.error(code: :not_implemented)} end - defoverridable [purchase: 3, authorize: 3, capture: 3, void: 2, refund: 3, store: 2, unstore: 2] + defoverridable purchase: 3, + authorize: 3, + capture: 3, + void: 2, + refund: 3, + store: 2, + unstore: 2 end end end diff --git a/lib/gringotts/gateways/bogus.ex b/lib/gringotts/gateways/bogus.ex index 14140f39..ada7575d 100644 --- a/lib/gringotts/gateways/bogus.ex +++ b/lib/gringotts/gateways/bogus.ex @@ -1,4 +1,6 @@ defmodule Gringotts.Gateways.Bogus do + @moduledoc false + use Gringotts.Gateways.Base alias Gringotts.{ @@ -6,36 +8,21 @@ defmodule Gringotts.Gateways.Bogus do Response } - def authorize(_amount, _card_or_id, _opts), - do: success() - - def purchase(_amount, _card_or_id, _opts), - do: success() - - def capture(id, amount, _opts), - do: success(id) + @some_authorization_id "14a62fff80f24a25f775eeb33624bbb3" - def void(id, _opts), - do: success(id) + def authorize(_amount, _card_or_id, _opts), do: success() - def refund(_amount, id, _opts), - do: success(id) + def purchase(_amount, _card_or_id, _opts), do: success() - def store(_card = %CreditCard{}, _opts), - do: success() + def capture(_id, _amount, _opts), do: success() - def unstore(customer_id, _opts), - do: success(customer_id) + def void(_id, _opts), do: success() - defp success, - do: {:ok, Response.success(authorization: random_string())} + def refund(_amount, _id, _opts), do: success() - defp success(id), - do: {:ok, Response.success(authorization: id)} + def store(%CreditCard{} = _card, _opts), do: success() - defp random_string(length \\ 10), - do: 1..length |> Enum.map(&random_char/1) |> Enum.join + def unstore(_customer_id, _opts), do: success() - defp random_char(_), - do: to_string(:rand.uniform(9)) + defp success, do: {:ok, Response.success(id: @some_authorization_id)} end diff --git a/lib/gringotts/gateways/cams.ex b/lib/gringotts/gateways/cams.ex index 7e37dd05..2f1d5805 100644 --- a/lib/gringotts/gateways/cams.ex +++ b/lib/gringotts/gateways/cams.ex @@ -38,13 +38,13 @@ defmodule Gringotts.Gateways.Cams do this is important to you. [issues]: https://github.com/aviabird/gringotts/issues/new - + ### Schema * `billing_address` is a `map` from `atoms` to `String.t`, and can include any of the keys from: `:name, :address1, :address2, :company, :city, :state, :zip, :country, :phone, :fax]` - + ## Registering your CAMS account at `Gringotts` | Config parameter | CAMS secret | @@ -56,7 +56,6 @@ defmodule Gringotts.Gateways.Cams do > fields** and would look something like this: config :gringotts, Gringotts.Gateways.Cams, - adapter: Gringotts.Gateways.Cams, username: "your_secret_user_name", password: "your_secret_password", @@ -82,26 +81,21 @@ defmodule Gringotts.Gateways.Cams do you get after [registering with CAMS](#module-registering-your-cams-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.Cams} - iex> card = %CreditCard{first_name: "Harry", - last_name: "Potter", - number: "4111111111111111", - year: 2099, - month: 12, - verification_code: "999", - brand: "VISA"} - iex> money = %{value: Decimal.new(20), currency: "USD"} - ``` - We'll be using these in the examples below. + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in + [this gist][cams.iex.exs] to introduce a set of handy bindings and + aliases. + + We'll be using these bindings in the examples below. + + [example-repo]: https://github.com/aviabird/gringotts_example + [iex-docs]: https://hexdocs.pm/iex/IEx.html#module-the-iex-exs-file + [cams.iex.exs]: https://gist.github.com/oyeb/9a299df95cc13a87324e321faca5c9b8 ## Integrating with phoenix Refer the [GringottsPay][gpay-heroku-cams] website for an example of how to integrate CAMS with phoenix. The source is available [here][gpay-repo]. - + [gpay-repo]: https://github.com/aviabird/gringotts_payment [gpay-heroku-cams]: http://gringottspay.herokuapp.com/cams @@ -150,7 +144,7 @@ defmodule Gringotts.Gateways.Cams do ## Optional Fields options[ order_id: String, - description: String + description: String ] ## Examples @@ -165,11 +159,11 @@ defmodule Gringotts.Gateways.Cams do month: 12, verification_code: "999", brand: "VISA"} - iex> money = %{value: Decimal.new(20), currency: "USD"} + iex> money = Money.new(20, :USD) iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Cams, money, card) ``` """ - @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def authorize(money, %CreditCard{} = card, options) do params = [] @@ -210,13 +204,13 @@ defmodule Gringotts.Gateways.Cams do month: 12, verification_code: "999", brand: "VISA"} - iex> money = %{value: Decimal.new(10), currency: "USD"} + iex> money = Money.new(10, :USD) iex> authorization = auth_result.authorization # authorization = "some_authorization_transaction_id" iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Cams, money, authorization) ``` """ - @spec capture(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + @spec capture(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} def capture(money, transaction_id, options) do params = [transactionid: transaction_id] @@ -248,11 +242,11 @@ defmodule Gringotts.Gateways.Cams do month: 12, verification_code: "999", brand: "VISA"} - iex> money = %{value: Decimal.new(20), currency: "USD"} + iex> money = Money.new(20, :USD) iex> Gringotts.purchase(Gringotts.Gateways.Cams, money, card) ``` """ - @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def purchase(money, %CreditCard{} = card, options) do params = [] @@ -280,11 +274,11 @@ defmodule Gringotts.Gateways.Cams do ``` iex> capture_id = capture_result.authorization # capture_id = "some_capture_transaction_id" - iex> money = %{value: Decimal.new(20), currency: "USD"} + iex> money = Money.new(20, :USD) iex> Gringotts.refund(Gringotts.Gateways.Cams, money, capture_id) ``` """ - @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} def refund(money, transaction_id, options) do params = [transactionid: transaction_id] @@ -311,7 +305,7 @@ defmodule Gringotts.Gateways.Cams do iex> Gringotts.void(Gringotts.Gateways.Cams, auth_id) ``` """ - @spec void(String.t(), keyword) :: {:ok | :error, Response} + @spec void(String.t(), keyword) :: {:ok | :error, Response.t()} def void(transaction_id, options) do params = [transactionid: transaction_id] commit("void", params, options) @@ -334,7 +328,7 @@ defmodule Gringotts.Gateways.Cams do iex> Gringotts.validate(Gringotts.Gateways.Cams, card) ``` """ - @spec validate(CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec validate(CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def validate(card, options) do params = [] @@ -384,76 +378,94 @@ defmodule Gringotts.Gateways.Cams do @moduledoc false alias Gringotts.Response + # Fetched from CAMS POST API docs. + @avs_code_translator %{ + "X" => {nil, "pass: 9-character numeric ZIP"}, + "Y" => {nil, "pass: 5-character numeric ZIP"}, + "D" => {nil, "pass: 5-character numeric ZIP"}, + "M" => {nil, "pass: 5-character numeric ZIP"}, + "2" => {"pass: customer name", "pass: 5-character numeric ZIP"}, + "6" => {"pass: customer name", "pass: 5-character numeric ZIP"}, + "A" => {"pass: only address", "fail"}, + "B" => {"pass: only address", "fail"}, + "3" => {"pass: address, customer name", "fail"}, + "7" => {"pass: address, customer name", "fail"}, + "W" => {"fail", "pass: 9-character numeric ZIP match"}, + "Z" => {"fail", "pass: 5-character ZIP match"}, + "P" => {"fail", "pass: 5-character ZIP match"}, + "L" => {"fail", "pass: 5-character ZIP match"}, + "1" => {"pass: only customer name", "pass: 5-character ZIP"}, + "5" => {"pass: only customer name", "pass: 5-character ZIP"}, + "N" => {"fail", "fail"}, + "C" => {"fail", "fail"}, + "4" => {"fail", "fail"}, + "8" => {"fail", "fail"}, + "U" => {nil, nil}, + "G" => {nil, nil}, + "I" => {nil, nil}, + "R" => {nil, nil}, + "E" => {nil, nil}, + "S" => {nil, nil}, + "0" => {nil, nil}, + "O" => {nil, nil}, + "" => {nil, nil} + } + + # Fetched from CAMS POST API docs. + @cvc_code_translator %{ + "M" => "pass", + "N" => "fail", + "P" => "not_processed", + "S" => "Merchant indicated that CVV2/CVC2 is not present on card", + "U" => "Issuer is not certified and/or has not provided Visa encryption key" + } + @doc false def parse({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do - body = URI.decode_query(body) - - [status_code: 200] - |> set_authorization(body) - |> set_success(body) - |> set_message(body) - |> set_params(body) - |> set_error_code(body) - |> handle_opts() - end - - def parse({:ok, %HTTPoison.Response{body: body, status_code: 400}}) do - body = URI.decode_query(body) - set_params([status_code: 400], body) + decoded_body = URI.decode_query(body) + {street, zip_code} = @avs_code_translator[decoded_body["avsresponse"]] + gateway_code = decoded_body["response_code"] + message = decoded_body["responsetext"] + + response = %Response{ + status_code: 200, + id: decoded_body["transactionid"], + gateway_code: gateway_code, + avs_result: %{street: street, zip_code: zip_code}, + cvc_result: @cvc_code_translator[decoded_body["cvvresponse"]], + message: decoded_body["responsetext"], + raw: body + } + + if successful?(gateway_code) do + {:ok, response} + else + {:error, %{response | reason: message}} + end end - def parse({:ok, %HTTPoison.Response{body: body, status_code: 404}}) do - body = URI.decode_query(body) + def parse({:ok, %HTTPoison.Response{body: body, status_code: code}}) do + response = %Response{ + status_code: code, + raw: body + } - [status_code: 404] - |> handle_not_found(body) - |> handle_opts() + {:error, response} end def parse({:error, %HTTPoison.Error{} = error}) do - [ - message: "HTTPoison says #{error.reason}", - error_code: error.id, - success: false - ] + { + :error, + %Response{ + reason: "network related failure", + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]", + success: false + } + } end - defp set_authorization(opts, %{"transactionid" => id}) do - opts ++ [authorization: id] - end - - defp set_message(opts, %{"responsetext" => message}) do - opts ++ [message: message] - end - - defp set_params(opts, body) do - opts ++ [params: body] - end - - defp set_error_code(opts, %{"response_code" => response_code}) do - opts ++ [error_code: response_code] - end - - defp set_success(opts, %{"response_code" => response_code}) do - opts ++ [success: response_code == "100"] - end - - defp handle_not_found(opts, body) do - error = parse_html(body) - opts ++ [success: false, message: error] - end - - defp parse_html(body) do - error_message = List.to_string(Map.keys(body)) - [_ | parse_message] = Regex.run(~r|(.*)|, error_message) - List.to_string(parse_message) - end - - defp handle_opts(opts) do - case Keyword.fetch(opts, :success) do - {:ok, true} -> {:ok, Response.success(opts)} - {:ok, false} -> {:ok, Response.error(opts)} - end + defp successful?(gateway_code) do + gateway_code == "100" end end end diff --git a/lib/gringotts/gateways/checkout.ex b/lib/gringotts/gateways/checkout.ex new file mode 100644 index 00000000..facb2876 --- /dev/null +++ b/lib/gringotts/gateways/checkout.ex @@ -0,0 +1,342 @@ +defmodule Gringotts.Gateways.Checkout do + @moduledoc """ + [checkout][home] gateway implementation. + + A module for working with the checkout payment gateway. + + Refer the official Checkout [API docs][docs]. + + The following set of functions for Checkout have been implemented: + + | Action | Method | + | ------ | ------ | + | Authorize a Credit Card | `authorize/3` | + | Capture a previously authorized amount | `capture/3` | + | Charge a Credit Card | `purchase/3` | + | Refund a transaction | `refund/3` | + | Void a transaction | `void/2` | + + ## Optional or extra parameters + + Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply + optional arguments for transactions with the gateway. + To know more about these keywords visit [Request and Response][req-resp] tabs for each + API method. + + [req-resp]: https://beta.docs.checkout.com/docs/payments-quickstart + + ## Registering your checkout account at `Gringotts` + + > Here's how the secrets map to the required configuration parameters for checkout: + > + > | Config parameter | checkout secret | + > | -------------- | ----------- | + > | `:secret_key` | **SecretKey** | + + > Your Application config **must include the `[:secret_key]` field(s)** and would look + > something like this: + > + > config :gringotts, Gringotts.Gateways.Checkout, + > secret_key: "your_secret_secret_key" + + ## Supported currencies and countries + + > * Europe + > * North America + + ## Following the examples + + 1. First, set up a sample application and configure it to work with checkout. + - 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-monei-account-at-checkout). + + 2. Run an `iex` session with `iex -S mix` and add some variable bindings and + aliases to it (to save some time): + We'll be using these in the examples below. + ``` + iex> alias Gringotts.{Response, CreditCard, Gateways.Checkout} + 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. + + [docs]: https://beta.docs.checkout.com/docs + [gs]: https://github.com/aviabird/gringotts/wiki/ + [home]: https://www.checkout.com + [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: [:secret_key] + + import Poison, only: [decode: 1] + + alias Gringotts.{Money, CreditCard, Response} + + @test_url "https://sandbox.checkout.com/api2/v2/" + @doc """ + Performs a (pre) Authorize operation. + The authorization validates the `card` details with the banking network, + places a hold on the transaction `amount` in the customer’s issuing bank. + Checkout returns an ID string which can be used to: + * `capture/3` _an_ amount. + * `void/2` an amount + ## Example + ``` + iex> amount = Money.new(42, :USD) + iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Checkout, amount, card, opts) + iex> auth_result.id # This is the charge ID + ``` + """ + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def authorize(amount, card = %CreditCard{}, opts) do + {currency, value, _} = Money.to_integer(amount) + + body = + Poison.encode!(%{ + email: opts[:email], + currency: currency, + value: value, + autoCapture: "n", + autoCapTime: opts[:autoCapTime], + shippingDetails: %{ + addressLine1: opts[:address].street1, + addressLine2: opts[:address].street2, + city: opts[:address].city, + state: opts[:address].region, + country: opts[:address].country, + postcode: opts[:address].postal_code, + phone: %{ + countryCode: opts[:countryCode], + number: opts[:number] + } + }, + chargeMode: opts[:chargeMode], + customerIp: opts[:customerIp], + customerName: opts[:customerName], + description: opts[:description], + descriptor: opts[:descriptor], + trackId: opts[:trackId], + card: %{ + number: card.number, + name: CreditCard.full_name(card), + cvv: card.verification_code, + expiryMonth: card.month, + expiryYear: card.year, + billingDetails: %{ + addressLine1: opts[:address].street1, + addressLine2: opts[:address].street2, + city: opts[:address].city, + state: opts[:address].region, + country: opts[:address].country, + postcode: opts[:address].postal_code, + phone: %{ + countryCode: opts[:countryCode], + number: opts[:number] + } + } + } + }) + + commit(:post, "charges/card", body, opts) + end + + @doc """ + Captures a pre-authorized `amount`. + `amount` is transferred to the merchant account by Checkout used in the + pre-authorization referenced by `charge_id`. + ## Note + > Checkout **do** support partial captures, but only once per authorized payment. + ## Example + ``` + iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Checkout, amount, auth_result.id, opts) + ``` + """ + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} + def capture(payment_id, amount, opts) do + {currency, value, _} = Money.to_integer(amount) + + body = + Poison.encode!(%{ + description: opts[:description], + trackId: opts[:trackId], + value: value + }) + + commit(:post, "charges/#{payment_id}/capture", body, opts) + end + + @doc """ + Transfers `amount` from the customer to the merchant. + Checkout attempts to process a purchase on behalf of the customer, by + debiting `amount` from the customer's account by charging the customer's + `card`. + ## Example + ``` + iex> amount = Money.new(42, :USD) + iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Checkout, amount, card, opts) + iex> purchase_result.id # This is the charge ID + """ + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def purchase(amount, card = %CreditCard{}, opts) do + {currency, value, _} = Money.to_integer(amount) + + body = + Poison.encode!(%{ + email: opts[:email], + currency: currency, + value: value, + autoCapTime: opts[:autoCapTime], + shippingDetails: %{ + addressLine1: opts[:address].street1, + addressLine2: opts[:address].street2, + city: opts[:address].city, + state: opts[:address].region, + country: opts[:address].country, + postcode: opts[:address].postal_code, + phone: %{ + countryCode: opts[:countryCode], + number: opts[:number] + } + }, + chargeMode: opts[:chargeMode], + customerIp: opts[:customerIp], + customerName: opts[:customerName], + description: opts[:description], + descriptor: opts[:descriptor], + trackId: opts[:trackId], + card: %{ + number: card.number, + name: CreditCard.full_name(card), + cvv: card.verification_code, + expiryMonth: card.month, + expiryYear: card.year, + billingDetails: %{ + addressLine1: opts[:address].street1, + addressLine2: opts[:address].street2, + city: opts[:address].city, + state: opts[:address].region, + country: opts[:address].country, + postcode: opts[:address].postal_code, + phone: %{ + countryCode: opts[:countryCode], + number: opts[:number] + } + } + } + }) + + commit(:post, "charges/card", body, opts) + end + + @doc """ + Voids the referenced payment. + This method attempts a reversal of a previous transaction referenced by + `charge_id`. + + ## Note + > As a consequence, the customer will never see any booking on his statement. + > Checkout must be in authorized state and **not** in captured state. + ## Example + ``` + iex> {:ok, void_result} = Gringotts.capture(Gringotts.Gateways.Checkout, purchase_result.id, opts) + ``` + """ + @spec void(String.t(), keyword) :: {:ok | :error, Response} + def void(payment_id, opts) do + body = + Poison.encode!(%{ + description: opts[:description], + trackId: opts[:trackId] + }) + + commit(:post, "charges/#{payment_id}/void", body, opts) + end + + @doc """ + Refunds the `amount` to the customer's account with reference to a prior transfer. + > Refunds are allowed on Captured / purchased transraction. + ## Note + * Checkout does support partial refunds, but only once per captured payment. + ## Example + ``` + iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Checkout, purchase_result.id, amount) + ``` + """ + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + def refund(amount, payment_id, opts) do + {currency, value, _} = Money.to_integer(amount) + + body = + Poison.encode!(%{ + description: opts[:description], + trackId: opts[:trackId], + value: value + }) + + commit(:post, "charges/#{payment_id}/refund", body, opts) + end + + ############################################################################### + # PRIVATE METHODS # + ############################################################################### + + # Makes the request to checkout's network. + # For consistency with other gateway implementations, make your (final) + # network request in here, and parse it using another private method called + # `respond`. + @spec commit(atom, String.t(), String.t(), keyword) :: {:ok | :error, Response} + defp commit(:post, endpoint, body, opts) do + url = @test_url <> "#{endpoint}" + + headers = [ + {"Content-Type", "application/json;charset=UTF-8"}, + {"Authorization", opts[:config][:secret_key]} + ] + + HTTPoison.request(:post, url, body, headers) + |> respond + end + + # Parses checkout'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) + + id = parsed["id"] + message = parsed["status"] + + { + :ok, + Response.success(id: id, message: message, raw: parsed, status_code: code) + } + end + + defp respond({:ok, %{status_code: status_code, body: body}}) do + {:ok, parsed} = decode(body) + detail = parsed["error_description"] + + { + :error, + Response.error(status_code: status_code, message: detail, raw: body) + } + end + + defp respond({:error, %HTTPoison.Error{} = error}) do + {:error, Response.error(code: error.id, message: "HTTPoison says '#{error.reason}")} + end +end diff --git a/lib/gringotts/gateways/global_collect.ex b/lib/gringotts/gateways/global_collect.ex index 0a2010e6..6075451d 100644 --- a/lib/gringotts/gateways/global_collect.ex +++ b/lib/gringotts/gateways/global_collect.ex @@ -2,9 +2,9 @@ defmodule Gringotts.Gateways.GlobalCollect do @moduledoc """ [GlobalCollect][home] gateway implementation. - For further details, please refer [GlobalCollect API documentation](https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/index.html). + For further details, please refer [GlobalCollect API documentation][docs]. - Following are the features that have been implemented for the GlobalCollect Gateway: + Following are the features that have been implemented for GlobalCollect: | Action | Method | | ------ | ------ | @@ -14,32 +14,34 @@ defmodule Gringotts.Gateways.GlobalCollect do | Refund | `refund/3` | | Void | `void/2` | - ## Optional or extra parameters + ## Optional parameters Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply optional arguments for transactions with the gateway. - | Key | Status | + | Key | Remark | | ---- | --- | - | `merchantCustomerId` | implemented | - | `description` | implemented | - | `customer_name` | implemented | - | `dob` | implemented | - | `company` | implemented | - | `email` | implemented | - | `phone` | implemented | - | `order_id` | implemented | - | `invoice` | implemented | - | `billingAddress` | implemented | - | `shippingAddress` | implemented | - | `name` | implemented | - | `skipAuthentication` | implemented | - + | `merchantCustomerId` | Identifier for the consumer that can be used as a search criteria in the Global Collect Payment Console | + | `description` | Descriptive text that is used towards to consumer, either during an online checkout at a third party and/or on the statement of the consumer | + | `dob` | The date of birth of the consumer Format: YYYYMMDD | + | `company` | Name of company, as a consumer | + | `email` | Email address of the consumer | + | `phone` | Phone number of the consumer | + | `invoice` | Object containing additional invoice data | + | `billingAddress` | Object containing billing address details | + | `shippingAddress` | Object containing shipping address details | + | `name` | Object containing the name details of the consumer | + | `skipAuthentication` | 3D Secure Authentication will be skipped for this transaction if set to true | + + For more details of the required keys refer [this.][options] ## Registering your GlobalCollect account at `Gringotts` - After creating your account successfully on [GlobalCollect](http://www.globalcollect.com/) follow the [dashboard link](https://sandbox.account.ingenico.com/#/account/apikey) to fetch the secret_api_key, api_key_id and [here](https://sandbox.account.ingenico.com/#/account/merchantid) for merchant_id. + After creating your account successfully on [GlobalCollect][home] open the + [dashboard][dashboard] to fetch the `secret_api_key`, `api_key_id` and + `merchant_id` from the menu. - Here's how the secrets map to the required configuration parameters for GlobalCollect: + Here's how the secrets map to the required configuration parameters for + GlobalCollect: | Config parameter | GlobalCollect secret | | ------- | ---- | @@ -47,20 +49,22 @@ defmodule Gringotts.Gateways.GlobalCollect do | `:api_key_id` | **ApiKeyId** | | `:merchant_id` | **MerchantId** | - Your Application config **must include the `[:secret_api_key, :api_key_id, :merchant_id]` field(s)** and would look - something like this: + Your Application config **must include the `:secret_api_key`, `:api_key_id`, + `:merchant_id` field(s)** and would look something like this: config :gringotts, Gringotts.Gateways.GlobalCollect, - adapter: Gringotts.Gateways.GlobalCollect, secret_api_key: "your_secret_secret_api_key" api_key_id: "your_secret_api_key_id" merchant_id: "your_secret_merchant_id" + ## Scope of this module + + * [All amount fields in globalCollect are in cents with each amount having 2 decimals.][amountReference] + ## Supported currencies and countries - The GlobalCollect platform is able to support payments in [over 150 currencies][currencies] + The GlobalCollect platform supports payments in [over 150 currencies][currencies]. - [currencies]: https://epayments.developer-ingenico.com/best-practices/services/currency-conversion ## Following the examples 1. First, set up a sample application and configure it to work with GlobalCollect. @@ -68,51 +72,74 @@ defmodule Gringotts.Gateways.GlobalCollect do - 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-globalcollect-account-at-GlobalCollect). + as described [above](#module-registering-your-globalcollect-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): + aliases to it (to save some time): ``` iex> alias Gringotts.{Response, CreditCard, Gateways.GlobalCollect} - iex> shippingAddress = %{ - street: "Desertroad", - houseNumber: "1", - additionalInfo: "Suite II", - zip: "84536", - city: "Monument Valley", - state: "Utah", - countryCode: "US" - } + street: "Desertroad", + houseNumber: "1", + additionalInfo: "Suite II", + zip: "84536", + city: "Monument Valley", + state: "Utah", + countryCode: "US" + } iex> billingAddress = %{ - street: "Desertroad", - houseNumber: "13", - additionalInfo: "b", - zip: "84536", - city: "Monument Valley", - state: "Utah", - countryCode: "US" - } + street: "Desertroad", + houseNumber: "13", + additionalInfo: "b", + zip: "84536", + city: "Monument Valley", + state: "Utah", + countryCode: "US" + } iex> invoice = %{ - invoiceNumber: "000000123", - invoiceDate: "20140306191500" - } + invoiceNumber: "000000123", + invoiceDate: "20140306191500" + } iex> name = %{ - title: "Miss", - firstName: "Road", - surname: "Runner" - } + title: "Miss", + firstName: "Road", + surname: "Runner" + } - iex> opts = [ description: "Store Purchase 1437598192", merchantCustomerId: "234", customer_name: "John Doe", dob: "19490917", company: "asma", email: "johndoe@gmail.com", phone: "7765746563", order_id: "2323", invoice: invoice, billingAddress: billingAddress, shippingAddress: shippingAddress, name: name, skipAuthentication: "true" ] + iex> card = %CreditCard{ + number: "4567350000427977", + month: 12, + year: 43, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "VISA" + } + iex> opts = [ + description: "Store Purchase 1437598192", + merchantCustomerId: "234", dob: "19490917", + company: "asma", email: "johndoe@gmail.com", + phone: "7765746563", invoice: invoice, + billingAddress: billingAddress, + shippingAddress: shippingAddress, + name: name, skipAuthentication: "true" + ] ``` We'll be using these in the examples below. + [home]: http://www.globalcollect.com/ + [docs]: https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/index.html + [dashboard]: https://sandbox.account.ingenico.com/#/dashboard + [gs]: # + [options]: https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/java/payments/create.html#payments-create-payload + [currencies]: https://epayments.developer-ingenico.com/best-practices/services/currency-conversion [example]: https://github.com/aviabird/gringotts_example + [amountReference]: https://epayments-api.developer-ingenico.com/c2sapi/v1/en_US/swift/services/convertAmount.html """ @base_url "https://api-sandbox.globalcollect.com/v1/" @@ -125,17 +152,15 @@ defmodule Gringotts.Gateways.GlobalCollect do import Poison, only: [decode: 1] - alias Gringotts.{Money, - CreditCard, - Response} - - @brand_map %{ - "visa": "1", - "american_express": "2", - "master": "3", - "discover": "128", - "jcb": "125", - "diners_club": "132" + alias Gringotts.{Money, CreditCard, Response} + + @brand_map %{ + VISA: "1", + AMERICAN_EXPRESS: "2", + MASTER: "3", + DISCOVER: "128", + JCB: "125", + DINERS_CLUB: "132" } @doc """ @@ -146,73 +171,70 @@ defmodule Gringotts.Gateways.GlobalCollect do also triggers risk management. Funds are not transferred. GlobalCollect returns a payment id which can be further used to: - * `capture/3` _an_ amount. - * `refund/3` _an_amount + * `capture/3` an amount. + * `refund/3` an amount * `void/2` a pre_authorization ## Example - > The following session shows how one would (pre) authorize a payment of $100 on + The following example shows how one would (pre) authorize a payment of $100 on a sample `card`. ``` iex> card = %CreditCard{ number: "4567350000427977", month: 12, - year: 18, + year: 43, first_name: "John", last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } - iex> amount = %{value: Decimal.new(100), currency: "USD"} + iex> amount = Money.new(100, :USD) iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.GlobalCollect, amount, card, opts) ``` """ @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} - def authorize(amount, card = %CreditCard{}, opts) do - params = create_params_for_auth_or_purchase(amount, card, opts) + def authorize(amount, %CreditCard{} = card, opts) do + params = %{ + order: add_order(amount, opts), + cardPaymentMethodSpecificInput: add_card(card, opts) + } + commit(:post, "payments", params, opts) end @doc """ Captures a pre-authorized `amount`. - `amount` is transferred to the merchant account by GlobalCollect used in the - pre-authorization referenced by `payment_id`. + `amount` used in the pre-authorization referenced by `payment_id` is + transferred to the merchant account by GlobalCollect. ## Note - > Authorized payment with PENDING_APPROVAL status only allow a single capture whereas the one with PENDING_CAPTURE status is used for payments that allow multiple captures. - > PENDING_APPROVAL is a common status only with card and direct debit transactions. + Authorized payment with PENDING_APPROVAL status only allow a single capture whereas + the one with PENDING_CAPTURE status is used for payments that allow multiple captures. ## Example - The following session shows how one would (partially) capture a previously + The following example shows how one would (partially) capture a previously authorized a payment worth $100 by referencing the obtained authorization `id`. ``` - iex> card = %CreditCard{ - number: "4567350000427977", - month: 12, - year: 18, - first_name: "John", - last_name: "Doe", - verification_code: "123", - brand: "visa" - } - - iex> amount = %{value: Decimal.new(100), currency: "USD"} + iex> amount = Money.new(100, :USD) - iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.GlobalCollect, amount, card, opts) + iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.GlobalCollect, auth_result.authorization, amount, opts) ``` """ - @spec capture(String.t(), Money.t, keyword) :: {:ok | :error, Response} + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} def capture(payment_id, amount, opts) do - params = create_params_for_capture(amount, opts) + params = %{ + order: add_order(amount, opts) + } + commit(:post, "payments/#{payment_id}/approve", params, opts) end @@ -225,28 +247,28 @@ defmodule Gringotts.Gateways.GlobalCollect do ## Example - > The following session shows how one would process a payment in one-shot, + The following example shows how one would process a payment in one-shot, without (pre) authorization. ``` iex> card = %CreditCard{ number: "4567350000427977", month: 12, - year: 18, + year: 43, first_name: "John", last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } - iex> amount = %{value: Decimal.new(100), currency: "USD"} + iex> amount = Money.new(100, :USD) iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.GlobalCollect, amount, card, opts) ``` """ - @spec purchase(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} - def purchase(amount, card = %CreditCard{}, opts) do + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def purchase(amount, %CreditCard{} = card, opts) do case authorize(amount, card, opts) do {:ok, results} -> payment_id = results.raw["payment"]["id"] @@ -260,50 +282,53 @@ defmodule Gringotts.Gateways.GlobalCollect do @doc """ Voids the referenced payment. - This makes it impossible to process the payment any further and will also try to reverse an authorization on a card. - Reversing an authorization that you will not be utilizing will prevent you from having to pay a fee/penalty for unused authorization requests. + This makes it impossible to process the payment any further and will also try + to reverse an authorization on a card. + Reversing an authorization that you will not be utilizing will prevent you + from having to [pay a fee/penalty][void] for unused authorization requests. + [void]: https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/java/payments/cancel.html#payments-cancel-request ## Example - > The following session shows how one would void a previous (pre) + The following example shows how one would void a previous (pre) authorization. Remember that our `capture/3` example only did a complete capture. ``` - iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.GlobalCollect, auth_result.payment.id, opts) + iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.GlobalCollect, auth_result.authorization, opts) ``` """ @spec void(String.t(), keyword) :: {:ok | :error, Response} def void(payment_id, opts) do - params = nil - commit(:post, "payments/#{payment_id}/cancel", params, opts) + commit(:post, "payments/#{payment_id}/cancel", [], opts) end @doc """ Refunds the `amount` to the customer's account with reference to a prior transfer. - > You can refund any transaction by just calling this API - - ## Note - You always have the option to refund just a portion of the payment amount. - It is also possible to submit multiple refund requests on one payment as long as the total amount to be refunded does not exceed the total amount that was paid. + It is also possible to submit multiple refund requests on one payment as long + as the total amount to be refunded does not exceed the total amount that was paid. ## Example - > The following session shows how one would refund a previous purchase (and + The following example shows how one would refund a previous purchase (and similarily for captures). ``` - iex> amount = %{value: Decimal.new(100), currency: "USD"} + iex> amount = Money.new(100, :USD) - iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.GlobalCollect, auth_result.payment.id, amount) + iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.GlobalCollect, auth_result.authorization, amount) ``` """ - @spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} def refund(amount, payment_id, opts) do - params = create_params_for_refund(amount, opts) + params = %{ + amountOfMoney: add_money(amount), + customer: add_customer(opts) + } + commit(:post, "payments/#{payment_id}/refund", params, opts) end @@ -311,41 +336,20 @@ defmodule Gringotts.Gateways.GlobalCollect do # PRIVATE METHODS # ############################################################################### - # Makes the request to GlobalCollect'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 create_params_for_refund(amount, opts) do - %{ - amountOfMoney: add_money(amount, opts), - customer: add_customer(opts) - } - end - - defp create_params_for_auth_or_purchase(amount, payment, opts) do - %{ - order: add_order(amount, opts), - cardPaymentMethodSpecificInput: add_payment(payment, @brand_map, opts) - } - end - - defp create_params_for_capture(amount, opts) do - %{ - order: add_order(amount, opts) - } - end - defp add_order(money, options) do %{ - amountOfMoney: add_money(money, options), + amountOfMoney: add_money(money), customer: add_customer(options), - references: add_references(options) + references: %{ + descriptor: options[:description], + invoiceData: options[:invoice] + } } end - defp add_money(amount, options) do + defp add_money(amount) do {currency, amount, _} = Money.to_integer(amount) + %{ amount: amount, currencyCode: currency @@ -355,85 +359,65 @@ defmodule Gringotts.Gateways.GlobalCollect do defp add_customer(options) do %{ merchantCustomerId: options[:merchantCustomerId], - personalInformation: personal_info(options), + personalInformation: %{ + name: options[:name] + }, dateOfBirth: options[:dob], - companyInformation: company_info(options), + companyInformation: %{ + name: options[:company] + }, billingAddress: options[:billingAddress], shippingAddress: options[:shippingAddress], - contactDetails: contact(options) + contactDetails: %{ + emailAddress: options[:email], + phoneNumber: options[:phone] + } } end - defp add_references(options) do + defp add_card(card, opts) do %{ - descriptor: options[:description], - invoiceData: options[:invoice] + paymentProductId: Map.fetch!(@brand_map, String.to_atom(card.brand)), + skipAuthentication: opts[:skipAuthentication], + card: %{ + cvv: card.verification_code, + cardNumber: card.number, + expiryDate: "#{card.month}#{card.year}", + cardholderName: CreditCard.full_name(card) + } } end - defp personal_info(options) do - %{ - name: options[:name] - } - end + defp commit(method, path, params, opts) do + headers = create_headers(path, opts) + data = Poison.encode!(params) + merchant_id = opts[:config][:merchant_id] + url = "#{@base_url}#{merchant_id}/#{path}" - defp company_info(options) do - %{ - name: options[:company] - } + gateway_response = HTTPoison.request(method, url, data, headers) + gateway_response |> respond end - defp contact(options) do - %{ - emailAddress: options[:email], - phoneNumber: options[:phone] - } - end + defp create_headers(path, opts) do + datetime = Timex.now() |> Timex.local() - def add_card(%CreditCard{} = payment) do - %{ - cvv: payment.verification_code, - cardNumber: payment.number, - expiryDate: "#{payment.month}"<>"#{payment.year}", - cardholderName: CreditCard.full_name(payment) - } - end + date_string = + "#{Timex.format!(datetime, "%a, %d %b %Y %H:%M:%S", :strftime)} #{datetime.zone_abbr}" - defp add_payment(payment, brand_map, opts) do - brand = payment.brand - %{ - paymentProductId: Map.fetch!(brand_map, String.to_atom(brand)), - skipAuthentication: opts[:skipAuthentication], - card: add_card(payment) - } - end + api_key_id = opts[:config][:api_key_id] - defp auth_digest(path, secret_api_key, time, opts) do - data = "POST\napplication/json\n#{time}\n/v1/#{opts[:config][:merchant_id]}/#{path}\n" - :crypto.hmac(:sha256, secret_api_key, data) - end + sha_signature = auth_digest(path, date_string, opts) - defp commit(method, path, params, opts) do - headers = create_headers(path, opts) - data = Poison.encode!(params) - url = "#{@base_url}#{opts[:config][:merchant_id]}/#{path}" - response = HTTPoison.request(method, url, data, headers) - response |> respond + auth_token = "GCS v1HMAC:#{api_key_id}:#{Base.encode64(sha_signature)}" + [{"Content-Type", "application/json"}, {"Authorization", auth_token}, {"Date", date_string}] end - defp create_headers(path, opts) do - time = date - sha_signature = auth_digest(path, opts[:config][:secret_api_key], time, opts) |> Base.encode64 - auth_token = "GCS v1HMAC:#{opts[:config][:api_key_id]}:#{sha_signature}" - headers = [{"Content-Type", "application/json"}, {"Authorization", auth_token}, {"Date", time}] - end + defp auth_digest(path, date_string, opts) do + secret_api_key = opts[:config][:secret_api_key] + merchant_id = opts[:config][:merchant_id] - defp date() do - use Timex - datetime = Timex.now |> Timex.local - strftime_str = Timex.format!(datetime, "%a, %d %b %Y %H:%M:%S ", :strftime) - time_zone = Timex.timezone(:local, datetime) - time = strftime_str <>"#{time_zone.abbreviation}" + data = "POST\napplication/json\n#{date_string}\n/v1/#{merchant_id}/#{path}\n" + :crypto.hmac(:sha256, secret_api_key, data) end # Parses GlobalCollect's response and returns a `Gringotts.Response` struct @@ -443,19 +427,49 @@ defmodule Gringotts.Gateways.GlobalCollect do defp respond({:ok, %{status_code: code, body: body}}) when code in [200, 201] do case decode(body) do - {:ok, results} -> {:ok, Response.success(raw: results, status_code: code)} + {:ok, results} -> + { + :ok, + Response.success( + authorization: results["payment"]["id"], + raw: results, + status_code: code, + avs_result: + results["payment"]["paymentOutput"]["cardPaymentMethodSpecificOutput"][ + "fraudResults" + ]["avsResult"], + cvc_result: + results["payment"]["paymentOutput"]["cardPaymentMethodSpecificOutput"][ + "fraudResults" + ]["cvcResult"], + message: results["payment"]["status"], + fraud_review: + results["payment"]["paymentOutput"]["cardPaymentMethodSpecificOutput"][ + "fraudResults" + ]["fraudServiceResult"] + ) + } + + {:error, _} -> + {:error, Response.error(raw: body, message: "undefined response from GlobalCollect")} end end defp respond({:ok, %{status_code: status_code, body: body}}) do {:ok, results} = decode(body) - message = Enum.map(results["errors"],fn (x) -> x["message"] end) + message = Enum.map(results["errors"], fn x -> x["message"] end) detail = List.to_string(message) {:error, Response.error(status_code: status_code, message: detail, raw: results)} end defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, Response.error(code: error.id, reason: :network_fail?, description: "HTTPoison says '#{error.reason}'")} + { + :error, + Response.error( + code: error.id, + reason: :network_fail?, + description: "HTTPoison says '#{error.reason}'" + ) + } end - end diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index 53ebb130..4d659656 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -73,7 +73,6 @@ defmodule Gringotts.Gateways.Monei do fields** and would look something like this: config :gringotts, Gringotts.Gateways.Monei, - adapter: Gringotts.Gateways.Monei, userId: "your_secret_user_id", password: "your_secret_password", entityId: "your_secret_channel_id" @@ -127,56 +126,15 @@ defmodule Gringotts.Gateways.Monei do that you see in `Dashboard > Sub-accounts` as described [above](#module-registering-your-monei-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.Monei} - iex> amount = %{value: Decimal.new(42), currency: "USD"} - iex> card = %CreditCard{first_name: "Harry", - last_name: "Potter", - number: "4200000000000000", - year: 2099, month: 12, - verification_code: "123", - brand: "VISA"} - iex> customer = %{"givenName": "Harry", - "surname": "Potter", - "merchantCustomerId": "the_boy_who_lived", - "sex": "M", - "birthDate": "1980-07-31", - "mobile": "+15252525252", - "email": "masterofdeath@ministryofmagic.gov", - "ip": "127.0.0.1", - "status": "NEW"} - iex> merchant = %{"name": "Ollivanders", - "city": "South Side", - "street": "Diagon Alley", - "state": "London", - "country": "GB", - "submerchantId": "Makers of Fine Wands since 382 B.C."} - iex> billing = %{"street1": "301, Gryffindor", - "street2": "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", - "city": "Highlands", - "state": "Scotland", - "country": "GB"} - iex> shipping = %{"street1": "301, Gryffindor", - "street2": "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", - "city": "Highlands", - "state": "Scotland", - "country": "GB", - "method": "SAME_DAY_SERVICE", - "comment": "For our valued customer, Mr. Potter"} - iex> opts = [customer: customer, - merchant: merchant, - billing: billing, - shipping: shipping, - category: "EC", - custom: %{"voldemort": "he who must not be named"}, - register: true] - ``` - - We'll be using these in the examples below. + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in + [this gist][monei.iex.exs] to introduce a set of handy bindings and + aliases. + + We'll be using these bindings in the examples below. [example-repo]: https://github.com/aviabird/gringotts_example + [iex-docs]: https://hexdocs.pm/iex/IEx.html#module-the-iex-exs-file + [monei.iex.exs]: https://gist.github.com/oyeb/a2e2ac5986cc90a12a6136f6bf1357e5 ## TODO @@ -195,15 +153,11 @@ defmodule Gringotts.Gateways.Monei do @base_url "https://test.monei-api.net" @default_headers ["Content-Type": "application/x-www-form-urlencoded", charset: "UTF-8"] - @supported_currencies [ - "AED", "AFN", "ANG", "AOA", "AWG", "AZN", "BAM", "BGN", "BRL", "BYN", "CDF", - "CHF", "CUC", "EGP", "EUR", "GBP", "GEL", "GHS", "MDL", "MGA", "MKD", "MWK", - "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "PAB", "PEN", "PGK", "PHP", - "PKR", "PLN", "PYG", "QAR", "RSD", "RUB", "RWF", "SAR", "SCR", "SDG", "SEK", - "SGD", "SHP", "SLL", "SOS", "SRD", "STD", "SYP", "SZL", "THB", "TJS", "TOP", - "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VND", "VUV", - "WST", "XAF", "XCD", "XOF", "XPF", "YER", "ZAR", "ZMW", "ZWL" - ] + @supported_currencies ~w(AED AFN ANG AOA AWG AZN BAM BGN BRL BYN CDF CHF CUC + EGP EUR GBP GEL GHS MDL MGA MKD MWK MZN NAD NGN NIO NOK NPR NZD PAB PEN PGK + PHP PKR PLN PYG QAR RSD RUB RWF SAR SCR SDG SEK SGD SHP SLL SOS SRD STD SYP + SZL THB TJS TOP TRY TTD TWD TZS UAH UGX USD UYU UZS VND VUV WST XAF XCD XOF + XPF YER ZAR ZMW ZWL) @version "v1" @@ -252,13 +206,13 @@ defmodule Gringotts.Gateways.Monei do The following example shows how one would (pre) authorize a payment of $42 on a sample `card`. - iex> amount = %{value: Decimal.new(42), currency: "USD"} + iex> amount = Money.new(42, :USD) iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Monei, amount, card, opts) iex> auth_result.id # This is the authorization ID iex> auth_result.token # This is the registration ID/token """ - @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def authorize(amount, %CreditCard{} = card, opts) do {currency, value} = Money.to_string(amount) @@ -290,10 +244,10 @@ defmodule Gringotts.Gateways.Monei do The following example shows how one would (partially) capture a previously authorized a payment worth $35 by referencing the obtained authorization `id`. - iex> amount = %{value: Decimal.new(35), currency: "USD"} + iex> amount = Money.new(35, :USD) iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Monei, amount, auth_result.id, opts) """ - @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response.t()} def capture(payment_id, amount, opts) def capture(<>, amount, opts) do @@ -324,12 +278,12 @@ defmodule Gringotts.Gateways.Monei do The following example shows how one would process a payment worth $42 in one-shot, without (pre) authorization. - iex> amount = %{value: Decimal.new(42), currency: "USD"} + iex> amount = Money.new(42, :USD) iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Monei, amount, card, opts) iex> purchase_result.token # This is the registration ID/token """ - @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def purchase(amount, %CreditCard{} = card, opts) do {currency, value} = Money.to_string(amount) @@ -358,10 +312,10 @@ defmodule Gringotts.Gateways.Monei do The following example shows how one would (completely) refund a previous purchase (and similarily for captures). - iex> amount = %{value: Decimal.new(42), currency: "USD"} + iex> amount = Money.new(42, :USD) iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Monei, purchase_result.id, amount) """ - @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} def refund(amount, <>, opts) do {currency, value} = Money.to_string(amount) @@ -380,6 +334,8 @@ defmodule Gringotts.Gateways.Monei do which can be used to effectively process _One-Click_ and _Recurring_ payments, and return a registration token for reference. + The registration token is available in the `Response.id` field. + It is recommended to associate these details with a "Customer" by passing customer details in the `opts`. @@ -394,9 +350,10 @@ defmodule Gringotts.Gateways.Monei do future use. iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> {:ok, store_result} = Gringotts.store(Gringotts.Gateways.Monei, card, []) + iex> {:ok, store_result} = Gringotts.store(Gringotts.Gateways.Monei, card) + iex> store_result.id # This is the registration token """ - @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def store(%CreditCard{} = card, opts) do params = card_params(card) commit(:post, "registrations", params, opts) @@ -409,7 +366,7 @@ defmodule Gringotts.Gateways.Monei do Deletes previously stored payment-source data. """ - @spec unstore(String.t(), keyword) :: {:ok | :error, Response} + @spec unstore(String.t(), keyword) :: {:ok | :error, Response.t()} def unstore(registration_id, opts) def unstore(<>, opts) do @@ -447,7 +404,7 @@ defmodule Gringotts.Gateways.Monei do iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.Monei, auth_result.id, opts) """ - @spec void(String.t(), keyword) :: {:ok | :error, Response} + @spec void(String.t(), keyword) :: {:ok | :error, Response.t()} def void(payment_id, opts) def void(<>, opts) do @@ -466,72 +423,103 @@ defmodule Gringotts.Gateways.Monei do ] end - # Makes the request to MONEI's network. - @spec commit(atom, String.t(), keyword, keyword) :: {:ok | :error, Response} - defp commit(method, endpoint, params, opts) do - auth_params = [ + defp auth_params(opts) do + [ "authentication.userId": opts[:config][:userId], "authentication.password": opts[:config][:password], "authentication.entityId": opts[:config][:entityId] ] + end + # Makes the request to MONEI's network. + @spec commit(atom, String.t(), keyword, keyword) :: {:ok | :error, Response.t()} + defp commit(:post, endpoint, params, opts) do url = "#{base_url(opts)}/#{version(opts)}/#{endpoint}" - case expand_params(opts, params[:paymentType]) do + case expand_params(Keyword.delete(opts, :config), params[:paymentType]) do {:error, reason} -> - {:error, Response.error(description: reason)} + {:error, Response.error(reason: reason)} validated_params -> - network_response = - case method do - :post -> - HTTPoison.post( - url, - {:form, params ++ validated_params ++ auth_params}, - @default_headers - ) - - :delete -> - HTTPoison.delete(url <> "?" <> URI.encode_query(auth_params)) - end - - respond(network_response) + url + |> HTTPoison.post( + {:form, params ++ validated_params ++ auth_params(opts)}, + @default_headers + ) + |> respond end end + # This clause is only used by `unstore/2` + defp commit(:delete, endpoint, _params, opts) do + base_url = "#{base_url(opts)}/#{version(opts)}/#{endpoint}" + auth_params = auth_params(opts) + query_string = auth_params |> URI.encode_query() + + (base_url <> "?" <> query_string) + |> HTTPoison.delete() + |> respond + end + # Parses MONEI's response and returns a `Gringotts.Response` struct in a # `:ok`, `:error` tuple. - @spec respond(term) :: {:ok | :error, Response} + @spec respond(term) :: {:ok | :error, Response.t()} defp respond(monei_response) defp respond({:ok, %{status_code: 200, body: body}}) do - case decode(body) do - {:ok, decoded_json} -> - case parse_response(decoded_json) do - {:ok, results} -> {:ok, Response.success([{:id, decoded_json["id"]} | results])} - {:error, errors} -> {:ok, Response.error([{:id, decoded_json["id"]} | errors])} - end + common = [raw: body, status_code: 200] + + with {:ok, decoded_json} <- decode(body), + {:ok, results} <- parse_response(decoded_json) do + {:ok, Response.success(common ++ results)} + else + {:not_ok, errors} -> + {:ok, Response.error(common ++ errors)} {:error, _} -> - {:error, Response.error(raw: body, code: :undefined_response_from_monei)} + {:error, Response.error([reason: "undefined response from monei"] ++ common)} end end defp respond({:ok, %{status_code: status_code, body: body}}) do - {:error, Response.error(code: status_code, raw: body)} + {:error, Response.error(status_code: status_code, raw: body)} end defp respond({:error, %HTTPoison.Error{} = error}) do { :error, Response.error( - code: error.id, reason: "network related failure", - description: "HTTPoison says '#{error.reason}'" + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]" ) } end + defp parse_response(%{"result" => result} = data) do + {address, zip_code} = @avs_code_translator[result["avsResponse"]] + + results = [ + id: data["id"], + token: data["registrationId"], + gateway_code: result["code"], + message: result["description"], + fraud_review: data["risk"], + cvc_result: @cvc_code_translator[result["cvvResponse"]], + avs_result: %{address: address, zip_code: zip_code} + ] + + non_nil_params = Enum.filter(results, fn {_, v} -> v != nil end) + verify(non_nil_params) + end + + defp verify(results) do + if String.match?(results[:gateway_code], ~r{^(000\.000\.|000\.100\.1|000\.[36])}) do + {:ok, results} + else + {:not_ok, [{:reason, results[:message]} | results]} + end + end + defp expand_params(params, action_type) do Enum.reduce_while(params, [], fn {k, v}, acc -> case k do @@ -541,16 +529,16 @@ defmodule Gringotts.Gateways.Monei do else: {:halt, {:error, "Invalid currency"}} :customer -> - {:cont, acc ++ make("customer", v)} + {:cont, acc ++ make(action_type, "customer", v)} :merchant -> - {:cont, acc ++ make("merchant", v)} + {:cont, acc ++ make(action_type, "merchant", v)} :billing -> - {:cont, acc ++ make("billing", v)} + {:cont, acc ++ make(action_type, "billing", v)} :shipping -> - {:cont, acc ++ make("shipping", v)} + {:cont, acc ++ make(action_type, "shipping", v)} :invoice_id -> {:cont, [{"merchantInvoiceId", v} | acc]} @@ -562,23 +550,16 @@ defmodule Gringotts.Gateways.Monei do {:cont, [{"transactionCategory", v} | acc]} :shipping_customer -> - {:cont, acc ++ make("shipping.customer", v)} + {:cont, acc ++ make(action_type, "shipping.customer", v)} :custom -> {:cont, acc ++ make_custom(v)} :register -> - { - :cont, - if action_type in ["PA", "DB"] do - [{"createRegistration", true} | acc] - else - acc - end - } - - _ -> - {:cont, acc} + {:cont, acc ++ make(action_type, :register, v)} + + unsupported -> + {:halt, {:error, "Unsupported optional param '#{unsupported}'"}} end end) end @@ -587,35 +568,18 @@ defmodule Gringotts.Gateways.Monei do currency in @supported_currencies end - defp parse_response(%{"result" => result} = data) do - {address, zip_code} = @avs_code_translator[result["avsResponse"]] + defp make(action_type, _prefix, _param) when action_type in ["CP", "RF", "RV"], do: [] - results = [ - code: result["code"], - description: result["description"], - risk: data["risk"]["score"], - cvc_result: @cvc_code_translator[result["cvvResponse"]], - avs_result: [address: address, zip_code: zip_code], - raw: data, - token: data["registrationId"] - ] + defp make(action_type, prefix, param) do + case prefix do + :register -> + if action_type in ["PA", "DB"], do: [createRegistration: true], else: [] - filtered = Enum.filter(results, fn {_, v} -> v != nil end) - verify(filtered) - end - - defp verify(results) do - if String.match?(results[:code], ~r{^(000\.000\.|000\.100\.1|000\.[36])}) do - {:ok, results} - else - {:error, [{:reason, results[:description]} | results]} + _ -> + Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) end end - defp make(prefix, param) do - Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) - end - defp make_custom(custom_map) do Enum.into(custom_map, [], fn {k, v} -> {"customParameters[#{k}]", "#{v}"} end) end diff --git a/lib/gringotts/gateways/paymill.ex b/lib/gringotts/gateways/paymill.ex index 7a05bca1..e9ab3015 100644 --- a/lib/gringotts/gateways/paymill.ex +++ b/lib/gringotts/gateways/paymill.ex @@ -1,8 +1,8 @@ defmodule Gringotts.Gateways.Paymill do @moduledoc """ - An Api Client for the [PAYMILL](https://www.paymill.com/) gateway. + [PAYMILL][home] gateway implementation. - For refernce see [PAYMILL's API (v2.1) documentation](https://developers.paymill.com/API/index) + For refernce see [PAYMILL's API (v2.1) documentation][docs]. The following features of PAYMILL are implemented: @@ -11,384 +11,399 @@ defmodule Gringotts.Gateways.Paymill do | Authorize | `authorize/3` | | Capture | `capture/3` | | Purchase | `purchase/3` | + | Refund | `refund/3` | | Void | `void/2` | - Following fields are required for config + ## The `opts` argument - | Config Parameter | PAYMILL secret | - | private_key | **your_private_key** | - | public_key | **your_public_key** | + Most `Gringotts` API calls accept an optional `keyword` list `opts` to supply + optional arguments for transactions with the PAYMILL gateway. **Currently, no + optional params are supported.** - Your application config must include 'private_key', 'public_key' + ## Registering your PAYMILL account at `Gringotts` + + After [making an account on PAYMILL][dashboard], head to the dashboard and find + your account "secrets". + + Here's how the secrets map to the required configuration parameters for PAYMILL: + + | Config parameter | PAYMILL secret | + | ------- | ---- | + | `:private_key` | **Private Key** | + | `:public_key` | **Public Key** | + + Your Application config **must include the `:private_key`, `:public_key` + fields** and would look something like this: config :gringotts, Gringotts.Gateways.Paymill, - adapter: Gringotts.Gateways.Paymill, - private_key: "your_privat_key", - public_key: "your_public_key" + private_key: "your_secret_private_key", + public_key: "your_secret_public_key" + + ## Scope of this module + + * PAYMILL processes money in the sub-divided unit of currency (ie, in case of + USD it works in cents). + * PAYMILL does not offer direct API integration for [PCI DSS][pci-dss] + compliant merchants, everyone must use PAYMILL as if they are not PCI + compliant. + * To use their product, a merchant (aka user of this library) would have to + use their [Bridge (js integration)][bridge] (or equivalent) in your + application frontend to collect Credit/Debit Card data. + * This would obtain a unique `card_token` at the client-side which can be used + by this module for various operations like `authorize/3` and `purchase/3`. + + [bridge]: https://developers.paymill.com/guides/reference/paymill-bridge.html + + ## Supported countries + As a PAYMILL merchant you can accept payments from around the globe. For more details + refer to [Paymill country support][country-support]. + + ## Supported currencies + Your transactions will be processed in your native currency. For more information + refer to [Paymill currency support][currency-support]. + + ## Following the examples + + 1. First, set up a sample application and configure it to work with PAYMILL. + - You could do that from scratch by following our [Getting Started][gs] guide. + - To save you time, we recommend [cloning our example repo][example-repo] + 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-paymill-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.Paymill} + iex> amount = Money.new(4200, :EUR) + ``` + + We'll be using these in the examples below. + + [home]: https://paymill.com + [docs]: https://developers.paymill.com + [dashboard]: https://app.paymill.com/user/register + [gs]: https://github.com/aviabird/gringotts/wiki + [example-repo]: https://github.com/aviabird/gringotts_example + [currency-support]: https://www.paymill.com/en/faq/in-which-currency-will-my-transactions-be-processed-and-payout-in + [country-support]: https://www.paymill.com/en/faq/which-countries-is-paymill-available-in + [pci-dss]: https://www.paymill.com/en/pci-dss """ - use Gringotts.Gateways.Base - alias Gringotts.{CreditCard, Address, Response} - alias Gringotts.Gateways.Paymill.ResponseHandler, as: ResponseParser + use Gringotts.Gateways.Base use Gringotts.Adapter, required_config: [:private_key, :public_key] - @home_page "https://paymill.com" - @money_format :cents - @default_currency "EUR" - @live_url "https://api.paymill.com/v2.1/" + alias Gringotts.{Response, Money} + + @base_url "https://api.paymill.com/v2.1/" @headers [{"Content-Type", "application/x-www-form-urlencoded"}] + @response_code %{ + 10_001 => "Undefined response", + 10_002 => "Waiting for something", + 11_000 => "Retry request at a later time", + 20_000 => "Operation successful", + 20_100 => "Funds held by acquirer", + 20_101 => "Funds held by acquirer because merchant is new", + 20_200 => "Transaction reversed", + 20_201 => "Reversed due to chargeback", + 20_202 => "Reversed due to money-back guarantee", + 20_203 => "Reversed due to complaint by buyer", + 20_204 => "Payment has been refunded", + 20_300 => "Reversal has been canceled", + 22_000 => "Initiation of transaction successful", + 30_000 => "Transaction still in progress", + 30_100 => "Transaction has been accepted", + 31_000 => "Transaction pending", + 31_100 => "Pending due to address", + 31_101 => "Pending due to uncleared eCheck", + 31_102 => "Pending due to risk review", + 31_103 => "Pending due regulatory review", + 31_104 => "Pending due to unregistered/unconfirmed receiver", + 31_200 => "Pending due to unverified account", + 31_201 => "Pending due to non-captured funds", + 31_202 => "Pending due to international account (accept manually)", + 31_203 => "Pending due to currency conflict (accept manually)", + 31_204 => "Pending due to fraud filters (accept manually)", + 40_000 => "Problem with transaction data", + 40_001 => "Problem with payment data", + 40_002 => "Invalid checksum", + 40_100 => "Problem with credit card data", + 40_101 => "Problem with CVV", + 40_102 => "Card expired or not yet valid", + 40_103 => "Card limit exceeded", + 40_104 => "Card is not valid", + 40_105 => "Expiry date not valid", + 40_106 => "Credit card brand required", + 40_200 => "Problem with bank account data", + 40_201 => "Bank account data combination mismatch", + 40_202 => "User authentication failed", + 40_300 => "Problem with 3-D Secure data", + 40_301 => "Currency/amount mismatch", + 40_400 => "Problem with input data", + 40_401 => "Amount too low or zero", + 40_402 => "Usage field too long", + 40_403 => "Currency not allowed", + 40_410 => "Problem with shopping cart data", + 40_420 => "Problem with address data", + 40_500 => "Permission error with acquirer API", + 40_510 => "Rate limit reached for acquirer API", + 42_000 => "Initiation of transaction failed", + 42_410 => "Initiation of transaction expired", + 50_000 => "Problem with back end", + 50_001 => "Country blacklisted", + 50_002 => "IP address blacklisted", + 50_004 => "Live mode not allowed", + 50_005 => "Insufficient permissions (API key)", + 50_100 => "Technical error with credit card", + 50_101 => "Error limit exceeded", + 50_102 => "Card declined", + 50_103 => "Manipulation or stolen card", + 50_104 => "Card restricted", + 50_105 => "Invalid configuration data", + 50_200 => "Technical error with bank account", + 50_201 => "Account blacklisted", + 50_300 => "Technical error with 3-D Secure", + 50_400 => "Declined because of risk issues", + 50_401 => "Checksum was wrong", + 50_402 => "Bank account number was invalid (formal check)", + 50_403 => "Technical error with risk check", + 50_404 => "Unknown error with risk check", + 50_405 => "Unknown bank code", + 50_406 => "Open chargeback", + 50_407 => "Historical chargeback", + 50_408 => "Institution / public bank account (NCA)", + 50_409 => "KUNO/Fraud", + 50_410 => "Personal Account Protection (PAP)", + 50_420 => "Rejected due to acquirer fraud settings", + 50_430 => "Rejected due to acquirer risk settings", + 50_440 => "Failed due to restrictions with acquirer account", + 50_450 => "Failed due to restrictions with user account", + 50_500 => "General timeout", + 50_501 => "Timeout on side of the acquirer", + 50_502 => "Risk management transaction timeout", + 50_600 => "Duplicate operation", + 50_700 => "Cancelled by user", + 50_710 => "Failed due to funding source", + 50_711 => "Payment method not usable, use other payment method", + 50_712 => "Limit of funding source was exceeded", + 50_713 => "Means of payment not reusable (canceled by user)", + 50_714 => "Means of payment not reusable (expired)", + 50_720 => "Rejected by acquirer", + 50_730 => "Transaction denied by merchant", + 50_800 => "Preauthorisation failed", + 50_810 => "Authorisation has been voided", + 50_820 => "Authorisation period expired" + } + @doc """ - Authorize a card with particular amount and return a token in response + Performs a (pre) Authorize operation. + + The authorization validates the `card` details for `token` with the banking network, + places a hold on the transaction `amount` in the customer’s issuing bank and + also triggers risk management. Funds are not transferred. - ### Example - amount = 100 + The authorization token is available in the `Response.id` field. - card = %CreditCard{ - first_name: "Sagar", - last_name: "Karwande", - number: "4111111111111111", - month: 12, - year: 2018, - verification_code: 123 - } + ## Example - options = [] + The following example shows how one would (pre) authorize a payment of €42 on + a sample `token`. + ``` + iex> amount = Money.new(4200, :EUR) + iex> card_token = "tok_XXXXXXXXXXXXXXXXXXXXXXXXXXXX" - iex> Gringotts.authorize(Gringotts.Gateways.Paymill, amount, card, options) + iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Paymill, amount, card_token, opts) + iex> auth_result.id # This is the preauth-id + ``` """ - @spec authorize(number, String.t | CreditCard.t, Keyword) :: {:ok | :error, Response} - def authorize(amount, card_or_token, options) do - Keyword.put(options, :money, amount) - action_with_token(:authorize, amount, card_or_token, options) + + @spec authorize(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} + def authorize(amount, card_token, opts) do + params = [{:token, card_token} | amount_params(amount)] + commit(:post, "preauthorizations", params, opts) end @doc """ - Purchase with a card - - ### Example - amount = 100 + Captures a pre-authorized `amount`. - card = %CreditCard{ - first_name: "Sagar", - last_name: "Karwande", - number: "4111111111111111", - month: 12, - year: 2018, - verification_code: 123 - } - - options = [] - - iex> Gringotts.purchase(Gringotts.Gateways.Paymill, amount, card, options) - """ - @spec purchase(number, CreditCard.t, Keyword) :: {:ok | :error, Response} - def purchase(amount, card, options) do - Keyword.put(options, :money, amount) - action_with_token(:purchase, amount, card, options) - end + `amount` is transferred to the merchant account by PAYMILL when it is smaller or + equal to the amount used in the pre-authorization referenced by `preauth_id`. - @doc """ - Capture a particular amount with authorization token + ## Note - ### Example - amount = 100 + PAYMILL allows partial captures and unlike many other gateways, and releases + any remaining amount back to the payment source. + > Thus, the same pre-authorisation ID **cannot** be used to perform multiple + captures. - token = "preauth_14c7c5268eb155a599f0" + ## Example - options = [] + The following example shows how one would (partially) capture a previously + authorized a payment worth €42 by referencing the obtained authorization `id`. - iex> Gringotts.capture(Gringotts.Gateways.Paymill, token, amount, options) + ``` + iex> amount = Money.new(4200, :EUR) + iex> preauth_id = auth_result.id + # preauth_id = "some_authorization_id" + iex> Gringotts.capture(Gringotts.Gateways.Paymill, preauth_id, amount, opts) + ``` """ - @spec capture(String.t, number, Keyword) :: {:ok | :error, Response} - def capture(authorization, amount, options) do - post = add_amount([], amount, options) ++ [{"preauthorization", authorization}] - - commit(:post, "transactions", post, options) + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response.t()} + def capture(id, amount, opts) do + params = [{:preauthorization, id} | amount_params(amount)] + commit(:post, "transactions", params, opts) end @doc """ - Voids a particular authorized amount + Transfers `amount` from the customer to the merchant. - ### Example - token = "preauth_14c7c5268eb155a599f0" + PAYMILL attempts to process a purchase on behalf of the customer, by debiting + `amount` from the customer's account by charging the customer's `card` via `token`. - options = [] + ## Example - iex> Gringotts.void(Gringotts.Gateways.Paymill, token, options) - """ - @spec void(String.t, Keyword) :: {:ok | :error, Response} - def void(authorization, options) do - commit(:delete, "preauthorizations/#{authorization}", [], options) - end + The following example shows how one would process a payment worth €42 in + one-shot, without (pre) authorization. - @doc false - @spec authorize_with_token(number, String.t, Keyword) :: term - def authorize_with_token(money, card_token, options) do - post = add_amount([], money, options) ++ [{"token", card_token}] + ``` + iex> amount = Money.new(4200, :EUR) + iex> token = "tok_XXXXXXXXXXXXXXXXXXXXXXXXXXXX" - commit(:post, "preauthorizations", post, options) + iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Paymill, amount, token, opts) + ``` + """ + @spec purchase(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} + def purchase(amount, card_token, opts) do + param = [{:token, card_token} | amount_params(amount)] + commit(:post, "transactions", param, opts) end - @doc false - @spec purchase_with_token(number, String.t, Keyword) :: term - def purchase_with_token(money, card_token, options) do - post = add_amount([], money, options) ++ [{"token", card_token}] - - commit(:post, "transactions", post, options) - end + @doc """ + Refunds the `amount` to the customer's account with reference to a prior transfer. - @spec save_card(CreditCard.t, Keyword) :: Response - defp save_card(card, options) do - {:ok, %HTTPoison.Response{body: response}} = HTTPoison.get( - get_save_card_url(), - get_headers(options), - params: get_save_card_params(card, options)) + PAYMILL processes a full or partial refund worth `amount`, where `transaction_id` + references a previous `purchase/3` or `capture/3` result. - parse_card_response(response) - end + Multiple partial refunds are allowed on the same `transaction_id` till all the + captured/purchased amount has been refunded. - @spec save(CreditCard.t, Keyword) :: Response - defp save(card, options) do - save_card(card, options) - end + ## Example - defp action_with_token(action, amount, "tok_" <> id = card_token, options) do - apply(__MODULE__, String.to_atom("#{action}_with_token"), [amount, card_token , options]) + The following example shows how one would refund a previous purchase (and + similarily for captures). + ``` + iex> transaction_id = purchase_result.id + iex> amount = Money.new(4200, :EUR) + iex> Gringotts.refund(Gringotts.Gateways.Paymill, amount, transaction_id) + ``` + """ + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} + def refund(amount, id, opts) do + {_, int_value, _} = Money.to_integer(amount) + commit(:post, "refunds/#{id}", [amount: int_value], opts) end - defp action_with_token(action, amount, %CreditCard{} = card, options) do - {:ok, response} = save_card(card, options) - card_token = get_token(response) + @doc """ + Voids the referenced authorization. - apply(__MODULE__, String.to_atom("#{action}_with_token"), [amount, card_token , options]) - end + Attempts a reversal of the a previous `authorize/3` referenced by + `preauth_id`. - defp get_save_card_params(card, options) do - [ - {"transaction.mode" , "CONNECTOR_TEST"}, - {"channel.id" , get_config(:public_key, options)}, - {"jsonPFunction" , "jsonPFunction"}, - {"account.number" , card.number}, - {"account.expiry.month" , card.month}, - {"account.expiry.year" , card.year}, - {"account.verification" , card.verification_code}, - {"account.holder" , "#{card.first_name} #{card.last_name}"}, - {"presentation.amount3D" , get_amount(options)}, - {"presentation.currency3D" , get_currency(options)} - ] - end + ## Example - defp get_headers(options) do - @headers ++ set_username(options) + The following example shows how one would void a previous authorization. + ``` + iex> preauth_id = auth_result.id + iex> Gringotts.void(Gringotts.Gateways.Paymill, preauth_id) + ``` + """ + @spec void(String.t(), keyword) :: {:ok | :error, Response.t()} + def void(id, opts) do + commit(:delete, "preauthorizations/#{id}", [], opts) end - defp add_amount(post, money, options) do - post ++ [{"amount", money}, {"currency", @default_currency}] + defp commit(method, endpoint, params, opts) do + method + |> HTTPoison.request(base_url(opts) <> endpoint, {:form, params}, headers(opts)) + |> respond() end - defp set_username(options) do - [{"Authorization", "Basic #{Base.encode64(get_config(:private_key, options))}"}] + @response_code_paths [ + ~w[transaction response_code], + ~w[data response_code], + ~w[data transaction response_code] + ] + @token_paths [~w[id], ~w[data id]] + @reason_paths [~w[error], ~w[exception]] + @fraud_paths [ + ~w[transaction is_fraud], + ~w[data transaction is_fraud], + ~w[data transaction is_markable_as_fraud], + ~w[data is_markable_as_fraud] + ] + + defp get_either(collection, paths) do + paths + |> Stream.map(&get_in(collection, &1)) + |> Enum.find(fn x -> x != nil end) end - defp get_save_card_url(), do: "https://test-token.paymill.com/" - - defp parse_card_response(response) do - response - |> String.replace(~r/jsonPFunction\(/, "") - |> String.replace(~r/\)/, "") - |> Poison.decode + defp respond({:ok, %{status_code: 200, body: body}}) do + case Poison.decode(body) do + {:ok, parsed_resp} -> + gateway_code = get_either(parsed_resp, @response_code_paths) + + status = if gateway_code in [20_000, 50_810], do: :ok, else: :error + + {status, + %Response{ + id: get_either(parsed_resp, @token_paths), + token: parsed_resp["transaction"]["identification"]["uniqueId"], + status_code: 200, + gateway_code: gateway_code, + reason: get_either(parsed_resp, @reason_paths), + message: @response_code[gateway_code], + raw: body, + fraud_review: get_either(parsed_resp, @fraud_paths) + }} + + :error -> + {:error, + %Response{status_code: 200, raw: body, reason: "could not parse paymill response"}} + end end - defp get_currency(options), do: options[:currency] || @default_currency - - defp get_amount(options), do: options[:money] - - defp get_token(response) do - get_in(response, ["transaction", "identification", "uniqueId"]) + defp respond({:ok, %{status_code: status_code, body: body}}) do + {:error, + %Response{ + status_code: status_code, + raw: body + }} end - defp commit(method, action, parameters \\ nil, options) do - method - |> HTTPoison.request(@live_url <> action, {:form, parameters}, get_headers(options), []) - |> ResponseParser.parse + defp respond({:error, %HTTPoison.Error{} = error}) do + { + :error, + Response.error( + reason: "network related failure", + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]" + ) + } end - defp get_config(key, options) do - get_in(options, [:config, key]) + defp headers(opts) do + [ + {"Authorization", "Basic #{Base.encode64(get_in(opts, [:config, :private_key]))}"} + | @headers + ] end - @moduledoc false - defmodule ResponseHandler do - alias Gringotts.Response - - @response_code %{ - 10_001 => "Undefined response", - 10_002 => "Waiting for something", - 11_000 => "Retry request at a later time", - - 20_000 => "Operation successful", - 20_100 => "Funds held by acquirer", - 20_101 => "Funds held by acquirer because merchant is new", - 20_200 => "Transaction reversed", - 20_201 => "Reversed due to chargeback", - 20_202 => "Reversed due to money-back guarantee", - 20_203 => "Reversed due to complaint by buyer", - 20_204 => "Payment has been refunded", - 20_300 => "Reversal has been canceled", - 22_000 => "Initiation of transaction successful", - - 30_000 => "Transaction still in progress", - 30_100 => "Transaction has been accepted", - 31_000 => "Transaction pending", - 31_100 => "Pending due to address", - 31_101 => "Pending due to uncleared eCheck", - 31_102 => "Pending due to risk review", - 31_103 => "Pending due regulatory review", - 31_104 => "Pending due to unregistered/unconfirmed receiver", - 31_200 => "Pending due to unverified account", - 31_201 => "Pending due to non-captured funds", - 31_202 => "Pending due to international account (accept manually)", - 31_203 => "Pending due to currency conflict (accept manually)", - 31_204 => "Pending due to fraud filters (accept manually)", - - 40_000 => "Problem with transaction data", - 40_001 => "Problem with payment data", - 40_002 => "Invalid checksum", - 40_100 => "Problem with credit card data", - 40_101 => "Problem with CVV", - 40_102 => "Card expired or not yet valid", - 40_103 => "Card limit exceeded", - 40_104 => "Card is not valid", - 40_105 => "Expiry date not valid", - 40_106 => "Credit card brand required", - 40_200 => "Problem with bank account data", - 40_201 => "Bank account data combination mismatch", - 40_202 => "User authentication failed", - 40_300 => "Problem with 3-D Secure data", - 40_301 => "Currency/amount mismatch", - 40_400 => "Problem with input data", - 40_401 => "Amount too low or zero", - 40_402 => "Usage field too long", - 40_403 => "Currency not allowed", - 40_410 => "Problem with shopping cart data", - 40_420 => "Problem with address data", - 40_500 => "Permission error with acquirer API", - 40_510 => "Rate limit reached for acquirer API", - 42_000 => "Initiation of transaction failed", - 42_410 => "Initiation of transaction expired", - - 50_000 => "Problem with back end", - 50_001 => "Country blacklisted", - 50_002 => "IP address blacklisted", - 50_004 => "Live mode not allowed", - 50_005 => "Insufficient permissions (API key)", - 50_100 => "Technical error with credit card", - 50_101 => "Error limit exceeded", - 50_102 => "Card declined", - 50_103 => "Manipulation or stolen card", - 50_104 => "Card restricted", - 50_105 => "Invalid configuration data", - 50_200 => "Technical error with bank account", - 50_201 => "Account blacklisted", - 50_300 => "Technical error with 3-D Secure", - 50_400 => "Declined because of risk issues", - 50_401 => "Checksum was wrong", - 50_402 => "Bank account number was invalid (formal check)", - 50_403 => "Technical error with risk check", - 50_404 => "Unknown error with risk check", - 50_405 => "Unknown bank code", - 50_406 => "Open chargeback", - 50_407 => "Historical chargeback", - 50_408 => "Institution / public bank account (NCA)", - 50_409 => "KUNO/Fraud", - 50_410 => "Personal Account Protection (PAP)", - 50_420 => "Rejected due to acquirer fraud settings", - 50_430 => "Rejected due to acquirer risk settings", - 50_440 => "Failed due to restrictions with acquirer account", - 50_450 => "Failed due to restrictions with user account", - 50_500 => "General timeout", - 50_501 => "Timeout on side of the acquirer", - 50_502 => "Risk management transaction timeout", - 50_600 => "Duplicate operation", - 50_700 => "Cancelled by user", - 50_710 => "Failed due to funding source", - 50_711 => "Payment method not usable, use other payment method", - 50_712 => "Limit of funding source was exceeded", - 50_713 => "Means of payment not reusable (canceled by user)", - 50_714 => "Means of payment not reusable (expired)", - 50_720 => "Rejected by acquirer", - 50_730 => "Transaction denied by merchant", - 50_800 => "Preauthorisation failed", - 50_810 => "Authorisation has been voided", - 50_820 => "Authorisation period expired" - } - - def parse({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do - body = Poison.decode!(body) - parse_body(body) - end - def parse({:ok, %HTTPoison.Response{body: body, status_code: 400}}) do - body = Poison.decode!(body) - [] - |> set_params(body) - end - def parse({:ok, %HTTPoison.Response{body: body, status_code: 404}}) do - body = Poison.decode!(body) - [] - |> set_success(body) - |> set_params(body) - |> handle_opts() - end - - defp set_success(opts, %{"error" => error}) do - opts ++ [message: error, success: false] - end - defp set_success(opts, %{"transaction" => %{"response_code" => 20_000}}) do - opts ++ [success: true] - end - - defp parse_body(%{"data" => data}) do - [] - |> set_success(data) - |> parse_authorization(data) - |> parse_status_code(data) - |> set_params(data) - |> handle_opts() - end - - defp handle_opts(opts) do - case Keyword.fetch(opts, :success) do - {:ok, true} -> {:ok, Response.success(opts)} - {:ok, false} -> {:error, Response.error(opts)} - end - end - - #Status code - defp parse_status_code(opts, %{"status" => "failed"} = body) do - response_code = get_in(body, ["transaction", "response_code"]) - response_msg = Map.get(@response_code, response_code, -1) - opts ++ [message: response_msg] - end - defp parse_status_code(opts, %{"transaction" => transaction}) do - response_code = Map.get(transaction, "response_code", -1) - response_msg = Map.get(@response_code, response_code, -1) - opts ++ [status_code: response_code, message: response_msg] - end - defp parse_status_code(opts, %{"response_code" => code}) do - response_msg = Map.get(@response_code, code, -1) - opts ++ [status_code: code, message: response_msg] - end - - #Authorization - defp parse_authorization(opts, %{"status" => "failed"}) do - opts ++ [success: false] - end - defp parse_authorization(opts, %{"id" => id} = auth) do - opts ++ [authorization: id] - end - - defp set_params(opts, body), do: opts ++ [params: body] + defp amount_params(money) do + {currency, int_value, _} = Money.to_integer(money) + [amount: int_value, currency: currency] end + defp base_url(opts), do: opts[:config][:test_url] || @base_url end diff --git a/lib/gringotts/gateways/stripe.ex b/lib/gringotts/gateways/stripe.ex index 1a0d2b89..94a8ce48 100644 --- a/lib/gringotts/gateways/stripe.ex +++ b/lib/gringotts/gateways/stripe.ex @@ -1,9 +1,8 @@ defmodule Gringotts.Gateways.Stripe do - @moduledoc """ Stripe gateway implementation. For reference see [Stripe's API documentation](https://stripe.com/docs/api). The following features of Stripe are implemented: - + | Action | Method | | ------ | ------ | | Pre-authorize | `authorize/3` | @@ -18,7 +17,7 @@ defmodule Gringotts.Gateways.Stripe do Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply optional arguments for transactions with the Stripe gateway. The following keys are supported: - + | Key | Remark | Status | | ---- | --- | ---- | | `currency` | | **Implemented** | @@ -38,20 +37,19 @@ defmodule Gringotts.Gateways.Stripe do | `default_source` | | Not implemented | | `email` | | Not implemented | | `shipping` | | Not implemented | - + ## Registering your Stripe account at `Gringotts` After [making an account on Stripe](https://stripe.com/), head to the dashboard and find your account `secrets` in the `API` section. - + ## Here's how the secrets map to the required configuration parameters for Stripe: | Config parameter | Stripe secret | | ------- | ---- | | `:secret_key` | **Secret key** | - + Your Application config must look something like this: - + config :gringotts, Gringotts.Gateways.Stripe, - adapter: Gringotts.Gateways.Stripe, secret_key: "your_secret_key", default_currency: "usd" """ @@ -59,11 +57,12 @@ defmodule Gringotts.Gateways.Stripe do @base_url "https://api.stripe.com/v1" use Gringotts.Gateways.Base - use Gringotts.Adapter, required_config: [:secret_key, :default_currency] + use Gringotts.Adapter, required_config: [:secret_key] alias Gringotts.{ CreditCard, - Address + Address, + Money } @doc """ @@ -72,17 +71,17 @@ defmodule Gringotts.Gateways.Stripe do The authorization validates the card details with the banking network, places a hold on the transaction amount in the customer’s issuing bank and also triggers risk management. Funds are not transferred. - + Stripe returns an `charge_id` which should be stored at your side and can be used later to: * `capture/3` an amount. * `void/2` a pre-authorization. - + ## Note Uncaptured charges expire in 7 days. For more information, [see authorizing charges and settling later](https://support.stripe.com/questions/can-i-authorize-a-charge-and-then-wait-to-settle-it-later). ## Example The following session shows how one would (pre) authorize a payment of $10 on a sample `card`. - + iex> card = %CreditCard{ first_name: "John", last_name: "Smith", @@ -105,7 +104,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.authorize(Gringotts.Gateways.Stripe, amount, card, opts) """ - @spec authorize(number, CreditCard.t() | String.t(), keyword) :: map + @spec authorize(Money.t(), CreditCard.t() | String.t(), keyword) :: map def authorize(amount, payment, opts \\ []) do params = create_params_for_auth_or_purchase(amount, payment, opts, false) commit(:post, "charges", params, opts) @@ -113,10 +112,10 @@ defmodule Gringotts.Gateways.Stripe do @doc """ Transfers amount from the customer to the merchant. - + Stripe attempts to process a purchase on behalf of the customer, by debiting amount from the customer's account by charging the customer's card. - + ## Example The following session shows how one would process a payment in one-shot, without (pre) authorization. @@ -143,7 +142,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.purchase(Gringotts.Gateways.Stripe, amount, card, opts) """ - @spec purchase(number, CreditCard.t() | String.t(), keyword) :: map + @spec purchase(Money.t(), CreditCard.t() | String.t(), keyword) :: map def purchase(amount, payment, opts \\ []) do params = create_params_for_auth_or_purchase(amount, payment, opts) commit(:post, "charges", params, opts) @@ -169,7 +168,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.capture(Gringotts.Gateways.Stripe, id, amount, opts) """ - @spec capture(String.t(), number, keyword) :: map + @spec capture(String.t(), Money.t(), keyword) :: map def capture(id, amount, opts \\ []) do params = optional_params(opts) ++ amount_params(amount) commit(:post, "charges/#{id}/capture", params, opts) @@ -177,7 +176,7 @@ defmodule Gringotts.Gateways.Stripe do @doc """ Voids the referenced payment. - + This method attempts a reversal of the either a previous `purchase/3` or `authorize/3` referenced by `charge_id`. As a consequence, the customer will never see any booking on his @@ -191,7 +190,7 @@ defmodule Gringotts.Gateways.Stripe do ## Voiding a previous purchase Stripe will reverse the payment, by sending all the amount back to the customer. Note that this is not the same as `refund/3`. - + ## Example The following session shows how one would void a previous (pre) authorization. Remember that our `capture/3` example only did a partial @@ -224,7 +223,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.refund(Gringotts.Gateways.Stripe, amount, id, opts) """ - @spec refund(number, String.t(), keyword) :: map + @spec refund(Money.t(), String.t(), keyword) :: map def refund(amount, id, opts \\ []) do params = optional_params(opts) ++ amount_params(amount) commit(:post, "charges/#{id}/refund", params, opts) @@ -232,7 +231,7 @@ defmodule Gringotts.Gateways.Stripe do @doc """ Stores the payment-source data for later use. - + Stripe can store the payment-source details, for example card which can be used to effectively to process One-Click and Recurring_ payments, and return a `customer_id` for reference. @@ -286,27 +285,22 @@ defmodule Gringotts.Gateways.Stripe do # Private methods defp create_params_for_auth_or_purchase(amount, payment, opts, capture \\ true) do - params = optional_params(opts) - ++ [capture: capture] - ++ amount_params(amount) - ++ source_params(payment, opts) - - params - |> Keyword.has_key?(:currency) - |> with_currency(params, opts[:config]) + [capture: capture] ++ + optional_params(opts) ++ amount_params(amount) ++ source_params(payment, opts) end - def with_currency(true, params, _), do: params - def with_currency(false, params, config), do: [{:currency, config[:default_currency]} | params] - defp create_card_token(params, opts) do commit(:post, "tokens", params, opts) end - defp amount_params(amount), do: [amount: money_to_cents(amount)] + defp amount_params(amount) do + {currency, int_value, _} = Money.to_integer(amount) + [amount: int_value, currency: currency] + end defp source_params(token_or_customer, _) when is_binary(token_or_customer) do [head, _] = String.split(token_or_customer, "_") + case head do "tok" -> [source: token_or_customer] "cus" -> [customer: token_or_customer] @@ -314,64 +308,68 @@ defmodule Gringotts.Gateways.Stripe do end defp source_params(%CreditCard{} = card, opts) do - params = - card_params(card) ++ - address_params(opts[:address]) + params = card_params(card) ++ address_params(opts[:address]) response = create_card_token(params, opts) - case Map.has_key?(response, "error") do - true -> [] - false -> response - |> Map.get("id") - |> source_params(opts) + if Map.has_key?(response, "error") do + [] + else + response + |> Map.get("id") + |> source_params(opts) end end defp source_params(_, _), do: [] defp card_params(%CreditCard{} = card) do - [ "card[name]": CreditCard.full_name(card), + [ + "card[name]": CreditCard.full_name(card), "card[number]": card.number, "card[exp_year]": card.year, "card[exp_month]": card.month, "card[cvc]": card.verification_code - ] + ] end defp card_params(_), do: [] defp address_params(%Address{} = address) do - [ "card[address_line1]": address.street1, + [ + "card[address_line1]": address.street1, "card[address_line2]": address.street2, - "card[address_city]": address.city, + "card[address_city]": address.city, "card[address_state]": address.region, - "card[address_zip]": address.postal_code, + "card[address_zip]": address.postal_code, "card[address_country]": address.country ] end defp address_params(_), do: [] - defp commit(method, path, params \\ [], opts \\ []) do + defp commit(method, path, params, opts) do auth_token = "Bearer " <> opts[:config][:secret_key] - headers = [{"Content-Type", "application/x-www-form-urlencoded"}, {"Authorization", auth_token}] - data = params_to_string(params) - response = HTTPoison.request(method, "#{@base_url}/#{path}", data, headers) + + headers = [ + {"Content-Type", "application/x-www-form-urlencoded"}, + {"Authorization", auth_token} + ] + + response = HTTPoison.request(method, "#{@base_url}/#{path}", {:form, params}, headers) format_response(response) end defp optional_params(opts) do opts - |> Keyword.delete(:config) - |> Keyword.delete(:address) + |> Keyword.delete(:config) + |> Keyword.delete(:address) end defp format_response(response) do case response do - {:ok, %HTTPoison.Response{body: body}} -> body |> Poison.decode! + {:ok, %HTTPoison.Response{body: body}} -> body |> Poison.decode!() _ -> %{"error" => "something went wrong, please try again later"} end end - end diff --git a/lib/gringotts/gateways/trexle.ex b/lib/gringotts/gateways/trexle.ex index d89d7767..a9c3d988 100644 --- a/lib/gringotts/gateways/trexle.ex +++ b/lib/gringotts/gateways/trexle.ex @@ -41,7 +41,6 @@ defmodule Gringotts.Gateways.Trexle do Your Application config must look something like this: config :gringotts, Gringotts.Gateways.Trexle, - adapter: Gringotts.Gateways.Trexle, api_key: "your-secret-API-key" [dashboard]: https://trexle.com/dashboard/ @@ -67,35 +66,16 @@ defmodule Gringotts.Gateways.Trexle do that as described [above](#module-registering-your-trexle-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.Trexle} - iex> card = %CreditCard{ - first_name: "Harry", - last_name: "Potter", - number: "4200000000000000", - year: 2099, month: 12, - verification_code: "123", - brand: "VISA"} - iex> address = %Address{ - street1: "301, Gryffindor", - street2: "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", - city: "Highlands", - region: "SL", - country: "GB", - postal_code: "11111", - phone: "(555)555-5555"} - iex> options = [email: "masterofdeath@ministryofmagic.gov", - ip_address: "127.0.0.1", - billing_address: address, - description: "For our valued customer, Mr. Potter"] - ``` + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in + [this gist][trexle.iex.exs] to introduce a set of handy bindings and + aliases. - We'll be using these in the examples below. + We'll be using these bindings in the examples below. [example-repo]: https://github.com/aviabird/gringotts_example - [gs]: # + [iex-docs]: https://hexdocs.pm/iex/IEx.html#module-the-iex-exs-file + [trexle.iex.exs]: https://gist.github.com/oyeb/055f40e9ad4102f5480febd2cfa00787 + [gs]: https://github.com/aviabird/gringotts/wiki """ @base_url "https://core.trexle.com/api/v1/" @@ -121,7 +101,7 @@ defmodule Gringotts.Gateways.Trexle do a sample `card`. ``` - iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> amount = Money.new(10, :USD) iex> card = %CreditCard{ first_name: "Harry", last_name: "Potter", @@ -171,7 +151,7 @@ defmodule Gringotts.Gateways.Trexle do authorized a payment worth $10 by referencing the obtained `charge_token`. ``` - iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> amount = Money.new(10, :USD) iex> token = "some-real-token" iex> Gringotts.capture(Gringotts.Gateways.Trexle, token, amount) ``` @@ -195,7 +175,7 @@ defmodule Gringotts.Gateways.Trexle do one-shot, without (pre) authorization. ``` - iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> amount = Money.new(10, :USD) iex> card = %CreditCard{ first_name: "Harry", last_name: "Potter", @@ -243,7 +223,7 @@ defmodule Gringotts.Gateways.Trexle do `purchase/3` (and similarily for `capture/3`s). ``` - iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> amount = Money.new(10, :USD) iex> token = "some-real-token" iex> Gringotts.refund(Gringotts.Gateways.Trexle, amount, token) ``` @@ -351,17 +331,23 @@ defmodule Gringotts.Gateways.Trexle do { :ok, - Response.success(authorization: token, message: message, raw: results, status_code: code) + %Response{id: token, message: message, raw: body, status_code: code} } end defp respond({:ok, %{status_code: status_code, body: body}}) do {:ok, results} = decode(body) detail = results["detail"] - {:error, Response.error(status_code: status_code, message: detail, raw: results)} + {:error, %Response{status_code: status_code, message: detail, reason: detail, raw: body}} end defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, Response.error(code: error.id, message: "HTTPoison says '#{error.reason}'")} + { + :error, + %Response{ + reason: "network related failure", + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]" + } + } end end diff --git a/lib/gringotts/gateways/wire_card.ex b/lib/gringotts/gateways/wire_card.ex index 5aafeb37..939a0fef 100644 --- a/lib/gringotts/gateways/wire_card.ex +++ b/lib/gringotts/gateways/wire_card.ex @@ -1,5 +1,6 @@ # call => Gringotts.Gateways.WireCard.authorize(100, creditcard, options) import XmlBuilder + defmodule Gringotts.Gateways.WireCard do @moduledoc """ WireCard System Plugins @@ -7,7 +8,7 @@ defmodule Gringotts.Gateways.WireCard do @test_url "https://c3-test.wirecard.com/secure/ssl-gateway" @live_url "https://c3.wirecard.com/secure/ssl-gateway" @homepage_url "http://www.wirecard.com" - + @doc """ Wirecard only allows phone numbers with a format like this: +xxx(yyy)zzz-zzzz-ppp, where: xxx = Country code @@ -18,8 +19,8 @@ defmodule Gringotts.Gateways.WireCard do number 5551234 within area code 202 (country code 1). """ @valid_phone_format ~r/\+\d{1,3}(\(?\d{3}\)?)?\d{3}-\d{4}-\d{3}/ - @default_currency "EUR" - @default_amount 100 + @default_currency "EUR" + @default_amount 100 use Gringotts.Gateways.Base use Gringotts.Adapter, required_config: [:login, :password, :signature] @@ -38,10 +39,10 @@ defmodule Gringotts.Gateways.WireCard do then then the :recurring option will be forced to "Repeated" =========================================================== TODO: Mandatorily check for :login,:password, :signature in options - Note: payment_menthod for now is only credit_card and + Note: payment_menthod for now is only credit_card and TODO: change it so it can also have GuWID ================================================ - E.g: => + E.g: => creditcard = %CreditCard{ number: "4200000000000000", month: 12, @@ -65,19 +66,19 @@ defmodule Gringotts.Gateways.WireCard do } options = [ config: %{ - login: "00000031629CA9FA", + login: "00000031629CA9FA", password: "TestXAPTER", signature: "00000031629CAFD5", - }, + }, order_id: 1, billing_address: address, description: 'Wirecard remote test purchase', email: "soleone@example.com", ip: "127.0.0.1", test: true - ] + ] """ - @spec authorize(Integer | Float, CreditCard.t | String.t, Keyword) :: {:ok, Map} + @spec authorize(Integer | Float, CreditCard.t() | String.t(), Keyword) :: {:ok, Map} def authorize(money, payment_method, options \\ []) def authorize(money, %CreditCard{} = creditcard, options) do @@ -92,9 +93,9 @@ defmodule Gringotts.Gateways.WireCard do @doc """ Capture - the first paramter here should be a GuWid/authorization. - Authorization is obtained by authorizing the creditcard. + Authorization is obtained by authorizing the creditcard. """ - @spec capture(String.t, Float, Keyword) :: {:ok, Map} + @spec capture(String.t(), Float, Keyword) :: {:ok, Map} def capture(authorization, money, options \\ []) when is_binary(authorization) do options = Keyword.put(options, :preauthorization, authorization) commit(:post, :capture, money, options) @@ -106,7 +107,7 @@ defmodule Gringotts.Gateways.WireCard do transaction. If a GuWID is given, rather than a CreditCard, then then the :recurring option will be forced to "Repeated" """ - @spec purchase(Float | Integer, CreditCard| String.t, Keyword) :: {:ok, Map} + @spec purchase(Float | Integer, CreditCard | String.t(), Keyword) :: {:ok, Map} def purchase(money, payment_method, options \\ []) def purchase(money, %CreditCard{} = creditcard, options) do @@ -120,33 +121,33 @@ defmodule Gringotts.Gateways.WireCard do end @doc """ - Void - A credit card purchase that a seller cancels after it has - been authorized but before it has been settled. - A void transaction does not appear on the customer's + Void - A credit card purchase that a seller cancels after it has + been authorized but before it has been settled. + A void transaction does not appear on the customer's credit card statement, though it might appear in a list - of pending transactions when the customer checks their + of pending transactions when the customer checks their account online. ==== Parameters ====== identification - The authorization string returned from the initial authorization or purchase. """ - @spec void(String.t, Keyword) :: {:ok, Map} + @spec void(String.t(), Keyword) :: {:ok, Map} def void(identification, options \\ []) when is_binary(identification) do options = Keyword.put(options, :preauthorization, identification) commit(:post, :reversal, nil, options) end - + @doc """ Performs a credit. - - This transaction indicates that money - should flow from the merchant to the customer. + + This transaction indicates that money + should flow from the merchant to the customer. ==== Parameters ==== - money -- The amount to be credited to the customer + money -- The amount to be credited to the customer as an Integer value in cents. identification -- GuWID """ - @spec refund(Float, String.t, Keyword) :: {:ok, Map} + @spec refund(Float, String.t(), Keyword) :: {:ok, Map} def refund(money, identification, options \\ []) when is_binary(identification) do options = Keyword.put(options, :preauthorization, identification) commit(:post, :bookback, money, options) @@ -161,54 +162,61 @@ defmodule Gringotts.Gateways.WireCard do "RECURRING_TRANSACTION/Type" set to "Initial". Subsequent transactions can then use the GuWID in place of a credit card by setting "RECURRING_TRANSACTION/Type" to "Repeated". - + This implementation of card store utilizes a Wirecard "Authorization Check" (a Preauthorization that is automatically reversed). It defaults to a check amount of "100" (i.e. $1.00) but this can be overriden (see below). - + IMPORTANT: In order to reuse the stored reference, the +authorization+ from the response should be saved by your application code. - + ==== Options specific to +store+ - + * :amount -- The amount, in cents, that should be "validated" by the Authorization Check. This amount will be reserved and then reversed. Default is 100. - + Note: This is not the only way to achieve a card store operation at Wirecard. Any +purchase+ or +authorize+ can be sent with +options[:recurring] = 'Initial'+ to make the returned authorization/GuWID usable in later transactions with +options[:recurring] = 'Repeated'+. """ - @spec store(CreditCard.t, Keyword) :: {:ok, Map} + @spec store(CreditCard.t(), Keyword) :: {:ok, Map} def store(%CreditCard{} = creditcard, options \\ []) do - options = options - |> Keyword.put(:credit_card, creditcard) - |> Keyword.put(:recurring, "Initial") + options = + options + |> Keyword.put(:credit_card, creditcard) + |> Keyword.put(:recurring, "Initial") + money = options[:amount] || @default_amount # Amex does not support authorization_check case creditcard.brand do "american_express" -> commit(:post, :preauthorization, money, options) - _ -> commit(:post, :authorization_check, money, options) + _ -> commit(:post, :authorization_check, money, options) end end - - # =================== Private Methods =================== - + + # =================== Private Methods =================== + # Contact WireCard, make the XML request, and parse the # reply into a Response object. defp commit(method, action, money, options) do # TODO: validate and setup address hash as per AM request = build_request(action, money, options) - headers = %{"Content-Type" => "text/xml", - "Authorization" => encoded_credentials( - options[:config][:login], options[:config][:password] - ) - } - method |> HTTPoison.request(base_url(options) , request, headers) |> respond + + headers = %{ + "Content-Type" => "text/xml", + "Authorization" => + encoded_credentials( + options[:config][:login], + options[:config][:password] + ) + } + + method |> HTTPoison.request(base_url(options), request, headers) |> respond end defp respond({:ok, %{status_code: 200, body: body}}) do @@ -217,13 +225,13 @@ defmodule Gringotts.Gateways.WireCard do end defp respond({:ok, %{body: body, status_code: status_code}}) do - {:error, "Some Error Occurred: \n #{ inspect body }"} + {:error, "Some Error Occurred: \n #{inspect(body)}"} end # Read the XML message from the gateway and check if it was successful, # and also extract required return values from the response # TODO: parse XML Response - defp parse(data) do + defp parse(data) do XmlToMap.naive_map(data) end @@ -231,15 +239,18 @@ defmodule Gringotts.Gateways.WireCard do defp build_request(action, money, options) do options = Keyword.put(options, :action, action) - request = doc(element(:WIRECARD_BXML, [ - element(:W_REQUEST, [ - element(:W_JOB, [ - element(:JobID, ""), - element(:BusinessCaseSignature, options[:config][:signature]), - add_transaction_data(action, money, options) + request = + doc( + element(:WIRECARD_BXML, [ + element(:W_REQUEST, [ + element(:W_JOB, [ + element(:JobID, ""), + element(:BusinessCaseSignature, options[:config][:signature]), + add_transaction_data(action, money, options) + ]) ]) ]) - ])) + ) request end @@ -250,11 +261,14 @@ defmodule Gringotts.Gateways.WireCard do defp add_transaction_data(action, money, options) do element("FNC_CC_#{atom_to_upcase_string(options[:action])}", [ element(:FunctionID, "dummy_description"), - element(:CC_TRANSACTION, [ - element(:TransactionID, options[:order_id]), - element(:CommerceType, (if options[:commerce_type], do: options[:commerce_type])) - ] ++ add_action_data(action, money, options) ++ add_customer_data(options) - )]) + element( + :CC_TRANSACTION, + [ + element(:TransactionID, options[:order_id]), + element(:CommerceType, if(options[:commerce_type], do: options[:commerce_type])) + ] ++ add_action_data(action, money, options) ++ add_customer_data(options) + ) + ]) end # Includes the IP address of the customer to the transaction-xml @@ -269,9 +283,14 @@ defmodule Gringotts.Gateways.WireCard do def add_action_data(action, money, options) do case options[:action] do # returns array of elements - action when(action in [:preauthorization, :purchase, :authorization_check]) -> create_elems_for_preauth_or_purchase_or_auth_check(money, options) - action when(action in [:capture, :bookback]) -> create_elems_for_capture_or_bookback(money, options) - action when(action == :reversal) -> add_guwid(options[:preauthorization]) + action when action in [:preauthorization, :purchase, :authorization_check] -> + create_elems_for_preauth_or_purchase_or_auth_check(money, options) + + action when action in [:capture, :bookback] -> + create_elems_for_capture_or_bookback(money, options) + + action when action == :reversal -> + add_guwid(options[:preauthorization]) end end @@ -280,25 +299,29 @@ defmodule Gringotts.Gateways.WireCard do add_guwid(options[:preauthorization]) ++ [add_amount(money, options)] end - # Creates xml request elements if action is preauth, purchase ir auth_check + # Creates xml request elements if action is preauth, purchase ir auth_check # TODO: handle nil values if array not generated defp create_elems_for_preauth_or_purchase_or_auth_check(money, options) do # TODO: setup_recurring_flag - add_invoice(money, options) ++ element_for_credit_card_or_guwid(options) ++ add_address(options[:billing_address]) + add_invoice(money, options) ++ + element_for_credit_card_or_guwid(options) ++ add_address(options[:billing_address]) end - + defp add_address(address) do if address do [ element(:CORPTRUSTCENTER_DATA, [ element(:ADDRESS, [ element(:Address1, address[:address1]), - element(:Address2, (if address[:address2], do: address[:address2])), + element(:Address2, if(address[:address2], do: address[:address2])), element(:City, address[:city]), - element(:Zip, address[:zip]), + element(:Zip, address[:zip]), add_state(address), element(:Country, address[:country]), - element(:Phone, (if regex_match(@valid_phone_format, address[:phone]), do: address[:phone])), + element( + :Phone, + if(regex_match(@valid_phone_format, address[:phone]), do: address[:phone]) + ), element(:Email, address[:email]) ]) ]) @@ -307,9 +330,9 @@ defmodule Gringotts.Gateways.WireCard do end defp add_state(address) do - if (regex_match(~r/[A-Za-z]{2}/, address[:state]) && regex_match(~r/^(us|ca)$/i, address[:country]) - ) do - element(:State, (String.upcase(address[:state]))) + if regex_match(~r/[A-Za-z]{2}/, address[:state]) && + regex_match(~r/^(us|ca)$/i, address[:country]) do + element(:State, String.upcase(address[:state])) end end @@ -320,7 +343,7 @@ defmodule Gringotts.Gateways.WireCard do add_guwid(options[:preauthorization]) end end - + # Includes Guwid data to transaction-xml defp add_guwid(preauth) do [element(:GuWID, preauth)] @@ -329,13 +352,15 @@ defmodule Gringotts.Gateways.WireCard do # Includes the credit-card data to the transaction-xml # TODO: Format Credit Card month, ref AM defp add_creditcard(creditcard) do - [element(:CREDIT_CARD_DATA, [ - element(:CreditCardNumber, creditcard.number), - element(:CVC2, creditcard.verification_code), - element(:ExpirationYear, creditcard.year), - element(:ExpirationMonth, creditcard.month), - element(:CardHolderName, join_string([creditcard.first_name, creditcard.last_name], " ")) - ])] + [ + element(:CREDIT_CARD_DATA, [ + element(:CreditCardNumber, creditcard.number), + element(:CVC2, creditcard.verification_code), + element(:ExpirationYear, creditcard.year), + element(:ExpirationMonth, creditcard.month), + element(:CardHolderName, join_string([creditcard.first_name, creditcard.last_name], " ")) + ]) + ] end # Includes the payment (amount, currency, country) to the transaction-xml @@ -345,11 +370,11 @@ defmodule Gringotts.Gateways.WireCard do element(:Currency, currency(options)), element(:CountryCode, options[:billing_address][:country]), element(:RECURRING_TRANSACTION, [ - element(:Type, (options[:recurring] || "Single")) + element(:Type, options[:recurring] || "Single") ]) ] end - + # Include the amount in the transaction-xml # TODO: check for localized currency or currency # localized_amount(money, options[:currency] || currency(money)) @@ -357,24 +382,24 @@ defmodule Gringotts.Gateways.WireCard do defp atom_to_upcase_string(atom) do atom - |> to_string - |> String.upcase + |> to_string + |> String.upcase() end # Encode login and password in Base64 to supply as HTTP header # (for http basic authentication) defp encoded_credentials(login, password) do [login, password] - |> join_string(":") - |> Base.encode64 - |> (&("Basic "<> &1)).() + |> join_string(":") + |> Base.encode64() + |> (&("Basic " <> &1)).() end defp join_string(list_of_words, joiner), do: Enum.join(list_of_words, joiner) defp regex_match(regex, string), do: Regex.match?(regex, string) - defp base_url(opts), do: if opts[:test], do: @test_url, else: @live_url + defp base_url(opts), do: if(opts[:test], do: @test_url, else: @live_url) defp currency(opts), do: opts[:currency] || @default_currency end diff --git a/lib/gringotts/response.ex b/lib/gringotts/response.ex index f9097490..c64ec0a2 100644 --- a/lib/gringotts/response.ex +++ b/lib/gringotts/response.ex @@ -1,26 +1,82 @@ defmodule Gringotts.Response do - @moduledoc ~S""" - Module which defines the struct for response struct. - - Response struct is a standard response from public API to the application. - - It mostly has such as:- - * `success`: boolean indicating the status of the transaction - * `authorization`: token which is used to issue requests without the card info - * `status_code`: response code - * `error_code`: error code if there is error else nil - * `message`: message related to the status of the response - * `avs_result`: result for address verfication - * `cvc_result`: result for cvc verification - * `params`: original raw response from the gateway - * `fraud_review`: information related to fraudulent transactions + @moduledoc """ + Defines the Response `struct` and some utilities. + + All `Gringotts` public API calls will return a `Response.t` wrapped in an + `:ok` or `:error` `tuple`. It is guaranteed that an `:ok` will be returned + only when the request succeeds at the gateway, ie, no error occurs. """ - + defstruct [ - :success, :authorization, :status_code, :error_code, :message, - :avs_result, :cvc_result, :params, :fraud_review + :success, + :id, + :token, + :status_code, + :gateway_code, + :reason, + :message, + :avs_result, + :cvc_result, + :raw, + :fraud_review ] + @typedoc """ + The standard Response from `Gringotts`. + + | Field | Type | Description | + |----------------|-------------------|---------------------------------------| + | `success` | `boolean` | Indicates the status of the\ + transaction. | + | `id` | `String.t` | Gateway supplied identifier of the\ + transaction. | + | `token` | `String.t` | Gateway supplied `token`. _This is\ + different from `Response.id`_. | + | `status_code` | `non_neg_integer` | `HTTP` response code. | + | `gateway_code` | `String.t` | Gateway's response code "as-is". | + | `message` | `String.t` | String describing the response status.| + | `avs_result` | `map` | Address Verification Result.\ + Schema: `%{street: String.t,\ + zip_code: String.t}` | + | `cvc_result` | `String.t` | Result of the [CVC][cvc] validation. | + | `reason` | `String.t` | Explain the `reason` of error, in\ + case of error. `nil` otherwise. | + | `raw` | `String.t` | Raw response from the gateway. | + | `fraud_review` | `term` | Gateway's risk assessment of the\ + transaction. | + + ## Notes + + 1. It is not guaranteed that all fields will be populated for all calls, and + some gateways might insert non-standard fields. Please refer the Gateways' + docs for that information. + + 2. `success` is deprecated in `v1.1.0` and will be removed in `v1.2.0`. + + 3. For some actions the Gateway returns an additional token, say as reponse to + a customer tokenization/registration. In such cases the `id` is not + useable because it refers to the transaction, the `token` is. + + > On the other hand for authorizations or captures, there's no `token`. + + 4. The schema of `fraud_review` is Gateway specific. + + [cvc]: https://en.wikipedia.org/wiki/Card_security_code + """ + @type t :: %__MODULE__{ + success: boolean, + id: String.t(), + token: String.t(), + status_code: non_neg_integer, + gateway_code: String.t(), + reason: String.t(), + message: String.t(), + avs_result: %{street: String.t(), zip_code: String.t()}, + cvc_result: String.t(), + raw: String.t(), + fraud_review: term + } + def success(opts \\ []) do new(true, opts) end diff --git a/lib/gringotts/worker.ex b/lib/gringotts/worker.ex deleted file mode 100644 index 786ad6ed..00000000 --- a/lib/gringotts/worker.ex +++ /dev/null @@ -1,89 +0,0 @@ -defmodule Gringotts.Worker do - @moduledoc ~S""" - A central supervised worker handling all the calls for different gateways - - It's main task is to re-route the requests to the respective gateway methods. - - State for this worker currently is:- - * `gateways`:- a list of all the gateways configured in the application. - * `all_configs`:- All the configurations for all the gateways that are configured. - """ - use GenServer - - def start_link(gateways, all_config, opts \\ []) do - GenServer.start_link(__MODULE__, [gateways, all_config], opts) - end - - def init([gateways, all_config]) do - {:ok, %{configs: all_config, gateways: gateways}} - end - - @doc """ - Handles call for `authorize` method - """ - def handle_call({:authorize, gateway, amount, card, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.authorize(amount, card, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for `purchase` method - """ - def handle_call({:purchase, gateway, amount, card, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.purchase(amount, card, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for `capture` method - """ - def handle_call({:capture, gateway, id, amount, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.capture(id, amount, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for `void` method - """ - def handle_call({:void, gateway, id, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.void(id, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for 'refund' method - """ - def handle_call({:refund, gateway, amount, id, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.refund(amount, id, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for `store` method - """ - def handle_call({:store, gateway, card, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.store(card, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for 'unstore' method - """ - def handle_call({:unstore, gateway, customer_id, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.unstore(customer_id, [{:config, config} | opts]) - {:reply, response, state} - end - - defp set_gateway_and_config(request_gateway) do - global_config = Application.get_env(:gringotts, :global_config) || [mode: :test] - gateway_config = Application.get_env(:gringotts, request_gateway) - {request_gateway, Keyword.merge(global_config, gateway_config)} - end -end diff --git a/lib/mix/new.ex b/lib/mix/new.ex index c37fd7d0..7b4fcd2e 100644 --- a/lib/mix/new.ex +++ b/lib/mix/new.ex @@ -6,10 +6,12 @@ defmodule Mix.Tasks.Gringotts.New do @moduledoc """ Generates a barebones implementation for a gateway. - It expects the (brand) name of the gateway as argument. This will not - necessarily be the module name, but we recommend the name be capitalized. + It expects the (brand) name of the gateway as argument and we recommend that + it be capitalized. *This will not necessarily be the module name*. - mix gringotts.new NAME [-m, --module MODULE] [--url URL] + ``` + mix gringotts.new NAME [-m, --module MODULE] [-f, --file FILENAME] [--url URL] + ``` A barebones implementation of the gateway will be created along with skeleton mock and integration tests in `lib/gringotts/gateways/`. The command will @@ -22,6 +24,7 @@ defmodule Mix.Tasks.Gringotts.New do > prompts. * `-m` `--module` - The module name for the Gateway. + * `-f` `--file` - The filename. * `--url` - The homepage of the gateway. ## Examples @@ -30,10 +33,10 @@ defmodule Mix.Tasks.Gringotts.New do The prompts for this will be: ``` - MODULE = `Foobar` - URL = `https://www.foobar.com` + MODULE = "Foobar" + URL = "https://www.foobar.com" + FILENAME = "foo_bar.ex" ``` - and the filename will be `foo_bar.ex` """ use Mix.Task @@ -48,17 +51,23 @@ Comma separated list of required configuration keys: {key_list, [name], []} = OptionParser.parse( args, - switches: [module: :string, url: :string], - aliases: [m: :module] + switches: [module: :string, url: :string, file: :string], + aliases: [m: :module, f: :file] ) Mix.Shell.IO.info("Generating barebones implementation for #{name}.") Mix.Shell.IO.info("Hit enter to select the suggestion.") + module_suggestion = + name |> String.split() |> Enum.map(&String.capitalize(&1)) |> Enum.join("") + module_name = case Keyword.fetch(key_list, :module) do - :error -> prompt_with_suggestion("\nModule name", String.capitalize(name)) - {:ok, mod_name} -> mod_name + :error -> + prompt_with_suggestion("\nModule name", module_suggestion) + + {:ok, mod_name} -> + mod_name end url = @@ -66,18 +75,28 @@ Comma separated list of required configuration keys: :error -> prompt_with_suggestion( "\nHomepage URL", - "https://www.#{String.Casing.downcase(name)}.com" + "https://www.#{String.downcase(module_suggestion)}.com" ) {:ok, url} -> url end - file_name = prompt_with_suggestion("\nFilename", Macro.underscore(name)) + file_name = + case Keyword.fetch(key_list, :file) do + :error -> + prompt_with_suggestion("\nFilename", Macro.underscore(module_name) <> ".ex") + + {:ok, filename} -> + filename + end + + file_base_name = String.slice(file_name, 0..-4) required_keys = case Mix.Shell.IO.prompt(@long_msg) |> String.trim() do - "" -> [] + "" -> + [] keys -> String.split(keys, ",") |> Enum.map(&String.trim(&1)) |> Enum.map(&String.to_atom(&1)) @@ -87,10 +106,12 @@ Comma separated list of required configuration keys: gateway: name, gateway_module: module_name, gateway_underscore: file_name, + # The key :gateway_filename is not used in any template as of now. + gateway_filename: "#{file_name}", required_config_keys: required_keys, gateway_url: url, - mock_test_filename: file_name <> "_test", - mock_response_filename: file_name <> "_mock" + mock_test_filename: "#{file_base_name}_test.exs", + mock_response_filename: "#{file_base_name}_mock.exs" ] if Mix.Shell.IO.yes?( @@ -101,12 +122,12 @@ Comma separated list of required configuration keys: mock_response = EEx.eval_file("templates/mock_response.eex", bindings) integration = EEx.eval_file("templates/integration.eex", bindings) - create_file("lib/gringotts/gateways/#{bindings[:gateway_underscore]}.ex", gateway) - create_file("test/integration/gateways/#{bindings[:mock_test_filename]}.exs", integration) + create_file("lib/gringotts/gateways/#{bindings[:gateway_filename]}", gateway) + create_file("test/integration/gateways/#{bindings[:mock_test_filename]}", integration) if Mix.Shell.IO.yes?("\nAlso create empty mock test suite?\n>") do - create_file("test/gateways/#{bindings[:mock_test_filename]}.exs", mock) - create_file("test/mocks/#{bindings[:mock_response_filename]}.exs", mock_response) + create_file("test/gateways/#{bindings[:mock_test_filename]}", mock) + create_file("test/mocks/#{bindings[:mock_response_filename]}", mock_response) end else Mix.Shell.IO.info("Doing nothing, bye!") diff --git a/mix.exs b/mix.exs index 7947413c..589f379c 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Gringotts.Mixfile do def project do [ app: :gringotts, - version: "1.0.2", + version: "1.1.0", description: description(), package: [ contributors: ["Aviabird Technologies"], @@ -16,15 +16,20 @@ defmodule Gringotts.Mixfile do test_coverage: [ tool: ExCoveralls ], + elixirc_paths: elixirc_paths(Mix.env()), preferred_cli_env: [ - "coveralls": :test, + coveralls: :test, "coveralls.detail": :test, - "coveralls.post": :test, + "coveralls.json": :test, "coveralls.html": :test, - "coveralls.travis": :test + vcr: :test, + "vcr.delete": :test, + "vcr.check": :test, + "vcr.show": :test ], deps: deps(), - docs: docs()] + docs: docs() + ] end # Configuration for the OTP application @@ -32,11 +37,14 @@ defmodule Gringotts.Mixfile do # Type `mix help compile.app` for more information def application do [ - applications: [:httpoison, :hackney, :elixir_xml_to_map, :timex], - mod: {Gringotts.Application, []} + applications: [:httpoison, :hackney, :elixir_xml_to_map, :timex] ] end + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/mocks"] + defp elixirc_paths(_), do: ["lib"] + # Dependencies can be hex.pm packages: # # {:mydep, "~> 0.3.0"} @@ -50,7 +58,7 @@ defmodule Gringotts.Mixfile do [ {:poison, "~> 3.1.0"}, {:httpoison, "~> 0.13"}, - {:xml_builder, "~> 0.1.1"}, + {:xml_builder, "~> 2.1"}, {:elixir_xml_to_map, "~> 0.1"}, # Money related @@ -59,16 +67,17 @@ defmodule Gringotts.Mixfile do {:ex_money, "~> 1.1.0", only: [:dev, :test], optional: true}, # docs and tests - {:ex_doc, "~> 0.16", only: :dev, runtime: false}, + {:ex_doc, "~> 0.18", only: :dev, runtime: false}, {:mock, "~> 0.3.0", only: :test}, {:bypass, "~> 0.8", only: :test}, - {:excoveralls, "~> 0.7", only: :test}, + {:excoveralls, "~> 0.8", only: :test}, # various analyses tools {:credo, "~> 0.3", only: [:dev, :test]}, {:inch_ex, "~> 0.5", only: :docs}, {:dialyxir, "~> 0.3", only: :dev}, - {:timex, "~> 3.1"} + {:timex, "~> 3.2"}, + {:exvcr, "~> 0.10", only: :test} ] end @@ -90,8 +99,8 @@ defmodule Gringotts.Mixfile do end defp groups_for_modules do - [ - "Gateways": ~r/^Gringotts.Gateways.?/, - ] + [ + Gateways: ~r/^Gringotts.Gateways.?/ + ] end end diff --git a/mix.lock b/mix.lock index df5aabfe..83a82360 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,9 @@ -%{"abnf2": {:hex, :abnf2, "0.1.2", "6f8792b8ac3288dba5fc889c2bceae9fe78f74e1a7b36bea9726ffaa9d7bef95", [:mix], [], "hexpm"}, +%{ + "abnf2": {:hex, :abnf2, "0.1.2", "6f8792b8ac3288dba5fc889c2bceae9fe78f74e1a7b36bea9726ffaa9d7bef95", [:mix], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "bypass": {:hex, :bypass, "0.8.1", "16d409e05530ece4a72fabcf021a3e5c7e15dcc77f911423196a0c551f2a15ca", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, "credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, @@ -10,13 +12,16 @@ "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"}, "elixir_xml_to_map": {:hex, :elixir_xml_to_map, "0.1.1", "57e924cd11731947bfd245ce57d0b8dd8b7168bf8edb20cd156a2982ca96fdfa", [:mix], [{:erlsom, "~>1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm"}, "erlsom": {:hex, :erlsom, "1.4.1", "53dbacf35adfea6f0714fd0e4a7b0720d495e88c5e24e12c5dc88c7b62bd3e49", [:rebar3], [], "hexpm"}, - "ex_cldr": {:hex, :ex_cldr, "1.1.0", "26f4a206307770b70139214ab820c5ed1f6241eb3394dd0db216ff95bf7e213a", [:mix], [{:abnf2, "~> 0.1", [hex: :abnf2, repo: "hexpm", optional: false]}, {:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, - "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.1.0", "75904f202ca602eca5f3af572d56ed3d4a51543fecd08c9ab626ae2d876f44da", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, - "ex_money": {:hex, :ex_money, "1.1.2", "4336192f1ac263900dfb4f63c1f71bc36a7cdee5d900e81937d3213be3360f9f", [: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"}, - "excoveralls": {:hex, :excoveralls, "0.8.0", "99d2691d3edf8612f128be3f9869c4d44b91c67cec92186ce49470ae7a7404cf", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "ex_cldr": {:hex, :ex_cldr, "1.4.4", "654966e8724d607e5cf9ecd5509ffcf66868b17e479bbd22ab2e9123595f9103", [:mix], [{:abnf2, "~> 0.1", [hex: :abnf2, repo: "hexpm", optional: false]}, {:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, + "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"}, - "hackney": {:hex, :hackney, "1.10.1", "c38d0ca52ea80254936a32c45bb7eb414e7a96a521b4ce76d00a69753b157f21", [: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"}, + "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"}, "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, @@ -26,9 +31,12 @@ "mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, - "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, + "plug": {:hex, :plug, "1.5.0", "224b25b4039bedc1eac149fb52ed456770b9678bbf0349cdd810460e1e09195b", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, + "timex": {:hex, :timex, "3.2.1", "639975eac45c4c08c2dbf7fc53033c313ff1f94fad9282af03619a3826493612", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "tzdata": {:hex, :tzdata, "0.5.16", "13424d3afc76c68ff607f2df966c0ab4f3258859bbe3c979c9ed1606135e7352", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, - "xml_builder": {:hex, :xml_builder, "0.1.2", "b48ab9ed0a24f43a6061e0c21deda88b966a2121af5c445d4fc550dd822e23dc", [:mix], [], "hexpm"}} + "xml_builder": {:hex, :xml_builder, "2.1.0", "c249d5339427c13cae11e9d9d0e8b40d25d228b9ecc54029f24017385e60280b", [:mix], [], "hexpm"}, +} diff --git a/templates/gateway.eex b/templates/gateway.eex index b68e0a31..48de7269 100644 --- a/templates/gateway.eex +++ b/templates/gateway.eex @@ -50,7 +50,6 @@ defmodule Gringotts.Gateways.<%= gateway_module %> do > something like this: > > config :gringotts, Gringotts.Gateways.<%= gateway_module %>, - > adapter: Gringotts.Gateways.<%= gateway_module %><%= if required_config_keys != [] do %>,<% end %> <%= for key <- required_config_keys do %>> <%= "#{key}" %>: "your_secret_<%= "#{key}" %>" <% end %> @@ -66,7 +65,7 @@ defmodule Gringotts.Gateways.<%= gateway_module %> do ## Following the examples - 1. First, set up a sample application and configure it to work with MONEI. + 1. First, set up a sample application and configure it to work with <%= gateway %>. - 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. @@ -259,7 +258,6 @@ defmodule Gringotts.Gateways.<%= gateway_module %> do # For consistency with other gateway implementations, make your (final) # network request in here, and parse it using another private method called # `respond`. - @spec commit(_) :: {:ok | :error, Response} defp commit(_) do # resp = HTTPoison.request(args, ...) # respond(resp, ...) @@ -267,7 +265,6 @@ defmodule Gringotts.Gateways.<%= gateway_module %> do # Parses <%= gateway %>'s response and returns a `Gringotts.Response` struct # in a `:ok`, `:error` tuple. - @spec respond(term) :: {:ok | :error, Response} defp respond(<%= gateway_underscore %>_response) defp respond({:ok, %{status_code: 200, body: body}}), do: "something" defp respond({:ok, %{status_code: status_code, body: body}}), do: "something" diff --git a/templates/integration.eex b/templates/integration.eex index f57b4b86..46e5b063 100644 --- a/templates/integration.eex +++ b/templates/integration.eex @@ -1,21 +1,24 @@ -defmodule Gringotts.Integration.Gateways.<%= gateway_module <> "Test"%> do - # Integration tests for the <%= gateway_module%> +defmodule Gringotts.Integration.Gateways.<%= gateway_module <> "Test" %> do + # Integration tests for the <%= gateway_module %> + # + # Note that your tests SHOULD NOT directly call the <%= gateway_module %>, but + # all calls must be via Gringotts' public API as defined in `lib`gringotts.ex` - use ExUnit.Case, async: false + use ExUnit.Case, async: true alias Gringotts.Gateways.<%= gateway_module%> @moduletag :integration setup_all do Application.put_env(:gringotts, Gringotts.Gateways.<%= gateway_module%>, - [ - adapter: Gringotts.Gateways.<%= gateway_module%><%= if required_config_keys != [] do %>,<%= for key <- Enum.intersperse(required_config_keys, ",") do %><%= if key === "," do %><%= "#{key}" %><% else %> + [ <%= if required_config_keys == [] do %># some_key: "some_secret_key"<% else %><%= for key <- Enum.intersperse(required_config_keys, ",") do %><%= if key === "," do %><%= "#{key}" %><% else %> <%= "#{key}" %>: "your_secret_<%= "#{key}" %>"<% end %><% end %><% end %> ] ) end # Group the test cases by public api + describe "purchase" do end diff --git a/templates/mock_response.eex b/templates/mock_response.eex index d4ad1f5b..b50bc667 100644 --- a/templates/mock_response.eex +++ b/templates/mock_response.eex @@ -1,6 +1,6 @@ defmodule Gringotts.Gateways.<%= gateway_module <> "Mock"%> do - # The module should include mock responses for test cases in <%= mock_test_filename <> ".exs"%>. + # The module should include mock responses for test cases in <%= mock_test_filename %>. # e.g. # def successful_purchase do # {:ok, %HTTPoison.Response{body: ~s[{data: "successful_purchase"}]} diff --git a/templates/test.eex b/templates/test.eex index 2fc65a79..f91b8d47 100644 --- a/templates/test.eex +++ b/templates/test.eex @@ -1,32 +1,64 @@ defmodule Gringotts.Gateways.<%= gateway_module <> "Test" %> do - # The file contains mocked tests for <%= gateway_module%> + # The file contains mock tests for <%= gateway_module%> - # We recommend using [mock][1] for this, you can place the mock responses from - # the Gateway in `test/mocks/<%= mock_response_filename%>.exs` file, which has also been - # generated for you. - # - # [1]: https://github.com/jjh42/mock + # We recommend using [`Bypass`][bypass] for this as it allows us to inspect + # the request body that is sent to the gateway. + + # After all, the only thing Gringotts does, is building HTTPoison requests + # from arguments. Thus by validating that a request has been properly + # constructed from the given arguments we accurately cover the behaviour of + # the module. + + # For inspiration and guidance to writing mock tests, please refer the mock + # tests of the MONEI gateway. Bypass has excellent documentation and there are + # numerous blog posts detailing good practices. - # Load the mock response file before running the tests. - Code.require_file "../mocks/<%= mock_response_filename <> ".exs"%>", __DIR__ + # [bypass]: https://github.com/pspdfkit-labs/bypass - use ExUnit.Case, async: false + use ExUnit.Case, async: true + + import Bypass + alias Gringotts.Gateways.<%= gateway_module%> - import Mock + alias Plug.{Conn, Parsers} - # Group the test cases by public api - describe "purchase" do + # A new Bypass instance is needed per test, so that we can do parallel tests + setup do + bypass = Bypass.open() + {:ok, bypass: bypass} end - describe "authorize" do - end + @doc """ + Parses the body of the `Plug.Conn.t`. - describe "capture" do - end + This is very useful when testing with `Bypass` to parse body of the request + built in the test. This makes it dead-simple to write asserts on the request + body! + + ## Example + ``` + test "something", %{bypass: bypass} do + Bypass.expect(bypass, "POST", "some/endpoint/", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "42.00" + assert params["currency"] == "USD" + Conn.resp(conn, 200, "the_mocked_reponse_body") + end) - describe "void" do + {:ok, response} = Gateway.authorize(@amount42, @card, @opts) + assert "something about the mocked response if necessary" end + ``` + """ + @spec parse(Plug.Conn.t(), keyword) :: Plug.Conn.t() + def parse(conn, opts \\ []) do + opts = Keyword.put_new(opts, :parsers, [Parsers.URLENCODED]) - describe "refund" do + # if your gateway returns JSON instead of URL Encoded responses, use the + # JSON parser + + # opts = Keyword.put_new(opts, :parsers, [Parsers.JSON]) + Parsers.call(conn, Parsers.init(opts)) end end diff --git a/test/gateways/authorize_net_test.exs b/test/gateways/authorize_net_test.exs index b4906339..66ae689d 100644 --- a/test/gateways/authorize_net_test.exs +++ b/test/gateways/authorize_net_test.exs @@ -1,5 +1,4 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do - Code.require_file("../mocks/authorize_net_mock.exs", __DIR__) use ExUnit.Case, async: false alias Gringotts.Gateways.AuthorizeNetMock, as: MockResponse alias Gringotts.CreditCard @@ -103,8 +102,8 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do customer_type: "individual" ] @opts_customer_profile_args [ - config: @auth, - customer_profile_id: "1814012002" + config: @auth, + customer_profile_id: "1814012002" ] @refund_id "60036752756" @@ -121,21 +120,19 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "purchase" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_purchase_response() end do - assert {:ok, response} = ANet.purchase(@amount, @card, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" + assert {:ok, _response} = ANet.purchase(@amount, @card, @opts) end end test "with bad card" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.bad_card_purchase_response() end do assert {:error, response} = ANet.purchase(@amount, @bad_card, @opts) - assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end end @@ -143,21 +140,19 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "authorize" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_authorize_response() end do - assert {:ok, response} = ANet.authorize(@amount, @card, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" + assert {:ok, _response} = ANet.authorize(@amount, @card, @opts) end end test "with bad card" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.bad_card_purchase_response() end do assert {:error, response} = ANet.authorize(@amount, @bad_card, @opts) - assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end end @@ -165,19 +160,16 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "capture" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_capture_response() end do - assert {:ok, response} = ANet.capture(@capture_id, @amount, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" + assert {:ok, _response} = ANet.capture(@capture_id, @amount, @opts) end end test "with bad transaction id" do - with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.bad_id_capture() end do + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.bad_id_capture() end do assert {:error, response} = ANet.capture(@capture_invalid_id, @amount, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -185,45 +177,38 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "refund" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_refund_response() end do - assert {:ok, response} = ANet.refund(@amount, @refund_id, @opts_refund) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" + assert {:ok, _response} = ANet.refund(@amount, @refund_id, @opts_refund) end end test "bad payment params" do - with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.bad_card_refund() end do + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.bad_card_refund() end do assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund_bad_payment) - assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end test "debit less than refund amount" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.debit_less_than_refund() end do + post: fn _url, _body, _headers -> MockResponse.debit_less_than_refund() end do assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end describe "void" do test "successful response with right params" do - with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.successful_void() end do - assert {:ok, response} = ANet.void(@void_id, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_void() end do + assert {:ok, _response} = ANet.void(@void_id, @opts) end end test "with bad transaction id" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.void_non_existent_id() end do + post: fn _url, _body, _headers -> MockResponse.void_non_existent_id() end do assert {:error, response} = ANet.void(@void_invalid_id, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -231,49 +216,44 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "store" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do - assert {:ok, response} = ANet.store(@card, @opts_store) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" + post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do + assert {:ok, _response} = ANet.store(@card, @opts_store) end end test "successful response without validation and customer type" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do - assert {:ok, response} = ANet.store(@card, @opts_store_without_validation) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" + post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do + assert {:ok, _response} = ANet.store(@card, @opts_store_without_validation) end end test "without any profile" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.store_without_profile_fields() end do assert {:error, response} = ANet.store(@card, @opts_store_no_profile) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == - "Error" + "Error" end end test "with customer profile id" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.customer_payment_profile_success_response() end do - assert {:ok, response} = ANet.store(@card, @opts_customer_profile) + assert {:ok, _response} = ANet.store(@card, @opts_customer_profile) - assert response.params["createCustomerPaymentProfileResponse"]["messages"]["resultCode"] == - "Ok" + "Ok" end end test "successful response without valiadtion mode and customer type" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do - assert {:ok, response} = ANet.store(@card, @opts_customer_profile_args) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" + post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do + assert {:ok, _response} = ANet.store(@card, @opts_customer_profile_args) end end end @@ -281,22 +261,21 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "unstore" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_unstore_response() end do - assert {:ok, response} = ANet.unstore(@unstore_id, @opts) - assert response.params["deleteCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" + assert {:ok, _response} = ANet.unstore(@unstore_id, @opts) end end end test "network error type non existent domain" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.netwok_error_non_existent_domain() end do assert {:error, response} = ANet.purchase(@amount, @card, @opts) - assert response.message == "HTTPoison says 'nxdomain'" + assert response.message == "HTTPoison says 'nxdomain' [ID: nil]" end end end diff --git a/test/gateways/bogus_test.exs b/test/gateways/bogus_test.exs index b7c10ebd..9235bcf8 100644 --- a/test/gateways/bogus_test.exs +++ b/test/gateways/bogus_test.exs @@ -4,50 +4,46 @@ defmodule Gringotts.Gateways.BogusTest do alias Gringotts.Response alias Gringotts.Gateways.Bogus, as: Gateway + @some_id "some_arbitrary_id" + @amount Money.new(5, :USD) + test "authorize" do - {:ok, %Response{authorization: authorization, success: success}} = - Gateway.authorize(10.95, :card, []) + {:ok, %Response{id: id, success: success}} = Gateway.authorize(@amount, :card, []) assert success - assert authorization != nil + assert id != nil end test "purchase" do - {:ok, %Response{authorization: authorization, success: success}} = - Gateway.purchase(10.95, :card, []) + {:ok, %Response{id: id, success: success}} = Gateway.purchase(@amount, :card, []) assert success - assert authorization != nil + assert id != nil end test "capture" do - {:ok, %Response{authorization: authorization, success: success}} = - Gateway.capture(1234, 5, []) + {:ok, %Response{id: id, success: success}} = Gateway.capture(@some_id, @amount, []) assert success - assert authorization != nil + assert id != nil end test "void" do - {:ok, %Response{authorization: authorization, success: success}} = - Gateway.void(1234, []) + {:ok, %Response{id: id, success: success}} = Gateway.void(@some_id, []) assert success - assert authorization != nil + assert id != nil end test "store" do - {:ok, %Response{success: success}} = - Gateway.store(%Gringotts.CreditCard{}, []) + {:ok, %Response{success: success}} = Gateway.store(%Gringotts.CreditCard{}, []) assert success end test "unstore with customer" do - {:ok, %Response{success: success}} = - Gateway.unstore(1234, []) + {:ok, %Response{success: success}} = Gateway.unstore(@some_id, []) assert success end - end diff --git a/test/gateways/cams_test.exs b/test/gateways/cams_test.exs index 3c20e545..43370865 100644 --- a/test/gateways/cams_test.exs +++ b/test/gateways/cams_test.exs @@ -1,5 +1,4 @@ defmodule Gringotts.Gateways.CamsTest do - Code.require_file("../mocks/cams_mock.exs", __DIR__) use ExUnit.Case, async: false alias Gringotts.{ @@ -43,9 +42,10 @@ defmodule Gringotts.Gateways.CamsTest do } @auth %{username: "some_secret_user_name", password: "some_secret_password"} @options [ - order_id: 0001, + order_id: 1, billing_address: @address, - description: "Store Purchase" + description: "Store Purchase", + config: @auth ] @money Money.new(:USD, 100) @@ -53,43 +53,31 @@ defmodule Gringotts.Gateways.CamsTest do @money_less Money.new(:USD, 99) @bad_currency Money.new(:INR, 100) - @authorization "some_transaction_id" - @bad_authorization "some_fake_transaction_id" - - setup_all do - Application.put_env( - :gringotts, - Gateway, - adapter: Gateway, - username: "some_secret_user_name", - password: "some_secret_password" - ) - end + @id "some_transaction_id" + @bad_id "some_fake_transaction_id" describe "purchase" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_purchase() end do - {:ok, %Response{success: result}} = Gringotts.purchase(Gateway, @money, @card, @options) - assert result + assert {:ok, %Response{}} = Gateway.purchase(@money, @card, @options) end end test "with bad card" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.failed_purchase_with_bad_credit_card() end do - {:ok, %Response{message: result}} = - Gringotts.purchase(Gateway, @money, @bad_card, @options) + {:error, %Response{reason: reason}} = Gateway.purchase(@money, @bad_card, @options) - assert String.contains?(result, "Invalid Credit Card Number") + assert String.contains?(reason, "Invalid Credit Card Number") end end test "with invalid currency" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.with_invalid_currency() end do - {:ok, %Response{message: result}} = Gringotts.purchase(Gateway, @bad_currency, @card, @options) - assert String.contains?(result, "The cc payment type") + {:error, %Response{reason: reason}} = Gateway.purchase(@bad_currency, @card, @options) + assert String.contains?(reason, "The cc payment type") end end end @@ -98,68 +86,63 @@ defmodule Gringotts.Gateways.CamsTest do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_authorize() end do - {:ok, %Response{success: result}} = Gringotts.authorize(Gateway, @money, @card, @options) - assert result + assert {:ok, %Response{}} = Gateway.authorize(@money, @card, @options) end end test "with bad card" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.failed_authorized_with_bad_card() end do - {:ok, %Response{message: result}} = - Gringotts.authorize(Gateway, @money, @bad_card, @options) + {:error, %Response{reason: reason}} = Gateway.authorize(@money, @bad_card, @options) - assert String.contains?(result, "Invalid Credit Card Number") + assert String.contains?(reason, "Invalid Credit Card Number") end end end describe "capture" do test "with full amount" do - with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do - {:ok, %Response{success: result}} = - Gringotts.capture(Gateway, @money, @authorization, @options) - - assert result + with_mock HTTPoison, + post: fn _url, _body, _headers -> + MockResponse.successful_capture() + end do + assert {:ok, %Response{}} = Gateway.capture(@money, @id, @options) end end test "with partial amount" do - with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do - {:ok, %Response{success: result}} = - Gringotts.capture(Gateway, @money_less, @authorization, @options) - - assert result + with_mock HTTPoison, + post: fn _url, _body, _headers -> + MockResponse.successful_capture() + end do + assert {:ok, %Response{}} = Gateway.capture(@money_less, @id, @options) end end test "with invalid transaction_id" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.invalid_transaction_id() end do - {:ok, %Response{message: result}} = - Gringotts.capture(Gateway, @money, @bad_authorization, @options) + {:error, %Response{reason: reason}} = Gateway.capture(@money, @bad_id, @options) - assert String.contains?(result, "Transaction not found") + assert String.contains?(reason, "Transaction not found") end end - test "with more than authorization amount" do + test "with more than authorized amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.more_than_authorization_amount() end do - {:ok, %Response{message: result}} = - Gringotts.capture(Gateway, @money_more, @authorization, @options) + {:error, %Response{reason: reason}} = Gateway.capture(@money_more, @id, @options) - assert String.contains?(result, "exceeds the authorization amount") + assert String.contains?(reason, "exceeds the authorization amount") end end test "on already captured transaction" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.multiple_capture_on_same_transaction() end do - {:ok, %Response{message: result}} = - Gringotts.capture(Gateway, @money, @authorization, @options) + {:error, %Response{reason: reason}} = Gateway.capture(@money, @id, @options) - assert String.contains?(result, "A capture requires that") + assert String.contains?(reason, "A capture requires that") end end end @@ -167,20 +150,16 @@ defmodule Gringotts.Gateways.CamsTest do describe "refund" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_refund() end do - {:ok, %Response{success: result}} = - Gringotts.refund(Gateway, @money, @authorization, @options) - - assert result + assert {:ok, %Response{}} = Gateway.refund(@money, @id, @options) end end test "with more than purchased amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.more_than_purchase_amount() end do - {:ok, %Response{message: result}} = - Gringotts.refund(Gateway, @money_more, @authorization, @options) + {:error, %Response{reason: reason}} = Gateway.refund(@money_more, @id, @options) - assert String.contains?(result, "Refund amount may not exceed") + assert String.contains?(reason, "Refund amount may not exceed") end end end @@ -188,16 +167,16 @@ defmodule Gringotts.Gateways.CamsTest do describe "void" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_void() end do - {:ok, %Response{message: result}} = Gringotts.void(Gateway, @authorization, @options) - assert String.contains?(result, "Void Successful") + {:ok, %Response{message: message}} = Gateway.void(@id, @options) + assert String.contains?(message, "Void Successful") end end test "with invalid transaction_id" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.invalid_transaction_id() end do - {:ok, %Response{message: result}} = Gringotts.void(Gateway, @bad_authorization, @options) - assert String.contains?(result, "Transaction not found") + {:error, %Response{reason: reason}} = Gateway.void(@bad_id, @options) + assert String.contains?(reason, "Transaction not found") end end end @@ -206,8 +185,7 @@ defmodule Gringotts.Gateways.CamsTest do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.validate_creditcard() end do - {:ok, %Response{success: result}} = Gateway.validate(@card, @options ++ [config: @auth]) - assert result + assert {:ok, %Response{}} = Gateway.validate(@card, @options ++ [config: @auth]) end end end diff --git a/test/gateways/global_collect_test.exs b/test/gateways/global_collect_test.exs index 87b0d702..ea0f4742 100644 --- a/test/gateways/global_collect_test.exs +++ b/test/gateways/global_collect_test.exs @@ -1,9 +1,8 @@ defmodule Gringotts.Gateways.GlobalCollectTest do - - Code.require_file "../mocks/global_collect_mock.exs", __DIR__ use ExUnit.Case, async: false alias Gringotts.Gateways.GlobalCollectMock, as: MockResponse alias Gringotts.Gateways.GlobalCollect + alias Gringotts.{ CreditCard } @@ -14,7 +13,7 @@ defmodule Gringotts.Gateways.GlobalCollectTest do @bad_amount Money.new("50.3", :USD) - @shippingAddress %{ + @shipping_address %{ street: "Desertroad", houseNumber: "1", additionalInfo: "Suite II", @@ -31,7 +30,7 @@ defmodule Gringotts.Gateways.GlobalCollectTest do first_name: "John", last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } @invalid_card %CreditCard{ @@ -41,10 +40,10 @@ defmodule Gringotts.Gateways.GlobalCollectTest do first_name: "John", last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } - @billingAddress %{ + @billing_address %{ street: "Desertroad", houseNumber: "13", additionalInfo: "b", @@ -69,46 +68,52 @@ defmodule Gringotts.Gateways.GlobalCollectTest do @invalid_token 30 - @invalid_config [config: %{secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", api_key_id: "e5743abfc360ed12"}] + @invalid_config [ + config: %{ + secret_api_key: "some_secret_api_key", + api_key_id: "some_api_key_id" + } + ] @options [ - config: %{secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", api_key_id: "e5743abfc360ed12", merchant_id: "1226"}, + config: %{ + secret_api_key: "some_secret_api_key", + api_key_id: "some_api_key_id", + merchant_id: "some_merchant_id" + }, description: "Store Purchase 1437598192", merchantCustomerId: "234", customer_name: "John Doe", - dob: "19490917", company: "asma", + dob: "19490917", + company: "asma", email: "johndoe@gmail.com", phone: "7468474533", order_id: "2323", invoice: @invoice, - billingAddress: @billingAddress, - shippingAddress: @shippingAddress, - name: @name, skipAuthentication: "true" + billingAddress: @billing_address, + shippingAddress: @shipping_address, + name: @name, + skipAuthentication: "true" ] - describe "validation arguments check" do - test "with no merchant id passed in config" do - assert_raise ArgumentError, fn -> - GlobalCollect.validate_config(@invalid_config) - end - end - end - describe "purchase" do test "with valid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_purchase_with_valid_card end] do - {:ok, response} = GlobalCollect.purchase(@amount, @valid_card, @options) - assert response.status_code == 201 - assert response.success == true - assert response.raw["payment"]["statusOutput"]["isAuthorized"] == true + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_purchase_with_valid_card() + end do + {:ok, response} = GlobalCollect.purchase(@amount, @valid_card, @options) + assert response.status_code == 201 + assert response.success == true + assert response.raw["payment"]["statusOutput"]["isAuthorized"] == true end end - test "with invalid amount" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_purchase_with_invalid_amount end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_purchase_with_invalid_amount() + end do {:error, response} = GlobalCollect.purchase(@bad_amount, @valid_card, @options) assert response.status_code == 400 assert response.success == false @@ -120,7 +125,9 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "authorize" do test "with valid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_authorize_with_valid_card end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_authorize_with_valid_card() + end do {:ok, response} = GlobalCollect.authorize(@amount, @valid_card, @options) assert response.status_code == 201 assert response.success == true @@ -130,17 +137,23 @@ defmodule Gringotts.Gateways.GlobalCollectTest do test "with invalid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_authorize_with_invalid_card end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_authorize_with_invalid_card() + end do {:error, response} = GlobalCollect.authorize(@amount, @invalid_card, @options) assert response.status_code == 400 assert response.success == false - assert response.message == "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT" + + assert response.message == + "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT" end end test "with invalid amount" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_authorize_with_invalid_amount end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_authorize_with_invalid_amount() + end do {:error, response} = GlobalCollect.authorize(@bad_amount, @valid_card, @options) assert response.status_code == 400 assert response.success == false @@ -152,7 +165,7 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "refund" do test "with refund not enabled for the respective account" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_refund end] do + request: fn _method, _url, _body, _headers -> MockResponse.test_for_refund() end do {:error, response} = GlobalCollect.refund(@amount, @valid_token, @options) assert response.status_code == 400 assert response.success == false @@ -164,7 +177,9 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "capture" do test "with valid payment id" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_capture_with_valid_paymentid end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_capture_with_valid_paymentid() + end do {:ok, response} = GlobalCollect.capture(@valid_token, @amount, @options) assert response.status_code == 200 assert response.success == true @@ -173,20 +188,24 @@ defmodule Gringotts.Gateways.GlobalCollectTest do end test "with invalid payment id" do - with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_capture_with_invalid_paymentid end] do - {:error, response} = GlobalCollect.capture(@invalid_token, @amount, @options) - assert response.status_code == 404 - assert response.success == false - assert response.message == "UNKNOWN_PAYMENT_ID" - end + with_mock HTTPoison, + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_capture_with_invalid_paymentid() + end do + {:error, response} = GlobalCollect.capture(@invalid_token, @amount, @options) + assert response.status_code == 404 + assert response.success == false + assert response.message == "UNKNOWN_PAYMENT_ID" + end end end describe "void" do test "with valid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_void_with_valid_card end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_void_with_valid_card() + end do {:ok, response} = GlobalCollect.void(@valid_token, @options) assert response.status_code == 200 assert response.raw["payment"]["status"] == "CANCELLED" @@ -197,7 +216,7 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "network failure" do test "with authorization" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_network_failure end] do + request: fn _method, _url, _body, _headers -> MockResponse.test_for_network_failure() end do {:error, response} = GlobalCollect.authorize(@amount, @valid_card, @options) assert response.success == false assert response.reason == :network_fail? diff --git a/test/gateways/monei_test.exs b/test/gateways/monei_test.exs index 9fd72895..e641d91b 100644 --- a/test/gateways/monei_test.exs +++ b/test/gateways/monei_test.exs @@ -6,6 +6,7 @@ defmodule Gringotts.Gateways.MoneiTest do } alias Gringotts.Gateways.Monei, as: Gateway + alias Plug.{Conn, Parsers} @amount42 Money.new(42, :USD) @amount3 Money.new(3, :USD) @@ -39,7 +40,7 @@ defmodule Gringotts.Gateways.MoneiTest do birthDate: "1980-07-31", mobile: "+15252525252", email: "masterofdeath@ministryofmagic.gov", - ip: "1.1.1", + ip: "127.0.0.1", status: "NEW" } @merchant %{ @@ -96,7 +97,7 @@ defmodule Gringotts.Gateways.MoneiTest do "card":{ "bin":"420000", "last4Digits":"0000", - "holder":"Jo Doe", + "holder":"Harry Potter", "expiryMonth":"12", "expiryYear":"2099" } @@ -119,20 +120,28 @@ defmodule Gringotts.Gateways.MoneiTest do describe "core" do test "with unsupported currency.", %{auth: auth} do {:error, response} = Gateway.authorize(@bad_currency, @card, config: auth) - assert response.description == "Invalid currency" + assert response.reason == "Invalid currency" end test "when MONEI is down or unreachable.", %{bypass: bypass, auth: auth} do - Bypass.expect_once(bypass, fn conn -> - Plug.Conn.resp(conn, 200, @auth_success) - end) - Bypass.down(bypass) {:error, response} = Gateway.authorize(@amount42, @card, config: auth) assert response.reason == "network related failure" - Bypass.up(bypass) - {:ok, _} = Gateway.authorize(@amount42, @card, config: auth) + end + + test "that all auth info is picked.", %{bypass: bypass, auth: auth} do + Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["authentication.entityId"] == "some_secret_entity_id" + assert params["authentication.password"] == "some_secret_password" + assert params["authentication.userId"] == "some_secret_user_id" + Conn.resp(conn, 200, @auth_success) + end) + + {:ok, response} = Gateway.purchase(@amount42, @card, config: auth) + assert response.gateway_code == "000.100.110" end test "with all extra_params.", %{bypass: bypass, auth: auth} do @@ -142,32 +151,32 @@ defmodule Gringotts.Gateways.MoneiTest do ] Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> - conn_ = parse(conn) - assert conn_.body_params["createRegistration"] == "true" - assert conn_.body_params["customParameters"] == @extra_opts[:custom] - assert conn_.body_params["merchantInvoiceId"] == randoms[:invoice_id] - assert conn_.body_params["merchantTransactionId"] == randoms[:transaction_id] - assert conn_.body_params["transactionCategory"] == @extra_opts[:category] - assert conn_.body_params["customer.merchantCustomerId"] == @customer[:merchantCustomerId] - - assert conn_.body_params["shipping.customer.merchantCustomerId"] == - @customer[:merchantCustomerId] - - assert conn_.body_params["merchant.submerchantId"] == @merchant[:submerchantId] - assert conn_.body_params["billing.city"] == @billing[:city] - assert conn_.body_params["shipping.method"] == @shipping[:method] - Plug.Conn.resp(conn, 200, @register_success) + p_conn = parse(conn) + params = p_conn.body_params + assert params["createRegistration"] == "true" + assert params["customParameters"] == @extra_opts[:custom] + assert params["merchantInvoiceId"] == randoms[:invoice_id] + assert params["merchantTransactionId"] == randoms[:transaction_id] + assert params["transactionCategory"] == @extra_opts[:category] + assert params["customer.merchantCustomerId"] == @customer[:merchantCustomerId] + + assert params["shipping.customer.merchantCustomerId"] == @customer[:merchantCustomerId] + + assert params["merchant.submerchantId"] == @merchant[:submerchantId] + assert params["billing.city"] == @billing[:city] + assert params["shipping.method"] == @shipping[:method] + Conn.resp(conn, 200, @register_success) end) opts = randoms ++ @extra_opts ++ [config: auth] {:ok, response} = Gateway.purchase(@amount42, @card, opts) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" assert response.token == "8a82944a60e09c550160e92da144491e" end - test "when card has expired.", %{bypass: bypass, auth: auth} do + test "when we get non-json.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> - Plug.Conn.resp(conn, 400, "") + Conn.resp(conn, 400, "") end) {:error, _} = Gateway.authorize(@amount42, @bad_card, config: auth) @@ -177,33 +186,44 @@ defmodule Gringotts.Gateways.MoneiTest do describe "authorize" do test "when all is good.", %{bypass: bypass, auth: auth} do Bypass.expect(bypass, "POST", "/v1/payments", fn conn -> - Plug.Conn.resp(conn, 200, @auth_success) + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "42.00" + assert params["currency"] == "USD" + assert params["paymentType"] == "PA" + Conn.resp(conn, 200, @auth_success) end) {:ok, response} = Gateway.authorize(@amount42, @card, config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end end describe "purchase" do test "when all is good.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> - Plug.Conn.resp(conn, 200, @auth_success) + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "42.00" + assert params["currency"] == "USD" + assert params["paymentType"] == "DB" + Conn.resp(conn, 200, @auth_success) end) {:ok, response} = Gateway.purchase(@amount42, @card, config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end test "with createRegistration.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> - conn_ = parse(conn) - assert conn_.body_params["createRegistration"] == "true" - Plug.Conn.resp(conn, 200, @register_success) + p_conn = parse(conn) + params = p_conn.body_params + assert params["createRegistration"] == "true" + Conn.resp(conn, 200, @register_success) end) {:ok, response} = Gateway.purchase(@amount42, @card, register: true, config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" assert response.token == "8a82944a60e09c550160e92da144491e" end end @@ -211,12 +231,19 @@ defmodule Gringotts.Gateways.MoneiTest do describe "store" do test "when all is good.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/registrations", fn conn -> - Plug.Conn.resp(conn, 200, @store_success) + p_conn = parse(conn) + params = p_conn.body_params + assert params["card.cvv"] == "123" + assert params["card.expiryMonth"] == "12" + assert params["card.expiryYear"] == "2099" + assert params["card.holder"] == "Harry Potter" + assert params["card.number"] == "4200000000000000" + assert params["paymentBrand"] == "VISA" + Conn.resp(conn, 200, @store_success) end) {:ok, response} = Gateway.store(@card, config: auth) - assert response.code == "000.100.110" - assert response.raw["card"]["holder"] == "Jo Doe" + assert response.gateway_code == "000.100.110" end end @@ -227,14 +254,19 @@ defmodule Gringotts.Gateways.MoneiTest do "POST", "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> - Plug.Conn.resp(conn, 200, @auth_success) + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "42.00" + assert params["currency"] == "USD" + assert params["paymentType"] == "CP" + Conn.resp(conn, 200, @auth_success) end ) {:ok, response} = Gateway.capture("7214344242e11af79c0b9e7b4f3f6234", @amount42, config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end test "with createRegistration that is ignored", %{bypass: bypass, auth: auth} do @@ -243,9 +275,10 @@ defmodule Gringotts.Gateways.MoneiTest do "POST", "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> - conn_ = parse(conn) - assert :error == Map.fetch(conn_.body_params, "createRegistration") - Plug.Conn.resp(conn, 200, @auth_success) + p_conn = parse(conn) + params = p_conn.body_params + assert :error == Map.fetch(params, "createRegistration") + Conn.resp(conn, 200, @auth_success) end ) @@ -257,7 +290,7 @@ defmodule Gringotts.Gateways.MoneiTest do config: auth ) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end end @@ -268,13 +301,18 @@ defmodule Gringotts.Gateways.MoneiTest do "POST", "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> - Plug.Conn.resp(conn, 200, @auth_success) + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "3.00" + assert params["currency"] == "USD" + assert params["paymentType"] == "RF" + Conn.resp(conn, 200, @auth_success) end ) {:ok, response} = Gateway.refund(@amount3, "7214344242e11af79c0b9e7b4f3f6234", config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end end @@ -285,12 +323,17 @@ defmodule Gringotts.Gateways.MoneiTest do "DELETE", "/v1/registrations/7214344242e11af79c0b9e7b4f3f6234", fn conn -> - Plug.Conn.resp(conn, 200, "") + p_conn = parse(conn) + params = p_conn.query_params + assert params["authentication.entityId"] == "some_secret_entity_id" + assert params["authentication.password"] == "some_secret_password" + assert params["authentication.userId"] == "some_secret_user_id" + Conn.resp(conn, 200, "") end ) {:error, response} = Gateway.unstore("7214344242e11af79c0b9e7b4f3f6234", config: auth) - assert response.code == :undefined_response_from_monei + assert response.reason == "undefined response from monei" end end @@ -301,29 +344,23 @@ defmodule Gringotts.Gateways.MoneiTest do "POST", "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> - Plug.Conn.resp(conn, 200, @auth_success) + p_conn = parse(conn) + params = p_conn.body_params + assert :error == Map.fetch(params, :amount) + assert :error == Map.fetch(params, :currency) + assert params["paymentType"] == "RV" + Conn.resp(conn, 200, @auth_success) end ) {:ok, response} = Gateway.void("7214344242e11af79c0b9e7b4f3f6234", config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end end - @tag :skip - test "respond various scenarios, can't test a private function." do - json_200 = %HTTPoison.Response{body: @auth_success, status_code: 200} - json_not_200 = %HTTPoison.Response{body: @auth_success, status_code: 300} - html_200 = %HTTPoison.Response{body: ~s[\n], status_code: 200} - html_not_200 = %HTTPoison.Response{body: ~s[ + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["token"] == "tok_d26e611c47d64693a281e8411934" + Conn.resp(conn, 200, Mock.auth_success()) + end) + + {:ok, response} = Gateway.authorize(@amount_42, @valid_token, config: opts) + assert response.gateway_code == 20000 + end + + test "when paymill is down or unreachable", %{bypass: bypass, opts: opts} do + Bypass.down(bypass) + {:error, response} = Gateway.authorize(@amount_42, @valid_token, config: opts) + assert response.reason == "network related failure" + Bypass.up(bypass) + end + + test "when token is invalid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/preauthorizations", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["token"] == "tok_d26e611c47d64693a281e841193" + Conn.resp(conn, 400, Mock.auth_purchase_invalid_token()) + end) + + {:error, response} = Gateway.authorize(@amount_42, @invalid_token, config: opts) + assert response.status_code == 400 + end + end + + describe "capture" do + test "when preauthorization is valid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/transactions", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["preauthorization"] == "preauth_d654694c8116109af903" + Conn.resp(conn, 200, Mock.capture_success()) + end) + + {:ok, response} = Gateway.capture(@capture_preauth_id, @amount_42, config: opts) + assert response.gateway_code == 20000 + end + + test "when preauthorization not found", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/transactions", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["preauthorization"] == "preauth_d654694c8116109af903" + Conn.resp(conn, 200, Mock.bad_preauth()) + end) + + {:error, response} = Gateway.capture(@capture_preauth_id, @amount_42, config: opts) + assert response.status_code == 200 + assert response.reason == "Preauthorize not found" + end + + test "when preauthorization done before", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/transactions", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["preauthorization"] == "preauth_d654694c8116109af903" + Conn.resp(conn, 200, Mock.capture_preauth_done_before()) + end) + + {:error, response} = Gateway.capture(@capture_preauth_id, @amount_42, config: opts) + assert response.status_code == 200 + assert response.reason == "Preauthorization has already been used" + end + end + + describe "purchase" do + test "when token is valid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/transactions", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["token"] == "tok_d26e611c47d64693a281e841193" + Conn.resp(conn, 200, Mock.purchase_valid_token()) + end) + + {:ok, response} = Gateway.purchase(@amount_42, @invalid_token, config: opts) + assert response.gateway_code == 20000 + assert response.fraud_review == true + assert response.status_code == 200 + end + + test "when token is invalid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/transactions", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + assert params["currency"] == "EUR" + assert params["token"] == "tok_d26e611c47d64693a281e841193" + Conn.resp(conn, 200, Mock.auth_purchase_invalid_token()) + end) + + {:error, response} = Gateway.purchase(@amount_42, @invalid_token, config: opts) + assert response.reason["field"] == "token" + + assert response.reason["messages"]["regexNotMatch"] == + "'tok_d26e611c47d64693a281e841193' does not match against pattern '\/^[a-zA-Z0-9_]{32}$\/'" + + assert response.status_code == 200 + end + end + + describe "refund" do + test "when transaction is valid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/refunds/#{@transaction_id}", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + Conn.resp(conn, 200, Mock.refund_success()) + end) + + {:ok, response} = Gateway.refund(@amount_42, @transaction_id, config: opts) + assert response.gateway_code == 20000 + assert response.status_code == 200 + end + + test "when transaction is used again", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/refunds/#{@transaction_id}", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + Conn.resp(conn, 200, Mock.refund_again()) + end) + + {:error, response} = Gateway.refund(@amount_42, @transaction_id, config: opts) + assert response.reason == "Amount to high" + assert response.status_code == 200 + end + + test "when transaction not found", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "POST", "/refunds/#{@invalid_transaction_id}", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "4200" + Conn.resp(conn, 200, Mock.refund_bad_transaction()) + end) + + {:error, response} = Gateway.refund(@amount_42, @invalid_transaction_id, config: opts) + assert response.reason == "Transaction not found" + assert response.status_code == 200 + end + end + + describe "void" do + test "when preauthorization is valid", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "DELETE", "/preauthorizations/#{@void_id}", fn conn -> + Conn.resp(conn, 200, Mock.void_success()) + end) + + {:ok, response} = Gateway.void(@void_id, config: opts) + assert response.gateway_code == 50810 + end + + test "when preauthorization used before", %{bypass: bypass, opts: opts} do + Bypass.expect(bypass, "DELETE", "/preauthorizations/#{@void_id}", fn conn -> + Conn.resp(conn, 200, Mock.void_done_before()) + end) + + {:error, response} = Gateway.void(@void_id, config: opts) + assert response.reason == "Preauthorization was not found" + assert response.status_code == 200 + end + end + + def parse(conn, opts \\ []) do + opts = Keyword.put_new(opts, :parsers, [Parsers.URLENCODED]) + Parsers.call(conn, Parsers.init(opts)) + end +end diff --git a/test/gateways/stripe_test.exs b/test/gateways/stripe_test.exs deleted file mode 100644 index 73bc211a..00000000 --- a/test/gateways/stripe_test.exs +++ /dev/null @@ -1,58 +0,0 @@ -defmodule Gringotts.Gateways.StripeTest do - - use ExUnit.Case - - alias Gringotts.Gateways.Stripe - alias Gringotts.{ - CreditCard, - Address - } - - @card %CreditCard{ - first_name: "John", - last_name: "Smith", - number: "4242424242424242", - year: "2017", - month: "12", - verification_code: "123" - } - - @address %Address{ - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111" - } - - @required_opts [config: [api_key: "sk_test_vIX41hayC0BKrPWQerLuOMld"], currency: "usd"] - @optional_opts [address: @address] - - describe "authorize/3" do - # test "should authorize wth card and required opts attrs" do - # amount = 5 - # response = Stripe.authorize(amount, @card, @required_opts ++ @optional_opts) - - # assert Map.has_key?(response, "id") - # assert response["amount"] == 500 - # assert response["captured"] == false - # assert response["currency"] == "usd" - # end - - # test "should not authorize if card is not passed" do - # amount = 5 - # response = Stripe.authorize(amount, %{}, @required_opts ++ @optional_opts) - - # assert Map.has_key?(response, "error") - # end - - # test "should not authorize if required opts not present" do - # amount = 5 - # response = Stripe.authorize(amount, @card, @optional_opts) - - # assert Map.has_key?(response, "error") - # end - - end -end diff --git a/test/gateways/trexle_test.exs b/test/gateways/trexle_test.exs index f8a50562..df74ff7d 100644 --- a/test/gateways/trexle_test.exs +++ b/test/gateways/trexle_test.exs @@ -1,5 +1,4 @@ defmodule Gringotts.Gateways.TrexleTest do - Code.require_file("../mocks/trexle_mock.exs", __DIR__) use ExUnit.Case, async: false alias Gringotts.Gateways.TrexleMock, as: MockResponse alias Gringotts.Gateways.Trexle @@ -14,8 +13,8 @@ defmodule Gringotts.Gateways.TrexleTest do @valid_card %CreditCard{ first_name: "Harry", last_name: "Potter", - number: "4200000000000000", - year: 2099, + number: "4000056655665556", + year: 2068, month: 12, verification_code: "123", brand: "VISA" @@ -24,7 +23,7 @@ defmodule Gringotts.Gateways.TrexleTest do @invalid_card %CreditCard{ first_name: "Harry", last_name: "Potter", - number: "4200000000000000", + number: "4000056655665556", year: 2010, month: 12, verification_code: "123", @@ -46,10 +45,10 @@ defmodule Gringotts.Gateways.TrexleTest do # 50 US cents, trexle does not work with amount smaller than 50 cents. @bad_amount Money.new("0.49", :USD) - @valid_token "7214344252e11af79c0b9e7b4f3f6234" - @invalid_token "14a62fff80f24a25f775eeb33624bbb3" + @valid_token "some_valid_token" + @invalid_token "some_invalid_token" - @auth %{api_key: "7214344252e11af79c0b9e7b4f3f6234"} + @auth %{api_key: "some_api_key"} @opts [ config: @auth, email: "masterofdeath@ministryofmagic.gov", @@ -64,10 +63,7 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_purchase_with_valid_card() end do - {:ok, response} = Trexle.purchase(@amount, @valid_card, @opts) - assert response.status_code == 201 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == false + assert {:ok, response} = Trexle.purchase(@amount, @valid_card, @opts) end end @@ -76,10 +72,8 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_purchase_with_invalid_card() end do - {:error, response} = Trexle.purchase(@amount, @invalid_card, @opts) - assert response.status_code == 400 - assert response.success == false - assert response.message == "Your card's expiration year is invalid." + assert {:error, response} = Trexle.purchase(@amount, @invalid_card, @opts) + assert response.reason == "Your card's expiration year is invalid." end end @@ -88,10 +82,9 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_purchase_with_invalid_amount() end do - {:error, response} = Trexle.purchase(@bad_amount, @valid_card, @opts) + assert {:error, response} = Trexle.purchase(@bad_amount, @valid_card, @opts) assert response.status_code == 400 - assert response.success == false - assert response.message == "Amount must be at least 50 cents" + assert response.reason == "Amount must be at least 50 cents" end end end @@ -102,10 +95,8 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_authorize_with_valid_card() end do - {:ok, response} = Trexle.authorize(@amount, @valid_card, @opts) + assert {:ok, response} = Trexle.authorize(@amount, @valid_card, @opts) assert response.status_code == 201 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == false end end end @@ -116,10 +107,8 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_authorize_with_valid_card() end do - {:ok, response} = Trexle.refund(@amount, @valid_token, @opts) + assert {:ok, response} = Trexle.refund(@amount, @valid_token, @opts) assert response.status_code == 201 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == false end end end @@ -130,11 +119,9 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_capture_with_valid_chargetoken() end do - {:ok, response} = Trexle.capture(@valid_token, @amount, @opts) + assert {:ok, response} = Trexle.capture(@valid_token, @amount, @opts) + # Why 200 here?? It's 201 everywhere lese. Check trexle docs. assert response.status_code == 200 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == true - assert response.message == "Transaction approved" end end @@ -143,10 +130,9 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_capture_with_invalid_chargetoken() end do - {:error, response} = Trexle.capture(@invalid_token, @amount, @opts) + assert {:error, response} = Trexle.capture(@invalid_token, @amount, @opts) assert response.status_code == 400 - assert response.success == false - assert response.message == "invalid token" + assert response.reason == "invalid token" end end end @@ -157,7 +143,7 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_store_with_valid_card() end do - {:ok, response} = Trexle.store(@valid_card, @opts) + assert {:ok, response} = Trexle.store(@valid_card, @opts) assert response.status_code == 201 end end @@ -170,8 +156,9 @@ defmodule Gringotts.Gateways.TrexleTest do MockResponse.test_for_network_failure() end do {:error, response} = Trexle.authorize(@amount, @valid_card, @opts) - assert response.success == false - assert response.message == "HTTPoison says 'some_hackney_error'" + + assert response.message == + "HTTPoison says 'some_hackney_error' [ID: some_hackney_error_id]" end end end diff --git a/test/gateways/wire_card_test.exs b/test/gateways/wire_card_test.exs index d23d9090..61633735 100644 --- a/test/gateways/wire_card_test.exs +++ b/test/gateways/wire_card_test.exs @@ -3,19 +3,10 @@ defmodule Gringotts.Gateways.WireCardTest do import Mock - alias Gringotts.{ - CreditCard, - Address, - Response - } - alias Gringotts.Gateways.WireCard, as: Gateway - setup do # TEST_AUTHORIZATION_GUWID = 'C822580121385121429927' # TEST_PURCHASE_GUWID = 'C865402121385575982910' # TEST_CAPTURE_GUWID = 'C833707121385268439116' - - # credit_card = %CreditCard{name: "Longbob", number: "4200000000000000", cvc: "123", expiration: {2015, 11}} # config = %{credentails: {'user', 'pass'}, default_currency: "EUR"} :ok @@ -24,7 +15,4 @@ defmodule Gringotts.Gateways.WireCardTest do test "test_successful_authorization" do assert 1 + 1 == 2 end - - - end diff --git a/test/gringotts_test.exs b/test/gringotts_test.exs index d4634e14..01d01824 100644 --- a/test/gringotts_test.exs +++ b/test/gringotts_test.exs @@ -4,12 +4,11 @@ defmodule GringottsTest do import Gringotts @test_config [ - adapter: GringottsTest.FakeGateway, some_auth_info: :merchant_secret_key, other_secret: :sun_rises_in_the_east ] - @bad_config [adapter: GringottsTest.FakeGateway, some_auth_info: :merchant_secret_key] + @bad_config [some_auth_info: :merchant_secret_key] defmodule FakeGateway do use Gringotts.Adapter, required_config: [:some_auth_info, :other_secret] @@ -49,13 +48,11 @@ defmodule GringottsTest do end test "authorization" do - assert authorize(GringottsTest.FakeGateway, 100, :card, []) == - :authorization_response + assert authorize(GringottsTest.FakeGateway, 100, :card, []) == :authorization_response end test "purchase" do - assert purchase(GringottsTest.FakeGateway, 100, :card, []) == - :purchase_response + assert purchase(GringottsTest.FakeGateway, 100, :card, []) == :purchase_response end test "capture" do @@ -83,7 +80,7 @@ defmodule GringottsTest do assert_raise( ArgumentError, - "expected [:other_secret] to be set, got: [adapter: GringottsTest.FakeGateway, some_auth_info: :merchant_secret_key]\n", + "expected [:other_secret] to be set, got: [some_auth_info: :merchant_secret_key]\n", fn -> authorize(GringottsTest.FakeGateway, 100, :card, []) end ) end diff --git a/test/integration/gateways/checkout_test.exs b/test/integration/gateways/checkout_test.exs new file mode 100644 index 00000000..f6292b74 --- /dev/null +++ b/test/integration/gateways/checkout_test.exs @@ -0,0 +1,140 @@ +defmodule Gringotts.Integration.Gateways.CheckoutTest do + # Integration tests for the Checkout + use ExUnit.Case, async: false + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + alias Gringotts.Gateways.Checkout + + alias Gringotts.{ + CreditCard, + Address + } + + alias Gringotts.Gateways.Checkout, as: Gateway + + # @moduletag :integration + + @amount Money.new(420, :USD) + + @bad_card1 %CreditCard{ + first_name: "Harry", + last_name: "Potter", + number: "4100000000000001", + year: 2009, + month: 12, + verification_code: "123", + brand: "VISA" + } + + @good_card %CreditCard{ + number: "4543474002249996", + month: 06, + year: 2025, + first_name: "Harry", + last_name: " Potter", + verification_code: "956", + brand: "VISA" + } + + @add %Address{ + street1: "OBH", + street2: "AIT", + city: "PUNE", + region: "MH", + country: "IN", + postal_code: "411015", + phone: "8007810916" + } + + @opts [ + description: "hello", + email: "hi@hello.com", + ip_address: "1.1.1.1", + chargeMode: 1, + config: [ + secret_key: "sk_test_f3695cf1-4f36-485b-bba9-caa5b5acb028" + ], + address: @add + ] + + describe "authorize" do + test "[authorize] with good parameters" do + use_cassette "Checkout/authorize_with_valid_card" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + + test "[authorize] with bad CreditCard" do + use_cassette "Checkout/authorize_with_invalid_card" do + assert {:error, response} = Gateway.authorize(@amount, @bad_card1, @opts) + assert response.success == false + assert response.status_code == 400 + end + end + end + + describe "purchase" do + test "[purchase] with good parameters" do + use_cassette "Checkout/purchase_with_valid_card" do + assert {:ok, response} = Gateway.purchase(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + + test "[purchase] with bad CreditCard" do + use_cassette "Checkout/purchase_with_invalid_card" do + assert {:error, response} = Gateway.purchase(@amount, @bad_card1, @opts) + assert response.success == false + assert response.status_code == 400 + end + end + end + + describe "capture" do + test "[Capture]" do + use_cassette "Checkout/capture" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + payment_id = response.id + assert {:ok, response} = Gateway.capture(payment_id, @amount, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + end + + describe "Void" do + test "[Void]" do + use_cassette "Checkout/void" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + payment_id = response.id + assert {:ok, response} = Gateway.void(payment_id, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + end + + describe "Refund" do + test "[Refund]" do + use_cassette "Checkout/Refund" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @opts) + assert response.success == true + assert response.status_code == 200 + payment_id = response.id + assert {:ok, response} = Gateway.capture(payment_id, @amount, @opts) + assert response.success == true + assert response.status_code == 200 + payment_id = response.id + assert {:ok, response} = Gateway.refund(@amount, payment_id, @opts) + assert response.success == true + assert response.status_code == 200 + end + end + end +end diff --git a/test/integration/gateways/monei_test.exs b/test/integration/gateways/monei_test.exs index 6619a43f..59bc9b88 100644 --- a/test/integration/gateways/monei_test.exs +++ b/test/integration/gateways/monei_test.exs @@ -10,10 +10,11 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do @moduletag :integration @amount Money.new(42, :EUR) + @sub_amount Money.new(21, :EUR) @card %CreditCard{ - first_name: "Jo", - last_name: "Doe", + first_name: "Harry", + last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, @@ -29,7 +30,7 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do birthDate: "1980-07-31", mobile: "+15252525252", email: "masterofdeath@ministryofmagic.gov", - ip: "1.1.1", + ip: "127.0.0.1", status: "NEW" } @merchant %{ @@ -62,14 +63,20 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do custom: %{voldemort: "he who must not be named"} ] + @auth %{ + userId: "8a8294186003c900016010a285582e0a", + password: "hMkqf2qbWf", + entityId: "8a82941760036820016010a28a8337f6" + } + setup_all do Application.put_env( :gringotts, Gringotts.Gateways.Monei, adapter: Gringotts.Gateways.Monei, - userId: "8a8294186003c900016010a285582e0a", - password: "hMkqf2qbWf", - entityId: "8a82941760036820016010a28a8337f6" + userId: @auth[:userId], + password: @auth[:password], + entityId: @auth[:entityId] ) end @@ -79,55 +86,79 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do transaction_id: Base.encode16(:crypto.hash(:md5, :crypto.strong_rand_bytes(32))) ] - {:ok, opts: randoms ++ @extra_opts} + {:ok, opts: [config: @auth] ++ randoms ++ @extra_opts} end - test "authorize", %{opts: opts} do - case Gringotts.authorize(Gateway, @amount, @card, opts) do - {:ok, response} -> - assert response.code == "000.100.110" - - assert response.description == - "Request successfully processed in 'Merchant in Integrator Test Mode'" - - assert String.length(response.id) == 32 - + test "[authorize] without tokenisation", %{opts: opts} do + with {:ok, auth_result} <- Gateway.authorize(@amount, @card, opts), + {:ok, _capture_result} <- Gateway.capture(auth_result.id, @amount, opts) do + "yay!" + else {:error, _err} -> flunk() end end - @tag :skip - test "capture", %{opts: _opts} do - case Gringotts.capture(Gateway, "s", @amount) do - {:ok, response} -> - assert response.code == "000.100.110" - - assert response.description == - "Request successfully processed in 'Merchant in Integrator Test Mode'" - - assert String.length(response.id) == 32 + test "[authorize -> capture] with tokenisation", %{opts: opts} do + with {:ok, auth_result} <- Gateway.authorize(@amount, @card, opts ++ [register: true]), + {:ok, _registration_token} <- Map.fetch(auth_result, :token), + {:ok, _capture_result} <- Gateway.capture(auth_result.id, @amount, opts) do + "yay!" + else + {:error, _err} -> + flunk() + end + end + test "[authorize -> void]", %{opts: opts} do + with {:ok, auth_result} <- Gateway.authorize(@amount, @card, opts), + {:ok, _void_result} <- Gateway.void(auth_result.id, opts) do + "yay!" + else {:error, _err} -> flunk() end end - test "purchase", %{opts: opts} do - case Gringotts.purchase(Gateway, @amount, @card, opts) do - {:ok, response} -> - assert response.code == "000.100.110" + test "[purchase/capture -> void]", %{opts: opts} do + with {:ok, purchase_result} <- Gateway.purchase(@amount, @card, opts), + {:ok, _void_result} <- Gateway.void(purchase_result.id, opts) do + "yay!" + else + {:error, _err} -> + flunk() + end + end - assert response.description == - "Request successfully processed in 'Merchant in Integrator Test Mode'" + test "[purchase/capture -> refund] (partial)", %{opts: opts} do + with {:ok, purchase_result} <- Gateway.purchase(@amount, @card, opts), + {:ok, _refund_result} <- Gateway.refund(@sub_amount, purchase_result.id, opts) do + "yay!" + else + {:error, _err} -> + flunk() + end + end - assert String.length(response.id) == 32 + test "[store]", %{opts: opts} do + assert {:ok, _store_result} = Gateway.store(@card, opts) + end + @tag :skip + test "[store -> unstore]", %{opts: opts} do + with {:ok, store_result} <- Gateway.store(@card, opts), + {:ok, _unstore_result} <- Gateway.unstore(store_result.id, opts) do + "yay!" + else {:error, _err} -> flunk() end end + test "[purchase]", %{opts: opts} do + assert {:ok, _response} = Gateway.purchase(@amount, @card, opts) + end + test "Environment setup" do config = Application.get_env(:gringotts, Gringotts.Gateways.Monei) assert config[:adapter] == Gringotts.Gateways.Monei diff --git a/test/integration/gateways/paymill_test.exs b/test/integration/gateways/paymill_test.exs new file mode 100644 index 00000000..f7919ac9 --- /dev/null +++ b/test/integration/gateways/paymill_test.exs @@ -0,0 +1,85 @@ +defmodule Gringotts.Integration.Gateways.PaymillTest do + use ExUnit.Case, async: true + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + + alias Gringotts.Gateways.Paymill, as: Gateway + + @moduletag integration: true + + @amount Money.new(4200, :EUR) + @valid_token1 "tok_784c33eeb9a6adfc2bd3c21f95e6" + @valid_token2 "tok_9e429fb2dc44bcf94bcd4e6e6ec5" + @valid_token3 "tok_55b80f87f44f9328bee99360c4cc" + @valid_token4 "tok_7fb13046921783327aaf3f69668c" + @valid_token5 "tok_182291df812e8de23ee7cd849768" + + setup_all do + Application.put_env( + :gringotts, + Gateway, + private_key: "a1bf5c1751ded07471ef246a29709c72", + public_key: "61296669594ebbcc7794acafa9811c4d", + mode: :test + ) + + on_exit(fn -> + Application.delete_env(:gringotts, Gateway) + end) + end + + describe "authorize" do + test "with valid token and currency" do + use_cassette "paymill/authorize with valid token and currency" do + {:ok, response} = Gringotts.authorize(Gateway, @amount, @valid_token1) + assert response.gateway_code == 20000 + assert response.status_code == 200 + end + end + end + + describe "capture" do + test "with valid token currency" do + use_cassette "paymill/capture with valid token currency" do + {:ok, response} = Gringotts.authorize(Gateway, @amount, @valid_token2) + payment_id = response.id + {:ok, response_cap} = Gringotts.capture(Gateway, payment_id, @amount) + assert response_cap.gateway_code == 20000 + assert response_cap.status_code == 200 + end + end + end + + describe "purchase" do + test "with valid token currency" do + use_cassette "paymill purchase with valid token currency" do + {:ok, response} = Gringotts.purchase(Gateway, @amount, @valid_token3) + assert response.gateway_code == 20000 + assert response.status_code == 200 + end + end + end + + describe "refund" do + test "with valid token currency" do + use_cassette "paymill/refund with valid token currency" do + {:ok, response} = Gringotts.purchase(Gateway, @amount, @valid_token4) + trans_id = response.id + {:ok, response_ref} = Gringotts.refund(Gateway, @amount, trans_id) + assert response_ref.gateway_code == 20000 + assert response_ref.status_code == 200 + end + end + end + + describe "void" do + test "with valid token currency" do + use_cassette "paymill/void with valid token currency" do + {:ok, response} = Gringotts.authorize(Gateway, @amount, @valid_token5) + auth_id = response.id + {:ok, response_void} = Gringotts.void(Gateway, auth_id) + assert response_void.gateway_code == 50810 + assert response_void.status_code == 200 + end + end + end +end diff --git a/test/integration/gateways/stripe_test.exs b/test/integration/gateways/stripe_test.exs new file mode 100644 index 00000000..e4d3e070 --- /dev/null +++ b/test/integration/gateways/stripe_test.exs @@ -0,0 +1,45 @@ +defmodule Gringotts.Gateways.StripeTest do + use ExUnit.Case + + alias Gringotts.Gateways.Stripe + + alias Gringotts.{ + CreditCard, + Address + } + + @moduletag integration: true + + @amount Money.new(5, :USD) + @card %CreditCard{ + first_name: "John", + last_name: "Smith", + number: "4242424242424242", + # Can't be more than 50 years in the future, Haha. + year: "2068", + month: "12", + verification_code: "123" + } + + @address %Address{ + street1: "123 Main", + street2: "Suite 100", + city: "New York", + region: "NY", + country: "US", + postal_code: "11111" + } + + @required_opts [config: [secret_key: "sk_test_vIX41hayC0BKrPWQerLuOMld"]] + @optional_opts [address: @address] + + describe "authorize/3" do + test "with correct params" do + response = Stripe.authorize(@amount, @card, @required_opts ++ @optional_opts) + assert Map.has_key?(response, "id") + assert response["amount"] == 500 + assert response["captured"] == false + assert response["currency"] == "usd" + end + end +end diff --git a/test/integration/money.exs b/test/integration/money.exs index ca42febe..e4aaa331 100644 --- a/test/integration/money.exs +++ b/test/integration/money.exs @@ -4,7 +4,7 @@ defmodule Gringotts.Integration.Gateways.MoneyTest do alias Gringotts.Money, as: MoneyProtocol @moduletag :integration - + @ex_money Money.new(42, :EUR) @ex_money_long Money.new("42.126456", :EUR) @ex_money_bhd Money.new(42, :BHD) @@ -12,50 +12,50 @@ defmodule Gringotts.Integration.Gateways.MoneyTest do @any %{value: Decimal.new(42), currency: "EUR"} @any_long %{value: Decimal.new("42.126456"), currency: "EUR"} @any_bhd %{value: Decimal.new("42"), currency: "BHD"} - + describe "ex_money" do test "value is a Decimal.t" do - assert match? %Decimal{}, MoneyProtocol.value(@ex_money) + assert match?(%Decimal{}, MoneyProtocol.value(@ex_money)) end test "currency is an upcase String.t" do the_currency = MoneyProtocol.currency(@ex_money) - assert match? currency when is_binary(currency), the_currency + assert match?(currency when is_binary(currency), the_currency) assert the_currency == String.upcase(the_currency) end test "to_integer" do - assert match? {"EUR", 4200, -2}, MoneyProtocol.to_integer(@ex_money) - assert match? {"BHD", 42000, -3}, MoneyProtocol.to_integer(@ex_money_bhd) + assert match?({"EUR", 4200, -2}, MoneyProtocol.to_integer(@ex_money)) + assert match?({"BHD", 42_000, -3}, MoneyProtocol.to_integer(@ex_money_bhd)) end test "to_string" do - assert match? {"EUR", "42.00"}, MoneyProtocol.to_string(@ex_money) - assert match? {"EUR", "42.13"}, MoneyProtocol.to_string(@ex_money_long) - assert match? {"BHD", "42.000"}, MoneyProtocol.to_string(@ex_money_bhd) + assert match?({"EUR", "42.00"}, MoneyProtocol.to_string(@ex_money)) + assert match?({"EUR", "42.13"}, MoneyProtocol.to_string(@ex_money_long)) + assert match?({"BHD", "42.000"}, MoneyProtocol.to_string(@ex_money_bhd)) end end describe "Any" do test "value is a Decimal.t" do - assert match? %Decimal{}, MoneyProtocol.value(@any) + assert match?(%Decimal{}, MoneyProtocol.value(@any)) end test "currency is an upcase String.t" do the_currency = MoneyProtocol.currency(@any) - assert match? currency when is_binary(currency), the_currency + assert match?(currency when is_binary(currency), the_currency) assert the_currency == String.upcase(the_currency) end test "to_integer" do - assert match? {"EUR", 4200, -2}, MoneyProtocol.to_integer(@any) - assert match? {"BHD", 4200, -2}, MoneyProtocol.to_integer(@any_bhd) + assert match?({"EUR", 4200, -2}, MoneyProtocol.to_integer(@any)) + assert match?({"BHD", 4200, -2}, MoneyProtocol.to_integer(@any_bhd)) end test "to_string" do - assert match? {"EUR", "42.00"}, MoneyProtocol.to_string(@any) - assert match? {"EUR", "42.13"}, MoneyProtocol.to_string(@any_long) - assert match? {"BHD", "42.00"}, MoneyProtocol.to_string(@any_bhd) + assert match?({"EUR", "42.00"}, MoneyProtocol.to_string(@any)) + assert match?({"EUR", "42.13"}, MoneyProtocol.to_string(@any_long)) + assert match?({"BHD", "42.00"}, MoneyProtocol.to_string(@any_bhd)) end end end diff --git a/test/mocks/authorize_net_mock.ex b/test/mocks/authorize_net_mock.ex new file mode 100644 index 00000000..953a57f5 --- /dev/null +++ b/test/mocks/authorize_net_mock.ex @@ -0,0 +1,423 @@ +defmodule Gringotts.Gateways.AuthorizeNetMock do + @moduledoc false + # purchase mock response + def successful_purchase_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.1C7HPT1YP2600365530965D6782A03246EE3BAFABE8006E32DE970XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-13182173"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "908"}, + {"Date", "Thu, 21 Dec 2017 09:29:12 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + def bad_card_purchase_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-10066531"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "514"}, + {"Date", "Thu, 21 Dec 2017 09:35:45 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + def bad_amount_purchase_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard5A valid amount is required.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-13187900"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "867"}, + {"Date", "Thu, 21 Dec 2017 09:44:33 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + # authorize mock response + def successful_authorize_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.1K6Z0ABYP260036854582A4AD079E22A271D92662CF093CED7A5D0XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15778237"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "908"}, + {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + def bad_card_authorize_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12660528"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "514"}, + {"Date", "Mon, 25 Dec 2017 14:19:29 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + def bad_amount_authorize_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard290There is one or more missing or invalid required fields.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15779095"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "898"}, + {"Date", "Mon, 25 Dec 2017 14:22:02 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + # capture mock response + + def successful_capture_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.14OKD6YP6003685493160036854931348C4ECD0F764736B012C4655BFA68EF0XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15783402"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "899"}, + {"Date", "Mon, 25 Dec 2017 14:39:28 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + def bad_id_capture do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P0A5280E2A6AA1290D451A24286692D1B0033A valid referenced transaction ID is required.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15784805"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "843"}, + {"Date", "Mon, 25 Dec 2017 14:45:32 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + # refund mock response + def successful_refund_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.1P6003685566160036752756169F2381B172A5AA247A01757A3E520A0XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12678232"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "884"}, + {"Date", "Mon, 25 Dec 2017 15:22:19 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + def bad_card_refund do + {:ok, + %HTTPoison.Response{ + body: + ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15795999"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "511"}, + {"Date", "Mon, 25 Dec 2017 15:21:20 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + def debit_less_than_refund do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P060036752756A5280E2A6AA1290D451A24286692D1B00XXXX0015MasterCard55The sum of credits against the referenced transaction would exceed original debit amount.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12681460"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "952"}, + {"Date", "Mon, 25 Dec 2017 15:39:25 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + # void mock response + def successful_void do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.1ZJPVRXP6003685521760036855217F09A215511891DCEA91B6CC52B9F4E870XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12682366"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "899"}, + {"Date", "Mon, 25 Dec 2017 15:43:56 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + def void_non_existent_id do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P060036855219C7C56F020A2AE2660A87637CD00B4D5C016The transaction cannot be found.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15801470"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "861"}, + {"Date", "Mon, 25 Dec 2017 15:49:38 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + # store mock response + + def successful_store_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{OkI00001Successful.18139914901808649724}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15829721"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "577"}, + {"Date", "Mon, 25 Dec 2017 17:08:12 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + def store_without_profile_fields do + {:ok, + %HTTPoison.Response{ + body: + ~s{ErrorE00041One or more fields in the profile must contain a value.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15831457"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "408"}, + {"Date", "Mon, 25 Dec 2017 17:12:30 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + # unstore mock response + def successful_unstore_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{OkI00001Successful.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15833786"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "361"}, + {"Date", "Mon, 25 Dec 2017 17:21:20 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + def customer_payment_profile_success_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{OkI00001Successful.181401200218086700051,1,1,(TESTMODE) This transaction has been approved.,000000,P,0,none,Test transaction for ValidateCustomerPaymentProfile.,1.00,CC,auth_only,none,,,,,,,,,,,email@example.com,,,,,,,,,0.00,0.00,0.00,FALSE,none,EA9FD49A9501D0415FE26BAEF9FD8B2C,,,,,,,,,,,,,XXXX0015,MasterCard,,,,,,,,,,,,,,,,,}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-17537805"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "828"}, + {"Date", "Thu, 28 Dec 2017 13:54:20 GMT"}, + {"Connection", "keep-alive"} + ], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200 + }} + end + + def netwok_error_non_existent_domain do + {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} + end +end diff --git a/test/mocks/authorize_net_mock.exs b/test/mocks/authorize_net_mock.exs deleted file mode 100644 index cb68df4a..00000000 --- a/test/mocks/authorize_net_mock.exs +++ /dev/null @@ -1,321 +0,0 @@ - defmodule Gringotts.Gateways.AuthorizeNetMock do - - # purchase mock response - def successful_purchase_response do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.1C7HPT1YP2600365530965D6782A03246EE3BAFABE8006E32DE970XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-13182173"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "908"}, {"Date", "Thu, 21 Dec 2017 09:29:12 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def bad_card_purchase_response do - {:ok, - %HTTPoison.Response{body: ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-10066531"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "514"}, {"Date", "Thu, 21 Dec 2017 09:35:45 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def bad_amount_purchase_response do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard5A valid amount is required.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-13187900"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "867"}, {"Date", "Thu, 21 Dec 2017 09:44:33 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - # authorize mock response - def successful_authorize_response do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.1K6Z0ABYP260036854582A4AD079E22A271D92662CF093CED7A5D0XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15778237"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "908"}, {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def bad_card_authorize_response do - {:ok, - %HTTPoison.Response{body: ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12660528"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "514"}, {"Date", "Mon, 25 Dec 2017 14:19:29 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def bad_amount_authorize_response do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard290There is one or more missing or invalid required fields.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15779095"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "898"}, {"Date", "Mon, 25 Dec 2017 14:22:02 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - # capture mock response - - def successful_capture_response do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.14OKD6YP6003685493160036854931348C4ECD0F764736B012C4655BFA68EF0XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15783402"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "899"}, {"Date", "Mon, 25 Dec 2017 14:39:28 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def bad_id_capture do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P0A5280E2A6AA1290D451A24286692D1B0033A valid referenced transaction ID is required.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15784805"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "843"}, {"Date", "Mon, 25 Dec 2017 14:45:32 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - # refund mock response - def successful_refund_response do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.1P6003685566160036752756169F2381B172A5AA247A01757A3E520A0XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12678232"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "884"}, {"Date", "Mon, 25 Dec 2017 15:22:19 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def bad_card_refund do - {:ok, - %HTTPoison.Response{body: ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15795999"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "511"}, {"Date", "Mon, 25 Dec 2017 15:21:20 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def debit_less_than_refund do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P060036752756A5280E2A6AA1290D451A24286692D1B00XXXX0015MasterCard55The sum of credits against the referenced transaction would exceed original debit amount.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12681460"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "952"}, {"Date", "Mon, 25 Dec 2017 15:39:25 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - # void mock response - def successful_void do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.1ZJPVRXP6003685521760036855217F09A215511891DCEA91B6CC52B9F4E870XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12682366"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "899"}, {"Date", "Mon, 25 Dec 2017 15:43:56 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def void_non_existent_id do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P060036855219C7C56F020A2AE2660A87637CD00B4D5C016The transaction cannot be found.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15801470"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "861"}, {"Date", "Mon, 25 Dec 2017 15:49:38 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - # store mock response - - def successful_store_response do - {:ok, - %HTTPoison.Response{body: ~s{OkI00001Successful.18139914901808649724}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15829721"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "577"}, {"Date", "Mon, 25 Dec 2017 17:08:12 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def store_without_profile_fields do - {:ok, - %HTTPoison.Response{body: ~s{ErrorE00041One or more fields in the profile must contain a value.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15831457"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "408"}, {"Date", "Mon, 25 Dec 2017 17:12:30 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - #unstore mock response - def successful_unstore_response do - {:ok, - %HTTPoison.Response{body: ~s{OkI00001Successful.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15833786"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "361"}, {"Date", "Mon, 25 Dec 2017 17:21:20 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def customer_payment_profile_success_response do - {:ok, - %HTTPoison.Response{body: ~s{OkI00001Successful.181401200218086700051,1,1,(TESTMODE) This transaction has been approved.,000000,P,0,none,Test transaction for ValidateCustomerPaymentProfile.,1.00,CC,auth_only,none,,,,,,,,,,,email@example.com,,,,,,,,,0.00,0.00,0.00,FALSE,none,EA9FD49A9501D0415FE26BAEF9FD8B2C,,,,,,,,,,,,,XXXX0015,MasterCard,,,,,,,,,,,,,,,,,}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-17537805"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "828"}, {"Date", "Thu, 28 Dec 2017 13:54:20 GMT"}, - {"Connection", "keep-alive"}], - request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def netwok_error_non_existent_domain do - {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} - end - end diff --git a/test/mocks/cams_mock.ex b/test/mocks/cams_mock.ex new file mode 100644 index 00000000..1698d43c --- /dev/null +++ b/test/mocks/cams_mock.ex @@ -0,0 +1,226 @@ +defmodule Gringotts.Gateways.CamsMock do + @moduledoc false + def successful_purchase do + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3916017714&avsresponse=N&cvvresponse=N&orderid=&type=sale&response_code=100", + headers: [ + {"Date", "Thu, 21 Dec 2017 12:45:16 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "137"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def failed_purchase_with_bad_credit_card do + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Invalid Credit Card Number REFID:3502947912&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=sale&response_code=300", + headers: [ + {"Date", "Thu, 21 Dec 2017 13:20:08 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "155"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def with_invalid_currency do + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=The cc payment type [Visa] and/or currency [INR] is not accepted REFID:3503238709&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", + headers: [ + {"Date", "Tue, 26 Dec 2017 10:37:42 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "193"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def successful_capture do + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=100", + headers: [ + {"Date", "Tue, 26 Dec 2017 12:16:55 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "138"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def successful_authorize do + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=N&cvvresponse=N&orderid=&type=auth&response_code=100", + headers: [ + {"Date", "Tue, 26 Dec 2017 12:16:11 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "137"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def invalid_transaction_id do + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Transaction not found REFID:3503243979&authcode=&transactionid=3921118690&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", + headers: [ + {"Date", "Tue, 26 Dec 2017 12:39:05 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "163"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def more_than_authorization_amount do + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=The specified amount of 1001 exceeds the authorization amount of 1000.00 REFID:3503244462&authcode=&transactionid=3921127126&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", + headers: [ + {"Date", "Tue, 26 Dec 2017 13:00:55 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "214"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def successful_refund do + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=&transactionid=3921158933&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=100", + headers: [ + {"Date", "Tue, 26 Dec 2017 14:00:08 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "131"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def more_than_purchase_amount do + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503249728&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", + headers: [ + {"Date", "Tue, 26 Dec 2017 14:05:31 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "183"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def successful_void do + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=Transaction Void Successful&authcode=123456&transactionid=3921178863&avsresponse=&cvvresponse=&orderid=&type=void&response_code=100", + headers: [ + {"Date", "Tue, 26 Dec 2017 14:26:05 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "155"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def failed_authorized_with_bad_card do + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Invalid Credit Card Number REFID:3503305883&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", + headers: [ + {"Date", "Wed, 27 Dec 2017 09:51:45 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "155"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def multiple_capture_on_same_transaction do + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=A capture requires that the existing transaction be an AUTH REFID:3503316182&authcode=&transactionid=3922433984&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", + headers: [ + {"Date", "Wed, 27 Dec 2017 13:47:12 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "201"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def refund_the_authorised_transaction do + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503316128&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", + headers: [ + {"Date", "Wed, 27 Dec 2017 13:45:19 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "183"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def validate_creditcard do + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=&authcode=&transactionid=3933708264&avsresponse=&cvvresponse=&orderid=&type=verify&response_code=100", + headers: [ + {"Date", "Thu, 04 Jan 2018 11:12:20 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "124"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end +end diff --git a/test/mocks/cams_mock.exs b/test/mocks/cams_mock.exs deleted file mode 100644 index e499b450..00000000 --- a/test/mocks/cams_mock.exs +++ /dev/null @@ -1,211 +0,0 @@ -defmodule Gringotts.Gateways.CamsMock do - def successful_purchase do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3916017714&avsresponse=N&cvvresponse=N&orderid=&type=sale&response_code=100", - headers: [ - {"Date", "Thu, 21 Dec 2017 12:45:16 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "137"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def failed_purchase_with_bad_credit_card do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Invalid Credit Card Number REFID:3502947912&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=sale&response_code=300", - headers: [ - {"Date", "Thu, 21 Dec 2017 13:20:08 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "155"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def with_invalid_currency do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=The cc payment type [Visa] and/or currency [INR] is not accepted REFID:3503238709&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", - headers: [ - {"Date", "Tue, 26 Dec 2017 10:37:42 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "193"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def successful_capture do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=100", - headers: [ - {"Date", "Tue, 26 Dec 2017 12:16:55 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "138"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def successful_authorize do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=N&cvvresponse=N&orderid=&type=auth&response_code=100", - headers: [ - {"Date", "Tue, 26 Dec 2017 12:16:11 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "137"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def invalid_transaction_id do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Transaction not found REFID:3503243979&authcode=&transactionid=3921118690&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", - headers: [ - {"Date", "Tue, 26 Dec 2017 12:39:05 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "163"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def more_than_authorization_amount do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=The specified amount of 1001 exceeds the authorization amount of 1000.00 REFID:3503244462&authcode=&transactionid=3921127126&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", - headers: [ - {"Date", "Tue, 26 Dec 2017 13:00:55 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "214"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def successful_refund do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=SUCCESS&authcode=&transactionid=3921158933&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=100", - headers: [ - {"Date", "Tue, 26 Dec 2017 14:00:08 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "131"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def more_than_purchase_amount do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503249728&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", - headers: [ - {"Date", "Tue, 26 Dec 2017 14:05:31 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "183"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def successful_void do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=Transaction Void Successful&authcode=123456&transactionid=3921178863&avsresponse=&cvvresponse=&orderid=&type=void&response_code=100", - headers: [ - {"Date", "Tue, 26 Dec 2017 14:26:05 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "155"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def failed_authorized_with_bad_card do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Invalid Credit Card Number REFID:3503305883&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", - headers: [ - {"Date", "Wed, 27 Dec 2017 09:51:45 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "155"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def multiple_capture_on_same_transaction do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=A capture requires that the existing transaction be an AUTH REFID:3503316182&authcode=&transactionid=3922433984&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", - headers: [ - {"Date", "Wed, 27 Dec 2017 13:47:12 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "201"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def refund_the_authorised_transaction do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503316128&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", - headers: [ - {"Date", "Wed, 27 Dec 2017 13:45:19 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "183"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end - - def validate_creditcard do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=&authcode=&transactionid=3933708264&avsresponse=&cvvresponse=&orderid=&type=verify&response_code=100", - headers: [ - {"Date", "Thu, 04 Jan 2018 11:12:20 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "124"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} - end -end diff --git a/test/mocks/global_collect_mock.ex b/test/mocks/global_collect_mock.ex new file mode 100644 index 00000000..22452930 --- /dev/null +++ b/test/mocks/global_collect_mock.ex @@ -0,0 +1,253 @@ +defmodule Gringotts.Gateways.GlobalCollectMock do + @moduledoc false + + def test_for_purchase_with_valid_card do + {:ok, + %HTTPoison.Response{ + body: + ~s/{"creationOutput":{"additionalReference":"00000012260000000074","externalReference": + "000000122600000000740000100001"},"payment":{"id":"000000122600000000740000100001", + "paymentOutput":{"amountOfMoney":{"amount":500,"currencyCode":"USD"},"references": + {"paymentReference":"0"},"paymentMethod":"card","cardPaymentMethodSpecificOutput": + {"paymentProductId":1,"authorisationCode":"OK1131","fraudResults":{"fraudServiceResult": + "no-advice","avsResult":"0","cvvResult":"0"},"card":{"cardNumber":"************7977", + "expiryDate":"1218"}}},"status":"PENDING_APPROVAL","statusOutput":{"isCancellable":true, + "statusCategory":"PENDING_MERCHANT","statusCode":600,"statusCodeChangeDateTime": + "20180118135349","isAuthorized":true,"isRefundable":false}}}/, + headers: [ + {"Date", "Thu, 18 Jan 2018 12:53:49 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"Location", + "https://api-sandbox.globalcollect.com:443/v1/1226/payments/000000122600000000740000100001"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 201 + }} + end + + def test_for_purchase_with_invalid_card do + {:ok, + %HTTPoison.Response{ + body: + ~s/{"errorId" : "363899bd-acfb-4452-bbb0-741c0df6b4b8","errors" : [ {"code" : "21000120", + "requestId" : "980825","propertyName" : "cardPaymentMethodSpecificInput.card.expiryDate", + "message" : "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT", + "httpStatusCode" : 400} ],"paymentResult" : {"creationOutput" : { " additionalReference" : "00000012260000000075", + "externalReference" : "000000122600000000750000100001"},"payment" : {"id" : "000000122600000000750000100001", + "paymentOutput" : {"amountOfMoney" : {"amount" : 500,"currencyCode" : "USD"},"references" : {"paymentReference" : "0"}, + "paymentMethod" : "card","cardPaymentMethodSpecificOutput" : {"paymentProductId" : 1}}, + "status" : "REJECTED","statusOutput" : {"errors" : [ {"code" : "21000120", + "requestId" : "546247","propertyName" : "cardPaymentMethodSpecificInput.card.expiryDate", + "message" : "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT", + "httpStatusCode" : 400} ],"isCancellable" : false,"statusCategory" : "UNSUCCESSFUL","statusCode" : 100, + "statusCodeChangeDateTime" : "20180118135651","isAuthorized" : false,"isRefundable" : false}}}}/, + headers: [ + {"Date", "Thu, 18 Jan 2018 12:56:51 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + }} + end + + def test_for_purchase_with_invalid_amount do + {:ok, + %HTTPoison.Response{ + body: + ~s/{ "errorId" : "8c34dc0b-776c-44e3-8cd4-b36222960153","errors" : [ {"code" : "1099","id" : + "INVALID_VALUE","category" : "CONNECT_PLATFORM_ERROR","message" : + "INVALID_VALUE: '50.3' is not a valid value for field 'amount'", + "httpStatusCode" : 400 } ]}/, + headers: [ + {"Date", "Wed, 24 Jan 2018 07:16:06 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + }} + end + + def test_for_authorize_with_valid_card do + {:ok, + %HTTPoison.Response{ + body: + ~s/{"creationOutput" : {"additionalReference" : "00000012260000000065","externalReference" : + "000000122600000000650000100001"},"payment" : {"id" : "000000122600000000650000100001", + "paymentOutput" :{"amountOfMoney" : {"amount" : 500,"currencyCode" : "USD"},"references" : + {"paymentReference" : "0" },"paymentMethod" : "card","cardPaymentMethodSpecificOutput" : + {"paymentProductId" : 1,"authorisationCode" : "OK1131","fraudResults" : + {"fraudServiceResult" : "no-advice","avsResult" : "0","cvvResult" : "0"},"card" : + {"cardNumber" : "************7977","expiryDate" : "1218"}}},"status" : "PENDING_APPROVAL", + "statusOutput" : {"isCancellable" : true,"statusCategory" : "PENDING_MERCHANT","statusCode" + : 600,"statusCodeChangeDateTime" : "20180118110419","isAuthorized" : true, + "isRefundable" : false}}}/, + headers: [ + {"Date", "Thu, 18 Jan 2018 10:04:19 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"Location", + "https://api-sandbox.globalcollect.com:443/v1/1226/payments/000000122600000000650000100001"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 201 + }} + end + + def test_for_authorize_with_invalid_card do + {:ok, + %HTTPoison.Response{ + body: + ~s/{"errorId" : "dcdf5c8d-e475-4fbc-ac57-76123c1640a2","errors" : [ {"code" : "21000120", + "requestId" : "978754","propertyName" : "cardPaymentMethodSpecificInput.card.expiryDate","message": + "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT", + "httpStatusCode" : 400} ],"paymentResult" : {"creationOutput" : + {"additionalReference" :"00000012260000000066","externalReference" : + "000000122600000000660000100001"},"payment" :{"id" : "000000122600000000660000100001", + "paymentOutput" : {"amountOfMoney" : {"amount" : 500,"currencyCode" : "USD"}, + "references" : {"paymentReference" : "0"},"paymentMethod" : "card", + "cardPaymentMethodSpecificOutput" : {"paymentProductId" : 1}},"status" : "REJECTED", + "statusOutput":{"errors" : [ {"code" : "21000120","requestId" : "978755","propertyName" : + "cardPaymentMethodSpecificInput.card.expiryDate","message" : + "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT", + "httpStatusCode" : 400} ],"isCancellable" : false,"statusCategory" : + "UNSUCCESSFUL","statusCode" : 100,"statusCodeChangeDateTime" : "20180118111508", + "isAuthorized" : false,"isRefundable" : false}}}}/, + headers: [ + {"Date", "Thu, 18 Jan 2018 10:15:08 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + }} + end + + def test_for_authorize_with_invalid_amount do + {:ok, + %HTTPoison.Response{ + body: + ~s/{"errorId" : "1dbef568-ed86-4c8d-a3c3-74ced258d5a2","errors" : [ {"code" : "1099","id" : + "INVALID_VALUE", "category" : "CONNECT_PLATFORM_ERROR","message" : + "INVALID_VALUE: '50.3' is not a valid value for field 'amount'","httpStatusCode" : 400} ]}/, + headers: [ + {"Date", "Tue, 23 Jan 2018 11:18:11 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + }} + end + + def test_for_refund do + {:ok, + %HTTPoison.Response{ + body: + ~s/{ "errorId" : "b6ba00d2-8f11-4822-8f32-c6d0a4d8793b", "errors" : [ {"code" : "300450", + "message" : "ORDER WITHOUT REFUNDABLE PAYMENTS", "httpStatusCode" : 400 } ]}/, + headers: [ + {"Date", "Wed, 24 Jan 2018 05:33:56 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: + "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/refund", + status_code: 400 + }} + end + + def test_for_capture_with_valid_paymentid do + {:ok, + %HTTPoison.Response{ + body: ~s/{ "payment" : {"id" : "000000122600000000650000100001", "paymentOutput" : { + "amountOfMoney" :{"amount" : 50,"currencyCode" : "USD"},"references" : {"paymentReference" + : "0"},"paymentMethod" : "card","cardPaymentMethodSpecificOutput" : {"paymentProductId" : + 1,"authorisationCode" : "OK1131","fraudResults" : {"fraudServiceResult" : "no-advice", + "avsResult" : "0","cvvResult" : "0"},"card" :{"cardNumber" : "************7977", + "expiryDate" : "1218"}}},"status" : "CAPTURE_REQUESTED","statusOutput" : + {"isCancellable" : true,"statusCategory" : "PENDING_CONNECT_OR_3RD_PARTY", + "statusCode" : 800,"statusCodeChangeDateTime" : "20180123140826","isAuthorized" : true, + "isRefundable" : false} }}/, + headers: [ + {"Date", "Tue, 23 Jan 2018 13:08:26 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: + "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000650000100001/approve", + status_code: 200 + }} + end + + def test_for_capture_with_invalid_paymentid do + {:ok, + %HTTPoison.Response{ + body: ~s/{ "errorId" : "ccb99804-0240-45b6-bb28-52aaae59d71b", "errors" : [ + {"code" : "1002","id" :"UNKNOWN_PAYMENT_ID","category" : "CONNECT_PLATFORM_ERROR", + "propertyName" : "paymentId","message": "UNKNOWN_PAYMENT_ID","httpStatusCode" :404}]}/, + headers: [ + {"Date", "Tue, 23 Jan 2018 12:25:59 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/30/approve", + status_code: 404 + }} + end + + def test_for_void_with_valid_card do + {:ok, + %HTTPoison.Response{ + body: + ~s/{ "payment" : {"id" : "000000122600000000870000100001","paymentOutput" : {"amountOfMoney" + :{"amount" : 50,"currencyCode" : "USD"},"references" : {"paymentReference" : "0"}, + "paymentMethod" : "card","cardPaymentMethodSpecificOutput" : {"paymentProductId" : 1, + "authorisationCode" : "OK1131","fraudResults" : {"fraudServiceResult" : "no-advice", + "avsResult" : "0","cvvResult" : "0"},"card" :{"cardNumber" : "************7977", + "expiryDate" : "1218"}}},"status" : "CANCELLED","statusOutput":{"isCancellable" : + false,"statusCategory" : "UNSUCCESSFUL","statusCode" : 99999, + "statusCodeChangeDateTime" : "20180124064204","isAuthorized" : false,"isRefundable" : + false}}}/, + headers: [ + {"Date", "Wed, 24 Jan 2018 05:42:04 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: + "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/cancel", + status_code: 200 + }} + end + + def test_for_network_failure do + {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} + end +end diff --git a/test/mocks/global_collect_mock.exs b/test/mocks/global_collect_mock.exs deleted file mode 100644 index 8bb9d553..00000000 --- a/test/mocks/global_collect_mock.exs +++ /dev/null @@ -1,182 +0,0 @@ -defmodule Gringotts.Gateways.GlobalCollectMock do - - def test_for_purchase_with_valid_card do - {:ok, - %HTTPoison.Response{ - body: "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000074\",\n \"externalReference\" : \"000000122600000000740000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000740000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118135349\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", - headers: [{"Date", "Thu, 18 Jan 2018 12:53:49 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"Location", - "https://api-sandbox.globalcollect.com:443/v1/1226/payments/000000122600000000740000100001"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 201 - } - } - end - - def test_for_purchase_with_invalid_card do - {:ok, - %HTTPoison.Response{ - body: "{\n \"errorId\" : \"363899bd-acfb-4452-bbb0-741c0df6b4b8\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"980825\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000075\",\n \"externalReference\" : \"000000122600000000750000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000750000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"546247\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118135651\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", - headers: [ - {"Date", "Thu, 18 Jan 2018 12:56:51 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 400 - } - } - end - - def test_for_purchase_with_invalid_amount do - {:ok, - %HTTPoison.Response{ - body: "{\n \"errorId\" : \"8c34dc0b-776c-44e3-8cd4-b36222960153\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", - headers: [ - {"Date", "Wed, 24 Jan 2018 07:16:06 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"}], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 400 - } - } - end - - def test_for_authorize_with_valid_card do - {:ok, - %HTTPoison.Response{ - body: "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000065\",\n \"externalReference\" : \"000000122600000000650000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118110419\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", - headers: [ - {"Date", "Thu, 18 Jan 2018 10:04:19 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"Location", - "https://api-sandbox.globalcollect.com:443/v1/1226/payments/000000122600000000650000100001"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 201 - } - } - end - - def test_for_authorize_with_invalid_card do - {:ok, - %HTTPoison.Response{ - body: "{\n \"errorId\" : \"dcdf5c8d-e475-4fbc-ac57-76123c1640a2\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978754\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000066\",\n \"externalReference\" : \"000000122600000000660000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000660000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978755\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118111508\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", - headers: [ - {"Date", "Thu, 18 Jan 2018 10:15:08 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 400 - } - } - end - - def test_for_authorize_with_invalid_amount do - {:ok, - %HTTPoison.Response{body: "{\n \"errorId\" : \"1dbef568-ed86-4c8d-a3c3-74ced258d5a2\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", - headers: [ - {"Date", "Tue, 23 Jan 2018 11:18:11 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 400 - } - } - end - - def test_for_refund do - {:ok, - %HTTPoison.Response{ - body: "{\n \"errorId\" : \"b6ba00d2-8f11-4822-8f32-c6d0a4d8793b\",\n \"errors\" : [ {\n \"code\" : \"300450\",\n \"message\" : \"ORDER WITHOUT REFUNDABLE PAYMENTS\",\n \"httpStatusCode\" : 400\n } ]\n}", - headers: [ - {"Date", "Wed, 24 Jan 2018 05:33:56 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/refund", - status_code: 400 - } - } - end - - def test_for_capture_with_valid_paymentid do - {:ok, - %HTTPoison.Response{ - body: "{\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CAPTURE_REQUESTED\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_CONNECT_OR_3RD_PARTY\",\n \"statusCode\" : 800,\n \"statusCodeChangeDateTime\" : \"20180123140826\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", - headers: [ - {"Date", "Tue, 23 Jan 2018 13:08:26 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000650000100001/approve", - status_code: 200 - } - } - end - - def test_for_capture_with_invalid_paymentid do - {:ok, - %HTTPoison.Response{ - body: "{\n \"errorId\" : \"ccb99804-0240-45b6-bb28-52aaae59d71b\",\n \"errors\" : [ {\n \"code\" : \"1002\",\n \"id\" : \"UNKNOWN_PAYMENT_ID\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"propertyName\" : \"paymentId\",\n \"message\" : \"UNKNOWN_PAYMENT_ID\",\n \"httpStatusCode\" : 404\n } ]\n}", - headers: [ - {"Date", "Tue, 23 Jan 2018 12:25:59 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/30/approve", - status_code: 404 - } - } - end - - def test_for_void_with_valid_card do - {:ok, - %HTTPoison.Response{ - body: "{\n \"payment\" : {\n \"id\" : \"000000122600000000870000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20180124064204\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n}", - headers: [ - {"Date", "Wed, 24 Jan 2018 05:42:04 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/cancel", - status_code: 200 - } - } - end - - def test_for_network_failure do - {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} - end -end diff --git a/test/mocks/paymill_mock.ex b/test/mocks/paymill_mock.ex new file mode 100644 index 00000000..da584905 --- /dev/null +++ b/test/mocks/paymill_mock.ex @@ -0,0 +1,136 @@ +defmodule Gringotts.Gateways.PaymillMock do + @moduledoc false + + def auth_success do + ~s/{ "data":{ "id":"preauth_7f0a5b2787d0acb96db5", "amount":"4200", + "currency":"EUR", "description":"description example", "status":"closed", + "livemode":false, "created_at":1523890381, "updated_at":1523890383, + "app_id":null, "payment":{ "id":"pay_abdd833557398641e9dfcc47", + "type":"creditcard", "client":"client_d8b9c9a37b0ecb1bbd83", + "card_type":"mastercard", "country":"DE", "expire_month":"12", + "expire_year":"2018", "card_holder":"Harry Potter", "last4":"0004", + "updated_at":1522922164, "created_at":1522922164, "app_id":null, + "is_recurring":true, "is_usable_for_preauthorization":true }, "client":{ + "id":"client_d8b9c9a37b0ecb1bbd83", "email":null, "description":null, + "app_id":null, "updated_at":1522922164, "created_at":1522922164, "payment":[ + "pay_abdd833557398641e9dfcc47" ], "subscription":null }, "transaction":{ + "id":"tran_7341c475993e3ddbbff801c47597", "amount":4200, + "origin_amount":4200, "status":"preauth", "description":"description + example", "livemode":false, "refunds":null, + "client":"client_d8b9c9a37b0ecb1bbd83", "currency":"EUR", + "created_at":1523890381, "updated_at":1523890383, "response_code":20000, + "short_id":null, "is_fraud":false, "invoices":[ ], "app_id":null, + "preauthorization":"preauth_7f0a5b2787d0acb96db5", "fees":[ ], + "payment":"pay_abdd833557398641e9dfcc47", "mandate_reference":null, + "is_refundable":false, "is_markable_as_fraud":true } }, "mode":"test" }/ + end + + def auth_purchase_invalid_token do + ~s/{ "error":{ "messages":{ + "regexNotMatch":"'tok_d26e611c47d64693a281e841193' does not match against pattern '\/^[a-zA-Z0-9_]{32}$\/'" + }, "field":"token" } }/ + end + + def purchase_valid_token do + ~s/{ "data":{ "id":"tran_de77d38b85d6eee2984accc8b2cc", "amount":4200, + "origin_amount":4200, "status":"closed", "description":"", "livemode":false, + "refunds":null, "client":{ "id":"client_d8b9c9a37b0ecb1bbd83", "email":null, + "description":null, "app_id":null, "updated_at":1522922164, + "created_at":1522922164, "payment":[ "pay_abdd833557398641e9dfcc47" ], + "subscription":null }, "currency":"EUR", "created_at":1524135111, + "updated_at":1524135111, "response_code":20000, "short_id":"0000.9999.0000", + "is_fraud":false, "invoices":[ ], "app_id":null, "preauthorization":null, + "fees":[ ], "payment":{ "id":"pay_abdd833557398641e9dfcc47", + "type":"creditcard", "client":"client_d8b9c9a37b0ecb1bbd83", + "card_type":"mastercard", "country":"DE", "expire_month":"12", + "expire_year":"2018", "card_holder":"Sagar Karwande", "last4":"0004", + "updated_at":1522922164, "created_at":1522922164, "app_id":null, + "is_recurring":true, "is_usable_for_preauthorization":true }, + "mandate_reference":null, "is_refundable":true, "is_markable_as_fraud":true + }, "mode":"test" }/ + end + + def refund_success do + ~s/{ "data":{ "id":"refund_96a0c66456a55ba3e746", "amount":4200, + "status":"refunded", "description":null, "livemode":false, + "created_at":1524138133, "updated_at":1524138133, + "short_id":"0000.9999.0000", "response_code":20000, "reason":null, + "app_id":null, "transaction":{ "id":"tran_de77d38b85d6eee2984accc8b2cc", + "amount":0, "origin_amount":4200, "status":"refunded", "description":"", + "livemode":false, "refunds":[ "refund_96a0c66456a55ba3e746" ], + "client":"client_d8b9c9a37b0ecb1bbd83", "currency":"EUR", + "created_at":1524135111, "updated_at":1524138134, "response_code":20000, + "short_id":"0000.9999.0000", "is_fraud":false, "invoices":[ ], + "app_id":null, "preauthorization":null, "fees":[ ], + "payment":"pay_abdd833557398641e9dfcc47", "mandate_reference":null, + "is_refundable":false, "is_markable_as_fraud":true } }, "mode":"test" }/ + end + + def refund_again do + ~s/{ "exception":"refund_amount_to_high", "error":"Amount to high" }/ + end + + def refund_bad_transaction do + ~s/{ "exception":"transaction_not_found", "error":"Transaction not found" }/ + end + + def capture_success do + ~s/{ "data":{ "id":"tran_2f46c44c4d5219e4ef4b7c6292ba", "amount":4200, + "origin_amount":4200, "status":"closed", "description":"", "livemode":false, + "refunds":null, "client":{ "id":"client_d8b9c9a37b0ecb1bbd83", "email":null, + "description":null, "app_id":null, "updated_at":1522922164, + "created_at":1522922164, "payment":[ "pay_abdd833557398641e9dfcc47" ], + "subscription":null }, "currency":"EUR", "created_at":1524138666, + "updated_at":1524138699, "response_code":20000, "short_id":"0000.9999.0000", + "is_fraud":false, "invoices":[ ], "app_id":null, "preauthorization":{ + "id":"preauth_d654694c8116109af903", "amount":"4200", "currency":"EUR", + "description":"description example", "status":"closed", "livemode":false, + "created_at":1524138666, "updated_at":1524138669, "app_id":null, + "payment":"pay_abdd833557398641e9dfcc47", + "client":"client_d8b9c9a37b0ecb1bbd83", + "transaction":"tran_2f46c44c4d5219e4ef4b7c6292ba" }, "fees":[ ], "payment":{ + "id":"pay_abdd833557398641e9dfcc47", "type":"creditcard", + "client":"client_d8b9c9a37b0ecb1bbd83", "card_type":"mastercard", + "country":"DE", "expire_month":"12", "expire_year":"2018", + "card_holder":"Sagar Karwande", "last4":"0004", "updated_at":1522922164, + "created_at":1522922164, "app_id":null, "is_recurring":true, + "is_usable_for_preauthorization":true }, "mandate_reference":null, + "is_refundable":true, "is_markable_as_fraud":true }, "mode":"test" }/ + end + + def bad_preauth do + ~s/{ "exception":"not_found_transaction_preauthorize", "error":"Preauthorize not found" }/ + end + + def capture_preauth_done_before do + ~s/{ "exception":"preauthorization_already_used", "error":"Preauthorization has already been used" }/ + end + + def void_success do + ~s/{ "data":{ "id":"preauth_0bfc975c2858980a6023", + "amount":"4200", "currency":"EUR", "description":"description example", + "status":"deleted", "livemode":false, "created_at":1524140381, + "updated_at":1524140479, "app_id":null, "payment":{ + "id":"pay_abdd833557398641e9dfcc47", "type":"creditcard", + "client":"client_d8b9c9a37b0ecb1bbd83", "card_type":"mastercard", + "country":"DE", "expire_month":"12", "expire_year":"2018", + "card_holder":"Sagar Karwande", "last4":"0004", "updated_at":1522922164, + "created_at":1522922164, "app_id":null, "is_recurring":true, + "is_usable_for_preauthorization":true }, "client":{ + "id":"client_d8b9c9a37b0ecb1bbd83", "email":null, "description":null, + "app_id":null, "updated_at":1522922164, "created_at":1522922164, "payment":[ + "pay_abdd833557398641e9dfcc47" ], "subscription":null }, "transaction":{ + "id":"tran_f360d805dce7f84baf07077a7f96", "amount":4200, "origin_amount":4200, + "status":"failed", "description":"description example", "livemode":false, + "refunds":null, "client":"client_d8b9c9a37b0ecb1bbd83", "currency":"EUR", + "created_at":1524140381, "updated_at":1524140479, "response_code":50810, + "short_id":null, "is_fraud":false, "invoices":[ ], "app_id":null, + "preauthorization":"preauth_0bfc975c2858980a6023", "fees":[ ], + "payment":"pay_abdd833557398641e9dfcc47", "mandate_reference":null, + "is_refundable":false, "is_markable_as_fraud":true } }, "mode":"test" }/ + end + + def void_done_before do + ~s/{ "exception":"preauthorization_not_found", "error":"Preauthorization was not found" }/ + end +end diff --git a/test/mocks/trexle_mock.ex b/test/mocks/trexle_mock.ex new file mode 100644 index 00000000..a0229b24 --- /dev/null +++ b/test/mocks/trexle_mock.ex @@ -0,0 +1,223 @@ +defmodule Gringotts.Gateways.TrexleMock do + @moduledoc false + + def test_for_purchase_with_valid_card do + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"charge_3e89c6f073606ac1efe62e76e22dd7885441dc72","success":true,"captured":false}}/, + headers: [ + {"Date", "Fri, 22 Dec 2017 11:57:28 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "9b2a1d30-9bca-48f2-862e-4090766689cb"}, + {"X-Runtime", "0.777520"}, + {"Content-Length", "104"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 201 + }} + end + + def test_for_purchase_with_invalid_card do + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, + headers: [ + {"Date", "Fri, 22 Dec 2017 13:20:50 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "eb8100a1-8ffa-47da-9623-8d3b2af51b84"}, + {"X-Runtime", "0.445244"}, + {"Content-Length", "77"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} + end + + def test_for_purchase_with_invalid_amount do + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:16:33 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "4ce2eea4-3ea9-4345-ac85-9bc45f22f5ac"}, + {"X-Runtime", "0.476058"}, + {"Content-Length", "70"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} + end + + def test_for_authorize_with_valid_card do + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"charge_8ab2b21a2f02495f5c36b34d129c8a0e836add32","success":true,"captured":false}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:33:31 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "51d28d13-81e5-41fd-b711-1b6531fdd3dd"}, + {"X-Runtime", "0.738395"}, + {"Content-Length", "104"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 201 + }} + end + + def test_for_authorize_with_invalid_card do + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:25:40 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "239e7054-9500-4a87-bf3b-09456d550b6d"}, + {"X-Runtime", "0.466670"}, + {"Content-Length", "77"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} + end + + def test_for_authorize_with_invalid_amount do + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:40:10 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "d58db806-8016-4a0e-8519-403a969fa1a7"}, + {"X-Runtime", "0.494636"}, + {"Content-Length", "70"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} + end + + def test_for_refund_with_valid_token do + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"refund_a86a757cc6bdabab50d6ebbfcdcd4db4e04198dd","success":true,"amount":50,"charge":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","status_message":"Transaction approved"}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:55:41 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "b1c94703-7fb4-48f2-b1b4-32e3b6a87e57"}, + {"X-Runtime", "1.097186"}, + {"Content-Length", "198"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: + "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/refunds", + status_code: 201 + }} + end + + def test_for_refund_with_invalid_token do + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Refund failed","detail":"invalid token"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:53:09 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "276fd8f5-dc21-40be-8add-fa76aabbfc5b"}, + {"X-Runtime", "0.009374"}, + {"Content-Length", "50"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges/34/refunds", + status_code: 400 + }} + end + + def test_for_capture_with_valid_chargetoken do + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","success":true,"captured":true,"amount":50,"status_message":"Transaction approved"}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:49:50 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "97ca2db6-fd4f-4a5b-ae45-01fae9c13668"}, + {"X-Runtime", "1.092051"}, + {"Content-Length", "155"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: + "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/capture", + status_code: 200 + }} + end + + def test_for_capture_with_invalid_chargetoken do + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Capture failed","detail":"invalid token"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:47:18 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "b46ecb8d-7df8-4c5f-b87f-c53fae364e79"}, + {"X-Runtime", "0.010255"}, + {"Content-Length", "51"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges/30/capture", + status_code: 400 + }} + end + + def test_for_store_with_valid_card do + {:ok, + %HTTPoison.Response{ + body: ~s/{"response":{"token":"token_94e333959850270460e89a86bad2246613528cbb", + "card":{"token":"token_2a1ba29522e4a377fafa62e8e204f76ad8ba8f1e", + "scheme":"master","display_number":"XXXX-XXXX-XXXX-8210","expiry_year":2018,"expiry_month":1, + "cvc":123,"name":"John Doe","address_line1":"456 My Street", + "address_line2":null, "address_city":"Ottawa", + "address_state":"ON","address_postcode":"K1C2N6", + "address_country":"CA","primary":true}}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 19:32:58 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "1a334b22-8e01-4e1b-8b58-90dfd0b7c12f"}, + {"X-Runtime", "0.122441"}, + {"Content-Length", "422"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: "https://core.trexle.com/api/v1//customers", + status_code: 201 + }} + end + + def test_for_network_failure do + {:error, %HTTPoison.Error{id: :some_hackney_error_id, reason: :some_hackney_error}} + end +end diff --git a/test/mocks/trexle_mock.exs b/test/mocks/trexle_mock.exs deleted file mode 100644 index 27c4d1c2..00000000 --- a/test/mocks/trexle_mock.exs +++ /dev/null @@ -1,200 +0,0 @@ -defmodule Gringotts.Gateways.TrexleMock do - def test_for_purchase_with_valid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"charge_3e89c6f073606ac1efe62e76e22dd7885441dc72","success":true,"captured":false}}/, - headers: [ - {"Date", "Fri, 22 Dec 2017 11:57:28 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "9b2a1d30-9bca-48f2-862e-4090766689cb"}, - {"X-Runtime", "0.777520"}, - {"Content-Length", "104"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 201 - }} - end - - def test_for_purchase_with_invalid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, - headers: [ - {"Date", "Fri, 22 Dec 2017 13:20:50 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "eb8100a1-8ffa-47da-9623-8d3b2af51b84"}, - {"X-Runtime", "0.445244"}, - {"Content-Length", "77"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - }} - end - - def test_for_purchase_with_invalid_amount do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:16:33 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "4ce2eea4-3ea9-4345-ac85-9bc45f22f5ac"}, - {"X-Runtime", "0.476058"}, - {"Content-Length", "70"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - }} - end - - def test_for_authorize_with_valid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"charge_8ab2b21a2f02495f5c36b34d129c8a0e836add32","success":true,"captured":false}}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:33:31 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "51d28d13-81e5-41fd-b711-1b6531fdd3dd"}, - {"X-Runtime", "0.738395"}, - {"Content-Length", "104"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 201 - }} - end - - def test_for_authorize_with_invalid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:25:40 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "239e7054-9500-4a87-bf3b-09456d550b6d"}, - {"X-Runtime", "0.466670"}, - {"Content-Length", "77"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - }} - end - - def test_for_authorize_with_invalid_amount do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:40:10 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "d58db806-8016-4a0e-8519-403a969fa1a7"}, - {"X-Runtime", "0.494636"}, - {"Content-Length", "70"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - }} - end - - def test_for_refund_with_valid_token do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"refund_a86a757cc6bdabab50d6ebbfcdcd4db4e04198dd","success":true,"amount":50,"charge":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","status_message":"Transaction approved"}}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:55:41 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "b1c94703-7fb4-48f2-b1b4-32e3b6a87e57"}, - {"X-Runtime", "1.097186"}, - {"Content-Length", "198"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: - "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/refunds", - status_code: 201 - }} - end - - def test_for_refund_with_invalid_token do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Refund failed","detail":"invalid token"}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:53:09 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "276fd8f5-dc21-40be-8add-fa76aabbfc5b"}, - {"X-Runtime", "0.009374"}, - {"Content-Length", "50"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges/34/refunds", - status_code: 400 - }} - end - - def test_for_capture_with_valid_chargetoken do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","success":true,"captured":true,"amount":50,"status_message":"Transaction approved"}}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:49:50 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "97ca2db6-fd4f-4a5b-ae45-01fae9c13668"}, - {"X-Runtime", "1.092051"}, - {"Content-Length", "155"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: - "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/capture", - status_code: 200 - }} - end - - def test_for_capture_with_invalid_chargetoken do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Capture failed","detail":"invalid token"}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:47:18 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "b46ecb8d-7df8-4c5f-b87f-c53fae364e79"}, - {"X-Runtime", "0.010255"}, - {"Content-Length", "51"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges/30/capture", - status_code: 400 - }} - end - - def test_for_store_with_valid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"token_94e333959850270460e89a86bad2246613528cbb","card":{"token":"token_2a1ba29522e4a377fafa62e8e204f76ad8ba8f1e","scheme":"master","display_number":"XXXX-XXXX-XXXX-8210","expiry_year":2018,"expiry_month":1,"cvc":123,"name":"John Doe","address_line1":"456 My Street","address_line2":null,"address_city":"Ottawa","address_state":"ON","address_postcode":"K1C2N6","address_country":"CA","primary":true}}}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 19:32:58 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "1a334b22-8e01-4e1b-8b58-90dfd0b7c12f"}, - {"X-Runtime", "0.122441"}, - {"Content-Length", "422"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//customers", - status_code: 201 - }} - end - - def test_for_network_failure do - {:error, %HTTPoison.Error{id: :some_hackney_error_id, reason: :some_hackney_error}} - end -end