├── .formatter.exs ├── lib ├── nubank_api.ex └── nubank_api │ ├── bill_summary.ex │ ├── event.ex │ ├── feature.ex │ ├── http.ex │ ├── access.ex │ ├── purchase.ex │ ├── config.ex │ ├── http_wrapper.ex │ ├── features │ ├── bill_summaries.ex │ ├── events.ex │ └── purchases.ex │ └── auth.ex ├── config └── config.exs ├── test ├── nubank_api_test.exs ├── test_helper.exs ├── features │ ├── purchases_test.exs │ ├── events_test.exs │ └── bill_summaries_test.exs └── fixtures │ ├── bills_summaries.json │ ├── events.json │ └── purchases.json ├── .gitignore ├── mix.exs ├── LICENSE ├── README.md └── mix.lock /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /lib/nubank_api.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI do 2 | @moduledoc """ 3 | NubankAPI is the main module. 4 | """ 5 | end 6 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | if Mix.env() == :test do 4 | config :nubank_api, http: NubankAPI.Mock.HTTP 5 | end 6 | -------------------------------------------------------------------------------- /lib/nubank_api/bill_summary.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.BillSummary do 2 | @moduledoc false 3 | 4 | defstruct [:id, :state, :summary] 5 | end 6 | -------------------------------------------------------------------------------- /test/nubank_api_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NubankAPITest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/nubank_api/event.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.Event do 2 | @moduledoc false 3 | 4 | defstruct [:id, :description, :amount, :time, :title, :category] 5 | end 6 | -------------------------------------------------------------------------------- /lib/nubank_api/feature.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.Feature do 2 | @moduledoc false 3 | 4 | defmacro __using__(_) do 5 | quote do 6 | @http Application.get_env(:nubank_api, :http, NubankAPI.HTTPWrapper) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Mox.defmock(NubankAPI.Mock.HTTP, for: NubankAPI.HTTP) 2 | 3 | defmodule NubankAPI.TestHelper do 4 | def load_fixture(fixture) do 5 | "#{__DIR__}/fixtures/#{fixture}.json" 6 | |> File.read!() 7 | |> Poison.decode!() 8 | end 9 | end 10 | 11 | ExUnit.start() 12 | -------------------------------------------------------------------------------- /lib/nubank_api/http.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.HTTP do 2 | @moduledoc """ 3 | Behaviour module 4 | """ 5 | 6 | @type link :: atom 7 | @type access :: NubankAPI.Access.t() 8 | @type body :: Map.t() 9 | @type response_body :: Map.t() 10 | 11 | @callback get(link, access) :: {:ok, response_body} | {:error, any} 12 | @callback post(link, access, body) :: {:ok, response_body} | {:error, any} 13 | end 14 | -------------------------------------------------------------------------------- /lib/nubank_api/access.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.Access do 2 | @moduledoc """ 3 | It's a struct to wrap the information that came from the authentication logic. 4 | 5 | In this struct includes the API access token, the it's type, it's expiration time and 6 | the refresh token, also there are a list of links which correspond the the available 7 | endpoints from thr Nubank API. 8 | """ 9 | 10 | defstruct access_token: nil, 11 | links: %{}, 12 | refresh_before: DateTime.utc_now(), 13 | refresh_token: nil, 14 | token_type: "bearer" 15 | end 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | nubank_api-*.tar 24 | 25 | -------------------------------------------------------------------------------- /lib/nubank_api/purchase.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.Purchase do 2 | @moduledoc false 3 | 4 | defstruct [ 5 | :account, 6 | :acquirer_id, 7 | :amount, 8 | :approved_reasons, 9 | :auth_code, 10 | :capture_mode, 11 | :card, 12 | :category, 13 | :chargebacks, 14 | :charges, 15 | :charges_list, 16 | :country, 17 | :customer, 18 | :event_type, 19 | :id, 20 | :expires_on, 21 | :mcc, 22 | :merchant_id, 23 | :merchant_name, 24 | :original_merchant_name, 25 | :postcode, 26 | :precise_amount, 27 | :recurring, 28 | :secure_code, 29 | :source, 30 | :stand_in, 31 | :status, 32 | :time, 33 | :time_wallclock, 34 | :lon, 35 | :lat, 36 | :_links, 37 | :tags, 38 | :fx 39 | ] 40 | end 41 | -------------------------------------------------------------------------------- /lib/nubank_api/config.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.Config do 2 | @moduledoc """ 3 | This configuration module is responsable for load the essential information from 4 | the configurations and set a default value for the missing configurations. 5 | """ 6 | 7 | @default_token_api_uri "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/AJxL5LBUC2Tx4PB-W6VD1SEIOd2xp14EDQ.aHR0cHM6Ly9wcm9kLWdsb2JhbC1hdXRoLm51YmFuay5jb20uYnIvYXBpL3Rva2Vu" 8 | 9 | def default_headers do 10 | [ 11 | {"Content-Type", "application/json"}, 12 | {"X-Correlation-Id", "WEB-APP.jO4x1"}, 13 | {"User-Agent", 14 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36"}, 15 | {"Origin", "https://conta.nubank.com.br"}, 16 | {"Referer", "https://conta.nubank.com.br/"} 17 | ] 18 | end 19 | 20 | def token_api_uri, 21 | do: Application.get_env(:nubank_api, :token_api_uri) || @default_token_api_uri 22 | end 23 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :nubank_api, 7 | version: "1.2.0", 8 | elixir: "~> 1.7", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | description: description(), 12 | package: package() 13 | ] 14 | end 15 | 16 | def application do 17 | [ 18 | extra_applications: [:logger] 19 | ] 20 | end 21 | 22 | defp deps do 23 | [ 24 | {:httpoison, "~> 1.5"}, 25 | {:poison, "~> 4.0"}, 26 | {:credo, "~> 1.0", only: :dev}, 27 | {:ex_doc, ">= 0.0.0", only: :dev}, 28 | {:mox, "~> 0.5.0", only: :test} 29 | ] 30 | end 31 | 32 | defp description, do: "Nubank API client implementation" 33 | 34 | defp package do 35 | [ 36 | maintainers: ["Jefferson Stachelski"], 37 | licenses: ["MIT"], 38 | links: %{"GitHub" => "https://github.com/jeffhsta/nubank_api"} 39 | ] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nubank API (nubank_api) 2 | 3 | **Work in progress** 4 | 5 | It's an Elixir library that abstract the HTTP request into functions. 6 | 7 | The goal is to facilitate the developers job to integrate with Nubank's API. 8 | 9 | This project was inspired by the repo 10 | [https://github.com/Astrocoders/nubank-api](https://github.com/Astrocoders/nubank-api), thanks for 11 | [Astrocoders](https://github.com/Astrocoders) for the work in figure out Nubank's API endpoints/payloads etc. 12 | 13 | ## Installation 14 | 15 | [Available in Hex](https://hex.pm/packages/nubank_api), the package can be installed 16 | by adding `:nubank_api` to your list of dependencies in `mix.exs`: 17 | 18 | ```elixir 19 | def deps do 20 | [ 21 | {:nubank_api, "~> 1.0.0"} 22 | ] 23 | end 24 | ``` 25 | 26 | Documentation can be found at [https://hexdocs.pm/nubank_api](https://hexdocs.pm/nubank_api). 27 | 28 | ## Contribution 29 | 30 | ### Setup 31 | 32 | To setup the project, is needed to have Elixir 1.7 or higher installed and run the the command: 33 | 34 | ```bash 35 | $ mix deps.get 36 | ``` 37 | 38 | ### QA 39 | 40 | In order to keep the quality of the project is important to create tests, format the code 41 | and make sure the code style is following the Elixir linter rules. 42 | 43 | To run the tests: 44 | 45 | ```bash 46 | $ mix test 47 | ``` 48 | 49 | To the code formater: 50 | 51 | ```bash 52 | $ mix format 53 | ``` 54 | 55 | To run the linter: 56 | 57 | ```bash 58 | $ mix credo --strict 59 | ``` 60 | -------------------------------------------------------------------------------- /lib/nubank_api/http_wrapper.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.HTTPWrapper do 2 | @moduledoc """ 3 | Wrappers the HTTP requests in order to set properly the default headers, the access token, 4 | encode and decode JSON payloads and access the link inside the NubankAPI.Access links. 5 | """ 6 | 7 | @behaviour NubankAPI.HTTP 8 | 9 | alias NubankAPI.{Access, Config} 10 | 11 | @impl NubankAPI.HTTP 12 | def get(link, access = %Access{}) when is_atom(link), do: request(:get, link, access, "") 13 | 14 | @impl NubankAPI.HTTP 15 | def post(link, access = %Access{}, body) when is_atom(link) and is_map(body) do 16 | encoded_body = Poison.encode!(body) 17 | request(:post, link, access, encoded_body) 18 | end 19 | 20 | defp request(http_method, link, access = %Access{}, encoded_body) do 21 | %{links: links, access_token: token, token_type: type} = access 22 | 23 | headers = 24 | Config.default_headers() ++ [{"Authorization", "#{String.capitalize(type)} #{token}"}] 25 | 26 | url = links[link] 27 | response = HTTPoison.request(http_method, url, encoded_body, headers) 28 | 29 | with {:ok, %{status_code: status_code, body: body}} <- response, 30 | :ok <- check_status_code(status_code) do 31 | Poison.decode(body) 32 | end 33 | end 34 | 35 | defp check_status_code(status_code) when status_code in 200..299, do: :ok 36 | 37 | defp check_status_code(status_code), 38 | do: {:error, "Expected status code be in range 200 and 299, but got #{status_code} instead"} 39 | end 40 | -------------------------------------------------------------------------------- /lib/nubank_api/features/bill_summaries.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.Feature.BillSummaries do 2 | @moduledoc """ 3 | Module responsible for handling the data from the :bills_summary endpoint. 4 | """ 5 | 6 | use NubankAPI.Feature 7 | alias NubankAPI.{Access, BillSummary} 8 | 9 | @link :bills_summary 10 | @known_states [ 11 | :open, 12 | :overdue 13 | ] 14 | 15 | @doc """ 16 | Fetch bill summaries (both open and overdue) 17 | 18 | ## Examples 19 | 20 | iex> NubankAPI.Feature.BillSummaries.fetch_bill_summaries(access) 21 | {:ok, [%BillSummary{}]} 22 | 23 | iex> NubankAPI.Feature.BillSummaries.fetch_bill_summaries(access, state: :open) 24 | {:ok, [%BillSummary{state: :open}]} 25 | """ 26 | def fetch_bill_summaries(access = %Access{}, opts \\ []) do 27 | state_filter = Keyword.get(opts, :state) 28 | 29 | with {:ok, %{"bills" => bills}} <- @http.get(@link, access), 30 | parsed_bill_summaries <- Enum.map(bills, &parse_bill_summary/1), 31 | {:ok, filtered_bill_summaries} <- 32 | filter_bill_summaries(parsed_bill_summaries, state_filter) do 33 | {:ok, filtered_bill_summaries} 34 | end 35 | end 36 | 37 | @doc """ 38 | List the known bill states 39 | 40 | ## Examples 41 | 42 | iex> NubankAPI.Feature.BillSummaries.list_known_states() 43 | [:open, :overdue] 44 | """ 45 | def list_known_states, do: @known_states 46 | 47 | defp filter_bill_summaries(bill_summaries, nil), do: {:ok, bill_summaries} 48 | 49 | defp filter_bill_summaries(bill_summaries, state) when state in @known_states, 50 | do: {:ok, Enum.filter(bill_summaries, &(&1.state == state))} 51 | 52 | defp filter_bill_summaries(_bill_summaries, _state), do: {:error, "Invalid bill state"} 53 | 54 | defp parse_bill_summary(bill_summary) do 55 | %BillSummary{ 56 | id: bill_summary["id"], 57 | state: String.to_atom(bill_summary["state"]), 58 | summary: bill_summary["summary"] 59 | } 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/features/purchases_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.Feature.PurchasesTest do 2 | use ExUnit.Case, async: true 3 | import Mox 4 | 5 | alias NubankAPI.{Access, Purchase} 6 | alias NubankAPI.Feature.Purchases 7 | alias NubankAPI.Mock.HTTP 8 | 9 | setup do 10 | purchases_fixture = NubankAPI.TestHelper.load_fixture(:purchases) 11 | 12 | access = %Access{ 13 | access_token: "fake_access_token", 14 | refresh_before: DateTime.utc_now(), 15 | refresh_token: "fake_refresh_token", 16 | token_type: "bearer", 17 | links: %{ 18 | puchases: "https://fakeapi.nubank.com.br/purchases" 19 | } 20 | } 21 | 22 | {:ok, access: access, purchases_fixture: purchases_fixture} 23 | end 24 | 25 | describe "NubankAPI.Feature.Purchases.fetch_purchases/1" do 26 | test "parses the response from the API to %Purchase{}", %{ 27 | access: access, 28 | purchases_fixture: purchases_fixture 29 | } do 30 | expect(HTTP, :get, fn :purchases, ^access -> {:ok, purchases_fixture} end) 31 | expected_length = Enum.count(purchases_fixture["transactions"]) 32 | 33 | {:ok, purchases} = Purchases.fetch_purchases(access) 34 | 35 | assert Enum.count(purchases) == expected_length 36 | 37 | Enum.each(purchases, fn purchase -> 38 | assert %Purchase{} = purchase 39 | end) 40 | end 41 | end 42 | 43 | describe "NubankAPI.Feature.Purchases.list_known_categories/0" do 44 | test "return a list of known categories as atoms" do 45 | categories = Purchases.list_known_categories() 46 | 47 | Enum.each(categories, fn category -> 48 | assert is_atom(category) 49 | end) 50 | end 51 | end 52 | 53 | describe "NubankAPI.Feature.Purchases.list_known_status/0" do 54 | test "return a list of known statuses as atoms" do 55 | statuses = Purchases.list_known_status() 56 | 57 | Enum.each(statuses, fn status -> 58 | assert is_atom(status) 59 | end) 60 | end 61 | end 62 | 63 | describe "NubankAPI.Feature.Purchases.list_known_event_types/0" do 64 | test "return a list of known event_types as atoms" do 65 | event_types = Purchases.list_known_event_types() 66 | 67 | Enum.each(event_types, fn event_type -> 68 | assert is_atom(event_type) 69 | end) 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/features/events_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.Feature.EventsTest do 2 | use ExUnit.Case, async: true 3 | import Mox 4 | 5 | alias NubankAPI.{Access, Event} 6 | alias NubankAPI.Feature.Events 7 | alias NubankAPI.Mock.HTTP 8 | 9 | setup do 10 | events_fixture = NubankAPI.TestHelper.load_fixture(:events) 11 | 12 | access = %Access{ 13 | access_token: "fake_access_token", 14 | refresh_before: DateTime.utc_now(), 15 | refresh_token: "fake_refresh_token", 16 | token_type: "bearer", 17 | links: %{ 18 | events: "https://fakeapi.nubank.com.br/events" 19 | } 20 | } 21 | 22 | {:ok, access: access, events_fixture: events_fixture} 23 | end 24 | 25 | describe "NubankAPI.Feature.Events.fetch_events/1" do 26 | test "parses the response from the API to %Event{}", %{ 27 | access: access, 28 | events_fixture: events_fixture 29 | } do 30 | expect(HTTP, :get, fn :events, ^access -> {:ok, events_fixture} end) 31 | expected_lenth = Enum.count(events_fixture["events"]) 32 | 33 | {:ok, events} = Events.fetch_events(access) 34 | 35 | assert Enum.count(events) == expected_lenth 36 | 37 | Enum.each(events, fn event -> 38 | assert %Event{} = event 39 | end) 40 | end 41 | end 42 | 43 | describe "NubankAPI.Feature.Events.fetch_events/2" do 44 | test "filter only events from 'transaction' category", %{ 45 | access: access, 46 | events_fixture: events_fixture 47 | } do 48 | expect(HTTP, :get, fn :events, ^access -> {:ok, events_fixture} end) 49 | 50 | {:ok, events} = Events.fetch_events(access, category: :transaction) 51 | 52 | assert Enum.count(events) == 1 53 | assert [event = %Event{}] = events 54 | assert event.category == :transaction 55 | end 56 | end 57 | 58 | describe "NubankAPI.Feature.Events.list_known_categories/0" do 59 | test "return a list of known categories as atoms" do 60 | categories = Events.list_known_categories() 61 | 62 | Enum.each(categories, fn category -> 63 | assert is_atom(category) 64 | end) 65 | end 66 | 67 | test "return a list of known categories with no duplicates" do 68 | categories = Events.list_known_categories() 69 | 70 | expected_lenth = 71 | categories 72 | |> MapSet.new() 73 | |> Enum.count() 74 | 75 | assert expected_lenth == Enum.count(categories) 76 | 77 | Enum.each(categories, fn category -> 78 | assert is_atom(category) 79 | end) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/nubank_api/features/events.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.Feature.Events do 2 | @moduledoc """ 3 | Module responsable for handle the data from the :events link. 4 | 5 | This link is avaible in NuBankAPI.Access links as response from NubankAPI.Auth.get_token/2. 6 | """ 7 | 8 | use NubankAPI.Feature 9 | alias NubankAPI.{Access, Event} 10 | 11 | @link :events 12 | @known_categories [ 13 | :account_limit_set, 14 | :anticipate_event, 15 | :bill_flow_on_due_date, 16 | :bill_flow_paid, 17 | :card_activated, 18 | :customer_device_authorized, 19 | :customer_invitations_changed, 20 | :due_day_changed, 21 | :earn_offer, 22 | :initial_account_limit, 23 | :payment, 24 | :rewards_canceled, 25 | :rewards_fee, 26 | :rewards_redemption, 27 | :rewards_signup, 28 | :transaction, 29 | :transaction_reversed, 30 | :tutorial, 31 | :virtual_card_encouragement, 32 | :welcome 33 | ] 34 | 35 | @doc """ 36 | Fetch events from all categories. 37 | 38 | Args: 39 | * `access` - %NubankAPI.Access{} which cotains the access token and the endpoints 40 | * `options` - Keyword list of options 41 | 42 | Options: 43 | * `:category` - Filter the events by the category, if nil this filter is not applied, by 44 | default it's nil 45 | 46 | ## Examples 47 | 48 | iex> NubankAPI.Feature.Events.fetch_events(access) 49 | {:ok, [%NubankAPI.Event{}]} 50 | 51 | or 52 | 53 | iex> NubankAPI.Feature.Events.fetch_events(access, category: :transaction) 54 | {:ok, [%NubankAPI.Event{category: :transaction}]} 55 | """ 56 | def fetch_events(access = %Access{}, opts \\ []) do 57 | category_filter = Keyword.get(opts, :category) 58 | 59 | with {:ok, %{"events" => events}} <- @http.get(@link, access), 60 | parsed_events <- Enum.map(events, &parse_event/1), 61 | {:ok, filtered_events} <- filter_events(parsed_events, category_filter) do 62 | {:ok, filtered_events} 63 | end 64 | end 65 | 66 | @doc """ 67 | List the known events categories 68 | 69 | ## Examples 70 | 71 | iex> NubankAPI.Feature.Events.list_known_categories() 72 | [:transaction, :payment, ...] 73 | """ 74 | def list_known_categories, do: @known_categories 75 | 76 | defp filter_events(events, nil), do: {:ok, events} 77 | 78 | defp filter_events(events, category) when category in @known_categories, 79 | do: {:ok, Enum.filter(events, &(&1.category == category))} 80 | 81 | defp filter_events(_events, _category), do: {:error, "Invalid category"} 82 | 83 | defp parse_event(event) do 84 | {:ok, event_datetime, 0} = DateTime.from_iso8601(event["time"]) 85 | 86 | %Event{ 87 | id: event["id"], 88 | description: event["description"], 89 | amount: event["amount"], 90 | time: event_datetime, 91 | title: event["title"], 92 | category: String.to_atom(event["category"]) 93 | } 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/nubank_api/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.Auth do 2 | @moduledoc """ 3 | NubankAPI.Auth is responsable for handle authentication. 4 | 5 | It has a login function that authenticates the user, getting the access token, it's 6 | expiration datetime and the refresh token, as well the link for other API features. 7 | 8 | Future implementation will be create the refresh token function. 9 | """ 10 | 11 | alias NubankAPI.{Access, Config} 12 | 13 | @doc """ 14 | Get auth token. 15 | 16 | It will return a Map with with the structure: 17 | %NubankAPI.Access{ 18 | access_token: "access token in string", 19 | links: %{}, 20 | refresh_before: "an expiration DateTime in UTC", 21 | refresh_token: "refresh token in string" 22 | token_type: "bearer" 23 | } 24 | 25 | ## Examples 26 | 27 | iex> NubankAPI.transactions(access) 28 | {:ok, []} 29 | """ 30 | @spec get_token(String.t(), String.t()) :: {:ok, NubankAPI.Access.t()} | {:error, any} 31 | def get_token(login, password) when is_bitstring(login) and is_bitstring(password) do 32 | with %{url: url, headers: headers, body: body} = prepare_request_data(login, password), 33 | {:ok, %{status_code: status_code, body: body}} <- HTTPoison.post(url, body, headers), 34 | :ok <- check_status_code(status_code), 35 | {:ok, parsed_body} <- Poison.decode(body), 36 | {:ok, data} <- extract_response_data(parsed_body) do 37 | {:ok, data} 38 | else 39 | unmatch_result -> {:error, unmatch_result} 40 | end 41 | end 42 | 43 | def get_token(_login, _password), do: {:error, "Login and password must be strings!"} 44 | 45 | defp check_status_code(status) when status in 200..299, do: :ok 46 | 47 | defp check_status_code(status), 48 | do: {:error, "Expected status code be a range in 200 and 299, but instead it is #{status}"} 49 | 50 | defp prepare_request_data(login, password) do 51 | body = 52 | Poison.encode!(%{ 53 | login: login, 54 | password: password, 55 | grant_type: "password", 56 | client_id: "other.conta", 57 | client_secret: "yQPeLzoHuJzlMMSAjC-LgNUJdUecx8XO" 58 | }) 59 | 60 | %{ 61 | url: Config.token_api_uri(), 62 | headers: Config.default_headers(), 63 | body: body 64 | } 65 | end 66 | 67 | defp extract_response_data(parsed_body) do 68 | with {:ok, refresh_bofore, 0} <- DateTime.from_iso8601(parsed_body["refresh_before"]) do 69 | {:ok, 70 | %Access{ 71 | access_token: parsed_body["access_token"], 72 | refresh_before: refresh_bofore, 73 | token_type: parsed_body["token_type"], 74 | refresh_token: parsed_body["refresh_token"], 75 | links: parse_api_links(parsed_body) 76 | }} 77 | end 78 | end 79 | 80 | defp parse_api_links(%{"_links" => links}) do 81 | links 82 | |> Map.keys() 83 | |> Enum.reduce(%{}, fn link_key, indexed_links -> 84 | Map.put(indexed_links, String.to_atom(link_key), links[link_key]["href"]) 85 | end) 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/features/bill_summaries_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.Feature.BillSummariesTest do 2 | use ExUnit.Case, async: true 3 | import Mox 4 | 5 | alias NubankAPI.{Access, BillSummary} 6 | alias NubankAPI.Feature.BillSummaries 7 | alias NubankAPI.Mock.HTTP 8 | 9 | setup do 10 | bill_summaries_fixture = NubankAPI.TestHelper.load_fixture(:bills_summaries) 11 | 12 | access = %Access{ 13 | access_token: "fake_access_token", 14 | refresh_before: DateTime.utc_now(), 15 | refresh_token: "fake_refresh_token", 16 | token_type: "bearer", 17 | links: %{ 18 | bills_summary: "https://fakeapi.nubank.com.br/bills_summary" 19 | } 20 | } 21 | 22 | {:ok, access: access, bill_summaries_fixture: bill_summaries_fixture} 23 | end 24 | 25 | describe "NubankAPI.Feature.BillSummaries.fetch_bill_summaries/1" do 26 | test "parses the response from the API to %BillSummary{}", %{ 27 | access: access, 28 | bill_summaries_fixture: bill_summaries_fixture 29 | } do 30 | expect(HTTP, :get, fn :bills_summary, ^access -> {:ok, bill_summaries_fixture} end) 31 | expected_lenth = Enum.count(bill_summaries_fixture["bills"]) 32 | 33 | {:ok, bill_summaries} = BillSummaries.fetch_bill_summaries(access) 34 | 35 | assert Enum.count(bill_summaries) == expected_lenth 36 | 37 | Enum.each(bill_summaries, fn bill_summary -> 38 | assert %BillSummary{} = bill_summary 39 | end) 40 | end 41 | end 42 | 43 | describe "NubankAPI.Feature.BillSummaries.fetch_bill_summaries/2" do 44 | test "filter only 'open' bills", %{ 45 | access: access, 46 | bill_summaries_fixture: bill_summaries_fixture 47 | } do 48 | expect(HTTP, :get, fn :bills_summary, ^access -> {:ok, bill_summaries_fixture} end) 49 | 50 | {:ok, bill_summaries} = BillSummaries.fetch_bill_summaries(access, state: :open) 51 | 52 | assert Enum.count(bill_summaries) == 1 53 | assert [bill_summary = %BillSummary{}] = bill_summaries 54 | assert bill_summary.state == :open 55 | end 56 | 57 | test "filter only 'overdue' bills", %{ 58 | access: access, 59 | bill_summaries_fixture: bill_summaries_fixture 60 | } do 61 | expect(HTTP, :get, fn :bills_summary, ^access -> {:ok, bill_summaries_fixture} end) 62 | 63 | {:ok, bill_summaries} = BillSummaries.fetch_bill_summaries(access, state: :overdue) 64 | 65 | assert Enum.count(bill_summaries) == 2 66 | 67 | Enum.each(bill_summaries, fn bill_summary -> 68 | assert %BillSummary{} = bill_summary 69 | assert bill_summary.state == :overdue 70 | end) 71 | end 72 | 73 | test "error when bill state do not exist", %{ 74 | access: access, 75 | bill_summaries_fixture: bill_summaries_fixture 76 | } do 77 | expect(HTTP, :get, fn :bills_summary, ^access -> {:ok, bill_summaries_fixture} end) 78 | 79 | {:error, response} = BillSummaries.fetch_bill_summaries(access, state: :i_promise_to_pay) 80 | 81 | assert response == "Invalid bill state" 82 | end 83 | end 84 | 85 | describe "NubankAPI.Feature.BillSummaries.list_known_states/0" do 86 | test "return a list of known states as atoms" do 87 | states = BillSummaries.list_known_states() 88 | 89 | Enum.each(states, fn state -> 90 | assert is_atom(state) 91 | end) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 3 | "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "credo": {:hex, :credo, "1.0.2", "88bc918f215168bf6ce7070610a6173c45c82f32baa08bdfc80bf58df2d103b6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm"}, 6 | "ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "hackney": {:hex, :hackney, "1.15.0", "287a5d2304d516f63e56c469511c42b016423bcb167e61b611f6bad47e3ca60e", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "httpoison": {:hex, :httpoison, "1.5.0", "71ae9f304bdf7f00e9cd1823f275c955bdfc68282bc5eb5c85c3a9ade865d68e", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 11 | "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 14 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 15 | "mox": {:hex, :mox, "0.5.0", "c519b48407017a85f03407a9a4c4ceb7cc6dec5fe886b2241869fb2f08476f9e", [:mix], [], "hexpm"}, 16 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, 17 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, 18 | "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm"}, 19 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, 20 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, 21 | } 22 | -------------------------------------------------------------------------------- /lib/nubank_api/features/purchases.ex: -------------------------------------------------------------------------------- 1 | defmodule NubankAPI.Feature.Purchases do 2 | @moduledoc """ 3 | Module responsible for handling the data from the :purchases endpoint. 4 | """ 5 | 6 | use NubankAPI.Feature 7 | alias NubankAPI.{Access, Purchase} 8 | 9 | @link :purchases 10 | @known_categories [ 11 | :lazer, 12 | :outros, 13 | :transporte, 14 | :eletrônicos, 15 | :restaurante, 16 | :vestuário, 17 | :serviços, 18 | :casa, 19 | :supermercado, 20 | :educação, 21 | :viagem 22 | ] 23 | 24 | @known_status [ 25 | :settled, 26 | :unsettled, 27 | :canceled, 28 | :expired, 29 | :reversed 30 | ] 31 | 32 | @known_event_types [ 33 | :transaction_card_not_present, 34 | :transaction_card_present 35 | ] 36 | 37 | @doc """ 38 | Fetches purchases information 39 | 40 | ## Examples 41 | 42 | iex> NubankAPI.Feature.Purchases.fetch_purchases(access) 43 | {:ok, [%SavingsAccount{}]} 44 | 45 | """ 46 | def fetch_purchases(access = %Access{}, opts \\ []) do 47 | with {:ok, %{"transactions" => purchases}} <- @http.get(@link, access), 48 | parsed_purchases <- Enum.map(purchases, &parse_purchase/1) do 49 | {:ok, parsed_purchases} 50 | end 51 | end 52 | 53 | @doc """ 54 | List the known purchase categories 55 | 56 | ## Examples 57 | 58 | iex> NubankAPI.Feature.Purchases.list_known_categories() 59 | [ :lazer, :outros, :transporte, :eletrônicos, :restaurante, :vestuário, :serviços, :casa, :supermercado, :educação, :viagem ] 60 | ] 61 | """ 62 | def list_known_categories, do: @known_categories 63 | 64 | @doc """ 65 | List the known purchase status 66 | 67 | ## Examples 68 | 69 | iex> NubankAPI.Feature.Purchases.list_known_status() 70 | [ :settled, :unsettled, :canceled, :expired, :reversed ] 71 | ] 72 | """ 73 | def list_known_status, do: @known_status 74 | 75 | @doc """ 76 | List the known purchase event types 77 | 78 | ## Examples 79 | 80 | iex> NubankAPI.Feature.Purchases.list_known_categories() 81 | [ :transaction_card_not_present, :transaction_card_present ] 82 | """ 83 | def list_known_event_types, do: @known_event_types 84 | 85 | defp parse_purchase(purchase) do 86 | %Purchase{ 87 | account: purchase["account"], 88 | acquirer_id: purchase["acquirer_id"], 89 | amount: purchase["amount"], 90 | approved_reasons: purchase["approved_reasons"], 91 | auth_code: purchase["auth_code"], 92 | capture_mode: purchase["capture_mode"], 93 | card: purchase["card"], 94 | category: purchase["category"], 95 | chargebacks: purchase["chargebacks"], 96 | charges: purchase["charges"], 97 | charges_list: purchase["charges_list"], 98 | country: purchase["country"], 99 | customer: purchase["customer"], 100 | event_type: purchase["event_type"], 101 | id: purchase["id"], 102 | expires_on: purchase["expires_on"], 103 | mcc: purchase["mcc"], 104 | merchant_id: purchase["merchant_id"], 105 | merchant_name: purchase["merchant_name"], 106 | original_merchant_name: purchase["original_merchant_name"], 107 | postcode: purchase["postcode"], 108 | precise_amount: purchase["precise_amount"], 109 | recurring: purchase["recurring"], 110 | secure_code: purchase["secure_code"], 111 | source: purchase["source"], 112 | stand_in: purchase["stand_in"], 113 | status: purchase["status"], 114 | time: purchase["time"], 115 | time_wallclock: purchase["time_wallclock"], 116 | lon: purchase["lon"], 117 | lat: purchase["lat"], 118 | tags: purchase["tags"], 119 | fx: purchase["fx"], 120 | _links: purchase["_links"] 121 | } 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /test/fixtures/bills_summaries.json: -------------------------------------------------------------------------------- 1 | { 2 | "_links" : { 3 | "future" : { 4 | "href" : "https://https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/AFBCD" 5 | }, 6 | "open" : { 7 | "href" : "https://https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/AFBCD" 8 | } 9 | }, 10 | "bills" : [ 11 | { 12 | "_links" : { 13 | "self" : { 14 | "href" : "https://https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/AFBCD" 15 | } 16 | }, 17 | "state" : "open", 18 | "summary" : { 19 | "adjustments" : "0", 20 | "close_date" : "2019-03-02", 21 | "due_date" : "2019-03-09", 22 | "effective_due_date" : "2019-03-09", 23 | "expenses" : "3333.17", 24 | "fees" : "0", 25 | "interest" : 0, 26 | "interest_charge" : "0", 27 | "interest_rate" : "0.14", 28 | "interest_reversal" : "0", 29 | "international_tax" : "3.928166", 30 | "minimum_payment" : 0, 31 | "open_date" : "2019-02-02", 32 | "paid" : 0, 33 | "past_balance" : 0, 34 | "payments" : "-3333.81", 35 | "precise_minimum_payment" : "0", 36 | "precise_total_balance" : "3333.09468587", 37 | "previous_bill_balance" : "3333.80651987", 38 | "tax" : "0", 39 | "total_accrued" : "0", 40 | "total_balance" : 333333, 41 | "total_credits" : "0", 42 | "total_cumulative" : 333333, 43 | "total_financed" : "0", 44 | "total_international" : "33.498166", 45 | "total_national" : "3333.600000", 46 | "total_payments" : "-3333.81" 47 | } 48 | }, 49 | { 50 | "_links" : { 51 | "self" : { 52 | "href" : "https://https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/AFBCD" 53 | } 54 | }, 55 | "href" : "https://https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/AFBCD", 56 | "id" : "5f5913g3-fdd0-4520-8b51-d2bc22feckbf", 57 | "state" : "overdue", 58 | "summary" : { 59 | "adjustments" : "-222.98", 60 | "close_date" : "2019-02-02", 61 | "due_date" : "2019-02-09", 62 | "effective_due_date" : "2019-02-09", 63 | "expenses" : "2222.88", 64 | "fees" : "0", 65 | "interest" : 0, 66 | "interest_charge" : "0", 67 | "interest_rate" : "0.14", 68 | "interest_reversal" : "0", 69 | "international_tax" : "2.906474", 70 | "minimum_payment" : 22222, 71 | "open_date" : "2019-01-02", 72 | "paid" : 222222, 73 | "past_balance" : 0, 74 | "payments" : "-2222.71", 75 | "precise_minimum_payment" : "222.4709779805", 76 | "precise_total_balance" : "2222.80651987", 77 | "previous_bill_balance" : "2222.71004587", 78 | "remaining_minimum_payment" : 0, 79 | "tax" : "0", 80 | "total_accrued" : "0", 81 | "total_balance" : 222222, 82 | "total_credits" : "-159.98", 83 | "total_cumulative" : 222222, 84 | "total_financed" : "0", 85 | "total_international" : "22.136474", 86 | "total_national" : "2222.650000", 87 | "total_payments" : "-2222.71" 88 | } 89 | }, 90 | { 91 | "_links" : { 92 | "self" : { 93 | "href" : "https://https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/AFBCD" 94 | } 95 | }, 96 | "href" : "https://https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/AFBCD", 97 | "id" : "1a30320g-6cfb-45e3-a1e2-6120adb1665", 98 | "state" : "overdue", 99 | "summary" : { 100 | "adjustments" : "0", 101 | "close_date" : "2019-01-02", 102 | "due_date" : "2019-01-09", 103 | "effective_due_date" : "2019-01-11", 104 | "expenses" : "1111.80", 105 | "fees" : "0", 106 | "interest" : 0, 107 | "interest_charge" : "0", 108 | "interest_rate" : "0.14", 109 | "interest_reversal" : "0", 110 | "international_tax" : "11.907916", 111 | "minimum_payment" : 0, 112 | "open_date" : "2018-12-02", 113 | "paid" : 333333, 114 | "past_balance" : -44444, 115 | "payments" : "-111.79", 116 | "precise_minimum_payment" : "0", 117 | "precise_total_balance" : "1111.71004587", 118 | "previous_bill_balance" : "111.79212987", 119 | "tax" : "0", 120 | "total_accrued" : "0", 121 | "total_balance" : 111111, 122 | "total_credits" : "0", 123 | "total_cumulative" : 111111, 124 | "total_financed" : "0", 125 | "total_international" : "555.727916", 126 | "total_national" : "1111.980000", 127 | "total_payments" : "-555.79" 128 | } 129 | } 130 | ] 131 | } 132 | -------------------------------------------------------------------------------- /test/fixtures/events.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [ 3 | { 4 | "amount": 1500000, 5 | "category": "account_limit_set", 6 | "id": "53CDD99E-5416-483B-B601-5D5F83D9FA4E", 7 | "message": "Seu limite para compras é R$ 15.000,00.", 8 | "time": "2019-02-25T14:30:00.123Z", 9 | "title": "Limite alterado" 10 | },{ 11 | "category": "anticipate_event", 12 | "description": "Localiza Aluguel\nVocê ganhou um desconto\nde R$ 0,36", 13 | "href": "nuapp://transaction/event_id", 14 | "id": "63347A96-8770-4F5B-ADCD-0F95CE2CE831", 15 | "message": "Localiza Aluguel\nVocê ganhou um desconto\nde R$ 0,36", 16 | "time": "2019-02-25T14:30:00.123Z", 17 | "title": "1 Parcela Antecipada" 18 | },{ 19 | "amount": 270636, 20 | "category": "bill_flow_on_due_date", 21 | "description": "É normal que o pagamento leve até 3 dias úteis para ser processado.", 22 | "href": "nuapp://bill/event_id", 23 | "id": "57C2E50A-8A0E-495B-953F-F64D6F95D075", 24 | "message": "É normal que o pagamento leve até 3 dias úteis para ser processado.", 25 | "time": "2019-02-25T14:30:00.123Z", 26 | "title": "Fatura vence hoje" 27 | },{ 28 | "category": "bill_flow_paid", 29 | "description": "", 30 | "href": "nuapp://bill/event_id", 31 | "id": "309D91DB-2471-4C5E-810D-D3FBE0D9A201", 32 | "message": "", 33 | "time": "2019-02-25T14:30:00.123Z", 34 | "title": "Fatura paga" 35 | },{ 36 | "category": "card_activated", 37 | "description": "Desbloqueado", 38 | "id": "F0CF1663-E781-4543-872A-B987F34BF50C", 39 | "message": "Seu cartão foi ativado.", 40 | "time": "2019-02-25T14:30:00.123Z", 41 | "title": "Seu cartão está" 42 | },{ 43 | "category": "customer_device_authorized", 44 | "description": "Seu dispositivo LG Nexus 5x foi autorizado", 45 | "id": "2F7E74FF-7061-4AB7-873B-5E5285CFC291", 46 | "message": "Seu dispositivo LG Nexus 5x foi autorizado", 47 | "time": "2019-02-25T14:30:00.123Z", 48 | "title": "Novo dispositivo autorizado" 49 | },{ 50 | "category": "customer_invitations_changed", 51 | "description": "Amigos indicados por você terão prioridade na lista", 52 | "id": "A3673761-64D0-4A73-9DF2-1EC2FD99AC61", 53 | "message": "Amigos indicados por você terão prioridade na lista", 54 | "time": "2019-02-25T14:30:00.123Z", 55 | "title": "Indique seus amigos!" 56 | },{ 57 | "category": "due_day_changed", 58 | "description": "Seus próximos vencimentos são 06 OUT, 09 NOV e 09 DEZ", 59 | "id": "1F5A1CA3-F605-4739-9C82-C174E3F4B69E", 60 | "message": "Seus próximos vencimentos são 06 OUT, 09 NOV e 09 DEZ", 61 | "time": "2019-02-25T14:30:00.123Z", 62 | "title": "Data de vencimento alterada" 63 | },{ 64 | "category": "earn_offer", 65 | "description": "Você ganhou 34 pts na sua compra Cabify", 66 | "id": "E52F6264-C19B-4EFB-8101-F81FBF2A51C7", 67 | "message": "Você ganhou 34 pts na sua compra Cabify", 68 | "time": "2019-02-25T14:30:00.123Z", 69 | "title": "Bonus Cabify!" 70 | },{ 71 | "amount": 125000, 72 | "category": "initial_account_limit", 73 | "id": "B80B291E-41E2-42EE-B00E-D5E6C5F8A514", 74 | "message": "Seu limite para compras é R$ 1.250,00.", 75 | "time": "2019-02-25T14:30:00.123Z", 76 | "title": "Limite inicial" 77 | },{ 78 | "amount": 385000, 79 | "category": "payment", 80 | "id": "4CBECEF1-25BE-477B-B63D-F5709A83BB36", 81 | "time": "2019-02-25T14:30:00.123Z", 82 | "title": "Pagamento recebido" 83 | },{ 84 | "category": "rewards_canceled", 85 | "description": "Assinatura cancelada", 86 | "id": "54CFA638-2714-4041-BF2C-25AF10B3EB14", 87 | "message": "Assinatura cancelada", 88 | "time": "2019-02-25T14:30:00.123Z", 89 | "title": "Nubank Rewards" 90 | },{ 91 | "amount": 19000, 92 | "category": "rewards_fee", 93 | "description": "Assinatura", 94 | "id": "269DD3DA-64FB-4196-B1EB-29227FC98174", 95 | "message": "Assinatura", 96 | "time": "2019-02-25T14:30:00.123Z", 97 | "title": "Nubank Rewards" 98 | },{ 99 | "category": "rewards_redemption", 100 | "description": "Você apagou a compra Ifood*Br e recebeu um crédito de R$ 26,00", 101 | "id": "C7288AC6-32C2-4BF2-9F74-ED41FD6F06A7", 102 | "message": "Você apagou a compra Ifood*Br e recebeu um crédito de R$ 26,00", 103 | "time": "2019-02-25T14:30:00.123Z", 104 | "title": "Nubank Rewards" 105 | },{ 106 | "category": "rewards_signup", 107 | "description": 108 | "Parabéns, agora você acumula pontos em todas as compras e pode usá-los para apagar transações da sua fatura.", 109 | "id": "9353FB2F-35A2-4EE4-A628-B85BA2A3EA73", 110 | "message": 111 | "Parabéns, agora você acumula pontos em todas as compras e pode usá-los para apagar transações da sua fatura.", 112 | "time": "2019-02-25T14:30:00.123Z", 113 | "title": "Nubank Rewards" 114 | },{ 115 | "amount": 2500, 116 | "category": "transaction", 117 | "description": "Restaurante da cidade", 118 | "details": {"subcategory": "card_present"}, 119 | "href": "nuapp://transaction/event_id", 120 | "id": "DB4A9E0B-9309-4C43-A511-8D764217EF7E", 121 | "time": "2019-02-25T14:30:00.123Z", 122 | "title": "restaurante" 123 | },{ 124 | "amount": 300, 125 | "category": "transaction_reversed", 126 | "description": "Itunes.Com/Bill", 127 | "details": {"subcategory": "unknown"}, 128 | "href": "nuapp://transaction/event_id", 129 | "id": "1DA51F73-3DA3-4C0D-B81B-1F765D99EDF1", 130 | "time": "2019-02-25T14:30:00.123Z", 131 | "title": "serviços" 132 | },{ 133 | "category": "tutorial", 134 | "id": "55ba8e18-84e6-4f18-bd85-f54d3d058ece", 135 | "time": "2019-02-25T14:30:00.123Z", 136 | "title": "Veja como funciona" 137 | },{ 138 | "category": "virtual_card_encouragement", 139 | "description": "", 140 | "id": "D01EE926-93FC-403E-9213-FC3EF65FE273", 141 | "message": "", 142 | "time": "2019-02-25T14:30:00.123Z", 143 | "title": "Enquanto seu roxinho está a caminho, use seu Cartão Virtual" 144 | },{ 145 | "category": "welcome", 146 | "description": "Nubank!", 147 | "id": "D45EAD8D-4584-425D-82A2-7322C7701B20", 148 | "message": "Seja bem vindo ao Nubank!", 149 | "time": "2019-02-25T14:30:00.123Z", 150 | "title": "Bem-vindo ao" 151 | } 152 | ] 153 | } 154 | -------------------------------------------------------------------------------- /test/fixtures/purchases.json: -------------------------------------------------------------------------------- 1 | { 2 | "transactions": [ 3 | { 4 | "category": "lazer", 5 | "amount": 1111, 6 | "tags": [], 7 | "chargebacks": [], 8 | "precise_amount": "11.12", 9 | "merchant_id": "174020076999", 10 | "time": "2018-06-29T07:31:03Z", 11 | "charges": 1, 12 | "original_merchant_name": "PP*HUMBLEBUNDL HUMBLEB", 13 | "postcode": "99999", 14 | "type": "card_not_present", 15 | "mcc": "lazer", 16 | "approved_reasons": [ 17 | "normal_approval" 18 | ], 19 | "expires_on": "2018-07-06", 20 | "charges_list": [ 21 | { 22 | "amount": 1111, 23 | "transaction_id": "5k32e038-ac49-4418-bd50-e1cca19cfz94", 24 | "index": 0, 25 | "account_id": "ca2126b4-4ef9-4ba2-299b-308ebaba1bf1", 26 | "precise_amount": "11.11", 27 | "promotion_reason": "settlement", 28 | "status": "historical", 29 | "id": "2j37ecb6-6a2c-42d8-8l76-azac4c0f2b61", 30 | "precise_amount_usd": "12", 31 | "extras": [ 32 | { 33 | "name": "iof", 34 | "amount": 111.1111, 35 | "precise_amount": "1.111111" 36 | } 37 | ], 38 | "post_date": "2018-06-30" 39 | } 40 | ], 41 | "source": "upfront_foreign", 42 | "capture_mode": { 43 | "entry_mode": "e_commerce_chip", 44 | "pin_mode": "not_accepted" 45 | }, 46 | "recurring": false, 47 | "customer": "aaaa240b-2d5d-2222-95a7-75cecdcaceced", 48 | "account": "5a6126b4-8ef9-4za2-999b-508ebaba1bf1", 49 | "card": "5a6126b4-7d6b-4815-8ab6-2d65dd0b6002", 50 | "secure_code": false, 51 | "status": "settled", 52 | "id": "5b35e031-aa49-4428-bc50-e1cca19cfe94", 53 | "merchant_name": "Pp*Humblebundl Humbleb", 54 | "event_type": "transaction_card_not_present", 55 | "_links": { 56 | "category": { 57 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 58 | }, 59 | "chargeback_reasons_v4": { 60 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 61 | }, 62 | "notify_geo": { 63 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 64 | }, 65 | "categories": { 66 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 67 | }, 68 | "chargeback": { 69 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 70 | }, 71 | "create_tag": { 72 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 73 | }, 74 | "chargeback_reasons": { 75 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 76 | }, 77 | "merchant": { 78 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 79 | }, 80 | "self": { 81 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 82 | } 83 | }, 84 | "fx": { 85 | "currency_origin": "USD", 86 | "amount_origin": 1100, 87 | "amount_usd": 1100, 88 | "precise_amount_origin": "11", 89 | "precise_amount_usd": "11", 90 | "exchange_rate": 4.01 91 | }, 92 | "auth_code": "Q4XX2F", 93 | "acquirer_id": "231497", 94 | "stand_in": false, 95 | "country": "USA", 96 | "time_wallclock": "2018-06-29T04:20:26" 97 | }, 98 | { 99 | "category": "transporte", 100 | "amount": 2222, 101 | "tags": [], 102 | "chargebacks": [], 103 | "precise_amount": "22.22", 104 | "merchant_id": "539701023040201", 105 | "time": "2018-08-22T11:04:12Z", 106 | "charges": 1, 107 | "original_merchant_name": "Uber Do Brasil Tecnolo", 108 | "postcode": "33333333", 109 | "type": "card_not_present", 110 | "mcc": "transporte", 111 | "approved_reasons": [ 112 | "normal_approval" 113 | ], 114 | "expires_on": "2018-08-29", 115 | "charges_list": [ 116 | { 117 | "amount": 2222, 118 | "transaction_id": "cb7d412c-63ef-479b-a3d5-2de4afb19ad0", 119 | "index": 0, 120 | "account_id": "5a6123q4-8ei9-4fl2-939b-508gbabacbf1", 121 | "precise_amount": "22.22", 122 | "promotion_reason": "settlement", 123 | "status": "historical", 124 | "id": "cb7d412c-63ef-479b-a3d5-2de4afb19ad0", 125 | "extras": [], 126 | "post_date": "2018-08-23" 127 | } 128 | ], 129 | "source": "upfront_national", 130 | "capture_mode": { 131 | "entry_mode": "e_commerce_chip", 132 | "pin_mode": "not_accepted" 133 | }, 134 | "recurring": true, 135 | "customer": "cb7d412c-63ef-479b-a3d5-2de4afb19ad0", 136 | "account": "cb7d412c-63ef-479b-a3d5-2de4afb19ad0", 137 | "card": "cb7d412c-63ef-479b-a3d5-2de4afb19ad0", 138 | "secure_code": false, 139 | "status": "settled", 140 | "id": "cb7d412c-63ef-479b-a3d5-2de4afb19ad0", 141 | "lon": -11.1757409, 142 | "lat": -11.9324676, 143 | "merchant_name": "Uber do Brasil Tecnolo", 144 | "event_type": "transaction_card_not_present", 145 | "_links": { 146 | "category": { 147 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 148 | }, 149 | "chargeback_reasons_v4": { 150 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 151 | }, 152 | "notify_geo": { 153 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 154 | }, 155 | "categories": { 156 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 157 | }, 158 | "chargeback": { 159 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 160 | }, 161 | "create_tag": { 162 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 163 | }, 164 | "chargeback_reasons": { 165 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 166 | }, 167 | "merchant": { 168 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 169 | }, 170 | "self": { 171 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 172 | } 173 | }, 174 | "auth_code": "QKSZX7", 175 | "acquirer_id": "116385", 176 | "stand_in": false, 177 | "country": "BRA", 178 | "time_wallclock": "2018-08-22T08:16:51" 179 | } 180 | ], 181 | "_links": { 182 | "updates": { 183 | "href": "https://prod-s0-webapp-proxy.nubank.com.br/api/proxy/ABC" 184 | } 185 | } 186 | } 187 | --------------------------------------------------------------------------------