diff --git a/.gitignore b/.gitignore index 4a7871f..d2981c9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,8 @@ abacatepay-*.tar # VSCode Elixir LS Extension .elixir_ls/ +# Environment files +.env* + # Priv /priv/ \ No newline at end of file diff --git a/README.md b/README.md index c493ddc..37b6b82 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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"}, } ``` diff --git a/lib/abacatepay.ex b/lib/abacatepay.ex new file mode 100644 index 0000000..db76c7c --- /dev/null +++ b/lib/abacatepay.ex @@ -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 diff --git a/lib/abacatepay/api/customer.ex b/lib/abacatepay/api/customer.ex index c3a6039..0d6d3a7 100644 --- a/lib/abacatepay/api/customer.ex +++ b/lib/abacatepay/api/customer.ex @@ -10,11 +10,11 @@ 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 @@ -22,15 +22,32 @@ defmodule AbacatePay.Api.Customer do 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 diff --git a/lib/abacatepay/api/pix.ex b/lib/abacatepay/api/pix.ex index 0e14394..34210ec 100644 --- a/lib/abacatepay/api/pix.ex +++ b/lib/abacatepay/api/pix.ex @@ -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 diff --git a/lib/abacatepay/application.ex b/lib/abacatepay/application.ex index ca3c796..9257270 100644 --- a/lib/abacatepay/application.ex +++ b/lib/abacatepay/application.ex @@ -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} diff --git a/lib/abacatepay/config.ex b/lib/abacatepay/config.ex new file mode 100644 index 0000000..3e10c92 --- /dev/null +++ b/lib/abacatepay/config.ex @@ -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 diff --git a/lib/abacatepay/http_client.ex b/lib/abacatepay/http_client.ex index cb44baa..85e5c1a 100644 --- a/lib/abacatepay/http_client.ex +++ b/lib/abacatepay/http_client.ex @@ -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. @@ -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) @@ -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) @@ -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) @@ -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) @@ -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} @@ -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 diff --git a/lib/abacatepay/pagination.ex b/lib/abacatepay/pagination.ex new file mode 100644 index 0000000..6c77f52 --- /dev/null +++ b/lib/abacatepay/pagination.ex @@ -0,0 +1,96 @@ +defmodule AbacatePay.Pagination do + @moduledoc ~S""" + Struct representing pagination information in API responses. + """ + + defstruct [ + :page, + :limit, + :items, + :total_pages, + :has_next, + :has_previous, + :next_cursor + ] + + @typedoc "Current page." + @type page :: integer() | nil + + @typedoc "Number of items per page." + @type limit :: integer() | nil + + @typedoc "Number of items." + @type items :: integer() | nil + + @typedoc "Number of total pages." + @type total_pages :: integer() | nil + + @typedoc "Indicates whether there is a next page." + @type has_next :: boolean() | nil + + @typedoc "Indicates whether there is a previous page." + @type has_previous :: boolean() | nil + + @typedoc "Cursor for the next page." + @type next_cursor :: String.t() | nil + + @typedoc "A `AbacatePay.Pagination` that has a `pagination` field (Not cursor based)." + @type pagination :: %__MODULE__{ + page: page, + limit: limit, + items: items, + total_pages: total_pages, + has_next: nil, + has_previous: nil, + next_cursor: nil + } + + @typedoc "A `AbacatePay.Pagination` that has a `pagination` field and is cursor-based." + @type cursor_based_pagination :: %__MODULE__{ + page: nil, + limit: nil, + items: nil, + total_pages: nil, + has_next: has_next, + has_previous: has_previous, + next_cursor: next_cursor + } + + @type t :: pagination | cursor_based_pagination + + @doc """ + Builds a `AbacatePay.Pagination` struct from raw API response data. + """ + @spec build_struct(raw_data :: map()) :: {:ok, t()} + def build_struct(raw_data) do + pretty_fields = %AbacatePay.Pagination{ + page: Map.get(raw_data, "page"), + limit: Map.get(raw_data, "limit"), + items: Map.get(raw_data, "items"), + total_pages: Map.get(raw_data, "totalPages"), + has_next: Map.get(raw_data, "hasNext"), + has_previous: Map.get(raw_data, "hasPrevious"), + next_cursor: Map.get(raw_data, "nextCursor") + } + + {:ok, pretty_fields} + end + + @doc """ + Builds raw pagination data from API response data. + """ + @spec build_raw(pagination :: map()) :: {:ok, map()} + def build_raw(pagination) do + raw = %{ + page: pagination.page, + limit: pagination.limit, + items: pagination.items, + totalPages: pagination.total_pages, + hasNext: pagination.has_next, + hasPrevious: pagination.has_previous, + nextCursor: pagination.next_cursor + } + + {:ok, raw} + end +end diff --git a/lib/abacatepay/schemas/customer.ex b/lib/abacatepay/schemas/customer.ex index a87c88e..88e2940 100644 --- a/lib/abacatepay/schemas/customer.ex +++ b/lib/abacatepay/schemas/customer.ex @@ -6,6 +6,14 @@ defmodule AbacatePay.Schema.Customer do name: [type: :string, required: true], cellphone: [type: :string, required: true], email: [type: :string, required: true], - tax_id: [type: :string, required: true] + tax_id: [type: :string, required: true], + zip_code: [type: :string, required: false], + metadata: [type: :map, required: false] + ] + + def list_customers_request, + do: [ + page: [type: :integer, default: 1, required: false], + limit: [type: :integer, default: 20, required: false] ] end diff --git a/lib/abacatepay/struct/billing.ex b/lib/abacatepay/struct/billing.ex index cc3a5c6..32b7aff 100644 --- a/lib/abacatepay/struct/billing.ex +++ b/lib/abacatepay/struct/billing.ex @@ -157,7 +157,7 @@ defmodule AbacatePay.Billing do parsed_customer = with %Customer{} = customer_struct <- validated_options[:customer], - {:ok, customer_map} <- Customer.build_api_customer(customer_struct) do + {:ok, customer_map} <- Customer.build_raw(customer_struct) do customer_map.metadata else _ -> nil @@ -242,7 +242,7 @@ defmodule AbacatePay.Billing do pretty_customer = with customer_data when is_map(customer_data) <- raw_data["customer"], - {:ok, customer_struct} <- Customer.build_pretty_customer(customer_data) do + {:ok, customer_struct} <- Customer.build_struct(customer_data) do customer_struct else _ -> nil @@ -332,7 +332,7 @@ defmodule AbacatePay.Billing do def build_api_billing(pretty_billing) do customer = with %Customer{} = customer_struct <- pretty_billing.customer, - {:ok, customer_map} <- Customer.build_api_customer(customer_struct) do + {:ok, customer_map} <- Customer.build_raw(customer_struct) do customer_map else _ -> nil diff --git a/lib/abacatepay/struct/customer.ex b/lib/abacatepay/struct/customer.ex index f58ce32..f0425d0 100644 --- a/lib/abacatepay/struct/customer.ex +++ b/lib/abacatepay/struct/customer.ex @@ -3,37 +3,57 @@ defmodule AbacatePay.Customer do Struct representing an AbacatePay customer. """ - alias AbacatePay.{Api, Schema} + alias AbacatePay.{Api, ApiError, Pagination, Schema} defstruct [ :id, + :dev_mode, + :country, :name, :cellphone, :tax_id, - :email + :zip_code, + :email, + :metadata ] - @typedoc "The customer unique ID in AbacatePay." + @typedoc "Unique customer identifier." @type id :: String.t() - @typedoc "The customer name." + @typedoc "Indicates whether the client was created in a testing environment." + @type dev_mode :: boolean() | nil + + @typedoc "Customer country." + @type country :: String.t() | nil + + @typedoc "Customer's full name." @type name :: String.t() | nil - @typedoc "The customer cellphone." + @typedoc "Customer's cell phone." @type cellphone :: String.t() | nil - @typedoc "The customer tax ID (CPF or CNPJ)." + @typedoc "Customer's CPF or CNPJ." @type tax_id :: String.t() | nil - @typedoc "The customer email." + @typedoc "Customer's email." @type email :: String.t() + @typedoc "Customer zip code." + @type zip_code :: String.t() | nil + + @typedoc "Additional customer metadata." + @type metadata :: map() | nil + @type t :: %__MODULE__{ - id: id, - name: name, - cellphone: cellphone, - tax_id: tax_id, - email: email + id: id(), + dev_mode: dev_mode(), + country: country(), + name: name(), + cellphone: cellphone(), + tax_id: tax_id(), + email: email(), + zip_code: zip_code(), + metadata: metadata() } @doc """ @@ -44,7 +64,9 @@ defmodule AbacatePay.Customer do name: "Daniel Lima", cellphone: "(11) 4002-8922", email: "daniel_lima@abacatepay.com", - tax_id: "123.456.789-01" + tax_id: "123.456.789-01", + zip_code: "12345-678", + metadata: %{key: "value"} ]) {:ok, %AbacatePay.Customer{id: "cust_aebxkhDZNaMmJeKsy0AHS0FQ", name: "Daniel Lima", ...}} @@ -59,13 +81,15 @@ defmodule AbacatePay.Customer do name: validated_options[:name], cellphone: validated_options[:cellphone], email: validated_options[:email], - taxId: validated_options[:tax_id] + taxId: validated_options[:tax_id], + zipCode: validated_options[:zip_code], + metadata: validated_options[:metadata] } |> Enum.reject(fn {_k, v} -> is_nil(v) end) |> Enum.into(%{}) - with {:ok, response} <- Api.Customer.create_customer(body) do - build_pretty_customer(response) + with {:ok, response} <- Api.Customer.create(body) do + build_struct(response) end {:error, %NimbleOptions.ValidationError{} = error} -> @@ -74,48 +98,89 @@ defmodule AbacatePay.Customer do end @doc """ - Gets a list of all customers. + Gets a customer by ID. ## Examples - iex> AbacatePay.Customer.list() - {:ok, [%AbacatePay.Customer{id: "cust_aebxkhDZNaMmJeKsy0AHS0FQ", ...}, ...]} + iex> AbacatePay.Customer.get("cust_aebxkhDZNaMmJeKsy0AHS0FQ") + {:ok, %AbacatePay.Customer{id: "cust_aebxkhDZNaMmJeKsy0AHS0FQ", name: "Daniel Lima", ...}} + + Options: \n#{NimbleOptions.docs(Schema.Customer.list_customers_request())} """ - @spec list() :: {:ok, [t()]} | {:error, any()} - def list do - with {:ok, data_list} <- Api.Customer.list_customers() do + @spec get(customer_id :: id()) :: {:ok, t()} | {:error, ApiError.t()} | {:error, any()} + def get(customer_id) do + with {:ok, response} <- Api.Customer.get(customer_id) do + build_struct(response) + end + end + + @doc """ + Gets a list of customers with pagination options. + + ## Examples + iex> AbacatePay.Customer.list(page: 2, limit: 10) + {:ok, [%AbacatePay.Customer{id: "cust_aebxkhDZNaMmJeKsy0AHS0FQ", name: "Daniel Lima", ...}, ...], %{page: 2, limit: 10}} + """ + @spec list(options :: keyword()) :: + {:ok, list(t()), map()} | {:error, ApiError.t()} | {:error, any()} + def list(options) do + {:ok, validated_options} = + NimbleOptions.validate(options, Schema.Customer.list_customers_request()) + + with {:ok, data_list, pagination} <- + Api.Customer.list(%{page: validated_options[:page], limit: validated_options[:limit]}) do pretty_customers = data_list - |> Enum.map(&build_pretty_customer/1) + |> Enum.map(&build_struct/1) |> Enum.map(fn {:ok, customer} -> customer end) - {:ok, pretty_customers} + {:ok, pretty_pagination} = Pagination.build_struct(pagination) + + {:ok, pretty_customers, pretty_pagination} end end + @doc """ + Gets a list of customers with default pagination (page: 1, limit: 20). + + ## Examples + iex> AbacatePay.Customer.list() + {:ok, [%AbacatePay.Customer{id: "cust_aebxkhDZNaMmJeKsy0AHS0FQ", name: "Daniel Lima", ...}, ...], %{page: 1, limit: 20}} + """ + @spec list() :: {:ok, list(t()), map()} | {:error, ApiError.t()} | {:error, any()} + def list do + list(page: 1, limit: 20) + end + @doc """ Builds a `AbacatePay.Customer` struct from raw API data. ## Examples iex> raw_data = %{ ...> "id" => "cust_aebxkhDZNaMmJeKsy0AHS0FQ", - ...> "metadata" => %{ - ...> "name" => "Daniel Lima", - ...> "cellphone" => "(11) 4002-8922", - ...> "email" => "daniel_lima@abacatepay.com", - ...> "taxId" => "123.456.789-01" - ...> } + ...> "name" => "Daniel Lima", + ...> "cellphone" => "(11) 4002-8922", + ...> "email" => "daniel_lima@abacatepay.com" + ...> "taxId" => "123.456.789-01", + ...> "devMode" => false, + ...> "country" => "BR", + ...> "zipCode" => "12345-678", + ...> "metadata" => %{"key" => "value"} ...> } - iex> AbacatePay.Customer.build_pretty_customer(raw_data) + iex> AbacatePay.Customer.build_struct(raw_data) {:ok, %AbacatePay.Customer{id: "cust_aebxkhDZNaMmJeKsy0AHS0FQ", name: "Daniel Lima", ...}} """ - @spec build_pretty_customer(raw_data :: map()) :: {:ok, t()} - def build_pretty_customer(raw_data) do + @spec build_struct(raw_data :: map()) :: {:ok, t()} + def build_struct(raw_data) do pretty_fields = %AbacatePay.Customer{ - id: raw_data["id"], - name: get_in(raw_data, ["metadata", "name"]), - cellphone: get_in(raw_data, ["metadata", "cellphone"]), - tax_id: get_in(raw_data, ["metadata", "taxId"]), - email: get_in(raw_data, ["metadata", "email"]) + id: Map.get(raw_data, "id"), + name: Map.get(raw_data, "name"), + cellphone: Map.get(raw_data, "cellphone"), + tax_id: Map.get(raw_data, "taxId"), + email: Map.get(raw_data, "email"), + dev_mode: Map.get(raw_data, "devMode"), + country: Map.get(raw_data, "country"), + zip_code: Map.get(raw_data, "zipCode"), + metadata: build_struct_metadata(Map.get(raw_data, "metadata")) } {:ok, pretty_fields} @@ -128,33 +193,55 @@ defmodule AbacatePay.Customer do iex> customer = %AbacatePay.Customer{ ...> id: "cust_aebxkhDZNaMmJeKsy0AHS0FQ", ...> name: "Daniel Lima", + ...> dev_mode: false, + ...> country: "BR", ...> cellphone: "(11) 4002-8922", ...> email: "daniel_lima@abacatepay.com", - ...> tax_id: "123.456.789-01" + ...> tax_id: "123.456.789-01", + ...> zip_code: "12345-678", + ...> metadata: %{key: "value"} ...> } - iex> AbacatePay.Customer.build_api_customer(customer) + iex> AbacatePay.Customer.build_raw(customer) {:ok, %{ id: "cust_aebxkhDZNaMmJeKsy0AHS0FQ", - metadata: %{ - name: "Daniel Lima", - cellphone: "(11) 4002-8922", - taxId: "123.456.789-01", - email: "daniel_lima@abacatepay.com" - } + name: "Daniel Lima", + devMode: false, + country: "BR", + cellphone: "(11) 4002-8922", + taxId: "123.456.789-01", + email: "daniel_lima@abacatepay.com", + zipCode: "12345-678", + metadata: %{"key" => "value"} }} """ - @spec build_api_customer(pretty_customer :: t()) :: {:ok, map()} - def build_api_customer(pretty_customer) do - api_fields = %{ - id: pretty_customer.id, - metadata: %{ - name: pretty_customer.name, - cellphone: pretty_customer.cellphone, - taxId: pretty_customer.tax_id, - email: pretty_customer.email - } + @spec build_raw(customer :: t()) :: {:ok, map()} + def build_raw(customer) do + raw = %{ + id: customer.id, + name: customer.name, + devMode: customer.dev_mode, + country: customer.country, + cellphone: customer.cellphone, + taxId: customer.tax_id, + email: customer.email, + zipCode: customer.zip_code, + metadata: build_raw_metadata(customer.metadata) } - {:ok, api_fields} + {:ok, raw} end + + @doc false + defp build_struct_metadata(nil), do: nil + + @doc false + defp build_struct_metadata(metadata) when is_map(metadata), + do: Map.new(metadata, fn {k, v} -> {String.to_atom(k), v} end) + + @doc false + defp build_raw_metadata(nil), do: nil + + @doc false + defp build_raw_metadata(metadata) when is_map(metadata), + do: Map.new(metadata, fn {k, v} -> {Atom.to_string(k), v} end) end diff --git a/lib/abacatepay/struct/pix.ex b/lib/abacatepay/struct/pix.ex index 155d794..4c0de9f 100644 --- a/lib/abacatepay/struct/pix.ex +++ b/lib/abacatepay/struct/pix.ex @@ -1,6 +1,6 @@ defmodule AbacatePay.Pix do @moduledoc ~S""" - Struct representing an AbacatePay Pix QR Code. + Struct representing an AbacatePay QR Code Pix. """ alias AbacatePay.{Api, Customer, Schema, Util} @@ -10,41 +10,34 @@ defmodule AbacatePay.Pix do :amount, :status, :dev_mode, - :customer, :br_code, :br_code_base_64, :platform_fee, - :description, :created_at, :updated_at, - :metadata, - :expires_at, - :expires_in + :expires_at ] - @typedoc "Unique billing identifier." + @typedoc "Unique QRCode PIX identifier." @type id :: String.t() @typedoc "Charge amount in cents (e.g. 4000 = R$40.00)." @type amount :: integer() @typedoc """ - Billing status. Can be one of the following: + PIX status. Can be one of the following: - - `:pending` - The Pix QRCode is pending payment. - - `:expired` - The Pix QRCode has expired. - - `:cancelled` - The Pix QRCode has been cancelled. - - `:paid` - The Pix QRCode has been paid. - - `:refunded` - The Pix QRCode payment has been refunded. + - `:pending`: The PIX QR Code has been created but not yet paid. + - `:expired`: The PIX QR Code has expired without being paid. + - `:cancelled`: The PIX QR Code has been cancelled. + - `:paid`: The PIX QR Code has been paid. + - `:refunded`: The payment for the PIX QR Code has been refunded. """ @type status :: :pending | :expired | :cancelled | :paid | :refunded @typedoc "Indicates whether the charge is in a testing (true) or production (false) environment." @type dev_mode :: boolean() - @typedoc "Customer associated with the Pix QRCode Payment." - @type customer :: Customer.t() | nil - @typedoc "PIX code (copy-and-paste) for payment." @type br_code :: String.t() @@ -54,9 +47,6 @@ defmodule AbacatePay.Pix do @typedoc "Platform fee in cents. Example: 80 means R$0.80." @type platform_fee :: integer() - @typedoc "Payment description." - @type description :: String.t() - @typedoc "QRCode PIX creation date and time." @type created_at :: DateTime.t() @@ -74,7 +64,6 @@ defmodule AbacatePay.Pix do br_code: br_code, br_code_base_64: br_code_base_64, platform_fee: platform_fee, - description: description, created_at: created_at, updated_at: updated_at, expires_at: expires_at @@ -106,7 +95,7 @@ defmodule AbacatePay.Pix do {:ok, validated_options} -> parsed_customer = with %Customer{} = customer_struct <- validated_options[:customer], - {:ok, customer_map} <- Customer.build_api_customer(customer_struct) do + {:ok, customer_map} <- Customer.build_raw(customer_struct) do customer_map.metadata else _ -> nil @@ -123,8 +112,8 @@ defmodule AbacatePay.Pix do |> Enum.reject(fn {_k, v} -> is_nil(v) end) |> Enum.into(%{}) - with {:ok, response} <- Api.Pix.create_pix_qrcode(body) do - build_pretty_pix_qrcode(response) + with {:ok, response} <- Api.Pix.create(body) do + build_struct(response) end {:error, %NimbleOptions.ValidationError{} = error} -> @@ -143,7 +132,7 @@ defmodule AbacatePay.Pix do {:ok, t()} | {:error, any()} def simulate_payment(id, metadata \\ %{}) do with {:ok, response} <- Api.Pix.simulate_payment(id, metadata) do - build_pretty_pix_qrcode(response) + build_struct(response) end end @@ -152,12 +141,12 @@ defmodule AbacatePay.Pix do ## Examples iex> AbacatePay.Pix.check_status("pix_charabc123456789") - {:ok, %AbacatePay.Pix{status: :pending, expires_at: "2026-01-01T12:00:00Z"}} + {:ok, %AbacatePay.Pix{status: :pending, expires_at: ~U[2026-01-01T12:00:00Z]}} """ @spec check_status(id :: id()) :: {:ok, t()} | {:error, any()} def check_status(id) do with {:ok, response} <- Api.Pix.check_status(id) do - build_pretty_pix_qrcode(response) + build_struct(response) end end @@ -173,12 +162,11 @@ defmodule AbacatePay.Pix do ...> "brCode" => "00020126360014BR.GOV.BCB.PIX0136+551199999999520400005303986540415005802BR5925Fulano de Tal6009Sao Paulo61080540900062070503***63041D3D", ...> "brCodeBase64" => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAYAAAC0...", ...> "platformFee" => 80, - ...> "description" => "PIX Payment for order #1234", ...> "createdAt" => "2026-01-01T12:00:00Z", ...> "updatedAt" => "2026-01-01T12:05:00Z", ...> "expiresAt" => "2026-01-02T12:00:00Z" ...> } - iex> AbacatePay.Pix.build_pretty_pix_qrcode(raw_data) + iex> AbacatePay.Pix.build_struct(raw_data) {:ok, %AbacatePay.Pix{ id: "pix_charabc123456789", amount: 1500, @@ -187,31 +175,16 @@ defmodule AbacatePay.Pix do br_code: "00020126360014BR.GOV.BCB.PIX0136+551199999999520400005303986540415005802BR5925Fulano de Tal6009Sao Paulo61080540900062070503***63041D3D", br_code_base_64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAYAAAC0...", platform_fee: 80, - description: "PIX Payment for order #1234", created_at: ~U[2026-01-01T12:00:00Z], updated_at: ~U[2026-01-01T12:05:00Z], expires_at: ~U[2026-01-02T12:00:00Z] }} """ - @spec build_pretty_pix_qrcode(raw_data :: map()) :: {:ok, t()} - def build_pretty_pix_qrcode(raw_data) do - created_at = - case raw_data["createdAt"] do - nil -> nil - datetime_str -> DateTime.from_iso8601(datetime_str) |> elem(1) - end - - updated_at = - case raw_data["updatedAt"] do - nil -> nil - datetime_str -> DateTime.from_iso8601(datetime_str) |> elem(1) - end - - expires_at = - case raw_data["expiresAt"] do - nil -> nil - datetime_str -> DateTime.from_iso8601(datetime_str) |> elem(1) - end + @spec build_struct(raw_data :: map()) :: {:ok, t()} + def build_struct(raw_data) do + created_at = build_struct_datetime(raw_data["createdAt"]) + updated_at = build_struct_datetime(raw_data["updatedAt"]) + expires_at = build_struct_datetime(raw_data["expiresAt"]) pretty_fields = %AbacatePay.Pix{ id: raw_data["id"], @@ -221,7 +194,6 @@ defmodule AbacatePay.Pix do br_code: raw_data["brCode"], br_code_base_64: raw_data["brCodeBase64"], platform_fee: raw_data["platformFee"], - description: raw_data["description"], created_at: created_at, updated_at: updated_at, expires_at: expires_at @@ -242,12 +214,11 @@ defmodule AbacatePay.Pix do ...> br_code: "00020126360014BR.GOV.BCB.PIX0136+551199999999520400005303986540415005802BR5925Fulano de Tal6009Sao Paulo61080540900062070503***63041D3D", ...> br_code_base_64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAYAAAC0...", ...> platform_fee: 80, - ...> description: "PIX Payment for order #1234", ...> created_at: ~U[2026-01-01T12:00:00Z], ...> updated_at: ~U[2026-01-01T12:05:00Z], ...> expires_at: ~U[2026-01-02T12:00:00Z] ...> } - iex> AbacatePay.Pix.build_api_pix_qrcode(pix_qrcode) + iex> AbacatePay.Pix.build_raw(pix_qrcode) {:ok, %{ id: "pix_charabc123456789", amount: 1500, @@ -256,41 +227,25 @@ defmodule AbacatePay.Pix do brCode: "00020126360014BR.GOV.BCB.PIX0136+551199999999520400005303986540415005802BR5925Fulano de Tal6009Sao Paulo61080540900062070503***63041D3D", brCodeBase64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAYAAAC0...", platformFee: 80, - description: "PIX Payment for order #1234", createdAt: "2026-01-01T12:00:00Z", updatedAt: "2026-01-01T12:05:00Z", expiresAt: "2026-01-02T12:00:00Z" }} """ - @spec build_api_pix_qrcode(pretty_pix_qrcode :: t()) :: {:ok, map()} - def build_api_pix_qrcode(pretty_pix_qrcode) do - created_at = - case pretty_pix_qrcode.created_at do - nil -> nil - datetime -> DateTime.to_iso8601(datetime) - end - - updated_at = - case pretty_pix_qrcode.updated_at do - nil -> nil - datetime -> DateTime.to_iso8601(datetime) - end - - expires_at = - case pretty_pix_qrcode.expires_at do - nil -> nil - datetime -> DateTime.to_iso8601(datetime) - end + @spec build_raw(pix :: t()) :: {:ok, map()} + def build_raw(pix) do + created_at = build_raw_datetime(pix.created_at) + updated_at = build_raw_datetime(pix.updated_at) + expires_at = build_raw_datetime(pix.expires_at) api_fields = %{ - id: pretty_pix_qrcode.id, - amount: pretty_pix_qrcode.amount, - status: Util.normalize_atom(pretty_pix_qrcode.status), - devMode: pretty_pix_qrcode.dev_mode, - brCode: pretty_pix_qrcode.br_code, - brCodeBase64: pretty_pix_qrcode.br_code_base_64, - platformFee: pretty_pix_qrcode.platform_fee, - description: pretty_pix_qrcode.description, + id: pix.id, + amount: pix.amount, + status: Util.normalize_atom(pix.status), + devMode: pix.dev_mode, + brCode: pix.br_code, + brCodeBase64: pix.br_code_base_64, + platformFee: pix.platform_fee, createdAt: created_at, updatedAt: updated_at, expiresAt: expires_at @@ -298,4 +253,16 @@ defmodule AbacatePay.Pix do {:ok, api_fields} end + + @doc false + defp build_struct_datetime(nil), do: nil + + @doc false + defp build_struct_datetime(datetime_str), do: DateTime.from_iso8601(datetime_str) |> elem(1) + + @doc false + defp build_raw_datetime(nil), do: nil + + @doc false + defp build_raw_datetime(datetime), do: DateTime.to_iso8601(datetime) end diff --git a/mix.exs b/mix.exs index a59259e..d75eb50 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule AbacatePay.MixProject do @app :abacatepay @description "Official AbacatePay Elixir SDK to integrate payments via PIX in a simple, secure and fast way." @name "AbacatePay" - @version "0.2.0" + @version "0.3.0" @source_url "https://github.com/AbacatePay/abacatepay-elixir-sdk" def project do @@ -24,7 +24,7 @@ defmodule AbacatePay.MixProject do ] end - defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(:test), do: ["lib", "test/support"] ++ elixirc_paths(:dev) defp elixirc_paths(_), do: ["lib"] # Run "mix help compile.app" to learn about applications. diff --git a/test/abacatepay/api/customer_test.exs b/test/abacatepay/api/customer_test.exs index 5d5b9dd..8de6421 100644 --- a/test/abacatepay/api/customer_test.exs +++ b/test/abacatepay/api/customer_test.exs @@ -1,5 +1,5 @@ defmodule AbacatePay.Api.CustomerTest do - use ExUnit.Case + use ExUnit.Case, async: true use Mimic alias AbacatePay.Api.Customer @@ -7,33 +7,41 @@ defmodule AbacatePay.Api.CustomerTest do @default_customer_id "cust_aebxkhDZNaMmJeKsy0AHS0FQ" - describe "create_customer/1" do + describe "create/1" do test "creates a customer successfully" do body = %{ name: "Daniel Lima", cellphone: "(11) 4002-8922", email: "daniel_lima@abacatepay.com", - taxId: "123.456.789-01" + taxId: "123.456.789-01", + country: "BR", + zipCode: "12345-678", + metadata: %{"key" => "value"} } expected_response = %{ "id" => @default_customer_id, - "metadata" => %{ - "name" => "Daniel Lima", - "cellphone" => "(11) 4002-8922", - "email" => "daniel_lima@abacatepay.com", - "taxId" => "123.456.789-01" - } + "name" => "Daniel Lima", + "cellphone" => "(11) 4002-8922", + "email" => "daniel_lima@abacatepay.com", + "taxId" => "123.456.789-01", + "country" => "BR", + "zipCode" => "12345-678", + "devMode" => false, + "metadata" => %{"key" => "value"} } MockHTTPServer.stub_post("/customers/create", body, expected_response) - assert {:ok, customer} = Customer.create_customer(body) + assert {:ok, customer} = Customer.create(body) assert customer["id"] == @default_customer_id - assert customer["metadata"]["name"] == "Daniel Lima" - assert customer["metadata"]["email"] == "daniel_lima@abacatepay.com" - assert customer["metadata"]["cellphone"] == "(11) 4002-8922" - assert customer["metadata"]["taxId"] == "123.456.789-01" + assert customer["name"] == "Daniel Lima" + assert customer["email"] == "daniel_lima@abacatepay.com" + assert customer["cellphone"] == "(11) 4002-8922" + assert customer["taxId"] == "123.456.789-01" + assert customer["country"] == "BR" + assert customer["zipCode"] == "12345-678" + assert customer["devMode"] == false end test "handles error response when creating customer" do @@ -45,54 +53,120 @@ defmodule AbacatePay.Api.CustomerTest do error = MockHTTPServer.mock_error(422, "Invalid email format") MockHTTPServer.stub_error(:post, "/customers/create", error) - assert {:error, returned_error} = Customer.create_customer(body) + assert {:error, returned_error} = Customer.create(body) assert returned_error.status_code == 422 end end - describe "list_customers/0" do + describe "get/1" do + test "retrieves a customer successfully" do + expected_response = %{ + "id" => @default_customer_id, + "name" => "Daniel Lima", + "cellphone" => "(11) 4002-8922", + "email" => "daniel_lima@abacatepay.com", + "taxId" => "123.456.789-01", + "country" => "BR", + "zipCode" => "12345-678", + "devMode" => false, + "metadata" => %{"key" => "value"} + } + + MockHTTPServer.stub_get("/customers/get?id=#{@default_customer_id}", expected_response) + assert {:ok, customer} = Customer.get(@default_customer_id) + assert customer["id"] == @default_customer_id + assert customer["name"] == "Daniel Lima" + assert customer["email"] == "daniel_lima@abacatepay.com" + assert customer["cellphone"] == "(11) 4002-8922" + assert customer["taxId"] == "123.456.789-01" + assert customer["country"] == "BR" + assert customer["zipCode"] == "12345-678" + assert customer["devMode"] == false + assert customer["metadata"] == %{"key" => "value"} + end + + test "handles error response when retrieving customer" do + error = MockHTTPServer.mock_error(404, "Customer not found") + MockHTTPServer.stub_error(:get, "/customers/get?id=#{@default_customer_id}", error) + + assert {:error, returned_error} = Customer.get(@default_customer_id) + assert returned_error.status_code == 404 + end + end + + describe "list/1" do test "lists all customers successfully" do - expected_response = [ - %{ - "id" => @default_customer_id, - "metadata" => %{ + expected_response = %{ + "data" => [ + %{ + "id" => @default_customer_id, "name" => "Daniel Lima", "cellphone" => "(11) 4002-8922", "email" => "daniel_lima@abacatepay.com", - "taxId" => "123.456.789-01" - } - }, - %{ - "id" => "cust_bcdxkhDZNaMmJeKsy0AHS0FQ", - "metadata" => %{ + "taxId" => "123.456.789-01", + "country" => "BR", + "zipCode" => "12345-678", + "devMode" => false, + "metadata" => %{"key" => "value"} + }, + %{ + "id" => "cust_bcdxkhDZNaMmJeKsy0AHS0FQ", "name" => "Customer 2", "email" => "customer2@example.com", "cellphone" => "(21) 3003-7788", - "taxId" => "987.654.321-00" + "taxId" => "987.654.321-00", + "country" => "BR", + "zipCode" => "87654-321", + "devMode" => true, + "metadata" => %{"another_key" => "another_value"} } + ], + "pagination" => %{ + "page" => 1, + "limit" => 20, + "totalPages" => 5, + "totalItems" => 100 } - ] + } - MockHTTPServer.stub_get("/customers/list", expected_response) + MockHTTPServer.stub_get("/customers/list?page=1&limit=20", expected_response) - assert {:ok, customers} = Customer.list_customers() + assert {:ok, customers, pagination} = Customer.list(%{page: 1, limit: 20}) assert is_list(customers) assert length(customers) == 2 assert Enum.all?(customers, &is_map/1) + assert pagination["page"] == 1 + assert pagination["limit"] == 20 + assert pagination["totalPages"] == 5 + assert pagination["totalItems"] == 100 end test "returns empty list when no customers exist" do - MockHTTPServer.stub_get("/customers/list", []) + expected_response = %{ + "data" => [], + "pagination" => %{ + "page" => 1, + "limit" => 20, + "totalPages" => 0, + "totalItems" => 0 + } + } + + MockHTTPServer.stub_get("/customers/list?page=1&limit=20", expected_response) - assert {:ok, customers} = Customer.list_customers() + assert {:ok, customers, pagination} = Customer.list(%{page: 1, limit: 20}) assert customers == [] + assert pagination["page"] == 1 + assert pagination["limit"] == 20 + assert pagination["totalPages"] == 0 + assert pagination["totalItems"] == 0 end test "handles error response when listing customers" do error = MockHTTPServer.mock_error(401, "Token de autenticação inválido ou ausente.") - MockHTTPServer.stub_error(:get, "/customers/list", error) + MockHTTPServer.stub_error(:get, "/customers/list?page=1&limit=20", error) - assert {:error, returned_error} = Customer.list_customers() + assert {:error, returned_error} = Customer.list(%{page: 1, limit: 20}) assert returned_error.status_code == 401 end end diff --git a/test/abacatepay/api/pix_test.exs b/test/abacatepay/api/pix_test.exs index 45541ed..9610137 100644 --- a/test/abacatepay/api/pix_test.exs +++ b/test/abacatepay/api/pix_test.exs @@ -7,17 +7,17 @@ defmodule AbacatePay.Api.PixTest do @default_pix_id "pix_char_123456" - describe "create_pix_qrcode/1" do - test "creates a pix QR code successfully with required fields" do + describe "create/1" do + test "creates a qr code successfully with required fields" do body = %{ - amount: 123 + amount: 4_000 } expected_response = %{ "id" => @default_pix_id, - "amount" => 123, + "amount" => 4_000, "status" => "PENDING", - "devMode" => true, + "devMode" => false, "brCode" => "00020101021226950014br.gov.bcb.pix", "brCodeBase64" => "data:image/png;base64,iVBORw0KGgoAAA", "platformFee" => 80, @@ -28,11 +28,11 @@ defmodule AbacatePay.Api.PixTest do MockHTTPServer.stub_post("/pixQrCode/create", body, expected_response) - assert {:ok, pix} = Pix.create_pix_qrcode(body) + assert {:ok, pix} = Pix.create(body) assert pix["id"] == @default_pix_id - assert pix["amount"] == 123 + assert pix["amount"] == 4_000 assert pix["status"] == "PENDING" - assert pix["devMode"] == true + assert pix["devMode"] == false assert pix["brCode"] == "00020101021226950014br.gov.bcb.pix" assert pix["brCodeBase64"] == "data:image/png;base64,iVBORw0KGgoAAA" assert pix["platformFee"] == 80 @@ -41,9 +41,9 @@ defmodule AbacatePay.Api.PixTest do assert pix["expiresAt"] == "2025-03-25T21:50:20.772Z" end - test "creates a pix QR code with all optional fields" do + test "creates a qr code with all optional fields" do body = %{ - amount: 123, + amount: 4_000, expires_in: 3_600, description: "Pagamento do pedido #12345", customer: %{ @@ -59,9 +59,9 @@ defmodule AbacatePay.Api.PixTest do expected_response = %{ "id" => @default_pix_id, - "amount" => 123, + "amount" => 4_000, "status" => "PENDING", - "devMode" => true, + "devMode" => false, "brCode" => "00020101021226950014br.gov.bcb.pix", "brCodeBase64" => "data:image/png;base64,iVBORw0KGgoAAA", "platformFee" => 80, @@ -72,11 +72,11 @@ defmodule AbacatePay.Api.PixTest do MockHTTPServer.stub_post("/pixQrCode/create", body, expected_response) - assert {:ok, pix} = Pix.create_pix_qrcode(body) + assert {:ok, pix} = Pix.create(body) assert pix["id"] == @default_pix_id - assert pix["amount"] == 123 + assert pix["amount"] == 4_000 assert pix["status"] == "PENDING" - assert pix["devMode"] == true + assert pix["devMode"] == false assert pix["brCode"] == "00020101021226950014br.gov.bcb.pix" assert pix["brCodeBase64"] == "data:image/png;base64,iVBORw0KGgoAAA" assert pix["platformFee"] == 80 @@ -100,7 +100,7 @@ defmodule AbacatePay.Api.PixTest do MockHTTPServer.stub_post("/pixQrCode/create", body, expected_response) - assert {:ok, pix} = Pix.create_pix_qrcode(body) + assert {:ok, pix} = Pix.create(body) assert pix["amount"] == 100 assert pix["id"] == "pix_minimum" assert pix["status"] == "PENDING" @@ -121,7 +121,7 @@ defmodule AbacatePay.Api.PixTest do MockHTTPServer.stub_post("/pixQrCode/create", body, expected_response) - assert {:ok, pix} = Pix.create_pix_qrcode(body) + assert {:ok, pix} = Pix.create(body) assert pix["amount"] == 99_999_999 assert pix["id"] == "pix_large" assert pix["status"] == "PENDING" @@ -135,7 +135,7 @@ defmodule AbacatePay.Api.PixTest do error = MockHTTPServer.mock_error(400, "Missing required field: amount") MockHTTPServer.stub_error(:post, "/pixQrCode/create", error) - assert {:error, returned_error} = Pix.create_pix_qrcode(body) + assert {:error, returned_error} = Pix.create(body) assert returned_error.status_code == 400 end @@ -148,7 +148,7 @@ defmodule AbacatePay.Api.PixTest do error = MockHTTPServer.mock_error(422, "Amount must be positive") MockHTTPServer.stub_error(:post, "/pixQrCode/create", error) - assert {:error, returned_error} = Pix.create_pix_qrcode(body) + assert {:error, returned_error} = Pix.create(body) assert returned_error.status_code == 422 end @@ -161,7 +161,7 @@ defmodule AbacatePay.Api.PixTest do error = MockHTTPServer.mock_error(401, "Unauthorized") MockHTTPServer.stub_error(:post, "/pixQrCode/create", error) - assert {:error, returned_error} = Pix.create_pix_qrcode(body) + assert {:error, returned_error} = Pix.create(body) assert returned_error.status_code == 401 end @@ -174,7 +174,7 @@ defmodule AbacatePay.Api.PixTest do error = MockHTTPServer.mock_error(500, "Internal Server Error") MockHTTPServer.stub_error(:post, "/pixQrCode/create", error) - assert {:error, returned_error} = Pix.create_pix_qrcode(body) + assert {:error, returned_error} = Pix.create(body) assert returned_error.status_code == 500 end end diff --git a/test/abacatepay/pagination_test.exs b/test/abacatepay/pagination_test.exs new file mode 100644 index 0000000..8e2d823 --- /dev/null +++ b/test/abacatepay/pagination_test.exs @@ -0,0 +1,66 @@ +defmodule AbacatePay.PaginationTest do + use ExUnit.Case, async: true + + alias AbacatePay.Pagination + + describe "build_struct/1" do + test "builds pagination struct from raw data" do + raw = %{ + "page" => 2, + "limit" => 20, + "items" => 100, + "totalPages" => 5 + } + + assert {:ok, %Pagination{} = pagination} = Pagination.build_struct(raw) + assert pagination.page == 2 + assert pagination.limit == 20 + assert pagination.items == 100 + assert pagination.total_pages == 5 + assert pagination.has_next == nil + assert pagination.has_previous == nil + assert pagination.next_cursor == nil + end + + test "builds cursor-based pagination struct from raw data" do + raw = %{ + "hasNext" => true, + "hasPrevious" => false, + "limit" => 20, + "nextCursor" => "cursor_123" + } + + assert {:ok, %Pagination{} = pagination} = Pagination.build_struct(raw) + assert pagination.page == nil + assert pagination.items == nil + assert pagination.total_pages == nil + assert pagination.limit == 20 + assert pagination.has_next == true + assert pagination.has_previous == false + assert pagination.next_cursor == "cursor_123" + end + end + + describe "build_raw/1" do + test "builds raw pagination data from pagination struct" do + pagination = %Pagination{ + page: 1, + limit: 10, + items: 50, + total_pages: 5, + has_next: true, + has_previous: false, + next_cursor: "cursor_456" + } + + assert {:ok, raw} = Pagination.build_raw(pagination) + assert raw[:page] == 1 + assert raw[:limit] == 10 + assert raw[:items] == 50 + assert raw[:totalPages] == 5 + assert raw[:hasNext] == true + assert raw[:hasPrevious] == false + assert raw[:nextCursor] == "cursor_456" + end + end +end diff --git a/test/abacatepay/struct/customer_test.exs b/test/abacatepay/struct/customer_test.exs index f909a24..dbf484b 100644 --- a/test/abacatepay/struct/customer_test.exs +++ b/test/abacatepay/struct/customer_test.exs @@ -10,7 +10,10 @@ defmodule AbacatePay.CustomerTest do name: "Daniel Lima", cellphone: "(11) 4002-8922", tax_id: "123.456.789-01", - email: "daniel_lima@abacatepay.com" + email: "daniel_lima@abacatepay.com", + country: "BR", + zip_code: "12345-678", + metadata: %{key: "value"} } assert customer.id == "cust_aebxkhDZNaMmJeKsy0AHS0FQ" @@ -18,6 +21,9 @@ defmodule AbacatePay.CustomerTest do assert customer.cellphone == "(11) 4002-8922" assert customer.tax_id == "123.456.789-01" assert customer.email == "daniel_lima@abacatepay.com" + assert customer.country == "BR" + assert customer.zip_code == "12345-678" + assert customer.metadata == %{key: "value"} end test "creates Customer with nil values" do @@ -28,144 +34,148 @@ defmodule AbacatePay.CustomerTest do assert customer.cellphone == nil assert customer.tax_id == nil assert customer.email == nil + assert customer.country == nil + assert customer.zip_code == nil + assert customer.metadata == nil end end - describe "build_pretty_customer/1" do + describe "build_struct/1" do test "builds Customer from raw API data" do raw_data = %{ "id" => "cust_aebxkhDZNaMmJeKsy0AHS0FQ", - "metadata" => %{ - "name" => "Daniel Lima", - "cellphone" => "(11) 4002-8922", - "taxId" => "123.456.789-01", - "email" => "daniel_lima@abacatepay.com" - } + "name" => "Daniel Lima", + "cellphone" => "(11) 4002-8922", + "taxId" => "123.456.789-01", + "email" => "daniel_lima@abacatepay.com", + "country" => "BR", + "zipCode" => "12345-678", + "metadata" => %{"key" => "value"} } - assert {:ok, customer} = Customer.build_pretty_customer(raw_data) + assert {:ok, customer} = Customer.build_struct(raw_data) assert customer.id == "cust_aebxkhDZNaMmJeKsy0AHS0FQ" assert customer.name == "Daniel Lima" assert customer.cellphone == "(11) 4002-8922" assert customer.tax_id == "123.456.789-01" assert customer.email == "daniel_lima@abacatepay.com" + assert customer.country == "BR" + assert customer.zip_code == "12345-678" + assert customer.metadata == %{key: "value"} end - test "handles missing metadata fields" do + test "handles missing fields" do raw_data = %{ "id" => "cust_aebxkhDZNaMmJeKsy0AHS0FQ", - "metadata" => %{ - "email" => "daniel_lima@abacatepay.com" - } + "email" => "daniel_lima@abacatepay.com" } - assert {:ok, customer} = Customer.build_pretty_customer(raw_data) + assert {:ok, customer} = Customer.build_struct(raw_data) assert customer.id == "cust_aebxkhDZNaMmJeKsy0AHS0FQ" + assert customer.email == "daniel_lima@abacatepay.com" assert customer.name == nil assert customer.cellphone == nil assert customer.tax_id == nil - assert customer.email == "daniel_lima@abacatepay.com" + assert customer.country == nil + assert customer.zip_code == nil + assert customer.metadata == nil end - test "handles missing metadata entirely" do + test "handles missing data entirely" do raw_data = %{ "id" => "cust_aebxkhDZNaMmJeKsy0AHS0FQ" } - assert {:ok, customer} = Customer.build_pretty_customer(raw_data) + assert {:ok, customer} = Customer.build_struct(raw_data) assert customer.id == "cust_aebxkhDZNaMmJeKsy0AHS0FQ" assert customer.name == nil assert customer.cellphone == nil assert customer.tax_id == nil assert customer.email == nil - end - - test "handles various phone formats" do - raw_data = %{ - "id" => "cust_aebxkhDZNaMmJeKsy0AHS0FQ", - "metadata" => %{ - "cellphone" => "+55 (11) 4002-8922", - "email" => "daniel_lima@abacatepay.com" - } - } - - assert {:ok, customer} = Customer.build_pretty_customer(raw_data) - assert customer.cellphone == "+55 (11) 4002-8922" - end - - test "handles various tax_id formats" do - raw_data = %{ - "id" => "cust_aebxkhDZNaMmJeKsy0AHS0FQ", - "metadata" => %{ - "taxId" => "12.345.678/0001-90", - "email" => "daniel_lima@abacatepay.com" - } - } - - assert {:ok, customer} = Customer.build_pretty_customer(raw_data) - assert customer.tax_id == "12.345.678/0001-90" + assert customer.country == nil + assert customer.zip_code == nil + assert customer.metadata == nil end test "handles special characters in name" do raw_data = %{ "id" => "cust_aebxkhDZNaMmJeKsy0AHS0FQ", - "metadata" => %{ - "name" => "José María da Silva", - "email" => "jose@example.com" - } + "name" => "José da Silva", + "email" => "jose@example.com" } - assert {:ok, customer} = Customer.build_pretty_customer(raw_data) - assert customer.name == "José María da Silva" + assert {:ok, customer} = Customer.build_struct(raw_data) + assert customer.name == "José da Silva" end end - describe "build_api_customer/1" do + describe "build_raw/1" do test "builds API map from Customer struct" do customer = %Customer{ id: "cust_aebxkhDZNaMmJeKsy0AHS0FQ", name: "Daniel Lima", + dev_mode: false, cellphone: "(11) 4002-8922", email: "daniel_lima@abacatepay.com", - tax_id: "123.456.789-01" + tax_id: "123.456.789-01", + country: "BR", + zip_code: "12345-678", + metadata: %{key: "value"} } - assert {:ok, api_map} = Customer.build_api_customer(customer) + assert {:ok, api_map} = Customer.build_raw(customer) assert api_map[:id] == "cust_aebxkhDZNaMmJeKsy0AHS0FQ" - assert api_map[:metadata][:name] == "Daniel Lima" - assert api_map[:metadata][:cellphone] == "(11) 4002-8922" - assert api_map[:metadata][:taxId] == "123.456.789-01" - assert api_map[:metadata][:email] == "daniel_lima@abacatepay.com" + assert api_map[:name] == "Daniel Lima" + assert api_map[:devMode] == false + assert api_map[:cellphone] == "(11) 4002-8922" + assert api_map[:taxId] == "123.456.789-01" + assert api_map[:email] == "daniel_lima@abacatepay.com" + assert api_map[:country] == "BR" + assert api_map[:zipCode] == "12345-678" + assert api_map[:metadata] == %{"key" => "value"} end test "handles nil optional fields" do customer = %Customer{ id: "cust_aebxkhDZNaMmJeKsy0AHS0FQ", + email: "daniel_lima@abacatepay.com", + dev_mode: false, name: nil, cellphone: nil, tax_id: nil, - email: "daniel_lima@abacatepay.com" + country: nil, + zip_code: nil, + metadata: nil } - assert {:ok, api_map} = Customer.build_api_customer(customer) - assert api_map[:metadata][:name] == nil - assert api_map[:metadata][:cellphone] == nil - assert api_map[:metadata][:taxId] == nil - assert api_map[:metadata][:email] == "daniel_lima@abacatepay.com" + assert {:ok, api_map} = Customer.build_raw(customer) + assert api_map[:id] == "cust_aebxkhDZNaMmJeKsy0AHS0FQ" + assert api_map[:email] == "daniel_lima@abacatepay.com" + assert api_map[:devMode] == false + assert api_map[:name] == nil + assert api_map[:cellphone] == nil + assert api_map[:taxId] == nil + assert api_map[:country] == nil + assert api_map[:zipCode] == nil + assert api_map[:metadata] == nil end - test "preserves all metadata fields" do + test "preserves all fields" do customer = %Customer{ id: "cust_aebxkhDZNaMmJeKsy0AHS0FQ", name: "Daniel Lima", - cellphone: "+55 (11) 99999-9999", + dev_mode: false, + cellphone: "(11) 4002-8922", tax_id: "123.456.789-01", - email: "daniel_lima@abacatepay.com" + email: "daniel_lima@abacatepay.com", + country: "BR", + zip_code: "12345-678", + metadata: %{key: "value"} } - assert {:ok, api_map} = Customer.build_api_customer(customer) - assert map_size(api_map) == 2 - assert map_size(api_map[:metadata]) == 4 + assert {:ok, api_map} = Customer.build_raw(customer) + assert map_size(api_map) == 9 + assert map_size(api_map[:metadata]) == 1 end end @@ -173,38 +183,28 @@ defmodule AbacatePay.CustomerTest do test "converts raw data to struct and back to API format" do raw_data = %{ "id" => "cust_aebxkhDZNaMmJeKsy0AHS0FQ", - "metadata" => %{ - "name" => "Daniel Lima", - "cellphone" => "(11) 4002-8922", - "taxId" => "123.456.789-01", - "email" => "daniel_lima@abacatepay.com" - } - } - - assert {:ok, customer} = Customer.build_pretty_customer(raw_data) - assert {:ok, api_map} = Customer.build_api_customer(customer) - - assert api_map[:id] == raw_data["id"] - assert api_map[:metadata][:name] == raw_data["metadata"]["name"] - assert api_map[:metadata][:cellphone] == raw_data["metadata"]["cellphone"] - assert api_map[:metadata][:taxId] == raw_data["metadata"]["taxId"] - assert api_map[:metadata][:email] == raw_data["metadata"]["email"] - end - - test "handles partial metadata in roundtrip" do - raw_data = %{ - "id" => "cust_aebxkhDZNaMmJeKsy0AHS0FQ", - "metadata" => %{ - "email" => "daniel_lima@abacatepay.com" - } + "name" => "Daniel Lima", + "devMode" => false, + "cellphone" => "(11) 4002-8922", + "taxId" => "123.456.789-01", + "email" => "daniel_lima@abacatepay.com", + "country" => "BR", + "zipCode" => "12345-678", + "metadata" => %{"key" => "value"} } - assert {:ok, customer} = Customer.build_pretty_customer(raw_data) - assert {:ok, api_map} = Customer.build_api_customer(customer) + assert {:ok, customer} = Customer.build_struct(raw_data) + assert {:ok, api_map} = Customer.build_raw(customer) assert api_map[:id] == raw_data["id"] - assert api_map[:metadata][:email] == raw_data["metadata"]["email"] - assert api_map[:metadata][:name] == nil + assert api_map[:name] == raw_data["name"] + assert api_map[:devMode] == raw_data["devMode"] + assert api_map[:cellphone] == raw_data["cellphone"] + assert api_map[:taxId] == raw_data["taxId"] + assert api_map[:email] == raw_data["email"] + assert api_map[:country] == raw_data["country"] + assert api_map[:zipCode] == raw_data["zipCode"] + assert api_map[:metadata] == raw_data["metadata"] end end end diff --git a/test/abacatepay/struct/pix_test.exs b/test/abacatepay/struct/pix_test.exs index 11d35e7..d3fc73e 100644 --- a/test/abacatepay/struct/pix_test.exs +++ b/test/abacatepay/struct/pix_test.exs @@ -12,8 +12,7 @@ defmodule AbacatePay.PixTest do dev_mode: false, br_code: "test_code", br_code_base_64: "data:image/png;base64,test", - platform_fee: 80, - description: "Test payment" + platform_fee: 80 } assert pix.id == "pix_123" @@ -40,23 +39,19 @@ defmodule AbacatePay.PixTest do "brCode" => "00020126360014BR.GOV.BCB.PIX", "brCodeBase64" => "data:image/png;base64,test", "platformFee" => 80, - "description" => "PIX Payment for order #1234", "createdAt" => "2026-01-01T12:00:00Z", "updatedAt" => "2026-01-01T12:05:00Z", "expiresAt" => "2026-01-02T12:00:00Z", - "customer" => nil, - "metadata" => nil, "expiresIn" => nil } - assert {:ok, pix} = Pix.build_pretty_pix_qrcode(raw_data) + assert {:ok, pix} = Pix.build_struct(raw_data) assert pix.id == "pix_charabc123456789" assert pix.amount == 1_500 assert pix.status == :paid assert pix.dev_mode == false assert pix.br_code == "00020126360014BR.GOV.BCB.PIX" assert pix.platform_fee == 80 - assert pix.description == "PIX Payment for order #1234" end test "handles datetime parsing" do @@ -68,16 +63,13 @@ defmodule AbacatePay.PixTest do "brCode" => "code", "brCodeBase64" => "data", "platformFee" => 0, - "description" => nil, "createdAt" => "2026-01-15T10:30:45Z", "updatedAt" => "2026-01-15T10:35:45Z", "expiresAt" => "2026-01-16T10:30:45Z", - "customer" => nil, - "metadata" => nil, "expiresIn" => nil } - assert {:ok, pix} = Pix.build_pretty_pix_qrcode(raw_data) + assert {:ok, pix} = Pix.build_struct(raw_data) assert pix.created_at != nil assert pix.updated_at != nil assert pix.expires_at != nil @@ -92,16 +84,13 @@ defmodule AbacatePay.PixTest do "brCode" => "code", "brCodeBase64" => "data", "platformFee" => 0, - "description" => nil, "createdAt" => nil, "updatedAt" => nil, "expiresAt" => nil, - "customer" => nil, - "metadata" => nil, "expiresIn" => nil } - assert {:ok, pix} = Pix.build_pretty_pix_qrcode(raw_data) + assert {:ok, pix} = Pix.build_struct(raw_data) assert pix.created_at == nil assert pix.updated_at == nil assert pix.expires_at == nil @@ -119,16 +108,13 @@ defmodule AbacatePay.PixTest do "brCode" => "code", "brCodeBase64" => "data", "platformFee" => 0, - "description" => nil, "createdAt" => nil, "updatedAt" => nil, "expiresAt" => nil, - "customer" => nil, - "metadata" => nil, "expiresIn" => nil } - assert {:ok, pix} = Pix.build_pretty_pix_qrcode(raw_data) + assert {:ok, pix} = Pix.build_struct(raw_data) assert pix.status == Util.atomize_enum(status) end) end @@ -142,16 +128,13 @@ defmodule AbacatePay.PixTest do "brCode" => "code", "brCodeBase64" => "data", "platformFee" => 80_000, - "description" => "Large payment", "createdAt" => nil, "updatedAt" => nil, "expiresAt" => nil, - "customer" => nil, - "metadata" => nil, "expiresIn" => nil } - assert {:ok, pix} = Pix.build_pretty_pix_qrcode(raw_data) + assert {:ok, pix} = Pix.build_struct(raw_data) assert pix.amount == 999_999_999 assert pix.platform_fee == 80_000 end @@ -167,21 +150,17 @@ defmodule AbacatePay.PixTest do br_code: "test_code", br_code_base_64: "data:image/png;base64,test", platform_fee: 80, - description: "Test payment", created_at: ~U[2026-01-01T12:00:00Z], updated_at: ~U[2026-01-01T12:05:00Z], - expires_at: ~U[2026-01-02T12:00:00Z], - customer: nil, - metadata: nil, - expires_in: nil + expires_at: ~U[2026-01-02T12:00:00Z] } - assert {:ok, api_map} = Pix.build_api_pix_qrcode(pix) - assert api_map[:id] == "pix_123" - assert api_map[:amount] == 1_500 - assert api_map[:status] == "PAID" - assert api_map[:devMode] == false - assert String.contains?(api_map[:createdAt], "2026-01-01") + assert {:ok, raw} = Pix.build_raw(pix) + assert raw[:id] == "pix_123" + assert raw[:amount] == 1_500 + assert raw[:status] == "PAID" + assert raw[:devMode] == false + assert String.contains?(raw[:createdAt], "2026-01-01") end test "handles nil datetimes in API format" do @@ -193,25 +172,21 @@ defmodule AbacatePay.PixTest do br_code: "code", br_code_base_64: "data", platform_fee: 0, - description: nil, created_at: nil, updated_at: nil, - expires_at: nil, - customer: nil, - metadata: nil, - expires_in: nil + expires_at: nil } - assert {:ok, api_map} = Pix.build_api_pix_qrcode(pix) - assert api_map[:createdAt] == nil - assert api_map[:updatedAt] == nil - assert api_map[:expiresAt] == nil + assert {:ok, raw} = Pix.build_raw(pix) + assert raw[:createdAt] == nil + assert raw[:updatedAt] == nil + assert raw[:expiresAt] == nil end end describe "roundtrip conversion" do test "converts raw data to struct and back" do - raw_data = %{ + data = %{ "id" => "pix_roundtrip", "amount" => 5_000, "status" => "PENDING", @@ -219,21 +194,18 @@ defmodule AbacatePay.PixTest do "brCode" => "00020126360014BR.GOV.BCB.PIX", "brCodeBase64" => "data:image/png;base64,test", "platformFee" => 80, - "description" => "Roundtrip test", "createdAt" => "2026-01-15T10:30:45Z", "updatedAt" => "2026-01-15T10:35:45Z", "expiresAt" => nil, - "customer" => nil, - "metadata" => nil, "expiresIn" => nil } - assert {:ok, pix} = Pix.build_pretty_pix_qrcode(raw_data) - assert {:ok, api_map} = Pix.build_api_pix_qrcode(pix) + assert {:ok, pix} = Pix.build_struct(data) + assert {:ok, raw} = Pix.build_raw(pix) - assert api_map[:id] == raw_data["id"] - assert api_map[:amount] == raw_data["amount"] - assert api_map[:status] == raw_data["status"] + assert raw[:id] == data["id"] + assert raw[:amount] == data["amount"] + assert raw[:status] == data["status"] end end end diff --git a/test/abacatepay/api_error_test.exs b/test/api_error_test.exs similarity index 100% rename from test/abacatepay/api_error_test.exs rename to test/api_error_test.exs diff --git a/test/config_test.exs b/test/config_test.exs new file mode 100644 index 0000000..3b71602 --- /dev/null +++ b/test/config_test.exs @@ -0,0 +1,103 @@ +defmodule AbacatePay.ConfigTest do + use ExUnit.Case, async: true + + alias AbacatePay.Config + + describe "validate!/0" do + test ":api_url from option" do + assert Config.validate!(api_url: "https://custom-url.abacatepay.com")[:api_url] == + "https://custom-url.abacatepay.com" + + assert Config.validate!()[:api_url] == "https://api.abacatepay.com" + end + + test ":api_url from system environment" do + with_system_env("ABACATEPAY_API_URL", "https://system-env.abacatepay.com", fn -> + assert Config.validate!()[:api_url] == "https://system-env.abacatepay.com" + end) + end + + test "invalid :api_url" do + # Not a string. + assert_raise ArgumentError, fn -> + Config.validate!(api_url: :not_a_string) + end + end + + test ":api_version from option" do + assert Config.validate!(api_version: "1")[:api_version] == "1" + assert Config.validate!()[:api_version] == "2" + end + + test ":api_version from system environment" do + with_system_env("ABACATEPAY_API_VERSION", "2", fn -> + assert Config.validate!()[:api_version] == "2" + end) + end + + test "invalid :api_version" do + # Not a string. + assert_raise ArgumentError, fn -> + Config.validate!(api_version: :not_a_string) + end + end + + test ":api_key from option" do + assert Config.validate!(api_key: "abc_dev_pWxM5GhSROzeerqmdkfu6mNN")[:api_key] == + "abc_dev_pWxM5GhSROzeerqmdkfu6mNN" + + assert Config.validate!()[:api_key] == nil + end + + test ":api_key from system environment" do + with_system_env("ABACATEPAY_API_KEY", "abc_dev_pWxM5GhSROzeerqmdkfu6mNN", fn -> + assert Config.validate!()[:api_key] == "abc_dev_pWxM5GhSROzeerqmdkfu6mNN" + end) + end + + test "invalid :api_key" do + # Not a string. + assert_raise ArgumentError, fn -> + Config.validate!(api_key: :not_a_string) + end + end + end + + describe "put_config/2" do + test "updates the configuration" do + api_key_before = :persistent_term.get({:abacatepay_config, :api_key}, :__not_set__) + + on_exit(fn -> + case api_key_before do + :__not_set__ -> :persistent_term.erase({:abacatepay_config, :api_key}) + other -> :persistent_term.put({:abacatepay_config, :api_key}, other) + end + end) + + new_api_key = "abc_dev_pWxM5GhSROzeerqmdkfu6mNN" + assert :ok = Config.put_config(:api_key, new_api_key) + + assert :persistent_term.get({:abacatepay_config, :api_key}) == new_api_key + end + + test "validates the given key" do + assert_raise ArgumentError, ~r/unknown option :non_existing/, fn -> + Config.put_config(:non_existing, "value") + end + end + end + + defp with_system_env(key, value, fun) when is_function(fun, 0) do + original_env = System.fetch_env(key) + System.put_env(key, value) + + try do + fun.() + after + case original_env do + {:ok, original_value} -> System.put_env(key, original_value) + :error -> System.delete_env(key) + end + end + end +end diff --git a/test/http_client_test.exs b/test/http_client_test.exs index 797154f..a51229b 100644 --- a/test/http_client_test.exs +++ b/test/http_client_test.exs @@ -24,6 +24,34 @@ defmodule AbacatePay.HttpClientTest do assert data == expected_data end + test "get/1 returns prased response data with pagination" do + expected_data = %{ + "data" => [ + %{ + "id" => "bill_123456", + "metadata" => %{ + "name" => "Daniel Lima", + "cellphone" => "(11) 4002-8922", + "email" => "daniel_lima@abacatepay.com", + "taxId" => "123.456.789-01" + } + } + ], + "pagination" => %{ + "page" => 1, + "limit" => 20, + "totalPages" => 5, + "totalItems" => 100 + } + } + + MockHTTPServer.stub_get("/customers/list?page=1&limit=20", expected_data) + assert {:ok, data, pagination} = HTTPClient.get("/customers/list?page=1&limit=20") + + assert data == expected_data["data"] + assert pagination == expected_data["pagination"] + end + test "post/2 returns parsed response data" do body = %{ name: "Daniel Lima", diff --git a/test/support/mock_http_server.ex b/test/support/mock_http_server.ex index e76d028..218a44b 100644 --- a/test/support/mock_http_server.ex +++ b/test/support/mock_http_server.ex @@ -17,8 +17,13 @@ defmodule AbacatePay.MockHTTPServer do stub_get("/customers/list", %{"data" => [...]}) """ def stub_get(path, response_data) do - stub(AbacatePay.HTTPClient, :get, fn ^path -> - {:ok, response_data} + expected_path = normalize_path(path) + + stub(AbacatePay.HTTPClient, :get, fn actual_path -> + case normalize_path(actual_path) do + ^expected_path -> normalize_response(response_data) + _ -> raise(FunctionClauseError) + end end) end @@ -48,8 +53,13 @@ defmodule AbacatePay.MockHTTPServer do Stub a successful DELETE request. """ def stub_delete(path, response_data) do - stub(AbacatePay.HTTPClient, :delete, fn ^path -> - {:ok, response_data} + expected_path = normalize_path(path) + + stub(AbacatePay.HTTPClient, :delete, fn actual_path -> + case normalize_path(actual_path) do + ^expected_path -> {:ok, response_data} + _ -> raise(FunctionClauseError) + end end) end @@ -61,8 +71,13 @@ defmodule AbacatePay.MockHTTPServer do stub_error(:get, "/invalid", %AbacatePay.ApiError{status_code: 404, message: "Not Found"}) """ def stub_error(method, path, error) when method in [:get, :delete] do - stub(AbacatePay.HTTPClient, method, fn ^path -> - {:error, error} + expected_path = normalize_path(path) + + stub(AbacatePay.HTTPClient, method, fn actual_path -> + case normalize_path(actual_path) do + ^expected_path -> {:error, error} + _ -> raise(FunctionClauseError) + end end) end @@ -83,4 +98,35 @@ defmodule AbacatePay.MockHTTPServer do def mock_error(status_code, message) do %AbacatePay.ApiError{status_code: status_code, message: message} end + + defp normalize_response(%{"data" => data, "pagination" => pagination}) do + {:ok, data, pagination} + end + + defp normalize_response(%{"data" => data}) do + {:ok, data} + end + + defp normalize_response(response_data) do + {:ok, response_data} + end + + defp normalize_path(path) do + case String.split(path, "?", parts: 2) do + [base, ""] -> + base + + [base] -> + base + + [base, query] -> + normalized_query = + query + |> URI.decode_query() + |> Enum.sort() + |> URI.encode_query() + + base <> "?" <> normalized_query + end + end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 9e12b5a..895905b 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,5 +1,3 @@ -Application.put_env(:abacatepay, :api_url, "http://localhost:4001") - Mimic.copy(AbacatePay.HTTPClient) ExUnit.configure(exclude: [disabled: true]) diff --git a/test/abacatepay/util_test.exs b/test/util_test.exs similarity index 100% rename from test/abacatepay/util_test.exs rename to test/util_test.exs