Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@ abacatepay-*.tar
# VSCode Elixir LS Extension
.elixir_ls/

# Environment files
.env*

# Priv
/priv/
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ customer_data = [
name: "Daniel Lima",
tax_id: "123.456.789-01",
email: "daniel_lima@abacatepay.com",
cellphone: "+5511999999999"
cellphone: "+5511999999999",
country: "BR",
zip_code: "01234-567",
metadata: %{notes: "Important Customer"}
]

{:ok, customer} = AbacatePay.Customer.create(customer_data)
Expand Down Expand Up @@ -153,7 +156,10 @@ Retrieve a list of all billings.
name: "Daniel Lima",
tax_id: "123.456.789-01",
email: "daniel_lima@abacatepay.com",
cellphone: "+5511999999999"
cellphone: "+5511999999999",
country: "BR",
zip_code: "01234-567",
metadata: %{notes: "Important Customer"},
}
```

Expand Down
26 changes: 26 additions & 0 deletions lib/abacatepay.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule AbacatePay do
@moduledoc """
Main module for the AbacatePay SDK.
"""

require Logger

@doc ~S"""
Updates the value of `key` in the configuration *at runtime*.

Once the `:abacatepay` application starts, it validates and caches the value of the
configuration options you start it with. Because of this, updating configuration
at runtime requires this function as opposed to just changing the application
environment.

> #### This Function Is Slow {: .warning}
>
> This function updates terms in [`:persistent_term`](`:persistent_term`), which is what
> this SDK uses to cache configuration. Updating terms in `:persistent_term` is slow
> and can trigger full GC sweeps. We recommend only using this function in rare cases,
> or during tests.
"""
@doc since: "0.3.0"
@spec put_config(atom(), term()) :: :ok
defdelegate put_config(key, value), to: AbacatePay.Config
end
33 changes: 25 additions & 8 deletions lib/abacatepay/api/customer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,44 @@ defmodule AbacatePay.Api.Customer do

## Examples

iex> AbacatePay.Api.Customer.create_customer(%{name: "Daniel Lima", cellphone: "(11) 4002-8922", email: "daniel.lima@example.com", taxId: "123.456.789-01"})
iex> AbacatePay.Api.Customer.create(%{name: "Daniel Lima", cellphone: "(11) 4002-8922", email: "daniel.lima@example.com", taxId: "123.456.789-01"})
{:ok, %{...}}
"""
@spec create_customer(body :: map()) :: {:ok, map()} | {:error, ApiError.t()} | {:error, any()}
def create_customer(body) do
@spec create(body :: map()) :: {:ok, map()} | {:error, ApiError.t()} | {:error, any()}
def create(body) do
HTTPClient.post(
"/customers/create",
body
)
end

@doc """
Gets a list of all customers.
Lists customers.

## Examples

iex> AbacatePay.Api.Customer.list_customers()
iex> AbacatePay.Api.Customer.list(%{page: 1, limit: 20})
{:ok, [%{...}, ...]}
"""
@spec list_customers() :: {:ok, list(map())} | {:error, ApiError.t()} | {:error, any()}
def list_customers do
HTTPClient.get("/customers/list")
@spec list(params :: map()) :: {:ok, list(), map()} | {:error, ApiError.t()} | {:error, any()}
def list(params \\ %{page: 1, limit: 20}) do
query = URI.encode_query(%{"page" => params.page, "limit" => params.limit})

HTTPClient.get("/customers/list?" <> query)
end

@doc """
Gets a customer by ID.

## Examples

iex> AbacatePay.Api.Customer.get("cust_aebxkhDZNaMmJeKsy0AHS0FQ")
{:ok, %{...}}
"""
@spec get(customer_id :: String.t()) :: {:ok, map()} | {:error, ApiError.t()} | {:error, any()}
def get(customer_id) do
query = URI.encode_query(%{"id" => customer_id})

HTTPClient.get("/customers/get?" <> query)
end
end
6 changes: 3 additions & 3 deletions lib/abacatepay/api/pix.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ defmodule AbacatePay.Api.Pix do

## Examples

iex> AbacatePay.Api.Pix.create_pix_qrcode(%{amount: 1000, description: "Payment for order #1234"})
iex> AbacatePay.Api.Pix.create(%{amount: 1000, description: "Payment for order #1234"})
{:ok, %{...}}
"""
@spec create_pix_qrcode(body :: map()) ::
@spec create(body :: map()) ::
{:ok, map()} | {:error, ApiError.t()} | {:error, any()}
def create_pix_qrcode(body) do
def create(body) do
HTTPClient.post(
"/pixQrCode/create",
body
Expand Down
5 changes: 5 additions & 0 deletions lib/abacatepay/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ defmodule AbacatePay.Application do

@moduledoc false

alias AbacatePay.Config

use Application

@impl true
def start(_type, _args) do
config = Config.validate!()
:ok = Config.persist(config)

children = [
# Start the Finch HTTP client
{Finch, name: AbacatePay.Finch}
Expand Down
135 changes: 135 additions & 0 deletions lib/abacatepay/config.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Forked from: https://github.com/getsentry/sentry-elixir/blob/master/lib/sentry/config.ex

defmodule AbacatePay.Config do
@moduledoc false

basic_options_schema = [
api_url: [
type: :string,
default: "https://api.abacatepay.com",
type_doc: "`t:String.t/0`",
required: false,
doc: "The base URL for the AbacatePay API."
],
api_version: [
type: :string,
default: "2",
type_doc: "`t:String.t/0`",
required: false,
doc: "The API version to use."
],
api_key: [
type: :string,
type_doc: "`t:String.t/0`",
doc: "The API key for authenticating with the AbacatePay API.",
required: false
]
]

@basic_options_schema NimbleOptions.new!(basic_options_schema)

@raw_opts_schema Enum.concat([
basic_options_schema
])

@opts_schema NimbleOptions.new!(@raw_opts_schema)
@valid_keys Keyword.keys(@raw_opts_schema)

@spec validate!() :: keyword()
def validate! do
:abacatepay
|> Application.get_all_env()
|> validate!()
end

@spec validate!(keyword()) :: keyword()
def validate!(config) when is_list(config) do
config_opts =
config
|> Keyword.take(@valid_keys)
|> fill_in_from_env(:api_url, "ABACATEPAY_API_URL")
|> fill_in_from_env(:api_version, "ABACATEPAY_API_VERSION")
|> fill_in_from_env(:api_key, "ABACATEPAY_API_KEY")

case NimbleOptions.validate(config_opts, @opts_schema) do
{:ok, opts} ->
opts

{:error, error} ->
raise ArgumentError, """
invalid configuration for the :abacatepay application, so we cannot start or update
its configuration. The error was:

#{Exception.message(error)}

See the documentation for the AbacatePay module for more information on configuration.
"""
end
end

@spec persist(keyword()) :: :ok
def persist(config) when is_list(config) do
Enum.each(config, fn {key, value} ->
:persistent_term.put({:abacatepay_config, key}, value)
end)
end

@spec docs() :: String.t()
def docs do
"""
#### Basic Options

#{NimbleOptions.docs(@basic_options_schema)}
"""
end

@spec api_url() :: String.t()
def api_url, do: get(:api_url)

@spec api_version() :: String.t()
def api_version, do: get(:api_version)

@spec api_key() :: String.t() | nil
def api_key, do: get(:api_key)

@spec put_config(atom(), term()) :: :ok
def put_config(key, value) when is_atom(key) do
unless key in @valid_keys do
raise ArgumentError, "unknown option #{inspect(key)}"
end

renamed_key =
case key do
:before_send_event -> :before_send
other -> other
end

[{key, value}]
|> validate!()
|> Keyword.take([renamed_key])
|> persist()
end

## Helpers

defp fill_in_from_env(config, key, system_key) do
case System.get_env(system_key) do
nil -> config
value -> Keyword.put_new(config, key, value)
end
end

@compile {:inline, get: 1}
defp get(key) do
# Check process dictionary first for test-specific config overrides.
# This allows tests to use put_test_config/1 for isolated configuration
# without affecting other tests, even when running async: true.
case Process.get({:abacatepay_test_config, key}, :__not_set__) do
:__not_set__ ->
:persistent_term.get({:abacatepay_config, key}, nil)

value ->
value
end
end
end
43 changes: 25 additions & 18 deletions lib/abacatepay/http_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,9 @@ defmodule AbacatePay.HTTPClient do
HTTP client for making requests to the AbacatePay API.
"""

@user_agent "Elixir-SDK (#{Mix.Project.config()[:source_url]}, #{Mix.Project.config()[:version]})"

defp api_url do
Application.get_env(:abacatepay, :api_url, "https://api.abacatepay.com")
end

defp api_version do
Application.get_env(:abacatepay, :api_version, "1")
end
alias AbacatePay.Config

defp api_key do
Application.get_env(:abacatepay, :api_key, nil)
end
@user_agent "Elixir-SDK (#{Mix.Project.config()[:source_url]}, #{Mix.Project.config()[:version]})"

@doc """
Performs a GET request to the API.
Expand All @@ -25,6 +15,7 @@ defmodule AbacatePay.HTTPClient do
iex> AbacatePay.Api.get("/customers/list")
{:ok, [%{...}, ...]}
"""
@spec get(path :: String.t()) :: {:ok, any(), map()} | {:error, any()}
def get(path) do
path
|> build_request(:get)
Expand All @@ -46,6 +37,7 @@ defmodule AbacatePay.HTTPClient do
iex> AbacatePay.Api.post("/customers/create", %{name: "Daniel Lima", cellphone: "(11) 4002-8922", email: "daniel_lima@abacatepay.com", taxId: "123.456.789-01"})
{:ok, %{...}}
"""
@spec post(path :: String.t(), body :: map()) :: {:ok, any()} | {:error, any()}
def post(path, body) do
path
|> build_request(:post, body)
Expand All @@ -62,6 +54,7 @@ defmodule AbacatePay.HTTPClient do
@doc """
Performs a PUT request to the API.
"""
@spec put(path :: String.t(), body :: map()) :: {:ok, any()} | {:error, any()}
def put(path, body) do
path
|> build_request(:put, body)
Expand All @@ -78,6 +71,7 @@ defmodule AbacatePay.HTTPClient do
@doc """
Performs a DELETE request to the API.
"""
@spec delete(path :: String.t()) :: {:ok, any()} | {:error, any()}
def delete(path) do
path
|> build_request(:delete)
Expand All @@ -101,6 +95,9 @@ defmodule AbacatePay.HTTPClient do
case Jason.decode(body) do
{:ok, decoded} ->
case decoded do
%{"data" => data, "pagination" => pagination} when data != nil and pagination != nil ->
{:ok, data, pagination}

%{"data" => data} when data != nil ->
{:ok, data}

Expand All @@ -112,35 +109,45 @@ defmodule AbacatePay.HTTPClient do
%AbacatePay.ApiError{status_code: status, message: "Unexpected response format"}}
end

{:error, %Jason.DecodeError{data: error_message}} ->
{:error, %AbacatePay.ApiError{status_code: status, message: error_message}}
{:error, %Jason.DecodeError{data: message}} ->
{:error, %AbacatePay.ApiError{status_code: status, message: message}}
end
end

@doc false
@spec build_request(path :: String.t(), method :: atom()) :: Finch.Request.t()
defp build_request(path, method) do
url = "#{api_url()}/v#{api_version()}" <> path
api_url = Config.api_url()
api_version = Config.api_version()
api_key = Config.api_key()

url = "#{api_url}/v#{api_version}" <> path

headers =
[
{"Content-Type", "application/json"},
{"Accept", "application/json"},
{"User-Agent", @user_agent}
] ++ if api_key(), do: [{"Authorization", "Bearer #{api_key()}"}], else: []
] ++ if api_key, do: [{"Authorization", "Bearer #{api_key}"}], else: []

Finch.build(method, url, headers)
end

@doc false
@spec build_request(path :: String.t(), method :: atom(), body :: map()) :: Finch.Request.t()
defp build_request(path, method, body) do
url = "#{api_url()}/v#{api_version()}" <> path
api_url = Config.api_url()
api_version = Config.api_version()
api_key = Config.api_key()

url = "#{api_url}/v#{api_version}" <> path

headers =
[
{"Content-Type", "application/json"},
{"Accept", "application/json"},
{"User-Agent", @user_agent}
] ++ if api_key(), do: [{"Authorization", "Bearer #{api_key()}"}], else: []
] ++ if api_key, do: [{"Authorization", "Bearer #{api_key}"}], else: []

Finch.build(method, url, headers, Jason.encode!(body))
end
Expand Down
Loading
Loading