Current section
Files
Jump to
Current section
Files
lib/http.ex
defmodule Affirm.HTTP do
@moduledoc """
Base client for all server interaction, used by all endpoint specific
modules. This request wrapper coordinates the remote server, headers,
authorization and SSL options.
This uses `HTTPoison.Base`, so all of the typical HTTP verbs are availble.
Sample Affirm config:
config :affirm,
url: "base_api_url",
public_key: "affirm_supplied_public_key",
private_key: "affirm_supplied_private_key"
"""
use HTTPoison.Base
alias HTTPoison.Response
alias HTTPoison.AsyncResponse
@spec request(atom, binary, binary, headers, Keyword.t()) ::
{:ok, Response.t() | AsyncResponse.t()} | {:error, integer, Response.t()} | {:error, atom | String.t()}
def request(method, url, body, headers \\ [], options \\ []) do
method
|> super(full_url(url), body, headers, options)
|> process_response
end
@spec full_url(binary) :: binary
def full_url(url) do
Affirm.get_env(:url) <> url
end
## HTTPoison Callbacks
@doc false
def process_request_body(body) when body == "" or body == %{}, do: ""
def process_request_body(body) do
Jason.encode!(body)
end
@doc false
def process_response_body(body) do
case Jason.decode(body) do
{:ok, parsed_body} ->
Enum.reduce(parsed_body, %{}, fn {key, val}, acc -> Map.put(acc, String.to_atom(key), val) end)
{:error, _} ->
body
end
end
@doc false
def process_response({:ok, %{status_code: 200, body: %{code: error_code} = body}}) do
case fetch_error_code(error_code) do
nil -> {:error, Affirm.Response.new(%{message: body["message"]})}
message -> {:error, Affirm.Response.new(%{message: message})}
end
end
def process_response({:ok, %{status_code: code, body: body}})
when code >= 200 and code <= 399,
do: {:ok, body}
def process_response({:ok, %{status_code: 400}}), do: {:error, :invalid_request}
def process_response({:ok, %{status_code: 401}}), do: {:error, :unauthorized}
def process_response({:ok, %{status_code: 404}}), do: {:error, :not_found}
def process_response({:ok, %{body: body}}), do: {:error, body}
def process_response({:error, ":econnrefused"}), do: {:error, :econnrefused}
def process_response({_code, %HTTPoison.Error{reason: reason}}), do: {:error, inspect(reason)}
@doc """
Provides a mapping from returned error code to error String.
Affirm also provides this message in the returned response, but this
provides an easier way of mocking/testing.
"""
@spec fetch_error_code(String.t()) :: String.t()
def fetch_error_code(error_code) do
# credo:disable-for-lines:2 Credo.Check.Readability.MaxLineLength
code_map = %{
"auth-declined" => "Charge authorization hold declined.",
"capture-greater-instrument" =>
"Charges on this instrument cannot be captured for more than the authorization hold amount.",
"capture-unequal-instrument" =>
"Charges on this instrument cannot be captured for an amount unequal to authorization hold amount.",
"capture-voided" => "Cannot capture voided charge.",
"partial-capture-instrument" => "Charges on this instrument cannot be partially captured.",
"refund-exceeded" => "Max refund amount exceeded.",
"refund-uncaptured" => "Cannot refund a charge that has not been captured.",
"refund-voided" => "Cannot refund voided charge.",
"capture-declined" => "Charge capture declined.",
"capture-limit-exceeded" => "Max capture amount on charge exceeded.",
"expired-authorization" => "Cannot capture expired charge authorization hold.",
"refund-expired" => "Charges on this instrument must be refunded within 120 days of capture.",
"financial-product-invalid" => "Please provide a valid financial product key.",
"invalid_field" => "An input field resulted in invalid request."
}
code_map[error_code]
end
end