├── .credo.exs ├── .formatter.exs ├── .github └── workflows │ └── elixir.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── guides └── webhooks.md ├── lib ├── plaid.ex └── plaid │ ├── account.ex │ ├── account │ ├── balances.ex │ └── historical_balances.ex │ ├── accounts.ex │ ├── address.ex │ ├── asset_report.ex │ ├── asset_report │ ├── item.ex │ ├── report.ex │ ├── user.ex │ ├── warning.ex │ └── warning │ │ └── cause.ex │ ├── auth.ex │ ├── auth │ ├── numbers.ex │ └── numbers │ │ ├── ach.ex │ │ ├── bacs.ex │ │ ├── eft.ex │ │ └── international.ex │ ├── castable.ex │ ├── categories.ex │ ├── categories │ └── category.ex │ ├── client.ex │ ├── client │ └── httpoison.ex │ ├── employer.ex │ ├── error.ex │ ├── identity.ex │ ├── identity │ ├── address.ex │ ├── email.ex │ ├── match │ │ ├── account.ex │ │ ├── account │ │ │ ├── address.ex │ │ │ ├── email_address.ex │ │ │ ├── legal_name.ex │ │ │ └── phone_number.ex │ │ ├── item.ex │ │ ├── user.ex │ │ └── user │ │ │ └── address.ex │ └── phone_number.ex │ ├── institution.ex │ ├── institution │ └── status.ex │ ├── institutions.ex │ ├── investments.ex │ ├── investments │ ├── holding.ex │ ├── security.ex │ └── transaction.ex │ ├── item.ex │ ├── item │ ├── status.ex │ └── status │ │ ├── investments.ex │ │ ├── last_webhook.ex │ │ └── transactions.ex │ ├── liabilities.ex │ ├── liabilities │ ├── credit.ex │ ├── credit │ │ └── apr.ex │ ├── mortgage.ex │ ├── mortgage │ │ └── interest_rate.ex │ ├── student.ex │ └── student │ │ ├── loan_status.ex │ │ ├── pslf_status.ex │ │ └── repayment_plan.ex │ ├── link_token.ex │ ├── link_token │ ├── deposit_switch.ex │ ├── metadata.ex │ ├── payment_initiation.ex │ └── user.ex │ ├── payment_initiation.ex │ ├── payment_initiation │ ├── address.ex │ ├── amount.ex │ ├── bacs.ex │ ├── payment.ex │ ├── recipient.ex │ └── schedule.ex │ ├── processor.ex │ ├── processor │ └── numbers.ex │ ├── sandbox.ex │ ├── simple_response.ex │ ├── transactions.ex │ ├── transactions │ ├── transaction.ex │ └── transaction │ │ ├── location.ex │ │ └── payment_meta.ex │ ├── util.ex │ └── webhooks.ex ├── mix.exs ├── mix.lock └── test ├── plaid ├── accounts_test.exs ├── asset_report_test.exs ├── auth_test.exs ├── categories_test.exs ├── employer_test.exs ├── identity_test.exs ├── institutions_test.exs ├── investments_test.exs ├── item_test.exs ├── liabilities_test.exs ├── link_token_test.exs ├── payment_initiation_test.exs ├── processor_test.exs ├── sandbox_test.exs ├── transactions_test.exs └── webhooks_test.exs └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | # 3 | configs: [ 4 | %{ 5 | name: "default", 6 | files: %{ 7 | # 8 | # You can give explicit globs or simply directories. 9 | # In the latter case `**/*.{ex,exs}` will be used. 10 | # 11 | included: [ 12 | "lib/", 13 | "src/", 14 | "test/" 15 | ], 16 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 17 | }, 18 | # 19 | # Load and configure plugins here: 20 | # 21 | plugins: [], 22 | # 23 | # If you create your own checks, you must specify the source files for 24 | # them here, so they can be loaded by Credo before running the analysis. 25 | # 26 | requires: [], 27 | # 28 | # If you want to enforce a style guide and need a more traditional linting 29 | # experience, you can change `strict` to `true` below: 30 | # 31 | strict: true, 32 | # 33 | # To modify the timeout for parsing files, change this value: 34 | # 35 | parse_timeout: 5000, 36 | # 37 | # If you want to use uncolored output by default, you can change `color` 38 | # to `false` below: 39 | # 40 | color: true, 41 | # 42 | # You can customize the parameters of any check by adding a second element 43 | # to the tuple. 44 | # 45 | # To disable a check put `false` as second element: 46 | # 47 | # {Credo.Check.Design.DuplicatedCode, false} 48 | # 49 | checks: [ 50 | # 51 | ## Consistency Checks 52 | # 53 | {Credo.Check.Consistency.ExceptionNames, []}, 54 | {Credo.Check.Consistency.LineEndings, []}, 55 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 56 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 57 | {Credo.Check.Consistency.SpaceInParentheses, []}, 58 | {Credo.Check.Consistency.TabsOrSpaces, []}, 59 | 60 | # 61 | ## Design Checks 62 | # 63 | # You can customize the priority of any check 64 | # Priority values are: `low, normal, high, higher` 65 | # 66 | {Credo.Check.Design.AliasUsage, 67 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 68 | # You can also customize the exit_status of each check. 69 | # If you don't want TODO comments to cause `mix credo` to fail, just 70 | # set this value to 0 (zero). 71 | # 72 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 73 | {Credo.Check.Design.TagFIXME, []}, 74 | 75 | # 76 | ## Readability Checks 77 | # 78 | {Credo.Check.Readability.AliasOrder, []}, 79 | {Credo.Check.Readability.FunctionNames, []}, 80 | {Credo.Check.Readability.LargeNumbers, []}, 81 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 82 | {Credo.Check.Readability.ModuleAttributeNames, []}, 83 | {Credo.Check.Readability.ModuleDoc, []}, 84 | {Credo.Check.Readability.ModuleNames, []}, 85 | {Credo.Check.Readability.ParenthesesInCondition, []}, 86 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 87 | {Credo.Check.Readability.PredicateFunctionNames, []}, 88 | {Credo.Check.Readability.PreferImplicitTry, []}, 89 | {Credo.Check.Readability.RedundantBlankLines, []}, 90 | {Credo.Check.Readability.Semicolons, []}, 91 | {Credo.Check.Readability.SpaceAfterCommas, []}, 92 | {Credo.Check.Readability.StringSigils, []}, 93 | {Credo.Check.Readability.TrailingBlankLine, []}, 94 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 95 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 96 | {Credo.Check.Readability.VariableNames, []}, 97 | 98 | # 99 | ## Refactoring Opportunities 100 | # 101 | {Credo.Check.Refactor.CondStatements, []}, 102 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 103 | {Credo.Check.Refactor.FunctionArity, []}, 104 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 105 | # {Credo.Check.Refactor.MapInto, []}, 106 | {Credo.Check.Refactor.MatchInCondition, []}, 107 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 108 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 109 | {Credo.Check.Refactor.Nesting, []}, 110 | {Credo.Check.Refactor.UnlessWithElse, []}, 111 | {Credo.Check.Refactor.WithClauses, []}, 112 | 113 | # 114 | ## Warnings 115 | # 116 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 117 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 118 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 119 | {Credo.Check.Warning.IExPry, []}, 120 | {Credo.Check.Warning.IoInspect, []}, 121 | # {Credo.Check.Warning.LazyLogging, []}, 122 | {Credo.Check.Warning.MixEnv, false}, 123 | {Credo.Check.Warning.OperationOnSameValues, []}, 124 | {Credo.Check.Warning.OperationWithConstantResult, []}, 125 | {Credo.Check.Warning.RaiseInsideRescue, []}, 126 | {Credo.Check.Warning.UnusedEnumOperation, []}, 127 | {Credo.Check.Warning.UnusedFileOperation, []}, 128 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 129 | {Credo.Check.Warning.UnusedListOperation, []}, 130 | {Credo.Check.Warning.UnusedPathOperation, []}, 131 | {Credo.Check.Warning.UnusedRegexOperation, []}, 132 | {Credo.Check.Warning.UnusedStringOperation, []}, 133 | {Credo.Check.Warning.UnusedTupleOperation, []}, 134 | {Credo.Check.Warning.UnsafeExec, []}, 135 | 136 | # 137 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 138 | 139 | # 140 | # Controversial and experimental checks (opt-in, just replace `false` with `[]`) 141 | # 142 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 143 | {Credo.Check.Consistency.UnusedVariableNames, false}, 144 | {Credo.Check.Design.DuplicatedCode, false}, 145 | {Credo.Check.Readability.AliasAs, false}, 146 | {Credo.Check.Readability.BlockPipe, false}, 147 | {Credo.Check.Readability.ImplTrue, false}, 148 | {Credo.Check.Readability.MultiAlias, false}, 149 | {Credo.Check.Readability.SeparateAliasRequire, false}, 150 | {Credo.Check.Readability.SinglePipe, false}, 151 | {Credo.Check.Readability.Specs, false}, 152 | {Credo.Check.Readability.StrictModuleLayout, false}, 153 | {Credo.Check.Readability.WithCustomTaggedTuple, false}, 154 | {Credo.Check.Refactor.ABCSize, false}, 155 | {Credo.Check.Refactor.AppendSingleItem, false}, 156 | {Credo.Check.Refactor.DoubleBooleanNegation, false}, 157 | {Credo.Check.Refactor.ModuleDependencies, false}, 158 | {Credo.Check.Refactor.NegatedIsNil, false}, 159 | {Credo.Check.Refactor.PipeChainStart, false}, 160 | {Credo.Check.Refactor.VariableRebinding, false}, 161 | {Credo.Check.Warning.LeakyEnvironment, false}, 162 | {Credo.Check.Warning.MapGetUnsafePass, false}, 163 | {Credo.Check.Warning.UnsafeToAtom, false} 164 | 165 | # 166 | # Custom checks can be created using `mix credo.gen.check`. 167 | # 168 | ] 169 | } 170 | ] 171 | } 172 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: ["master"] # adapt branch for project 6 | pull_request: 7 | branches: ["master"] # adapt branch for project 8 | 9 | # Sets the ENV `MIX_ENV` to `test` for running tests 10 | env: 11 | MIX_ENV: test 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | name: Test on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} 20 | strategy: 21 | # Specify the OTP and Elixir versions to use when building 22 | # and running the workflow steps. 23 | matrix: 24 | otp: ["26.0.2"] # Define the OTP version [required] 25 | elixir: ["1.15.4"] # Define the elixir version [required] 26 | steps: 27 | # Step: Setup Elixir + Erlang image as the base. 28 | - name: Set up Elixir 29 | uses: erlef/setup-beam@v1 30 | with: 31 | otp-version: ${{matrix.otp}} 32 | elixir-version: ${{matrix.elixir}} 33 | 34 | # Step: Check out the code. 35 | - name: Checkout code 36 | uses: actions/checkout@v3 37 | 38 | # Step: Define how to cache the `_build` directory. After the first run, 39 | # this speeds up tests runs a lot. This includes not re-compiling our 40 | # project's downloaded deps every run. 41 | - name: Cache PLTS 42 | id: cache-plts 43 | uses: actions/cache@v3 44 | env: 45 | cache-name: cache-plts 46 | with: 47 | path: priv/plts 48 | key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} 49 | restore-keys: | 50 | ${{ runner.os }}-mix-${{ env.cache-name }}- 51 | 52 | # Step: Define how to cache deps. Restores existing cache if present. 53 | - name: Cache deps 54 | id: cache-deps 55 | uses: actions/cache@v3 56 | env: 57 | cache-name: cache-elixir-deps 58 | with: 59 | path: deps 60 | key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} 61 | restore-keys: | 62 | ${{ runner.os }}-mix-${{ env.cache-name }}- 63 | 64 | # Step: Define how to cache the `_build` directory. After the first run, 65 | # this speeds up tests runs a lot. This includes not re-compiling our 66 | # project's downloaded deps every run. 67 | - name: Cache compiled build 68 | id: cache-build 69 | uses: actions/cache@v3 70 | env: 71 | cache-name: cache-compiled-build 72 | with: 73 | path: _build 74 | key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} 75 | restore-keys: | 76 | ${{ runner.os }}-mix-${{ env.cache-name }}- 77 | ${{ runner.os }}-mix- 78 | 79 | # Step: Conditionally bust the cache when job is re-run. 80 | # Sometimes, we may have issues with incremental builds that are fixed by 81 | # doing a full recompile. In order to not waste dev time on such trivial 82 | # issues (while also reaping the time savings of incremental builds for 83 | # *most* day-to-day development), force a full recompile only on builds 84 | # that are retried. 85 | - name: Clean to rule out incremental build as a source of flakiness 86 | if: github.run_attempt != '1' 87 | run: | 88 | mix deps.clean --all 89 | mix clean 90 | shell: sh 91 | 92 | # Step: Download project dependencies. If unchanged, uses 93 | # the cached version. 94 | - name: Install dependencies 95 | run: mix deps.get 96 | 97 | # Step: Compile the project treating any warnings as errors. 98 | # Customize this step if a different behavior is desired. 99 | - name: Compiles without warnings 100 | run: mix compile --warnings-as-errors 101 | 102 | # Step: Check that the checked in code has already been formatted. 103 | # This step fails if something was found unformatted. 104 | # Customize this step as desired. 105 | - name: Check Formatting 106 | run: mix format --check-formatted 107 | 108 | # Step: Run the linter. 109 | - name: Lint 110 | run: mix credo 111 | 112 | # Step: Execute the tests. 113 | - name: Run tests 114 | run: mix test 115 | 116 | # Step: Run dialyzer to check typespecs. 117 | - name: Type Check 118 | run: mix dialyzer --quiet 119 | -------------------------------------------------------------------------------- /.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 third-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 | elixir_plaid-*.tar 24 | 25 | 26 | # Temporary files for e.g. tests 27 | /tmp 28 | 29 | /priv/plts/*.plt 30 | /priv/plts/*.plt.hash 31 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.15.4 2 | erlang 26.0.2 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.2.0] - 2023-08-01 11 | 12 | - Adds Plaid Identity Match API's. [PR](https://github.com/tylerwray/elixir-plaid/pull/10). Thanks [@robacarp](https://github.com/robacarp) and [@mackshkatz](https://github.com/mackshkatz)! 13 | - Bumps Supported elixir version to 1.15 and supported erlang version to 26. 14 | - Bumps [httpoison](https://hexdocs.pm/httpoison/HTTPoison.html) version to 2.0. 15 | 16 | ## [1.1.2] - 2022-03-19 17 | 18 | ### Chore 19 | 20 | - Updated dependencies. Mainly to ship new version of elixir docs. 21 | 22 | ## [1.1.1] - 2022-03-19 23 | 24 | ### Fixed 25 | 26 | - Made link token user fields optional. [PR](https://github.com/tylerwray/elixir-plaid/pull/8) Thanks [@ktayah](https://github.com/ktayah)! 27 | 28 | ## [1.1.0] - 2021-06-16 29 | 30 | ### Added 31 | 32 | - Swappable HTTP client. 33 | 34 | ## [1.0.0] - 2021-04-06 35 | 36 | ### Added 37 | 38 | - Full Plaid API coverage and documentation. 39 | - Webhook helpers. 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | I especially want to help first-time contributors to open source and/or elixir contribute to this project. 4 | 5 | 1. Search for an existing issue that may have the answers you need. 6 | 2. Create an issue to collaborate with the maintainers on the problem. 7 | 3. Create an MR and schedule it for a release with the maintainers. 8 | 9 | Alternatively, just create a PR! They are always welcome 🤗 10 | 11 | ## Motivation & Principles; Explained 12 | 13 | 1. **Provide FANTASTIC documentation**: Provide just the right amount of documentation for developers to find what they need. 14 | 2. **Full plaid API coverage**: Do our best to keep pairity with the production Plaid API. 15 | 3. **Use the plaid API versioning plan**: Maintain a version of this library that matches each supported version of the plaid API. 16 | 4. **Return well-defined structs, always**: Structs are a form of documentation, they make it easier for developers to work with responses. Always return them. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tyler Wray 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 | -------------------------------------------------------------------------------- /guides/webhooks.md: -------------------------------------------------------------------------------- 1 | # Webhooks example 2 | 3 | Implementing webhook verification can be very tricky. Hopefully this library 4 | can help make it a little less painful by strictly adhering to contract with the Plaid API so you don't have to worry about it. 5 | 6 | > This is the path of least resistence gathered from phoenix/plug documentation. There may be other nuances that your app 7 | > needs to account for. This guide is based on a base generated phoenix app with `mix phx.new my_app`. 8 | 9 | To start using plaid webhooks in a standard phoenix application, you can do the following. 10 | 11 | 1. [Raw body access](#raw-body-access) 12 | 2. [Register a webhook route](#register-a-webhook-route) 13 | 3. [Setup a controller](#setup-a-controller) 14 | 15 | ## Raw body access 16 | 17 | Plaid uses a verification token that is generated from the "raw" request body sent to your webhook endpoint. You will 18 | need access to that same "raw" request body to verify the request using their token. 19 | 20 | To do so, add a custom body reader so that you'll have access to the raw body in the controller. 21 | 22 | Pulled straight from the [Plug.Parser docs](https://hexdocs.pm/plug/Plug.Parsers.html#module-custom-body-reader). 23 | 24 | ```elixir 25 | # lib/my_app_web/endpoint.ex 26 | defmodule CacheBodyReader do 27 | def read_body(conn, opts) do 28 | {:ok, body, conn} = Plug.Conn.read_body(conn, opts) 29 | conn = update_in(conn.assigns[:raw_body], &[body | &1 || []]) 30 | {:ok, body, conn} 31 | end 32 | end 33 | 34 | plug Plug.Parsers, 35 | parsers: [:urlencoded, :multipart, :json], 36 | pass: ["*/*"], 37 | body_reader: {CacheBodyReader, :read_body, []}, 38 | json_decoder: Phoenix.json_library() 39 | ``` 40 | 41 | ## Register a webhook route 42 | 43 | You need a route that your app exposes so that Plaid can make an API call to you. 44 | 45 | ```elixir 46 | # lib/my_app_web/router.ex 47 | scope "/webhooks", MyAppWeb do 48 | pipe_through :api 49 | 50 | post "/plaid", PlaidWebhookController, :index 51 | end 52 | ``` 53 | 54 | ## Setup a controller 55 | 56 | Now you need to handle any requests that come to the `/webhooks/plaid` route on your app. 57 | 58 | > Notice how we're using the raw body from `conn.assigns[:raw_body]` setup in the first step. 59 | 60 | ```elixir 61 | # lib/my_app_web/controllers/plaid_webhook_controller.ex 62 | defmodule MyAppWeb.PlaidWebhookController do 63 | use MyAppWeb, :controller 64 | 65 | def index(conn, _) do 66 | jwt = 67 | conn 68 | |> get_req_header("plaid-verification") 69 | |> List.first() 70 | 71 | raw_body = conn.assigns[:raw_body] 72 | 73 | config = [client_id: "123", secret: "abc"] 74 | 75 | case Plaid.Webhooks.verify_and_construct(jwt, raw_body, config) do 76 | {:ok, body} -> 77 | handle_webhook(body) 78 | json(conn, %{"status" => "ok"}) 79 | 80 | _ -> 81 | conn 82 | |> put_status(400) 83 | |> json(%{"status" => "error"}) 84 | end 85 | end 86 | 87 | defp handle_webhook(%{ 88 | webhook_type: "TRANSACTIONS", 89 | webhook_code: "DEFAULT_UPDATE", 90 | item_id: item_id 91 | }) do 92 | # Update transactions with item_id 93 | end 94 | 95 | defp handle_webhook(%{ 96 | webhook_type: "AUTH", 97 | webhook_code: "VERIFICATION_EXPIRED", 98 | account_id: account_id 99 | }) do 100 | # Tell user to re-verify 101 | end 102 | 103 | defp handle_webhook(_), do: :ok 104 | end 105 | ``` 106 | 107 | --- 108 | 109 | There you have it! You are now setup to process webhooks from plaid 🎉 110 | -------------------------------------------------------------------------------- /lib/plaid.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid do 2 | @moduledoc """ 3 | Shared types used in the library. 4 | """ 5 | 6 | @typedoc "Supported environments in the Plaid API." 7 | @type env :: :production | :development | :sandbox 8 | 9 | @typedoc """ 10 | Configuration that can be passed to each authenticated request. 11 | 12 | * `:client_id` - The client_id Plaid uses for authentication. 13 | * `:secret` - The secret Plaid uses for authentication. 14 | * `:env` - A supported [Plaid environment](https://plaid.com/docs/api/#api-host). 15 | * `:http_client` - Any module that implements the `Plaid.Client` behaviour. 16 | * `:test_api_host` - A way to override the URL for requests. Useful for E2E or integration testing. 17 | 18 | > `client_id` and `secret` are required. 19 | """ 20 | @type config :: [ 21 | client_id: String.t(), 22 | secret: String.t(), 23 | env: env(), 24 | http_client: module(), 25 | test_api_host: String.t() 26 | ] 27 | 28 | @typedoc """ 29 | Configuration that can be passed to any un-authenticated request. 30 | 31 | * `:env` - A supported [Plaid environment](https://plaid.com/docs/api/#api-host). 32 | * `:http_client` - Any module that implements the `Plaid.Client` behaviour. 33 | * `:test_api_host` - A way to override the URL for requests. Useful for E2E or integration testing. 34 | """ 35 | @type noauth_config :: [ 36 | test_api_host: String.t(), 37 | http_client: module(), 38 | env: env() 39 | ] 40 | end 41 | -------------------------------------------------------------------------------- /lib/plaid/account.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Account do 2 | @moduledoc """ 3 | [Plaid Account schema.](https://plaid.com/docs/api/accounts) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Account.{Balances, HistoricalBalances} 9 | alias Plaid.Castable 10 | alias Plaid.Identity 11 | alias Plaid.Item 12 | alias Plaid.Transactions.Transaction 13 | 14 | @type t :: %__MODULE__{ 15 | account_id: String.t(), 16 | balances: Balances.t(), 17 | days_available: non_neg_integer() | nil, 18 | historical_balances: [HistoricalBalances.t()] | nil, 19 | mask: String.t() | nil, 20 | name: String.t(), 21 | official_name: String.t() | nil, 22 | type: String.t(), 23 | subtype: String.t(), 24 | transactions: [Transaction.t()] | nil, 25 | verification_status: String.t() | nil, 26 | item: Plaid.Item.t(), 27 | owners: [Plaid.Identity.t()] | nil, 28 | ownership_type: String.t() | nil 29 | } 30 | 31 | defstruct [ 32 | :account_id, 33 | :balances, 34 | :days_available, 35 | :historical_balances, 36 | :mask, 37 | :name, 38 | :official_name, 39 | :type, 40 | :subtype, 41 | :transactions, 42 | :verification_status, 43 | :item, 44 | :owners, 45 | :ownership_type 46 | ] 47 | 48 | @impl true 49 | def cast(generic_map) do 50 | %__MODULE__{ 51 | account_id: generic_map["account_id"], 52 | balances: Castable.cast(Balances, generic_map["balances"]), 53 | days_available: generic_map["days_available"], 54 | historical_balances: 55 | Castable.cast_list(HistoricalBalances, generic_map["historical_balances"]), 56 | mask: generic_map["mask"], 57 | name: generic_map["name"], 58 | official_name: generic_map["official_name"], 59 | type: generic_map["type"], 60 | subtype: generic_map["subtype"], 61 | transactions: Castable.cast_list(Transaction, generic_map["transactions"]), 62 | verification_status: generic_map["verification_status"], 63 | item: Castable.cast(Item, generic_map["item"]), 64 | owners: Castable.cast_list(Identity, generic_map["owners"]), 65 | ownership_type: generic_map["ownership_type"] 66 | } 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/plaid/account/balances.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Account.Balances do 2 | @moduledoc """ 3 | [Plaid Balances schema.](https://plaid.com/docs/api/accounts) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | available: number() | nil, 10 | current: number(), 11 | limit: number() | nil, 12 | iso_currency_code: String.t() | nil, 13 | unofficial_currency_code: String.t() | nil 14 | } 15 | 16 | defstruct [ 17 | :available, 18 | :current, 19 | :limit, 20 | :iso_currency_code, 21 | :unofficial_currency_code 22 | ] 23 | 24 | @impl true 25 | def cast(generic_map) do 26 | %__MODULE__{ 27 | available: generic_map["available"], 28 | current: generic_map["current"], 29 | limit: generic_map["limit"], 30 | iso_currency_code: generic_map["iso_currency_code"], 31 | unofficial_currency_code: generic_map["unofficial_currency_code"] 32 | } 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/plaid/account/historical_balances.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Account.HistoricalBalances do 2 | @moduledoc """ 3 | [Plaid Account Historical Balances schema](https://plaid.com/docs/api/products/#asset_report-get-response-historical-balances) 4 | 5 | Only used when retrieving Asset Report's. 6 | """ 7 | 8 | @behaviour Plaid.Castable 9 | 10 | @type t :: %__MODULE__{ 11 | current: number(), 12 | date: String.t(), 13 | iso_currency_code: String.t() | nil, 14 | unofficial_currency_code: String.t() | nil 15 | } 16 | 17 | defstruct [ 18 | :current, 19 | :date, 20 | :iso_currency_code, 21 | :unofficial_currency_code 22 | ] 23 | 24 | @impl true 25 | def cast(generic_map) do 26 | %__MODULE__{ 27 | current: generic_map["current"], 28 | date: generic_map["date"], 29 | iso_currency_code: generic_map["iso_currency_code"], 30 | unofficial_currency_code: generic_map["unofficial_currency_code"] 31 | } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/plaid/accounts.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Accounts do 2 | @moduledoc """ 3 | [Plaid Accounts API](https://plaid.com/docs/api/accounts) calls and schema. 4 | """ 5 | 6 | alias Plaid.Castable 7 | 8 | defmodule GetResponse do 9 | @moduledoc """ 10 | [Plaid API /accounts/get response schema.](https://plaid.com/docs/api/accounts). 11 | """ 12 | 13 | @behaviour Castable 14 | 15 | alias Plaid.Account 16 | alias Plaid.Item 17 | 18 | @type t :: %__MODULE__{ 19 | accounts: [Account.t()], 20 | item: Item.t(), 21 | request_id: String.t() 22 | } 23 | 24 | defstruct [:accounts, :item, :request_id] 25 | 26 | @impl true 27 | def cast(generic_map) do 28 | %__MODULE__{ 29 | accounts: Castable.cast_list(Account, generic_map["accounts"]), 30 | item: Castable.cast(Item, generic_map["item"]), 31 | request_id: generic_map["request_id"] 32 | } 33 | end 34 | end 35 | 36 | @doc """ 37 | Get information about all available accounts. 38 | 39 | Does a `POST /accounts/get` call to retrieve high level account information 40 | associated with an access_token's item. 41 | 42 | Params: 43 | * `access_token` - Token to fetch accounts for. 44 | 45 | Options: 46 | * `:account_ids` - Specific account ids to fetch accounts for. 47 | 48 | ## Examples 49 | 50 | Accounts.get("access-sandbox-123xxx", client_id: "123", secret: "abc") 51 | {:ok, %Accounts.GetResponse{}} 52 | 53 | """ 54 | @spec get(String.t(), options, Plaid.config()) :: 55 | {:ok, GetResponse.t()} | {:error, Plaid.Error.t()} 56 | when options: %{optional(:account_ids) => [String.t()]} 57 | def get(access_token, options \\ %{}, config) do 58 | options_payload = Map.take(options, [:account_ids]) 59 | 60 | payload = 61 | %{} 62 | |> Map.put(:access_token, access_token) 63 | |> Map.put(:options, options_payload) 64 | 65 | Plaid.Client.call("/accounts/get", payload, GetResponse, config) 66 | end 67 | 68 | @doc """ 69 | Get information about all available balances. 70 | 71 | Does a `POST /accounts/balance/get` call to retrieve real-time balance 72 | information for all accounts associated with an access_token's item. 73 | 74 | This API call will force balances to be refreshed, rather than use 75 | the cache like other API calls that return balances. 76 | 77 | Params: 78 | * `access_token` - Token to fetch accounts for. 79 | 80 | Options: 81 | * `:account_ids` - Specific account ids to fetch balances for. 82 | 83 | ## Examples 84 | 85 | Accounts.get_balance("access-sandbox-123xxx", client_id: "123", secret: "abc") 86 | {:ok, %Accounts.GetResponse{}} 87 | 88 | """ 89 | @spec get_balance(String.t(), options, Plaid.config()) :: 90 | {:ok, GetResponse.t()} | {:error, Plaid.Error.t()} 91 | when options: %{optional(:account_ids) => [String.t()]} 92 | def get_balance(access_token, options \\ %{}, config) do 93 | options_payload = Map.take(options, [:account_ids]) 94 | 95 | payload = 96 | %{} 97 | |> Map.put(:access_token, access_token) 98 | |> Map.put(:options, options_payload) 99 | 100 | Plaid.Client.call("/accounts/balance/get", payload, GetResponse, config) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/plaid/address.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Address do 2 | @moduledoc """ 3 | [Plaid Address schema.](https://plaid.com/docs/api/products/#identity-get-response-data) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | city: String.t() | nil, 10 | region: String.t() | nil, 11 | street: String.t() | nil, 12 | postal_code: String.t() | nil, 13 | country: String.t() | nil 14 | } 15 | 16 | @derive Jason.Encoder 17 | defstruct [ 18 | :city, 19 | :region, 20 | :street, 21 | :postal_code, 22 | :country 23 | ] 24 | 25 | @impl true 26 | def cast(generic_map) do 27 | %__MODULE__{ 28 | city: generic_map["city"], 29 | region: generic_map["region"], 30 | street: generic_map["street"], 31 | postal_code: generic_map["postal_code"], 32 | country: generic_map["country"] 33 | } 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/plaid/asset_report/item.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.AssetReport.Report.Item do 2 | @moduledoc """ 3 | [Plaid Asset Report Item schema](https://plaid.com/docs/api/products/#asset_report-get-response-items) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Account 9 | alias Plaid.Castable 10 | 11 | @type t :: %__MODULE__{ 12 | item_id: String.t(), 13 | institution_name: String.t(), 14 | institution_id: String.t(), 15 | date_last_updated: String.t(), 16 | accounts: [Account.t()] 17 | } 18 | 19 | defstruct [ 20 | :item_id, 21 | :institution_name, 22 | :institution_id, 23 | :date_last_updated, 24 | :accounts 25 | ] 26 | 27 | @impl true 28 | def cast(generic_map) do 29 | %__MODULE__{ 30 | item_id: generic_map["item_id"], 31 | institution_name: generic_map["institution_name"], 32 | institution_id: generic_map["institution_id"], 33 | date_last_updated: generic_map["date_last_updated"], 34 | accounts: Castable.cast_list(Account, generic_map["accounts"]) 35 | } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/plaid/asset_report/report.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.AssetReport.Report do 2 | @moduledoc """ 3 | [Plaid Asset Report schema](https://plaid.com/docs/api/products/#asset_reportget) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.AssetReport.Report 9 | alias Plaid.AssetReport.User 10 | alias Plaid.Castable 11 | 12 | @type t :: %__MODULE__{ 13 | asset_report_id: String.t(), 14 | client_report_id: String.t(), 15 | date_generated: String.t(), 16 | days_requested: non_neg_integer(), 17 | items: [Report.Item.t()], 18 | user: User.t() 19 | } 20 | 21 | defstruct [ 22 | :asset_report_id, 23 | :client_report_id, 24 | :date_generated, 25 | :days_requested, 26 | :items, 27 | :user 28 | ] 29 | 30 | @impl true 31 | def cast(generic_map) do 32 | %__MODULE__{ 33 | asset_report_id: generic_map["asset_report_id"], 34 | client_report_id: generic_map["client_report_id"], 35 | date_generated: generic_map["date_generated"], 36 | days_requested: generic_map["days_requested"], 37 | items: Castable.cast_list(Report.Item, generic_map["items"]), 38 | user: Castable.cast(User, generic_map["user"]) 39 | } 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/plaid/asset_report/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.AssetReport.User do 2 | @moduledoc """ 3 | [Plaid Asset Report User schema.](https://plaid.com/docs/api/products/#asset_report-create-request-user) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | client_user_id: String.t(), 10 | first_name: String.t(), 11 | middle_name: String.t(), 12 | last_name: String.t(), 13 | ssn: String.t(), 14 | phone_number: String.t(), 15 | email: String.t() 16 | } 17 | 18 | @derive Jason.Encoder 19 | defstruct [ 20 | :client_user_id, 21 | :first_name, 22 | :middle_name, 23 | :last_name, 24 | :ssn, 25 | :phone_number, 26 | :email 27 | ] 28 | 29 | @impl true 30 | def cast(generic_map) do 31 | %__MODULE__{ 32 | client_user_id: generic_map["client_user_id"], 33 | first_name: generic_map["first_name"], 34 | middle_name: generic_map["middle_name"], 35 | last_name: generic_map["last_name"], 36 | ssn: generic_map["ssn"], 37 | phone_number: generic_map["phone_number"], 38 | email: generic_map["email"] 39 | } 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/plaid/asset_report/warning.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.AssetReport.Warning do 2 | @moduledoc """ 3 | [Plaid Asset Report Warning schema](https://plaid.com/docs/api/products/#asset_report-get-response-warnings) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.AssetReport.Warning.Cause 9 | alias Plaid.Castable 10 | 11 | @type t :: %__MODULE__{ 12 | warning_type: String.t(), 13 | warning_code: String.t(), 14 | cause: Cause.t() 15 | } 16 | 17 | defstruct [ 18 | :warning_type, 19 | :warning_code, 20 | :cause 21 | ] 22 | 23 | @impl true 24 | def cast(generic_map) do 25 | %__MODULE__{ 26 | warning_type: generic_map["warning_type"], 27 | warning_code: generic_map["warning_code"], 28 | cause: Castable.cast(Cause, generic_map["cause"]) 29 | } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/plaid/asset_report/warning/cause.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.AssetReport.Warning.Cause do 2 | @moduledoc """ 3 | [Plaid Asset Report Warning Cause schema](https://plaid.com/docs/api/products/#asset_report-get-response-cause) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Castable 9 | 10 | @type t :: %__MODULE__{ 11 | item_id: String.t(), 12 | error: Plaid.Error.t() | nil 13 | } 14 | 15 | defstruct [ 16 | :item_id, 17 | :error 18 | ] 19 | 20 | @impl true 21 | def cast(generic_map) do 22 | %__MODULE__{ 23 | item_id: generic_map["item_id"], 24 | error: Castable.cast(Plaid.Error, generic_map["error"]) 25 | } 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/plaid/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Auth do 2 | @moduledoc """ 3 | [Plaid Auth API](https://plaid.com/docs/api/products/#auth) calls and schema. 4 | """ 5 | 6 | defmodule GetResponse do 7 | @moduledoc """ 8 | [Plaid API /auth/get response schema.](https://plaid.com/docs/api/products/#auth). 9 | """ 10 | 11 | @behaviour Plaid.Castable 12 | 13 | alias Plaid.Account 14 | alias Plaid.Auth.Numbers 15 | alias Plaid.Castable 16 | alias Plaid.Item 17 | 18 | @type t :: %__MODULE__{ 19 | accounts: [Account.t()], 20 | numbers: Numbers.t(), 21 | item: Item.t(), 22 | request_id: String.t() 23 | } 24 | 25 | defstruct [ 26 | :accounts, 27 | :numbers, 28 | :item, 29 | :request_id 30 | ] 31 | 32 | @impl true 33 | def cast(generic_map) do 34 | %__MODULE__{ 35 | accounts: Castable.cast_list(Account, generic_map["accounts"]), 36 | numbers: Castable.cast(Numbers, generic_map["numbers"]), 37 | item: Castable.cast(Item, generic_map["item"]), 38 | request_id: generic_map["request_id"] 39 | } 40 | end 41 | end 42 | 43 | @doc """ 44 | Get information about account and routing numbers for 45 | checking and savings accounts. 46 | 47 | Does a `POST /auth/get` call which returns high level account information 48 | along with account and routing numbers for checking and savings 49 | accounts. 50 | 51 | Params: 52 | * `access_token` - Token to fetch accounts for. 53 | 54 | Options: 55 | * `:account_ids` - Specific account ids to fetch balances for. 56 | 57 | ## Examples 58 | 59 | Auth.get("access-sandbox-123xxx", client_id: "123", secret: "abc") 60 | {:ok, %Auth.GetResponse{}} 61 | 62 | """ 63 | @spec get(String.t(), options, Plaid.config()) :: 64 | {:ok, GetResponse.t()} | {:error, Plaid.Error.t()} 65 | when options: %{optional(:account_ids) => [String.t()]} 66 | def get(access_token, options \\ %{}, config) do 67 | options_payload = Map.take(options, [:account_ids]) 68 | 69 | payload = 70 | %{} 71 | |> Map.put(:access_token, access_token) 72 | |> Map.put(:options, options_payload) 73 | 74 | Plaid.Client.call( 75 | "/auth/get", 76 | payload, 77 | GetResponse, 78 | config 79 | ) 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/plaid/auth/numbers.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Auth.Numbers do 2 | @moduledoc """ 3 | [Plaid Numbers schema](https://plaid.com/docs/api/products/#auth-get-response-numbers). 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Auth.Numbers.{ACH, BACS, EFT, International} 9 | alias Plaid.Castable 10 | 11 | @type t :: %__MODULE__{ 12 | ach: [ACH.t()], 13 | eft: [EFT.t()], 14 | international: [International.t()], 15 | bacs: [BACS.t()] 16 | } 17 | 18 | defstruct [ 19 | :ach, 20 | :eft, 21 | :international, 22 | :bacs 23 | ] 24 | 25 | @impl true 26 | def cast(generic_map) do 27 | %__MODULE__{ 28 | ach: Castable.cast_list(ACH, generic_map["ach"]), 29 | eft: Castable.cast_list(EFT, generic_map["eft"]), 30 | international: Castable.cast_list(International, generic_map["international"]), 31 | bacs: Castable.cast_list(BACS, generic_map["bacs"]) 32 | } 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/plaid/auth/numbers/ach.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Auth.Numbers.ACH do 2 | @moduledoc """ 3 | [Plaid Numbers ACH schema](https://plaid.com/docs/api/products/#auth-get-response-ach). 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | account: String.t(), 10 | account_id: String.t(), 11 | routing: String.t(), 12 | wire_routing: String.t() | nil 13 | } 14 | 15 | defstruct [ 16 | :account, 17 | :account_id, 18 | :routing, 19 | :wire_routing 20 | ] 21 | 22 | @impl true 23 | def cast(generic_map) do 24 | %__MODULE__{ 25 | account: generic_map["account"], 26 | account_id: generic_map["account_id"], 27 | routing: generic_map["routing"], 28 | wire_routing: generic_map["wire_routing"] 29 | } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/plaid/auth/numbers/bacs.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Auth.Numbers.BACS do 2 | @moduledoc """ 3 | [Plaid Numbers BACS schema](https://plaid.com/docs/api/products/#auth-get-response-bacs). 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | account: String.t(), 10 | account_id: String.t(), 11 | sort_code: String.t() 12 | } 13 | 14 | defstruct [ 15 | :account, 16 | :account_id, 17 | :sort_code 18 | ] 19 | 20 | @impl true 21 | def cast(generic_map) do 22 | %__MODULE__{ 23 | account: generic_map["account"], 24 | account_id: generic_map["account_id"], 25 | sort_code: generic_map["sort_code"] 26 | } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/plaid/auth/numbers/eft.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Auth.Numbers.EFT do 2 | @moduledoc """ 3 | [Plaid Numbers EFT schema](https://plaid.com/docs/api/products/#auth-get-response-eft). 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | account: String.t(), 10 | account_id: String.t(), 11 | institution: String.t(), 12 | branch: String.t() 13 | } 14 | 15 | defstruct [ 16 | :account, 17 | :account_id, 18 | :institution, 19 | :branch 20 | ] 21 | 22 | @impl true 23 | def cast(generic_map) do 24 | %__MODULE__{ 25 | account: generic_map["account"], 26 | account_id: generic_map["account_id"], 27 | institution: generic_map["institution"], 28 | branch: generic_map["branch"] 29 | } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/plaid/auth/numbers/international.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Auth.Numbers.International do 2 | @moduledoc """ 3 | [Plaid Numbers International schema](https://plaid.com/docs/api/products/#auth-get-response-international). 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | account_id: String.t(), 10 | iban: String.t(), 11 | bic: String.t() 12 | } 13 | 14 | defstruct [ 15 | :account_id, 16 | :iban, 17 | :bic 18 | ] 19 | 20 | @impl true 21 | def cast(generic_map) do 22 | %__MODULE__{ 23 | account_id: generic_map["account_id"], 24 | iban: generic_map["iban"], 25 | bic: generic_map["bic"] 26 | } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/plaid/castable.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Castable do 2 | @moduledoc false 3 | 4 | @type generic_map :: %{String.t() => any()} 5 | 6 | @doc """ 7 | Core to how this library functions is the `Plaid.Castable` behaviour for casting response objects to structs. 8 | Each struct implements the behaviour by adding a `cast/1` fuction that take a generic string-keyed 9 | map and turns it into a struct of it's own type. 10 | 11 | `Plaid.Castable.cast/2` and `Plaid.Castable.cast_list/2` are helper functions for casting nested objects. 12 | Both which have the same signature taking first a module and second the string-keyed map. 13 | 14 | Each API call function will return some `__Response` struct. i.e. `GetResponse`, `SearchResponse`, `CreateResponse`, etc. 15 | This is because of how plaid's API is structured, and it follows the same pattern as the client-libraries they 16 | maintain in go, ruby, java, and other languages. 17 | 18 | Take advantage of nested modules for structs when possible and it makes sense. 19 | Nest all `__Response` structs next to the functions that make the API calls. 20 | 21 | The `Plaid.Client` module that makes all the API calls must be passed a `__Response` module that implements 22 | the `Plaid.Castable` behaviour. It then calls the `cast/1` function on that `__Response` module; passing 23 | the response body to kick off the response casting process. 24 | 25 | ### Made-up example 26 | 27 | Endpoint: `/payments/get` 28 | 29 | Response: 30 | 31 | ```json 32 | { 33 | "payment": { 34 | "customer_name": "Stefani Germanotta", 35 | "items": [ 36 | { 37 | "amount": 1200, 38 | "name": "Toy Solider" 39 | }, 40 | { 41 | "amount": 3100, 42 | "name": "Video Game" 43 | } 44 | ] 45 | } 46 | } 47 | ``` 48 | 49 | Elixir structs: 50 | 51 | ```elixir 52 | defmodule Plaid.Payment do 53 | @behaviour Plaid.Castable 54 | 55 | alias Plaid.Castable 56 | 57 | defstruct [:customer_name, :items] 58 | 59 | defmodule Item do 60 | @behaviour Plaid.Castable 61 | 62 | defstruct [:amount, :name] 63 | 64 | @impl true 65 | def cast(generic_map) do 66 | %__MODULE__{ 67 | amount: generic_map["amount"], 68 | name: generic_map["name"] 69 | } 70 | end 71 | end 72 | 73 | @impl true 74 | def cast(generic_map) do 75 | %__MODULE__{ 76 | customer_name: generic_map["customer_name"], 77 | items: Castable.cast_list(Item, generic_map["items"]) 78 | } 79 | end 80 | end 81 | ``` 82 | 83 | Elixir API module: 84 | 85 | ```elixir 86 | defmodule Plaid.Payments do 87 | alias Plaid.Payment 88 | 89 | defmodule GetResponse do 90 | @behaviour Plaid.Castable 91 | 92 | alias Plaid.Castable 93 | 94 | defstruct [:payment, :request_id] 95 | 96 | @impl true 97 | def cast(generic_map) do 98 | %__MODULE__{ 99 | payment: Castable.cast(Payment, generic_map["payment"]), 100 | request_id: generic_map["request_id"] 101 | } 102 | end 103 | end 104 | 105 | def get(id, config) do 106 | Plaid.Client.call( 107 | "/payments/get" 108 | %{id: id}, 109 | GetResponse, 110 | config 111 | ) 112 | end 113 | end 114 | ``` 115 | 116 | Tying it all together in IEx: 117 | 118 | ```elixir 119 | iex> Plaid.Payments.get("5930", client_id: "123", secret: "abc") 120 | {:ok, %Plaid.Payments.GetResponse{ 121 | payment: %Plaid.Payment{ 122 | customer_name: "Stefani Germanotta" 123 | items: [ 124 | %Plaid.Payment.Item{ 125 | amount: 1200, 126 | name: "Toy Solider" 127 | }, 128 | { 129 | amount: 3100, 130 | name: "Video Game" 131 | } 132 | ] 133 | } 134 | }} 135 | ``` 136 | """ 137 | @callback cast(generic_map() | nil) :: struct() | nil 138 | 139 | @doc """ 140 | Take a generic string-key map and cast it into a well defined struct. 141 | 142 | Passing `:raw` as the module will just give back the string-key map; Useful for fallbacks. 143 | 144 | ## Examples 145 | 146 | cast(MyStruct, %{"key" => "value"}) 147 | %MyStruct{key: "value"} 148 | 149 | cast(:raw, %{"key" => "value"}) 150 | %{"key" => "value"} 151 | 152 | """ 153 | @spec cast(module() | :raw, generic_map() | nil) :: generic_map() | nil 154 | def cast(_implementation, nil) do 155 | nil 156 | end 157 | 158 | def cast(:raw, generic_map) do 159 | generic_map 160 | end 161 | 162 | def cast(implementation, generic_map) when is_map(generic_map) do 163 | implementation.cast(generic_map) 164 | end 165 | 166 | @doc """ 167 | Take a list of generic string-key maps and cast them into well defined structs. 168 | 169 | ## Examples 170 | 171 | cast_list(MyStruct, [%{"a" => "b"}, %{"c" => "d"}]) 172 | [%MyStruct{a: "b"}, %MyStruct{c: "d"}] 173 | 174 | """ 175 | @spec cast_list(module(), [generic_map()] | nil) :: [struct()] | nil 176 | def cast_list(_implementation, nil) do 177 | nil 178 | end 179 | 180 | def cast_list(implementation, list_of_generic_maps) when is_list(list_of_generic_maps) do 181 | Enum.map(list_of_generic_maps, &cast(implementation, &1)) 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/plaid/categories.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Categories do 2 | @moduledoc """ 3 | [Plaid Categories API](https://plaid.com/docs/api/products/#categoriesget) calls and schema. 4 | """ 5 | 6 | defmodule GetResponse do 7 | @moduledoc """ 8 | [Plaid API /categories/get response schema.](https://plaid.com/docs/api/products/#categoriesget) 9 | """ 10 | 11 | @behaviour Plaid.Castable 12 | 13 | alias Plaid.Castable 14 | alias Plaid.Categories.Category 15 | 16 | @type t :: %__MODULE__{ 17 | categories: [Category.t()], 18 | request_id: String.t() 19 | } 20 | 21 | defstruct [ 22 | :categories, 23 | :request_id 24 | ] 25 | 26 | @impl true 27 | def cast(generic_map) do 28 | %__MODULE__{ 29 | categories: Castable.cast_list(Category, generic_map["categories"]), 30 | request_id: generic_map["request_id"] 31 | } 32 | end 33 | end 34 | 35 | @doc """ 36 | Get information about all Plaid categories. 37 | 38 | Does a `POST /categories/get` call to retrieve a list of all categories. 39 | 40 | > No authentication required. 41 | 42 | ## Example 43 | 44 | Categories.get(env: :production) 45 | {:ok, %Categories.GetResponse{}} 46 | 47 | """ 48 | @spec get(Plaid.noauth_config()) :: {:ok, GetResponse.t()} | {:error, Plaid.Error.t()} 49 | def get(config \\ []) do 50 | Plaid.Client.call( 51 | "/categories/get", 52 | %{}, 53 | GetResponse, 54 | config 55 | ) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/plaid/categories/category.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Categories.Category do 2 | @moduledoc """ 3 | [Plaid Category schema.](https://plaid.com/docs/api/products/#categoriesget) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | category_id: String.t(), 10 | group: String.t(), 11 | hierarchy: [String.t()] 12 | } 13 | 14 | defstruct [ 15 | :category_id, 16 | :group, 17 | :hierarchy 18 | ] 19 | 20 | @impl true 21 | def cast(generic_map) do 22 | %__MODULE__{ 23 | category_id: generic_map["category_id"], 24 | group: generic_map["group"], 25 | hierarchy: generic_map["hierarchy"] 26 | } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/plaid/client.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Client do 2 | @moduledoc """ 3 | Make API calls to plaid and convert the responses from JSON -> well typed elixir structs. 4 | 5 | To use a different HTTP client, create a new module like `MyApp.PlaidClient` which implements 6 | `post/3` and implements the `@behaviour Plaid.Client` behaviour. The success response of those functions must return a `:body` key with a JSON string value 7 | and a `:status_code` key with an integer HTTP status. For an example, see the `Plaid.Client.HTTPoison` module. 8 | 9 | > For network errors where you don't get a body or status code, you may return an error tuple 10 | > with any error value, but the error value is not currently utilized. 11 | """ 12 | 13 | require Logger 14 | 15 | alias Plaid.Castable 16 | 17 | @doc """ 18 | Callback to POST the data to the Plaid API. 19 | 20 | Will be called with the full URL, payload, and headers. Simply take these values 21 | execute the HTTP request. 22 | 23 | > `headers` passed in will be a list of two item tuples where the first item is the header key 24 | > and the second is the value. e.g. `[{"content-type", "application/json"}]` 25 | 26 | ## Examples 27 | 28 | iex> post("https://production.plaid.com/categories/get", ~s<{"thing": "stuff"}>, [{"content-type", "application/json"}]) 29 | {:ok, %{body: ~s<{"foo": "bar"}>, status_code: 200}} 30 | 31 | """ 32 | @callback post(url :: String.t(), payload :: String.t(), headers :: [{String.t(), String.t()}]) :: 33 | {:ok, %{body: String.t(), status_code: integer()}} | {:error, any()} 34 | 35 | @doc """ 36 | Make a Plaid API call. 37 | 38 | Takes in everything needed to complete the request and 39 | return a well formed struct of the response. 40 | 41 | ## Examples 42 | 43 | call( 44 | "/categories/get", 45 | %{}, 46 | Plaid.Categories.GetResponse, 47 | client_id: "123", 48 | secret: "abc" 49 | ) 50 | {:ok, %Plaid.Categories.GetResponse{}} 51 | 52 | """ 53 | @spec call(String.t(), map(), module(), Plaid.config()) :: 54 | {:ok, any()} | {:error, Plaid.Error.t()} 55 | def call(endpoint, payload \\ %{}, castable_module, config) do 56 | url = build_url(config, endpoint) 57 | 58 | payload = 59 | payload 60 | |> add_auth(config) 61 | |> Jason.encode!() 62 | 63 | headers = [{"content-type", "application/json"}] 64 | 65 | http_client = Keyword.get(config, :http_client, Plaid.Client.HTTPoison) 66 | 67 | case http_client.post(url, payload, headers) do 68 | {:ok, %{body: body, status_code: status_code}} when status_code in 200..299 -> 69 | {:ok, cast_body(body, castable_module)} 70 | 71 | {:ok, %{body: body}} -> 72 | {:error, cast_body(body, Plaid.Error)} 73 | 74 | {:error, _error} -> 75 | {:error, Castable.cast(Plaid.Error, %{})} 76 | end 77 | end 78 | 79 | @spec build_url(Plaid.config(), String.t()) :: String.t() 80 | defp build_url(config, endpoint) do 81 | test_api_host = Keyword.get(config, :test_api_host) 82 | 83 | if is_binary(test_api_host) do 84 | test_api_host <> endpoint 85 | else 86 | env = Keyword.get(config, :env, :sandbox) 87 | "https://#{env}.plaid.com#{endpoint}" 88 | end 89 | end 90 | 91 | @spec add_auth(map(), Plaid.config()) :: map() 92 | defp add_auth(payload, config) do 93 | auth = 94 | config 95 | |> Map.new() 96 | |> Map.take([:client_id, :secret]) 97 | 98 | Map.merge(payload, auth) 99 | end 100 | 101 | @spec cast_body(String.t(), module() | :raw) :: String.t() | %{optional(any) => any} 102 | defp cast_body(body, :raw) do 103 | body 104 | end 105 | 106 | defp cast_body(json_body, castable_module) do 107 | case Jason.decode(json_body) do 108 | {:ok, generic_map} -> Castable.cast(castable_module, generic_map) 109 | _ -> Castable.cast(Plaid.Error, %{}) 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/plaid/client/httpoison.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Client.HTTPoison do 2 | @moduledoc """ 3 | Implements the Plaid.Client behaviour, which calls for a `post/3` function. 4 | """ 5 | 6 | @behaviour Plaid.Client 7 | 8 | require Logger 9 | 10 | @impl true 11 | def post(url, payload, headers) do 12 | HTTPoison.post(url, payload, headers) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/plaid/employer.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Employer do 2 | @moduledoc """ 3 | [Plaid Employer API](https://plaid.com/docs/api/employers/) calls and schema. 4 | 5 | 🏗 I haven't tested this yet against the actual plaid API because I can't get the 6 | `deposit_switch` product in plaid yet. If you test it, let me know and I can take 7 | off the in-progress status! 8 | """ 9 | 10 | @behaviour Plaid.Castable 11 | 12 | alias Plaid.Address 13 | alias Plaid.Castable 14 | alias __MODULE__ 15 | 16 | @type t :: %__MODULE__{ 17 | address: Address.t() | nil, 18 | confidence_score: number() | nil, 19 | employer_id: String.t(), 20 | name: String.t() 21 | } 22 | 23 | defstruct [:address, :confidence_score, :employer_id, :name] 24 | 25 | @impl true 26 | def cast(generic_map) do 27 | %__MODULE__{ 28 | address: Castable.cast(Address, generic_map["address"]), 29 | confidence_score: generic_map["confidence_score"], 30 | employer_id: generic_map["employer_id"], 31 | name: generic_map["name"] 32 | } 33 | end 34 | 35 | defmodule SearchResponse do 36 | @moduledoc """ 37 | [Plaid API /employers/search response schema.](https://plaid.com/docs/api/employers/#employerssearch) 38 | """ 39 | 40 | @behaviour Castable 41 | 42 | @type t :: %__MODULE__{ 43 | employers: [Employer.t()], 44 | request_id: String.t() 45 | } 46 | 47 | defstruct [:employers, :request_id] 48 | 49 | @impl true 50 | def cast(generic_map) do 51 | %__MODULE__{ 52 | employers: Castable.cast_list(Employer, generic_map["employers"]), 53 | request_id: generic_map["request_id"] 54 | } 55 | end 56 | end 57 | 58 | @doc """ 59 | Search employers information. 60 | 61 | Does a `POST /employers/search` call to search Plaid’s database of known employers, 62 | for use with Deposit Switch 63 | 64 | Params: 65 | * `query` - The employer name to be searched for. 66 | * `products` - The Plaid products the returned employers should support. 67 | 68 | > Currently in the Plaid API, `products` must be set to `["deposit_switch"]`. 69 | 70 | ## Examples 71 | 72 | Employer.search("Plaid", ["deposit_switch"], client_id: "123", secret: "abc") 73 | {:ok, %Employer.SearchResponse{}} 74 | 75 | """ 76 | @spec search(String.t(), [String.t()], Plaid.config()) :: 77 | {:ok, SearchResponse.t()} | {:error, Plaid.Error.t()} 78 | def search(query, products, config) do 79 | Plaid.Client.call( 80 | "/employers/search", 81 | %{query: query, products: products}, 82 | SearchResponse, 83 | config 84 | ) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/plaid/error.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Error do 2 | @moduledoc """ 3 | [Plaid API Error response Schema.](https://plaid.com/docs/errors/) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | error_type: String.t(), 10 | error_code: String.t(), 11 | error_message: String.t(), 12 | display_message: String.t() | nil, 13 | request_id: String.t(), 14 | causes: [], 15 | status: integer() | nil, 16 | documentation_url: String.t(), 17 | suggested_action: String.t() | nil 18 | } 19 | 20 | defstruct [ 21 | :error_type, 22 | :error_code, 23 | :error_message, 24 | :display_message, 25 | :request_id, 26 | :causes, 27 | :status, 28 | :documentation_url, 29 | :suggested_action 30 | ] 31 | 32 | @impl true 33 | def cast(generic_map) do 34 | %__MODULE__{ 35 | error_type: generic_map["error_type"], 36 | error_code: generic_map["error_code"], 37 | error_message: generic_map["error_message"], 38 | display_message: generic_map["display_message"], 39 | request_id: generic_map["request_id"], 40 | causes: generic_map["causes"], 41 | status: generic_map["status"], 42 | documentation_url: generic_map["documentation_url"], 43 | suggested_action: generic_map["suggested_action"] 44 | } 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/plaid/identity.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Identity do 2 | @moduledoc """ 3 | [Plaid Identity API](https://plaid.com/docs/api/products/#identity) calls and schema. 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Castable 9 | alias Plaid.Identity.{Address, Email, Match, PhoneNumber} 10 | 11 | @type t :: %__MODULE__{ 12 | addresses: [Address.t()], 13 | emails: [Email.t()], 14 | names: [String.t()], 15 | phone_numbers: [PhoneNumber.t()] 16 | } 17 | 18 | defstruct [:addresses, :emails, :names, :phone_numbers] 19 | 20 | @impl true 21 | def cast(generic_map) do 22 | %__MODULE__{ 23 | addresses: Castable.cast_list(Address, generic_map["addresses"]), 24 | emails: Castable.cast_list(Email, generic_map["emails"]), 25 | names: generic_map["names"], 26 | phone_numbers: Castable.cast_list(PhoneNumber, generic_map["phone_numbers"]) 27 | } 28 | end 29 | 30 | defmodule GetResponse do 31 | @moduledoc """ 32 | [Plaid API /identity/get response schema.](https://plaid.com/docs/api/products/identity/#identityget). 33 | """ 34 | 35 | @behaviour Castable 36 | 37 | alias Plaid.Account 38 | alias Plaid.Item 39 | 40 | @type t :: %__MODULE__{ 41 | accounts: [Account.t()], 42 | item: Item.t(), 43 | request_id: String.t() 44 | } 45 | 46 | defstruct [:accounts, :item, :request_id] 47 | 48 | @impl true 49 | def cast(generic_map) do 50 | %__MODULE__{ 51 | accounts: Castable.cast_list(Account, generic_map["accounts"]), 52 | item: Castable.cast(Item, generic_map["item"]), 53 | request_id: generic_map["request_id"] 54 | } 55 | end 56 | end 57 | 58 | @doc """ 59 | Get information about all available accounts. 60 | 61 | Does a `POST /identity/get` call to retrieve account information, 62 | along with the `owners` info for each account associated with an access_token's item. 63 | 64 | Params: 65 | * `access_token` - Token to fetch identity for. 66 | 67 | Options: 68 | * `:account_ids` - Specific account ids to fetch identity for. 69 | 70 | ## Examples 71 | 72 | Identity.get("access-sandbox-123xxx", client_id: "123", secret: "abc") 73 | {:ok, %Identity.GetResponse{}} 74 | 75 | """ 76 | @spec get(String.t(), options, Plaid.config()) :: 77 | {:ok, GetResponse.t()} | {:error, Plaid.Error.t()} 78 | when options: %{optional(:account_ids) => [String.t()]} 79 | def get(access_token, options \\ %{}, config) do 80 | options_payload = Map.take(options, [:account_ids]) 81 | 82 | payload = 83 | %{} 84 | |> Map.put(:access_token, access_token) 85 | |> Map.put(:options, options_payload) 86 | 87 | Plaid.Client.call("/identity/get", payload, GetResponse, config) 88 | end 89 | 90 | defmodule MatchResponse do 91 | @moduledoc """ 92 | [Plaid API /identity/match response schema.](https://plaid.com/docs/api/products/identity/#identitymatch). 93 | """ 94 | 95 | @behaviour Plaid.Castable 96 | 97 | defstruct [:accounts, :item, :request_id] 98 | 99 | @impl true 100 | def cast(generic_map) do 101 | %__MODULE__{ 102 | accounts: Plaid.Castable.cast_list(Match.Account, generic_map["accounts"]), 103 | item: Plaid.Castable.cast(Match.Item, generic_map["item"]), 104 | request_id: generic_map["request_id"] 105 | } 106 | end 107 | end 108 | 109 | @doc """ 110 | Perform an identity check match. 111 | 112 | Does a `POST /identity/match` call to retrieve match scores and account metadata 113 | for each connected account. 114 | 115 | ## Params: 116 | * `access_token` - Plaid access token. 117 | 118 | ## Options: 119 | * `user` - User details for identity check match. 120 | 121 | ## Examples 122 | 123 | Identity.match("access-sandbox-123xxx", %{ 124 | user: %Match.User{ 125 | legal_name: "full legal name", 126 | phone_number: "123-456-7890", 127 | email_address: "email@address.com", 128 | address: %Match.User.Address{ 129 | street: "123 Main St", 130 | city: "New York", 131 | region: "NY", 132 | postal_code: "10001", 133 | country: "US" 134 | } 135 | } 136 | }, client_id: "123", secret: "abc") 137 | {:ok, %Identity.MatchResponse{}} 138 | 139 | """ 140 | def match(access_token, options, config) do 141 | payload = 142 | options 143 | |> Map.put(:access_token, access_token) 144 | 145 | Plaid.Client.call("/identity/match", payload, MatchResponse, config) 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/plaid/identity/address.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Identity.Address do 2 | @moduledoc """ 3 | [Plaid Identity Address schema.](https://plaid.com/docs/api/products/#identity-get-response-addresses) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Address, as: AddressData 9 | alias Plaid.Castable 10 | 11 | @type t :: %__MODULE__{ 12 | data: AddressData.t(), 13 | primary: boolean() | nil 14 | } 15 | 16 | defstruct [ 17 | :data, 18 | :primary 19 | ] 20 | 21 | @impl true 22 | def cast(generic_map) do 23 | %__MODULE__{ 24 | data: Castable.cast(AddressData, generic_map["data"]), 25 | primary: generic_map["primary"] 26 | } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/plaid/identity/email.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Identity.Email do 2 | @moduledoc """ 3 | [Plaid Identity Email schema.](https://plaid.com/docs/api/products/#identity-get-response-emails) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | data: String.t(), 10 | primary: boolean(), 11 | type: String.t() 12 | } 13 | 14 | defstruct [ 15 | :data, 16 | :primary, 17 | :type 18 | ] 19 | 20 | @impl true 21 | def cast(generic_map) do 22 | %__MODULE__{ 23 | data: generic_map["data"], 24 | primary: generic_map["primary"], 25 | type: generic_map["type"] 26 | } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/plaid/identity/match/account.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Identity.Match.Account do 2 | @moduledoc """ 3 | [Plaid Identity Match Account schema.](https://plaid.com/docs/api/products/identity/#identity-match-response-accounts). 4 | """ 5 | @behaviour Plaid.Castable 6 | 7 | alias Plaid.Identity.Match.Account.{ 8 | Address, 9 | EmailAddress, 10 | LegalName, 11 | PhoneNumber 12 | } 13 | 14 | alias Plaid.Castable 15 | 16 | @type t :: %__MODULE__{ 17 | account_id: String.t(), 18 | balances: Plaid.Account.Balances.t() | nil, 19 | mask: String.t() | nil, 20 | name: String.t(), 21 | official_name: String.t() | nil, 22 | type: String.t(), 23 | subtype: String.t() | nil, 24 | verification_status: String.t(), 25 | persistent_account_id: String.t(), 26 | legal_name: LegalName.t() | nil, 27 | phone_number: PhoneNumber.t() | nil, 28 | email_address: EmailAddress.t() | nil, 29 | address: Address.t() | nil 30 | } 31 | 32 | defstruct [ 33 | :account_id, 34 | :balances, 35 | :mask, 36 | :name, 37 | :verification_status, 38 | :persistent_account_id, 39 | :official_name, 40 | :type, 41 | :subtype, 42 | :legal_name, 43 | :phone_number, 44 | :email_address, 45 | :address 46 | ] 47 | 48 | @impl true 49 | def cast(generic_map) do 50 | %__MODULE__{ 51 | account_id: generic_map["account_id"], 52 | balances: Castable.cast(Plaid.Account.Balances, generic_map["balances"]), 53 | mask: generic_map["mask"], 54 | name: generic_map["name"], 55 | official_name: generic_map["official_name"], 56 | type: generic_map["type"], 57 | subtype: generic_map["subtype"], 58 | verification_status: generic_map["verification_status"], 59 | persistent_account_id: generic_map["persistent_account_id"], 60 | legal_name: Castable.cast(LegalName, generic_map["legal_name"]), 61 | phone_number: Castable.cast(PhoneNumber, generic_map["phone_number"]), 62 | email_address: Castable.cast(EmailAddress, generic_map["email_address"]), 63 | address: Castable.cast(Address, generic_map["address"]) 64 | } 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/plaid/identity/match/account/address.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Identity.Match.Account.Address do 2 | @moduledoc """ 3 | Struct for Plaid's [Identity Match Address](https://plaid.com/docs/api/products/identity/#identity-match-response-accounts-address) object. 4 | """ 5 | @behaviour Plaid.Castable 6 | 7 | @type t :: %__MODULE__{ 8 | score: integer() | nil, 9 | is_postal_code_match: boolean() | nil 10 | } 11 | 12 | defstruct [:score, :is_postal_code_match] 13 | 14 | @impl true 15 | def cast(map) do 16 | %__MODULE__{ 17 | score: map["score"], 18 | is_postal_code_match: map["is_postal_code_match"] 19 | } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/plaid/identity/match/account/email_address.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Identity.Match.Account.EmailAddress do 2 | @moduledoc """ 3 | Struct for Plaid's [Identity Match EmailAddress](https://plaid.com/docs/api/products/identity/#identity-match-response-accounts-email-address) object. 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | score: integer() | nil 10 | } 11 | 12 | defstruct [:score] 13 | 14 | @impl true 15 | def cast(map) do 16 | %__MODULE__{score: map["score"]} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/plaid/identity/match/account/legal_name.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Identity.Match.Account.LegalName do 2 | @moduledoc """ 3 | [Plaid Identity Match Legal Name schema.](https://plaid.com/docs/api/products/identity/#identity-match-response-accounts-legal-name). 4 | """ 5 | @behaviour Plaid.Castable 6 | 7 | @type t :: %__MODULE__{ 8 | score: integer() | nil, 9 | is_nickname_match: boolean() | nil, 10 | is_first_name_or_last_name_match: boolean() | nil, 11 | is_business_name_detected: boolean() | nil 12 | } 13 | 14 | defstruct [ 15 | :score, 16 | :is_nickname_match, 17 | :is_first_name_or_last_name_match, 18 | :is_business_name_detected 19 | ] 20 | 21 | @impl true 22 | def cast(map) do 23 | %__MODULE__{ 24 | score: map["score"], 25 | is_nickname_match: map["is_nickname_match"], 26 | is_first_name_or_last_name_match: map["is_first_name_or_last_name_match"], 27 | is_business_name_detected: map["is_business_name_detected"] 28 | } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/plaid/identity/match/account/phone_number.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Identity.Match.Account.PhoneNumber do 2 | @moduledoc """ 3 | Struct for Plaid's [Identity Match Phone Number](https://plaid.com/docs/api/products/identity/#identity-match-response-accounts-phone-number) object. 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | score: integer() | nil 10 | } 11 | 12 | defstruct [:score] 13 | 14 | @impl true 15 | def cast(map) do 16 | %__MODULE__{score: map["score"]} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/plaid/identity/match/item.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Identity.Match.Item do 2 | @moduledoc """ 3 | [Plaid Identity Match Item schema.](https://plaid.com/docs/api/products/identity/#identity-match-response-item). 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | item_id: String.t(), 10 | institution_id: String.t() | nil, 11 | webhook: String.t() | nil, 12 | error: Plaid.Error.t() | nil, 13 | available_products: [String.t()] | nil, 14 | billed_products: [String.t()] | nil, 15 | products: [String.t()] | nil, 16 | consented_products: [String.t()] | nil, 17 | consent_expiration_time: String.t() | nil, 18 | update_type: String.t() | nil 19 | } 20 | 21 | defstruct [ 22 | :item_id, 23 | :institution_id, 24 | :webhook, 25 | :error, 26 | :available_products, 27 | :billed_products, 28 | :products, 29 | :consented_products, 30 | :consent_expiration_time, 31 | :update_type 32 | ] 33 | 34 | @impl true 35 | def cast(map) do 36 | %__MODULE__{ 37 | item_id: map["item_id"], 38 | institution_id: map["institution_id"], 39 | webhook: map["webhook"], 40 | error: Plaid.Castable.cast(Plaid.Error, map["error"]), 41 | available_products: map["available_products"], 42 | billed_products: map["billed_products"], 43 | products: map["products"], 44 | consented_products: map["consented_products"], 45 | consent_expiration_time: map["consent_expiration_time"], 46 | update_type: map["update_type"] 47 | } 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/plaid/identity/match/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Identity.Match.User do 2 | @moduledoc """ 3 | Struct for Plaid's [Identity Match User](https://plaid.com/docs/api/products/identity/#identity-match-request-user) object. 4 | """ 5 | alias __MODULE__ 6 | 7 | @type t :: %__MODULE__{ 8 | legal_name: String.t(), 9 | phone_number: String.t(), 10 | email_address: String.t(), 11 | address: User.Address.t() 12 | } 13 | 14 | @enforce_keys [:address] 15 | 16 | @derive Jason.Encoder 17 | defstruct [ 18 | :legal_name, 19 | :phone_number, 20 | :email_address, 21 | :address 22 | ] 23 | end 24 | -------------------------------------------------------------------------------- /lib/plaid/identity/match/user/address.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Identity.Match.User.Address do 2 | @moduledoc """ 3 | Struct for Plaid's [Identity Match User Address](https://plaid.com/docs/api/products/identity/#identity-match-request-user-address) object. 4 | """ 5 | @behaviour Plaid.Castable 6 | 7 | @enforce_keys [:city, :region, :street, :postal_code, :country] 8 | 9 | @type t :: %__MODULE__{ 10 | city: String.t(), 11 | region: String.t(), 12 | street: String.t(), 13 | postal_code: String.t(), 14 | country: String.t() 15 | } 16 | 17 | @derive Jason.Encoder 18 | defstruct [ 19 | :city, 20 | :region, 21 | :street, 22 | :postal_code, 23 | :country 24 | ] 25 | 26 | @impl true 27 | def cast(map) do 28 | %__MODULE__{ 29 | city: map["city"], 30 | region: map["region"], 31 | street: map["street"], 32 | postal_code: map["postal_code"], 33 | country: map["country"] 34 | } 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/plaid/identity/phone_number.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Identity.PhoneNumber do 2 | @moduledoc """ 3 | [Plaid Identity Phone Number schema.](https://plaid.com/docs/api/products/#identity-get-response-phone-numbers) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | data: String.t(), 10 | primary: boolean() | nil, 11 | type: String.t() | nil 12 | } 13 | 14 | defstruct [ 15 | :data, 16 | :primary, 17 | :type 18 | ] 19 | 20 | @impl true 21 | def cast(generic_map) do 22 | %__MODULE__{ 23 | data: generic_map["data"], 24 | primary: generic_map["primary"], 25 | type: generic_map["type"] 26 | } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/plaid/institution.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Institution do 2 | @moduledoc """ 3 | [Plaid Institution schema](https://plaid.com/docs/api/institutions/#institutions-get-response-institutions) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Castable 9 | alias Plaid.Institution.Status 10 | 11 | @type t :: %__MODULE__{ 12 | country_codes: [String.t()], 13 | institution_id: String.t(), 14 | logo: String.t() | nil, 15 | name: String.t(), 16 | oauth: boolean(), 17 | primary_color: String.t() | nil, 18 | products: [String.t()], 19 | routing_numbers: [String.t()] | nil, 20 | status: Status.t(), 21 | url: String.t() | nil 22 | } 23 | 24 | defstruct [ 25 | :country_codes, 26 | :institution_id, 27 | :logo, 28 | :name, 29 | :oauth, 30 | :primary_color, 31 | :products, 32 | :routing_numbers, 33 | :status, 34 | :url 35 | ] 36 | 37 | @impl true 38 | def cast(generic_map) do 39 | %__MODULE__{ 40 | country_codes: generic_map["country_codes"], 41 | institution_id: generic_map["institution_id"], 42 | logo: generic_map["logo"], 43 | name: generic_map["name"], 44 | oauth: generic_map["oauth"], 45 | primary_color: generic_map["primary_color"], 46 | products: generic_map["products"], 47 | routing_numbers: generic_map["routing_numbers"], 48 | status: Castable.cast(Status, generic_map["status"]), 49 | url: generic_map["url"] 50 | } 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/plaid/institution/status.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Institution.Status do 2 | @moduledoc """ 3 | [Plaid institution status schema.](https://plaid.com/docs/api/institutions/#institutions-get-response-status) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Castable 9 | 10 | defmodule Breakdown do 11 | @moduledoc """ 12 | [Plaid institution status breakdown schema.](https://plaid.com/docs/api/institutions/#institutions-get-response-breakdown) 13 | """ 14 | 15 | @behaviour Castable 16 | 17 | @type t :: %__MODULE__{ 18 | success: number(), 19 | error_plaid: number(), 20 | error_institution: number(), 21 | refresh_interval: String.t() | nil 22 | } 23 | 24 | defstruct [ 25 | :success, 26 | :error_plaid, 27 | :error_institution, 28 | :refresh_interval 29 | ] 30 | 31 | @impl true 32 | def cast(generic_map) do 33 | %__MODULE__{ 34 | success: generic_map["success"], 35 | error_plaid: generic_map["error_plaid"], 36 | error_institution: generic_map["error_institution"], 37 | refresh_interval: generic_map["refresh_interval"] 38 | } 39 | end 40 | end 41 | 42 | defmodule Auth do 43 | @moduledoc """ 44 | [Plaid institution auth status schema.](https://plaid.com/docs/api/institutions/#institutions-get-response-auth) 45 | """ 46 | 47 | @behaviour Castable 48 | 49 | @type t :: %__MODULE__{ 50 | status: String.t(), 51 | last_status_change: String.t(), 52 | breakdown: Breakdown.t() 53 | } 54 | 55 | defstruct [ 56 | :status, 57 | :last_status_change, 58 | :breakdown 59 | ] 60 | 61 | @impl true 62 | def cast(generic_map) do 63 | %__MODULE__{ 64 | status: generic_map["status"], 65 | last_status_change: generic_map["last_status_change"], 66 | breakdown: Castable.cast(Breakdown, generic_map["breakdown"]) 67 | } 68 | end 69 | end 70 | 71 | defmodule Balance do 72 | @moduledoc """ 73 | [Plaid institution balance status schema.](https://plaid.com/docs/api/institutions/#institutions-get-response-balance) 74 | """ 75 | 76 | @behaviour Castable 77 | 78 | @type t :: %__MODULE__{ 79 | status: String.t(), 80 | last_status_change: String.t(), 81 | breakdown: Breakdown.t() 82 | } 83 | 84 | defstruct [ 85 | :status, 86 | :last_status_change, 87 | :breakdown 88 | ] 89 | 90 | @impl true 91 | def cast(generic_map) do 92 | %__MODULE__{ 93 | status: generic_map["status"], 94 | last_status_change: generic_map["last_status_change"], 95 | breakdown: Castable.cast(Breakdown, generic_map["breakdown"]) 96 | } 97 | end 98 | end 99 | 100 | defmodule HealthIncidentUpdate do 101 | @moduledoc """ 102 | [Plaid institution status health incident update schema.](https://plaid.com/docs/api/institutions/#institutions-get-response-incident-updates) 103 | """ 104 | 105 | @behaviour Castable 106 | 107 | @type t :: %__MODULE__{ 108 | description: String.t(), 109 | status: String.t(), 110 | updated_date: String.t() 111 | } 112 | 113 | defstruct [ 114 | :description, 115 | :status, 116 | :updated_date 117 | ] 118 | 119 | @impl true 120 | def cast(generic_map) do 121 | %__MODULE__{ 122 | description: generic_map["description"], 123 | status: generic_map["status"], 124 | updated_date: generic_map["updated_date"] 125 | } 126 | end 127 | end 128 | 129 | defmodule HealthIncident do 130 | @moduledoc """ 131 | [Plaid institution status health incident schema.](https://plaid.com/docs/api/institutions/#institutions-get-response-health-incidents) 132 | """ 133 | 134 | @behaviour Castable 135 | 136 | @type t :: %__MODULE__{ 137 | start_date: String.t() | nil, 138 | end_date: String.t() | nil, 139 | title: String.t(), 140 | incident_updates: [HealthIncidentUpdate.t()] 141 | } 142 | 143 | defstruct [ 144 | :start_date, 145 | :end_date, 146 | :title, 147 | :incident_updates 148 | ] 149 | 150 | @impl true 151 | def cast(generic_map) do 152 | %__MODULE__{ 153 | start_date: generic_map["start_date"], 154 | end_date: generic_map["end_date"], 155 | title: generic_map["title"], 156 | incident_updates: 157 | Castable.cast_list(HealthIncidentUpdate, generic_map["incident_updates"]) 158 | } 159 | end 160 | end 161 | 162 | defmodule Identity do 163 | @moduledoc """ 164 | [Plaid institution identity status schema.](https://plaid.com/docs/api/institutions/#institutions-get-response-identity) 165 | """ 166 | 167 | @behaviour Castable 168 | 169 | @type t :: %__MODULE__{ 170 | status: String.t(), 171 | last_status_change: String.t(), 172 | breakdown: Breakdown.t() 173 | } 174 | 175 | defstruct [ 176 | :status, 177 | :last_status_change, 178 | :breakdown 179 | ] 180 | 181 | @impl true 182 | def cast(generic_map) do 183 | %__MODULE__{ 184 | status: generic_map["status"], 185 | last_status_change: generic_map["last_status_change"], 186 | breakdown: Castable.cast(Breakdown, generic_map["breakdown"]) 187 | } 188 | end 189 | end 190 | 191 | defmodule InvestmentsUpdates do 192 | @moduledoc """ 193 | [Plaid institution investments updates status schema.](https://plaid.com/docs/api/institutions/#institutions-get-response-investments-updates) 194 | """ 195 | 196 | @behaviour Castable 197 | 198 | @type t :: %__MODULE__{ 199 | status: String.t(), 200 | last_status_change: String.t(), 201 | breakdown: Breakdown.t() 202 | } 203 | 204 | defstruct [ 205 | :status, 206 | :last_status_change, 207 | :breakdown 208 | ] 209 | 210 | @impl true 211 | def cast(generic_map) do 212 | %__MODULE__{ 213 | status: generic_map["status"], 214 | last_status_change: generic_map["last_status_change"], 215 | breakdown: Castable.cast(Breakdown, generic_map["breakdown"]) 216 | } 217 | end 218 | end 219 | 220 | defmodule ItemLogins do 221 | @moduledoc """ 222 | [Plaid institution item logins status schema.](https://plaid.com/docs/api/institutions/#institutions-get-response-item-logins) 223 | """ 224 | 225 | @behaviour Castable 226 | 227 | @type t :: %__MODULE__{ 228 | status: String.t(), 229 | last_status_change: String.t(), 230 | breakdown: Breakdown.t() 231 | } 232 | 233 | defstruct [ 234 | :status, 235 | :last_status_change, 236 | :breakdown 237 | ] 238 | 239 | @impl true 240 | def cast(generic_map) do 241 | %__MODULE__{ 242 | status: generic_map["status"], 243 | last_status_change: generic_map["last_status_change"], 244 | breakdown: Castable.cast(Breakdown, generic_map["breakdown"]) 245 | } 246 | end 247 | end 248 | 249 | defmodule TransactionsUpdates do 250 | @moduledoc """ 251 | [Plaid institution transactions updates status schema.](https://plaid.com/docs/api/institutions/#institutions-get-response-transactions-updates) 252 | """ 253 | 254 | @behaviour Castable 255 | 256 | @type t :: %__MODULE__{ 257 | status: String.t(), 258 | last_status_change: String.t(), 259 | breakdown: Breakdown.t() 260 | } 261 | 262 | defstruct [ 263 | :status, 264 | :last_status_change, 265 | :breakdown 266 | ] 267 | 268 | @impl true 269 | def cast(generic_map) do 270 | %__MODULE__{ 271 | status: generic_map["status"], 272 | last_status_change: generic_map["last_status_change"], 273 | breakdown: Castable.cast(Breakdown, generic_map["breakdown"]) 274 | } 275 | end 276 | end 277 | 278 | @type t :: %__MODULE__{ 279 | auth: Auth.t(), 280 | balance: Balance.t(), 281 | health_incidents: [HealthIncident.t()] | nil, 282 | identity: Identity.t(), 283 | investments_updates: InvestmentsUpdates.t(), 284 | item_logins: ItemLogins.t(), 285 | transactions_updates: TransactionsUpdates.t() 286 | } 287 | 288 | defstruct [ 289 | :auth, 290 | :balance, 291 | :health_incidents, 292 | :identity, 293 | :investments_updates, 294 | :item_logins, 295 | :transactions_updates 296 | ] 297 | 298 | @impl true 299 | def cast(generic_map) do 300 | %__MODULE__{ 301 | auth: Castable.cast(Auth, generic_map["auth"]), 302 | balance: Castable.cast(Balance, generic_map["balance"]), 303 | health_incidents: Castable.cast_list(HealthIncident, generic_map["health_incidents"]), 304 | identity: Castable.cast(Identity, generic_map["identity"]), 305 | investments_updates: Castable.cast(InvestmentsUpdates, generic_map["investments_updates"]), 306 | item_logins: Castable.cast(ItemLogins, generic_map["item_logins"]), 307 | transactions_updates: 308 | Castable.cast(TransactionsUpdates, generic_map["transactions_updates"]) 309 | } 310 | end 311 | end 312 | -------------------------------------------------------------------------------- /lib/plaid/institutions.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Institutions do 2 | @moduledoc """ 3 | [Plaid Institutions API](https://plaid.com/docs/api/institutions/) calls and schema. 4 | """ 5 | 6 | defmodule GetResponse do 7 | @moduledoc """ 8 | [Plaid API /institutions/get response schema.](https://plaid.com/docs/api/institutions/#institutionsget) 9 | """ 10 | 11 | @behaviour Plaid.Castable 12 | 13 | alias Plaid.Castable 14 | alias Plaid.Institution 15 | 16 | @type t :: %__MODULE__{ 17 | institutions: [Institution.t()], 18 | total: integer(), 19 | request_id: String.t() 20 | } 21 | 22 | defstruct [ 23 | :institutions, 24 | :total, 25 | :request_id 26 | ] 27 | 28 | @impl true 29 | def cast(generic_map) do 30 | %__MODULE__{ 31 | institutions: Castable.cast_list(Institution, generic_map["institutions"]), 32 | total: generic_map["total"], 33 | request_id: generic_map["request_id"] 34 | } 35 | end 36 | end 37 | 38 | @doc """ 39 | Get information about Plaid institutions. 40 | 41 | Does a `POST /institutions/get` call to list the supported Plaid 42 | institutions with their details. 43 | 44 | ## Params 45 | * `:count` - The total number of Institutions to return. 46 | * `:offset` - The number of Institutions to skip. 47 | * `:country_codes` - Array of country codes the institution supports. 48 | 49 | ## Options 50 | * `:products` - Filter based on which products they support. 51 | * `:routing_numbers` - Filter based on routing numbers. 52 | * `:oauth` - Filter institutions with or without OAuth login flows. 53 | * `:include_optional_metadata` - When true, return the institution's homepage URL, logo and primary brand color. 54 | 55 | ## Examples 56 | 57 | Institutions.get(%{count: 25, offset: 0, country_codes: ["CA", "GB]}, client_id: "123", secret: "abc") 58 | {:ok, %Institutions.GetResponse{}} 59 | 60 | """ 61 | @spec get(params, options, Plaid.config()) :: {:ok, GetResponse.t()} | {:error, Plaid.Error.t()} 62 | when params: %{ 63 | required(:count) => integer(), 64 | required(:offset) => integer(), 65 | required(:country_codes) => [String.t()] 66 | }, 67 | options: %{ 68 | optional(:products) => [String.t()], 69 | optional(:routing_numbers) => [String.t()], 70 | optional(:oauth) => boolean(), 71 | optional(:include_optional_metadata) => boolean() 72 | } 73 | def get(params, options \\ %{}, config) do 74 | options_payload = 75 | Map.take(options, [:products, :routing_numbers, :oauth, :include_optional_metadata]) 76 | 77 | payload = 78 | params 79 | |> Map.take([:count, :offset, :country_codes]) 80 | |> Map.merge(%{options: options_payload}) 81 | 82 | Plaid.Client.call( 83 | "/institutions/get", 84 | payload, 85 | GetResponse, 86 | config 87 | ) 88 | end 89 | 90 | defmodule GetByIdResponse do 91 | @moduledoc """ 92 | [Plaid API /institutions/get_by_id response schema.](https://plaid.com/docs/api/institutions/#institutionsget_by_id) 93 | """ 94 | 95 | @behaviour Plaid.Castable 96 | 97 | alias Plaid.Castable 98 | alias Plaid.Institution 99 | 100 | @type t :: %__MODULE__{ 101 | institution: Institution.t(), 102 | request_id: String.t() 103 | } 104 | 105 | defstruct [ 106 | :institution, 107 | :request_id 108 | ] 109 | 110 | @impl true 111 | def cast(generic_map) do 112 | %__MODULE__{ 113 | institution: Castable.cast(Institution, generic_map["institution"]), 114 | request_id: generic_map["request_id"] 115 | } 116 | end 117 | end 118 | 119 | @doc """ 120 | Get information about a Plaid institution. 121 | 122 | Does a `POST /institutions/get_by_id` call to retrieve a Plaid 123 | institution by it's ID. 124 | 125 | ## Params 126 | * `institution_id` - The ID of the institution to get details about. 127 | * `country_codes` - Array of country codes the institution supports. 128 | 129 | ## Options 130 | * `:include_optional_metadata` - When true, return the institution's homepage URL, logo and primary brand color. 131 | * `:include_status` - When true, the response will include status information about the institution. 132 | 133 | ## Examples 134 | 135 | Institutions.get_by_id("ins_1", ["CA", "GB], client_id: "123", secret: "abc") 136 | {:ok, %Institutions.GetByIdResponse{}} 137 | 138 | """ 139 | @spec get_by_id(String.t(), [String.t()], options, Plaid.config()) :: 140 | {:ok, GetByIdResponse.t()} | {:error, Plaid.Error.t()} 141 | when options: %{ 142 | optional(:products) => [String.t()], 143 | optional(:routing_numbers) => [String.t()], 144 | optional(:oauth) => boolean(), 145 | optional(:include_optional_metadata) => boolean() 146 | } 147 | def get_by_id(institution_id, country_codes, options \\ %{}, config) do 148 | options_payload = Map.take(options, [:include_optional_metadata, :include_status]) 149 | 150 | payload = 151 | %{} 152 | |> Map.put(:institution_id, institution_id) 153 | |> Map.put(:country_codes, country_codes) 154 | |> Map.merge(%{options: options_payload}) 155 | 156 | Plaid.Client.call( 157 | "/institutions/get_by_id", 158 | payload, 159 | GetByIdResponse, 160 | config 161 | ) 162 | end 163 | 164 | defmodule SearchResponse do 165 | @moduledoc """ 166 | [Plaid API /institutions/search response schema.](https://plaid.com/docs/api/institutions/#institutionssearch) 167 | """ 168 | 169 | @behaviour Plaid.Castable 170 | 171 | alias Plaid.Castable 172 | alias Plaid.Institution 173 | 174 | @type t :: %__MODULE__{ 175 | institutions: [Institution.t()], 176 | request_id: String.t() 177 | } 178 | 179 | defstruct [ 180 | :institutions, 181 | :request_id 182 | ] 183 | 184 | @impl true 185 | def cast(generic_map) do 186 | %__MODULE__{ 187 | institutions: Castable.cast_list(Institution, generic_map["institutions"]), 188 | request_id: generic_map["request_id"] 189 | } 190 | end 191 | end 192 | 193 | @doc """ 194 | Get information about all Plaid institutions matching the search params. 195 | 196 | Does a `POST /institutions/search` call to list the supported Plaid 197 | institutions with their details based on your search query. 198 | 199 | ## Params 200 | * `:query` - The search query. Institutions with names matching the query are returned 201 | * `:products` - Filter the Institutions based on whether they support listed products. 202 | * `:country_codes` - Array of country codes the institution supports. 203 | 204 | ## Options 205 | * `:include_optional_metadata` - When true, return the institution's homepage URL, logo and primary brand color. 206 | * `:oauth` - Filter institutions with or without OAuth login flows. 207 | * `:account_filter` - Object allowing account type -> sub-type filtering. 208 | 209 | > See [Account Type Schema](https://plaid.com/docs/api/accounts/#account-type-schema) for more details on the `account_filter` option. 210 | 211 | ## Examples 212 | 213 | Institutions.search(%{query: "Ally", products: ["auth"], country_codes: ["US"]}, client_id: "123", secret: "abc") 214 | {:ok, %Institutions.SearchResponse{}} 215 | 216 | """ 217 | @spec search(params, options, Plaid.config()) :: 218 | {:ok, SearchResponse.t()} | {:error, Plaid.Error.t()} 219 | when params: %{ 220 | required(:query) => String.t(), 221 | required(:products) => [String.t()], 222 | required(:country_codes) => [String.t()] 223 | }, 224 | options: %{ 225 | optional(:include_optional_metadata) => boolean(), 226 | optional(:oauth) => boolean(), 227 | optional(:account_filter) => map() 228 | } 229 | def search(params, options \\ %{}, config) do 230 | options_payload = Map.take(options, [:oauth, :include_optional_metadata, :account_filter]) 231 | 232 | payload = 233 | params 234 | |> Map.take([:query, :products, :country_codes]) 235 | |> Map.merge(%{options: options_payload}) 236 | 237 | Plaid.Client.call( 238 | "/institutions/search", 239 | payload, 240 | SearchResponse, 241 | config 242 | ) 243 | end 244 | end 245 | -------------------------------------------------------------------------------- /lib/plaid/investments.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Investments do 2 | @moduledoc """ 3 | [Plaid Investments APIs](https://plaid.com/docs/api/products/#investments) 4 | """ 5 | 6 | alias Plaid.Castable 7 | 8 | defmodule GetHoldingsResponse do 9 | @moduledoc """ 10 | [Plaid API /investments/holdings/get response schema.](https://plaid.com/docs/api/products/#investmentsholdingsget) 11 | """ 12 | 13 | @behaviour Castable 14 | 15 | alias Plaid.Account 16 | alias Plaid.Investments.{Holding, Security} 17 | alias Plaid.Item 18 | 19 | @type t :: %__MODULE__{ 20 | accounts: [Account.t()], 21 | holdings: [Holding.t()], 22 | securities: [Security.t()], 23 | item: Item.t(), 24 | request_id: String.t() 25 | } 26 | 27 | defstruct [ 28 | :accounts, 29 | :holdings, 30 | :securities, 31 | :item, 32 | :request_id 33 | ] 34 | 35 | @impl true 36 | def cast(generic_map) do 37 | %__MODULE__{ 38 | accounts: Castable.cast_list(Account, generic_map["accounts"]), 39 | holdings: Castable.cast_list(Holding, generic_map["holdings"]), 40 | securities: Castable.cast_list(Security, generic_map["securities"]), 41 | item: Castable.cast(Item, generic_map["item"]), 42 | request_id: generic_map["request_id"] 43 | } 44 | end 45 | end 46 | 47 | @doc """ 48 | Get user-authorized stock position data for investment-type accounts. 49 | 50 | Does a `POST /investments/holdings/get` call to retrieve 51 | invesment holdings associated with an access_token's item. 52 | 53 | Params: 54 | * `access_token` - Token to fetch investment holdings for. 55 | 56 | Options: 57 | * `:account_ids` - Specific account ids to fetch investment holdings for. 58 | 59 | ## Examples 60 | 61 | Investments.get_holdings("access-sandbox-123xxx", client_id: "123", secret: "abc") 62 | {:ok, %Investments.GetHoldingsResponse{}} 63 | 64 | """ 65 | @spec get_holdings(String.t(), options, Plaid.config()) :: 66 | {:ok, GetHoldingsResponse.t()} | {:error, Plaid.Error.t()} 67 | when options: %{optional(:account_ids) => [String.t()]} 68 | def get_holdings(access_token, options \\ %{}, config) do 69 | options_payload = Map.take(options, [:account_ids]) 70 | 71 | payload = %{access_token: access_token, options: options_payload} 72 | 73 | Plaid.Client.call( 74 | "/investments/holdings/get", 75 | payload, 76 | GetHoldingsResponse, 77 | config 78 | ) 79 | end 80 | 81 | defmodule GetTransactionsResponse do 82 | @moduledoc """ 83 | [Plaid API /investments/transactions/get response schema.](https://plaid.com/docs/api/products/#investmentstransactionsget) 84 | """ 85 | 86 | @behaviour Castable 87 | 88 | alias Plaid.Account 89 | alias Plaid.Investments.{Security, Transaction} 90 | alias Plaid.Item 91 | 92 | @type t :: %__MODULE__{ 93 | item: Item.t(), 94 | accounts: [Account.t()], 95 | securities: [Security.t()], 96 | investment_transactions: [Transaction.t()], 97 | total_investment_transactions: integer(), 98 | request_id: String.t() 99 | } 100 | 101 | defstruct [ 102 | :item, 103 | :accounts, 104 | :securities, 105 | :investment_transactions, 106 | :total_investment_transactions, 107 | :request_id 108 | ] 109 | 110 | @impl true 111 | def cast(generic_map) do 112 | %__MODULE__{ 113 | item: Castable.cast(Item, generic_map["item"]), 114 | accounts: Castable.cast_list(Account, generic_map["accounts"]), 115 | securities: Castable.cast_list(Security, generic_map["securities"]), 116 | investment_transactions: 117 | Castable.cast_list(Transaction, generic_map["investment_transactions"]), 118 | total_investment_transactions: generic_map["total_investment_transactions"], 119 | request_id: generic_map["request_id"] 120 | } 121 | end 122 | end 123 | 124 | @doc """ 125 | Get information about all available investment transactions. 126 | 127 | Does a `POST /investments/transactions/get` call which gives you high level 128 | account data along with investment transactions and associated securities 129 | from all investment accounts contained in the access_token's item. 130 | 131 | Params: 132 | * `access_token` - Token to fetch investment holdings for. 133 | * `start_date` - Start of query for investment transactions. 134 | * `end_date` - End of query for investment transactions. 135 | 136 | Options: 137 | * `:account_ids` - Specific account ids to fetch investment holdings for. 138 | * `:count` - Amount of investment transactions to pull (optional). 139 | * `:offset` - Offset to start pulling investment transactions (optional). 140 | 141 | ## Examples 142 | 143 | Investments.get_transactions("access-sandbox-123xxx", "2020-01-01", "2020-01-31", client_id: "123", secret: "abc") 144 | {:ok, %Investments.GetTransactionsResponse{}} 145 | 146 | """ 147 | @spec get_transactions(String.t(), String.t(), String.t(), options, Plaid.config()) :: 148 | {:ok, GetTransactionsResponse.t()} | {:error, Plaid.Error.t()} 149 | when options: %{ 150 | optional(:account_ids) => [String.t()], 151 | optional(:count) => integer(), 152 | optional(:offset) => integer() 153 | } 154 | def get_transactions(access_token, start_date, end_date, options \\ %{}, config) do 155 | options_payload = Map.take(options, [:account_ids, :count, :offset]) 156 | 157 | payload = %{ 158 | access_token: access_token, 159 | start_date: start_date, 160 | end_date: end_date, 161 | options: options_payload 162 | } 163 | 164 | Plaid.Client.call( 165 | "/investments/transactions/get", 166 | payload, 167 | GetTransactionsResponse, 168 | config 169 | ) 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /lib/plaid/investments/holding.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Investments.Holding do 2 | @moduledoc """ 3 | [Plaid Investments Holding schema](https://plaid.com/docs/api/products/#investments-holdings-get-response-holdings) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | account_id: String.t(), 10 | security_id: String.t(), 11 | institution_price: number(), 12 | institution_price_as_of: String.t() | nil, 13 | institution_value: number(), 14 | cost_basis: number() | nil, 15 | quantity: number(), 16 | iso_currency_code: String.t() | nil, 17 | unofficial_currency_code: String.t() | nil 18 | } 19 | 20 | defstruct [ 21 | :account_id, 22 | :cost_basis, 23 | :institution_price, 24 | :institution_price_as_of, 25 | :institution_value, 26 | :iso_currency_code, 27 | :quantity, 28 | :security_id, 29 | :unofficial_currency_code 30 | ] 31 | 32 | @impl true 33 | def cast(generic_map) do 34 | %__MODULE__{ 35 | account_id: generic_map["account_id"], 36 | cost_basis: generic_map["cost_basis"], 37 | institution_price: generic_map["institution_price"], 38 | institution_price_as_of: generic_map["institution_price_as_of"], 39 | institution_value: generic_map["institution_value"], 40 | iso_currency_code: generic_map["iso_currency_code"], 41 | quantity: generic_map["quantity"], 42 | security_id: generic_map["security_id"], 43 | unofficial_currency_code: generic_map["unofficial_currency_code"] 44 | } 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/plaid/investments/security.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Investments.Security do 2 | @moduledoc """ 3 | [Plaid Investments Security schema](https://plaid.com/docs/api/products/#investments-holdings-get-response-securities) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | close_price: number() | nil, 10 | close_price_as_of: String.t() | nil, 11 | cusip: String.t() | nil, 12 | institution_id: String.t() | nil, 13 | institution_security_id: String.t() | nil, 14 | is_cash_equivalent: boolean(), 15 | isin: String.t() | nil, 16 | iso_currency_code: String.t() | nil, 17 | name: String.t() | nil, 18 | proxy_security_id: String.t() | nil, 19 | security_id: String.t(), 20 | sedol: String.t() | nil, 21 | ticker_symbol: String.t() | nil, 22 | type: String.t(), 23 | unofficial_currency_code: String.t() | nil 24 | } 25 | 26 | defstruct [ 27 | :close_price, 28 | :close_price_as_of, 29 | :cusip, 30 | :institution_id, 31 | :institution_security_id, 32 | :is_cash_equivalent, 33 | :isin, 34 | :iso_currency_code, 35 | :name, 36 | :proxy_security_id, 37 | :security_id, 38 | :sedol, 39 | :ticker_symbol, 40 | :type, 41 | :unofficial_currency_code 42 | ] 43 | 44 | @impl true 45 | def cast(generic_map) do 46 | %__MODULE__{ 47 | close_price: generic_map["close_price"], 48 | close_price_as_of: generic_map["close_price_as_of"], 49 | cusip: generic_map["cusip"], 50 | institution_id: generic_map["institution_id"], 51 | institution_security_id: generic_map["institution_security_id"], 52 | is_cash_equivalent: generic_map["is_cash_equivalent"], 53 | isin: generic_map["isin"], 54 | iso_currency_code: generic_map["iso_currency_code"], 55 | name: generic_map["name"], 56 | proxy_security_id: generic_map["proxy_security_id"], 57 | security_id: generic_map["security_id"], 58 | sedol: generic_map["sedol"], 59 | ticker_symbol: generic_map["ticker_symbol"], 60 | type: generic_map["type"], 61 | unofficial_currency_code: generic_map["unofficial_currency_code"] 62 | } 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/plaid/investments/transaction.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Investments.Transaction do 2 | @moduledoc """ 3 | [Plaid Investments Transaction schema](https://plaid.com/docs/api/products/#investments-transactions-get-response-investment-transactions) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | account_id: String.t(), 10 | amount: number(), 11 | cancel_transaction_id: String.t() | nil, 12 | date: String.t(), 13 | fees: number() | nil, 14 | investment_transaction_id: String.t(), 15 | iso_currency_code: String.t() | nil, 16 | name: String.t(), 17 | price: number(), 18 | quantity: number(), 19 | security_id: String.t() | nil, 20 | subtype: String.t(), 21 | type: String.t(), 22 | unofficial_currency_code: String.t() | nil 23 | } 24 | 25 | defstruct [ 26 | :account_id, 27 | :amount, 28 | :cancel_transaction_id, 29 | :date, 30 | :fees, 31 | :investment_transaction_id, 32 | :iso_currency_code, 33 | :name, 34 | :price, 35 | :quantity, 36 | :security_id, 37 | :subtype, 38 | :type, 39 | :unofficial_currency_code 40 | ] 41 | 42 | @impl true 43 | def cast(generic_map) do 44 | %__MODULE__{ 45 | account_id: generic_map["account_id"], 46 | amount: generic_map["amount"], 47 | cancel_transaction_id: generic_map["cancel_transaction_id"], 48 | date: generic_map["date"], 49 | fees: generic_map["fees"], 50 | investment_transaction_id: generic_map["investment_transaction_id"], 51 | iso_currency_code: generic_map["iso_currency_code"], 52 | name: generic_map["name"], 53 | price: generic_map["price"], 54 | quantity: generic_map["quantity"], 55 | security_id: generic_map["security_id"], 56 | subtype: generic_map["subtype"], 57 | type: generic_map["type"], 58 | unofficial_currency_code: generic_map["unofficial_currency_code"] 59 | } 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/plaid/item.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Item do 2 | @moduledoc """ 3 | [Plaid Item API](https://plaid.com/docs/api/items/#itemget) calls and schema. 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Castable 9 | 10 | @type t :: %__MODULE__{ 11 | available_products: [String.t()], 12 | billed_products: [String.t()], 13 | consent_expiration_time: String.t() | nil, 14 | error: Plaid.Error.t() | nil, 15 | has_perpetual_otp: boolean(), 16 | institution_id: String.t() | nil, 17 | item_id: String.t(), 18 | update_type: String.t(), 19 | webhook: String.t() | nil 20 | } 21 | 22 | defstruct [ 23 | :available_products, 24 | :billed_products, 25 | :consent_expiration_time, 26 | :has_perpetual_otp, 27 | :error, 28 | :institution_id, 29 | :item_id, 30 | :update_type, 31 | :webhook 32 | ] 33 | 34 | @impl true 35 | def cast(generic_map) do 36 | %__MODULE__{ 37 | available_products: generic_map["available_products"], 38 | billed_products: generic_map["billed_products"], 39 | consent_expiration_time: generic_map["consent_expiration_time"], 40 | error: Castable.cast(Plaid.Error, generic_map["error"]), 41 | has_perpetual_otp: generic_map["has_perpetual_otp"], 42 | institution_id: generic_map["institution_id"], 43 | item_id: generic_map["item_id"], 44 | update_type: generic_map["update_type"], 45 | webhook: generic_map["webhook"] 46 | } 47 | end 48 | 49 | defmodule GetResponse do 50 | @moduledoc """ 51 | [Plaid API /item/get response schema.](https://plaid.com/docs/api/items/#itemget) 52 | """ 53 | 54 | @behaviour Castable 55 | 56 | alias Plaid.Castable 57 | alias Plaid.Item 58 | alias Plaid.Item.Status 59 | 60 | @type t :: %__MODULE__{ 61 | item: Item.t(), 62 | status: Status.t() | nil, 63 | request_id: String.t(), 64 | access_token: String.t() | nil 65 | } 66 | 67 | defstruct [ 68 | :item, 69 | :status, 70 | :request_id, 71 | :access_token 72 | ] 73 | 74 | @impl true 75 | def cast(generic_map) do 76 | %__MODULE__{ 77 | item: Castable.cast(Item, generic_map["item"]), 78 | status: Castable.cast(Status, generic_map["status"]), 79 | request_id: generic_map["request_id"], 80 | access_token: generic_map["access_token"] 81 | } 82 | end 83 | end 84 | 85 | @doc """ 86 | Get information about an item. 87 | 88 | Does a `POST /item/get` call which returns information about an item and 89 | its status. 90 | 91 | ## Params 92 | 93 | * `access_token` - The access token associated with the item. 94 | 95 | ## Examples 96 | 97 | Item.get("access-prod-123xxx", client_id: "123", secret: "abc") 98 | {:ok, %Item.GetResponse{}} 99 | 100 | """ 101 | @spec get(String.t(), Plaid.config()) :: {:ok, GetResponse.t()} | {:error, Plaid.Error.t()} 102 | def get(access_token, config) do 103 | Plaid.Client.call("/item/get", %{access_token: access_token}, GetResponse, config) 104 | end 105 | 106 | @doc """ 107 | Removes an item. 108 | 109 | Does a `POST /item/remove` call to remove an item. 110 | 111 | ## Params 112 | 113 | * `access_token` - The access token associated with the item. 114 | 115 | ## Examples 116 | 117 | Item.remove("access-prod-123xxx", client_id: "123", secret: "abc") 118 | {:ok, %Plaid.SimpleResponse{}} 119 | 120 | """ 121 | @spec remove(String.t(), Plaid.config()) :: 122 | {:ok, Plaid.SimpleResponse.t()} | {:error, Plaid.Error.t()} 123 | def remove(access_token, config) do 124 | Plaid.Client.call("/item/remove", %{access_token: access_token}, Plaid.SimpleResponse, config) 125 | end 126 | 127 | defmodule UpdateWebhookResponse do 128 | @moduledoc """ 129 | [Plaid API /item/webhook/update response schema.](https://plaid.com/docs/api/items/#itemwebhookupdate) 130 | """ 131 | 132 | @behaviour Castable 133 | 134 | alias Plaid.Item 135 | 136 | @type t :: %__MODULE__{ 137 | item: Item.t(), 138 | request_id: String.t() 139 | } 140 | 141 | defstruct [ 142 | :item, 143 | :request_id 144 | ] 145 | 146 | @impl true 147 | def cast(generic_map) do 148 | %__MODULE__{ 149 | item: Castable.cast(Item, generic_map["item"]), 150 | request_id: generic_map["request_id"] 151 | } 152 | end 153 | end 154 | 155 | @doc """ 156 | Update a webhook for an access_token. 157 | 158 | Does a `POST /item/webhook/update` call which is used to update webhook 159 | for a particular access_token. 160 | 161 | ## Params 162 | 163 | * `access_token` - The access token associated with the item. 164 | * `webhook` - The new webhook URL. 165 | 166 | ## Examples 167 | 168 | Item.update_webhook( 169 | "access-prod-123xxx", 170 | "https://plaid.com/fake/webhook", 171 | client_id: "123", 172 | secret: "abc" 173 | ) 174 | {:ok, %Item.UpdateWebhookResponse{}} 175 | 176 | """ 177 | @spec update_webhook(String.t(), String.t(), Plaid.config()) :: 178 | {:ok, UpdateWebhookResponse.t()} | {:error, Plaid.Error.t()} 179 | def update_webhook(access_token, webhook, config) do 180 | Plaid.Client.call( 181 | "/item/webhook/update", 182 | %{access_token: access_token, webhook: webhook}, 183 | UpdateWebhookResponse, 184 | config 185 | ) 186 | end 187 | 188 | defmodule ExchangePublicTokenResponse do 189 | @moduledoc """ 190 | [Plaid API /item/public_token/exchange response schema.](https://plaid.com/docs/api/tokens/#itempublic_tokenexchange) 191 | """ 192 | 193 | @behaviour Castable 194 | 195 | @type t :: %__MODULE__{ 196 | access_token: String.t(), 197 | item_id: String.t(), 198 | request_id: String.t() 199 | } 200 | 201 | defstruct [ 202 | :access_token, 203 | :item_id, 204 | :request_id 205 | ] 206 | 207 | @impl true 208 | def cast(generic_map) do 209 | %__MODULE__{ 210 | access_token: generic_map["access_token"], 211 | item_id: generic_map["item_id"], 212 | request_id: generic_map["request_id"] 213 | } 214 | end 215 | end 216 | 217 | @doc """ 218 | Exchange a public token for an access token. 219 | 220 | Does a `POST /item/public_token/exchange` call which exchanges a public token 221 | for an access token. 222 | 223 | ## Params 224 | 225 | * `public_token` - Your public_token, obtained from the Link `onSuccess` callback 226 | or `POST /sandbox/item/public_token/create.` 227 | 228 | ## Examples 229 | 230 | Item.exchange_public_token( 231 | "public-prod-123xxx", 232 | client_id: "123", 233 | secret: "abc" 234 | ) 235 | {:ok, %Item.ExchangePublicTokenResponse{}} 236 | 237 | """ 238 | @spec exchange_public_token(String.t(), Plaid.config()) :: 239 | {:ok, ExchangePublicTokenResponse.t()} | {:error, Plaid.Error.t()} 240 | def exchange_public_token(public_token, config) do 241 | Plaid.Client.call( 242 | "/item/public_token/exchange", 243 | %{public_token: public_token}, 244 | ExchangePublicTokenResponse, 245 | config 246 | ) 247 | end 248 | 249 | defmodule InvalidateAccessTokenResponse do 250 | @moduledoc """ 251 | [Plaid API /item/access_token/invalidate response schema.](https://plaid.com/docs/api/tokens/#itemaccess_tokeninvalidate) 252 | """ 253 | 254 | @behaviour Castable 255 | 256 | @type t :: %__MODULE__{ 257 | new_access_token: String.t(), 258 | request_id: String.t() 259 | } 260 | 261 | defstruct [ 262 | :new_access_token, 263 | :request_id 264 | ] 265 | 266 | @impl true 267 | def cast(generic_map) do 268 | %__MODULE__{ 269 | new_access_token: generic_map["new_access_token"], 270 | request_id: generic_map["request_id"] 271 | } 272 | end 273 | end 274 | 275 | @doc """ 276 | Invalidate an access token. 277 | 278 | Does a `POST /item/access_token/invalidate` call which rotates an access token 279 | for an item. Immediately invalidating it and returning a new access token. 280 | 281 | ## Params 282 | 283 | * `access_token` - The access token associated with the Item data is being requested for. 284 | 285 | ## Examples 286 | 287 | Item.invalidate_access_token( 288 | "access-prod-123xxx", 289 | client_id: "123", 290 | secret: "abc" 291 | ) 292 | {:ok, %Item.InvalidateAccessTokenResponse{}} 293 | 294 | """ 295 | @spec invalidate_access_token(String.t(), Plaid.config()) :: 296 | {:ok, InvalidateAccessTokenResponse.t()} | {:error, Plaid.Error.t()} 297 | def invalidate_access_token(access_token, config) do 298 | Plaid.Client.call( 299 | "/item/access_token/invalidate", 300 | %{access_token: access_token}, 301 | InvalidateAccessTokenResponse, 302 | config 303 | ) 304 | end 305 | end 306 | -------------------------------------------------------------------------------- /lib/plaid/item/status.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Item.Status do 2 | @moduledoc """ 3 | [Plaid item status schema.](https://plaid.com/docs/api/items/#item-get-response-status) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Castable 9 | 10 | alias Plaid.Item.Status.{ 11 | Investments, 12 | LastWebhook, 13 | Transactions 14 | } 15 | 16 | @type t :: %__MODULE__{ 17 | investments: Investments.t() | nil, 18 | transactions: Transactions.t() | nil, 19 | last_webhook: LastWebhook.t() | nil 20 | } 21 | 22 | defstruct [ 23 | :investments, 24 | :transactions, 25 | :last_webhook 26 | ] 27 | 28 | @impl true 29 | def cast(generic_map) do 30 | %__MODULE__{ 31 | investments: Castable.cast(Investments, generic_map["investments"]), 32 | transactions: Castable.cast(Transactions, generic_map["transactions"]), 33 | last_webhook: Castable.cast(LastWebhook, generic_map["last_webhook"]) 34 | } 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/plaid/item/status/investments.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Item.Status.Investments do 2 | @moduledoc """ 3 | [Plaid item status investments information.](https://plaid.com/docs/api/items/#item-get-response-investments) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | last_successful_update: String.t() | nil, 10 | last_failed_update: String.t() | nil 11 | } 12 | 13 | defstruct [ 14 | :last_successful_update, 15 | :last_failed_update 16 | ] 17 | 18 | @impl true 19 | def cast(generic_map) do 20 | %__MODULE__{ 21 | last_successful_update: generic_map["last_successful_update"], 22 | last_failed_update: generic_map["last_failed_update"] 23 | } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/plaid/item/status/last_webhook.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Item.Status.LastWebhook do 2 | @moduledoc """ 3 | [Plaid item status last webhook information.](https://plaid.com/docs/api/items/#item-get-response-last-webhookhttps://plaid.com/docs/api/items/#item-get-response-last-webhookhttps://plaid.com/docs/api/items/#item-get-response-last-webhookhttps://plaid.com/docs/api/items/#item-get-response-last-webhookhttps://plaid.com/docs/api/items/#item-get-response-last-webhookhttps://plaid.com/docs/api/items/#item-get-response-last-webhookhttps://plaid.com/docs/api/items/#item-get-response-last-webhookhttps://plaid.com/docs/api/items/#item-get-response-last-webhookhttps://plaid.com/docs/api/items/#item-get-response-last-webhook) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | sent_at: String.t() | nil, 10 | code_sent: String.t() | nil 11 | } 12 | 13 | defstruct [ 14 | :sent_at, 15 | :code_sent 16 | ] 17 | 18 | @impl true 19 | def cast(generic_map) do 20 | %__MODULE__{ 21 | sent_at: generic_map["sent_at"], 22 | code_sent: generic_map["code_sent"] 23 | } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/plaid/item/status/transactions.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Item.Status.Transactions do 2 | @moduledoc """ 3 | [Plaid item status transactions information.](https://plaid.com/docs/api/items/#item-get-response-transactions) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | last_successful_update: String.t() | nil, 10 | last_failed_update: String.t() | nil 11 | } 12 | 13 | defstruct [ 14 | :last_successful_update, 15 | :last_failed_update 16 | ] 17 | 18 | @impl true 19 | def cast(generic_map) do 20 | %__MODULE__{ 21 | last_successful_update: generic_map["last_successful_update"], 22 | last_failed_update: generic_map["last_failed_update"] 23 | } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/plaid/liabilities.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Liabilities do 2 | @moduledoc """ 3 | [Plaid Liabilities API](https://plaid.com/docs/api/products/#liabilities) calls and schema. 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Castable 9 | alias Plaid.Liabilities.{Credit, Mortgage, Student} 10 | alias __MODULE__ 11 | 12 | @type t :: %__MODULE__{ 13 | credit: [Credit.t()], 14 | mortgage: [Mortgage.t()], 15 | student: [Student.t()] 16 | } 17 | 18 | defstruct [ 19 | :credit, 20 | :mortgage, 21 | :student 22 | ] 23 | 24 | @impl true 25 | def cast(generic_map) do 26 | %__MODULE__{ 27 | credit: Castable.cast_list(Credit, generic_map["credit"]), 28 | mortgage: Castable.cast_list(Mortgage, generic_map["mortgage"]), 29 | student: Castable.cast_list(Student, generic_map["student"]) 30 | } 31 | end 32 | 33 | defmodule GetResponse do 34 | @moduledoc """ 35 | [Plaid API /liabilities/get response schema.](https://plaid.com/docs/api/products/#liabilitiesget) 36 | """ 37 | 38 | @behaviour Castable 39 | 40 | alias Plaid.Account 41 | alias Plaid.Item 42 | 43 | @type t :: %__MODULE__{ 44 | accounts: [Account.t()], 45 | item: Item.t(), 46 | liabilities: Liabilities.t(), 47 | request_id: String.t() 48 | } 49 | 50 | defstruct [ 51 | :accounts, 52 | :item, 53 | :liabilities, 54 | :request_id 55 | ] 56 | 57 | @impl true 58 | def cast(generic_map) do 59 | %__MODULE__{ 60 | accounts: Castable.cast_list(Account, generic_map["accounts"]), 61 | item: Castable.cast(Item, generic_map["item"]), 62 | liabilities: Castable.cast(Liabilities, generic_map["liabilities"]), 63 | request_id: generic_map["request_id"] 64 | } 65 | end 66 | end 67 | 68 | @doc """ 69 | Get liabilities information. 70 | 71 | Does a `POST /liabilities/get` call which fetches liabilities associated 72 | with an access_token's item. 73 | 74 | Params: 75 | * `access_token` - Token to fetch liabilities for. 76 | 77 | Options: 78 | * `:account_ids` - Specific account ids to fetch liabilities for. 79 | 80 | ## Examples 81 | 82 | Liabilities.get("access-sandbox-123xxx", client_id: "123", secret: "abc") 83 | {:ok, %Liabilities.GetResponse{}} 84 | 85 | """ 86 | @spec get(String.t(), options, Plaid.config()) :: 87 | {:ok, GetResponse.t()} | {:error, Plaid.Error.t()} 88 | when options: %{optional(:account_ids) => [String.t()]} 89 | def get(access_token, options \\ %{}, config) do 90 | options_payload = Map.take(options, [:account_ids]) 91 | 92 | payload = %{access_token: access_token, options: options_payload} 93 | 94 | Plaid.Client.call( 95 | "/liabilities/get", 96 | payload, 97 | GetResponse, 98 | config 99 | ) 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/plaid/liabilities/credit.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Liabilities.Credit do 2 | @moduledoc """ 3 | [Plaid Liabilities Credit Schema.](https://plaid.com/docs/api/products/#liabilities-get-response-credit) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Castable 9 | alias Plaid.Liabilities.Credit.APR 10 | 11 | @type t :: %__MODULE__{ 12 | account_id: String.t() | nil, 13 | aprs: [APR.t()], 14 | is_overdue: boolean() | nil, 15 | last_payment_amount: number(), 16 | last_payment_date: String.t(), 17 | last_statement_balance: number(), 18 | last_statement_issue_date: String.t(), 19 | minimum_payment_amount: number(), 20 | next_payment_due_date: String.t() 21 | } 22 | 23 | defstruct [ 24 | :account_id, 25 | :aprs, 26 | :is_overdue, 27 | :last_payment_amount, 28 | :last_payment_date, 29 | :last_statement_balance, 30 | :last_statement_issue_date, 31 | :minimum_payment_amount, 32 | :next_payment_due_date 33 | ] 34 | 35 | @impl true 36 | def cast(generic_map) do 37 | %__MODULE__{ 38 | account_id: generic_map["account_id"], 39 | aprs: Castable.cast_list(APR, generic_map["aprs"]), 40 | is_overdue: generic_map["is_overdue"], 41 | last_payment_amount: generic_map["last_payment_amount"], 42 | last_payment_date: generic_map["last_payment_date"], 43 | last_statement_balance: generic_map["last_statement_balance"], 44 | last_statement_issue_date: generic_map["last_statement_issue_date"], 45 | minimum_payment_amount: generic_map["minimum_payment_amount"], 46 | next_payment_due_date: generic_map["next_payment_due_date"] 47 | } 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/plaid/liabilities/credit/apr.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Liabilities.Credit.APR do 2 | @moduledoc """ 3 | [Plaid Liabilities Credit APR Schema.](https://plaid.com/docs/api/products/#liabilities-get-response-aprs) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | apr_percentage: number(), 10 | apr_type: String.t(), 11 | balance_subject_to_apr: number() | nil, 12 | interest_charge_amount: number() | nil 13 | } 14 | 15 | defstruct [ 16 | :apr_percentage, 17 | :apr_type, 18 | :balance_subject_to_apr, 19 | :interest_charge_amount 20 | ] 21 | 22 | @impl true 23 | def cast(generic_map) do 24 | %__MODULE__{ 25 | apr_percentage: generic_map["apr_percentage"], 26 | apr_type: generic_map["apr_type"], 27 | balance_subject_to_apr: generic_map["balance_subject_to_apr"], 28 | interest_charge_amount: generic_map["interest_charge_amount"] 29 | } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/plaid/liabilities/mortgage.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Liabilities.Mortgage do 2 | @moduledoc """ 3 | [Plaid Liabilities Mortage Schema.](https://plaid.com/docs/api/products/#liabilities-get-response-mortgage) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Address 9 | alias Plaid.Castable 10 | alias Plaid.Liabilities.Mortgage.InterestRate 11 | 12 | @type t :: %__MODULE__{ 13 | account_id: String.t() | nil, 14 | account_number: String.t(), 15 | current_late_fee: number() | nil, 16 | escrow_balance: number() | nil, 17 | has_pmi: boolean() | nil, 18 | has_prepayment_penalty: boolean() | nil, 19 | interest_rate: InterestRate.t(), 20 | last_payment_amount: number() | nil, 21 | last_payment_date: String.t() | nil, 22 | loan_type_description: String.t() | nil, 23 | loan_term: String.t() | nil, 24 | maturity_date: String.t() | nil, 25 | next_monthly_payment: number() | nil, 26 | next_payment_due_date: String.t() | nil, 27 | origination_date: String.t() | nil, 28 | origination_principal_amount: number() | nil, 29 | past_due_amount: number() | nil, 30 | property_address: Address.t(), 31 | ytd_interest_paid: number() | nil, 32 | ytd_principal_paid: number() | nil 33 | } 34 | 35 | defstruct [ 36 | :account_id, 37 | :account_number, 38 | :current_late_fee, 39 | :escrow_balance, 40 | :has_pmi, 41 | :has_prepayment_penalty, 42 | :interest_rate, 43 | :last_payment_amount, 44 | :last_payment_date, 45 | :loan_type_description, 46 | :loan_term, 47 | :maturity_date, 48 | :next_monthly_payment, 49 | :next_payment_due_date, 50 | :origination_date, 51 | :origination_principal_amount, 52 | :past_due_amount, 53 | :property_address, 54 | :ytd_interest_paid, 55 | :ytd_principal_paid 56 | ] 57 | 58 | @impl true 59 | def cast(generic_map) do 60 | %__MODULE__{ 61 | account_id: generic_map["account_id"], 62 | account_number: generic_map["account_number"], 63 | current_late_fee: generic_map["current_late_fee"], 64 | escrow_balance: generic_map["escrow_balance"], 65 | has_pmi: generic_map["has_pmi"], 66 | has_prepayment_penalty: generic_map["has_prepayment_penalty"], 67 | interest_rate: Castable.cast(InterestRate, generic_map["interest_rate"]), 68 | last_payment_amount: generic_map["last_payment_amount"], 69 | last_payment_date: generic_map["last_payment_date"], 70 | loan_type_description: generic_map["loan_type_description"], 71 | loan_term: generic_map["loan_term"], 72 | maturity_date: generic_map["maturity_date"], 73 | next_monthly_payment: generic_map["next_monthly_payment"], 74 | next_payment_due_date: generic_map["next_payment_due_date"], 75 | origination_date: generic_map["origination_date"], 76 | origination_principal_amount: generic_map["origination_principal_amount"], 77 | past_due_amount: generic_map["past_due_amount"], 78 | property_address: Castable.cast(Address, generic_map["property_address"]), 79 | ytd_interest_paid: generic_map["ytd_interest_paid"], 80 | ytd_principal_paid: generic_map["ytd_principal_paid"] 81 | } 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/plaid/liabilities/mortgage/interest_rate.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Liabilities.Mortgage.InterestRate do 2 | @moduledoc """ 3 | [Plaid Liabilities Mortage Interest Rate Schema.](https://plaid.com/docs/api/products/#liabilities-get-response-interest-rate) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | percentage: number() | nil, 10 | type: String.t() | nil 11 | } 12 | 13 | defstruct [ 14 | :percentage, 15 | :type 16 | ] 17 | 18 | @impl true 19 | def cast(generic_map) do 20 | %__MODULE__{ 21 | percentage: generic_map["percentage"], 22 | type: generic_map["type"] 23 | } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/plaid/liabilities/student.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Liabilities.Student do 2 | @moduledoc """ 3 | [Plaid Liabilities Student Schema.](https://plaid.com/docs/api/products/#liabilities-get-response-student) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Address 9 | alias Plaid.Castable 10 | alias Plaid.Liabilities.Student.{LoanStatus, PSLFStatus, RepaymentPlan} 11 | 12 | @type t :: %__MODULE__{ 13 | account_id: String.t() | nil, 14 | account_number: String.t() | nil, 15 | disbursement_dates: [String.t()] | nil, 16 | expected_payoff_date: String.t() | nil, 17 | guarantor: String.t() | nil, 18 | interest_rate_percentage: number(), 19 | is_overdue: boolean() | nil, 20 | last_payment_amount: number() | nil, 21 | last_payment_date: String.t() | nil, 22 | last_statement_balance: number() | nil, 23 | last_statement_issue_date: String.t() | nil, 24 | loan_name: String.t() | nil, 25 | loan_status: LoanStatus.t(), 26 | minimum_payment_amount: number() | nil, 27 | next_payment_due_date: String.t() | nil, 28 | origination_date: String.t() | nil, 29 | origination_principal_amount: number() | nil, 30 | outstanding_interest_amount: number() | nil, 31 | payment_reference_number: String.t() | nil, 32 | pslf_status: PSLFStatus.t(), 33 | repayment_plan: RepaymentPlan.t(), 34 | sequence_number: String.t() | nil, 35 | servicer_address: Address.t(), 36 | ytd_interest_paid: number() | nil, 37 | ytd_principal_paid: number() | nil 38 | } 39 | 40 | defstruct [ 41 | :account_id, 42 | :account_number, 43 | :disbursement_dates, 44 | :expected_payoff_date, 45 | :guarantor, 46 | :interest_rate_percentage, 47 | :is_overdue, 48 | :last_payment_amount, 49 | :last_payment_date, 50 | :last_statement_balance, 51 | :last_statement_issue_date, 52 | :loan_name, 53 | :loan_status, 54 | :minimum_payment_amount, 55 | :next_payment_due_date, 56 | :origination_date, 57 | :origination_principal_amount, 58 | :outstanding_interest_amount, 59 | :payment_reference_number, 60 | :pslf_status, 61 | :repayment_plan, 62 | :sequence_number, 63 | :servicer_address, 64 | :ytd_interest_paid, 65 | :ytd_principal_paid 66 | ] 67 | 68 | @impl true 69 | def cast(generic_map) do 70 | %__MODULE__{ 71 | account_id: generic_map["account_id"], 72 | account_number: generic_map["account_number"], 73 | disbursement_dates: generic_map["disbursement_dates"], 74 | expected_payoff_date: generic_map["expected_payoff_date"], 75 | guarantor: generic_map["guarantor"], 76 | interest_rate_percentage: generic_map["interest_rate_percentage"], 77 | is_overdue: generic_map["is_overdue"], 78 | last_payment_amount: generic_map["last_payment_amount"], 79 | last_payment_date: generic_map["last_payment_date"], 80 | last_statement_balance: generic_map["last_statement_balance"], 81 | last_statement_issue_date: generic_map["last_statement_issue_date"], 82 | loan_name: generic_map["loan_name"], 83 | loan_status: Castable.cast(LoanStatus, generic_map["loan_status"]), 84 | minimum_payment_amount: generic_map["minimum_payment_amount"], 85 | next_payment_due_date: generic_map["next_payment_due_date"], 86 | origination_date: generic_map["origination_date"], 87 | origination_principal_amount: generic_map["origination_principal_amount"], 88 | outstanding_interest_amount: generic_map["outstanding_interest_amount"], 89 | payment_reference_number: generic_map["payment_reference_number"], 90 | pslf_status: Castable.cast(PSLFStatus, generic_map["pslf_status"]), 91 | repayment_plan: Castable.cast(RepaymentPlan, generic_map["repayment_plan"]), 92 | sequence_number: generic_map["sequence_number"], 93 | servicer_address: Castable.cast(Address, generic_map["servicer_address"]), 94 | ytd_interest_paid: generic_map["ytd_interest_paid"], 95 | ytd_principal_paid: generic_map["ytd_principal_paid"] 96 | } 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/plaid/liabilities/student/loan_status.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Liabilities.Student.LoanStatus do 2 | @moduledoc """ 3 | [Plaid Liabilities Student Loan Status Schema.](https://plaid.com/docs/api/products/#liabilities-get-response-loan-status) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | end_date: String.t() | nil, 10 | type: String.t() | nil 11 | } 12 | 13 | defstruct [ 14 | :end_date, 15 | :type 16 | ] 17 | 18 | @impl true 19 | def cast(generic_map) do 20 | %__MODULE__{ 21 | end_date: generic_map["end_date"], 22 | type: generic_map["type"] 23 | } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/plaid/liabilities/student/pslf_status.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Liabilities.Student.PSLFStatus do 2 | @moduledoc """ 3 | [Plaid Liabilities Student PSLF Status Schema.](https://plaid.com/docs/api/products/#liabilities-get-response-pslf-status) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | estimated_eligibility_date: String.t() | nil, 10 | payments_made: number() | nil, 11 | payments_remaining: number() | nil 12 | } 13 | 14 | defstruct [ 15 | :estimated_eligibility_date, 16 | :payments_made, 17 | :payments_remaining 18 | ] 19 | 20 | @impl true 21 | def cast(generic_map) do 22 | %__MODULE__{ 23 | estimated_eligibility_date: generic_map["estimated_eligibility_date"], 24 | payments_made: generic_map["payments_made"], 25 | payments_remaining: generic_map["payments_remaining"] 26 | } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/plaid/liabilities/student/repayment_plan.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Liabilities.Student.RepaymentPlan do 2 | @moduledoc """ 3 | [Plaid Liabilities Student Repayment Plan Schema.](https://plaid.com/docs/api/products/#liabilities-get-response-repayment-plan) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | description: String.t() | nil, 10 | type: String.t() | nil 11 | } 12 | 13 | defstruct [ 14 | :description, 15 | :type 16 | ] 17 | 18 | @impl true 19 | def cast(generic_map) do 20 | %__MODULE__{ 21 | description: generic_map["description"], 22 | type: generic_map["type"] 23 | } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/plaid/link_token.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.LinkToken do 2 | @moduledoc """ 3 | [Pliad link token API](https://plaid.com/docs/api/tokens/) calls and schema. 4 | """ 5 | 6 | alias Plaid.Castable 7 | 8 | alias Plaid.LinkToken.{ 9 | DepositSwitch, 10 | Metadata, 11 | PaymentInitiation, 12 | User 13 | } 14 | 15 | defmodule CreateResponse do 16 | @moduledoc """ 17 | [Plaid API /link/token/create response schema.](https://plaid.com/docs/api/tokens/#linktokencreate) 18 | """ 19 | 20 | @behaviour Castable 21 | 22 | @type t :: %__MODULE__{ 23 | link_token: String.t(), 24 | expiration: String.t(), 25 | request_id: String.t() 26 | } 27 | 28 | defstruct [:link_token, :expiration, :request_id] 29 | 30 | @impl true 31 | def cast(generic_map) do 32 | %__MODULE__{ 33 | link_token: generic_map["link_token"], 34 | expiration: generic_map["expiration"], 35 | request_id: generic_map["request_id"] 36 | } 37 | end 38 | end 39 | 40 | @doc """ 41 | Creates a token for Plaid Link. 42 | 43 | Does a `POST /link/token/create` call which creates a link token which is 44 | required to initialize Plaid Link. 45 | 46 | Params: 47 | * `client_name` - The name of your application, as it should be displayed in Link. 48 | * `language` - The language that Link should be displayed in. 49 | * `country_codes` - Array of country codes to launch Link with. 50 | * `user` - An object specifying information about the end user who will be linking their account. 51 | * `products` - List of Plaid product(s) you wish to use. 52 | * `webhook` - The destination URL to which any webhooks should be sent. 53 | * `access_token` - The access_token associated with the Item to update. 54 | * `link_customization_name` - The name of the Link customization from the Plaid Dashboard to be applied to Link. 55 | * `redirect_uri` - A URI indicating the destination where a user should be forwarded after completing the Link flow. 56 | * `android_package_name` - The name of your app's Android package. Required if initializing android Link. 57 | * `account_filters` - Filter the accounts shown in Link. 58 | * `payment_initiation` - For initializing Link for use with the Payment Initiation. 59 | * `deposit_switch` - For initializing Link for use with the Deposit Switch. 60 | 61 | ## Examples 62 | 63 | LinkToken.create( 64 | %{ 65 | client_name: "Plaid Test App", 66 | language: "en", 67 | country_codes: ["US", "CA"], 68 | user: %LinkToken.User{ 69 | client_user_id: "123-test-user", 70 | legal_name: "Test User", 71 | phone_number: "+19995550123", 72 | phone_number_verified_time: "2020-01-01T00:00:00Z", 73 | email_address: "test@example.com", 74 | email_address_verified_time: "2020-01-01T00:00:00Z", 75 | ssn: "444-33-2222", 76 | date_of_birth: "1990-01-01" 77 | }, 78 | products: ["auth", "transactions"], 79 | webhook: "https://example.com/webhook", 80 | access_token: "access-prod-123xxx", 81 | link_customization_name: "vip-user", 82 | redirect_uri: "https://example.com/redirect", 83 | android_package_name: "com.service.user", 84 | account_filters: %{ 85 | depository: %{ 86 | account_subtypes: ["401k", "529"] 87 | } 88 | }, 89 | payment_initiation: %LinkToken.PaymentInitiation{ 90 | payment_id: "payment-id-sandbox-123xxx" 91 | }, 92 | deposit_switch: %LinkToken.DepositSwitch{ 93 | deposit_switch_id: "deposit-switch-id-sandbox-123xxx" 94 | } 95 | }, 96 | test_api_host: api_host, 97 | client_id: "123", 98 | secret: "abc" 99 | ) 100 | {:ok, LinkToken.CreateResponse{}} 101 | 102 | """ 103 | @spec create(payload, Plaid.config()) :: {:ok, CreateResponse.t()} | {:error, Plaid.Error.t()} 104 | when payload: %{ 105 | :client_name => String.t(), 106 | :language => String.t(), 107 | :country_codes => [String.t()], 108 | :user => User.t(), 109 | optional(:products) => [String.t()], 110 | optional(:webhook) => String.t(), 111 | optional(:access_token) => String.t(), 112 | optional(:link_customization_name) => String.t(), 113 | optional(:redirect_uri) => String.t(), 114 | optional(:android_package_name) => String.t(), 115 | optional(:account_filters) => map(), 116 | optional(:payment_initiation) => PaymentInitiation.t(), 117 | optional(:deposit_switch) => DepositSwitch.t() 118 | } 119 | def create(payload, config) do 120 | Plaid.Client.call("/link/token/create", payload, CreateResponse, config) 121 | end 122 | 123 | defmodule GetResponse do 124 | @moduledoc """ 125 | [Plaid API /link/token/get response schema.](https://plaid.com/docs/api/tokens/#linktokenget) 126 | """ 127 | 128 | @behaviour Castable 129 | 130 | @type t :: %__MODULE__{ 131 | created_at: String.t() | nil, 132 | expiration: String.t() | nil, 133 | link_token: String.t() | nil, 134 | metadata: Metadata.t(), 135 | request_id: String.t() 136 | } 137 | 138 | defstruct [ 139 | :created_at, 140 | :link_token, 141 | :expiration, 142 | :metadata, 143 | :request_id 144 | ] 145 | 146 | @impl true 147 | def cast(generic_map) do 148 | %__MODULE__{ 149 | created_at: generic_map["created_at"], 150 | expiration: generic_map["expiration"], 151 | link_token: generic_map["link_token"], 152 | metadata: Castable.cast(Metadata, generic_map["metadata"]), 153 | request_id: generic_map["request_id"] 154 | } 155 | end 156 | end 157 | 158 | @doc """ 159 | Get information about a previously created link token. 160 | 161 | Does a `POST /link/token/get` call which returns details about a link token which 162 | was previously created. 163 | 164 | Params: 165 | * `link_token` - A link_token from a previous invocation of /link/token/create. 166 | 167 | ## Examples 168 | 169 | LinkToken.get("link-prod-123xxx", client_id: "123", secret: "abc") 170 | {:ok, %Plaid.LinkToken.GetResponse{}} 171 | 172 | """ 173 | @spec get(String.t(), Plaid.config()) :: {:ok, GetResponse.t()} | {:error, Plaid.Error.t()} 174 | def get(link_token, config) do 175 | Plaid.Client.call("/link/token/get", %{link_token: link_token}, GetResponse, config) 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /lib/plaid/link_token/deposit_switch.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.LinkToken.DepositSwitch do 2 | @moduledoc """ 3 | [Plaid link token deposit switch argument.](https://plaid.com/docs/api/tokens/#link-token-create-request-deposit-switch) 4 | """ 5 | 6 | @type t :: %__MODULE__{ 7 | deposit_switch_id: String.t() 8 | } 9 | 10 | @enforce_keys [:deposit_switch_id] 11 | 12 | @derive Jason.Encoder 13 | defstruct [:deposit_switch_id] 14 | end 15 | -------------------------------------------------------------------------------- /lib/plaid/link_token/metadata.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.LinkToken.Metadata do 2 | @moduledoc """ 3 | [Plaid link token metadata.](https://plaid.com/docs/api/tokens/#link-token-get-response-metadata) 4 | """ 5 | 6 | alias Plaid.Castable 7 | 8 | @behaviour Castable 9 | 10 | defmodule Filter do 11 | @moduledoc """ 12 | [Plaid link token account subtype filter.](https://plaid.com/docs/api/tokens/#link-token-get-response-depository) 13 | """ 14 | 15 | @behaviour Castable 16 | 17 | @type t :: %__MODULE__{ 18 | account_subtypes: [String.t()] 19 | } 20 | 21 | defstruct [:account_subtypes] 22 | 23 | @impl true 24 | def cast(generic_map) do 25 | %__MODULE__{ 26 | account_subtypes: generic_map["account_subtypes"] 27 | } 28 | end 29 | end 30 | 31 | defmodule AccountFilters do 32 | @moduledoc """ 33 | [Plaid link token account filters.](https://plaid.com/docs/api/tokens/#link-token-get-response-account-filters) 34 | 35 | > This cannot be a struct because the presence of a key with a `nil` value causes an error 36 | > in the plaid API. 37 | """ 38 | 39 | @behaviour Castable 40 | 41 | @type t :: %{ 42 | optional(:depository) => Filter.t(), 43 | optional(:credit) => Filter.t(), 44 | optional(:loan) => Filter.t(), 45 | optional(:investment) => Filter.t() 46 | } 47 | 48 | @impl true 49 | def cast(generic_map) do 50 | %{} 51 | |> Map.put(:depository, Castable.cast(Filter, generic_map["depository"])) 52 | |> Map.put(:credit, Castable.cast(Filter, generic_map["credit"])) 53 | |> Map.put(:loan, Castable.cast(Filter, generic_map["loan"])) 54 | |> Map.put(:investment, Castable.cast(Filter, generic_map["investment"])) 55 | |> Enum.reject(fn {_, v} -> is_nil(v) end) 56 | |> Map.new() 57 | end 58 | end 59 | 60 | @type t :: %__MODULE__{ 61 | account_filters: AccountFilters.t(), 62 | client_name: String.t() | nil, 63 | country_codes: [String.t()], 64 | initial_products: [String.t()], 65 | language: String.t() | nil, 66 | redirect_uri: String.t() | nil, 67 | webhook: String.t() | nil 68 | } 69 | 70 | @derive Jason.Encoder 71 | defstruct [ 72 | :account_filters, 73 | :client_name, 74 | :country_codes, 75 | :initial_products, 76 | :language, 77 | :redirect_uri, 78 | :webhook 79 | ] 80 | 81 | @impl true 82 | def cast(generic_map) do 83 | %__MODULE__{ 84 | account_filters: Castable.cast(AccountFilters, generic_map["account_filters"]), 85 | client_name: generic_map["client_name"], 86 | country_codes: generic_map["country_codes"], 87 | initial_products: generic_map["initial_products"], 88 | language: generic_map["language"], 89 | redirect_uri: generic_map["redirect_uri"], 90 | webhook: generic_map["webhook"] 91 | } 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/plaid/link_token/payment_initiation.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.LinkToken.PaymentInitiation do 2 | @moduledoc """ 3 | [Plaid link token payment initiation argument.](https://plaid.com/docs/api/tokens/#link-token-create-request-payment-initiation) 4 | """ 5 | 6 | @type t :: %__MODULE__{ 7 | payment_id: String.t() 8 | } 9 | 10 | @enforce_keys [:payment_id] 11 | 12 | @derive Jason.Encoder 13 | defstruct [:payment_id] 14 | end 15 | -------------------------------------------------------------------------------- /lib/plaid/link_token/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.LinkToken.User do 2 | @moduledoc """ 3 | [Plaid link token user argument.](https://plaid.com/docs/api/tokens/#link-token-create-request-user) 4 | """ 5 | 6 | @type t :: %__MODULE__{ 7 | client_user_id: String.t(), 8 | legal_name: String.t() | nil, 9 | phone_number: String.t() | nil, 10 | phone_number_verified_time: String.t() | nil, 11 | email_address: String.t() | nil, 12 | email_address_verified_time: String.t() | nil, 13 | ssn: String.t() | nil, 14 | date_of_birth: String.t() | nil 15 | } 16 | 17 | @enforce_keys [:client_user_id] 18 | 19 | @derive Jason.Encoder 20 | defstruct [ 21 | :client_user_id, 22 | :legal_name, 23 | :phone_number, 24 | :phone_number_verified_time, 25 | :email_address, 26 | :email_address_verified_time, 27 | :ssn, 28 | :date_of_birth 29 | ] 30 | end 31 | -------------------------------------------------------------------------------- /lib/plaid/payment_initiation/address.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.PaymentInitiation.Address do 2 | @moduledoc """ 3 | [Plaid Payment Initiation Address schema.](https://plaid.com/docs/api/products/#payment_initiation-recipient-get-response-address) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | city: String.t(), 10 | street: [String.t()], 11 | postal_code: String.t(), 12 | country: String.t() | nil 13 | } 14 | 15 | @derive Jason.Encoder 16 | defstruct [ 17 | :city, 18 | :street, 19 | :postal_code, 20 | :country 21 | ] 22 | 23 | @impl true 24 | def cast(generic_map) do 25 | %__MODULE__{ 26 | city: generic_map["city"], 27 | street: generic_map["street"], 28 | postal_code: generic_map["postal_code"], 29 | country: generic_map["country"] 30 | } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/plaid/payment_initiation/amount.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.PaymentInitiation.Amount do 2 | @moduledoc """ 3 | [Plaid payment initiation payment amount.](https://plaid.com/docs/api/products/#payment_initiation-payment-create-request-amount) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | currency: number(), 10 | value: String.t() 11 | } 12 | 13 | @enforce_keys [:currency, :value] 14 | 15 | @derive Jason.Encoder 16 | defstruct [ 17 | :currency, 18 | :value 19 | ] 20 | 21 | @impl true 22 | def cast(generic_map) do 23 | %__MODULE__{ 24 | currency: generic_map["currency"], 25 | value: generic_map["value"] 26 | } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/plaid/payment_initiation/bacs.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.PaymentInitiation.BACS do 2 | @moduledoc """ 3 | [Plaid payment initiation recipient BACS details.](https://plaid.com/docs/api/products/#payment_initiation-recipient-create-request-bacs) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | account: String.t(), 10 | sort_code: String.t() 11 | } 12 | 13 | @derive Jason.Encoder 14 | defstruct [ 15 | :account, 16 | :sort_code 17 | ] 18 | 19 | @impl true 20 | def cast(generic_map) do 21 | %__MODULE__{ 22 | account: generic_map["account"], 23 | sort_code: generic_map["sort_code"] 24 | } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/plaid/payment_initiation/payment.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.PaymentInitiation.Payment do 2 | @moduledoc """ 3 | [Plaid API payment initiation payment schema.](https://plaid.com/docs/api/products/#payment_initiation-recipient-list-response-recipients) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Castable 9 | alias Plaid.PaymentInitiation.{Amount, Schedule} 10 | 11 | @type t :: %__MODULE__{ 12 | adjusted_reference: String.t(), 13 | amount: Amount.t() | nil, 14 | last_status_update: String.t(), 15 | payment_id: String.t(), 16 | recipient_id: String.t(), 17 | reference: String.t(), 18 | schedule: Schedule.t(), 19 | status: String.t() | nil 20 | } 21 | 22 | defstruct [ 23 | :adjusted_reference, 24 | :amount, 25 | :last_status_update, 26 | :payment_id, 27 | :recipient_id, 28 | :reference, 29 | :schedule, 30 | :status 31 | ] 32 | 33 | @impl true 34 | def cast(generic_map) do 35 | %__MODULE__{ 36 | adjusted_reference: generic_map["adjusted_reference"], 37 | amount: Castable.cast(Amount, generic_map["amount"]), 38 | last_status_update: generic_map["last_status_update"], 39 | payment_id: generic_map["payment_id"], 40 | recipient_id: generic_map["recipient_id"], 41 | reference: generic_map["reference"], 42 | schedule: Castable.cast(Schedule, generic_map["schedule"]), 43 | status: generic_map["status"] 44 | } 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/plaid/payment_initiation/recipient.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.PaymentInitiation.Recipient do 2 | @moduledoc """ 3 | [Plaid API payment initiation recipient schema.](https://plaid.com/docs/api/products/#payment_initiation-recipient-list-response-recipients) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Castable 9 | alias Plaid.PaymentInitiation.{Address, BACS} 10 | 11 | @type t :: %__MODULE__{ 12 | recipient_id: String.t(), 13 | name: String.t(), 14 | address: Address.t() | nil, 15 | iban: String.t() | nil, 16 | bacs: BACS.t() | nil 17 | } 18 | 19 | defstruct [ 20 | :recipient_id, 21 | :name, 22 | :address, 23 | :iban, 24 | :bacs 25 | ] 26 | 27 | @impl true 28 | def cast(generic_map) do 29 | %__MODULE__{ 30 | recipient_id: generic_map["recipient_id"], 31 | name: generic_map["name"], 32 | address: Castable.cast(Address, generic_map["address"]), 33 | iban: generic_map["iban"], 34 | bacs: Castable.cast(BACS, generic_map["bacs"]) 35 | } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/plaid/payment_initiation/schedule.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.PaymentInitiation.Schedule do 2 | @moduledoc """ 3 | [Plaid payment initiation payment schedule.](https://plaid.com/docs/api/products/#payment_initiation-payment-create-request-schedule) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | interval: String.t(), 10 | interval_execution_day: number(), 11 | start_date: String.t(), 12 | end_date: String.t() 13 | } 14 | 15 | @enforce_keys [:interval, :interval_execution_day, :start_date] 16 | 17 | @derive Jason.Encoder 18 | defstruct [ 19 | :interval, 20 | :interval_execution_day, 21 | :start_date, 22 | :end_date 23 | ] 24 | 25 | @impl true 26 | def cast(generic_map) do 27 | %__MODULE__{ 28 | interval: generic_map["interval"], 29 | interval_execution_day: generic_map["interval_execution_day"], 30 | start_date: generic_map["start_date"], 31 | end_date: generic_map["end_date"] 32 | } 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/plaid/processor.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Processor do 2 | @moduledoc """ 3 | [Plaid Processor API](https://plaid.com/docs/api/processors) calls and schema. 4 | """ 5 | 6 | alias Plaid.Castable 7 | 8 | defmodule CreateTokenResponse do 9 | @moduledoc """ 10 | [Plaid API /processor/token/create response schema.](https://plaid.com/docs/api/processors/#processortokencreate) 11 | """ 12 | 13 | @behaviour Castable 14 | 15 | @type t :: %__MODULE__{ 16 | processor_token: String.t(), 17 | request_id: String.t() 18 | } 19 | 20 | defstruct [:processor_token, :request_id] 21 | 22 | @impl true 23 | def cast(generic_map) do 24 | %__MODULE__{ 25 | processor_token: generic_map["processor_token"], 26 | request_id: generic_map["request_id"] 27 | } 28 | end 29 | end 30 | 31 | @doc """ 32 | Creates a processor token from an access_token. 33 | 34 | Does a `POST /processor/token/create` call which generates 35 | any non-stripe processor token for a given account ID. 36 | 37 | Params: 38 | * `access_token` - access_token to create a processor token for. 39 | * `account_id` - ID of the account to create a processor token for. 40 | * `processor` - name of the processor to create a token for. 41 | 42 | ## Examples 43 | 44 | Processor.create_token("access-prod-123xxx", "blejdkalk3kdlsl", "galileo", client_id: "123", secret: "abc") 45 | {:ok, %Processor.CreateTokenResponse{}} 46 | 47 | """ 48 | @spec create_token(String.t(), String.t(), String.t(), Plaid.config()) :: 49 | {:ok, CreateTokenResponse.t()} | {:error, Plaid.Error.t()} 50 | def create_token(access_token, account_id, processor, config) do 51 | Plaid.Client.call( 52 | "/processor/token/create", 53 | %{access_token: access_token, account_id: account_id, processor: processor}, 54 | CreateTokenResponse, 55 | config 56 | ) 57 | end 58 | 59 | defmodule CreateStripeBankAccountTokenResponse do 60 | @moduledoc """ 61 | [Plaid API /processor/stripe/bank_account_token/create response schema.](https://plaid.com/docs/api/processors/#processorstripebank_account_tokencreate) 62 | """ 63 | 64 | @behaviour Castable 65 | 66 | @type t :: %__MODULE__{ 67 | stripe_bank_account_token: String.t(), 68 | request_id: String.t() 69 | } 70 | 71 | defstruct [:stripe_bank_account_token, :request_id] 72 | 73 | @impl true 74 | def cast(generic_map) do 75 | %__MODULE__{ 76 | stripe_bank_account_token: generic_map["stripe_bank_account_token"], 77 | request_id: generic_map["request_id"] 78 | } 79 | end 80 | end 81 | 82 | @doc """ 83 | Creates a stripe bank account token from an access_token. 84 | 85 | Does a POST `/processor/stripe/bank_account_token/create` call which 86 | generates a stripe bank account token for a given account ID. 87 | 88 | Params: 89 | * `access_token` - access_token to create a processor token for. 90 | * `account_id` - ID of the account to create a processor token for. 91 | 92 | ## Examples 93 | 94 | Processor.create_stripe_bank_account_token("access-prod-123xxx", "blejdkalk3kdlsl", client_id: "123", secret: "abc") 95 | {:ok, %Processor.CreateStripeBankAccountTokenResponse{}} 96 | 97 | """ 98 | @spec create_stripe_bank_account_token(String.t(), String.t(), Plaid.config()) :: 99 | {:ok, CreateStripeBankAccountTokenResponse.t()} | {:error, Plaid.Error.t()} 100 | def create_stripe_bank_account_token(access_token, account_id, config) do 101 | Plaid.Client.call( 102 | "/processor/stripe/bank_account_token/create", 103 | %{access_token: access_token, account_id: account_id}, 104 | CreateStripeBankAccountTokenResponse, 105 | config 106 | ) 107 | end 108 | 109 | defmodule GetAuthResponse do 110 | @moduledoc """ 111 | [Plaid API /processor/auth/get response schema.](https://plaid.com/docs/api/processors/#processorauthget) 112 | """ 113 | 114 | @behaviour Castable 115 | alias Plaid.Account 116 | alias Plaid.Processor.Numbers 117 | 118 | @type t :: %__MODULE__{ 119 | account: Account.t(), 120 | numbers: Numbers.t(), 121 | request_id: String.t() 122 | } 123 | 124 | defstruct [:account, :numbers, :request_id] 125 | 126 | @impl true 127 | def cast(generic_map) do 128 | %__MODULE__{ 129 | account: Castable.cast(Account, generic_map["account"]), 130 | numbers: Castable.cast(Numbers, generic_map["numbers"]), 131 | request_id: generic_map["request_id"] 132 | } 133 | end 134 | end 135 | 136 | @doc """ 137 | Get the bank account info given a processor_token. 138 | 139 | Does a POST `/processor/auth/get` call which returns the bank account and bank 140 | identification number (such as the routing number, for US accounts), for a checking or 141 | savings account that's associated with a given processor_token. 142 | 143 | Params: 144 | * `processor_token` - The processor token obtained from the Plaid integration partner. 145 | 146 | ## Examples 147 | 148 | Processor.get_auth("processor-prod-123xxx", client_id: "123", secret: "abc") 149 | {:ok, %Processor.GetAuthResponse{}} 150 | 151 | """ 152 | @spec get_auth(String.t(), Plaid.config()) :: 153 | {:ok, GetAuthResponse.t()} | {:error, Plaid.Error.t()} 154 | def get_auth(processor_token, config) do 155 | Plaid.Client.call( 156 | "/processor/auth/get", 157 | %{processor_token: processor_token}, 158 | GetAuthResponse, 159 | config 160 | ) 161 | end 162 | 163 | defmodule GetBalanceResponse do 164 | @moduledoc """ 165 | [Plaid API /processor/balance/get response schema.](https://plaid.com/docs/api/processors/#processorbalanceget) 166 | """ 167 | 168 | @behaviour Castable 169 | alias Plaid.Account 170 | 171 | @type t :: %__MODULE__{ 172 | account: Account.t(), 173 | request_id: String.t() 174 | } 175 | 176 | defstruct [:account, :request_id] 177 | 178 | @impl true 179 | def cast(generic_map) do 180 | %__MODULE__{ 181 | account: Castable.cast(Account, generic_map["account"]), 182 | request_id: generic_map["request_id"] 183 | } 184 | end 185 | end 186 | 187 | @doc """ 188 | Get real-time balance for each of an Item's accounts. 189 | 190 | Does a POST `/processor/balance/get` call which returns the balance for each of a Item's 191 | accounts. 192 | 193 | While other endpoints may return a balance object, only /processor/balance/get 194 | forces the available and current balance fields to be refreshed rather than cached. 195 | 196 | Params: 197 | * `processor_token` - The processor token obtained from the Plaid integration partner. 198 | 199 | ## Examples 200 | 201 | Processor.get_balance("processor-prod-123xxx", client_id: "123", secret: "abc") 202 | {:ok, %Processor.GetBalanceResponse{}} 203 | 204 | """ 205 | @spec get_balance(String.t(), Plaid.config()) :: 206 | {:ok, GetBalanceResponse.t()} | {:error, Plaid.Error.t()} 207 | def get_balance(processor_token, config) do 208 | Plaid.Client.call( 209 | "/processor/balance/get", 210 | %{processor_token: processor_token}, 211 | GetBalanceResponse, 212 | config 213 | ) 214 | end 215 | 216 | defmodule GetIdentityResponse do 217 | @moduledoc """ 218 | [Plaid API /processor/identity/get response schema.](https://plaid.com/docs/api/processors/#processoridentityget) 219 | """ 220 | 221 | @behaviour Castable 222 | alias Plaid.Account 223 | 224 | @type t :: %__MODULE__{ 225 | account: Account.t(), 226 | request_id: String.t() 227 | } 228 | 229 | defstruct [:account, :request_id] 230 | 231 | @impl true 232 | def cast(generic_map) do 233 | %__MODULE__{ 234 | account: Castable.cast(Account, generic_map["account"]), 235 | request_id: generic_map["request_id"] 236 | } 237 | end 238 | end 239 | 240 | @doc """ 241 | Get account holder information on file with the financial institution. 242 | 243 | Does a POST `/processor/identity/get` call which allows you to retrieve various 244 | account holder information on file with the financial institution, 245 | including names, emails, phone numbers, and addresses. 246 | 247 | Params: 248 | * `processor_token` - The processor token obtained from the Plaid integration partner. 249 | 250 | ## Examples 251 | 252 | Processor.get_identity("processor-prod-123xxx", client_id: "123", secret: "abc") 253 | {:ok, %Processor.GetIdentityResponse{}} 254 | 255 | """ 256 | @spec get_identity(String.t(), Plaid.config()) :: 257 | {:ok, GetIdentityResponse.t()} | {:error, Plaid.Error.t()} 258 | def get_identity(processor_token, config) do 259 | Plaid.Client.call( 260 | "/processor/identity/get", 261 | %{processor_token: processor_token}, 262 | GetIdentityResponse, 263 | config 264 | ) 265 | end 266 | end 267 | -------------------------------------------------------------------------------- /lib/plaid/processor/numbers.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Processor.Numbers do 2 | @moduledoc """ 3 | [Plaid Processor Numbers schema](https://plaid.com/docs/api/processors/#processorauthget). 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Auth.Numbers.{ACH, BACS, EFT, International} 9 | alias Plaid.Castable 10 | 11 | @type t :: %__MODULE__{ 12 | ach: ACH.t(), 13 | eft: EFT.t(), 14 | international: International.t(), 15 | bacs: BACS.t() 16 | } 17 | 18 | defstruct [ 19 | :ach, 20 | :eft, 21 | :international, 22 | :bacs 23 | ] 24 | 25 | @impl true 26 | def cast(generic_map) do 27 | %__MODULE__{ 28 | ach: Castable.cast(ACH, generic_map["ach"]), 29 | eft: Castable.cast(EFT, generic_map["eft"]), 30 | international: Castable.cast(International, generic_map["international"]), 31 | bacs: Castable.cast(BACS, generic_map["bacs"]) 32 | } 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/plaid/simple_response.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.SimpleResponse do 2 | @moduledoc """ 3 | The most simple response that the Plaid API provides. 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | request_id: String.t() 10 | } 11 | 12 | defstruct [ 13 | :request_id 14 | ] 15 | 16 | @impl true 17 | def cast(generic_map) do 18 | %__MODULE__{ 19 | request_id: generic_map["request_id"] 20 | } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/plaid/transactions.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Transactions do 2 | @moduledoc """ 3 | [Plaid Transactions API](https://plaid.com/docs/api/transactions) calls and schema. 4 | """ 5 | 6 | defmodule GetResponse do 7 | @moduledoc """ 8 | [Plaid API /transactions/get response schema.](https://plaid.com/docs/api/transactions) 9 | """ 10 | 11 | @behaviour Plaid.Castable 12 | 13 | alias Plaid.Account 14 | alias Plaid.Castable 15 | alias Plaid.Item 16 | alias Plaid.Transactions.Transaction 17 | 18 | @type t :: %__MODULE__{ 19 | accounts: [Account.t()], 20 | transactions: [Transaction.t()], 21 | item: Item.t(), 22 | total_transactions: integer(), 23 | request_id: String.t() 24 | } 25 | 26 | defstruct [ 27 | :accounts, 28 | :transactions, 29 | :item, 30 | :total_transactions, 31 | :request_id 32 | ] 33 | 34 | @impl true 35 | def cast(generic_map) do 36 | %__MODULE__{ 37 | accounts: Castable.cast_list(Account, generic_map["accounts"]), 38 | transactions: Castable.cast_list(Transaction, generic_map["transactions"]), 39 | item: Castable.cast(Item, generic_map["item"]), 40 | total_transactions: generic_map["total_transactions"], 41 | request_id: generic_map["request_id"] 42 | } 43 | end 44 | end 45 | 46 | @doc """ 47 | Get information about transactions. 48 | 49 | Does a `POST /transactions/get` call which gives you high level account 50 | data along with transactions from all accounts contained in the 51 | `access_token`'s item. 52 | 53 | Params: 54 | * `access_token` - Token to fetch transactions for. 55 | * `start_date` - Start of query for transactions. 56 | * `end_date` - End of query for transactions. 57 | 58 | Options: 59 | * `:account_ids` - Specific account ids to fetch balances for. 60 | * `:count` - Amount of transactions to pull. 61 | * `:offset` - Offset to start pulling transactions. 62 | 63 | ## Example 64 | 65 | Transactions.get("access-sandbox-123xxx", "2019-10-10", "2019-10-20", client_id: "123", secret: "abc") 66 | {:ok, %Transactions.GetResponse{}} 67 | 68 | """ 69 | @spec get(String.t(), String.t(), String.t(), options, Plaid.config()) :: 70 | {:ok, GetResponse.t()} | {:error, Plaid.Error.t()} 71 | when options: %{ 72 | optional(:account_ids) => [String.t()], 73 | optional(:count) => integer(), 74 | optional(:offset) => integer() 75 | } 76 | def get(access_token, start_date, end_date, options \\ %{}, config) do 77 | options_payload = Map.take(options, [:account_ids, :count, :offset]) 78 | 79 | payload = 80 | %{} 81 | |> Map.put(:access_token, access_token) 82 | |> Map.put(:start_date, start_date) 83 | |> Map.put(:end_date, end_date) 84 | |> Map.put(:options, options_payload) 85 | 86 | Plaid.Client.call( 87 | "/transactions/get", 88 | payload, 89 | Plaid.Transactions.GetResponse, 90 | config 91 | ) 92 | end 93 | 94 | @doc """ 95 | Manually refresh transactions. 96 | 97 | Does a `POST /transactions/refresh` call which kicks off a manual 98 | transactions extraction for all accounts contained in the `access_token`'s 99 | item. 100 | 101 | * `access_token` - Token to fetch transactions for. 102 | 103 | ## Examples 104 | 105 | Transactions.refresh("access-sandbox-123xxx", client_id: "123", secret: "abc") 106 | {:ok, %Plaid.SimpleResponse{}} 107 | 108 | """ 109 | @spec refresh(String.t(), Plaid.config()) :: 110 | {:ok, Plaid.SimpleResponse.t()} | {:error, Plaid.Error.t()} 111 | def refresh(access_token, config) do 112 | Plaid.Client.call( 113 | "/transactions/refresh", 114 | %{access_token: access_token}, 115 | Plaid.SimpleResponse, 116 | config 117 | ) 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/plaid/transactions/transaction.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Transactions.Transaction do 2 | @moduledoc """ 3 | [Plaid Transaction schema.](https://plaid.com/docs/api/transactions) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | alias Plaid.Castable 9 | alias Plaid.Transactions.Transaction.{Location, PaymentMeta} 10 | 11 | @type t :: %__MODULE__{ 12 | account_id: String.t(), 13 | amount: number(), 14 | iso_currency_code: String.t() | nil, 15 | unofficial_currency_code: String.t() | nil, 16 | category: [String.t()] | nil, 17 | category_id: String.t(), 18 | date: String.t(), 19 | authorized_date: String.t() | nil, 20 | location: Location.t(), 21 | name: String.t(), 22 | merchant_name: String.t() | nil, 23 | payment_meta: PaymentMeta.t(), 24 | payment_channel: String.t(), 25 | pending: boolean(), 26 | pending_transaction_id: String.t() | nil, 27 | account_owner: String.t() | nil, 28 | transaction_id: String.t(), 29 | transaction_code: String.t() | nil, 30 | transaction_type: String.t(), 31 | date_transacted: String.t() | nil, 32 | original_description: String.t() | nil 33 | } 34 | 35 | defstruct [ 36 | :account_id, 37 | :amount, 38 | :iso_currency_code, 39 | :unofficial_currency_code, 40 | :category, 41 | :category_id, 42 | :date, 43 | :authorized_date, 44 | :location, 45 | :name, 46 | :merchant_name, 47 | :payment_meta, 48 | :payment_channel, 49 | :pending, 50 | :pending_transaction_id, 51 | :account_owner, 52 | :transaction_id, 53 | :transaction_code, 54 | :transaction_type, 55 | :date_transacted, 56 | :original_description 57 | ] 58 | 59 | @impl true 60 | def cast(generic_map) do 61 | %__MODULE__{ 62 | account_id: generic_map["account_id"], 63 | amount: generic_map["amount"], 64 | iso_currency_code: generic_map["iso_currency_code"], 65 | unofficial_currency_code: generic_map["unofficial_currency_code"], 66 | category: generic_map["category"], 67 | category_id: generic_map["category_id"], 68 | date: generic_map["date"], 69 | authorized_date: generic_map["authorized_date"], 70 | location: Castable.cast(Location, generic_map["location"]), 71 | name: generic_map["name"], 72 | merchant_name: generic_map["merchant_name"], 73 | payment_meta: Castable.cast(PaymentMeta, generic_map["payment_meta"]), 74 | payment_channel: generic_map["payment_channel"], 75 | pending: generic_map["pending"], 76 | pending_transaction_id: generic_map["pending_transaction_id"], 77 | account_owner: generic_map["account_owner"], 78 | transaction_id: generic_map["transaction_id"], 79 | transaction_code: generic_map["transaction_code"], 80 | transaction_type: generic_map["transaction_type"], 81 | date_transacted: generic_map["date_transacted"], 82 | original_description: generic_map["original_description"] 83 | } 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/plaid/transactions/transaction/location.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Transactions.Transaction.Location do 2 | @moduledoc """ 3 | [Plaid Transaction Location schema.](https://plaid.com/docs/api/products/#transactions-get-response-location] 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | address: String.t() | nil, 10 | city: String.t() | nil, 11 | region: String.t() | nil, 12 | postal_code: String.t() | nil, 13 | country: String.t() | nil, 14 | lat: number() | nil, 15 | lon: number() | nil, 16 | store_number: String.t() | nil 17 | } 18 | 19 | defstruct [ 20 | :address, 21 | :city, 22 | :region, 23 | :postal_code, 24 | :country, 25 | :lat, 26 | :lon, 27 | :store_number 28 | ] 29 | 30 | @impl true 31 | def cast(generic_map) do 32 | %__MODULE__{ 33 | address: generic_map["address"], 34 | city: generic_map["city"], 35 | region: generic_map["region"], 36 | postal_code: generic_map["postal_code"], 37 | country: generic_map["country"], 38 | lat: generic_map["lat"], 39 | lon: generic_map["lon"], 40 | store_number: generic_map["store_number"] 41 | } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/plaid/transactions/transaction/payment_meta.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Transactions.Transaction.PaymentMeta do 2 | @moduledoc """ 3 | [Plaid Transaction Payment-Meta schema.](https://plaid.com/docs/api/products/#transactions-get-response-payment-meta) 4 | """ 5 | 6 | @behaviour Plaid.Castable 7 | 8 | @type t :: %__MODULE__{ 9 | reference_number: String.t() | nil, 10 | ppd_id: String.t() | nil, 11 | payee: String.t() | nil, 12 | by_order_of: String.t() | nil, 13 | payer: String.t() | nil, 14 | payment_method: String.t() | nil, 15 | payment_processor: String.t() | nil, 16 | reason: String.t() | nil 17 | } 18 | 19 | defstruct [ 20 | :reference_number, 21 | :ppd_id, 22 | :payee, 23 | :by_order_of, 24 | :payer, 25 | :payment_method, 26 | :payment_processor, 27 | :reason 28 | ] 29 | 30 | @impl true 31 | def cast(generic_map) do 32 | %__MODULE__{ 33 | reference_number: generic_map["reference_number"], 34 | ppd_id: generic_map["ppd_id"], 35 | payee: generic_map["payee"], 36 | by_order_of: generic_map["by_order_of"], 37 | payer: generic_map["payer"], 38 | payment_method: generic_map["payment_method"], 39 | payment_processor: generic_map["payment_processor"], 40 | reason: generic_map["reason"] 41 | } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/plaid/util.ex: -------------------------------------------------------------------------------- 1 | defmodule Plaid.Util do 2 | @moduledoc false 3 | 4 | @doc """ 5 | Put a key-value pair on a map, only if it exists in a source map. 6 | 7 | ## Examples 8 | 9 | iex> maybe_put(%{foo: "bar"}, :test, %{test: 1}) 10 | %{foo: "bar", test: 1} 11 | 12 | iex> maybe_put(%{}, :stuff, %{test: 1}) 13 | %{} 14 | 15 | """ 16 | @spec maybe_put(map(), any(), map()) :: map() 17 | def maybe_put(map, key, source) do 18 | case Map.has_key?(source, key) do 19 | true -> Map.put(map, key, Map.get(source, key)) 20 | false -> map 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Plaid.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :elixir_plaid, 7 | version: "1.2.1", 8 | description: description(), 9 | package: package(), 10 | elixir: "~> 1.10", 11 | start_permanent: Mix.env() == :prod, 12 | source_url: "https://github.com/tylerwray/elixir-plaid", 13 | dialyzer: [ 14 | plt_file: {:no_warn, "priv/plts/dialyzer.plt"}, 15 | plt_add_apps: [:httpoison] 16 | ], 17 | # Suppress warnings 18 | xref: [ 19 | exclude: [ 20 | :httpoison 21 | ] 22 | ], 23 | docs: [ 24 | main: "readme", 25 | extras: ["README.md", "CONTRIBUTING.md", "guides/webhooks.md"], 26 | groups_for_modules: [ 27 | Accounts: [ 28 | Plaid.Account, 29 | Plaid.Account.Balances, 30 | Plaid.Account.HistoricalBalances, 31 | Plaid.Accounts.GetResponse 32 | ], 33 | "Asset Report": [ 34 | Plaid.AssetReport.AsyncResponse, 35 | Plaid.AssetReport.CreateAuditCopyResponse, 36 | Plaid.AssetReport.GetResponse, 37 | Plaid.AssetReport.RemoveAuditCopyResponse, 38 | Plaid.AssetReport.RemoveResponse, 39 | Plaid.AssetReport.Report, 40 | Plaid.AssetReport.Report.Item, 41 | Plaid.AssetReport.User, 42 | Plaid.AssetReport.Warning, 43 | Plaid.AssetReport.Warning.Cause 44 | ], 45 | Auth: [ 46 | Plaid.Auth.GetResponse, 47 | Plaid.Auth.Numbers, 48 | Plaid.Auth.Numbers.ACH, 49 | Plaid.Auth.Numbers.BACS, 50 | Plaid.Auth.Numbers.EFT, 51 | Plaid.Auth.Numbers.International 52 | ], 53 | Categories: [ 54 | Plaid.Categories.Category, 55 | Plaid.Categories.GetResponse 56 | ], 57 | Employer: [ 58 | Plaid.Employer.SearchResponse 59 | ], 60 | Identity: [ 61 | Plaid.Identity.Address, 62 | Plaid.Identity.AddressData, 63 | Plaid.Identity.Email, 64 | Plaid.Identity.GetResponse, 65 | Plaid.Identity.PhoneNumber 66 | ], 67 | Institutions: [ 68 | Plaid.Institution, 69 | Plaid.Institution.Status, 70 | Plaid.Institution.Status.Auth, 71 | Plaid.Institution.Status.Balance, 72 | Plaid.Institution.Status.Breakdown, 73 | Plaid.Institution.Status.HealthIncident, 74 | Plaid.Institution.Status.HealthIncidentUpdate, 75 | Plaid.Institution.Status.Identity, 76 | Plaid.Institution.Status.InvestmentsUpdates, 77 | Plaid.Institution.Status.ItemLogins, 78 | Plaid.Institution.Status.TransactionsUpdates, 79 | Plaid.Institutions.GetByIdResponse, 80 | Plaid.Institutions.GetResponse, 81 | Plaid.Institutions.SearchResponse 82 | ], 83 | Investments: [ 84 | Plaid.Investments.GetHoldingsResponse, 85 | Plaid.Investments.GetTransactionsResponse, 86 | Plaid.Investments.Holding, 87 | Plaid.Investments.Security, 88 | Plaid.Investments.Transaction 89 | ], 90 | Item: [ 91 | Plaid.Item.ExchangePublicTokenResponse, 92 | Plaid.Item.GetResponse, 93 | Plaid.Item.InvalidateAccessTokenResponse, 94 | Plaid.Item.Status, 95 | Plaid.Item.Status.Investments, 96 | Plaid.Item.Status.LastWebhook, 97 | Plaid.Item.Status.Transactions, 98 | Plaid.Item.UpdateWebhookResponse 99 | ], 100 | Liabilities: [ 101 | Plaid.Liabilities.Credit, 102 | Plaid.Liabilities.Credit.APR, 103 | Plaid.Liabilities.GetResponse, 104 | Plaid.Liabilities.Mortgage, 105 | Plaid.Liabilities.Mortgage.InterestRate, 106 | Plaid.Liabilities.Student, 107 | Plaid.Liabilities.Student.LoanStatus, 108 | Plaid.Liabilities.Student.PSLFStatus, 109 | Plaid.Liabilities.Student.RepaymentPlan 110 | ], 111 | "Link Token": [ 112 | Plaid.LinkToken.CreateResponse, 113 | Plaid.LinkToken.DepositSwitch, 114 | Plaid.LinkToken.GetResponse, 115 | Plaid.LinkToken.Metadata, 116 | Plaid.LinkToken.Metadata.AccountFilters, 117 | Plaid.LinkToken.Metadata.Filter, 118 | Plaid.LinkToken.PaymentInitiation, 119 | Plaid.LinkToken.User 120 | ], 121 | "Payment Initiation": [ 122 | Plaid.PaymentInitiation.Address, 123 | Plaid.PaymentInitiation.Amount, 124 | Plaid.PaymentInitiation.BACS, 125 | Plaid.PaymentInitiation.CreatePaymentResponse, 126 | Plaid.PaymentInitiation.CreateRecipientResponse, 127 | Plaid.PaymentInitiation.GetPaymentResponse, 128 | Plaid.PaymentInitiation.GetRecipientResponse, 129 | Plaid.PaymentInitiation.ListPaymentsResponse, 130 | Plaid.PaymentInitiation.ListRecipientsResponse, 131 | Plaid.PaymentInitiation.Payment, 132 | Plaid.PaymentInitiation.Recipient, 133 | Plaid.PaymentInitiation.Schedule 134 | ], 135 | Processor: [ 136 | Plaid.Processor.CreateStripeBankAccountTokenResponse, 137 | Plaid.Processor.CreateTokenResponse, 138 | Plaid.Processor.GetAuthResponse, 139 | Plaid.Processor.GetBalanceResponse, 140 | Plaid.Processor.GetIdentityResponse, 141 | Plaid.Processor.Numbers 142 | ], 143 | Sandbox: [ 144 | Plaid.Sandbox.CreateProcessorTokenResponse, 145 | Plaid.Sandbox.CreatePublicTokenResponse, 146 | Plaid.Sandbox.FireItemWebhookResponse, 147 | Plaid.Sandbox.ResetItemLoginResponse, 148 | Plaid.Sandbox.TransactionsOptions 149 | ], 150 | Transactions: [ 151 | Plaid.Transactions.GetResponse, 152 | Plaid.Transactions.Transaction, 153 | Plaid.Transactions.Transaction.Location, 154 | Plaid.Transactions.Transaction.PaymentMeta 155 | ], 156 | Webhooks: [ 157 | Plaid.Webhooks.AssetsError, 158 | Plaid.Webhooks.AssetsProductReady, 159 | Plaid.Webhooks.Auth, 160 | Plaid.Webhooks.HoldingsUpdate, 161 | Plaid.Webhooks.InvestmentsTransactionsUpdate, 162 | Plaid.Webhooks.ItemError, 163 | Plaid.Webhooks.ItemPendingExpiration, 164 | Plaid.Webhooks.ItemUserPermissionRevoked, 165 | Plaid.Webhooks.ItemWebhookUpdateAcknowledged, 166 | Plaid.Webhooks.TransactionsRemoved, 167 | Plaid.Webhooks.TransactionsUpdate, 168 | Plaid.Webhooks.PaymentInitiationPaymentStatusUpdate 169 | ], 170 | Other: [ 171 | Plaid, 172 | Plaid.Address, 173 | Plaid.Error, 174 | Plaid.SimpleResponse 175 | ], 176 | Internal: [ 177 | Plaid.Client, 178 | Plaid.Client.HTTPoison 179 | ] 180 | ] 181 | ], 182 | deps: deps() 183 | ] 184 | end 185 | 186 | # Run "mix help compile.app" to learn about applications. 187 | def application do 188 | [ 189 | extra_applications: [:logger] 190 | ] 191 | end 192 | 193 | # Run "mix help deps" to learn about dependencies. 194 | defp deps do 195 | [ 196 | {:bypass, "~> 2.1", only: :test}, 197 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 198 | {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, 199 | {:ex_doc, "~> 0.28", only: :dev, runtime: false}, 200 | {:httpoison, "~> 2.0", optional: true}, 201 | {:jason, "~> 1.3"}, 202 | {:joken, "~> 2.4"}, 203 | {:secure_compare, "~> 0.1.0"} 204 | ] 205 | end 206 | 207 | defp description do 208 | "Simply Beautiful Elixir bindings for the Plaid API." 209 | end 210 | 211 | defp package do 212 | [ 213 | licenses: ["MIT"], 214 | links: %{"GitHub" => "https://github.com/tylerwray/elixir-plaid"} 215 | ] 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, 4 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, 5 | "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, 6 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, 7 | "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, 8 | "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, 9 | "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, 10 | "earmark_parser": {:hex, :earmark_parser, "1.4.23", "1d5f22a2802160fd454404fbf5e8f5d14cd8eb727c63701397b72d8c35267e69", [:mix], [], "hexpm", "2ec13bf14b2f4bbb4a15480970e295eede8bb01087fad6ceca27b724ab8e9d18"}, 11 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 12 | "ex_doc": {:hex, :ex_doc, "0.28.2", "e031c7d1a9fc40959da7bf89e2dc269ddc5de631f9bd0e326cbddf7d8085a9da", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "51ee866993ffbd0e41c084a7677c570d0fc50cb85c6b5e76f8d936d9587fa719"}, 13 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 14 | "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, 15 | "httpoison": {:hex, :httpoison, "2.1.0", "655fd9a7b0b95ee3e9a3b535cf7ac8e08ef5229bab187fa86ac4208b122d934b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fc455cb4306b43827def4f57299b2d5ac8ac331cb23f517e734a4b78210a160c"}, 16 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 17 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 18 | "joken": {:hex, :joken, "2.4.1", "63a6e47aaf735637879f31babfad93c936d63b8b7d01c5ef44c7f37689e71ab4", [:mix], [{:jose, "~> 1.11.2", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "d4fc7c703112b2dedc4f9ec214856c3a07108c4835f0f174a369521f289c98d1"}, 19 | "jose": {:hex, :jose, "1.11.2", "f4c018ccf4fdce22c71e44d471f15f723cb3efab5d909ab2ba202b5bf35557b3", [:mix, :rebar3], [], "hexpm", "98143fbc48d55f3a18daba82d34fe48959d44538e9697c08f34200fa5f0947d2"}, 20 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 21 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, 22 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 23 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 24 | "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"}, 25 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 26 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 27 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 28 | "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"}, 29 | "plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"}, 30 | "plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"}, 31 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, 32 | "secure_compare": {:hex, :secure_compare, "0.1.0", "01b3c93c8edb696e8a5b38397ed48e10958c8a5ec740606656445bcbec0aadb8", [:mix], [], "hexpm", "6391a49eb4a6182f0d7425842fc774bbed715e78b2bfb0c83b99c94e02c78b5c"}, 33 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 34 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 35 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 36 | } 37 | -------------------------------------------------------------------------------- /test/plaid/auth_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plaid.AuthTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Plug.Conn 5 | 6 | setup do 7 | bypass = Bypass.open() 8 | api_host = "http://localhost:#{bypass.port}/" 9 | {:ok, bypass: bypass, api_host: api_host} 10 | end 11 | 12 | test "/auth/get", %{bypass: bypass, api_host: api_host} do 13 | Bypass.expect_once(bypass, "POST", "/auth/get", fn conn -> 14 | Conn.resp(conn, 200, ~s<{ 15 | "accounts": [ 16 | { 17 | "account_id": "vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D", 18 | "balances": { 19 | "available": 100, 20 | "current": 110, 21 | "limit": null, 22 | "iso_currency_code": "USD", 23 | "unofficial_currency_code": null 24 | }, 25 | "mask": "9606", 26 | "name": "Plaid Checking", 27 | "official_name": "Plaid Gold Checking", 28 | "subtype": "checking", 29 | "type": "depository" 30 | } 31 | ], 32 | "numbers": { 33 | "ach": [ 34 | { 35 | "account": "9900009606", 36 | "account_id": "vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D", 37 | "routing": "011401533", 38 | "wire_routing": "021000021" 39 | } 40 | ], 41 | "eft": [ 42 | { 43 | "account": "111122223333", 44 | "account_id": "vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D", 45 | "institution": "021", 46 | "branch": "01140" 47 | } 48 | ], 49 | "international": [ 50 | { 51 | "account_id": "vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D", 52 | "bic": "NWBKGB21", 53 | "iban": "GB29NWBK60161331926819" 54 | } 55 | ], 56 | "bacs": [ 57 | { 58 | "account": "31926819", 59 | "account_id": "vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D", 60 | "sort_code": "601613" 61 | } 62 | ] 63 | }, 64 | "item": { 65 | "available_products": [ 66 | "balance", 67 | "identity", 68 | "payment_initiation", 69 | "transactions" 70 | ], 71 | "billed_products": [ 72 | "assets", 73 | "auth" 74 | ], 75 | "consent_expiration_time": null, 76 | "error": null, 77 | "institution_id": "ins_117650", 78 | "item_id": "DWVAAPWq4RHGlEaNyGKRTAnPLaEmo8Cvq7na6", 79 | "webhook": "https://www.genericwebhookurl.com/webhook" 80 | }, 81 | "request_id": "m8MDnv9okwxFNBV" 82 | }>) 83 | end) 84 | 85 | {:ok, 86 | %Plaid.Auth.GetResponse{ 87 | accounts: [ 88 | %Plaid.Account{ 89 | account_id: "vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D", 90 | balances: %Plaid.Account.Balances{ 91 | available: 100, 92 | current: 110, 93 | limit: nil, 94 | iso_currency_code: "USD", 95 | unofficial_currency_code: nil 96 | }, 97 | mask: "9606", 98 | name: "Plaid Checking", 99 | official_name: "Plaid Gold Checking", 100 | subtype: "checking", 101 | type: "depository" 102 | } 103 | ], 104 | numbers: %Plaid.Auth.Numbers{ 105 | ach: [ 106 | %Plaid.Auth.Numbers.ACH{ 107 | account: "9900009606", 108 | account_id: "vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D", 109 | routing: "011401533", 110 | wire_routing: "021000021" 111 | } 112 | ], 113 | eft: [ 114 | %Plaid.Auth.Numbers.EFT{ 115 | account: "111122223333", 116 | account_id: "vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D", 117 | institution: "021", 118 | branch: "01140" 119 | } 120 | ], 121 | international: [ 122 | %Plaid.Auth.Numbers.International{ 123 | account_id: "vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D", 124 | bic: "NWBKGB21", 125 | iban: "GB29NWBK60161331926819" 126 | } 127 | ], 128 | bacs: [ 129 | %Plaid.Auth.Numbers.BACS{ 130 | account: "31926819", 131 | account_id: "vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D", 132 | sort_code: "601613" 133 | } 134 | ] 135 | }, 136 | item: %Plaid.Item{ 137 | available_products: [ 138 | "balance", 139 | "identity", 140 | "payment_initiation", 141 | "transactions" 142 | ], 143 | billed_products: [ 144 | "assets", 145 | "auth" 146 | ], 147 | consent_expiration_time: nil, 148 | error: nil, 149 | institution_id: "ins_117650", 150 | item_id: "DWVAAPWq4RHGlEaNyGKRTAnPLaEmo8Cvq7na6", 151 | webhook: "https://www.genericwebhookurl.com/webhook" 152 | }, 153 | request_id: "m8MDnv9okwxFNBV" 154 | }} = 155 | Plaid.Auth.get( 156 | "access-prod-123xxx", 157 | test_api_host: api_host, 158 | client_id: "123", 159 | secret: "abc" 160 | ) 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /test/plaid/categories_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plaid.CategoriesTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Plug.Conn 5 | 6 | setup do 7 | bypass = Bypass.open() 8 | api_host = "http://localhost:#{bypass.port}/" 9 | {:ok, bypass: bypass, api_host: api_host} 10 | end 11 | 12 | test "/categories/get", %{bypass: bypass, api_host: api_host} do 13 | Bypass.expect_once(bypass, "POST", "/categories/get", fn conn -> 14 | Conn.resp(conn, 200, ~s<{ 15 | "categories": [ 16 | { 17 | "category_id": "10000000", 18 | "group": "special", 19 | "hierarchy": [ 20 | "Bank Fees" 21 | ] 22 | }, 23 | { 24 | "category_id": "10001000", 25 | "group": "special", 26 | "hierarchy": [ 27 | "Bank Fees", 28 | "Overdraft" 29 | ] 30 | }, 31 | { 32 | "category_id": "12001000", 33 | "group": "place", 34 | "hierarchy": [ 35 | "Community", 36 | "Animal Shelter" 37 | ] 38 | } 39 | ], 40 | "request_id": "1vwmF5TBQwiqfwP" 41 | }>) 42 | end) 43 | 44 | {:ok, 45 | %Plaid.Categories.GetResponse{ 46 | categories: [ 47 | %Plaid.Categories.Category{ 48 | category_id: "10000000", 49 | group: "special", 50 | hierarchy: [ 51 | "Bank Fees" 52 | ] 53 | }, 54 | %Plaid.Categories.Category{ 55 | category_id: "10001000", 56 | group: "special", 57 | hierarchy: [ 58 | "Bank Fees", 59 | "Overdraft" 60 | ] 61 | }, 62 | %Plaid.Categories.Category{ 63 | category_id: "12001000", 64 | group: "place", 65 | hierarchy: [ 66 | "Community", 67 | "Animal Shelter" 68 | ] 69 | } 70 | ], 71 | request_id: "1vwmF5TBQwiqfwP" 72 | }} = 73 | Plaid.Categories.get( 74 | test_api_host: api_host, 75 | client_id: "123", 76 | secret: "abc" 77 | ) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/plaid/employer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plaid.EmployerTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Plug.Conn 5 | 6 | setup do 7 | bypass = Bypass.open() 8 | api_host = "http://localhost:#{bypass.port}/" 9 | {:ok, bypass: bypass, api_host: api_host} 10 | end 11 | 12 | test "/employers/search", %{bypass: bypass, api_host: api_host} do 13 | Bypass.expect_once(bypass, "POST", "/employers/search", fn conn -> 14 | Conn.resp(conn, 200, ~s<{ 15 | "employers": [ 16 | { 17 | "name": "Plaid Technologies Inc", 18 | "address": { 19 | "city": "San Francisco", 20 | "country": "US", 21 | "postal_code": "94103", 22 | "region": "CA", 23 | "street": "1098 Harrison St" 24 | }, 25 | "confidence_score": 1, 26 | "employer_id": "emp_1" 27 | } 28 | ], 29 | "request_id": "ixTBLZGvhD4NnmB" 30 | }>) 31 | end) 32 | 33 | {:ok, 34 | %Plaid.Employer.SearchResponse{ 35 | employers: [ 36 | %Plaid.Employer{ 37 | name: "Plaid Technologies Inc", 38 | address: %Plaid.Address{ 39 | city: "San Francisco", 40 | country: "US", 41 | postal_code: "94103", 42 | region: "CA", 43 | street: "1098 Harrison St" 44 | }, 45 | confidence_score: 1, 46 | employer_id: "emp_1" 47 | } 48 | ], 49 | request_id: "ixTBLZGvhD4NnmB" 50 | }} = 51 | Plaid.Employer.search( 52 | "Plaid Technologies", 53 | ["auth"], 54 | test_api_host: api_host, 55 | client_id: "123", 56 | secret: "abc" 57 | ) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/plaid/item_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plaid.ItemTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Plug.Conn 5 | 6 | setup do 7 | bypass = Bypass.open() 8 | api_host = "http://localhost:#{bypass.port}/" 9 | {:ok, bypass: bypass, api_host: api_host} 10 | end 11 | 12 | test "/item/get", %{bypass: bypass, api_host: api_host} do 13 | Bypass.expect_once(bypass, "POST", "/item/get", fn conn -> 14 | Conn.resp(conn, 200, ~s<{ 15 | "item": { 16 | "available_products": [ 17 | "balance", 18 | "auth" 19 | ], 20 | "billed_products": [ 21 | "identity", 22 | "transactions" 23 | ], 24 | "error": null, 25 | "institution_id": "ins_109508", 26 | "item_id": "Ed6bjNrDLJfGvZWwnkQlfxwoNz54B5C97ejBr", 27 | "update_type": "background", 28 | "webhook": "https://plaid.com/example/hook", 29 | "consent_expiration_time": null 30 | }, 31 | "status": { 32 | "investments": { 33 | "last_successful_update": "2019-02-15T15:52:39.000Z", 34 | "last_failed_update": "2019-01-22T04:32:00.000Z" 35 | }, 36 | "transactions": { 37 | "last_successful_update": "2019-02-15T15:52:39.000Z", 38 | "last_failed_update": "2019-01-22T04:32:00.000Z" 39 | }, 40 | "last_webhook": { 41 | "sent_at": "2019-02-15T15:53:00.000Z", 42 | "code_sent": "DEFAULT_UPDATE" 43 | } 44 | }, 45 | "request_id": "m8MDnv9okwxFNBV" 46 | }>) 47 | end) 48 | 49 | {:ok, 50 | %Plaid.Item.GetResponse{ 51 | item: %Plaid.Item{ 52 | available_products: [ 53 | "balance", 54 | "auth" 55 | ], 56 | billed_products: [ 57 | "identity", 58 | "transactions" 59 | ], 60 | error: nil, 61 | institution_id: "ins_109508", 62 | item_id: "Ed6bjNrDLJfGvZWwnkQlfxwoNz54B5C97ejBr", 63 | update_type: "background", 64 | webhook: "https://plaid.com/example/hook", 65 | consent_expiration_time: nil 66 | }, 67 | status: %Plaid.Item.Status{ 68 | investments: %Plaid.Item.Status.Investments{ 69 | last_successful_update: "2019-02-15T15:52:39.000Z", 70 | last_failed_update: "2019-01-22T04:32:00.000Z" 71 | }, 72 | transactions: %Plaid.Item.Status.Transactions{ 73 | last_successful_update: "2019-02-15T15:52:39.000Z", 74 | last_failed_update: "2019-01-22T04:32:00.000Z" 75 | }, 76 | last_webhook: %Plaid.Item.Status.LastWebhook{ 77 | sent_at: "2019-02-15T15:53:00.000Z", 78 | code_sent: "DEFAULT_UPDATE" 79 | } 80 | }, 81 | request_id: "m8MDnv9okwxFNBV" 82 | }} = 83 | Plaid.Item.get( 84 | "access-prod-123xxx", 85 | test_api_host: api_host, 86 | client_id: "123", 87 | secret: "abc" 88 | ) 89 | end 90 | 91 | test "/item/remove", %{bypass: bypass, api_host: api_host} do 92 | Bypass.expect_once(bypass, "POST", "/item/remove", fn conn -> 93 | Conn.resp(conn, 200, ~s<{ 94 | "request_id": "m8MDnv9okwxFNBV" 95 | }>) 96 | end) 97 | 98 | {:ok, %Plaid.SimpleResponse{request_id: "m8MDnv9okwxFNBV"}} = 99 | Plaid.Item.remove( 100 | "access-prod-123xxx", 101 | test_api_host: api_host, 102 | client_id: "123", 103 | secret: "abc" 104 | ) 105 | end 106 | 107 | test "/item/webhook/update", %{bypass: bypass, api_host: api_host} do 108 | Bypass.expect_once(bypass, "POST", "/item/webhook/update", fn conn -> 109 | Conn.resp(conn, 200, ~s<{ 110 | "item": { 111 | "available_products": [ 112 | "balance", 113 | "identity", 114 | "payment_initiation", 115 | "transactions" 116 | ], 117 | "billed_products": [ 118 | "assets", 119 | "auth" 120 | ], 121 | "consent_expiration_time": null, 122 | "error": null, 123 | "institution_id": "ins_117650", 124 | "item_id": "DWVAAPWq4RHGlEaNyGKRTAnPLaEmo8Cvq7na6", 125 | "update_type": "background", 126 | "webhook": "https://www.genericwebhookurl.com/webhook" 127 | }, 128 | "request_id": "vYK11LNTfRoAMbj" 129 | }>) 130 | end) 131 | 132 | {:ok, 133 | %Plaid.Item.UpdateWebhookResponse{ 134 | item: %Plaid.Item{ 135 | available_products: [ 136 | "balance", 137 | "identity", 138 | "payment_initiation", 139 | "transactions" 140 | ], 141 | billed_products: [ 142 | "assets", 143 | "auth" 144 | ], 145 | consent_expiration_time: nil, 146 | error: nil, 147 | institution_id: "ins_117650", 148 | item_id: "DWVAAPWq4RHGlEaNyGKRTAnPLaEmo8Cvq7na6", 149 | update_type: "background", 150 | webhook: "https://www.genericwebhookurl.com/webhook" 151 | }, 152 | request_id: "vYK11LNTfRoAMbj" 153 | }} = 154 | Plaid.Item.update_webhook( 155 | "access-prod-123xxx", 156 | "https://plaid.com/updated/hook", 157 | test_api_host: api_host, 158 | client_id: "123", 159 | secret: "abc" 160 | ) 161 | end 162 | 163 | test "/item/public_token/exchange", %{bypass: bypass, api_host: api_host} do 164 | Bypass.expect_once(bypass, "POST", "/item/public_token/exchange", fn conn -> 165 | Conn.resp(conn, 200, ~s<{ 166 | "access_token": "access-sandbox-de3ce8ef-33f8-452c-a685-8671031fc0f6", 167 | "item_id": "M5eVJqLnv3tbzdngLDp9FL5OlDNxlNhlE55op", 168 | "request_id": "Aim3b" 169 | }>) 170 | end) 171 | 172 | {:ok, 173 | %Plaid.Item.ExchangePublicTokenResponse{ 174 | access_token: "access-sandbox-de3ce8ef-33f8-452c-a685-8671031fc0f6", 175 | item_id: "M5eVJqLnv3tbzdngLDp9FL5OlDNxlNhlE55op", 176 | request_id: "Aim3b" 177 | }} = 178 | Plaid.Item.exchange_public_token( 179 | "public-prod-123xxx", 180 | test_api_host: api_host, 181 | client_id: "123", 182 | secret: "abc" 183 | ) 184 | end 185 | 186 | test "/item/access_token/invalidate", %{bypass: bypass, api_host: api_host} do 187 | Bypass.expect_once(bypass, "POST", "/item/access_token/invalidate", fn conn -> 188 | Conn.resp(conn, 200, ~s<{ 189 | "new_access_token": "access-sandbox-8ab976e6-64bc-4b38-98f7-731e7a349970", 190 | "request_id": "m8MDnv9okwxFNBV" 191 | }>) 192 | end) 193 | 194 | {:ok, 195 | %Plaid.Item.InvalidateAccessTokenResponse{ 196 | new_access_token: "access-sandbox-8ab976e6-64bc-4b38-98f7-731e7a349970", 197 | request_id: "m8MDnv9okwxFNBV" 198 | }} = 199 | Plaid.Item.invalidate_access_token( 200 | "access-prod-123xxx", 201 | test_api_host: api_host, 202 | client_id: "123", 203 | secret: "abc" 204 | ) 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /test/plaid/link_token_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plaid.LinkTokenTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Plug.Conn 5 | 6 | setup do 7 | bypass = Bypass.open() 8 | api_host = "http://localhost:#{bypass.port}/" 9 | {:ok, bypass: bypass, api_host: api_host} 10 | end 11 | 12 | test "/link/token/create", %{bypass: bypass, api_host: api_host} do 13 | Bypass.expect_once(bypass, "POST", "/link/token/create", fn conn -> 14 | Conn.resp(conn, 200, ~s<{ 15 | "link_token": "link-production-840204-193734", 16 | "expiration": "2020-03-27T12:56:34Z", 17 | "request_id": "XQVgFigpGHXkb0b" 18 | }>) 19 | end) 20 | 21 | {:ok, 22 | %Plaid.LinkToken.CreateResponse{ 23 | link_token: "link-production-840204-193734", 24 | expiration: "2020-03-27T12:56:34Z", 25 | request_id: "XQVgFigpGHXkb0b" 26 | }} = 27 | Plaid.LinkToken.create( 28 | %{ 29 | client_name: "Plaid Test App", 30 | language: "en", 31 | country_codes: ["US", "CA"], 32 | user: %Plaid.LinkToken.User{ 33 | client_user_id: "123-test-user", 34 | legal_name: "Test User", 35 | phone_number: "+19995550123", 36 | phone_number_verified_time: "2020-01-01T00:00:00Z", 37 | email_address: "test@example.com", 38 | email_address_verified_time: "2020-01-01T00:00:00Z", 39 | ssn: "444-33-2222", 40 | date_of_birth: "1990-01-01" 41 | }, 42 | products: ["auth", "transactions"], 43 | webhook: "https://example.com/webhook", 44 | access_token: "access-prod-123xxx", 45 | link_customization_name: "vip-user", 46 | redirect_uri: "https://example.com/redirect", 47 | android_package_name: "com.service.user", 48 | account_filters: %{ 49 | depository: %{ 50 | account_subtypes: ["401k", "529"] 51 | } 52 | }, 53 | payment_initiation: %Plaid.LinkToken.PaymentInitiation{ 54 | payment_id: "payment-id-sandbox-123xxx" 55 | }, 56 | deposit_switch: %Plaid.LinkToken.DepositSwitch{ 57 | deposit_switch_id: "deposit-switch-id-sandbox-123xxx" 58 | } 59 | }, 60 | test_api_host: api_host, 61 | client_id: "123", 62 | secret: "abc" 63 | ) 64 | end 65 | 66 | test "/link/token/get", %{bypass: bypass, api_host: api_host} do 67 | Bypass.expect_once(bypass, "POST", "/link/token/get", fn conn -> 68 | Conn.resp(conn, 200, ~s<{ 69 | "created_at": "2020-12-02T21:14:54Z", 70 | "expiration": "2020-12-03T01:14:54Z", 71 | "link_token": "link-sandbox-33792986-2b9c-4b80-b1f2-518caaac6183", 72 | "metadata": { 73 | "account_filters": { 74 | "depository": { 75 | "account_subtypes": [ 76 | "checking", 77 | "savings" 78 | ] 79 | } 80 | }, 81 | "client_name": "Insert Client name here", 82 | "country_codes": [ 83 | "US" 84 | ], 85 | "initial_products": [ 86 | "auth" 87 | ], 88 | "language": "en", 89 | "redirect_uri": null, 90 | "webhook": "https://www.example.com/webhook" 91 | }, 92 | "request_id": "u0ydFs493XjyTYn" 93 | }>) 94 | end) 95 | 96 | {:ok, 97 | %Plaid.LinkToken.GetResponse{ 98 | created_at: "2020-12-02T21:14:54Z", 99 | expiration: "2020-12-03T01:14:54Z", 100 | link_token: "link-sandbox-33792986-2b9c-4b80-b1f2-518caaac6183", 101 | metadata: %Plaid.LinkToken.Metadata{ 102 | account_filters: %{ 103 | depository: %Plaid.LinkToken.Metadata.Filter{ 104 | account_subtypes: [ 105 | "checking", 106 | "savings" 107 | ] 108 | } 109 | }, 110 | client_name: "Insert Client name here", 111 | country_codes: [ 112 | "US" 113 | ], 114 | initial_products: [ 115 | "auth" 116 | ], 117 | language: "en", 118 | redirect_uri: nil, 119 | webhook: "https://www.example.com/webhook" 120 | }, 121 | request_id: "u0ydFs493XjyTYn" 122 | }} = 123 | Plaid.LinkToken.get( 124 | "link-production-123xxx", 125 | test_api_host: api_host, 126 | client_id: "123", 127 | secret: "abc" 128 | ) 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /test/plaid/sandbox_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plaid.SandboxTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Plug.Conn 5 | 6 | setup do 7 | bypass = Bypass.open() 8 | api_host = "http://localhost:#{bypass.port}/" 9 | {:ok, bypass: bypass, api_host: api_host} 10 | end 11 | 12 | test "/sandbox/public_token/create", %{bypass: bypass, api_host: api_host} do 13 | Bypass.expect_once(bypass, "POST", "/sandbox/public_token/create", fn conn -> 14 | Conn.resp(conn, 200, ~s<{ 15 | "public_token": "public-sandbox-b0e2c4ee-a763-4df5-bfe9-46a46bce993d", 16 | "request_id": "Aim3b" 17 | }>) 18 | end) 19 | 20 | {:ok, 21 | %Plaid.Sandbox.CreatePublicTokenResponse{ 22 | public_token: "public-sandbox-b0e2c4ee-a763-4df5-bfe9-46a46bce993d", 23 | request_id: "Aim3b" 24 | }} = 25 | Plaid.Sandbox.create_public_token( 26 | "ins_1", 27 | ["assets", "auth", "balance"], 28 | %{ 29 | webhook: "https://webhook.example.com/webhook", 30 | override_username: "user_is_good", 31 | override_password: "pass_is_good", 32 | transactions: %Plaid.Sandbox.TransactionsOptions{ 33 | start_date: "2010-01-01", 34 | end_date: "2020-01-01" 35 | } 36 | }, 37 | test_api_host: api_host, 38 | client_id: "123", 39 | secret: "abc" 40 | ) 41 | end 42 | 43 | test "/sandbox/public_token/create without options", %{bypass: bypass, api_host: api_host} do 44 | Bypass.expect_once(bypass, "POST", "/sandbox/public_token/create", fn conn -> 45 | Conn.resp(conn, 200, ~s<{ 46 | "public_token": "public-sandbox-b0e2c4ee-a763-4df5-bfe9-46a46bce993d", 47 | "request_id": "Aim3b" 48 | }>) 49 | end) 50 | 51 | {:ok, 52 | %Plaid.Sandbox.CreatePublicTokenResponse{ 53 | public_token: "public-sandbox-b0e2c4ee-a763-4df5-bfe9-46a46bce993d", 54 | request_id: "Aim3b" 55 | }} = 56 | Plaid.Sandbox.create_public_token( 57 | "ins_1", 58 | ["assets", "auth", "balance"], 59 | test_api_host: api_host, 60 | client_id: "123", 61 | secret: "abc" 62 | ) 63 | end 64 | 65 | test "/sandbox/item/reset_login", %{bypass: bypass, api_host: api_host} do 66 | Bypass.expect_once(bypass, "POST", "/sandbox/item/reset_login", fn conn -> 67 | Conn.resp(conn, 200, ~s<{ 68 | "reset_login": true, 69 | "request_id": "m8MDnv9okwxFNBV" 70 | }>) 71 | end) 72 | 73 | {:ok, 74 | %Plaid.Sandbox.ResetItemLoginResponse{ 75 | reset_login: true, 76 | request_id: "m8MDnv9okwxFNBV" 77 | }} = 78 | Plaid.Sandbox.reset_item_login( 79 | "access-prod-123xxx", 80 | test_api_host: api_host, 81 | client_id: "123", 82 | secret: "abc" 83 | ) 84 | end 85 | 86 | test "/sandbox/item/set_verification_status", %{bypass: bypass, api_host: api_host} do 87 | Bypass.expect_once(bypass, "POST", "/sandbox/item/set_verification_status", fn conn -> 88 | Conn.resp(conn, 200, ~s<{ 89 | "request_id": "1vwmF5TBQwiqfwP" 90 | }>) 91 | end) 92 | 93 | {:ok, 94 | %Plaid.SimpleResponse{ 95 | request_id: "1vwmF5TBQwiqfwP" 96 | }} = 97 | Plaid.Sandbox.set_item_verification_status( 98 | "access-prod-123xxx", 99 | "39flxk4ek2xs", 100 | "verification_expired", 101 | test_api_host: api_host, 102 | client_id: "123", 103 | secret: "abc" 104 | ) 105 | end 106 | 107 | test "/sandbox/item/fire_webhook", %{bypass: bypass, api_host: api_host} do 108 | Bypass.expect_once(bypass, "POST", "/sandbox/item/fire_webhook", fn conn -> 109 | Conn.resp(conn, 200, ~s<{ 110 | "webhook_fired": true, 111 | "request_id": "1vwmF5TBQwiqfwP" 112 | }>) 113 | end) 114 | 115 | {:ok, 116 | %Plaid.Sandbox.FireItemWebhookResponse{ 117 | webhook_fired: true, 118 | request_id: "1vwmF5TBQwiqfwP" 119 | }} = 120 | Plaid.Sandbox.fire_item_webhook( 121 | "access-prod-123xxx", 122 | "DEFAULT_UPDATE", 123 | test_api_host: api_host, 124 | client_id: "123", 125 | secret: "abc" 126 | ) 127 | end 128 | 129 | test "/sandbox/bank_transfer/simulate", %{bypass: bypass, api_host: api_host} do 130 | Bypass.expect_once(bypass, "POST", "/sandbox/bank_transfer/simulate", fn conn -> 131 | Conn.resp(conn, 200, ~s<{ 132 | "request_id": "1vwmF5TBQwiqfwP" 133 | }>) 134 | end) 135 | 136 | {:ok, 137 | %Plaid.SimpleResponse{ 138 | request_id: "1vwmF5TBQwiqfwP" 139 | }} = 140 | Plaid.Sandbox.simulate_bank_transfer( 141 | "bt_123xxx", 142 | "failed", 143 | %{ 144 | failure_reason: %{ 145 | ach_return_code: "R01", 146 | description: "Failed for unknown reason" 147 | } 148 | }, 149 | test_api_host: api_host, 150 | client_id: "123", 151 | secret: "abc" 152 | ) 153 | end 154 | 155 | test "/sandbox/bank_transfer/simulate without options", %{bypass: bypass, api_host: api_host} do 156 | Bypass.expect_once(bypass, "POST", "/sandbox/bank_transfer/simulate", fn conn -> 157 | Conn.resp(conn, 200, ~s<{ 158 | "request_id": "1vwmF5TBQwiqfwP" 159 | }>) 160 | end) 161 | 162 | {:ok, 163 | %Plaid.SimpleResponse{ 164 | request_id: "1vwmF5TBQwiqfwP" 165 | }} = 166 | Plaid.Sandbox.simulate_bank_transfer( 167 | "bt_123xxx", 168 | "failed", 169 | test_api_host: api_host, 170 | client_id: "123", 171 | secret: "abc" 172 | ) 173 | end 174 | 175 | test "/sandbox/bank_transfer/fire_webhook", %{bypass: bypass, api_host: api_host} do 176 | Bypass.expect_once(bypass, "POST", "/sandbox/bank_transfer/fire_webhook", fn conn -> 177 | Conn.resp(conn, 200, ~s<{ 178 | "request_id": "1vwmF5TBQwiqfwP" 179 | }>) 180 | end) 181 | 182 | {:ok, 183 | %Plaid.SimpleResponse{ 184 | request_id: "1vwmF5TBQwiqfwP" 185 | }} = 186 | Plaid.Sandbox.fire_bank_transfer_webhook( 187 | "https://example.com/webhook", 188 | test_api_host: api_host, 189 | client_id: "123", 190 | secret: "abc" 191 | ) 192 | end 193 | 194 | test "/sandbox/processor_token/create", %{bypass: bypass, api_host: api_host} do 195 | Bypass.expect_once(bypass, "POST", "/sandbox/processor_token/create", fn conn -> 196 | Conn.resp(conn, 200, ~s<{ 197 | "processor_token": "processor-sandbox-b0e2c4ee-a763-4df5-bfe9-46a46bce993d", 198 | "request_id": "Aim3b" 199 | }>) 200 | end) 201 | 202 | {:ok, 203 | %Plaid.Sandbox.CreateProcessorTokenResponse{ 204 | processor_token: "processor-sandbox-b0e2c4ee-a763-4df5-bfe9-46a46bce993d", 205 | request_id: "Aim3b" 206 | }} = 207 | Plaid.Sandbox.create_processor_token( 208 | "ins_1", 209 | %{override_username: "user_is_good", override_password: "pass_is_good"}, 210 | test_api_host: api_host, 211 | client_id: "123", 212 | secret: "abc" 213 | ) 214 | end 215 | 216 | test "/sandbox/processor_token/create without options", %{bypass: bypass, api_host: api_host} do 217 | Bypass.expect_once(bypass, "POST", "/sandbox/processor_token/create", fn conn -> 218 | Conn.resp(conn, 200, ~s<{ 219 | "processor_token": "processor-sandbox-b0e2c4ee-a763-4df5-bfe9-46a46bce993d", 220 | "request_id": "Aim3b" 221 | }>) 222 | end) 223 | 224 | {:ok, 225 | %Plaid.Sandbox.CreateProcessorTokenResponse{ 226 | processor_token: "processor-sandbox-b0e2c4ee-a763-4df5-bfe9-46a46bce993d", 227 | request_id: "Aim3b" 228 | }} = 229 | Plaid.Sandbox.create_processor_token( 230 | "ins_1", 231 | test_api_host: api_host, 232 | client_id: "123", 233 | secret: "abc" 234 | ) 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Application.ensure_all_started(:httpoison) 3 | --------------------------------------------------------------------------------