lib/gringotts/gateways/authorize_net.ex
defmodule Gringotts.Gateways.AuthorizeNet do
@moduledoc """
A module for working with the Authorize.Net payment gateway.
Refer the official Authorize.Net [API docs][docs].
The following set of functions for Authorize.Net 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` |
| Create Customer Profile | `store/2` |
| Create Customer Payment Profile | `store/2` |
| Delete Customer Profile | `unstore/2` |
Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply
optional arguments for transactions with the Authorize.Net gateway. The
following keys are supported:
| Key |
| ---- |
| `customer` |
| `invoice` |
| `bill_to` |
| `ship_to` |
| `customer_ip` |
| `order` |
| `lineitems` |
| `ref_id` |
| `tax` |
| `duty` |
| `shipping` |
| `po_number` |
| `customer_type` |
| `customer_profile_id` |
| `profile` |
To know more about these keywords check the [Request and Response][req-resp] tabs for each
API method.
[docs]: https://developer.authorize.net/api/reference/index.html
[req-resp]: https://developer.authorize.net/api/reference/index.html#payment-transactions
## Notes
1. Though Authorize.Net supports [multiple currencies][currencies] however,
multiple currencies in one account is not supported. A merchant would need
multiple Authorize.Net accounts, one for each chosen currency. Please refer
the section on "Supported acquirers and currencies" [here][currencies].
2. _You, the merchant needs to be PCI-DSS Compliant if you wish to use this
module. Your server will recieve sensitive card and customer information._
3. 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
## Configuring your AuthorizeNet account at `Gringotts`
To use this module you need to [create an account][dashboard] with the
Authorize.Net gateway and obtain your login secrets: `name` and
`transactionKey`.
Your Application config **must include the `name` and `transaction_key`
fields** and would look something like this:
config :gringotts, Gringotts.Gateways.AuthorizeNet,
name: "name_provided_by_authorize_net",
transaction_key: "transactionKey_provided_by_authorize_net"
## Scope of this module
Although Authorize.Net supports payments from various sources (check your
[dashboard][dashboard]), this library currently accepts payments by
(supported) credit cards only.
[dashboard]: https://www.authorize.net/solutions/merchantsolutions/onlinemerchantaccount/
## Following the examples
1. First, set up a sample application and configure it to work with Authorize.Net.
- 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"
[above](#module-configuring-your-authorizenet-account-at-gringotts).
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 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
use Gringotts.Gateways.Base
use Gringotts.Adapter, required_config: [:name, :transaction_key]
alias Gringotts.Gateways.AuthorizeNet.ResponseHandler
@test_url "https://apitest.authorize.net/xml/v1/request.api"
@production_url "https://api.authorize.net/xml/v1/request.api"
@headers [{"Content-Type", "text/xml"}]
@transaction_type %{
purchase: "authCaptureTransaction",
authorize: "authOnlyTransaction",
capture: "priorAuthCaptureTransaction",
refund: "refundTransaction",
void: "voidTransaction"
}
@aut_net_namespace "AnetApi/xml/v1/schema/AnetApiSchema.xsd"
alias Gringotts.{CreditCard, Money, Response}
@doc """
Transfers `amount` from the customer to the merchant.
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.id` field) which
can be used to:
* `refund/3` a settled transaction.
* `void/2` a transaction.
[auth-cap-same-time]: https://developer.authorize.net/api/reference/index.html#payment-transactions-charge-a-credit-card
## Optional Fields
opts = [
order: %{invoice_number: String, description: String},
ref_id: String,
lineitems: %{
item_id: String, name: String, description: String,
quantity: Integer, unit_price: Gringotts.Money.t()
},
tax: %{amount: Gringotts.Money.t(), name: String, description: String},
duty: %{amount: Gringotts.Money.t(), name: String, description: String},
shipping: %{amount: Gringotts.Money.t(), name: String, description: String},
po_number: String,
customer: %{id: String},
bill_to: %{
first_name: String, last_name: String, company: String,
address: String, city: String, state: String, zip: String,
country: String
},
ship_to: %{
first_name: String, last_name: String, company: String, address: String,
city: String, state: String, zip: String, country: String
},
customer_ip: String
]
## Example
iex> amount = Money.new(20, :USD)
iex> opts = [
ref_id: "123456",
order: %{invoice_number: "INV-12345", description: "Product Description"},
lineitems: %{item_id: "1", name: "vase", description: "Cannes logo", quantity: 1, unit_price: amount},
tax: %{name: "VAT", amount: Money.new("0.1", :EUR), description: "Value Added Tax"},
shipping: %{name: "SAME-DAY-DELIVERY", amount: Money.new("0.56", :EUR), description: "Zen Logistics"},
duty: %{name: "import_duty", amount: Money.new("0.25", :EUR), description: "Upon import of goods"}
]
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.t()}
def purchase(amount, payment, opts) do
request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:purchase])
commit(request_data, opts)
end
@doc """
Authorize a credit card transaction.
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.
To transfer the funds to merchant's account follow this up with a `capture/3`.
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.
## Optional Fields
opts = [
order: %{invoice_number: String, description: String},
ref_id: String,
lineitems: %{
item_id: String, name: String, description: String,
quantity: Integer, unit_price: Gringotts.Money.t()
},
tax: %{amount: Gringotts.Money.t(), name: String, description: String},
duty: %{amount: Gringotts.Money.t(), name: String, description: String},
shipping: %{amount: Gringotts.Money.t(), name: String, description: String},
po_number: String,
customer: %{id: String},
bill_to: %{
first_name: String, last_name: String, company: String,
address: String, city: String, state: String, zip: String,
country: String
},
ship_to: %{
first_name: String, last_name: String, company: String, address: String,
city: String, state: String, zip: String, country: String
},
customer_ip: String
]
## Example
iex> amount = Money.new(20, :USD)
iex> opts = [
ref_id: "123456",
order: %{invoice_number: "INV-12345", description: "Product Description"},
lineitems: %{item_id: "1", name: "vase", description: "Cannes logo", quantity: 1, unit_price: amount},
tax: %{name: "VAT", amount: Money.new("0.1", :EUR), description: "Value Added Tax"},
shipping: %{name: "SAME-DAY-DELIVERY", amount: Money.new("0.56", :EUR), description: "Zen Logistics"},
duty: %{name: "import_duty", amount: Money.new("0.25", :EUR), description: "Upon import of goods"}
]
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.t()}
def authorize(amount, payment, opts) do
request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:authorize])
commit(request_data, opts)
end
@doc """
Captures a pre-authorized `amount`.
`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.id` field) which
can be used to:
* `refund/3` a settled transaction.
* `void/2` a transaction.
## Notes
* Authorize.Net automatically settles authorized transactions after 24
hours. Hence, unnecessary authorizations must be `void/2`ed within this
period!
* Though Authorize.Net supports partial capture of the authorized `amount`, it
is [advised][sound-advice] not to do so.
[sound-advice]: https://support.authorize.net/authkb/index?page=content&id=A1720&actp=LIST
## Optional Fields
opts = [
order: %{invoice_number: String, description: String},
ref_id: String
]
## Example
iex> opts = [
ref_id: "123456"
]
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.t()}
def capture(id, amount, opts) do
request_data = normal_capture(amount, id, opts, @transaction_type[:capture])
commit(request_data, opts)
end
@doc """
Refund `amount` for a settled transaction referenced by `id`.
The `payment` field in the `opts` is used to set the instrument/mode of
payment, which could be different from the original one. Currently, we
support only refunds to cards, so put the `card` details in the `payment`.
## Required fields
opts = [
payment: %{card: %{number: String, year: Integer, month: Integer}}
]
## Optional fields
opts = [ref_id: String]
## Example
iex> opts = [
payment: %{card: %{number: "5424000000000015", year: 2099, month: 12}}
ref_id: "123456"
]
iex> id = "123456"
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.t()}
def refund(amount, id, opts) do
request_data = normal_refund(amount, id, opts, @transaction_type[:refund])
commit(request_data, opts)
end
@doc """
Voids the referenced payment.
This method attempts a reversal of the either a previous `purchase/3` or
`authorize/3` referenced by `id`.
It can cancel either an original transaction that may not be settled or an
entire order composed of more than one transaction.
## Optional fields
opts = [ref_id: String]
## Example
iex> opts = [
ref_id: "123456"
]
iex> id = "123456"
iex> result = Gringotts.void(Gringotts.Gateways.AuthorizeNet, id, opts)
"""
@spec void(String.t(), Keyword.t()) :: {:ok | :error, Response.t()}
def void(id, opts) do
request_data = normal_void(id, opts, @transaction_type[:void])
commit(request_data, opts)
end
@doc """
Store a customer's profile and optionally associate it with a payment profile.
Authorize.Net separates a [customer's profile][cust-profile] from their payment
profile. Thus a customer can have multiple payment profiles.
## Create both profiles
Add `:customer` details in `opts` and also provide `card` details. The response
will contain a `:customer_profile_id`.
## Associate payment profile with existing customer profile
Simply pass the `:customer_profile_id` in the `opts`. This will add the `card`
details to the profile referenced by the supplied `:customer_profile_id`.
## Notes
* Currently only supports `credit card` in the payment profile.
* The supplied `card` details can be validated by supplying a
[`:validation_mode`][cust-profile], available options are `testMode` and
`liveMode`, the deafult is `testMode`.
[cust-profile]: https://developer.authorize.net/api/reference/index.html#customer-profiles-create-customer-profile
## Required Fields
opts = [
profile: %{merchant_customer_id: String, description: String, email: String}
]
## Optional Fields
opts = [
validation_mode: String,
bill_to: %{
first_name: String, last_name: String, company: String, address: String,
city: String, state: String, zip: String, country: String
},
customer_type: String,
customer_profile_id: String
]
## Example
iex> opts = [
profile: %{merchant_customer_id: 123456, description: "test store", email: "test@gmail.com"},
validation_mode: "testMode"
]
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.t()}
def store(card, opts) do
request_data =
if opts[:customer_profile_id] do
card |> create_customer_payment_profile(opts) |> generate(format: :none)
else
card |> create_customer_profile(opts) |> generate(format: :none)
end
commit(request_data, opts)
end
@doc """
Remove a customer profile from the payment gateway.
Use this function to unstore the customer card information by deleting the customer profile
present. Requires the customer profile id.
## Example
iex> id = "123456"
iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, id)
"""
@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(format: :none)
commit(request_data, opts)
end
# method to make the API request with params
defp commit(payload, opts) do
opts
|> base_url()
|> HTTPoison.post(payload, @headers)
|> respond()
end
defp respond({:ok, %{body: body, status_code: 200}}), do: ResponseHandler.respond(body)
defp respond({:ok, %{body: body, status_code: code}}) do
{:error, %Response{raw: body, status_code: code}}
end
defp respond({:error, %HTTPoison.Error{} = error}) do
{
:error,
%Response{
reason: "network related failure",
message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]"
}
}
end
##############################################################################
# HELPER METHODS #
##############################################################################
# function for formatting the request as an xml for purchase and authorize method
defp add_auth_purchase(amount, payment, opts, transaction_type) do
:createTransactionRequest
|> element(%{xmlns: @aut_net_namespace}, [
add_merchant_auth(opts[:config]),
add_order_id(opts),
add_purchase_transaction_request(amount, transaction_type, payment, opts)
])
|> generate(format: :none)
end
# function for formatting the request for normal capture
defp normal_capture(amount, id, opts, transaction_type) do
:createTransactionRequest
|> element(%{xmlns: @aut_net_namespace}, [
add_merchant_auth(opts[:config]),
add_order_id(opts),
add_capture_transaction_request(amount, id, transaction_type)
])
|> generate(format: :none)
end
# function to format the request for normal refund
defp normal_refund(amount, id, opts, transaction_type) do
:createTransactionRequest
|> element(%{xmlns: @aut_net_namespace}, [
add_merchant_auth(opts[:config]),
add_order_id(opts),
add_refund_transaction_request(amount, id, opts, transaction_type)
])
|> generate(format: :none)
end
# function to format the request for normal void operation
defp normal_void(id, opts, transaction_type) do
:createTransactionRequest
|> element(%{xmlns: @aut_net_namespace}, [
add_merchant_auth(opts[:config]),
add_order_id(opts),
element(:transactionRequest, [
add_transaction_type(transaction_type),
add_ref_trans_id(id)
])
])
|> generate(format: :none)
end
defp create_customer_payment_profile(card, opts) do
element(:createCustomerPaymentProfileRequest, %{xmlns: @aut_net_namespace}, [
add_merchant_auth(opts[:config]),
element(:customerProfileId, opts[:customer_profile_id]),
element(:paymentProfile, [
add_billing_info(opts),
add_payment_source(card)
]),
element(
:validationMode,
if(opts[:validation_mode], do: opts[:validation_mode], else: "testMode")
)
])
end
defp create_customer_profile(card, opts) do
element(:createCustomerProfileRequest, %{xmlns: @aut_net_namespace}, [
add_merchant_auth(opts[:config]),
element(:profile, [
element(:merchantCustomerId, opts[:profile][:merchant_customer_id]),
element(:description, opts[:profile][:description]),
element(:email, opts[:profile][:description]),
element(:paymentProfiles, [
element(
:customerType,
if(opts[:customer_type], do: opts[:customer_type], else: "individual")
),
add_billing_info(opts),
add_payment_source(card)
])
]),
element(
:validationMode,
if(opts[:validation_mode], do: opts[:validation_mode], else: "testMode")
)
])
end
defp delete_customer_profile(id, opts) do
element(:deleteCustomerProfileRequest, %{xmlns: @aut_net_namespace}, [
add_merchant_auth(opts[:config]),
element(:customerProfileId, id)
])
end
##############################################################################
# HELPERS TO ASSIST IN BUILDING AND #
# COMPOSING DIFFERENT XmlBuilder TAGS #
##############################################################################
defp add_merchant_auth(opts) do
element(:merchantAuthentication, [
element(:name, opts[:name]),
element(:transactionKey, opts[:transaction_key])
])
end
defp add_order_id(opts) do
element(:refId, opts[:ref_id])
end
defp add_purchase_transaction_request(amount, transaction_type, payment, opts) do
element(:transactionRequest, [
add_transaction_type(transaction_type),
add_amount(amount),
add_payment_source(payment),
add_invoice(opts),
add_tax_fields(opts),
add_duty_fields(opts),
add_shipping_fields(opts),
add_po_number(opts),
add_customer_info(opts)
])
end
defp add_capture_transaction_request(amount, id, transaction_type) do
element(:transactionRequest, [
add_transaction_type(transaction_type),
add_amount(amount),
add_ref_trans_id(id)
])
end
defp add_refund_transaction_request(amount, id, opts, transaction_type) do
element(:transactionRequest, [
add_transaction_type(transaction_type),
add_amount(amount),
element(:payment, [
element(:creditCard, [
element(:cardNumber, opts[:payment][:card][:number]),
element(
:expirationDate,
join_string([opts[:payment][:card][:year], opts[:payment][:card][:month]], "-")
)
])
]),
add_ref_trans_id(id)
])
end
defp add_ref_trans_id(id) do
element(:refTransId, id)
end
defp add_transaction_type(transaction_type) do
element(:transactionType, transaction_type)
end
defp add_amount(amount) do
if amount do
{_, value} = amount |> Money.to_string()
element(:amount, value)
end
end
defp add_payment_source(source) do
# have to implement for other sources like apple pay
# token payment method and check currently only for credit card
add_credit_card(source)
end
defp add_credit_card(source) do
element(:payment, [
element(:creditCard, [
element(:cardNumber, source.number),
element(:expirationDate, join_string([source.year, source.month], "-")),
element(:cardCode, source.verification_code)
])
])
end
defp add_invoice(opts) do
element([
element(:order, [
element(:invoiceNumber, opts[:order][:invoice_number]),
element(:description, opts[:order][:description])
]),
element(:lineItems, [
element(:lineItem, [
element(:itemId, opts[:lineitems][:item_id]),
element(:name, opts[:lineitems][:name]),
element(:description, opts[:lineitems][:description]),
element(:quantity, opts[:lineitems][:quantity]),
element(
:unitPrice,
opts[:lineitems][:unit_price] |> Money.value() |> Decimal.to_float()
)
])
])
])
end
defp add_tax_fields(opts) do
element(:tax, [
add_amount(opts[:tax][:amount]),
element(:name, opts[:tax][:name]),
element(:description, opts[:tax][:description])
])
end
defp add_duty_fields(opts) do
element(:duty, [
add_amount(opts[:duty][:amount]),
element(:name, opts[:duty][:name]),
element(:description, opts[:duty][:description])
])
end
defp add_shipping_fields(opts) do
element(:shipping, [
add_amount(opts[:shipping][:amount]),
element(:name, opts[:shipping][:name]),
element(:description, opts[:shipping][:description])
])
end
defp add_po_number(opts) do
element(:poNumber, opts[:po_number])
end
defp add_customer_info(opts) do
element([
add_customer_id(opts),
add_billing_info(opts),
add_shipping_info(opts),
add_customer_ip(opts)
])
end
defp add_customer_id(opts) do
element(:customer, [
element(:id, opts[:customer][:id])
])
end
defp add_billing_info(opts) do
element(:billTo, [
element(:firstName, opts[:bill_to][:first_name]),
element(:lastName, opts[:bill_to][:last_name]),
element(:company, opts[:bill_to][:company]),
element(:address, opts[:bill_to][:address]),
element(:city, opts[:bill_to][:city]),
element(:state, opts[:bill_to][:state]),
element(:zip, opts[:bill_to][:zip]),
element(:country, opts[:bill_to][:country])
])
end
defp add_shipping_info(opts) do
element(:shipTo, [
element(:firstName, opts[:ship_to][:first_name]),
element(:lastName, opts[:ship_to][:last_name]),
element(:company, opts[:ship_to][:company]),
element(:address, opts[:ship_to][:address]),
element(:city, opts[:ship_to][:city]),
element(:state, opts[:ship_to][:state]),
element(:zip, opts[:ship_to][:zip]),
element(:country, opts[:ship_to][:country])
])
end
defp add_customer_ip(opts) do
element(:customerIP, opts[:customer_ip])
end
defp join_string(list, symbol) do
Enum.join(list, symbol)
end
defp base_url(opts) do
if opts[:config][:mode] == :prod do
@production_url
else
@test_url
end
end
##################################################################################
# RESPONSE_HANDLER MODULE #
# #
##################################################################################
defmodule ResponseHandler do
@moduledoc false
alias Gringotts.Response
@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}
}
@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
}
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_cavv_result(cavv_result)
end
def parse_gateway_error(result, base_response) do
message = result["messages"]["message"]["text"]
gateway_code = result["messages"]["message"]["code"]
error_text = result["transactionResponse"]["errors"]["error"]["errorText"]
error_code = result["transactionResponse"]["errors"]["error"]["errorCode"]
reason = "#{error_text} [Error code (#{error_code})]"
base_response
|> set_message(message)
|> set_gateway_code(gateway_code)
|> set_reason(reason)
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_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