diff --git a/README.md b/README.md index 072b6a4..d27ef85 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Visit our [API docs](https://documentation.ibanity.com/xs2a/1/api/elixir). In the `mix.exs` file: ```elixir def deps do - [{:ibanity, "~> 0.11.0}] + [{:ibanity, "~> 0.11.0"}] end ``` diff --git a/lib/ibanity/api/ponto_connect/account.ex b/lib/ibanity/api/ponto_connect/account.ex new file mode 100644 index 0000000..12415be --- /dev/null +++ b/lib/ibanity/api/ponto_connect/account.ex @@ -0,0 +1,222 @@ +defmodule Ibanity.PontoConnect.Account do + @moduledoc """ + [Account API wrapper](https://documentation.ibanity.com/ponto-connect/2/api#account) + """ + + use Ibanity.Resource + + alias Ibanity.PontoConnect + + @api_schema_path ["ponto-connect", "accounts"] + + defstruct [ + :id, + :description, + :deprecated, + :product, + :reference, + :currency, + :subtype, + :available_balance, + :available_balance_changed_at, + :available_balance_reference_date, + :current_balance, + :current_balance_changed_at, + :current_balance_reference_date, + :holder_name, + :reference_type, + :authorization_expiration_expected_at, + :authorized_at, + :available_balance_variation_observed_at, + :current_balance_variation_observed_at, + :internal_reference, + :availability, + :latest_synchronization, + :synchronized_at + ] + + @doc """ + [List accounts](https://documentation.ibanity.com/ponto-connect/2/api#list-accounts) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as argument. + + ## Examples + + iex> Ibanity.PontoConnect.Account.list(token) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Account{}] + }} + + iex> token |> Ibanity.Request.token() |> Ibanity.PontoConnect.Accounts.list() + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Account{}] + }} + + iex> invalid_token |> Ibanity.PontoConnect.Account.list() + {:error, + [ + %{ + "code" => "invalidAccessToken", + "detail" => "Your access token is invalid.", + "meta" => %{"requestId" => "00077F000001D3A87F0000011F4066E43AFD1900051"} + } + ]} + + """ + def list(%Request{token: token} = request_or_token) + when not is_nil(token) do + request_or_token + |> Request.id(:id, "") + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def list(%PontoConnect.Token{} = request_or_token) do + request_or_token + |> Request.token() + |> list() + end + + def list(other) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Accounts", other) + end + + @doc """ + [Find Account by id](https://documentation.ibanity.com/ponto-connect/2/api#get-account) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument, and a Account + ID as second argument. + + ## Examples + + With token + + iex> Ibanity.PontoConnect.Account.find(token, "953934eb-229a-4fd2-8675-07794078cc7d") + {:ok, %Ibanity.PontoConnect.Account{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + With request + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Account.find("953934eb-229a-4fd2-8675-07794078cc7d") + {:ok, %Ibanity.PontoConnect.Account{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + Error + + iex> Ibanity.PontoConnect.Account.find(token, "does-not-exist") + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "account" + } + } + ]} + """ + def find(%Request{token: token} = request_or_token, id) + when not is_nil(token) do + request_or_token + |> Request.id(id) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def find(%PontoConnect.Token{} = request_or_token, id) do + request_or_token + |> Request.token() + |> find(id) + end + + def find(other, _id) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Account", other) + end + + @doc """ + [Revoke Account by id](https://documentation.ibanity.com/ponto-connect/2/api#revoke-account) + + ## Examples + + With token + + iex> Ibanity.PontoConnect.Account.delete(token, "953934eb-229a-4fd2-8675-07794078cc7d") + {:ok, %Ibanity.PontoConnect.Account{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + With request + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Account.delete("953934eb-229a-4fd2-8675-07794078cc7d") + {:ok, %Ibanity.PontoConnect.Account{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + Error + + iex> Ibanity.PontoConnect.Account.delete(token, "does-not-exist") + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "account" + } + } + ]} + """ + def delete(%Request{token: token} = request_or_token, id) + when not is_nil(token) do + formatted_ids = PontoConnect.RequestUtils.format_ids(%{id: id}) + + request_or_token + |> Request.ids(formatted_ids) + |> Client.execute(:delete, @api_schema_path, __MODULE__) + end + + def delete(%PontoConnect.Token{} = request_or_token, id) do + request_or_token + |> Request.token() + |> delete(id) + end + + def delete(other, _id) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Account", other) + end + + @doc false + def key_mapping do + [ + id: {~w(id), :string}, + description: {~w(attributes description), :string}, + deprecated: {~w(attributes deprecated), :boolean}, + product: {~w(attributes product), :string}, + reference: {~w(attributes reference), :string}, + currency: {~w(attributes currency), :string}, + subtype: {~w(attributes subtype), :string}, + available_balance: {~w(attributes availableBalance), :number}, + available_balance_changed_at: {~w(attributes availableBalanceChangedAt), :datetime}, + available_balance_reference_date: {~w(attributes availableBalanceReferenceDate), :datetime}, + current_balance: {~w(attributes currentBalance), :number}, + current_balance_changed_at: {~w(attributes currentBalanceChangedAt), :datetime}, + current_balance_reference_date: {~w(attributes currentBalanceReferenceDate), :datetime}, + holder_name: {~w(attributes holderName), :string}, + reference_type: {~w(attributes referenceType), :string}, + authorization_expiration_expected_at: + {~w(attributes authorizationExpirationExpectedAt), :datetime}, + authorized_at: {~w(attributes authorizedAt), :datetime}, + available_balance_variation_observed_at: + {~w(attributes availableBalanceVariationObservedAt), :datetime}, + current_balance_variation_observed_at: + {~w(attributes currentBalanceVariationObservedAt), :datetime}, + internal_reference: {~w(attributes internalReference), :string}, + avilability: {~w(meta availability), :string}, + latest_synchronization: {~w(meta latestSynchronization), :map}, + synchronized_at: {~w(meta synchronizedAt), :datetime} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/bulk_payment.ex b/lib/ibanity/api/ponto_connect/bulk_payment.ex new file mode 100644 index 0000000..108edf1 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/bulk_payment.ex @@ -0,0 +1,251 @@ +defmodule Ibanity.PontoConnect.BulkPayment do + @moduledoc """ + [Bulk Payment](https://documentation.ibanity.com/ponto-connect/api#bulk-payment) API wrapper + """ + + use Ibanity.Resource + + @api_schema_path ["ponto-connect", "account", "bulkPayments"] + + defstruct [ + :id, + :status, + :reference, + :requested_execution_date, + :batch_booking_preferred, + :redirect + ] + + alias Ibanity.PontoConnect + + @doc """ + [Creates a bulk payment](https://documentation.ibanity.com/ponto-connect/api#create-bulk-payment). + + Returns `{:ok, %__MODULE__{}}` if successful, `{:error, reason}` otherwise. + + ## Example + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + Attributes + + iex> attributes = [ + ...> reference: "Invoice Payments", + ...> redirect_uri: "https://fake-tpp.com/payment-confirmation?payment=123", + ...> requested_execution_date: "2025-05-05", + ...> batch_booking_preferred: true, + ...> payments: [ + ...> %{ + ...> remittance_information: "payment 1", + ...> remittance_information_type: "unstructured", + ...> currency: "EUR", + ...> amount: 59, + ...> creditor_name: "Alex Creditor", + ...> creditor_account_reference: "BE55732022998044", + ...> creditor_account_reference_type: "IBAN", + ...> creditor_agent: "NBBEBEBB203", + ...> creditor_agent_type: "BIC", + ...> end_to_end_id: "1234567890" + ...> }, + ...> %{ + ...> remittance_information: "payment 2", + ...> remittance_information_type: "unstructured", + ...> currency: "EUR", + ...> amount: 25, + ...> creditor_name: "Pat Smith", + ...> creditor_account_reference: "BE73055155935764", + ...> creditor_account_reference_type: "IBAN", + ...> creditor_agent: "NBBEBEBB203", + ...> creditor_agent_type: "BIC", + ...> end_to_end_id: "0987654321" + ...> } + ...> ] + ...> ] + + With token + + iex> Ibanity.PontoConnect.BulkPayment.create(%Ibanity.PontoConnect.Token{}, account_or_id, attributes) + {:ok, %Ibanity.PontoConnect.BulkPayment{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + + With request + + iex> request = Ibanity.Request.token(%PontoConnect.Token{}) + iex> Ibanity.PontoConnect.BulkPayment.create(request, account_or_id, attributes) + {:ok, %Ibanity.PontoConnect.BulkPayment{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + """ + def create(%PontoConnect.Token{} = request_or_token, account_or_id, attrs) do + request_or_token + |> Request.token() + |> create(account_or_id, attrs) + end + + def create(%Request{} = request_or_token, %PontoConnect.Account{id: account_id}, attrs) do + create(request_or_token, account_id, attrs) + end + + def create(%Request{} = request_or_token, account_id, attrs) + when is_bitstring(account_id) and is_list(attrs) do + request_or_token + |> Request.id(:account_id, account_id) + |> Request.attributes(attrs) + |> create() + end + + def create(other, _account_id, _attrs) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("BulkPayment", other) + end + + @doc """ + Same as `create/3`, but `:attributes`, `:account_id`, and `:token` must be set in request. + + ## Examples + + Set id and token to request a BulkPayment + + iex> %Ibanity.PontoConnect.Token{} + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.id(:account_id, account_id) + ...> |> Ibanity.Request.attributes(attributes) + ...> |> Ibanity.PontoConnect.BulkPayment.create() + {:ok, %Ibanity.PontoConnect.BulkPayment{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + """ + def create(%Request{} = request) do + request + |> Request.id("") + |> Client.execute(:post, @api_schema_path, __MODULE__) + end + + @doc """ + [Find Bulk Payment by id](https://documentation.ibanity.com/ponto-connect/2/api#get-payment) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + #{PontoConnect.CommonDocs.fetch!(:account_and_id_second_arg)} + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + iex> %Ibanity.PontoConnect.Token{} + ...> |> Ibanity.PontoConnect.BulkPayment.find(%{account_id: account_or_id, id: "d0e23b50-e150-403b-aa50-581a2329b5f5"}) + {:ok, %Ibanity.PontoConnect.BulkPayment{id: "d0e23b50-e150-403b-aa50-581a2329b5f5"}} + + iex> %Ibanity.PontoConnect.Token{} + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.BulkPayment.find(%{account_id: account_or_id, id: "d0e23b50-e150-403b-aa50-581a2329b5f5"}) + {:ok, %Ibanity.PontoConnect.BulkPayment{id: "d0e23b50-e150-403b-aa50-581a2329b5f5"}} + + iex> %Ibanity.PontoConnect.Token{} + ...> |> Ibanity.PontoConnect.BulkPayment.find(%{account_id: account_or_id, id: "does-not-exist"}) + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "bulkPayment" + } + } + ]} + """ + def find(%Request{token: token} = request_or_token, ids) + when not is_nil(token) do + formatted_ids = PontoConnect.RequestUtils.format_ids(ids) + + request_or_token + |> Request.ids(formatted_ids) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def find(%PontoConnect.Token{} = request_or_token, ids) do + request_or_token + |> Request.token() + |> find(ids) + end + + def find(other, _ids) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("BulkPayment", other) + end + + @doc """ + [Delete a Bulk Payment by id](https://documentation.ibanity.com/ponto-connect/2/api#delete-bulk-payment) + + #{PontoConnect.CommonDocs.fetch!(:account_and_id_second_arg)} + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + With token + + iex> %Ibanity.PontoConnect.Token{} + ...> |> Ibanity.PontoConnect.BulkPayment.delete(%{ + ...> id: "953934eb-229a-4fd2-8675-07794078cc7d", account_id: account_or_id + ...> }) + {:ok, %Ibanity.PontoConnect.BulkPayment{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + With request + + iex> %Ibanity.PontoConnect.Token{} + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.BulkPayment.delete(%{ + ...> id: "953934eb-229a-4fd2-8675-07794078cc7d", account_id: account_or_id + ...> }) + {:ok, %Ibanity.PontoConnect.BulkPayment{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + Error + + iex> %Ibanity.PontoConnect.Token{} + ...> |> Ibanity.PontoConnect.BulkPayment.delete(%{ + ...> id: "does-not-exist", + ...> account_id: account_or_id + ...> }) + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "bulkPayment" + } + } + ]} + """ + def delete(%Request{} = request_or_token, %{account_id: account_id} = ids) + when not is_nil(account_id) do + formatted_ids = PontoConnect.RequestUtils.format_ids(ids) + + request_or_token + |> Request.ids(formatted_ids) + |> Client.execute(:delete, @api_schema_path, __MODULE__) + end + + def delete(%PontoConnect.Token{} = request_or_token, ids) do + request_or_token + |> Request.token() + |> delete(ids) + end + + def delete(other, _ids) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("BulkPayment", other) + end + + @doc false + def key_mapping do + [ + id: {["id"], :string}, + status: {["attributes", "status"], :string}, + reference: {["attributes", "reference"], :string}, + requested_execution_date: {["attributes", "requestedExecutionDate"], :date}, + batch_booking_preferred: {["attributes", "batchBookingPreferred"], :boolean}, + redirect: {["links", "redirect"], :string} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/common_docs.ex b/lib/ibanity/api/ponto_connect/common_docs.ex new file mode 100644 index 0000000..4458dd0 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/common_docs.ex @@ -0,0 +1,79 @@ +defmodule Ibanity.PontoConnect.CommonDocs do + @moduledoc false + + @common_docs %{ + client_token: """ + **NOTE:** This resource needs a client token! + + See `Ibanity.PontoConnect.Token.create/1` to find out how to request a client token. + """, + account_id: """ + Fetch an account before each example, or use a valid account id + + iex> {:ok, account_or_id} = token |> Ibanity.PontoConnect.Account.find("03ebe0ae-f630-4414-b37b-afde7de67229") + + Or + + iex> account_or_id = "03ebe0ae-f630-4414-b37b-afde7de67229" + "03ebe0ae-f630-4414-b37b-afde7de67229" + """, + synchronization_id: """ + Fetch a synchronization before each example, or use a valid synchronization id + + iex> {:ok, synchronization_or_id} = token |> Ibanity.PontoConnect.Suynchronization.find("03ebe0ae-f630-4414-b37b-afde7de67229") + + Or + + iex> synchronization_or_id = "03ebe0ae-f630-4414-b37b-afde7de67229" + "03ebe0ae-f630-4414-b37b-afde7de67229" + """, + financial_institution_id: """ + Fetch a financial institution before each example, or use a valid financial institution id + + iex> {:ok, financial_institution_or_id} = Ibanity.PontoConnect.FinancialInstitution.find_public("953934eb-229a-4fd2-8675-07794078cc7d") + + Or + + iex> financial_institution_or_id = "953934eb-229a-4fd2-8675-07794078cc7d" + "953934eb-229a-4fd2-8675-07794078cc7d" + """, + financial_institution_and_account_ids: """ + Fetch a financial institution and financial institution account before each example, or use a valid financial institution id + + iex> {:ok, financial_institution_or_id} = Ibanity.PontoConnect.FinancialInstitution.find_public( + ...> "953934eb-229a-4fd2-8675-07794078cc7d" + ...> ) + iex> {:ok, financial_institution_account_or_id} = Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount.find( + ...> token, + ...> %{ + ...> financial_institution_id: financial_institution_or_id, + ...> id: "cb0bb5ab-dc3d-4832-b2ff-e6629240732f" + ...> } + ...> ) + + Or + + iex> financial_institution_or_id = "953934eb-229a-4fd2-8675-07794078cc7d" + iex> financial_institution_account_or_id = "cb0bb5ab-dc3d-4832-b2ff-e6629240732f" + """, + account_and_id_second_arg: """ + Takes a map with the following keys as second argument: + - `:account_id`: `Ibanity.PontoConnect.Account` struct or account ID as a string + - `:id`: resource ID as a string + """, + financial_institution_and_id_second_arg: """ + Takes a map with the following keys as second argument: + - `:financial_institution_id`: `Ibanity.PontoConnect.Sandbox.FinancialInstitution` struct or account ID as a string + - `:id`: resource ID as a string + """, + financial_institution_and_account_second_arg: """ + Takes a map with the following keys as second argument: + - `:financial_institution_id`: `Ibanity.PontoConnect.FinancialInstitution` struct or account ID as a string + - `:financial_institution_account_id`: `Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount` struct or account ID as a string + - `:id`: resource ID as a string + """ + } + + @doc false + def fetch!(key), do: Map.fetch!(@common_docs, key) +end diff --git a/lib/ibanity/api/ponto_connect/exceptions.ex b/lib/ibanity/api/ponto_connect/exceptions.ex new file mode 100644 index 0000000..4b1c6d0 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/exceptions.ex @@ -0,0 +1,15 @@ +defmodule Ibanity.PontoConnect.Exceptions do + @moduledoc false + + @doc false + def token_argument_error_msg(resource_name, other) do + """ + Cannot access #{resource_name} with given arguments. + Expected one of: + - `%Ibanity.Request{}` with `:token` set + - `%Ibanity.PontoConnect.Token{}` + + Got: #{inspect(other)} + """ + end +end diff --git a/lib/ibanity/api/ponto_connect/financial_institution.ex b/lib/ibanity/api/ponto_connect/financial_institution.ex new file mode 100644 index 0000000..ffbd789 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/financial_institution.ex @@ -0,0 +1,283 @@ +defmodule Ibanity.PontoConnect.FinancialInstitution do + @moduledoc """ + [Financial Institutions API wrapper](https://documentation.ibanity.com/ponto-connect/2/api#financial-institution) + """ + use Ibanity.Resource + + defstruct [ + :id, + :name, + :status, + :deprecated, + :country, + :bic, + :bulk_payments_enabled, + :bulk_payments_product_types, + :future_dated_payments_allowed, + :logo_url, + :maintenance_from, + :maintenance_to, + :maintenance_type, + :payments_enabled, + :payments_product_types, + :periodic_payments_enabled, + :periodic_payments_product_types, + :primary_color, + :secondary_color, + :shared_brand_name, + :shared_brand_reference, + :time_zone + ] + + alias Ibanity.PontoConnect + + @api_schema_path ["ponto-connect", "financialInstitutions"] + + @doc """ + [List public financial institutions](https://documentation.ibanity.com/ponto-connect/2/api#list-financial-institutions) + + ## Examples + + For `:default` application + + iex> Ibanity.PontoConnect.FinancialInstitution.list_public() + %Ibanity.Collection{ + items: [ + %Ibanity.PontoConnect.FinancialInstitution{}, + %Ibanity.PontoConnect.FinancialInstitution{}, + %Ibanity.PontoConnect.FinancialInstitution{}, + %Ibanity.PontoConnect.FinancialInstitution{} + ] + } + + With request + + iex> Ibanity.Request.limit(1) + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.FinancialInstitution.list_public() + %Ibanity.Collection{ + items: [ + %Ibanity.PontoConnect.FinancialInstitution{} + ], + page_limit: 1, + after_cursor: "953934eb-229a-4fd2-8675-07794078cc7d", + first_link: "https://api.ibanity.com/ponto-connect/financial-institutions?page[limit]=1", + next_link: "https://api.ibanity.com/ponto-connect/financial-institutions?page[after]=953934eb-229a-4fd2-8675-07794078cc7d&page[limit]=1", + } + """ + def list_public(%Request{} = request \\ %Request{}) do + request + |> Request.token(nil) + |> Request.id(:id, "") + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + @doc """ + [List organization's financial institutions](https://documentation.ibanity.com/ponto-connect/2/api#list-organization-financial-institutions) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as argument. + + ## Examples + + With token + + iex> token |> Ibanity.PontoConnect.FinancialInstitution.list_organization() + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.FinancialInstitution{}] + }} + + With request + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.PontoConnect.FinancialInstitutions.list_organizations() + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.FinancialInstitution{}] + }} + + Error + + iex> invalid_token |> Ibanity.PontoConnect.FinancialInstitution.list_organization() + {:error, + [ + %{ + "code" => "invalidAccessToken", + "detail" => "Your access token is invalid.", + "meta" => %{"requestId" => "00077F000001D3A87F0000011F4066E43AFD1900051"} + } + ]} + + """ + def list_organization(%Request{token: token} = request) + when not is_nil(token) do + request + |> Request.id("") + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def list_organization(%PontoConnect.Token{} = token) do + token + |> Request.token() + |> list_organization() + end + + def list_organization(other) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Financial Institutions", other) + end + + @doc """ + [Find public financial institution by id](https://documentation.ibanity.com/ponto-connect/2/api#get-financial-institution) + + ## Examples + + For `:default` application + + iex> Ibanity.PontoConnect.FinancialInstitution.find_public("953934eb-229a-4fd2-8675-07794078cc7d") + {:ok, + %Ibanity.PontoConnect.FinancialInstitution{ + id: "953934eb-229a-4fd2-8675-07794078cc7d", + name: "Fake Bank", + status: "stable", + deprecated: nil, + country: "BE", + bic: "NBBEBEBB203", + bulk_payments_enabled: nil, + bulk_payments_product_types: nil, + future_dated_payments_allowed: nil, + logo_url: nil, + maintenance_from: nil, + maintenance_to: nil, + maintenance_type: nil, + payments_enabled: nil, + payments_product_types: nil, + periodic_payments_enabled: nil, + periodic_payments_product_types: nil, + primary_color: nil, + secondary_color: nil, + shared_brand_name: nil, + shared_brand_reference: nil, + time_zone: nil + }} + + With request + + iex> Ibanity.Request.id("953934eb-229a-4fd2-8675-07794078cc7d") + ...> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.FinancialInstitution.find_public() + {:ok, + %Ibanity.PontoConnect.FinancialInstitution{ + id: "953934eb-229a-4fd2-8675-07794078cc7d", + name: "Fake Bank", + status: "stable", + deprecated: nil, + country: "BE", + bic: "NBBEBEBB203", + bulk_payments_enabled: nil, + bulk_payments_product_types: nil, + future_dated_payments_allowed: nil, + logo_url: nil, + maintenance_from: nil, + maintenance_to: nil, + maintenance_type: nil, + payments_enabled: nil, + payments_product_types: nil, + periodic_payments_enabled: nil, + periodic_payments_product_types: nil, + primary_color: nil, + secondary_color: nil, + shared_brand_name: nil, + shared_brand_reference: nil, + time_zone: nil + }} + """ + def find_public(%Request{} = request \\ %Request{}, id) do + request + |> Request.id(id) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + @doc """ + [Find organization's financial institution by id](https://documentation.ibanity.com/ponto-connect/2/api#get-organization-financial-institution) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument, and a Financial Institution + ID as second argument. + + ## Examples + + With token + + iex> token + ...> |> Ibanity.PontoConnect.FinancialInstitution.find_organization("953934eb-229a-4fd2-8675-07794078cc7d") + {:ok, %Ibanity.PontoConnect.FinancialInstitution{id: "953934eb-229a-4fd2-8675-07794078cc7d", name: "Fake Bank"}} + + With request + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.FinancialInstitution.find_organization("953934eb-229a-4fd2-8675-07794078cc7d") + {:ok, %Ibanity.PontoConnect.FinancialInstitution{id: "953934eb-229a-4fd2-8675-07794078cc7d", name: "Fake Bank"}} + + Error + + iex> token + ...> |> Ibanity.PontoConnect.FinancialInstitution.find_organization("does-not-exist") + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "financialInstitution" + } + } + ]} + """ + def find_organization(%Request{token: token} = request_or_token, id) + when not is_nil(token) do + request_or_token + |> Request.id(id) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def find_organization(%PontoConnect.Token{} = request_or_token, id) do + request_or_token + |> Request.token() + |> find_organization(id) + end + + def find_organization(other, _id) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("FinancialInstitution", other) + end + + @doc false + def key_mapping do + [ + id: {~w(id), :string}, + name: {~w(attributes name), :string}, + status: {~w(attributes status), :string}, + deprecated: {~w(attributes deprecated), :boolean}, + country: {~w(attributes country), :string}, + bic: {~w(attributes bic), :string}, + bulk_payments_enabled: {~w(attributes bulkPaymentsEnabled), :boolean}, + bulk_payments_product_types: {~w(attributes bulkPaymentsProductTypes), :string}, + future_dated_payments_allowed: {~w(attributes futureDatedPaymentsAllowed), :boolean}, + logo_url: {~w(attributes logo_url), :string}, + maintenance_from: {~w(attributes maintenanceFrom), :datetime}, + maintenance_to: {~w(attributes maintenanceTo), :datetime}, + maintenance_type: {~w(attributes maintenanceType), :string}, + payments_enabled: {~w(attributes paymentsEnabled), :boolean}, + payments_product_types: {~w(attributes paymentsProductTypes), :string}, + periodic_payments_enabled: {~w(attributes periodicPaymentsEnabled), :boolean}, + periodic_payments_product_types: {~w(attributes periodicPaymentsProductTypes), :string}, + primary_color: {~w(attributes primaryColor), :string}, + secondary_color: {~w(attributes secondaryColor), :string}, + shared_brand_name: {~w(attributes sharedBrandName), :string}, + shared_brand_reference: {~w(attributes sharedBrandReference), :string}, + time_zone: {~w(attributes timeZone), :string} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/integration.ex b/lib/ibanity/api/ponto_connect/integration.ex new file mode 100644 index 0000000..759ea3b --- /dev/null +++ b/lib/ibanity/api/ponto_connect/integration.ex @@ -0,0 +1,75 @@ +defmodule Ibanity.PontoConnect.Integration do + @moduledoc """ + [Integration](https://documentation.ibanity.com/ponto-connect/api#integration) API wrapper + + #{Ibanity.PontoConnect.CommonDocs.fetch!(:client_token)} + """ + + use Ibanity.Resource + + @api_schema_path ["ponto-connect", "organizations", "integration"] + + defstruct [:id] + + alias Ibanity.PontoConnect + + @doc """ + [Revoke Integration by id](https://documentation.ibanity.com/ponto-connect/2/api#delete-organization-integration) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + Takes a valid organization ID as string as second argument. + + ## Examples + + With token + + iex> Ibanity.PontoConnect.Integration.delete(client_token, "16e79b57-6113-4292-9bfe-87580ff2b317") + {:ok, %Ibanity.PontoConnect.Integration{id: "501d0365-ce90-4c10-8c5f-0fe259908101"}} + + With request + + iex> client_token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Integration.delete("16e79b57-6113-4292-9bfe-87580ff2b317") + {:ok, %Ibanity.PontoConnect.Integration{id: "501d0365-ce90-4c10-8c5f-0fe259908101"}} + + iex> client_token + ...> |> Ibanity.PontoConnect.Integration.delete("does-not-exist") + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "integration" + } + } + ]} + """ + def delete( + %Request{token: token} = request_or_token, + organization_id + ) + when is_bitstring(organization_id) and not is_nil(token) do + request_or_token + |> Request.id(:organization_id, organization_id) + |> Client.execute(:delete, @api_schema_path, __MODULE__) + end + + def delete(%PontoConnect.Token{} = request_token, id) do + request_token + |> Request.token() + |> delete(id) + end + + def delete(other, _id) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Integration", other) + end + + @doc false + def key_mapping, do: [id: {["id"], :string}] +end diff --git a/lib/ibanity/api/ponto_connect/integration_account.ex b/lib/ibanity/api/ponto_connect/integration_account.ex new file mode 100644 index 0000000..3b617b8 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/integration_account.ex @@ -0,0 +1,85 @@ +defmodule Ibanity.PontoConnect.IntegrationAccount do + @moduledoc """ + [Integration Account](https://documentation.ibanity.com/ponto-connect/api#integration-account) API wrapper + + #{Ibanity.PontoConnect.CommonDocs.fetch!(:client_token)} + """ + + use Ibanity.Resource + + @api_schema_path ["ponto-connect", "integrationAccounts"] + + defstruct [ + :id, + :created_at, + :last_accessed_at, + :organization_id, + :financial_institution_id, + :account_id + ] + + alias Ibanity.PontoConnect + + @doc """ + [List Integration Accounts](https://documentation.ibanity.com/ponto-connect/2/api#list-integration-account) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as argument. + + ## Examples + + With client token + + iex> Ibanity.PontoConnect.IntegrationAccount.list(client_token) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.IntegrationAccount{}] + }} + + With request + + iex> client_token |> Ibanity.Request.token() |> Ibanity.PontoConnect.IntegrationAccounts.list() + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.IntegrationAccount{}] + }} + + Error + + iex> invalid_token |> Ibanity.PontoConnect.IntegrationAccount.list() + {:error, + [ + %{ + "code" => "invalidAccessToken", + "detail" => "Your access token is invalid.", + "meta" => %{"requestId" => "00077F000001D3A87F0000011F4066E43AFD1900051"} + } + ]} + + """ + def list(%Request{token: token} = request_or_token) + when not is_nil(token) do + Client.execute(request_or_token, :get, @api_schema_path, __MODULE__) + end + + def list(%PontoConnect.Token{} = request_or_token) do + request_or_token + |> Request.token() + |> list() + end + + def list(other) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Integration Accounts", other) + end + + @doc false + def key_mapping do + [ + id: {["id"], :string}, + created_at: {["attributes", "createdAt"], :datetime}, + last_accessed_at: {["attributes", "lastAccessedAt"], :datetime}, + organization_id: {["relationships", "organization", "data", "id"], :string}, + financial_institution_id: + {["relationships", "financialInstitution", "data", "id"], :string}, + account_id: {["relationships", "account", "data", "id"], :string} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/onboarding_details.ex b/lib/ibanity/api/ponto_connect/onboarding_details.ex new file mode 100644 index 0000000..6423dcb --- /dev/null +++ b/lib/ibanity/api/ponto_connect/onboarding_details.ex @@ -0,0 +1,132 @@ +defmodule Ibanity.PontoConnect.OnboardingDetails do + @moduledoc """ + [Onboarding Details](https://documentation.ibanity.com/ponto-connect/api#onboarding-details) API wrapper + + #{Ibanity.PontoConnect.CommonDocs.fetch!(:client_token)} + """ + + use Ibanity.Resource + + @api_schema_path ["ponto-connect", "onboardingDetails"] + + defstruct [ + :id, + :email, + :first_name, + :last_name, + :address_city, + :address_country, + :address_postal_code, + :address_street_address, + :enterprise_number, + :vat_number, + :phone_number, + :automatic_submission_on_completed_forms, + :initial_financial_institution_id, + :organization_name, + :organization_type, + :preferred_opt_method, + :requested_organization_id, + :partner_reference + ] + + alias Ibanity.PontoConnect + + @doc """ + [Creates Onboarding Details](https://documentation.ibanity.com/ponto-connect/api#create-onboarding-details). + + Returns `{:ok, %__MODULE__{}}` if successful, `{:error, reason}` otherwise. + + ## Example + + Attributes + + iex> attributes = [ + ...> email: "jsmith@example.com", + ...> first_name: "Jo", + ...> last_name: "Smith", + ...> organization_name: "Smith Ltd", + ...> enterprise_number: "0999999999", + ...> vat_number: "BE0999999999", + ...> address_street_address: "123 Main St", + ...> address_country: "BE", + ...> address_postal_code: "1000", + ...> address_city: "Brussels", + ...> phone_number: "+32484000000", + ...> initial_financial_institution_id: "953934eb-229a-4fd2-8675-07794078cc7d" + ...> ] + + With token + + iex> Ibanity.PontoConnect.OnboardingDetails.create(client_token, attributes) + {:ok, %Ibanity.PontoConnect.OnboardingDetails{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + + With request + + iex> request = Request.token(client_token) + iex> Ibanity.PontoConnect.OnboardingDetails.create(request, attributes) + {:ok, %Ibanity.PontoConnect.OnboardingDetails{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + """ + def create(%PontoConnect.Token{} = request_or_token, attrs) do + request_or_token + |> Request.token() + |> create(attrs) + end + + def create(%Request{token: token} = request_or_token, attrs) + when not is_nil(token) and is_list(attrs) do + request_or_token + |> Request.attributes(attrs) + |> create() + end + + def create(other, _attrs) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("OnboardingDetails", other) + end + + @doc """ + Same as `create/2`, but `:attributes`, `:account_id`, and `:token` must be set in request. + + ## Examples + + Set id and token to request a BulkPayment + + iex> %PontoConnect.Token{} + ...> |> Request.token() + ...> |> Request.attributes(attributes) + ...> |> PontoConnect.BulkPayment.create() + {:ok, %PontoConnect.BulkPayment{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + """ + def create(%Request{} = request) do + request + |> Request.id("") + |> Client.execute(:post, @api_schema_path, __MODULE__) + end + + @doc false + def key_mapping do + [ + id: {["id"], :string}, + email: {["attributes", "email"], :string}, + first_name: {["attributes", "firstName"], :string}, + last_name: {["attributes", "lastName"], :string}, + address_city: {["attributes", "addressCity"], :string}, + address_country: {["attributes", "addressCountry"], :string}, + address_postal_code: {["attributes", "addressPostalCode"], :string}, + address_street_address: {["attributes", "addressStreetAddress"], :string}, + enterprise_number: {["attributes", "enterpriseNumber"], :string}, + vat_number: {["attributes", "vatNumber"], :string}, + phone_number: {["attributes", "phoneNumber"], :string}, + initial_financial_institution_id: + {["attributes", "initialFinancialInstitutionId"], :string}, + organization_name: {["attributes", "organizationName"], :string}, + organization_type: {["attributes", "organizationType"], :string}, + automatic_submission_on_completed_forms: + {["attributes", "automaticSubmissionOnCompletedForms"], :boolean}, + preferred_opt_method: {["attributes", "preferredOtpMethod"], :string}, + requested_organization_id: {["attributes", "requestedOrganizationId"], :string}, + partner_reference: {["attributes", "partnerReference"], :string} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/payment.ex b/lib/ibanity/api/ponto_connect/payment.ex new file mode 100644 index 0000000..1e3412c --- /dev/null +++ b/lib/ibanity/api/ponto_connect/payment.ex @@ -0,0 +1,257 @@ +defmodule Ibanity.PontoConnect.Payment do + @moduledoc """ + [Payment](https://documentation.ibanity.com/ponto-connect/api#payment) API wrapper + """ + + use Ibanity.Resource + + @api_schema_path ["ponto-connect", "account", "payments"] + + defstruct [ + :id, + :status, + :currency, + :amount, + :end_to_end_id, + :remittance_information, + :remittance_information_type, + :creditor_account_reference, + :creditor_account_reference_type, + :creditor_agent, + :creditor_agent_type, + :creditor_name, + :requested_execution_date, + :redirect + ] + + alias Ibanity.PontoConnect + + @doc """ + [Creates a payment](https://documentation.ibanity.com/ponto-connect/api#create-payment). + + Returns `{:ok, %__MODULE__{}}` if successful, `{:error, reason}` otherwise. + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + Attributes + + iex> attributes = [ + ...> remittance_information: "payment", + ...> remittance_information_type: "unstructured", + ...> requested_execution_date: "2025-05-05", + ...> currency: "EUR", + ...> amount: 59, + ...> creditor_name: "Alex Creditor", + ...> creditor_account_reference: "BE55732022998044", + ...> creditor_account_reference_type: "IBAN", + ...> creditor_agent: "NBBEBEBB203", + ...> creditor_agent_type: "BIC", + ...> redirect_uri: "https://fake-tpp.com/payment-confirmation?payment=123", + ...> end_to_end_id: "1234567890" + ...> ] + + Use attributes and account_or_id: + + iex> Ibanity.PontoConnect.Payment.create(token, account_or_id, attributes) + {:ok, %Ibanity.PontoConnect.Payment{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + + With request + + iex> request = Ibanity.Request.token(token) + iex> Ibanity.PontoConnect.Payment.create(request, account_or_id, attributes) + {:ok, %Ibanity.PontoConnect.Payment{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + """ + def create(%PontoConnect.Token{} = request_or_token, account_or_id, attrs) do + request_or_token + |> Request.token() + |> create(account_or_id, attrs) + end + + def create(%Request{token: token} = request_or_token, account_or_id, attrs) + when not is_nil(token) and is_list(attrs) do + formatted_ids = PontoConnect.RequestUtils.format_ids(%{account_id: account_or_id}) + + request_or_token + |> Request.ids(formatted_ids) + |> Request.attributes(attrs) + |> create() + end + + def create(other, _account_or_id, _attrs) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Payment", other) + end + + @doc """ + Same as create/3, but `:attributes`, `:account_id`, and `:token` must be set in request. + + ## Examples + + Set id and token to request a Payment + + iex> %PontoConnect.Token{} + ...> |> Request.token() + ...> |> Request.id(:account_id, account_id) + ...> |> Request.attributes(attributes) + ...> |> PontoConnect.Payment.create() + {:ok, %PontoConnect.Payment{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + """ + def create(%Request{} = request) do + request + |> Request.id("") + |> Client.execute(:post, @api_schema_path, __MODULE__) + end + + @doc """ + [Find Payment by id](https://documentation.ibanity.com/ponto-connect/2/api#get-payment) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + #{PontoConnect.CommonDocs.fetch!(:account_and_id_second_arg)} + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + IDs + + iex> ids = %{ + ...> account_id: account_or_id, + ...> id: "953934eb-229a-4fd2-8675-07794078cc7d" + ...> } + + With token + + iex> Ibanity.PontoConnect.Payment.find(token, ids) + {:ok, %Ibanity.PontoConnect.Payment{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + With request + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Payment.find(ids) + {:ok, %Ibanity.PontoConnect.Payment{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + iex> Ibanity.PontoConnect.Payment.find(token, %{account_id: account_or_id, id: "does-not-exist"}) + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "payment" + } + } + ]} + """ + def find(%Request{token: token} = request_or_token, ids) + when not is_nil(token) do + formatted_ids = PontoConnect.RequestUtils.format_ids(ids) + + request_or_token + |> Request.ids(formatted_ids) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def find(%PontoConnect.Token{} = request_or_token, ids) do + request_or_token + |> Request.token() + |> find(ids) + end + + def find(other, _ids) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Payment", other) + end + + @doc """ + [Delete a Payment by id](https://documentation.ibanity.com/ponto-connect/2/api#delete-payment) + + #{PontoConnect.CommonDocs.fetch!(:account_and_id_second_arg)} + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + IDs + + iex> ids = %{ + ...> account_id: account_or_id, + ...> id: "953934eb-229a-4fd2-8675-07794078cc7d" + ...> } + + With token + + iex> Ibanity.PontoConnect.Payment.delete(token, ids) + {:ok, %Ibanity.PontoConnect.Payment{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + With request + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Payment.delete(ids) + {:ok, %Ibanity.PontoConnect.Payment{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + Error + + iex> Ibanity.PontoConnect.Payment.delete(token, %{ + ...> id: "does-not-exist", + ...> account_id: account_or_id + ...> }) + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "payment" + } + } + ]} + """ + def delete(%Request{token: token} = request_or_token, ids) + when not is_nil(token) do + formatted_ids = PontoConnect.RequestUtils.format_ids(ids) + + request_or_token + |> Request.ids(formatted_ids) + |> Client.execute(:delete, @api_schema_path, __MODULE__) + end + + def delete(%PontoConnect.Token{} = request_or_token, ids) do + request_or_token + |> Request.token() + |> delete(ids) + end + + def delete(other, _ids) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Payment", other) + end + + @doc false + def key_mapping do + [ + id: {["id"], :string}, + status: {["attributes", "status"], :string}, + currency: {["attributes", "currency"], :string}, + amount: {["attributes", "amount"], :number}, + end_to_end_id: {["attributes", "endToEndId"], :string}, + remittance_information: {["attributes", "remittanceInformation"], :string}, + remittance_information_type: {["attributes", "remittanceInformationType"], :string}, + creditor_account_reference: {["attributes", "creditorAccountReference"], :string}, + creditor_account_reference_type: {["attributes", "creditorAccountReferenceType"], :string}, + creditor_agent: {["attributes", "creditorAgent"], :string}, + creditor_agent_type: {["attributes", "creditorAgentType"], :string}, + creditor_name: {["attributes", "creditorName"], :string}, + requested_execution_date: {["attributes", "requestedExecutionDate"], :date}, + redirect: {["links", "redirect"], :string} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/payment_activation_request.ex b/lib/ibanity/api/ponto_connect/payment_activation_request.ex new file mode 100644 index 0000000..1a43589 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/payment_activation_request.ex @@ -0,0 +1,81 @@ +defmodule Ibanity.PontoConnect.PaymentActivationRequest do + @moduledoc """ + [Payment Activation Request](https://documentation.ibanity.com/ponto-connect/api#payment-activation-request) API wrapper + """ + + use Ibanity.Resource + + @api_schema_path ["ponto-connect", "paymentActivationRequests"] + + defstruct [ + :id, + :redirect + ] + + alias Ibanity.PontoConnect + + @doc """ + [Creates a Payment Activation Request](https://documentation.ibanity.com/ponto-connect/api#request-payment-activation). + + Returns `{:ok, %__MODULE__{}}` if successful, `{:error, reason}` otherwise. + + ## Examples + + Attributes + + iex> attributes = [redirect_uri: "https://fake-tpp.com/payment-activation-request-confirmation"] + + With token + + iex> Ibanity.PontoConnect.PaymentActivationRequest.create(token, attributes) + {:ok, %Ibanity.PontoConnect.PaymentActivationRequest{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + + With request + + iex> request = Ibanity.Request.token(token) + iex> Ibanity.PontoConnect.PaymentActivationRequest.create(request, attributes) + {:ok, %Ibanity.PontoConnect.PaymentActivationRequest{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + """ + def create(%PontoConnect.Token{} = request_or_token, attrs) do + request_or_token + |> Request.token() + |> create(attrs) + end + + def create(%Request{token: token} = request_or_token, attrs) + when not is_nil(token) and is_list(attrs) do + request_or_token + |> Request.attributes(attrs) + |> create() + end + + def create(other, _attrs) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("PaymentActivationRequest", other) + end + + @doc """ + Same as `create/2`, but `:attributes` and `:token` must be set in request. + + ## Examples + + Set id and token to create a PaymentActivationRequest + + iex> token + ...> |> Request.token() + ...> |> Request.attributes(attributes) + ...> |> PontoConnect.PaymentActivationRequest.create() + {:ok, %PontoConnect.PaymentActivationRequest{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + """ + def create(%Request{} = request) do + Client.execute(request, :post, @api_schema_path, __MODULE__) + end + + @doc false + def key_mapping do + [ + id: {["id"], :string}, + redirect: {["links", "redirect"], :string} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/payment_request.ex b/lib/ibanity/api/ponto_connect/payment_request.ex new file mode 100644 index 0000000..86f3335 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/payment_request.ex @@ -0,0 +1,248 @@ +defmodule Ibanity.PontoConnect.PaymentRequest do + @moduledoc """ + [Payment Request](https://documentation.ibanity.com/ponto-connect/api#payment-request) API wrapper + """ + + use Ibanity.Resource + + @api_schema_path ["ponto-connect", "account", "paymentRequests"] + + defstruct [ + :id, + :currency, + :amount, + :end_to_end_id, + :remittance_information, + :remittance_information_type, + :creditor_account_reference, + :creditor_account_reference_type, + :closed_at, + :debtor_account_reference, + :debtor_account_reference_type, + :redirect_uri, + :signed_at, + :signing_uri + ] + + alias Ibanity.PontoConnect + + @doc """ + [Creates a Payment Request](https://documentation.ibanity.com/ponto-connect/api#create-payment-request). + + Returns `{:ok, %__MODULE__{}}` if successful, `{:error, reason}` otherwise. + + ## Example + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + Attributes + + iex> attributes = [ + ...> remittanceInformation: "payment-request", + ...> remittanceInformationType: "unstructured", + ...> amount: 0.5, + ...> endToEndId: "4874366da78549e0b3014a86cd646dc4", + ...> redirectUri: "https://fake-tpp.com/payment-request-confirmation?paymentRequest=123" + ...> ] + + With token + + iex> Ibanity.PontoConnect.PaymentRequest.create(token, account_or_id, attributes) + {:ok, %Ibanity.PontoConnect.PaymentRequest{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + + With request + + iex> request = Ibanity.Request.token(token) + iex> Ibanity.PontoConnect.PaymentRequest.create(request, account_or_id, attributes) + {:ok, %PontoConnect.PaymentRequest{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + """ + def create(%PontoConnect.Token{} = request_or_token, account_or_id, attrs) do + request_or_token + |> Request.token() + |> create(account_or_id, attrs) + end + + def create(%Request{token: token} = request_or_token, account_or_id, attrs) + when not is_nil(token) and is_list(attrs) do + formatted_ids = PontoConnect.RequestUtils.format_ids(%{account_id: account_or_id}) + + request_or_token + |> Request.ids(formatted_ids) + |> Request.attributes(attrs) + |> create() + end + + def create(other, _account_or_id, _attrs) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("PaymentRequest", other) + end + + @doc """ + Same as create/3, but `:attributes`, `:account_id`, and `:token` must be set in request. + + ## Examples + + Set id and token to create a PaymentRequest + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.id(:account_id, account_id) + ...> |> Ibanity.Request.attributes(attributes) + ...> |> Ibanity.PontoConnect.PaymentRequest.create() + {:ok, %Ibanity.PontoConnect.PaymentRequest{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + """ + def create(%Request{} = request) do + request + |> Request.id("") + |> Client.execute(:post, @api_schema_path, __MODULE__) + end + + @doc """ + [Find Payment Request by id](https://documentation.ibanity.com/ponto-connect/2/api#get-payment-request) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + #{PontoConnect.CommonDocs.fetch!(:account_and_id_second_arg)} + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + IDs + + iex> ids = %{ + ...> account_id: account_or_id, + ...> id: "953934eb-229a-4fd2-8675-07794078cc7d" + ...> } + + With token + + iex> Ibanity.PontoConnect.PaymentRequest.find(token, ids) + {:ok, %Ibanity.PontoConnect.PaymentRequest{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + With request + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.PaymentRequest.find(ids) + {:ok, %Ibanity.PontoConnect.PaymentRequest{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + Error + + iex> Ibanity.PontoConnect.PaymentRequest.find(token, %{account_id: account_or_id, id: "does-not-exist"}) + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "paymentRequest" + } + } + ]} + """ + def find(%Request{} = request, %{account_id: account_id, id: id} = ids) + when not is_nil(account_id) and not is_nil(id) do + formatted_ids = PontoConnect.RequestUtils.format_ids(ids) + + request + |> Request.ids(formatted_ids) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def find(%PontoConnect.Token{} = token, ids) do + token + |> Request.token() + |> find(ids) + end + + def find(other, _id) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("PaymentRequest", other) + end + + @doc """ + [Delete a Payment Request by id](https://documentation.ibanity.com/ponto-connect/2/api#delete-payment-request) + + #{PontoConnect.CommonDocs.fetch!(:account_and_id_second_arg)} + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + + With token + + iex> Ibanity.PontoConnect.PaymentRequest.delete(token, ids) + {:ok, %Ibanity.PontoConnect.PaymentRequest{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + With request + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.PaymentRequest.delete(%{ + ...> id: "953934eb-229a-4fd2-8675-07794078cc7d", account_id: account_or_id + ...> }) + {:ok, %Ibanity.PontoConnect.PaymentRequest{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + Error + + iex> Ibanity.PontoConnect.PaymentRequest.delete(token, %{ + ...> id: "does-not-exist", + ...> account_id: account_or_id + ...> }) + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "paymentRequest" + } + } + ]} + """ + def delete(%Request{} = request_or_token, %{account_id: account_id} = ids) + when not is_nil(account_id) do + formatted_ids = PontoConnect.RequestUtils.format_ids(ids) + + request_or_token + |> Request.ids(formatted_ids) + |> Client.execute(:delete, @api_schema_path, __MODULE__) + end + + def delete(%PontoConnect.Token{} = request_or_token, ids) do + request_or_token + |> Request.token() + |> delete(ids) + end + + def delete(other, _ids) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("PaymentRequest", other) + end + + @doc false + def key_mapping do + [ + id: {["id"], :string}, + currency: {["attributes", "currency"], :string}, + amount: {["attributes", "amount"], :number}, + end_to_end_id: {["attributes", "endToEndId"], :string}, + remittance_information: {["attributes", "remittanceInformation"], :string}, + remittance_information_type: {["attributes", "remittanceInformationType"], :string}, + creditor_account_reference: {["attributes", "creditorAccountReference"], :string}, + creditor_account_reference_type: {["attributes", "creditorAccountReferenceType"], :string}, + closed_at: {["attributes", "closedAt"], :map}, + debtor_account_reference: {["attributes", "debtorAccountReference"], :map}, + debtor_account_reference_type: {["attributes", "debtorAccountReferenceType"], :map}, + redirect_uri: {["attributes", "redirectUri"], :string}, + signed_at: {["attributes", "signedAt"], :map}, + signing_uri: {["attributes", "signingUri"], :string} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/reauthorization_request.ex b/lib/ibanity/api/ponto_connect/reauthorization_request.ex new file mode 100644 index 0000000..cb3fe7a --- /dev/null +++ b/lib/ibanity/api/ponto_connect/reauthorization_request.ex @@ -0,0 +1,89 @@ +defmodule Ibanity.PontoConnect.ReauthorizationRequest do + @moduledoc """ + [Reauthorization API wrapper](https://documentation.ibanity.com/ponto-connect/2/api#reauthorization-request) + """ + use Ibanity.Resource + + defstruct [:id, :redirect] + + alias Ibanity.PontoConnect + + @api_schema_path ["ponto-connect", "account", "reauthorizationRequests"] + + @doc """ + [Requests an account reauthorization](https://documentation.ibanity.com/ponto-connect/api#request-account-reauthorization). + + Returns `{:ok, %__MODULE__{}}` if successful, `{:error, reason}` otherwise. + + ## Example + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + Attributes + + iex> attributes = [redirect_uri: "https://fake-tpp.com/account-reauthorization-confirmation] + + With token + + iex> Ibanity.PontoConnect.ReauthorizationRequest.create(token, account_or_id, attributes) + {:ok, %Ibanity.PontoConnect.ReauthorizationRequest{ + redirect: "https://authorize.myponto.com/organizations/dd311734-9762-457e-989f-03292a1e55c9/sandbox/integrations/ad15e188-9385-44ef-a641-1cce3852c520/accounts/10cffaaf-f793-4672-bca1-4f150161b97e/reauthorization-requests/21bd0546-4ec2-4da3-8117-56b265255dec" + }} + + With request + + iex> token + ...> Ibanity.Request.token() + ...> |> Ibanity.PontoConnect.ReauthorizationRequest.create(account_or_id, attributes) + {:ok, %Ibanity.PontoConnect.ReauthorizationRequest{ + redirect: "https://authorize.myponto.com/organizations/dd311734-9762-457e-989f-03292a1e55c9/sandbox/integrations/ad15e188-9385-44ef-a641-1cce3852c520/accounts/10cffaaf-f793-4672-bca1-4f150161b97e/reauthorization-requests/21bd0546-4ec2-4da3-8117-56b265255dec" + }} + """ + def create(%PontoConnect.Token{} = request_or_token, account_or_id, attrs) do + request_or_token + |> Request.token() + |> create(account_or_id, attrs) + end + + def create(%Request{token: token} = request_or_token, account_or_id, attrs) + when not is_nil(token) do + formatted_ids = PontoConnect.RequestUtils.format_ids(%{account_id: account_or_id}) + + request_or_token + |> Request.ids(formatted_ids) + |> Request.attributes(attrs) + |> create() + end + + def create(other, _account_or_id, _attrs) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("ReauthorizationRequest", other) + end + + @doc """ + Same as create/2, but `:attributes` and `:token` must be set in request. + + ## Examples + + Set id and token to request a reauthorization + + iex> %Ibanity.PontoConnect.Token{} + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.id(:account_id, account_id) + ...> |> Ibanity.Request.attribute(:redirect_uri, "https://fake-tpp.com/account-reauthorization-confirmation") + ...> |> Ibanity.PontoConnect.Reauthorization.create() + {:ok, %Ibanity.PontoConnect.Reauthorization{ + redirect: "https://authorize.myponto.com/organizations/dd311734-9762-457e-989f-03292a1e55c9/sandbox/integrations/ad15e188-9385-44ef-a641-1cce3852c520/accounts/10cffaaf-f793-4672-bca1-4f150161b97e/reauthorization-requests/21bd0546-4ec2-4da3-8117-56b265255dec" + }} + """ + def create(%Request{} = request), + do: Client.execute(request, :post, @api_schema_path, __MODULE__) + + @doc false + def key_mapping do + [ + redirect: {["links", "redirect"], :string}, + id: {["id"], :string} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/request_utils.ex b/lib/ibanity/api/ponto_connect/request_utils.ex new file mode 100644 index 0000000..33b3978 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/request_utils.ex @@ -0,0 +1,68 @@ +defmodule Ibanity.PontoConnect.RequestUtils do + @moduledoc false + + alias Ibanity.Request + + @doc false + def format_ids(ids) do + for {key, val} <- ids, val != nil do + case val do + %{id: id} -> {key, id} + _ -> {key, val} + end + end + end + + @doc false + def create_token_default_request(%Request{} = request) do + new_attributes = + %{ + client_id: client_id(request), + grant_type: grant_type(request) + } + |> Map.merge(request.attributes) + + %Request{request | attributes: new_attributes} + |> Request.header(:Authorization, "Basic " <> authorization_header(request)) + end + + defp grant_type(%{attributes: attributes}) do + case attributes do + %{refresh_token: _} -> + "refresh_token" + + %{code: _} -> + "authorization_code" + + _ -> + "client_credentials" + end + end + + @doc false + def delete_token_default_request(%Request{} = request) do + new_attributes = %{ + client_id: client_id(request), + token: request.attributes.refresh_token + } + + %Request{request | attributes: new_attributes} + |> Request.header(:Authorization, "Basic " <> authorization_header(request)) + end + + defp authorization_header(request) do + [client_id(request), client_secret(request)] + |> Enum.join(":") + |> Base.encode64() + end + + defp client_secret(request) do + Application.get_env(:ibanity, :applications, []) + |> get_in([request.application, :ponto_connect_client_secret]) + end + + defp client_id(request) do + Application.get_env(:ibanity, :applications, []) + |> get_in([request.application, :ponto_connect_client_id]) + end +end diff --git a/lib/ibanity/api/ponto_connect/sandbox/financial_institution_account.ex b/lib/ibanity/api/ponto_connect/sandbox/financial_institution_account.ex new file mode 100644 index 0000000..765f851 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/sandbox/financial_institution_account.ex @@ -0,0 +1,181 @@ +defmodule Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount do + @moduledoc """ + [Sandbox Financial Institution Account](https://documentation.ibanity.com/ponto-connect/api#financial-institution-account) API wrapper + """ + + use Ibanity.Resource + + @api_schema_path [ + "ponto-connect", + "sandbox", + "financialInstitution", + "financialInstitutionAccounts" + ] + + defstruct [ + :id, + :description, + :product, + :currency, + :subtype, + :available_balance, + :available_balance_changed_at, + :available_balance_reference_date, + :current_balance, + :current_balance_changed_at, + :current_balance_reference_date, + :holder_name, + :reference, + :reference_type + ] + + alias Ibanity.PontoConnect + + @doc """ + [List sandbox Financial Institution Accounts](https://documentation.ibanity.com/ponto-connect/2/api#list-financial-institution-accounts) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as argument. + + #{PontoConnect.CommonDocs.fetch!(:financial_institution_id)} + + ## Examples + + iex> token |> Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount.list(financial_institution_or_id) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount{}] + }} + + iex> token |> Ibanity.Request.token() |> Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount.list(financial_institution_or_id) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount{}] + }} + + iex> invalid_token |> Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount.list(financial_institution_or_id) + {:error, + [ + %{ + "code" => "invalidAccessToken", + "detail" => "Your access token is invalid.", + "meta" => %{"requestId" => "00077F000001D3A87F0000011F4066E43AFD1900051"} + } + ]} + + """ + def list( + %Request{token: token} = request, + financial_institution_or_id + ) + when not is_nil(token) and not is_nil(financial_institution_or_id) do + formatted_ids = + PontoConnect.RequestUtils.format_ids(%{ + id: "", + financial_institution_id: financial_institution_or_id + }) + + request + |> Request.ids(formatted_ids) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def list(%PontoConnect.Token{} = token, financial_institution__or_id) do + token + |> Request.token() + |> list(financial_institution__or_id) + end + + def list(other, _financial_institution__or_id) do + raise ArgumentError, + message: + PontoConnect.Exceptions.token_argument_error_msg("FinancialInstitutionAccount", other) + end + + @doc """ + [Find sandbox Financial Institution Account by id](https://documentation.ibanity.com/ponto-connect/2/api#get-financial-institution-account) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + #{PontoConnect.CommonDocs.fetch!(:financial_institution_and_id_second_arg)} + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:financial_institution_id)} + + iex> token + ...> |> Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount.find(%{ + ...> financial_institution_id: financial_institution_or_id, + ...> id: "953934eb-229a-4fd2-8675-07794078cc7d" + ...> }) + {:ok, %Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount.find(%{ + ...> financial_institution_id: financial_institution_or_id, + ...> id: "953934eb-229a-4fd2-8675-07794078cc7d" + ...> }) + {:ok, %Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + iex> token + ...> |> Ibanity.PontoConnect.Sandbox.FinancialInstitutionAccount.find(%{ + ...> financial_institution_id: financial_institution_or_id, + ...> id: "does-not-exist" + ...> }) + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "Sandbox.FinancialInstitutionAccount" + } + } + ]} + """ + def find( + %Request{} = request_or_token, + %{financial_institution_id: financial_institution_or_id, id: id} = ids + ) + when not is_nil(financial_institution_or_id) and not is_nil(id) and + not is_nil(financial_institution_or_id) do + formatted_ids = PontoConnect.RequestUtils.format_ids(ids) + + request_or_token + |> Request.ids(formatted_ids) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def find(%PontoConnect.Token{} = request_or_token, ids) do + request_or_token + |> Request.token() + |> find(ids) + end + + def find(other, _id) do + raise ArgumentError, + message: + PontoConnect.Exceptions.token_argument_error_msg("FinancialInstitutionAccount", other) + end + + @doc false + def key_mapping do + [ + id: {["id"], :string}, + description: {["attributes", "description"], :string}, + product: {["attributes", "product"], :string}, + reference: {["attributes", "reference"], :string}, + currency: {["attributes", "currency"], :string}, + subtype: {["attributes", "subtype"], :string}, + available_balance: {["attributes", "availableBalance"], :number}, + available_balance_changed_at: {["attributes", "availableBalanceChangedAt"], :datetime}, + available_balance_reference_date: + {["attributes", "availableBalanceReferenceDate"], :datetime}, + current_balance: {["attributes", "currentBalance"], :number}, + current_balance_changed_at: {["attributes", "currentBalanceChangedAt"], :datetime}, + current_balance_reference_date: {["attributes", "currentBalanceReferenceDate"], :datetime}, + holder_name: {["attributes", "holderName"], :string}, + reference_type: {["attributes", "referenceType"], :string} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/sandbox/financial_institution_transaction.ex b/lib/ibanity/api/ponto_connect/sandbox/financial_institution_transaction.ex new file mode 100644 index 0000000..2ef4423 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/sandbox/financial_institution_transaction.ex @@ -0,0 +1,404 @@ +defmodule Ibanity.PontoConnect.Sandbox.FinancialInstitutionTransaction do + @moduledoc """ + [Sandbox Financial Institution Transaction](https://documentation.ibanity.com/ponto-connect/api#financial-institution-transaction) API wrapper + """ + + use Ibanity.Resource + + @api_schema_path [ + "ponto-connect", + "sandbox", + "financialInstitution", + "financialInstitutionAccount", + "financialInstitutionTransactions" + ] + + defstruct [ + :id, + :description, + :currency, + :created_at, + :updated_at, + :amount, + :fee, + :additional_information, + :bank_transaction_code, + :card_reference, + :card_reference_type, + :counterpart_name, + :counterpart_reference, + :creditor_id, + :end_to_end_id, + :execution_date, + :mandate_id, + :proprietary_bank_transaction_code, + :purpose_code, + :remittance_information, + :remittance_information_type, + :value_date + ] + + alias Ibanity.PontoConnect + + @doc """ + [Creates a sandbox Financial Institution Transaction](https://documentation.ibanity.com/ponto-connect/api#create-financial-institution-transaction). + + Returns `{:ok, %__MODULE__{}}` if successful, `{:error, reason}` otherwise. + + #{PontoConnect.CommonDocs.fetch!(:financial_institution_and_account_second_arg)} + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:financial_institution_and_account_ids)} + + Attributes + + iex> attributes = [ + ...> valueDate: "2020-05-22T00:00:00Z", + ...> executionDate: "2020-05-25T00:00:00Z", + ...> amount: 84.42, + ...> currency: "EUR", + ...> counterpartName: "Otro Bank", + ...> counterpartReference: "BE9786154282554", + ...> description: "Small Cotton Shoes", + ...> remittanceInformation: "NEW SHOES", + ...> remittanceInformationType: "unstructured", + ...> endToEndId: "ref.243435343", + ...> purposeCode: "CASH", + ...> mandateId: "234", + ...> creditorId: "123498765421", + ...> additionalInformation: "Online payment on fake-tpp.com", + ...> proprietaryBankTransactionCode: "12267", + ...> bankTransactionCode: "PMNT-IRCT-ESCT", + ...> cardReferenceType: "MASKEDPAN", + ...> cardReference: "6666", + ...> fee: 3.14 + ...> ] + + IDs + + iex> ids = %{ + ...> financial_institution_id: financial_institution_or_id, + ...> financial_institution_account_id: financial_institution_account_or_id + ...> } + + Use attributes and ids: + + iex> PontoConnect.Sandbox.FinancialInstitutionTransaction.create(token, ids, attributes) + {:ok, %PontoConnect.Sandbox.FinancialInstitutionTransaction{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + + iex> request = Request.token(token) + iex> PontoConnect.Sandbox.FinancialInstitutionTransaction.create(request, ids, attributes) + {:ok, %PontoConnect.Sandbox.FinancialInstitutionTransaction{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + """ + def create(%PontoConnect.Token{} = request_or_token, ids, attrs) do + request_or_token + |> Request.token() + |> create(ids, attrs) + end + + def create( + %Request{token: token} = request_or_token, + %{ + financial_institution_id: financial_institution_or_id, + financial_institution_account_id: financial_institution_account_or_id + } = ids, + attrs + ) + when not is_nil(token) and not is_nil(financial_institution_or_id) and + not is_nil(financial_institution_account_or_id) and is_list(attrs) do + formatted_ids = PontoConnect.RequestUtils.format_ids(ids) + + request_or_token + |> Request.ids(formatted_ids) + |> Request.attributes(attrs) + |> create() + end + + def create(other, _ids, _attrs) do + raise ArgumentError, + message: + PontoConnect.Exceptions.token_argument_error_msg( + "FinancialInstitutionTransaction", + other + ) + end + + @doc """ + Same as create/3, but `:attributes`, `:account_id`, and `:token` must be set in request. + + ## Examples + + Set id and token to create a FinancialInstitutionTransaction + + iex> %PontoConnect.Token{} + ...> |> Request.token() + ...> |> Request.ids(financial_institution_id: financial_institution_id, financial_institution_account_id: financial_institution_account_id) + ...> |> Request.attributes(attributes) + ...> |> PontoConnect.FinancialInstitutionTransaction.create() + {:ok, %PontoConnect.FinancialInstitutionTransaction{id: "343e64e5-4882-4559-96d0-221c398288f3"}} + """ + def create(%Request{} = request) do + request + |> Request.id("") + |> Client.execute(:post, @api_schema_path, __MODULE__) + end + + @doc """ + [List sandbox Financial Institution Transactions](https://documentation.ibanity.com/ponto-connect/2/api#list-financial-institution-transactions) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as argument. + + #{PontoConnect.CommonDocs.fetch!(:financial_institution_and_account_second_arg)} + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:financial_institution_and_account_ids)} + + Use `financial_institution_or_id` and `financial_institution_account_or_id` to list Financial Institution Transactions + + iex> token |> Ibanity.PontoConnect.Sandbox.FinancialInstitutionTransaction.list(%{ + ...> financial_institution_id: financial_institution_or_id, + ...> financial_institution_account_id: financial_institution_account_or_id + ...> }) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Sandbox.FinancialInstitutionTransaction{}] + }} + + iex> token |> Ibanity.Request.token() |> Ibanity.PontoConnect.Sandbox.FinancialInstitutionTransaction.list(financial_institution_or_id) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Sandbox.FinancialInstitutionTransaction{}] + }} + + iex> invalid_token |> Ibanity.PontoConnect.Sandbox.FinancialInstitutionTransaction.list(financial_institution_or_id) + {:error, + [ + %{ + "code" => "invalidAccessToken", + "detail" => "Your access token is invalid.", + "meta" => %{"requestId" => "00077F000001D3A87F0000011F4066E43AFD1900051"} + } + ]} + + """ + def list( + %Request{token: token} = request_or_token, + %{ + financial_institution_id: financial_institution_or_id, + financial_institution_account_id: financial_institution_account_or_id + } = ids + ) + when not is_nil(token) and not is_nil(financial_institution_or_id) and + not is_nil(financial_institution_account_or_id) do + formatted_ids = + ids + |> Map.put_new(:id, "") + |> PontoConnect.RequestUtils.format_ids() + + request_or_token + |> Request.ids(formatted_ids) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def list(%PontoConnect.Token{} = token, ids) do + token + |> Request.token() + |> list(ids) + end + + def list(other, _ids) do + raise ArgumentError, + message: + PontoConnect.Exceptions.token_argument_error_msg( + "FinancialInstitutionTransaction", + other + ) + end + + @doc """ + [Find sandbox Financial Institution Transaction by id](https://documentation.ibanity.com/ponto-connect/2/api#get-financial-institution-transaction) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + #{PontoConnect.CommonDocs.fetch!(:financial_institution_and_account_second_arg)} + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:financial_institution_and_account_ids)} + + Use `financial_institution_or_id` and `financial_institution_account_or_id` to find a Financial Institution Transactions + + iex> token + ...> |> Ibanity.PontoConnect.Sandbox.FinancialInstitutionTransaction.find(%{ + ...> financial_institution_id: financial_institution_or_id, + ...> financial_institution_account_id: financial_institution_account_or_id, + ...> id: "953934eb-229a-4fd2-8675-07794078cc7d" + ...> }) + {:ok, %Ibanity.PontoConnect.Sandbox.FinancialInstitutionTransaction{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Sandbox.FinancialInstitutionTransaction.find(%{ + ...> financial_institution_id: financial_institution_or_id, + ...> financial_institution_account_id: financial_institution_account_or_id, + ...> id: "953934eb-229a-4fd2-8675-07794078cc7d" + ...> }) + {:ok, %Ibanity.PontoConnect.Sandbox.FinancialInstitutionTransaction{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + iex> token + ...> |> Ibanity.PontoConnect.Sandbox.FinancialInstitutionTransaction.find(%{ + ...> financial_institution_id: financial_institution_or_id, + ...> financial_institution_account_id: financial_institution_account_or_id, + ...> id: "does-not-exist" + ...> }) + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "Sandbox.FinancialInstitutionTransaction" + } + } + ]} + """ + def find( + %Request{token: token} = request_or_token, + %{ + financial_institution_id: financial_institution_or_id, + financial_institution_account_id: financial_institution_account_or_id, + id: id + } = ids + ) + when not is_nil(token) and not is_nil(financial_institution_or_id) and not is_nil(id) and + not is_nil(financial_institution_account_or_id) do + formatted_ids = PontoConnect.RequestUtils.format_ids(ids) + + request_or_token + |> Request.ids(formatted_ids) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def find(%PontoConnect.Token{} = request_or_token, ids) do + request_or_token + |> Request.token() + |> find(ids) + end + + def find(other, _id) do + raise ArgumentError, + message: + PontoConnect.Exceptions.token_argument_error_msg( + "FinancialInstitutionTransaction", + other + ) + end + + @doc """ + [Updates an existing financial institution transaction](https://documentation.ibanity.com/ponto-connect/api#update-financial-institution-transaction) + + Returns `{:ok, %__MODULE__{}}` if successful, `{:error, reason}` otherwise. + + #{PontoConnect.CommonDocs.fetch!(:financial_institution_and_account_second_arg)} + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:financial_institution_and_account_ids)} + + Attributes + + iex> attributes = [ + ...> counterpart_name: "Otro Bank", + ...> description: "Small Cotton Shoes", + ...> remittance_information: "NEW SHOES", + ...> end_to_end_id: "ref.243435343", + ...> purpose_code: "CASH", + ...> mandate_id: "234", + ...> creditor_id: "123498765421", + ...> additional_information: "Online payment on fake-tpp.com", + ...> proprietary_bank_transaction_code: "12267", + ...> bank_transaction_code: "PMNT-IRCT-ESCT" + ...> ] + + IDs + + iex> ids = %{ + ...> financial_institution_id: financial_institution_or_id, + ...> financial_institution_account_id: financial_institution_account_or_id, + ...> id: financial_institution_transaction_or_id + ...> } + + Use `attributes` and `ids` + + iex> PontoConnect.Sandbox.FinancialInstitutionTransaction.update(token, ids, attributes) + {:ok, %PontoConnect.Sandbox.FinancialInstitutionTransaction{}} + + iex> request = token |> Request.token() |> Request.application(:my_application) + iex> PontoConnect.Sandbox.FinancialInstitutionTransaction.update(request, ids, attributes) + {:ok, %PontoConnect.Sandbox.FinancialInstitutionTransaction{}} + """ + def update(%PontoConnect.Token{} = request_or_token, ids, attrs) do + request_or_token + |> Request.token() + |> update(ids, attrs) + end + + def update( + %Request{token: token} = request_or_token, + %{ + financial_institution_id: financial_institution_or_id, + financial_institution_account_id: financial_institution_account_or_id, + id: id + } = ids, + attrs + ) + when not is_nil(token) and not is_nil(financial_institution_or_id) and not is_nil(id) and + not is_nil(financial_institution_account_or_id) do + formatted_ids = PontoConnect.RequestUtils.format_ids(ids) + + request_or_token + |> Request.ids(formatted_ids) + |> Request.attributes(attrs) + |> Client.execute(:patch, @api_schema_path, __MODULE__) + end + + def update(other, _ids, _attrs) do + raise ArgumentError, + message: + PontoConnect.Exceptions.token_argument_error_msg( + "FinancialInstitutionTransaction", + other + ) + end + + @doc false + def key_mapping do + [ + id: {["id"], :string}, + description: {["attributes", "description"], :string}, + currency: {["attributes", "currency"], :string}, + created_at: {["attributes", "createdAt"], :datetime}, + updated_at: {["attributes", "updatedAt"], :datetime}, + amount: {["attributes", "amount"], :number}, + fee: {["attributes", "fee"], :number}, + additional_information: {["attributes", "additionalInformation"], :string}, + bank_transaction_code: {["attributes", "bankTransactionCode"], :string}, + card_reference: {["attributes", "cardReference"], :string}, + card_reference_type: {["attributes", "cardReferenceType"], :string}, + counterpart_name: {["attributes", "counterpartName"], :string}, + counterpart_reference: {["attributes", "counterpartReference"], :string}, + creditor_id: {["attributes", "creditorId"], :string}, + end_to_end_id: {["attributes", "endToEndId"], :string}, + execution_date: {["attributes", "executionDate"], :datetime}, + mandate_id: {["attributes", "mandateId"], :string}, + proprietary_bank_transaction_code: + {["attributes", "proprietaryBankTransactionCode"], :string}, + purpose_code: {["attributes", "purposeCode"], :string}, + remittance_information: {["attributes", "remittanceInformation"], :string}, + remittance_information_type: {["attributes", "remittanceInformationType"], :string}, + value_date: {["attributes", "valueDate"], :datetime} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/synchronization.ex b/lib/ibanity/api/ponto_connect/synchronization.ex new file mode 100644 index 0000000..4caf4da --- /dev/null +++ b/lib/ibanity/api/ponto_connect/synchronization.ex @@ -0,0 +1,168 @@ +defmodule Ibanity.PontoConnect.Synchronization do + @moduledoc """ + [Synchronization](https://documentation.ibanity.com/ponto-connect/api#synchronization) API wrapper + """ + + use Ibanity.Resource + + @api_schema_path ["ponto-connect", "synchronizations"] + + @resource_type "account" + + defstruct [ + :id, + :subtype, + :status, + :resource_id, + :created_at, + :updated_at, + resource_type: @resource_type, + errors: [] + ] + + alias Ibanity.PontoConnect + + @doc """ + [Creates a new synchronization resource](https://documentation.ibanity.com/ponto-connect/api#create-synchronization). + + *Note that at this moment it only supports `account` as resource type.* + + Returns `{:ok, synchronization}` if successful, `{:error, reason}` otherwise. + + ## Example + + Attributes + + iex> attributes = [ + ...> resource_id: "88099509-ce43-4a49-ba98-115af962d96d", + ...> subtype: "accountDetails", + ...> customer_ip_address: "123.123.123.123" + ...> ] + + With request + + iex> attributes + ...> |> Ibanity.Request.attributes() + ...> |> Ibanity.Request.token(token) + ...> |> Ibanity.PontoConnect.Synchronization.create() + {:ok, %Ibanity.PontoConnect.Synchronization{id: "f92fc927-7c39-48c1-aa4b-2820efbfed00"}} + + With token + + iex> Ibanity.PontoConnect.Synchronization.create(token, attributes) + {:ok, %Ibanity.PontoConnect.Synchronization{id: "f92fc927-7c39-48c1-aa4b-2820efbfed00"}} + """ + def create(%PontoConnect.Token{} = request_or_token, attrs) do + request_or_token + |> Request.token() + |> create(attrs) + end + + def create(%Request{token: token} = request_or_token, attrs) when not is_nil(token) do + request_or_token + |> Request.attributes(attrs) + |> create() + end + + def create(other, _attrs) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Synchronization", other) + end + + @doc """ + Same as create/2, but `:attributes` and `:token` must be set in request. + + ## Examples + + Attributes + + iex> attributes = [ + ...> resource_id: "88099509-ce43-4a49-ba98-115af962d96d", + ...> subtype: "accountDetails", + ...> customer_ip_address: "123.123.123.123" + ...> ] + + Set attributes and token to create a synchronization + + iex> attributes + ...> |> Ibanity.Request.attributes() + ...> |> Ibanity.Request.token(token) + ...> |> Ibanity.PontoConnect.Synchronization.create() + {:ok, %Ibanity.PontoConnect.Synchronization{id: "f92fc927-7c39-48c1-aa4b-2820efbfed00"}} + """ + def create(%Request{} = request) do + request + |> Request.id("") + |> Request.attribute(:resource_type, @resource_type) + |> Request.resource_type(@resource_type) + |> Client.execute(:post, @api_schema_path, __MODULE__) + end + + @doc """ + [Find Synchronization by id](https://documentation.ibanity.com/ponto-connect/2/api#get-synchronization) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument, and a Synchronization + ID as second argument. + + ## Examples + + iex> token + ...> |> Ibanity.PontoConnect.Synchronization.find("953934eb-229a-4fd2-8675-07794078cc7d") + {:ok, %Ibanity.PontoConnect.Synchronization{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Synchronization.find("953934eb-229a-4fd2-8675-07794078cc7d") + {:ok, %Ibanity.PontoConnect.Synchronization{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + iex> Ibanity.PontoConnect.Synchronization.find(token, "does-not-exist") + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "synchronization" + } + } + ]} + """ + def find(%Request{token: token} = request_or_token, id) + when not is_nil(token) do + request_or_token + |> Request.id(id) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def find(%PontoConnect.Token{} = request_or_token, id) do + request_or_token + |> Request.token() + |> find(id) + end + + def find(other, _id) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Synchronization", other) + end + + @doc false + def key_mapping do + [ + id: {~w(id), :string}, + subtype: {~w(attributes subtype), :string}, + status: {~w(attributes status), :string}, + resource_type: {~w(attributes resourceType), :string}, + resource_id: {~w(attributes resourceId), :string}, + customer_online: {~w(attributes customerOnline), :boolean}, + customer_ip_address: {~w(attributes customerIpAddress), :string}, + errors: {~w(attributes errors), :string}, + created_at: {~w(attributes createdAt), :datetime}, + updated_at: {~w(attributes updatedAt), :datetime}, + updated_pending_transactions: + {~w(relationships updatedPendingTransactions links related), :string}, + updated_transactions: {~w(relationships updatedTransactions links related), :string} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/token.ex b/lib/ibanity/api/ponto_connect/token.ex new file mode 100644 index 0000000..cba17e0 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/token.ex @@ -0,0 +1,209 @@ +defmodule Ibanity.PontoConnect.Token do + @moduledoc """ + [Token](https://documentation.ibanity.com/ponto-connect/2/api#token) API wrapper + """ + + use Ibanity.Resource + + alias Ibanity.PontoConnect + + defstruct [ + :access_token, + :expires_in, + :refresh_token, + :scope, + :issued_at, + token_type: "bearer", + application: :default + ] + + @api_schema_path ["ponto-connect", "oauth2", "token"] + @api_schema_delete_path ["ponto-connect", "oauth2", "revoke"] + + @doc """ + Requests an access or client token, or a new token from a refresh token. + + All access token types provide a convenience function that does not require a request struct, but is limitted to + the `:default` application. + + ## Examples + + Request **Initial Access Token** using the authorization code and code verifier from the user linking process + + iex> [ + ...> code: "nwma9L6Ca_Hx_95RZNbYvZbf7bDuw-H7F1s0tiaYt1c.kd9X3R61y8KaEdFvYo_OMdZ5Ufm8EYbpxekYv0RlQRQ", + ...> redirect_uri: "https://fake-tpp.com/ponto-authorization", + ...> code_verifier: "71855516a563705a2f13e4a10375efa2a0e7584ed89accaa69" + ...> ] + ...> |> Ibanity.Request.attributes() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Token.create() + {:ok, + %Ibanity.PontoConnect.Token{ + access_token: "qOYqfBqFHQ8dzEgayUEpWOoU3r6A3AjYSgMr5FMOcH4.UGkNelkyrgZgmfsasI-qYSJ5iyl570rodsSgZtxkdzI", + expires_in: 1799, + refresh_token: "6_-XgJvLqa7gF6pRl10pfMCr3EUPMlxN7rsiclGWugo.8uowPzwsyPNzkhlJw_UR3ZZ2-zCpw1Tksn2GQK_cJoI", + scope: "offline_access ai pi name", + issued_at: ~U[2024-09-19 12:52:00.229666Z], + token_type: "bearer", + application: :default + } + } + + Request access token using the **Refresh Token** + + iex> [refresh_token: "JAuojyAZmuga8giR0hc-xbzXUSXaLzrnj1adUAhr5V8.M7byqLdx2QAApy3qrTecoGMC3egoceNw8K3GYnycsZA"] + ...> |> Ibanity.Request.attributes() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Token.create() + {:ok, + %Ibanity.PontoConnect.Token{ + access_token: "qOYqfBqFHQ8dzEgayUEpWOoU3r6A3AjYSgMr5FMOcH4.UGkNelkyrgZgmfsasI-qYSJ5iyl570rodsSgZtxkdzI", + expires_in: 1799, + refresh_token: "6_-XgJvLqa7gF6pRl10pfMCr3EUPMlxN7rsiclGWugo.8uowPzwsyPNzkhlJw_UR3ZZ2-zCpw1Tksn2GQK_cJoI", + scope: "offline_access ai pi name", + issued_at: ~U[2024-09-19 12:52:00.229666Z], + token_type: "bearer", + application: :default + } + } + + Request **Client Access Token** + + iex> :my_application + ...> Ibanity.Request.application() + ...> Ibanity.PontoConnect.Token.create() + {:ok, + %Ibanity.PontoConnect.Token{ + access_token: "TCgfyT_O9QVTwTbqW9kM7RgHpJDg2g9l1YRTgHRvMy8.CHiN7_tbwv-8w3hJXXnf0CO7KO_IoPsGSbanXNvOi-8", + expires_in: 1799, + refresh_token: nil, + scope: "", + issued_at: ~U[2024-09-19 12:55:13.118113Z], + token_type: "bearer", + application: :default + } + } + """ + def create(request_or_attributes \\ %Request{}) + + def create(attrs) when is_list(attrs) do + attrs + |> Request.attributes() + |> create() + end + + def create(%Request{} = request) do + request + |> Request.resource_type(__MODULE__) + |> PontoConnect.RequestUtils.create_token_default_request() + |> Client.execute_basic(:post, @api_schema_path) + |> put_application(request.application) + end + + defp put_application({:ok, %__MODULE__{} = token}, application), + do: {:ok, %__MODULE__{token | application: application}} + + defp put_application(response, _application), do: response + + @doc """ + Convenience function to receive a new token using a `%Ibanity.PontoConnect.Token{}` struct, + equivalent to using `create/1` with the `:refresh_token` attribute. + + Note: `refresh/1` only works for access tokens with a `:refresh_token` (needs scope `"offline_access"`) + + Equivalent to + + iex> [refresh_token: token.refresh_token] + ...> |> Ibanity.Request.attributes() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Token.create() + {:ok, %Ibanity.PontoConnect.Token{}} + + ## Examples + + iex> attrs = [ + ...> code: "nwma9L6Ca_Hx_95RZNbYvZbf7bDuw-H7F1s0tiaYt1c.kd9X3R61y8KaEdFvYo_OMdZ5Ufm8EYbpxekYv0RlQRQ", + ...> redirect_uri: "https://fake-tpp.com/ponto-authorization", + ...> code_verifier: "71855516a563705a2f13e4a10375efa2a0e7584ed89accaa69" + ...> ] + iex> {:ok, token} = Ibanity.PontoConnect.Token.create(attrs) + iex> Ibanity.PontoConnect.Token.refresh(token) + {:ok, %Ibanity.PontoConnect.Token{}} + """ + def refresh(%__MODULE__{} = token) do + token.application + |> Request.application() + |> Request.attributes(refresh_token: token.refresh_token) + |> create() + end + + @doc """ + [Revoke Refresh Token](https://documentation.ibanity.com/ponto-connect/2/api#revoke-refresh-token) + + ## Examples + + With refresh token as string + + iex> [refresh_token: "H1Sc-bFi3946Xzca5yuUMZDjVz6WuZ061Hkt3V_lpWs.8wJzYLM8vx1ONzaYlMHcCl_OM_nPOzDGcuCAQPqKPAc"] + ...> |> Ibanity.Request.attributes() + ...> |> Ibanity.Request.application(:my_application) + ...> Ibanity.PontoConnect.Token.delete() + {:ok, %{}} + + With token struct + + iex> Ibanity.PontoConnect.Token.delete(token) + {:ok, %{}} + """ + def delete(attrs) when is_list(attrs) do + attrs + |> Request.attributes() + |> delete() + end + + def delete(%__MODULE__{} = token) do + [refresh_token: token.refresh_token] + |> Request.attributes() + |> Request.application(token.application) + |> delete() + end + + def delete(%Request{attributes: %{refresh_token: refresh_token}} = request) + when not is_nil(refresh_token) do + request + |> Request.resource_type(__MODULE__) + |> PontoConnect.RequestUtils.delete_token_default_request() + |> Client.execute_basic(:post, @api_schema_delete_path) + end + + @doc """ + Convenience function to determine if a token is expired. + + Note: The result is based on the UTC datetime of when the token was received, so it will be off + by the transfer time + processing time (up to 1 second). + + ## Example + iex> {:ok, token} = Ibanity.PontoConnect.Token.create() + iex> Ibanity.PontoConnect.Token.expired?(token) + false + """ + def expired?(%__MODULE__{issued_at: issued_at, expires_in: expires_in}) do + expiration_dt = DateTime.add(issued_at, expires_in) + now = DateTime.utc_now() + + DateTime.before?(expiration_dt, now) + end + + @doc false + def key_mapping do + [ + access_token: {~w(access_token), :string}, + expires_in: {~w(expires_in), :integer}, + refresh_token: {~w(refresh_token), :string}, + scope: {~w(scope), :string}, + token_type: {~w(token_type), :string}, + issued_at: {fn _ -> DateTime.utc_now() end, :function} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/transaction.ex b/lib/ibanity/api/ponto_connect/transaction.ex new file mode 100644 index 0000000..10e71e9 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/transaction.ex @@ -0,0 +1,453 @@ +defmodule Ibanity.PontoConnect.Transaction do + @moduledoc """ + [Transactions API wrapper](https://documentation.ibanity.com/ponto-connect/2/api#transaction) + """ + use Ibanity.Resource + + defstruct [ + :id, + :description, + :currency, + :digest, + :created_at, + :updated_at, + :amount, + :fee, + :additional_information, + :bank_transaction_code, + :card_reference, + :card_reference_type, + :counterpart_name, + :counterpart_reference, + :creditor_id, + :end_to_end_id, + :execution_date, + :mandate_id, + :proprietary_bank_transaction_code, + :purpose_code, + :remittance_information, + :remittance_information_type, + :value_date, + :internal_reference, + :account_id + ] + + alias Ibanity.PontoConnect + + @api_schema_path ["ponto-connect", "account", "transactions"] + @api_schema_synchronization_path ["ponto-connect", "synchronization"] + @api_schema_account_path ["ponto-connect", "account"] + + @doc """ + [List transactions](https://documentation.ibanity.com/ponto-connect/2/api#list-transactions) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + Takes a `Ibanity.PontoConnect.Account` or the account id as a string as second argument. + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + With token + + iex> Ibanity.PontoConnect.Token{} |> Ibanity.PontoConnect.Transaction.list(account_or_id) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Transaction{}] + }} + + With request + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.PontoConnect.Transaction.list(account_or_id) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Transaction{}] + }} + + Error + + iex> invalid_token |> Ibanity.PontoConnect.Transaction.list(account_or_id) + {:error, + [ + %{ + "code" => "invalidAccessToken", + "detail" => "Your access token is invalid.", + "meta" => %{"requestId" => "00077F000001D3A87F0000011F4066E43AFD1900051"} + } + ]} + """ + def list(%Request{token: token} = request, account_or_id) + when not is_nil(token) do + formatted_ids = PontoConnect.RequestUtils.format_ids(id: "", account_id: account_or_id) + + request + |> Request.ids(formatted_ids) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def list(%PontoConnect.Token{} = token, account_or_id) do + token + |> Request.token() + |> list(account_or_id) + end + + def list(other, _account_id) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Transactions", other) + end + + @doc """ + [List updated Transactions for synchronizations](https://documentation.ibanity.com/ponto-connect/2/api#list-updated-transactions-for-synchronization) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + Takes a map with the following keys as second argument: + - `:synchronization``Ibanity.PontoConnect.Synchronization` struct or account ID as a string + - `:id` Transaction ID as a string + + #{PontoConnect.CommonDocs.fetch!(:synchronization_id)} + + ## Examples + + With token + + iex> token |> Ibanity.PontoConnect.Transaction.list_updated_for_synchronization(account_or_id) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Transaction{}] + }} + + With request + + iex> token |> + ...> |> Ibanity.Request.token() + ...> |> Ibanity.PontoConnect.Transaction.list_updated_for_synchronization(synchronization_or_id) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Transaction{}] + }} + + Error + + iex> invalid_token + ...> |> Ibanity.PontoConnect.Transaction.list_updated_for_synchronization(synchronization_or_id) + {:error, + [ + %{ + "code" => "invalidAccessToken", + "detail" => "Your access token is invalid.", + "meta" => %{"requestId" => "00077F000001D3A87F0000011F4066E43AFD1900051"} + } + ]} + + iex> + """ + def list_updated_for_synchronization( + %Request{token: token} = request, + synchronization_or_id + ) + when not is_nil(token) do + api_schema_path = List.insert_at(@api_schema_synchronization_path, -1, "updatedTransactions") + + formatted_ids = + PontoConnect.RequestUtils.format_ids(id: "", synchronization_id: synchronization_or_id) + + request + |> Request.ids(formatted_ids) + |> Client.execute(:get, api_schema_path, __MODULE__) + end + + def list_updated_for_synchronization(%PontoConnect.Token{} = token, synchronization_or_id) do + token + |> Request.token() + |> list_updated_for_synchronization(synchronization_or_id) + end + + def list_updated_for_synchronization(other, _synchronization_id) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Transactions", other) + end + + @doc """ + [List updated pending Transactions for synchronizations](https://documentation.ibanity.com/ponto-connect/2/api#list-updated-pending-transactions-for-synchronization) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + Takes a map with the following keys as second argument: + - `:synchronization``Ibanity.PontoConnect.Synchronization` struct or account ID as a string + - `:id` Transaction ID as a string + + #{PontoConnect.CommonDocs.fetch!(:synchronization_id)} + + ## Examples + + With token + + iex> token + ...> |> Ibanity.PontoConnect.Transaction.list_updated_pending_for_synchronization(synchronization_or_id) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Transaction{}] + }} + + With request + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.PontoConnect.Transaction.list_updated_pending_for_synchronization(synchronization_or_id) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Transaction{}] + }} + + Error + + iex> invalid_token + ...> |> Ibanity.PontoConnect.Transaction.list_updated_pending_for_synchronization(synchronization_or_id) + {:error, + [ + %{ + "code" => "invalidAccessToken", + "detail" => "Your access token is invalid.", + "meta" => %{"requestId" => "00077F000001D3A87F0000011F4066E43AFD1900051"} + } + ]} + """ + def list_updated_pending_for_synchronization( + %Request{token: token} = request, + synchronization_or_id + ) + when not is_nil(token) do + api_schema_path = List.insert_at(@api_schema_synchronization_path, -1, "updatedTransactions") + + formatted_ids = + PontoConnect.RequestUtils.format_ids(id: "", synchronization_id: synchronization_or_id) + + request + |> Request.ids(formatted_ids) + |> Client.execute(:get, api_schema_path, __MODULE__) + end + + def list_updated_pending_for_synchronization( + %PontoConnect.Token{} = token, + synchronization_or_id + ) do + token + |> Request.token() + |> list_updated_pending_for_synchronization(synchronization_or_id) + end + + def list_updated_pending_for_synchronization(other, _synchronization_or_id) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Transactions", other) + end + + @doc """ + [List updated pending transactions](https://documentation.ibanity.com/ponto-connect/2/api#pending-transactions) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + Takes a `Ibanity.PontoConnect.Account` or the account id as a string as second argument. + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + With token + + iex> token + ...> |> Ibanity.PontoConnect.Transaction.list_pending_for_account(account_or_id) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Transaction{}] + }} + + With request + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.PontoConnect.Transaction.list_pending_for_account(account_or_id) + {:ok, %Ibanity.Collection{ + items: [%Ibanity.PontoConnect.Transaction{}] + }} + + Error + + iex> invalid_token + ...> |> Ibanity.PontoConnect.Transaction.list_pending_for_account(account_or_id) + {:error, + [ + %{ + "code" => "invalidAccessToken", + "detail" => "Your access token is invalid.", + "meta" => %{"requestId" => "00077F000001D3A87F0000011F4066E43AFD1900051"} + } + ]} + """ + def list_pending_for_account( + %Request{token: token} = request, + account_or_id + ) + when not is_nil(token) do + api_schema_path = List.insert_at(@api_schema_account_path, -1, "pendingTransactions") + formatted_ids = PontoConnect.RequestUtils.format_ids(id: "", account_id: account_or_id) + + request + |> Request.ids(formatted_ids) + |> Client.execute(:get, api_schema_path, __MODULE__) + end + + def list_pending_for_account(%PontoConnect.Token{} = token, account_or_id) do + token + |> Request.token() + |> list_pending_for_account(account_or_id) + end + + def list_pending_for_account(other, _account_or_id) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Transactions", other) + end + + @doc """ + [Find Transaction by id](https://documentation.ibanity.com/ponto-connect/2/api#get-transaction) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + #{PontoConnect.CommonDocs.fetch!(:account_and_id_second_arg)} + + ## Examples + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + iex> %Ibanity.PontoConnect.Token{} + ...> |> Ibanity.PontoConnect.Transaction.find(%{account_id: account_or_id, id: "d0e23b50-e150-403b-aa50-581a2329b5f5"}) + {:ok, %Ibanity.PontoConnect.Transaction{id: "d0e23b50-e150-403b-aa50-581a2329b5f5"}} + + iex> %Ibanity.PontoConnect.Token{} + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Transaction.find(%{account_id: account_or_id, id: "d0e23b50-e150-403b-aa50-581a2329b5f5"}) + {:ok, %Ibanity.PontoConnect.Transaction{id: "d0e23b50-e150-403b-aa50-581a2329b5f5"}} + + iex> %Ibanity.PontoConnect.Token{} + ...> |> Ibanity.PontoConnect.Transaction.find(%{account_id: account_or_id, id: "does-not-exist"}) + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "transaction" + } + } + ]} + """ + def find(%Request{token: token} = request, ids) + when not is_nil(token) do + formatted_ids = PontoConnect.RequestUtils.format_ids(ids) + + request + |> Request.ids(formatted_ids) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def find(%PontoConnect.Token{} = token, ids) do + token + |> Request.token() + |> find(ids) + end + + def find(other, _ids) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Transaction", other) + end + + @doc """ + [Find pending Transaction by id](https://documentation.ibanity.com/ponto-connect/2/api#get-pending-transaction) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + Takes a map with the following keys as second argument: + - `:account_id``Ibanity.PontoConnect.Account` struct or account ID as a string + - `:id` Transaction ID as a string + + #{PontoConnect.CommonDocs.fetch!(:account_id)} + + ## Examples + + iex> token + ...> |> Ibanity.PontoConnect.Transaction.find_pending_for_account(%{account_id: account_or_id, id: "d0e23b50-e150-403b-aa50-581a2329b5f5"}) + {:ok, %Ibanity.PontoConnect.Transaction{id: "d0e23b50-e150-403b-aa50-581a2329b5f5"}} + + iex> token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Transaction.find_pending_for_account(%{account_id: account_or_id, id: "d0e23b50-e150-403b-aa50-581a2329b5f5"}) + {:ok, %Ibanity.PontoConnect.Transaction{id: "d0e23b50-e150-403b-aa50-581a2329b5f5"}} + + iex> token + ...> |> Ibanity.PontoConnect.Transaction.find_pending_for_account(%{account_id: account_or_id, id: "does-not-exist"}) + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "transaction" + } + } + ]} + """ + def find_pending_for_account( + %Request{token: token} = request, + ids + ) + when not is_nil(token) do + formatted_ids = PontoConnect.RequestUtils.format_ids(ids) + + request + |> Request.ids(formatted_ids) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def find_pending_for_account(%PontoConnect.Token{} = token, ids) do + token + |> Request.token() + |> find_pending_for_account(ids) + end + + def find_pending_for_account(other, _ids) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("Transaction", other) + end + + @doc false + def key_mapping do + [ + id: {["id"], :string}, + description: {["attributes", "description"], :string}, + currency: {["attributes", "currency"], :string}, + digest: {["attributes", "digest"], :string}, + created_at: {["attributes", "createdAt"], :datetime}, + updated_at: {["attributes", "updatedAt"], :datetime}, + amount: {["attributes", "amount"], :number}, + fee: {["attributes", "fee"], :number}, + additional_information: {["attributes", "additionalInformation"], :string}, + bank_transaction_code: {["attributes", "bankTransactionCode"], :string}, + card_reference: {["attributes", "cardReference"], :string}, + card_reference_type: {["attributes", "cardReferenceType"], :string}, + counterpart_name: {["attributes", "counterpartName"], :string}, + counterpart_reference: {["attributes", "counterpartReference"], :string}, + creditor_id: {["attributes", "creditorId"], :string}, + end_to_end_id: {["attributes", "endToEndId"], :string}, + execution_date: {["attributes", "executionDate"], :datetime}, + mandate_id: {["attributes", "mandateId"], :string}, + proprietary_bank_transaction_code: + {["attributes", "proprietaryBankTransactionCode"], :string}, + purpose_code: {["attributes", "purposeCode"], :string}, + remittance_information: {["attributes", "remittanceInformation"], :string}, + remittance_information_type: {["attributes", "remittanceInformationType"], :string}, + value_date: {["attributes", "valueDate"], :datetime}, + internal_reference: {["attributes", "internalReference"], :string}, + account_id: {["relationships", "account", "data", "id"], :string} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/usage.ex b/lib/ibanity/api/ponto_connect/usage.ex new file mode 100644 index 0000000..0678684 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/usage.ex @@ -0,0 +1,108 @@ +defmodule Ibanity.PontoConnect.Usage do + @moduledoc """ + [Payment Activation Request](https://documentation.ibanity.com/ponto-connect/api#payment-activation-request) API wrapper + + #{Ibanity.PontoConnect.CommonDocs.fetch!(:client_token)} + """ + + use Ibanity.Resource + + @api_schema_path ["ponto-connect", "organizations", "usage"] + + defstruct [ + :account_count, + :bulk_payment_bundle_count, + :bulk_payment_count, + :payment_account_count, + :payment_count + ] + + alias Ibanity.PontoConnect + + @doc """ + [Find organization's Usage for a given month](https://documentation.ibanity.com/ponto-connect/2/api#get-organization-usage) + + Takes a `Ibanity.PontoConnect.Token`, or a `Ibanity.Request` with set `:token` as first argument. + + Takes a map with the following keys as second argument: + - `:organization_id`: a valid organization ID as string + - `:month`: month of a year in the format `"[year]-[month]"` e.g. `"2024-09"` + + ## Examples + + Attributes + + iex> attributes = %{ + ...> organization_id: "16e79b57-6113-4292-9bfe-87580ff2b317", + ...> month: "2024-09" + ...> } + + With client token + + iex> Ibanity.PontoConnect.Usage.find(client_token, attributes) + {:ok, %Ibanity.PontoConnect.Usage{account_count: 7}} + + With request + + iex> client_token + ...> |> Ibanity.Request.token() + ...> |> Ibanity.Request.application(:my_application) + ...> |> Ibanity.PontoConnect.Usage.find(attributes) + {:ok, %Ibanity.PontoConnect.Sandbox.Usage{id: "953934eb-229a-4fd2-8675-07794078cc7d"}} + + Error + + iex> Ibanity.PontoConnect.Usage.find(client_token, %{organization_id: "does-not-exist", month: "2024-09"}) + {:error, + [ + %{ + "code" => "resourceNotFound", + "detail" => "The requested resource was not found.", + "meta" => %{ + "requestId" => "00077F00000184847F0000011F4066E44223327005A", + "resource" => "Usage" + } + } + ]} + """ + def find( + %Request{token: token} = request_or_token, + %{ + month: month, + organization_id: organization_id + } + ) + when not is_nil(token) and not is_nil(month) and not is_nil(organization_id) do + formatted_ids = [id: month, organization_id: organization_id] + + request_or_token + |> Request.ids(formatted_ids) + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def find(%PontoConnect.Token{} = request_or_token, ids) do + request_or_token + |> Request.token() + |> find(ids) + end + + def find(other, _ids) do + raise ArgumentError, + message: + PontoConnect.Exceptions.token_argument_error_msg( + "FinancialInstitutionTransaction", + other + ) + end + + @doc false + def key_mapping do + [ + account_count: {["attributes", "accountCount"], :number}, + bulk_payment_bundle_count: {["attributes", "bulkPaymentBundleCount"], :number}, + bulk_payment_count: {["attributes", "bulkPaymentCount"], :number}, + payment_account_count: {["attributes", "paymentAccountCount"], :number}, + payment_count: {["attributes", "paymentCount"], :number} + ] + end +end diff --git a/lib/ibanity/api/ponto_connect/user_info.ex b/lib/ibanity/api/ponto_connect/user_info.ex new file mode 100644 index 0000000..bbb9d82 --- /dev/null +++ b/lib/ibanity/api/ponto_connect/user_info.ex @@ -0,0 +1,67 @@ +defmodule Ibanity.PontoConnect.UserInfo do + @moduledoc """ + [User Info](https://documentation.ibanity.com/ponto-connect/api#user-info) API wrapper + """ + + use Ibanity.Resource + + @api_schema_path ["ponto-connect", "userinfo"] + + defstruct [ + :name, + :sub, + :payments_activated, + :payments_activation_requested, + :payment_requests_activated, + :payment_requests_activation_requested, + :onboarding_complete + ] + + alias Ibanity.PontoConnect + + @doc """ + [Get User Info for access token](https://documentation.ibanity.com/ponto-connect/2/api#user-info-object) + + ## Examples + + Use a client token + + iex> Ibanity.PontoConnect.UserInfo.myself(client_token) + {:ok, %Ibanity.PontoConnect.UserInfo{}} + + Or a request with client token set as `:token` + + iex> request = Ibanity.Request.token(client_token) + iex> Ibanity.PontoConnect.UserInfo.myself(request) + {:ok, %Ibanity.PontoConnect.UserInfo{}} + """ + def myself(%PontoConnect.Token{} = request_or_token) do + request_or_token + |> Request.token() + |> myself() + end + + def myself(%Request{token: client_token} = request_or_token) + when not is_nil(client_token) do + request_or_token + |> Client.execute(:get, @api_schema_path, __MODULE__) + end + + def myself(other) do + raise ArgumentError, + message: PontoConnect.Exceptions.token_argument_error_msg("UserInfo", other) + end + + @doc false + def key_mapping do + [ + name: {["name"], :string}, + sub: {["sub"], :string}, + payments_activated: {["paymentsActivated"], :boolean}, + payments_activation_requested: {["paymentsActivationRequested"], :boolean}, + payment_requests_activated: {["paymentRequestsActivated"], :boolean}, + payment_requests_activation_requested: {["paymentRequestsActivationRequested"], :boolean}, + onboarding_complete: {["onboardingComplete"], :boolean} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/account/details_updated.ex b/lib/ibanity/api/webhooks/ponto_connect/account/details_updated.ex new file mode 100644 index 0000000..79b5df1 --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/account/details_updated.ex @@ -0,0 +1,23 @@ +defmodule Ibanity.Webhooks.PontoConnect.Account.DetailsUpdated do + @moduledoc """ + pontoConnect.account.detailsUpdated webhook event + """ + + defstruct [ + :id, + :created_at, + :account_id, + :synchronization_id, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + account_id: {~w(relationships account data id), :string}, + synchronization_id: {~w(relationships synchronization data id), :string}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/account/pending_transactions_created.ex b/lib/ibanity/api/webhooks/ponto_connect/account/pending_transactions_created.ex new file mode 100644 index 0000000..22ef269 --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/account/pending_transactions_created.ex @@ -0,0 +1,25 @@ +defmodule Ibanity.Webhooks.PontoConnect.Account.PendingTransactionsCreated do + @moduledoc """ + pontoConnect.account.pendingTransactionsCreated webhook event + """ + + defstruct [ + :id, + :created_at, + :count, + :account_id, + :synchronization_id, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + count: {~w(attributes count), :string}, + account_id: {~w(relationships account data id), :string}, + synchronization_id: {~w(relationships synchronization data id), :string}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/account/pending_transactions_updated.ex b/lib/ibanity/api/webhooks/ponto_connect/account/pending_transactions_updated.ex new file mode 100644 index 0000000..ba9c488 --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/account/pending_transactions_updated.ex @@ -0,0 +1,25 @@ +defmodule Ibanity.Webhooks.PontoConnect.Account.PendingTransactionsUpdated do + @moduledoc """ + pontoConnect.account.pendingTransactionsUpdated webhook event + """ + + defstruct [ + :id, + :created_at, + :count, + :account_id, + :synchronization_id, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + count: {~w(attributes count), :string}, + account_id: {~w(relationships account data id), :string}, + synchronization_id: {~w(relationships synchronization data id), :string}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/account/reauthorized.ex b/lib/ibanity/api/webhooks/ponto_connect/account/reauthorized.ex new file mode 100644 index 0000000..43048ae --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/account/reauthorized.ex @@ -0,0 +1,21 @@ +defmodule Ibanity.Webhooks.PontoConnect.Account.Reauthorized do + @moduledoc """ + pontoConnect.account.reauthorized webhook event + """ + + defstruct [ + :id, + :created_at, + :account_id, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + account_id: {~w(relationships account data id), :string}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/account/transactions_created.ex b/lib/ibanity/api/webhooks/ponto_connect/account/transactions_created.ex new file mode 100644 index 0000000..d82eaa6 --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/account/transactions_created.ex @@ -0,0 +1,25 @@ +defmodule Ibanity.Webhooks.PontoConnect.Account.TransactionsCreated do + @moduledoc """ + pontoConnect.account.transactionsCreated webhook event + """ + + defstruct [ + :id, + :created_at, + :count, + :account_id, + :synchronization_id, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + count: {~w(attributes count), :number}, + account_id: {~w(relationships account data id), :string}, + synchronization_id: {~w(relationships synchronization data id), :string}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/account/transactions_updated.ex b/lib/ibanity/api/webhooks/ponto_connect/account/transactions_updated.ex new file mode 100644 index 0000000..cffc097 --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/account/transactions_updated.ex @@ -0,0 +1,25 @@ +defmodule Ibanity.Webhooks.PontoConnect.Account.TransactionsUpdated do + @moduledoc """ + pontoConnect.account.transactionsUpdated webhook event + """ + + defstruct [ + :id, + :created_at, + :count, + :account_id, + :synchronization_id, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + count: {~w(attributes count), :number}, + account_id: {~w(relationships account data id), :string}, + synchronization_id: {~w(relationships synchronization data id), :string}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/integration/account_added.ex b/lib/ibanity/api/webhooks/ponto_connect/integration/account_added.ex new file mode 100644 index 0000000..3af71a5 --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/integration/account_added.ex @@ -0,0 +1,21 @@ +defmodule Ibanity.Webhooks.PontoConnect.Integration.AccountAdded do + @moduledoc """ + pontoConnect.integration.accountAdded webhook event + """ + + defstruct [ + :id, + :created_at, + :account_id, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + account_id: {~w(relationships account data id), :string}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/integration/account_revoked.ex b/lib/ibanity/api/webhooks/ponto_connect/integration/account_revoked.ex new file mode 100644 index 0000000..887d346 --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/integration/account_revoked.ex @@ -0,0 +1,21 @@ +defmodule Ibanity.Webhooks.PontoConnect.Integration.AccountRevoked do + @moduledoc """ + pontoConnect.integration.accoRevoked webhook event + """ + + defstruct [ + :id, + :created_at, + :account_id, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + account_id: {~w(relationships account data id), :string}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/integration/created.ex b/lib/ibanity/api/webhooks/ponto_connect/integration/created.ex new file mode 100644 index 0000000..28bccce --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/integration/created.ex @@ -0,0 +1,19 @@ +defmodule Ibanity.Webhooks.PontoConnect.Integration.Created do + @moduledoc """ + pontoConnect.integration.created webhook event + """ + + defstruct [ + :id, + :created_at, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/integration/revoked.ex b/lib/ibanity/api/webhooks/ponto_connect/integration/revoked.ex new file mode 100644 index 0000000..f54c187 --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/integration/revoked.ex @@ -0,0 +1,19 @@ +defmodule Ibanity.Webhooks.PontoConnect.Integration.Revoked do + @moduledoc """ + pontoConnect.integration.revoked webhook event + """ + + defstruct [ + :id, + :created_at, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/organization/blocked.ex b/lib/ibanity/api/webhooks/ponto_connect/organization/blocked.ex new file mode 100644 index 0000000..c062c9d --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/organization/blocked.ex @@ -0,0 +1,19 @@ +defmodule Ibanity.Webhooks.PontoConnect.Organization.Blocked do + @moduledoc """ + pontoConnect.organization.blocked webhook event + """ + + defstruct [ + :id, + :created_at, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/organization/unblocked.ex b/lib/ibanity/api/webhooks/ponto_connect/organization/unblocked.ex new file mode 100644 index 0000000..820237b --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/organization/unblocked.ex @@ -0,0 +1,19 @@ +defmodule Ibanity.Webhooks.PontoConnect.Organization.Unblocked do + @moduledoc """ + pontoConnect.organization.unblocked webhook event + """ + + defstruct [ + :id, + :created_at, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/payment_request/closed.ex b/lib/ibanity/api/webhooks/ponto_connect/payment_request/closed.ex new file mode 100644 index 0000000..08ac1f3 --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/payment_request/closed.ex @@ -0,0 +1,23 @@ +defmodule Ibanity.Webhooks.PontoConnect.PaymentRequest.Closed do + @moduledoc """ + pontoConnect.paymentRequest.closed webhook event + """ + + defstruct [ + :id, + :created_at, + :account_id, + :payment_request_id, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + account_id: {~w(relationships account data id), :string}, + payment_request_id: {~w(relationships paymentRequest data id), :string}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/synchronization/failed.ex b/lib/ibanity/api/webhooks/ponto_connect/synchronization/failed.ex new file mode 100644 index 0000000..1019788 --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/synchronization/failed.ex @@ -0,0 +1,25 @@ +defmodule Ibanity.Webhooks.PontoConnect.Synchronization.Failed do + @moduledoc """ + pontoConnect.synchronization.failed webhook event + """ + + defstruct [ + :id, + :created_at, + :synchronization_subtype, + :account_id, + :synchronization_id, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + synchronization_subtype: {~w(attributes synchronizationSubtype), :string}, + account_id: {~w(relationships account data id), :string}, + synchronization_id: {~w(relationships synchronization data id), :string}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/webhooks/ponto_connect/synchronization/succeeded_without_change.ex b/lib/ibanity/api/webhooks/ponto_connect/synchronization/succeeded_without_change.ex new file mode 100644 index 0000000..fbc15be --- /dev/null +++ b/lib/ibanity/api/webhooks/ponto_connect/synchronization/succeeded_without_change.ex @@ -0,0 +1,25 @@ +defmodule Ibanity.Webhooks.PontoConnect.Synchronization.SucceededWithoutChange do + @moduledoc """ + pontoConnect.synchronization.succeededWithoutChange webhook event + """ + + defstruct [ + :id, + :created_at, + :synchronization_subtype, + :account_id, + :synchronization_id, + :organization_id + ] + + def key_mapping do + [ + id: {~w(id), :string}, + created_at: {~w(attributes createdAt), :datetime}, + synchronization_subtype: {~w(attributes synchronizationSubtype), :string}, + account_id: {~w(relationships account data id), :string}, + synchronization_id: {~w(relationships synchronization data id), :string}, + organization_id: {~w(relationships organization data id), :string} + ] + end +end diff --git a/lib/ibanity/api/xs2a/account_information_access_request.ex b/lib/ibanity/api/xs2a/account_information_access_request.ex index a423d0f..5510c9c 100644 --- a/lib/ibanity/api/xs2a/account_information_access_request.ex +++ b/lib/ibanity/api/xs2a/account_information_access_request.ex @@ -8,6 +8,7 @@ defmodule Ibanity.Xs2a.AccountInformationAccessRequest do defstruct id: nil, redirect_link: nil, requested_account_references: nil, + status: nil, errors: nil, skip_ibanity_completion_callback: false, allow_financial_institution_redirect_uri: false @@ -76,6 +77,7 @@ defmodule Ibanity.Xs2a.AccountInformationAccessRequest do id: {~w(id), :string}, redirect_link: {~w(links redirect), :string}, requested_account_references: {~w(attributes requestedAccountReferences), :string}, + status: {~w(attributes status), :string}, errors: {~w(attributes errors), :string}, skip_ibanity_completion_callback: {~w(attributes skipIbanityCompletionCallback), :boolean}, allow_financial_institution_redirect_uri: {~w(attributes allowFinancialInstitutionRedirectUri), :boolean}, diff --git a/lib/ibanity/api/xs2a/financial_institution.ex b/lib/ibanity/api/xs2a/financial_institution.ex index 0adf7cd..31e528e 100644 --- a/lib/ibanity/api/xs2a/financial_institution.ex +++ b/lib/ibanity/api/xs2a/financial_institution.ex @@ -34,7 +34,11 @@ defmodule Ibanity.Xs2a.FinancialInstitution do maintenance_to: nil, maintenance_type: nil, time_zone: nil, - pending_transactions_available: nil + pending_transactions_available: nil, + bulk_payment_instructions_limit: nil, + expected_authorization_lifetime: nil, + payment_debtor_account_reference_required: nil, + bulk_payment_debtor_account_reference_required: nil @resource_type "financial_institution" @@ -209,7 +213,11 @@ defmodule Ibanity.Xs2a.FinancialInstitution do maintenance_to: {~w(attributes maintenanceTo), :datetime}, maintenance_type: {~w(attributes maintenanceType), :string}, time_zone: {~w(attributes timeZone), :string}, - pending_transactions_available: {~w(attributes pendingTransactionsAvailable), :boolean} + pending_transactions_available: {~w(attributes pendingTransactionsAvailable), :boolean}, + bulk_payment_instructions_limit: {~w(attributes bulkPaymentInstructionsLimit), :integer}, + expected_authorization_lifetime: {~w(attributes expectedAuthorizationLifetime), :integer}, + payment_debtor_account_reference_required: {~w(attributes paymentDebtorAccountReferenceRequired), :boolean}, + bulk_payment_debtor_account_reference_required: {~w(attributes bulkPaymentDebtorAccountReferenceRequired), :boolean} ] end end diff --git a/lib/ibanity/api_schema.ex b/lib/ibanity/api_schema.ex index cb4b1d2..b8e4cb4 100644 --- a/lib/ibanity/api_schema.ex +++ b/lib/ibanity/api_schema.ex @@ -32,7 +32,8 @@ defmodule Ibanity.ApiSchema do "paymentInitiationRequests" => "https://api.ibanity.com/xs2a/customer/financial-institutions/{financial_institution_id}/payment-initiation-requests/{id}", "paymentInitiationRequest" => %{ - "authorizations" => "https://api.ibanity.com/xs2a/customer/financial-institutions/{financial_institution_id}/payment-initiation-requests/{payment_initiation_request_id}/authorizations" + "authorizations" => + "https://api.ibanity.com/xs2a/customer/financial-institutions/{financial_institution_id}/payment-initiation-requests/{payment_initiation_request_id}/authorizations" }, "pendingTransactions" => "https://api.ibanity.com/xs2a/customer/financial-institutions/{financial_institution_id}/accounts/{account_id}/pending-transactions/{id}", @@ -41,14 +42,17 @@ defmodule Ibanity.ApiSchema do "holdings" => "https://api.ibanity.com/xs2a/customer/financial-institutions/{financial_institution_id}/accounts/{account_id}/holdings/{id}", "accountInformationAccessRequest" => %{ - "accounts" => "https://api.ibanity.com/xs2a/customer/financial-institutions/{financial_institution_id}/account-information-access-requests/{account_information_access_request_id}/accounts", - "authorizations" => "https://api.ibanity.com/xs2a/customer/financial-institutions/{financial_institution_id}/account-information-access-requests/{account_information_access_request_id}/authorizations", + "accounts" => + "https://api.ibanity.com/xs2a/customer/financial-institutions/{financial_institution_id}/account-information-access-requests/{account_information_access_request_id}/accounts", + "authorizations" => + "https://api.ibanity.com/xs2a/customer/financial-institutions/{financial_institution_id}/account-information-access-requests/{account_information_access_request_id}/authorizations" } }, "financialInstitutions" => "https://api.ibanity.com/xs2a/customer/financial-institutions", "self" => "https://api.ibanity.com/xs2a/customer", "synchronizations" => "https://api.ibanity.com/xs2a/customer/synchronizations/{id}", - "transactionDeleteRequests" => "https://api.ibanity.com/xs2a/customer/transaction-delete-requests" + "transactionDeleteRequests" => + "https://api.ibanity.com/xs2a/customer/transaction-delete-requests" }, "customerAccessTokens" => "https://api.ibanity.com/xs2a/customer-access-tokens", "financialInstitutions" => "https://api.ibanity.com/financial-institutions/{id}", @@ -93,9 +97,8 @@ defmodule Ibanity.ApiSchema do %{ "consent" => %{ "processingOperations" => - "https://api.ibanity.com/consent/consents/{consent_id}/processing-operations", - "validations" => - "https://api.ibanity.com/consent/consents/{consent_id}/validations", + "https://api.ibanity.com/consent/consents/{consent_id}/processing-operations", + "validations" => "https://api.ibanity.com/consent/consents/{consent_id}/validations" }, "consents" => "https://api.ibanity.com/consent/consents/{id}" } @@ -104,13 +107,12 @@ defmodule Ibanity.ApiSchema do def fetch("https://api.ibanity.com/billing", _, :test) do %{ "xs2a" => %{ - "financialInstitutionStatuses" => "https://api.ibanity.com/billing/products/xs2a/financial-institution-statuses", + "financialInstitutionStatuses" => + "https://api.ibanity.com/billing/products/xs2a/financial-institution-statuses", "customer" => %{ - "report" => - "https://api.ibanity.com/billing/products/xs2a/customer/report" + "report" => "https://api.ibanity.com/billing/products/xs2a/customer/report" } } - } end @@ -124,11 +126,64 @@ defmodule Ibanity.ApiSchema do %{ "xs2a" => %{ "customer" => %{ - "nbbReport" => - "https://api.ibanity.com/reporting/products/xs2a/customer/nbb-report" + "nbbReport" => "https://api.ibanity.com/reporting/products/xs2a/customer/nbb-report" } } + } + end + def fetch("https://api.ibanity.com/ponto-connect", _, :test) do + %{ + "userinfo" => "https://api.ibanity.com/ponto-connect/userinfo", + "account" => %{ + "bulkPayments" => + "https://api.ibanity.com/ponto-connect/accounts/{accountId}/bulk-payments/{bulkPaymentId}", + "paymentRequests" => + "https://api.ibanity.com/ponto-connect/accounts/{accountId}/payment-requests/{paymentRequestId}", + "payments" => + "https://api.ibanity.com/ponto-connect/accounts/{accountId}/payments/{paymentId}", + "pendingTransactions" => + "https://api.ibanity.com/ponto-connect/accounts/{accountId}/pending-transactions/{pendingTransactionId}", + "reauthorizationRequests" => + "https://api.ibanity.com/ponto-connect/accounts/{accountId}/reauthorization-requests", + "transactions" => + "https://api.ibanity.com/ponto-connect/accounts/{accountId}/transactions/{transactionId}" + }, + "accounts" => "https://api.ibanity.com/ponto-connect/accounts/{accountId}", + "financialInstitutions" => + "https://api.ibanity.com/ponto-connect/financial-institutions/{financialInstitutionId}", + "integrationAccounts" => "https://api.ibanity.com/ponto-connect/integration-accounts", + "oauth" => %{ + "toke" => "https://api.ibanity.com/ponto-connect/oauth2/token", + "revok" => "https://api.ibanity.com/ponto-connect/oauth2/revoke" + }, + "onboardingDetails" => "https://api.ibanity.com/ponto-connect/onboarding-details", + "organizations" => %{ + "usage" => + "https://api.ibanity.com/ponto-connect/organizations/{organizationId}/usage/{month}", + "integration" => + "https://api.ibanity.com/ponto-connect/organizations/{organizationId}/integration" + }, + "paymentActivationRequests" => + "https://api.ibanity.com/ponto-connect/payment-activation-requests", + "sandbox" => %{ + "financialInstitution" => %{ + "financialInstitutionAccount" => %{ + "financialInstitutionTransactions" => + "https://api.ibanity.com/ponto-connect/sandbox/financial-institutions/{financialInstitutionId}/financial-institution-accounts/{financialInstitutionAccountId}/financial-institution-transactions/{financialInstitutionTransactionId}" + }, + "financialInstitutionAccounts" => + "https://api.ibanity.com/ponto-connect/sandbox/financial-institutions/{financialInstitutionId}/financial-institution-accounts/{financialInstitutionAccountId}" + } + }, + "synchronization" => %{ + "updatedPendingTransactions" => + "https://api.ibanity.com/ponto-connect/synchronizations/{synchronizationId}/updated-pending-transactions", + "updatedTransactions" => + "https://api.ibanity.com/ponto-connect/synchronizations/{synchronizationId}/updated-transactions" + }, + "synchronizations" => + "https://api.ibanity.com/ponto-connect/synchronizations/{synchronizationId}" } end @@ -148,17 +203,25 @@ defmodule Ibanity.ApiSchema do defp fetch_api_schema(api_url, app_options) do retry with: backoff(), rescue_only: [HTTPoison.Error] do url = api_url <> "/" - res = HTTPoison.get!(url, headers(url, app_options.signature), ssl: app_options.ssl, hackney: [pool: :default]) + + res = + HTTPoison.get!(url, headers(url, app_options.signature), + ssl: app_options.ssl, + hackney: [pool: :default] + ) handle_response(res) after result -> result else - _ -> raise HTTPoison.Error, reason: :timeout + error -> + IO.inspect(error, label: "Fetch API schema") + raise HTTPoison.Error, reason: :timeout end end defp headers(_, nil), do: @base_headers + defp headers(url, signature_options) do key = Keyword.get(signature_options, :signature_key) certificate_id = Keyword.get(signature_options, :certificate_id) @@ -174,6 +237,7 @@ defmodule Ibanity.ApiSchema do defp handle_response(response) do code = response.status_code + cond do code in 200..499 -> response diff --git a/lib/ibanity/client.ex b/lib/ibanity/client.ex index 8051406..1644a65 100644 --- a/lib/ibanity/client.ex +++ b/lib/ibanity/client.ex @@ -7,8 +7,31 @@ defmodule Ibanity.Client do def execute(%Ibanity.Request{} = request, method, uri_path, type \\ nil) do case HttpRequest.build(request, method, uri_path) do - {:ok, http_request} -> execute(http_request, type) - {:error, reason} -> {:error, reason} + {:ok, http_request} -> + body = + if method_has_body?(http_request.method), + do: Jason.encode!(%{data: http_request.data}), + else: "" + + send_request(http_request, body, type) + + {:error, reason} -> + {:error, reason} + end + end + + def execute_basic(%Ibanity.Request{} = request, method, uri_path) do + case HttpRequest.build(request, method, uri_path) do + {:ok, http_request} -> + body = + if method_has_body?(http_request.method), + do: Jason.encode!(http_request.data), + else: "" + + send_request(http_request, body, request.resource_type) + + {:error, reason} -> + {:error, reason} end end @@ -24,19 +47,21 @@ defmodule Ibanity.Client do end end - defp execute(%HttpRequest{method: method, application: application} = request, type) do - body = if method_has_body?(method), do: Jason.encode!(%{data: request.data}), else: "" + defp send_request( + %HttpRequest{method: method, application: application} = request, + body, + type + ) do retry with: Configuration.retry_backoff(), rescue_only: [] do case HTTPoison.request( - method, - request.uri, - body, - request.headers, - options(application) - ) - do - {:ok, res} -> res |> process_response - error -> error + method, + request.uri, + body, + request.headers, + options(application) + ) do + {:ok, res} -> res |> process_response() + error -> error end after {:ok, response} -> response |> handle_response_body(type) @@ -46,7 +71,10 @@ defmodule Ibanity.Client do end defp options(application) do - Keyword.merge([ssl: Configuration.ssl_options(application), hackney: [pool: application]], Configuration.timeout_options()) + Keyword.merge( + [ssl: Configuration.ssl_options(application), hackney: [pool: application]], + Configuration.timeout_options() + ) end defp method_has_body?(method) do @@ -62,16 +90,19 @@ defmodule Ibanity.Client do {:ok, {:ok, body}} code >= 400 and code <= 500 -> - {:ok, {:error, Map.fetch!(body, "errors")}} + {:ok, {:error, fetch_errors(body)}} code >= 501 and code <= 599 -> - {:error, Map.fetch!(body, "errors")} + {:error, fetch_errors(body)} true -> {:error, :unknown_return_code} end end + defp fetch_errors(%{"errors" => errors}), do: errors + defp fetch_errors(%{"error" => _} = error), do: error + defp handle_response_body(%{"message" => reason}, _), do: {:error, reason} defp handle_response_body({:error, _} = error, _), do: error @@ -80,17 +111,22 @@ defmodule Ibanity.Client do collection = data |> Enum.map(&deserialize(&1, type)) - |> Collection.new(meta["paging"], links, meta["synchronizedAt"], meta["latestSynchronization"]) + |> Collection.new( + meta["paging"], + links, + meta["synchronizedAt"], + meta["latestSynchronization"] + ) {:ok, collection} end defp handle_response_body({:ok, %{"data" => data}}, type) - when is_list(data) do + when is_list(data) do collection = - data - |> Enum.map(&deserialize(&1, type)) - |> Collection.new(%{}, %{}) + data + |> Enum.map(&deserialize(&1, type)) + |> Collection.new(%{}, %{}) {:ok, collection} end @@ -98,7 +134,7 @@ defmodule Ibanity.Client do defp handle_response_body({:ok, %{"data" => data}}, type), do: {:ok, deserialize(data, type)} defp handle_response_body({:ok, %{"keys" => keys}}, type) - when is_list(keys) do + when is_list(keys) do collection = keys |> Enum.map(&deserialize(&1, type)) @@ -107,5 +143,11 @@ defmodule Ibanity.Client do {:ok, collection} end - defp handle_response_body({:ok, %{}}, _), do: {:ok, %{}} + defp handle_response_body({:ok, response}, type) do + if response == %{} do + {:ok, %{}} + else + handle_response_body({:ok, %{"data" => response}}, type) + end + end end diff --git a/lib/ibanity/configuration.ex b/lib/ibanity/configuration.ex index ebcc184..d9f3ba7 100644 --- a/lib/ibanity/configuration.ex +++ b/lib/ibanity/configuration.ex @@ -165,8 +165,7 @@ defmodule Ibanity.Configuration do defp add_ca_cert(ssl_options, environment) do case Keyword.get(environment, :ssl_ca) do nil -> - ssl_options - + Keyword.put_new(ssl_options, :cacertfile, :certifi.cacertfile()) cert -> Keyword.put_new(ssl_options, :cacerts, [der_encoded_certificate(cert)]) end diff --git a/lib/ibanity/http_request.ex b/lib/ibanity/http_request.ex index 142de59..dab6f15 100644 --- a/lib/ibanity/http_request.ex +++ b/lib/ibanity/http_request.ex @@ -11,6 +11,8 @@ defmodule Ibanity.HttpRequest do return_type: nil, application: :default + @token_resource_types [Ibanity.PontoConnect.Token] + def build(%Ibanity.Request{} = request, http_method, uri_path, resource_type \\ nil) do case UriUtil.from_request(request, uri_path) do {:ok, uri} -> @@ -39,7 +41,8 @@ defmodule Ibanity.HttpRequest do defp add_signature(request, method, signature_options) do with {:ok, private_key} <- Keyword.fetch(signature_options, :signature_key), {:ok, certificate_id} <- Keyword.fetch(signature_options, :certificate_id), - {:ok, headers} <- Ibanity.Signature.signature_headers(request, method, private_key, certificate_id) do + {:ok, headers} <- + Ibanity.Signature.signature_headers(request, method, private_key, certificate_id) do {:ok, %__MODULE__{request | headers: Keyword.merge(request.headers, headers)}} else {:error, reason} -> {:error, reason} @@ -72,13 +75,20 @@ defmodule Ibanity.HttpRequest do end defp add_customer_access_token(headers, request) do - if request.customer_access_token do - Keyword.put(headers, :Authorization, "Bearer #{request.customer_access_token}") + token = request.customer_access_token || request.token + + if token do + Keyword.put(headers, :Authorization, "Bearer #{request.token}") else headers end end + defp create_data(%{resource_type: resource_type, attributes: attributes}) + when resource_type in @token_resource_types do + attributes + end + defp create_data(request) do data = %{} diff --git a/lib/ibanity/json_deserializer.ex b/lib/ibanity/json_deserializer.ex index f3ae1a3..d1e3530 100644 --- a/lib/ibanity/json_deserializer.ex +++ b/lib/ibanity/json_deserializer.ex @@ -17,9 +17,11 @@ defmodule Ibanity.JsonDeserializer do "paymentInitiationRequest" => Ibanity.Xs2a.PaymentInitiationRequest, "paymentInitiationRequestAuthorization" => Ibanity.Xs2a.PaymentInitiationRequestAuthorization, "periodicPaymentInitiationRequest" => Ibanity.Xs2a.PeriodicPaymentInitiationRequest, - "periodicPaymentInitiationRequestAuthorization" => Ibanity.Xs2a.PeriodicPaymentInitiationRequestAuthorization, + "periodicPaymentInitiationRequestAuthorization" => + Ibanity.Xs2a.PeriodicPaymentInitiationRequestAuthorization, "bulkPaymentInitiationRequest" => Ibanity.Xs2a.BulkPaymentInitiationRequest, - "bulkPaymentInitiationRequestAuthorization" => Ibanity.Xs2a.BulkPaymentInitiationRequestAuthorization, + "bulkPaymentInitiationRequestAuthorization" => + Ibanity.Xs2a.BulkPaymentInitiationRequestAuthorization, "paymentItem" => Ibanity.Xs2a.BulkPaymentInitiationRequest.Payment, "pendingTransaction" => Ibanity.Xs2a.PendingTransaction, "transaction" => Ibanity.Xs2a.Transaction, @@ -32,22 +34,54 @@ defmodule Ibanity.JsonDeserializer do "financialInstitutionStatus" => Ibanity.Billing.Xs2a.FinancialInstitutionStatus, "nbbReport" => Ibanity.Reporting.Xs2a.NbbReport, "nbbReportAiSynchronization" => Ibanity.Reporting.Xs2a.NbbReportAiSynchronization, - "accountInformationAccessRequestAuthorization" => Ibanity.Xs2a.AccountInformationAccessRequestAuthorization, + "accountInformationAccessRequestAuthorization" => + Ibanity.Xs2a.AccountInformationAccessRequestAuthorization, "key" => Ibanity.Webhooks.Key, - "xs2a.synchronization.succeededWithoutChange" => Ibanity.Webhooks.Xs2a.Synchronization.SucceededWithoutChange, + "xs2a.synchronization.succeededWithoutChange" => + Ibanity.Webhooks.Xs2a.Synchronization.SucceededWithoutChange, "xs2a.account.transactionsUpdated" => Ibanity.Webhooks.Xs2a.Account.TransactionsUpdated, "xs2a.account.transactionsCreated" => Ibanity.Webhooks.Xs2a.Account.TransactionsCreated, "xs2a.account.transactionsDeleted" => Ibanity.Webhooks.Xs2a.Account.TransactionsDeleted, - "xs2a.account.pendingTransactionsUpdated" => Ibanity.Webhooks.Xs2a.Account.PendingTransactionsUpdated, - "xs2a.account.pendingTransactionsCreated" => Ibanity.Webhooks.Xs2a.Account.PendingTransactionsCreated, + "xs2a.account.pendingTransactionsUpdated" => + Ibanity.Webhooks.Xs2a.Account.PendingTransactionsUpdated, + "xs2a.account.pendingTransactionsCreated" => + Ibanity.Webhooks.Xs2a.Account.PendingTransactionsCreated, "xs2a.synchronization.failed" => Ibanity.Webhooks.Xs2a.Synchronization.Failed, "xs2a.account.detailsUpdated" => Ibanity.Webhooks.Xs2a.Account.DetailsUpdated, - "xs2a.bulkPaymentInitiationRequest.authorizationCompleted" => Ibanity.Webhooks.Xs2a.BulkPaymentInitiationRequest.AuthorizationCompleted, - "xs2a.paymentInitiationRequest.authorizationCompleted" => Ibanity.Webhooks.Xs2a.PaymentInitiationRequest.AuthorizationCompleted, - "xs2a.periodicPaymentInitiationRequest.authorizationCompleted" => Ibanity.Webhooks.Xs2a.PeriodicPaymentInitiationRequest.AuthorizationCompleted, - "xs2a.bulkPaymentInitiationRequest.statusUpdated" => Ibanity.Webhooks.Xs2a.BulkPaymentInitiationRequest.StatusUpdated, - "xs2a.paymentInitiationRequest.statusUpdated" => Ibanity.Webhooks.Xs2a.PaymentInitiationRequest.StatusUpdated, - "xs2a.periodicPaymentInitiationRequest.statusUpdated" => Ibanity.Webhooks.Xs2a.PeriodicPaymentInitiationRequest.StatusUpdated + "xs2a.bulkPaymentInitiationRequest.authorizationCompleted" => + Ibanity.Webhooks.Xs2a.BulkPaymentInitiationRequest.AuthorizationCompleted, + "xs2a.paymentInitiationRequest.authorizationCompleted" => + Ibanity.Webhooks.Xs2a.PaymentInitiationRequest.AuthorizationCompleted, + "xs2a.periodicPaymentInitiationRequest.authorizationCompleted" => + Ibanity.Webhooks.Xs2a.PeriodicPaymentInitiationRequest.AuthorizationCompleted, + "xs2a.bulkPaymentInitiationRequest.statusUpdated" => + Ibanity.Webhooks.Xs2a.BulkPaymentInitiationRequest.StatusUpdated, + "xs2a.paymentInitiationRequest.statusUpdated" => + Ibanity.Webhooks.Xs2a.PaymentInitiationRequest.StatusUpdated, + "xs2a.periodicPaymentInitiationRequest.statusUpdated" => + Ibanity.Webhooks.Xs2a.PeriodicPaymentInitiationRequest.StatusUpdated, + "pontoConnect.synchronization.succeededWithoutChange" => + Ibanity.Webhooks.PontoConnect.Synchronization.SucceededWithoutChange, + "pontoConnect.synchronization.failed" => Ibanity.Webhooks.PontoConnect.Synchronization.Failed, + "pontoConnect.account.detailsUpdated" => Ibanity.Webhooks.PontoConnect.Account.DetailsUpdated, + "pontoConnect.account.transactionsCreated" => + Ibanity.Webhooks.PontoConnect.Account.TransactionsCreated, + "pontoConnect.account.transactionsUpdated" => + Ibanity.Webhooks.PontoConnect.Account.TransactionsUpdated, + "pontoConnect.account.pendingTransactionsCreated" => + Ibanity.Webhooks.PontoConnect.Account.PendingTransactionsCreated, + "pontoConnect.account.pendingTransactionsUpdated" => + Ibanity.Webhooks.PontoConnect.Account.PendingTransactionsUpdated, + "pontoConnect.account.reauthorized" => Ibanity.Webhooks.PontoConnect.Account.Reauthorized, + "pontoConnect.organization.blocked" => Ibanity.Webhooks.PontoConnect.Organization.Blocked, + "pontoConnect.organization.unblocked" => Ibanity.Webhooks.PontoConnect.Organization.Unblocked, + "pontoConnect.integration.accountAdded" => + Ibanity.Webhooks.PontoConnect.Integration.AccountAdded, + "pontoConnect.integration.accountRevoked" => + Ibanity.Webhooks.PontoConnect.Integration.AccountRevoked, + "pontoConnect.integration.created" => Ibanity.Webhooks.PontoConnect.Integration.Created, + "pontoConnect.integration.revoked" => Ibanity.Webhooks.PontoConnect.Integration.Revoked, + "pontoConnect.paymentRequest.closed" => Ibanity.Webhooks.PontoConnect.PaymentRequest.Closed } def deserialize(item) do @@ -59,23 +93,30 @@ defmodule Ibanity.JsonDeserializer do end def deserialize(item, resource_type) do - return_type = Map.fetch!(@type_mappings, resource_type) + return_type = return_type_module(resource_type) mapping = return_type.key_mapping() keys = - Enum.map(mapping, fn {key, {path, type}} -> - {key, item |> get_in(path) |> deserialize_field(type)} + Enum.map(mapping, fn + {key, {fun, :function}} -> + {key, fun.(item)} + + {key, {path, type}} -> + {key, item |> get_in(path) |> deserialize_field(type)} end) struct(return_type, keys) end + defp return_type_module(key) when is_bitstring(key), do: Map.fetch!(@type_mappings, key) + defp return_type_module(module) when is_atom(module), do: module + defp deserialize_field(nil, _), do: nil defp deserialize_field(field, :string) when is_list(field), do: field defp deserialize_field(field, type) when is_list(field) do - Enum.map(field, &(deserialize(&1, type))) + Enum.map(field, &deserialize(&1, type)) end defp deserialize_field(field, :datetime), do: DateTimeUtil.parse(field) diff --git a/lib/ibanity/request.ex b/lib/ibanity/request.ex index 6bc43b4..166ac25 100644 --- a/lib/ibanity/request.ex +++ b/lib/ibanity/request.ex @@ -24,7 +24,8 @@ defmodule Ibanity.Request do resource_type: nil, resource_ids: [], page: %{}, - query_params: %{} + query_params: %{}, + token: nil @doc """ Creates a new request and sets the application name @@ -135,7 +136,10 @@ defmodule Ibanity.Request do Overrides existing query params with the same name """ def query_params(%__MODULE__{} = request, query_params) do - %__MODULE__{request | query_params: Map.merge(request.query_params, Enum.into(query_params, %{}))} + %__MODULE__{ + request + | query_params: Map.merge(request.query_params, Enum.into(query_params, %{})) + } end @doc """ @@ -161,6 +165,25 @@ defmodule Ibanity.Request do %__MODULE__{request | customer_access_token: token} end + @doc """ + Sets the [token](https://documentation.ibanity.com/ponto-connect/api#token) used in some Ponto Connect requests. + + Since the token is bound to an application, the application is set to `token.application`. + """ + def token(request \\ %__MODULE__{}, token, application \\ :default) + + def token(%__MODULE__{} = request, token, application) + when is_bitstring(token) or is_nil(token) do + %__MODULE__{request | token: token, application: application} + end + + def token( + %__MODULE__{} = request, + %Ibanity.PontoConnect.Token{access_token: token, application: token_application}, + _application + ), + do: token(request, token, token_application) + @doc """ Creates a new request and adds the attribute and its value to it """ @@ -235,6 +258,11 @@ defmodule Ibanity.Request do @doc """ Sets URI template identifiers to their corresponding values. Overrides existing values if identifiers are already present """ + def ids(%__MODULE__{} = request, ids) when is_map(ids) do + ids_keyword_list = Enum.into(ids, []) + ids(request, ids_keyword_list) + end + def ids(%__MODULE__{} = request, ids) when is_list(ids) do %__MODULE__{request | resource_ids: Keyword.merge(request.resource_ids, ids)} end diff --git a/test/lib/http_request_test.exs b/test/lib/http_request_test.exs index 607c015..47d9b61 100644 --- a/test/lib/http_request_test.exs +++ b/test/lib/http_request_test.exs @@ -49,22 +49,22 @@ defmodule Ibanity.HttpRequestTest do |> HttpRequest.build(:get, @api_schema_path) assert res.uri |> String.split(["?", "&"]) |> Enum.sort() == - [ - "https://api.ibanity.com/xs2a/customer/accounts", - "page[after]=a6299d4d-eb81-4dfb-bb1b-b727000b2621", - "page[before]=27e718a7-af87-479f-bf78-b05027080188", - "page[limit]=50" - ] + [ + "https://api.ibanity.com/xs2a/customer/accounts", + "page[after]=a6299d4d-eb81-4dfb-bb1b-b727000b2621", + "page[before]=27e718a7-af87-479f-bf78-b05027080188", + "page[limit]=50" + ] end test "specifies country filter" do {:ok, res} = %Request{} - |> Request.query_params(filter: %{ country: "BE" }) + |> Request.query_params(filter: %{country: "BE"}) |> HttpRequest.build(:get, @api_schema_path) - assert res.uri == "https://api.ibanity.com/xs2a/customer/accounts" <> - "?filter[country]=BE" + assert "https://api.ibanity.com/xs2a/customer/accounts" <> query_params = res.uri + assert query_params =~ "filter[country]=BE" end test "specifies name filter" do @@ -73,13 +73,14 @@ defmodule Ibanity.HttpRequestTest do contains: "Jack" } } + {:ok, res} = %Request{} |> Request.query_params(filter: filter) |> HttpRequest.build(:get, @api_schema_path) - assert res.uri == "https://api.ibanity.com/xs2a/customer/accounts" <> - "?filter[name][contains]=Jack" + assert "https://api.ibanity.com/xs2a/customer/accounts" <> query_params = res.uri + assert query_params =~ "filter[name][contains]=Jack" end end end diff --git a/test/lib/ponto_connect/request_utils_test.exs b/test/lib/ponto_connect/request_utils_test.exs new file mode 100644 index 0000000..85faa00 --- /dev/null +++ b/test/lib/ponto_connect/request_utils_test.exs @@ -0,0 +1,155 @@ +defmodule Ibanity.PontoConnect.RequestUtilsTest do + use ExUnit.Case + + alias Ibanity.PontoConnect.RequestUtils + alias Ibanity.Request + + describe "format_ids/1" do + defmodule TestStruct do + defstruct id: "123-test", something_else: "something" + end + + test "returns a keyword list" do + assert [key: _] = RequestUtils.format_ids(%{key: "value"}) + end + + test "puts id from a struct into the ID map" do + test_struct = %TestStruct{} + ids = %{test_id: test_struct} + + struct_id = test_struct.id + assert [test_id: struct_id] == RequestUtils.format_ids(ids) + end + + test "leaves other ids as they are" do + ids = %{simple_id_string: "abc"} + + assert [simple_id_string: "abc"] == RequestUtils.format_ids(ids) + end + end + + describe "create_token_default_request/1" do + setup do + apps_env = Application.get_env(:ibanity, :applications) + + new_apps_env = + Keyword.update!(apps_env, :default, fn current_env -> + [ + ponto_connect_client_id: "542cbc7a-7968-426c-830f-f9dc1c3c358a", + ponto_connect_client_secret: "test-client-secret" + ] + |> Keyword.merge(current_env) + end) + + Application.put_env(:ibanity, :applications, new_apps_env) + end + + test "returns a request for a client token request" do + expected = %Request{ + attributes: %{ + grant_type: "client_credentials", + client_id: "542cbc7a-7968-426c-830f-f9dc1c3c358a" + }, + headers: [ + {:Authorization, + "Basic NTQyY2JjN2EtNzk2OC00MjZjLTgzMGYtZjlkYzFjM2MzNThhOnRlc3QtY2xpZW50LXNlY3JldA=="}, + {:Accept, "application/json"}, + {:"Content-Type", "application/json"} + ] + } + + assert ^expected = RequestUtils.create_token_default_request(%Request{}) + end + + test "returns a request for a refresh token" do + expected = %Request{ + attributes: %{ + grant_type: "refresh_token", + client_id: "542cbc7a-7968-426c-830f-f9dc1c3c358a", + refresh_token: "example-token" + }, + headers: [ + {:Authorization, + "Basic NTQyY2JjN2EtNzk2OC00MjZjLTgzMGYtZjlkYzFjM2MzNThhOnRlc3QtY2xpZW50LXNlY3JldA=="}, + {:Accept, "application/json"}, + {:"Content-Type", "application/json"} + ] + } + + request = + [refresh_token: "example-token"] + |> Request.attributes() + + assert ^expected = RequestUtils.create_token_default_request(request) + end + + test "returns a request for an initial token" do + expected = %Request{ + attributes: %{ + grant_type: "authorization_code", + client_id: "542cbc7a-7968-426c-830f-f9dc1c3c358a", + code: "example-code", + redirect_uri: "https://example.com/oaut2/return", + code_verifier: "pkce-code-verifier" + }, + headers: [ + {:Authorization, + "Basic NTQyY2JjN2EtNzk2OC00MjZjLTgzMGYtZjlkYzFjM2MzNThhOnRlc3QtY2xpZW50LXNlY3JldA=="}, + {:Accept, "application/json"}, + {:"Content-Type", "application/json"} + ] + } + + request = + [ + code: "example-code", + redirect_uri: "https://example.com/oaut2/return", + code_verifier: "pkce-code-verifier" + ] + |> Request.attributes() + + assert ^expected = RequestUtils.create_token_default_request(request) + end + end + + describe "delete_token_default_request" do + setup do + apps_env = Application.get_env(:ibanity, :applications) + + new_apps_env = + Keyword.update!(apps_env, :default, fn current_env -> + [ + ponto_connect_client_id: "542cbc7a-7968-426c-830f-f9dc1c3c358a", + ponto_connect_client_secret: "test-client-secret" + ] + |> Keyword.merge(current_env) + end) + + Application.put_env(:ibanity, :applications, new_apps_env) + end + + test "returns a request for revoking an access token" do + test_token = "123.test-token" + + expected = + %Request{ + attributes: %{ + client_id: "542cbc7a-7968-426c-830f-f9dc1c3c358a", + token: test_token + }, + headers: [ + {:Authorization, + "Basic NTQyY2JjN2EtNzk2OC00MjZjLTgzMGYtZjlkYzFjM2MzNThhOnRlc3QtY2xpZW50LXNlY3JldA=="}, + {:Accept, "application/json"}, + {:"Content-Type", "application/json"} + ] + } + + request = + Request.attribute(:refresh_token, test_token) + |> RequestUtils.delete_token_default_request() + + assert ^expected = request + end + end +end diff --git a/test/lib/query_params_util_test.exs b/test/lib/query_params_util_test.exs index 224fcda..3427de5 100644 --- a/test/lib/query_params_util_test.exs +++ b/test/lib/query_params_util_test.exs @@ -10,13 +10,13 @@ defmodule Ibanity.QueryParamsUtilTest do end test "simple params" do - res = QueryParamsUtil.encode_query([limit: 10]) + res = QueryParamsUtil.encode_query(limit: 10) assert res == "limit=10" end test "multiple params" do - res = QueryParamsUtil.encode_query([limit: 10, before: "cursor"]) + res = QueryParamsUtil.encode_query(limit: 10, before: "cursor") assert res == "limit=10&before=cursor" end @@ -29,7 +29,8 @@ defmodule Ibanity.QueryParamsUtilTest do res = QueryParamsUtil.encode_query(query_params) - assert res == "start_date=2016-05-24+13%3A26%3A08.003Z&end_date=2016-05-24+13%3A26%3A08.003Z" + assert res == + "start_date=2016-05-24+13%3A26%3A08.003Z&end_date=2016-05-24+13%3A26%3A08.003Z" end test "nested params" do @@ -43,9 +44,11 @@ defmodule Ibanity.QueryParamsUtilTest do } } ] + res = QueryParamsUtil.encode_query(query_params) - assert res == "filter[name][eq]=Einstein&filter[country][eq]=DE" + assert res =~ "filter[country][eq]=DE" + assert res =~ "filter[name][eq]=Einstein" end end end diff --git a/test/lib/request_test.exs b/test/lib/request_test.exs index 1a83d1f..b129152 100644 --- a/test/lib/request_test.exs +++ b/test/lib/request_test.exs @@ -57,6 +57,18 @@ defmodule Ibanity.RequestTest do end end + describe ".token/2" do + test "set the token when passing a PontoConnect.Token struct" do + token = %Ibanity.PontoConnect.Token{access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."} + + request = + %Request{} + |> Request.token(token) + + assert request.token == "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + end + end + describe ".has_customer_access_token?/1" do test "false when creating empty request" do refute Request.has_customer_access_token?(%Request{})