## ## Copyright (c) 2020 Kwame Yamgane. All rights reserved. ## ## Redistribution and use in source and binary forms, with or ## without modification, are permitted provided that the following ## conditions are met: ## ## 1. Redistributions of source code must retain the above copyright ## notice, this list of conditions and the following disclaimer. ## ## 2. Redistributions in binary form must reproduce the above ## copyright notice, this list of conditions and the following ## disclaimer in the documentation and/or other materials ## provided with the distribution. ## ## 3. All advertising materials mentioning features or use of this ## software must display the following acknowledgement:
This ## product includes software developed by the organization. ## ## 4. Neither the name of the copyright holder nor the names of its ## contributors may be used to endorse or promote products derived ## from this software without specific prior written permission. ## ## THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY ## EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ## THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A ## PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT ## HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ## EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED ## TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, ## OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR ## TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT ## OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY ## OF SUCH DAMAGE. defmodule Billcom do @moduledoc """ Simple api library to connect to bill.com api config your bill.com connection: config :billcom, :api, %{ devKey: "myDevKey", orgId: "myOrgId", password: "myPassword", userName: "myUsername", prod: true #optional: to request of bill.com production servers } Then use ... conn = Billcom.login Billcom.list_orgs(conn) ... """ @doc """ Login to bill.com api ## Parameters: none - the module load it's configuration from config file ## return: success - {:ok, conn} where conn is an updated version of the conn passed in parameters with sessionId and usersId failure - raise error """ @spec login! :: conn def login! do conn = get_conf() |> update_conn_url(:login) HTTPoison.start() case create_body(conn, :no_session) |> execute(conn) do {:ok, val} -> update_conn(val, conn, ["sessionId", "usersId"]) {:error, val} -> raise "Error: cannot login: #{to_string(val)}" end end @doc """ logout from Bill.com api ## Parameters: conn - see login ## return: success - {:ok, val} where data are logout data failure - {:error, val} where date are failure reasons """ @spec logout(conn) :: {atom(), map()} def logout(connection) do conn = update_conn_url(connection, :logout) create_body(conn) |> execute(conn) end @doc """ Return the list of organisation associated with your account ## Parameters: conn - see login ## return: success - {:ok, val} from where you can fetch organisation list (example: val |> Map.fetch!("response_data")) failure - {:error, val} where date are failure reasons """ @spec list_orgs(conn()) :: {atom(), map()} def list_orgs(connection) do conn = update_conn_url(connection, :list_orgs) create_body(conn) |> execute(conn) end @doc """ Check for the presence of a key in the result collections ## Parameters: result - result of a api call key - the key you are looking for ## return: success - true failure - false """ @spec has_key?(map(), String.t) :: atom() def has_key?(result, key) do result |> elem(1) |> Map.fetch!("response_data") |> Map.has_key?(key) end @doc """ Return the value of a key in the result collections ## Parameters: result - result collection of a api call key - the key from which to return a value ## return: success - value failure - unkown behavior see has_key? """ @spec get_val(map(), String.t) :: atom() def get_val(result, key) do result |> elem(1) |> Map.fetch!("response_data") |> Map.fetch!(key) end @api_function_list_data [ "RecordAPPayment", "VoidAPPayment", "CancelAPPayment", "GetAPSummary", "GetDisbursementData", "ListPayments", "GetCheckImageData", "SetApprovers", "ListApprovers", "ListUserApprovals", "Approve", "Deny", "ClearApprovers", "SendInvoice", "MailInvoice", "ChargeCustomer", "RecordARPayment", "GetARSummary", "SetCustomerAuthorization", "GetProfilePermissions", "GetBankBalance", "SetBankBalance", "UploadAttachment", "SendMessage", "GetDocumentPages", "ListMessage", "NetworkSearch", "SendVendorInvite", "SendInvite", "LargeBillerSearch", "GetLargeBillerPaymentAddress", "ConnectLargeBillerAsVendor", "GetNetworkStatus", "CancelInvite", "DisconnectVendorFromNetwork", "DisconnectCustomerFromNetwork", "MFAStatus", "GetObjectUrl", "SearchEntity", "GetEntityMetadata", "MFAChallenge", "MFAAuthenticate" ] for function <- @api_function_list_data do function_name = function |> String.replace(~r/([A-Z][a-z]+)/, "_\\1") |> String.downcase() |> String.slice(1..-1) @doc """ #{function} for bill.com api ## Parameters: conn - a connection structure (see Billcom.login/0) data - data object to send for the object ## return: success - {:ok, val} fail - {:error, val} """ @spec unquote(:"#{function_name}")(map(), map()) :: any def unquote(:"#{function_name}")(connection, data) do conn = Billcom.update_map(connection, :conn_url, connection.api_url <> "#{unquote(function)}.json") Billcom.create_body(conn, data) |> Billcom.execute(conn) end end @type conn :: %{ dev_key: String.t, org_id: String.t, password: String.t, user_name: String.t, session_id: String.t, api_url: String.t, conn_url: String.t } defmodule Conn do defstruct dev_key: "", org_id: "", password: "", user_name: "", session_id: "", api_url: "" end is_struct? = fn atom -> atom == :__struct__ end for field <- Conn.__struct__() |> Map.keys(), not is_struct?.(field) do field_str = Atom.to_string(field) @doc """ get_#{field_str} get object value #{field_str} ## Parameters: conn - actual connexion structure ## return: #{field_str} value """ @spec unquote(:"get_#{field_str}")(conn) :: String.t def unquote(:"get_#{field_str}")(conn) do Map.fetch!(conn, unquote(field)) end @doc """ set_#{field_str} set object "#{field_str}" value ## Parameters: conn - actual connexion structure val - value of the object ## return: new conn strucutre with #{field_str} set to val """ @spec unquote(:"set_#{field_str}")(conn, String.t) :: String.t def unquote(:"set_#{field_str}")(conn, val) do Map.put(conn, unquote(field), val) end end def execute(body, conn) do result = HTTPoison.post(conn.conn_url, URI.encode_query(body), %{"Content-Type" => "application/x-www-form-urlencoded"}) case result do {:ok, answer} -> answer _ -> raise "Cannot execute request" end |> Map.fetch!(:body) |> Poison.decode!() |> check_answer() end def update_map(map, key, val) do Map.update(map, key, val, fn _ -> val end) end def create_body(conn, :no_session) do %{ devKey: conn.dev_key, orgId: conn.org_id, password: conn.password, userName: conn.user_name } end def create_body(conn, data) do create_body(conn) |> Map.put_new(:data, Poison.encode!(data)) end def create_body(conn) do create_body(conn, :no_session) |> Map.put_new(:sessionId, conn.session_id) end defp get_conf do configuration = Application.fetch_env!(:billcom, :api) api_url = cond do Map.has_key?(configuration, :prod) == true -> "https://api.bill.com/api/v2/" true -> "https://api-sandbox.bill.com/api/v2" end %Conn{ dev_key: configuration.devKey, org_id: configuration.orgId, password: configuration.password, user_name: configuration.userName, api_url: api_url } end @json_url [ {:login, "Login.json"}, {:logout, "Logout.json"}, {:list_orgs, "ListOrgs.json"}, {:approve, "Approve.json"}, {:pay_bill, "PayBill.json"}, {:record_ap_payment, "RecordAPPayment.json"}, {:upload_attachment, "UploadAttachment.json"} ] for {token, json} <- @json_url do defp update_conn_url(conn, unquote(token)), do: update_map(conn, :conn_url, conn.api_url <> "/" <> unquote(json)) end defp check_answer(answer) do cond do Map.fetch!(answer, "response_status") == 0 -> {:ok, answer} Map.fetch!(answer, "response_status") != 0 -> {:error, answer} end end defp key_to_atom(key) do key |> String.replace(~r/([A-Z]\w+)/, "_\\1") |> String.downcase() |> String.to_atom() end defp update_conn(_, conn, []) do conn end defp update_conn(data, conn, [val | last]) do value = Map.fetch!(data, "response_data") |> Map.fetch!(val) new_conn = update_map(conn, key_to_atom(val), value) update_conn(data, new_conn, last) end defp update_conn(data, conn, val) do Map.update(conn, key_to_atom(val), Map.fetch(data, val), fn _ -> Map.fetch(data, val) end) end end