Current section
Files
Jump to
Current section
Files
lib/carrier.ex
defmodule Carrier do
@moduledoc """
Elixir library for interacting with Smarty's API. The main functions in this
module are `verify/1` to verify a single address and `verify_many/1` to verify
multiple addresses using batch processing.
"""
# Get hostname from config instead of hardcoding it
@hostname Application.compile_env(:carrier, Carrier)[:hostname] ||
"https://us-street.api.smarty.com/street-address"
@spec verify(map()) :: {:ok, map()} | {:error, term()}
@doc """
Verifies a single address.
"""
def verify(address) do
client()
|> Req.get(params: Map.merge(auth_params(), standardize_request(address)))
|> standardize_response()
end
@spec verify_many([map()]) :: {:ok, [map()]} | {:error, term()}
@doc """
Verifies multiple addresses in a batch.
"""
def verify_many(addresses) do
%{to_verify: addresses, cached: cached} =
Enum.reduce(addresses, %{to_verify: [], cached: []}, fn address, acc ->
Map.put(acc, :to_verify, [address | acc.to_verify])
end)
case client()
|> Req.post(params: auth_params(), json: Enum.map(addresses, &standardize_request/1))
|> standardize_response(force_list: true) do
{:ok, result} -> {:ok, result ++ cached}
error -> error
end
end
defp standardize_response(resp, opts \\ [])
defp standardize_response({:ok, %{status: 200, body: body}}, opts) when is_list(body) do
force_list = Keyword.get(opts, :force_list)
if force_list do
{:ok, Enum.map(body, &standardize_result/1)}
else
case body do
[] -> {:error, :no_results}
[res] -> {:ok, standardize_result(res)}
_ -> {:ok, Enum.map(body, &standardize_result/1)}
end
end
end
defp standardize_response(resp, _opts), do: resp
defp auth_params do
%{"auth-id" => auth_id(), "auth-token" => auth_token()}
end
@doc """
This is the function used to standardize requests. You should not need to use
this function directly, as it is called by `verify/1` and `verify_many/1`. It
is public for testing purposes.
"""
def standardize_request(address) do
address
|> Enum.filter(fn {k, _} ->
k in [:input_id, :street, :city, :state, :zip, "street", "city", "state", "zip", "input_id"]
end)
|> Enum.map(fn
{k, v} when is_atom(k) -> {Atom.to_string(k), v}
{k, v} -> {k, v}
end)
|> Enum.into(%{})
|> Map.merge(%{"candidates" => 1})
end
@doc """
This is the function used to standardize responses. You should not need to use
this function directly, as it is called by `verify/1` and `verify_many/1`. It
is public for testing purposes.
"""
def standardize_result(result) do
zip =
case result["components"] do
%{"zipcode" => zip, "plus4_code" => plus4} -> "#{zip}-#{plus4}"
%{"zipcode" => zip} -> zip
end
meta =
case result do
%{"input_id" => id} -> %{"input_id" => id}
_ -> %{}
end
deliverable =
case result["analysis"] do
%{"dpv_match_code" => "Y", "dpv_vacant" => "N"} -> "deliverable"
%{"dpv_match_code" => "Y", "dpv_vacant" => "Y"} -> "vacant"
%{"dpv_match_code" => "D"} -> "incomplete"
_ -> "undeliverable"
end
notes =
case result["analysis"] do
%{"dpv_footnotes" => footnotes} ->
footnotes
|> String.codepoints()
|> Enum.chunk_every(2)
|> Enum.map(&Enum.join/1)
|> Enum.map(fn
"AA" -> "valid"
"A1" -> "invalid"
"BB" -> "valid"
"CC" -> "unknown_secondary"
"M1" -> "missing_primary_number"
"M3" -> "invalid_primary_number"
"N1" -> "missing_secondary"
"P1" -> "missing_po"
"P3" -> "invalid_po"
"RR" -> "valid"
_note -> nil
end)
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
_ ->
[]
end
parsed_result = %{
"street" => result["delivery_line_1"],
"city" => result["components"]["city_name"],
"state" => result["components"]["state_abbreviation"],
"zip" => zip,
"verified" => String.length(zip) == 10 && deliverable == "deliverable",
"meta" => %{
"latitude" => result["metadata"]["latitude"],
"longitude" => result["metadata"]["longitude"],
"county" => result["metadata"]["county_name"],
"deliverable" => deliverable,
"notes" => notes
}
}
Map.merge(meta, parsed_result)
end
defp client do
Req.new(base_url: @hostname, headers: [{"Accept", "application/json"}])
end
defp auth_id do
Application.get_env(:carrier, Carrier)[:auth_id]
end
defp auth_token do
Application.get_env(:carrier, Carrier)[:auth_token]
end
end