Current section

Files

Jump to
shippex lib shippex.ex
Raw

lib/shippex.ex

defmodule Shippex do
@moduledoc """
## Configuration
config :shippex,
env: :dev,
carriers: [
ups: [
username: "MyUsername",
password: "MyPassword",
secret_key: "123123",
shipper: %{
account_number: "AB1234",
name: "My Company",
phone: "123-456-7890",
address: "1234 Foo St",
city: "Foo",
state: "TX",
zip: "78999"
}
]
]
## Create origin/destination addresses
origin = Shippex.Address.to_struct(%{
name: "Earl G",
phone: "123-123-1234",
address: "9999 Hobby Lane",
address_line_2: nil,
city: "Austin",
state: "TX",
zip: "78703"
})
destination = Shippex.Address.to_struct(%{
name: "Bar Baz",
phone: "123-123-1234",
address: "1234 Foo Blvd",
address_line_2: nil,
city: "Plano",
state: "TX",
zip: "75074"
})
## Create a package
# Currently only inches and pounds (lbs) supported.
package = %Shippex.Package{
length: 8,
width: 8,
height: 4,
weight: 5,
description: "Headphones"
}
## Link the origin, destination, and package with a Shipment
shipment = %Shippex.Shipment{
from: origin,
to: destination,
package: package
}
## Fetch rates to present to the user.
rates = Shippex.fetch_rates(shipment)
## Accept one of the services and print the label
{:ok, rate} = Enum.shuffle(rates) |> hd
{:ok, label} = Shippex.fetch_label(rate, shipment)
## Write the label gif to disk
File.write!("\#{label.tracking_number}.gif", Base.decode64!(label.image))
"""
@type response :: %{code: String.t, message: String.t}
defmodule InvalidConfigError do
defexception [:message]
def exception(message) do
"Invalid config: #{inspect message}"
end
end
@doc false
def config do
case Application.get_env(:shippex, :carriers, :not_found) do
:not_found -> raise InvalidConfigError, "Shippex config not found"
config -> config
end
end
@doc """
Provides a method of returning all available carriers. This is based on
the config and does not include validation.
Shippex.carriers #=> [:ups]
"""
@spec carriers() :: [atom]
def carriers do
cfg = Shippex.config()
ups = if Keyword.get(cfg, :ups), do: :ups
fedex = if Keyword.get(cfg, :fedex), do: :fedex
usps = if Keyword.get(cfg, :usps), do: :usps
Enum.filter [ups, fedex, usps], fn (c) -> not is_nil(c) end
end
@doc """
Fetches the env atom for the config. Must be either `:dev` or `:prod`, or an
exception will be thrown.
config :shippex, :env, :dev
Shippex.env #=> :dev
"""
@spec env() :: atom
def env do
case Application.get_env(:shippex, :env, :dev) do
e when e in [:dev, :prod] -> e
_ -> raise InvalidConfigError, "Shippex env must be either :dev or :prod"
end
end
@doc """
Fetches rates from `carriers` for a given `Shipment`.
"""
@spec fetch_rates(Shipment.t, [atom]) :: [{atom, Rate.t}]
def fetch_rates(%Shippex.Shipment{} = shipment, carriers \\ :all) do
# Convert the atom to a list if necessary.
carriers = cond do
is_nil(carriers) -> [:all]
is_atom(carriers) -> [carriers]
is_list(carriers) -> carriers
true ->
raise """
#{inspect carriers} is an invalid carrier or list of carriers. Try using an atom. For example:
Shippex.fetch_rates(shipment, :ups)
"""
end
# Validate each carrier.
available_carriers = Shippex.carriers()
Enum.each carriers, fn (carrier) ->
unless Enum.any?(available_carriers, fn (c) -> c == carrier end) do
raise "#{inspect carrier} not found in #{inspect available_carriers}"
end
end
# TODO
rates = Shippex.Carrier.UPS.fetch_rates(shipment)
oks = Enum.filter rates, &(elem(&1, 0) == :ok)
errors = Enum.filter rates, &(elem(&1, 0) == :error)
Enum.sort(oks, fn (r1, r2) ->
{:ok, r1} = r1
{:ok, r2} = r2
r1.price < r2.price
end) ++ errors
end
@doc """
Fetches the rate for `shipment` for a specific `Service`. The `service` module
contains the `Carrier` and selected delivery speed.
Shippex.fetch_rate(shipment, service)
"""
@spec fetch_rate(Shipment.t, Service.t) :: {atom, Rate.t}
def fetch_rate(%Shippex.Shipment{} = shipment, %Shippex.Service{} = service) do
Shippex.Carrier.UPS.fetch_rate(shipment, service)
end
@doc """
Fetches the label for `shipment` for a specific `Service`. The `service`
module contains the `Carrier` and selected delivery speed.
Shippex.fetch_label(shipment, service)
"""
@spec fetch_label(Shipment.t, Service.t) :: {atom, Label.t}
def fetch_label(%Shippex.Shipment{} = shipment, %Shippex.Service{} = service) do
Shippex.Carrier.UPS.fetch_label(shipment, service)
end
@doc """
Cancels the shipment associated with `label`, if possible. The result is
returned in a tuple.
You may pass in either the label or tracking number.
case Shippex.cancel_shipment(label) do
{:ok, result} ->
IO.inspect(result) #=> %{code: "1", message: "Voided successfully."}
{:error, %{code: code, message: message}} ->
IO.inspect(code)
IO.inspect(message)
end
"""
@spec cancel_shipment(Label.t | String.t) :: {atom, response}
def cancel_shipment(%Shippex.Label{} = label) do
Shippex.Carrier.UPS.cancel_shipment(label.tracking_number)
end
def cancel_shipment(tracking_number) when is_bitstring(tracking_number) do
Shippex.Carrier.UPS.cancel_shipment(tracking_number)
end
@doc """
Performs address validation. If the address is completely invalid,
`{:error, result}` is returned. For addresses that may have typos,
`{:ok, candidates}` is returned. You can iterate through the list of
candidates to present to the end user. Addresses that pass validation
perfectly will still be in a `list` where `length(candidates) == 1`.
Note that the `candidates` returned will automatically pass through
`Shippex.Address.to_struct()` for casting.
address = Shippex.Address.to_struct(%{
name: "Earl G",
phone: "123-123-1234",
address: "9999 Hobby Lane",
address_line_2: nil,
city: "Austin",
state: "TX",
zip: "78703"
})
case Shippex.validate_address(address) do
{:error, %{code: code, message: message}} ->
# Present the error.
{:ok, candidates} ->
if length(candidates) == 1 do
# Use the address
else
# Present candidates to user for selection
end
end
"""
@spec validate_address(Address.t) :: {atom, response | [Address.t]}
def validate_address(%Shippex.Address{} = address) do
Shippex.Carrier.UPS.validate_address(address)
end
end