From 1aac4911ed34c85747ef3972f762a46f91ebceef Mon Sep 17 00:00:00 2001 From: Fernando Almeida Date: Thu, 29 Apr 2021 17:39:28 -0300 Subject: [PATCH] Add get transactions by account --- CHANGELOG.md | 1 + lib/pluggy_elixir/transaction.ex | 214 ++++++++++++++++++++++++ test/pluggy_elixir/transaction_test.exs | 214 ++++++++++++++++++++++++ 3 files changed, 429 insertions(+) create mode 100644 lib/pluggy_elixir/transaction.ex create mode 100644 test/pluggy_elixir/transaction_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c3e9d2..2314610 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,5 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Get all webhooks +- Get transactions by account [unreleased]: https://github.com/brainnco/strong_params/compare/main diff --git a/lib/pluggy_elixir/transaction.ex b/lib/pluggy_elixir/transaction.ex new file mode 100644 index 0000000..0c3b9ee --- /dev/null +++ b/lib/pluggy_elixir/transaction.ex @@ -0,0 +1,214 @@ +defmodule PluggyElixir.Transaction do + @moduledoc """ + Handle transactions actions. + """ + + alias PluggyElixir.{Config, HttpClient} + + defstruct [ + :id, + :description, + :description_raw, + :currency_code, + :amount, + :date, + :balance, + :category, + :account_id, + :provider_code, + :status, + :payment_data + ] + + @type t :: %__MODULE__{ + id: binary(), + description: binary(), + description_raw: binary(), + currency_code: binary(), + amount: float(), + date: NaiveDateTime.t(), + balance: float(), + category: binary(), + account_id: binary(), + provider_code: binary(), + status: binary(), + payment_data: payment_data() + } + + @type payment_data :: %{ + payer: identity(), + receiver: identity(), + reason: binary(), + payment_method: binary(), + reference_number: binary() + } + + @type identity :: %{ + type: binary(), + branch_number: binary(), + account_number: binary(), + routing_number: binary(), + document_number: document() + } + + @type document :: %{ + type: binary(), + value: binary() + } + + @transactions_path "/transactions" + @default_page_size 20 + @default_page_number 1 + + @doc """ + List transactions supporting filters (by account and period) and pagination. + + ### Examples + + iex> params = %{ + account_id: "03cc0eff-4ec5-495c-adb3-1ef9611624fc", + from: ~D[2021-04-01], + to: ~D[2021-05-01], + page_size: 100, + page: 1 + } + iex> Transaction.all_by_account(params) + {:ok, + %{ + page: 1, + total: 1, + total_pages: 1, + transactions: [ + %Transaction{ + account_id: "03cc0eff-4ec5-495c-adb3-1ef9611624fc", + amount: 1_500, + balance: 3_000, + category: "Transfer", + currency_code: "BRL", + date: ~N[2021-04-12 00:00:00.000], + description: "TED Example", + description_raw: nil, + id: "6ec156fe-e8ac-4d9a-a4b3-7770529ab01c", + payment_data: %{ + payer: %{ + account_number: "1234-5", + branch_number: "090", + document_number: %{ + type: "CPF", + value: "882.937.076-23" + }, + routing_number: "001", + type: nil + }, + payment_method: "TED", + reason: "Taxa de serviço", + receiver: %{ + account_number: "9876-1", + branch_number: "999", + document_number: %{ + type: "CNPJ", + value: "08.050.608/0001-32" + }, + routing_number: "002", + type: nil + }, + reference_number: "123456789" + }, + provider_code: "123", + status: "POSTED" + } + ] + }} + """ + + @spec all_by_account( + %{ + :account_id => String.t(), + :from => Date.t(), + :to => Date.t(), + optional(:page_size) => integer(), + optional(:page) => integer() + }, + Config.config_overrides() + ) :: + {:ok, %{page: integer(), total_pages: integer(), total: integer(), transactions: [t()]}} + | {:error, PluggyElixir.HttpClient.Error.t() | String.t()} + def all_by_account(params, config_overrides \\ []) + + def all_by_account(%{account_id: _, from: _, to: _} = params, config_overrides) do + @transactions_path + |> HttpClient.get(format_params(params), Config.override(config_overrides)) + |> handle_response + end + + def all_by_account(_params, _config_overrides), + do: {:error, ":account_id, :from, and :to are required"} + + defp handle_response({:ok, %{status: 200, body: body}}) do + result = %{ + page: body["page"], + total_pages: body["totalPages"], + total: body["total"], + transactions: Enum.map(body["results"], &parse_transaction/1) + } + + {:ok, result} + end + + defp handle_response({:error, _reason} = error), do: error + + defp format_params(params) do + [ + accountId: params[:account_id], + from: params[:from], + to: params[:to], + pageSize: Map.get(params, :page_size, @default_page_size), + page: Map.get(params, :page, @default_page_number) + ] + end + + defp parse_transaction(transaction) do + %__MODULE__{ + id: transaction["id"], + description: transaction["description"], + description_raw: transaction["descriptionRaw"], + currency_code: transaction["currencyCode"], + amount: parse_float(transaction["amount"]), + date: NaiveDateTime.from_iso8601!(transaction["date"]), + balance: parse_float(transaction["balance"]), + category: transaction["category"], + account_id: transaction["accountId"], + provider_code: transaction["providerCode"], + status: transaction["status"], + payment_data: parse_payment_data(transaction["paymentData"]) + } + end + + defp parse_payment_data(nil), do: nil + + defp parse_payment_data(payment_data) do + %{ + payer: parse_identity(payment_data["payer"]), + receiver: parse_identity(payment_data["receiver"]), + reason: payment_data["reason"], + payment_method: payment_data["paymentMethod"], + reference_number: payment_data["referenceNumber"] + } + end + + defp parse_identity(identity) do + %{ + type: identity["type"], + branch_number: identity["branchNumber"], + account_number: identity["accountNumber"], + routing_number: identity["routingNumber"], + document_number: %{ + type: get_in(identity, ["documentNumber", "type"]), + value: get_in(identity, ["documentNumber", "value"]) + } + } + end + + defp parse_float(number) when is_float(number), do: number + defp parse_float(number), do: "#{number}" |> Float.parse() |> elem(0) +end diff --git a/test/pluggy_elixir/transaction_test.exs b/test/pluggy_elixir/transaction_test.exs new file mode 100644 index 0000000..adb4798 --- /dev/null +++ b/test/pluggy_elixir/transaction_test.exs @@ -0,0 +1,214 @@ +defmodule PluggyElixir.TransactionTest do + use PluggyElixir.Case + + alias PluggyElixir.Transaction + alias PluggyElixir.HttpClient.Error + + describe "all_by_account/4" do + test "list transactions of an account and period", %{bypass: bypass} do + params = %{ + account_id: "d619cfde-a8d7-4fe0-a10d-6de488bde4e0", + from: ~D[2020-01-01], + to: ~D[2020-02-01] + } + + config_overrides = [host: "http://localhost:#{bypass.port}"] + + create_and_save_api_key() + + bypass_expect(bypass, "GET", "/transactions", fn conn -> + assert conn.query_params == %{ + "accountId" => "d619cfde-a8d7-4fe0-a10d-6de488bde4e0", + "from" => "2020-01-01", + "sandbox" => "true", + "to" => "2020-02-01", + "pageSize" => "20", + "page" => "1" + } + + Conn.resp( + conn, + 200, + ~s<{"total": 3, "totalPages": 1, "page": 1, "results": [{"id": "5d6b9f9a-06aa-491f-926a-15ba46c6366d", "accountId": "03cc0eff-4ec5-495c-adb3-1ef9611624fc", "description": "Rappi", "currencyCode": "BRL", "amount": 41.58, "date": "2020-06-08T00:00:00.000Z", "balance": 41.58, "category": "Online Payment", "status": "POSTED"}, {"id": "6ec156fe-e8ac-4d9a-a4b3-7770529ab01c", "description": "TED Example", "descriptionRaw": null, "currencyCode": "BRL", "amount": 1500, "date": "2021-04-12T00:00:00.000Z", "balance": 3000, "category": "Transfer", "accountId": "03cc0eff-4ec5-495c-adb3-1ef9611624fc", "providerCode": "123", "status": "POSTED", "paymentData": {"payer": {"name": "Tiago Rodrigues Santos", "branchNumber": "090", "accountNumber": "1234-5", "routingNumber": "001", "documentNumber": {"type": "CPF", "value": "882.937.076-23"}}, "reason": "Taxa de serviço", "receiver": {"name": "Pluggy", "branchNumber": "999", "accountNumber": "9876-1", "routingNumber": "002", "documentNumber": {"type": "CNPJ", "value": "08.050.608/0001-32"}}, "paymentMethod": "TED", "referenceNumber": "123456789"}}]} > + ) + end) + + assert {:ok, result} = Transaction.all_by_account(params, config_overrides) + + assert result == %{ + page: 1, + total: 3, + total_pages: 1, + transactions: [ + %Transaction{ + account_id: "03cc0eff-4ec5-495c-adb3-1ef9611624fc", + amount: 41.58, + balance: 41.58, + category: "Online Payment", + currency_code: "BRL", + date: ~N[2020-06-08 00:00:00.000], + description: "Rappi", + description_raw: nil, + id: "5d6b9f9a-06aa-491f-926a-15ba46c6366d", + payment_data: nil, + provider_code: nil, + status: "POSTED" + }, + %Transaction{ + account_id: "03cc0eff-4ec5-495c-adb3-1ef9611624fc", + amount: 1_500, + balance: 3_000, + category: "Transfer", + currency_code: "BRL", + date: ~N[2021-04-12 00:00:00.000], + description: "TED Example", + description_raw: nil, + id: "6ec156fe-e8ac-4d9a-a4b3-7770529ab01c", + payment_data: %{ + payer: %{ + account_number: "1234-5", + branch_number: "090", + document_number: %{ + type: "CPF", + value: "882.937.076-23" + }, + routing_number: "001", + type: nil + }, + payment_method: "TED", + reason: "Taxa de serviço", + receiver: %{ + account_number: "9876-1", + branch_number: "999", + document_number: %{ + type: "CNPJ", + value: "08.050.608/0001-32" + }, + routing_number: "002", + type: nil + }, + reference_number: "123456789" + }, + provider_code: "123", + status: "POSTED" + } + ] + } + end + + test "allow custom pagination", %{bypass: bypass} do + params = %{ + account_id: "d619cfde-a8d7-4fe0-a10d-6de488bde4e0", + from: ~D[2020-01-01], + to: ~D[2020-02-01], + page_size: 1, + page: 2 + } + + config_overrides = [host: "http://localhost:#{bypass.port}"] + + create_and_save_api_key() + + bypass_expect(bypass, "GET", "/transactions", fn conn -> + assert conn.query_params == %{ + "accountId" => "d619cfde-a8d7-4fe0-a10d-6de488bde4e0", + "from" => "2020-01-01", + "sandbox" => "true", + "to" => "2020-02-01", + "pageSize" => "1", + "page" => "2" + } + + Conn.resp( + conn, + 200, + ~s<{"total": 3, "totalPages": 3, "page": 2, "results": [{"id": "5d6b9f9a-06aa-491f-926a-15ba46c6366d", "accountId": "03cc0eff-4ec5-495c-adb3-1ef9611624fc", "description": "Rappi", "currencyCode": "BRL", "amount": 41.58, "date": "2020-06-08T00:00:00.000Z", "balance": 41.58, "category": "Online Payment", "status": "POSTED"}]}> + ) + end) + + assert {:ok, result} = Transaction.all_by_account(params, config_overrides) + + assert result == %{ + page: 2, + total: 3, + total_pages: 3, + transactions: [ + %Transaction{ + account_id: "03cc0eff-4ec5-495c-adb3-1ef9611624fc", + amount: 41.58, + balance: 41.58, + category: "Online Payment", + currency_code: "BRL", + date: ~N[2020-06-08 00:00:00.000], + description: "Rappi", + description_raw: nil, + id: "5d6b9f9a-06aa-491f-926a-15ba46c6366d", + payment_data: nil, + provider_code: nil, + status: "POSTED" + } + ] + } + end + + test "handle empty results", %{bypass: bypass} do + params = %{ + account_id: "d619cfde-a8d7-4fe0-a10d-6de488bde4e0", + from: "invalid-date", + to: ~D[2020-02-01] + } + + config_overrides = [host: "http://localhost:#{bypass.port}"] + + create_and_save_api_key() + + bypass_expect(bypass, "GET", "/transactions", fn conn -> + Conn.resp(conn, 200, ~s<{"total": 0, "totalPages": 0, "page": 1, "results": []}>) + end) + + assert {:ok, result} = Transaction.all_by_account(params, config_overrides) + + assert result == %{ + page: 1, + total: 0, + total_pages: 0, + transactions: [] + } + end + + test "when params are invalid, returns a validation error" do + invalid_params = %{} + + assert Transaction.all_by_account(invalid_params) == + {:error, ":account_id, :from, and :to are required"} + end + + test "when has error to get transactions list, returns that error", %{bypass: bypass} do + params = %{ + account_id: "invalid-account-id", + from: "invalid-date", + to: ~D[2020-02-01] + } + + config_overrides = [host: "http://localhost:#{bypass.port}"] + + create_and_save_api_key() + + bypass_expect(bypass, "GET", "/transactions", fn conn -> + Conn.resp( + conn, + 500, + ~s<{"message": "There was an error processing your request", "code": 500}> + ) + end) + + assert {:error, reason} = Transaction.all_by_account(params, config_overrides) + + assert reason == %Error{ + code: 500, + details: nil, + message: "There was an error processing your request" + } + end + end +end