From bd451f97f3838784f890ff4a1ff0c0b37fd53cb1 Mon Sep 17 00:00:00 2001 From: Allan Calix Date: Wed, 8 Sep 2021 21:06:59 -0700 Subject: [PATCH] Initial commit --- .github/workflows/release.yaml | 19 + .github/workflows/test.yaml | 21 + .gitignore | 2 + Cargo.toml | 37 + LICENSE | 21 + README.md | 78 + examples/builder.rs | 23 + examples/curl_client.rs | 25 + src/client.rs | 920 +++ src/lib.rs | 53 + src/model/account.rs | 70 + src/model/auth.rs | 67 + src/model/balance.rs | 28 + src/model/common.rs | 48 + src/model/employers.rs | 39 + src/model/identity.rs | 27 + src/model/institutions.rs | 117 + src/model/item.rs | 93 + src/model/mod.rs | 28 + src/model/sandbox.rs | 104 + src/model/token.rs | 157 + src/model/transactions.rs | 161 + src/model/webhooks.rs | 23 + ..._tests__can_fetch_accounts_with_token.snap | 142 + ...__tests__can_fetch_single_institution.snap | 24 + ..._tests__can_get_multiple_institutions.snap | 206 + ...laid__client__tests__can_modify_items.snap | 23 + .../rplaid__client__tests__can_read_auth.snap | 182 + ...d__client__tests__can_read_categories.snap | 5151 +++++++++++++++++ ...aid__client__tests__can_read_identity.snap | 163 + ..._client__tests__can_read_transactions.snap | 124 + ...lient__tests__can_search_institutions.snap | 207 + 32 files changed, 8383 insertions(+) create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/test.yaml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/builder.rs create mode 100644 examples/curl_client.rs create mode 100644 src/client.rs create mode 100644 src/lib.rs create mode 100644 src/model/account.rs create mode 100644 src/model/auth.rs create mode 100644 src/model/balance.rs create mode 100644 src/model/common.rs create mode 100644 src/model/employers.rs create mode 100644 src/model/identity.rs create mode 100644 src/model/institutions.rs create mode 100644 src/model/item.rs create mode 100644 src/model/mod.rs create mode 100644 src/model/sandbox.rs create mode 100644 src/model/token.rs create mode 100644 src/model/transactions.rs create mode 100644 src/model/webhooks.rs create mode 100644 src/snapshots/rplaid__client__tests__can_fetch_accounts_with_token.snap create mode 100644 src/snapshots/rplaid__client__tests__can_fetch_single_institution.snap create mode 100644 src/snapshots/rplaid__client__tests__can_get_multiple_institutions.snap create mode 100644 src/snapshots/rplaid__client__tests__can_modify_items.snap create mode 100644 src/snapshots/rplaid__client__tests__can_read_auth.snap create mode 100644 src/snapshots/rplaid__client__tests__can_read_categories.snap create mode 100644 src/snapshots/rplaid__client__tests__can_read_identity.snap create mode 100644 src/snapshots/rplaid__client__tests__can_read_transactions.snap create mode 100644 src/snapshots/rplaid__client__tests__can_search_institutions.snap diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..d784376 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,19 @@ +name: release +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" +jobs: + release: + name: Publish release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - uses: actions-rs/cargo@v1 + with: + command: publish + args: --token ${{ secrets.CRATESIO_TOKEN }} + - uses: softprops/action-gh-release@v1 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..824bfc3 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,21 @@ +name: Test +on: + push: + branches: + - main +jobs: + linux: + name: Linux Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + env: + PLAID_SECRET: ${{ secrets.PLAID_SECRET }} + PLAID_CLIENT_ID: ${{ secrets.PLAID_CLIENT_ID }} + with: + command: test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..87a4d78 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "rplaid" +version = "0.2.0" +authors = ["Allan Calix "] +edition = "2018" +description = """ +An async client library for Plaid APIs. +""" +homepage = "https://github.com/allancalix/rplaid" +documentation = "https://docs.rs/rplaid" +repository = "https://github.com/allancalix/rplaid" +readme = "README.md" +keywords = ["plaid", "api", "client", "async", "finance"] +categories = ["api-bindings"] +exclude = ["/.github/*"] +license = "MIT" + +[features] +default = ["streams"] +bare = [] +streams = ["async-stream", "futures-core", "futures-util"] + +[dependencies] +http-client = "6.5.1" +tokio = { version = "1", features = ["macros"] } +http-types = "2.12.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0.28" +async-stream = { version = "0.3.2", optional = true } +futures-core = { version = "0.3.17", optional = true } +futures-util = { version = "0.3.17", optional = true } + +[dev-dependencies] +insta = { version = "1.7.2", features = ["redactions"] } +tokio = { version = "1", features = ["test-util", "rt-multi-thread"] } +http-client = { version = "6.5.1", features = ["curl_client", "rustls"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..43b865b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Allan Calix + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8750ab --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# rplaid + +[![crates.io](https://img.shields.io/crates/v/rplaid.svg)](https://crates.io/crates/rplaid) +[![Released API docs](https://docs.rs/rplaid/badge.svg)](https://docs.rs/rplaid) +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) +[![GHA Build Status](https://github.com/allancalix/rplaid/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/allancalix/rplaid/actions?query=workflow%3ATEST) + +**rplaid** is an async client for the [Plaid API](https://plaid.com/docs/api/). +With minimal features, the client is meant to be extensible and lightweight. +Additional features can be enabled to improve ergonomics of the API at the +cost of additional dependencies. + +The goal is to provide expressive bindings that provide sensible defaults where +possible for ease of use. + +See official [API docs](https://plaid.com/docs/) for more information about +endpoints or specific products. + +__These are not official Plaid bindings.__ + +```rust +use rplaid::client::{Builder, Credentials}; +use rplaid::model::*; + +#[tokio::main] +async fn main() { + let client = Builder::new() + .with_credentials(Credentials { + client_id: std::env::var("PLAID_CLIENT_ID").unwrap(), + secret: std::env::var("PLAID_SECRET").unwrap(), + }) + .build(); + let institutions = client + .get_institutions(&InstitutionsGetRequest { + count: 10, + offset: 0, + country_codes: &["US"], + options: None, + }) + .await + .unwrap(); + + println!("{:?}", institutions); +} +``` + +## Glossary +* Item - A Item represents a connection to a single financial instution. + Typically links are associated with a pair of credentials and an + `access_token`. Items are associated to one or more accounts. + +* Link - Link is a client-side component that Plaid provides to link to accounts. + See https://plaid.com/docs/link/#introduction-to-link for more + information. + +* Account - An account is a financial account that is linked to an Item. An item, + or financial institution, may have multiple accounts for a single + user (e.g. a checking account and a credit account). + +* Product - Entities with services offered by Plaid, see + https://plaid.com/docs/api/products/ for more information. + +## Features +* Idiomatic futures generator for easily reading multiple pages of transactions. +* Extensible HttpClient interfaces supports multiple HTTP clients with minimal + effort (surf, H1, and reqwest). The trait can also be implemented to have full + control over the HTTP client used. +* Rust types, including variant types, for most API return types. + +## Limitations +Some endpoints are production specific or beta products and are not yet +supported by the client. + +For a breakdown of endpoint support visit: +https://docs.google.com/spreadsheets/d/1xqUXdfllo37Rx5MVrQODbVqNQvuktiCVL5Uh8y9mYYw + +## License +[MIT](LICENSE) diff --git a/examples/builder.rs b/examples/builder.rs new file mode 100644 index 0000000..dc61ae0 --- /dev/null +++ b/examples/builder.rs @@ -0,0 +1,23 @@ +use rplaid::client::{Builder, Credentials}; +use rplaid::model::*; + +#[tokio::main] +async fn main() { + let client = Builder::new() + .with_credentials(Credentials { + client_id: std::env::var("PLAID_CLIENT_ID").unwrap(), + secret: std::env::var("PLAID_SECRET").unwrap(), + }) + .build(); + let institutions = client + .get_institutions(&InstitutionsGetRequest { + count: 10, + offset: 0, + country_codes: &["US"], + options: None, + }) + .await + .unwrap(); + + println!("{:?}", institutions); +} diff --git a/examples/curl_client.rs b/examples/curl_client.rs new file mode 100644 index 0000000..b0617ea --- /dev/null +++ b/examples/curl_client.rs @@ -0,0 +1,25 @@ +use http_client::isahc::IsahcClient; +use rplaid::client::{Builder, Credentials}; +use rplaid::model::*; + +#[tokio::main] +async fn main() { + let client = Builder::new() + .with_credentials(Credentials { + client_id: std::env::var("PLAID_CLIENT_ID").unwrap(), + secret: std::env::var("PLAID_SECRET").unwrap(), + }) + .with_http_client(IsahcClient::new()) + .build(); + let institutions = client + .get_institutions(&InstitutionsGetRequest { + count: 10, + offset: 0, + country_codes: &["US"], + options: None, + }) + .await + .unwrap(); + + println!("{:?}", institutions); +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..e59f201 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,920 @@ +use http_client::h1::H1Client; +use http_client::Error as HttpError; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::model::*; +use crate::HttpClient; + +const SANDBOX_DOMAIN: &str = "https://sandbox.plaid.com"; +const DEVELOPMENT_DOMAIN: &str = "https://development.plaid.com"; +const PRODUCTION_DOMAIN: &str = "https://production.plaid.com"; + +/// Error returned by client requests. +#[derive(Error, Debug)] +pub enum ClientError { + /// Wraps errors from the underlying HTTP client. + #[error("http request failed: {0}")] + Http(HttpError), + /// Error either serializing request types or deserializing response types + /// from requests. + #[error(transparent)] + Parse(#[from] serde_json::Error), + /// Wraps errors from Plaid's API responses. If an error is parsed then + /// Plaid successfully returned a response but returned with errors. + #[error(transparent)] + App(#[from] ErrorResponse), +} + +/// Credentials required to make authenticated calls to the Plaid API. +#[derive(Debug, Default)] +pub struct Credentials { + /// Plaid API client id token. + pub client_id: String, + /// Plaid API secret for the configured environment (e.g. sandbox, dev, prod). + pub secret: String, +} + +impl From for ClientError { + fn from(error: HttpError) -> Self { + Self::Http(error) + } +} + +/// Environment controls the domain for the client, matches Plaid's sandbox, +/// development, and production environments. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum Environment { + /// Used to configure the client to request against a the domain in the string. + /// Should be a fully qualified domain with protocol and scheme, for example + /// http://localhost:3000. + Custom(String), + /// Plaid sandbox environment. + Sandbox, + /// Plaid development environment. + Development, + /// Plaid production environment. + Production, +} + +impl std::default::Default for Environment { + fn default() -> Self { + Environment::Sandbox + } +} + +impl std::string::ToString for Environment { + fn to_string(&self) -> String { + match self { + Environment::Sandbox => SANDBOX_DOMAIN.into(), + Environment::Development => DEVELOPMENT_DOMAIN.into(), + Environment::Production => PRODUCTION_DOMAIN.into(), + Environment::Custom(s) => s.into(), + } + } +} + +/// Plaid API client type. +pub struct Plaid { + http: T, + credentials: Credentials, + env: Environment, +} + +/// Builder helps construct Plaid client types with sensible defaults. +pub struct Builder { + http: Option>, + credentials: Option, + env: Option, +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} + +impl Builder { + /// Constructs a new Plaid client builder. + /// + /// ``` + /// use rplaid::client::Builder; + /// + /// let client = Builder::new().build(); + /// ``` + pub fn new() -> Self { + Self { + http: None, + credentials: None, + env: None, + } + } + + /// Override the default HTTP client. + pub fn with_http_client(mut self, client: impl HttpClient) -> Self { + self.http = Some(Box::new(client)); + self + } + + /// Set Plaid API credentials for authenticating Plaid API calls. + pub fn with_credentials(mut self, creds: Credentials) -> Self { + self.credentials = Some(creds); + self + } + + /// Set API request environment. + pub fn with_env(mut self, env: Environment) -> Self { + self.env = Some(env); + self + } + + /// Consume a builder returning a Plaid client instance. + pub fn build(self) -> Plaid> { + let http = self.http.unwrap_or_else(|| Box::new(H1Client::new())); + Plaid { + http, + credentials: self.credentials.unwrap_or_default(), + env: self.env.unwrap_or_default(), + } + } +} + +impl Plaid { + /// Creates a new Plaid instance from a set of credentials and an HttpClient. + /// + /// ``` + /// use rplaid::client::{Plaid, Credentials, Environment}; + /// use http_client::h1::H1Client; + /// + /// let client = Plaid::new( + /// H1Client::new(), + /// Credentials{client_id: "".into(), secret: "".into()}, + /// Environment::Sandbox); + /// ``` + pub fn new(http: T, credentials: Credentials, env: Environment) -> Self { + Self { + http, + credentials, + env, + } + } + + async fn request( + &self, + endpoint: &E, + ) -> Result + where + for<'de> ::Response: serde::Deserialize<'de>, + { + let mut post = endpoint.request(&self.env.to_string()); + post.insert_header("Content-Type", "application/json"); + post.insert_header("PLAID-CLIENT-ID", &self.credentials.client_id); + post.insert_header("PLAID-SECRET", &self.credentials.secret); + let mut res = self.http.send(post).await?; + + match res.status() { + http_client::http_types::StatusCode::Ok => Ok(res.body_json::().await?), + _ => Err(ClientError::from(res.body_json::().await?)), + } + } + + /// Returns details for institutions that match the query parameters up to a + /// maximum of ten institutions per query. + /// + /// https://plaid.com/docs/api/institutions/#institutionssearch + pub async fn search_institutions + http_types::convert::Serialize>( + &self, + req: &InstitutionsSearchRequest<'_, P>, + ) -> Result, ClientError> { + Ok(self.request(req).await?.institutions) + } + + /// Returns details on an institution currently supported by plaid. + /// + /// https://plaid.com/docs/api/institutions/#institutionsget_by_id + pub async fn get_institution_by_id + http_types::convert::Serialize>( + &self, + req: &InstitutionGetRequest<'_, P>, + ) -> Result { + Ok(self.request(req).await?.institution) + } + + /// Returns details on all financial institutions currently supported by + /// Plaid. Plaid supports thousands of institutions and results are + /// paginated. Institutions with no overlap to the client's enabled products + /// are filtered from results. + /// + /// https://plaid.com/docs/api/institutions/#institutionsget + pub async fn get_institutions + http_types::convert::Serialize>( + &self, + req: &InstitutionsGetRequest<'_, P>, + ) -> Result, ClientError> { + Ok(self.request(req).await?.institutions) + } + + /// Creates a valid `public_token` for an institution ID, initial products, + /// and test credentials. The created public token maps to a new Sandbox + /// item. + /// + /// https://plaid.com/docs/api/sandbox/#sandboxpublic_tokencreate + pub async fn create_public_token + http_types::convert::Serialize>( + &self, + req: CreatePublicTokenRequest<'_, P>, + ) -> Result { + Ok(self.request(&req).await?.public_token) + } + + /// Forces an item into an `ITEM_LOGIN_REQUIRED` state in order to simulate + /// an Item whose login is no longer valid. + /// + /// https://plaid.com/docs/api/sandbox/#sandboxitemreset_login + pub async fn reset_login + http_types::convert::Serialize>( + &self, + access_token: P, + ) -> Result<(), ClientError> { + let res = self.request(&ResetLoginRequest { access_token }).await?; + match res.reset_login { + true => Ok(()), + false => Err(ClientError::App(ErrorResponse { + error_message: Some("failed to reset login".into()), + ..ErrorResponse::default() + })), + } + } + + /// Exchange a Link `public_token` for an API `access_token`. Public tokens + /// are ephemeral and expires after 30 minutes. + /// + /// https://plaid.com/docs/api/tokens/#itempublic_tokenexchange + pub async fn exchange_public_token + http_types::convert::Serialize>( + &self, + public_token: P, + ) -> Result { + Ok(self + .request(&ExchangePublicTokenRequest { public_token }) + .await?) + } + + /// Creates a `link_token` that is required as a parameter when initializing + /// a Link. + /// + /// https://plaid.com/docs/api/tokens/#linktokencreate + pub async fn create_link_token + http_types::convert::Serialize>( + &self, + req: &CreateLinkTokenRequest<'_, P>, + ) -> Result { + Ok(self.request(req).await?) + } + + /// Retrieves information for any linked item, only active accounts are + /// returned. Responses may be cached, if up-to-date information is required + /// use `balances` instead. + /// + /// https://plaid.com/docs/api/accounts/#accountsget + pub async fn accounts + http_types::convert::Serialize>( + &self, + access_token: P, + ) -> Result, ClientError> { + Ok(self + .request(&GetAccountsRequest { + access_token, + options: None, + }) + .await? + .accounts) + } + + /// Returns information about the status of an Item. + /// + /// https://plaid.com/docs/api/items/#itemget + pub async fn item + http_types::convert::Serialize>( + &self, + access_token: P, + ) -> Result { + Ok(self.request(&GetItemRequest { access_token }).await?.item) + } + + /// Removes an Item. Once removed, the `access_token` associated with the + /// Item is no longer valid and cannot be used to access any data that was + /// associated with the Item. + /// + /// https://plaid.com/docs/api/items/#itemremove + pub async fn item_del + http_types::convert::Serialize>( + &self, + access_token: P, + ) -> Result<(), ClientError> { + self.request(&RemoveItemRequest { access_token }).await?; + + Ok(()) + } + + /// Updates the webhook URL associated with an Item. Updates trigger a + /// `WEBHOOK_UPDATE_ACKNOWLEDGED` event to the new webhook URL. + /// + /// https://plaid.com/docs/api/items/#itemwebhookupdate + pub async fn item_webhook_update + http_types::convert::Serialize>( + &self, + access_token: P, + webhook: P, + ) -> Result { + Ok(self + .request(&UpdateItemWebhookRequest { + access_token, + webhook, + }) + .await? + .item) + } + + /// Verify real-time account balances. This endpoint can be used as long as + /// Link has been initialized with any other product. + /// + /// https://plaid.com/docs/api/products/#balance + pub async fn balances + http_types::convert::Serialize>( + &self, + access_token: P, + ) -> Result, ClientError> { + Ok(self + .request(&AccountBalancesGetRequest { + access_token, + options: None, + }) + .await? + .accounts) + } + + /// Returns the bank account and bank identification numbers associated with + /// an Item's checking and savings accounts, along with high-level account + /// data and balances when available. + /// + /// https://plaid.com/docs/api/products/#auth + pub async fn auth + http_types::convert::Serialize>( + &self, + req: &GetAuthRequest<'_, P>, + ) -> Result { + Ok(self.request(req).await?) + } + + /// Verify the name, address, phone number, and email address of a user + /// against bank account information on file. + /// + /// https://plaid.com/docs/api/products/#identity + pub async fn identity + http_types::convert::Serialize>( + &self, + req: &GetIdentityRequest<'_, P>, + ) -> Result { + Ok(self.request(req).await?) + } + + /// Triggers a Transactions `DEFAULT_UPDATE` webhook for a given Sandbox + /// Item. If the Item does not support Transactions, a + /// `SANDBOX_PRODUCT_NOT_ENABLED` error will result. + /// + /// https://plaid.com/docs/api/sandbox/#sandboxitemfire_webhook + pub async fn fire_webhook + http_types::convert::Serialize>( + &self, + req: &FireWebhookRequest

, + ) -> Result { + Ok(self.request(req).await?) + } + + /// Changes the verification status of an Item in the sandbox in order to + /// simulate the Automated Micro-deposit flow. + /// + /// https://plaid.com/docs/api/sandbox/#sandboxitemset_verification_status + pub async fn set_verification_status + http_types::convert::Serialize>( + &self, + req: &SetVerificationStatusRequest

, + ) -> Result { + Ok(self.request(req).await?) + } + + /// Searches Plaid's database for known employers to use with Deposit + /// Switch. + /// + /// https://plaid.com/docs/api/employers/ + pub async fn search_employers + http_types::convert::Serialize>( + &self, + req: &SearchEmployerRequest<'_, P>, + ) -> Result { + Ok(self.request(req).await?) + } + + /// Provides a JSON Web Key (JWK) that can be used to verify a JWT. + /// + /// https://plaid.com/docs/api/webhooks/webhook-verification/#webhook_verification_keyget + pub async fn create_webhook_verification_key + http_types::convert::Serialize>( + &self, + req: &GetWebhookVerificationKeyRequest

, + ) -> Result { + Ok(self.request(req).await?) + } + + /// Gets information about a `link_token`, can be useful for debugging. + /// + /// https://plaid.com/docs/api/tokens/#linktokenget + pub async fn link_token + http_types::convert::Serialize>( + &self, + req: &GetLinkTokenRequest

, + ) -> Result { + Ok(self.request(req).await?) + } + + /// Rotate the `access_token` associated with an Item. Call returns a new + /// `access_token` and immediately invalidates the previous token. + /// + /// https://plaid.com/docs/api/tokens/#itemaccess_tokeninvalidate + pub async fn invalidate_access_token + http_types::convert::Serialize>( + &self, + req: &InvalidateAccessTokenRequest

, + ) -> Result { + Ok(self.request(req).await?) + } + + /// Get detailed information on categories returned by Plaid. This endpoint + /// does not require authentication. + /// + /// https://plaid.com/docs/api/products/#categoriesget + pub async fn categories( + &self, + req: &GetCategoriesRequest, + ) -> Result { + Ok(self.request(req).await?) + } + + /// Initiates on-demand extraction to fetch the newest transactions for an + /// Item. + /// + /// https://plaid.com/docs/api/products/#transactionsrefresh + pub async fn refresh_transactions + http_types::convert::Serialize>( + &self, + req: &RefreshTransactionsRequest

, + ) -> Result<(), ClientError> { + self.request(req).await?; + Ok(()) + } + + /// Returns user-authorized transaction data for credit, depository, and + /// some loan-type accounts. For transaction history from investment + /// accounts, use investment endpoints instead (production only). Results + /// are paginated based on request options and defaults to 100 entities per + /// page. + /// + /// https://plaid.com/docs/api/products/#transactionsget + pub async fn transactions + http_types::convert::Serialize>( + &self, + req: &GetTransactionsRequest

, + ) -> Result { + Ok(self.request(req).await?) + } + + /// Returns a Stream of transactions that can be used to iterative fetch + /// pages from the transaction endpoint. Each call will return the number of + /// items configured in the original request. + /// + /// ```ignore + /// use futures_util::pin_mut; + /// use futures_util::StreamExt; + /// + /// ... + /// + /// let req = GetTransactionsRequest { + /// access_token: res.access_token.as_str(), + /// start_date: "2019-09-01", + /// end_date: "2021-09-05", + /// options: Some(GetTransactionsOptions { + /// // Number of items to return per page. + /// count: Some(10), + /// // Number of items from the start_date to offset results by. + /// offset: Some(5), + /// account_ids: None, + /// include_original_description: None, + /// }), + /// }; + /// let iter = client.transactions_iter(req); + /// pin_mut!(iter); + /// + /// while let Some(txn) = iter.next().await { + /// println!("{:?}", txn); + /// } + /// ``` + #[cfg(feature = "streams")] + pub fn transactions_iter<'a, P: AsRef + http_types::convert::Serialize + Clone + 'a>( + &'a self, + req: GetTransactionsRequest

, + ) -> impl futures_core::stream::Stream, ClientError>> + 'a { + async_stream::try_stream! { + let mut yielded = 0; + let mut total_xacts = None; + let mut request = req.clone(); + let count = req.options.as_ref().unwrap().count.unwrap_or(100); + let mut offset = req.options.as_ref().unwrap().offset.unwrap_or(0); + + while total_xacts.is_none() || total_xacts.unwrap() > yielded { + if let Some(ref mut opts) = &mut request.options { + opts.count = Some(count); + opts.offset = Some(offset); + } else { + request.options = Some(GetTransactionsOptions{ + count: Some(count), + offset: Some(offset), + account_ids: None, + include_original_description: None, + }); + } + + let res = self.transactions(&request).await?; + if total_xacts.is_none() { + total_xacts = Some(res.total_transactions - offset); + } + yielded += res.transactions.len(); + offset += yielded; + + yield res.transactions; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures_util::pin_mut; + use futures_util::StreamExt; + + const INSTITUTION_ID: &str = "ins_129571"; + + fn credentials() -> Credentials { + Credentials { + client_id: std::env::var("PLAID_CLIENT_ID") + .expect("Variable PLAID_CLIENT_ID must be defined."), + secret: std::env::var("PLAID_SECRET").expect("Variable PLAID_SECRET must be defined."), + } + } + + #[tokio::test] + async fn unauthorized_calls_return_parsable_error() { + let client = Builder::new().with_credentials(credentials()).build(); + let res = client + // Accounts is an authenticated call and requires a valid access token. + .accounts("") + .await; + + match res.unwrap_err() { + ClientError::App(e) => { + assert_eq!(e.error_type.unwrap(), ErrorType::InvalidRequest); + } + _ => panic!("unexpected error"), + } + } + + #[tokio::test] + async fn can_get_multiple_institutions() { + let client = Builder::new().with_credentials(credentials()).build(); + let res = client + .get_institutions(&InstitutionsGetRequest { + count: 10, + offset: 0, + country_codes: &["US"], + options: None, + }) + .await + .unwrap(); + + insta::assert_json_snapshot!(res); + } + + #[tokio::test] + async fn can_fetch_single_institution() { + let client = Builder::new().with_credentials(credentials()).build(); + let res = client + .get_institution_by_id(&InstitutionGetRequest { + institution_id: INSTITUTION_ID, + country_codes: &[], + options: None, + }) + .await + .unwrap(); + + insta::assert_json_snapshot!(res); + } + + #[tokio::test] + async fn can_search_institutions() { + let client = Builder::new().with_credentials(credentials()).build(); + let res = client + .search_institutions(&InstitutionsSearchRequest { + query: "Banque Populaire", + country_codes: &[], + products: None, + options: None, + }) + .await + .unwrap(); + + insta::assert_json_snapshot!(res); + } + + #[tokio::test] + async fn can_create_sandbox_pub_token() { + let client = Builder::new().with_credentials(credentials()).build(); + let public_token = client + .create_public_token(CreatePublicTokenRequest { + institution_id: INSTITUTION_ID, + initial_products: &["assets", "auth", "balance"], + options: None, + }) + .await + .unwrap(); + + let res = client.exchange_public_token(public_token).await.unwrap(); + assert!(!res.access_token.is_empty()); + // Should succeed. + client.reset_login(res.access_token).await.unwrap(); + } + + #[tokio::test] + async fn can_fetch_accounts_with_token() { + let client = Builder::new().with_credentials(credentials()).build(); + let public_token = client + .create_public_token(CreatePublicTokenRequest { + institution_id: INSTITUTION_ID, + initial_products: &["assets", "auth", "balance"], + options: None, + }) + .await + .unwrap(); + + let res = client.exchange_public_token(public_token).await.unwrap(); + assert!(!res.access_token.is_empty()); + let accounts = client.accounts(res.access_token).await.unwrap(); + + insta::assert_json_snapshot!(accounts, { + "[].account_id" => "[account_id]" + }); + } + + #[tokio::test] + async fn can_modify_items() { + let client = Builder::new().with_credentials(credentials()).build(); + let public_token = client + .create_public_token(CreatePublicTokenRequest { + institution_id: INSTITUTION_ID, + initial_products: &["assets", "auth", "balance"], + options: None, + }) + .await + .unwrap(); + + let res = client.exchange_public_token(public_token).await.unwrap(); + assert!(!res.access_token.is_empty()); + let item = client.item(&res.access_token).await.unwrap(); + + insta::assert_json_snapshot!(item, { + ".item_id" => "[item_id]" + }); + + // Should succeed. + client.item_del(res.access_token).await.unwrap(); + } + + #[tokio::test] + async fn can_create_link_token() { + let client = Builder::new().with_credentials(credentials()).build(); + let create_res = client + .create_link_token(&CreateLinkTokenRequest { + client_name: "test_client", + user: LinkUser::new("test-user"), + language: "en", + country_codes: &["US"], + products: &["transactions"], + ..CreateLinkTokenRequest::default() + }) + .await + .unwrap(); + + assert!(!create_res.link_token.is_empty()); + // Check that we can read back the token we created. + let res = client + .link_token(&GetLinkTokenRequest { + link_token: &create_res.link_token, + }) + .await + .unwrap(); + assert_eq!(create_res.link_token, res.link_token); + } + + #[tokio::test] + async fn can_read_transactions() { + let client = Builder::new().with_credentials(credentials()).build(); + let public_token = client + .create_public_token(CreatePublicTokenRequest { + institution_id: INSTITUTION_ID, + initial_products: &["assets", "auth", "balance", "transactions"], + options: None, + }) + .await + .unwrap(); + + let res = client.exchange_public_token(public_token).await.unwrap(); + assert!(!res.access_token.is_empty()); + // TODO(allancalix): Transaction isn't available immediately after the + // token is created, we probably want to find a better way to find out if + // the product is ready. + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + let res = client + .transactions(&GetTransactionsRequest { + access_token: res.access_token.as_str(), + start_date: "2021-09-01", + end_date: "2021-09-05", + options: None, + }) + .await + .unwrap(); + + insta::assert_json_snapshot!(res.transactions, { + "[].transaction_id" => "[transaction_id]", + "[].account_id" => "[account_id]", + }); + } + + #[tokio::test] + async fn can_drain_transaction_stream() { + let client = Builder::new().with_credentials(credentials()).build(); + let public_token = client + .create_public_token(CreatePublicTokenRequest { + institution_id: INSTITUTION_ID, + initial_products: &["assets", "auth", "balance", "transactions"], + options: None, + }) + .await + .unwrap(); + + let res = client.exchange_public_token(public_token).await.unwrap(); + assert!(!res.access_token.is_empty()); + // TODO(allancalix): Transaction isn't available immediately after the + // token is created, we probably want to find a better way to find out if + // the product is ready. + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + let req = GetTransactionsRequest { + access_token: res.access_token.as_str(), + start_date: "2019-09-01", + end_date: "2021-09-05", + options: Some(GetTransactionsOptions { + count: Some(10), + offset: Some(5), + account_ids: None, + include_original_description: None, + }), + }; + let iter = client.transactions_iter(req); + pin_mut!(iter); + + let xacts = iter + .fold(vec![], |mut acc, x| async move { + acc.append(&mut x.unwrap()); + acc + }) + .await; + assert_eq!(xacts.len(), 7); + } + + #[tokio::test] + async fn can_read_categories() { + let client = Builder::new().with_credentials(credentials()).build(); + let res = client.categories(&GetCategoriesRequest {}).await.unwrap(); + insta::assert_json_snapshot!(res.categories); + } + + #[tokio::test] + async fn can_refresh_transactions() { + let client = Builder::new().with_credentials(credentials()).build(); + let public_token = client + .create_public_token(CreatePublicTokenRequest { + institution_id: INSTITUTION_ID, + initial_products: &["assets", "auth", "balance", "transactions"], + options: None, + }) + .await + .unwrap(); + let res = client.exchange_public_token(public_token).await.unwrap(); + assert!(!res.access_token.is_empty()); + + client + .refresh_transactions(&RefreshTransactionsRequest { + access_token: res.access_token, + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn can_read_auth() { + let client = Builder::new().with_credentials(credentials()).build(); + let public_token = client + .create_public_token(CreatePublicTokenRequest { + institution_id: INSTITUTION_ID, + initial_products: &["assets", "auth", "balance", "transactions"], + options: None, + }) + .await + .unwrap(); + let res = client.exchange_public_token(public_token).await.unwrap(); + assert!(!res.access_token.is_empty()); + + let res = client + .auth(&GetAuthRequest { + access_token: res.access_token, + options: None, + }) + .await + .unwrap(); + insta::assert_json_snapshot!(res, { + ".accounts[].account_id" => "[account_id]", + ".numbers.ach[].account_id" => "[ach_account_id]", + ".request_id" => "[request_id]", + ".item.item_id" => "[item_id]", + }); + } + + #[tokio::test] + async fn can_read_identity() { + let client = Builder::new().with_credentials(credentials()).build(); + let public_token = client + .create_public_token(CreatePublicTokenRequest { + institution_id: INSTITUTION_ID, + initial_products: &["assets", "auth", "balance", "transactions"], + options: None, + }) + .await + .unwrap(); + let res = client.exchange_public_token(public_token).await.unwrap(); + assert!(!res.access_token.is_empty()); + + let res = client + .identity(&GetIdentityRequest { + access_token: res.access_token, + options: None, + }) + .await + .unwrap(); + insta::assert_json_snapshot!(res, { + ".accounts[].account_id" => "[account_id]", + ".item.item_id" => "[item_id]", + ".request_id" => "[request_id]", + }); + } + + #[tokio::test] + async fn can_invalidate_access_token() { + let client = Builder::new().with_credentials(credentials()).build(); + let public_token = client + .create_public_token(CreatePublicTokenRequest { + institution_id: INSTITUTION_ID, + initial_products: &["assets", "auth", "balance", "transactions"], + options: None, + }) + .await + .unwrap(); + let create_res = client.exchange_public_token(public_token).await.unwrap(); + assert!(!create_res.access_token.is_empty()); + + let res = client + .invalidate_access_token(&InvalidateAccessTokenRequest { + access_token: &create_res.access_token, + }) + .await + .unwrap(); + // A new access token should be returned. + assert_ne!(res.new_access_token, create_res.access_token); + } + + #[tokio::test] + async fn can_fire_webhook() { + let client = Builder::new().with_credentials(credentials()).build(); + let public_token = client + .create_public_token(CreatePublicTokenRequest { + institution_id: INSTITUTION_ID, + initial_products: &["assets", "auth", "balance", "transactions"], + options: Some(CreatePublicTokenOptions { + webhook: Some("localhost:3000"), + override_username: None, + override_password: None, + transactions: None, + }), + }) + .await + .unwrap(); + let res = client.exchange_public_token(public_token).await.unwrap(); + let res = client + .fire_webhook(&FireWebhookRequest { + access_token: res.access_token.as_str(), + webhook_code: WebhookCode::DefaultUpdate, + }) + .await + .unwrap(); + + assert!(res.webhook_fired); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8fddd62 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,53 @@ +/*! +# Description +**rplaid** is an async client for the [Plaid API](https://plaid.com/docs/api/). +With minimal features, the client is meant to be extensible and lightweight. +Additional features can be enabled to improve ergonomics of the API at the +cost of additional dependencies. + +The goal is to provide expressive bindings that provide sensible defaults where +possible for ease of use. + +See official [API docs](https://plaid.com/docs/) for more information about +endpoints or specific products. + +__These bindings are not official Plaid bindings.__ + +# Glossary +* Item - A Item represents a connection to a single financial instution. + Typically links are associated with a pair of credentials and an + `access_token`. Items are associated to one or more accounts. + +* Link - Link is a client-side component that Plaid provides to link to accounts. + See https://plaid.com/docs/link/#introduction-to-link for more + information. + +* Account - An account is a financial account that is linked to an Item. An item, + or financial institution, may have multiple accounts for a single + user (e.g. a checking account and a credit account). + +* Product - Entities with services offered by Plaid, see + https://plaid.com/docs/api/products/ for more information. + +# Features +* Idiomatic futures generator for easily reading multiple pages of transactions. +* Extensible HttpClient interfaces supports multiple HTTP clients with minimal + effort (surf, H1, and reqwest). The trait can also be implemented to have full + control over the HTTP client used. +* Rust types, including variant types, for most API return types. + +# Limitations +Some endpoints are production specific or beta products and are not yet +supported by the client. + +For a breakdown of endpoint support visit: +https://docs.google.com/spreadsheets/d/1xqUXdfllo37Rx5MVrQODbVqNQvuktiCVL5Uh8y9mYYw +*/ +#[deny(missing_docs)] +/// Exposes primary client type for sending requests to Plaid's API. +pub mod client; +/// Data types for entities returned by Plaid API endpoints. +pub mod model; + +/// Re-exports HttpClient trait for implementing a custom HTTP client. +pub use http_client::HttpClient; diff --git a/src/model/account.rs b/src/model/account.rs new file mode 100644 index 0000000..3b9d7b5 --- /dev/null +++ b/src/model/account.rs @@ -0,0 +1,70 @@ +use super::*; + +#[derive(Debug, Serialize)] +pub struct GetAccountsRequest<'a, T: AsRef> { + pub access_token: T, + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option>, +} + +#[derive(Debug, Serialize)] +pub struct GetAccountsRequestFilter<'a, T: AsRef> { + pub account_ids: &'a [T], +} + +impl + HttpSerialize> Endpoint for GetAccountsRequest<'_, T> { + type Response = GetAccountsResponse; + + fn path(&self) -> String { + "/accounts/get".into() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetAccountsResponse { + pub accounts: Vec, + pub item: Item, + pub request_id: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Account { + pub account_id: String, + pub balances: Balance, + pub mask: Option, + pub name: String, + pub official_name: Option, + /// One of investment | credit | depository | loan | brokerage | other. + pub r#type: AccountType, + pub subtype: Option, + // This field is listed on the documentation for this type as non-nullable + // but doesn't appear to be returned in payloads. + // https://plaid.com/docs/api/accounts/#accounts-get-response-verification-status_accounts + #[serde(skip_serializing_if = "Option::is_none")] + pub verification_status: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq)] +pub enum AccountType { + #[serde(rename = "investment")] + Investment, + #[serde(rename = "credit")] + Credit, + #[serde(rename = "depository")] + Depository, + #[serde(rename = "loan")] + Loan, + #[serde(rename = "brokerage")] + Brokerage, + #[serde(rename = "other")] + Other, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Balance { + pub available: Option, + pub current: Option, + pub iso_current_code: Option, + pub limit: Option, + pub unofficial_currency_code: Option, +} diff --git a/src/model/auth.rs b/src/model/auth.rs new file mode 100644 index 0000000..299bd55 --- /dev/null +++ b/src/model/auth.rs @@ -0,0 +1,67 @@ +use super::*; + +#[derive(Debug, Serialize)] +pub struct GetAuthRequest<'a, T: AsRef> { + pub access_token: T, + pub options: Option>, +} + +#[derive(Debug, Serialize)] +pub struct GetAuthRequestOptions<'a, T: AsRef> { + pub account_ids: &'a [T], +} + +impl + HttpSerialize> Endpoint for GetAuthRequest<'_, T> { + type Response = GetAuthResponse; + fn path(&self) -> String { + "/auth/get".into() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetAuthResponse { + pub accounts: Vec, + pub numbers: AccountNumbers, + pub item: Item, + pub request_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AccountNumbers { + pub ach: Vec, + pub eft: Vec, + pub international: Vec, + pub bacs: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ACHAccountNumber { + pub account_id: String, + pub account: String, + pub routing: String, + pub wire_routing: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct EFTAccountNumber { + pub account_id: String, + pub account: String, + pub institution: String, + pub branch: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct InternationalAccountNumber { + pub account_id: String, + /// The International Bank Account Number (IBAN) for the account. + pub iban: String, + /// The Bank Identifier Code (BIC) for the account + pub bic: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BACSAccountNumber { + pub account_id: String, + pub account: String, + pub sort_code: String, +} diff --git a/src/model/balance.rs b/src/model/balance.rs new file mode 100644 index 0000000..84f0ec0 --- /dev/null +++ b/src/model/balance.rs @@ -0,0 +1,28 @@ +use super::*; + +#[derive(Debug, Serialize)] +pub struct AccountBalancesGetRequest<'a, T: AsRef> { + pub access_token: T, + pub options: Option>, +} + +#[derive(Debug, Serialize)] +pub struct AccountBalanceFilter<'a, T: AsRef> { + pub account_ids: &'a [T], + pub min_last_updated_datetime: Option, +} + +impl + HttpSerialize> Endpoint for AccountBalancesGetRequest<'_, T> { + type Response = AccountBalancesGetResponse; + + fn path(&self) -> String { + "/accounts/balance/get".into() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct AccountBalancesGetResponse { + pub accounts: Vec, + pub item: Item, + pub request_id: String, +} diff --git a/src/model/common.rs b/src/model/common.rs new file mode 100644 index 0000000..fceb719 --- /dev/null +++ b/src/model/common.rs @@ -0,0 +1,48 @@ +use super::*; + +pub(crate) trait Endpoint: HttpSerialize { + type Response; + + fn path(&self) -> String; + + fn request(&self, domain: &str) -> http_client::Request { + let mut req = http_client::Request::post(format!("{}{}", domain, self.path()).as_str()); + req.set_body(self.payload()); + + req + } + + fn payload(&self) -> http_types::Body { + http_types::Body::from_json(&self).unwrap() + } +} + +#[derive(thiserror::Error, Debug, Serialize, Deserialize, Default)] +#[error("request failed with code {error_code:?}: {display_message:?}")] +pub struct ErrorResponse { + pub display_message: Option, + pub documentation_url: Option, + pub error_code: Option, + pub error_message: Option, + pub error_type: Option, + pub request_id: Option, + pub status: Option, + pub suggested_action: Option, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ErrorType { + InvalidRequest, + InvalidResult, + InvalidInput, + InstitutionError, + RateLimitExceeded, + ApiError, + ItemError, + AssetReportError, + RecaptchaError, + OauthError, + PaymentError, + BankTransferError, +} diff --git a/src/model/employers.rs b/src/model/employers.rs new file mode 100644 index 0000000..6b6dc3a --- /dev/null +++ b/src/model/employers.rs @@ -0,0 +1,39 @@ +use super::*; + +#[derive(Debug, Serialize)] +pub struct SearchEmployerRequest<'a, T: AsRef> { + pub query: T, + /// This field must be set to deposit_switch. + pub products: &'a [T], +} + +impl + HttpSerialize> Endpoint for SearchEmployerRequest<'_, T> { + type Response = SearchEmployerResponse; + + fn path(&self) -> String { + "/employers/search".into() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SearchEmployerResponse { + pub employers: Vec, + pub request_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Employer { + pub employer_id: String, + pub name: String, + pub address: Option

, + pub confidence_score: f32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Address { + pub city: String, + pub region: Option, + pub street: String, + pub postal_code: Option, + pub country: Option, +} diff --git a/src/model/identity.rs b/src/model/identity.rs new file mode 100644 index 0000000..fe210fe --- /dev/null +++ b/src/model/identity.rs @@ -0,0 +1,27 @@ +use super::*; + +#[derive(Debug, Serialize)] +pub struct GetIdentityRequest<'a, T: AsRef> { + pub access_token: T, + pub options: Option>, +} + +#[derive(Debug, Serialize)] +pub struct IdentityFilter<'a, T: AsRef> { + pub account_ids: &'a [T], +} + +impl + HttpSerialize> Endpoint for GetIdentityRequest<'_, T> { + type Response = GetIdentityResponse; + + fn path(&self) -> String { + "/identity/get".into() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetIdentityResponse { + pub accounts: Vec, + pub item: Item, + pub request_id: String, +} diff --git a/src/model/institutions.rs b/src/model/institutions.rs new file mode 100644 index 0000000..c3f43a4 --- /dev/null +++ b/src/model/institutions.rs @@ -0,0 +1,117 @@ +use super::*; + +#[derive(Debug, Serialize)] +pub struct InstitutionsSearchRequest<'a, T: AsRef> { + pub query: T, + pub products: Option<&'a [T]>, + pub country_codes: &'a [T], + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option>, +} + +#[derive(Debug, Serialize)] +pub struct SearchInstitutionFilter> { + pub oauth: Option, + pub include_optional_metadata: Option, + pub include_auth_metadata: Option, + pub include_payment_initiation_metadata: Option, + pub payment_initiation: Option>, +} + +#[derive(Debug, Serialize)] +pub struct PaymentInitiationFilter> { + pub payment_id: Option, +} + +impl + HttpSerialize> Endpoint for InstitutionsSearchRequest<'_, T> { + type Response = InstitutionSearchResponse; + fn path(&self) -> String { + "/institutions/search".into() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct InstitutionSearchResponse { + pub institutions: Vec, +} + +#[derive(Debug, Serialize)] +pub struct InstitutionGetRequest<'a, T: AsRef> { + pub institution_id: T, + pub country_codes: &'a [T], + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option, +} + +#[derive(Debug, Serialize)] +pub struct GetInstitutionFilter { + pub include_optional_metadata: Option, + pub include_status: Option, + pub include_auth_metadata: Option, + pub include_payment_initiation_metadata: Option, +} + +impl + HttpSerialize> Endpoint for InstitutionGetRequest<'_, T> { + type Response = InstitutionGetResponse; + fn path(&self) -> String { + "/institutions/get_by_id".into() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct InstitutionGetResponse { + pub institution: Institution, +} + +#[derive(Debug, Serialize)] +pub struct InstitutionsGetRequest<'a, T: AsRef> { + pub count: usize, + pub offset: usize, + pub country_codes: &'a [T], + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option>, +} + +#[derive(Debug, Serialize)] +pub struct GetInstitutionsFilter<'a, T: AsRef> { + /// Filter the Institutions based on which products they support. + pub products: &'a [T], + /// Specify an array of routing numbers to filter institutions. The + /// response will only return institutions that match all of the routing + /// numbers in the array. + pub routing_numbers: &'a [T], + pub oauth: bool, + pub include_optional_metadata: bool, + /// When true, returns metadata related to the Auth product indicating + /// which auth methods are supported. Defaults to false. + pub include_auth_metadata: bool, + /// When true, returns metadata related to the Payment Initiation product + /// indicating which payment configurations are supported. Defaults to + /// false. + pub include_payment_initiation_metadata: bool, +} + +impl + HttpSerialize> Endpoint for InstitutionsGetRequest<'_, T> { + type Response = InstitutionsGetResponse; + fn path(&self) -> String { + "/institutions/get".into() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct InstitutionsGetResponse { + pub institutions: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Institution { + pub institution_id: String, + pub name: String, + pub products: Vec, + pub country_codes: Vec, + pub url: Option, + pub primary_color: Option, + pub logo: Option, + pub routing_numbers: Option>, + pub oauth: bool, +} diff --git a/src/model/item.rs b/src/model/item.rs new file mode 100644 index 0000000..2a94700 --- /dev/null +++ b/src/model/item.rs @@ -0,0 +1,93 @@ +use super::*; + +#[derive(Debug, Serialize)] +pub struct GetItemRequest> { + pub access_token: T, +} + +impl + HttpSerialize> Endpoint for GetItemRequest { + type Response = GetItemResponse; + + fn path(&self) -> String { + "/item/get".into() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetItemResponse { + pub item: Item, + pub status: Option, + pub request_id: String, +} + +#[derive(Debug, Serialize)] +pub struct RemoveItemRequest> { + pub access_token: T, +} + +impl + HttpSerialize> Endpoint for RemoveItemRequest { + type Response = RemoveItemResponse; + + fn path(&self) -> String { + "/item/remove".into() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct RemoveItemResponse { + pub request_id: String, +} + +#[derive(Debug, Serialize)] +pub struct UpdateItemWebhookRequest> { + pub access_token: T, + /// The new url to associate with the item. + pub webhook: T, +} + +impl + HttpSerialize> Endpoint for UpdateItemWebhookRequest { + type Response = UpdateItemWebhookResponse; + + fn path(&self) -> String { + "/item/webhook/update".into() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct UpdateItemWebhookResponse { + pub item: Item, + pub request_id: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Item { + pub item_id: String, + pub institution_id: Option, + pub webhook: Option, + pub error: Option, + pub available_products: Vec, + pub billed_products: Vec, + // An RFC 3339 timestamp after which the consent provided by the end user will expire. + pub consent_expiration_time: Option, + pub update_type: String, + pub status: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Status { + pub investments: Option, + pub transactions: Option, + pub last_webhook: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct StatusMessage { + pub last_successful_update: Option, + pub last_failed_update: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct WebhookStatus { + pub sent_at: Option, + pub code_sent: Option, +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..8bfa0c7 --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,28 @@ +mod account; +mod auth; +mod balance; +mod common; +mod employers; +mod identity; +mod institutions; +mod item; +mod sandbox; +mod token; +mod transactions; +mod webhooks; + +use http_types::convert::Serialize as HttpSerialize; +use serde::{Deserialize, Serialize}; + +pub use account::*; +pub use auth::*; +pub use balance::*; +pub use common::*; +pub use employers::*; +pub use identity::*; +pub use institutions::*; +pub use item::*; +pub use sandbox::*; +pub use token::*; +pub use transactions::*; +pub use webhooks::*; diff --git a/src/model/sandbox.rs b/src/model/sandbox.rs new file mode 100644 index 0000000..768fff4 --- /dev/null +++ b/src/model/sandbox.rs @@ -0,0 +1,104 @@ +use super::*; + +#[derive(Debug, Serialize, Default)] +pub struct CreatePublicTokenRequest<'a, T: AsRef> { + pub institution_id: T, + pub initial_products: &'a [T], + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option>, +} + +#[derive(Debug, Serialize)] +pub struct CreatePublicTokenOptions> { + pub webhook: Option, + /// Default username is "user_good". + pub override_username: Option, + /// Default password is "pass_good". + pub override_password: Option, + pub transactions: Option>, +} + +#[derive(Debug, Serialize)] +pub struct CreatePublicTokenOptionsTransactions> { + pub start_date: Option, + pub end_date: Option, +} + +impl + HttpSerialize> Endpoint for CreatePublicTokenRequest<'_, T> { + type Response = CreatePublicTokenResponse; + + fn path(&self) -> String { + "/sandbox/public_token/create".into() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CreatePublicTokenResponse { + pub public_token: String, +} + +#[derive(Debug, Serialize)] +pub struct ResetLoginRequest> { + pub access_token: T, +} + +impl + HttpSerialize> Endpoint for ResetLoginRequest { + type Response = ResetLoginResponse; + + fn path(&self) -> String { + "/sandbox/item/reset_login".into() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ResetLoginResponse { + pub reset_login: bool, +} + +#[derive(Debug, Serialize)] +pub struct SetVerificationStatusRequest> { + pub access_token: T, + pub account_id: T, + /// One of automatically_verified or verification_required + pub verification_status: T, +} + +impl + HttpSerialize> Endpoint for SetVerificationStatusRequest { + type Response = SetVerificationStatusResponse; + + fn path(&self) -> String { + "/sandbox/item/set_verification_status".into() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SetVerificationStatusResponse { + pub reset_login: bool, +} + +#[derive(Debug, Serialize)] +pub struct FireWebhookRequest> { + pub access_token: T, + /// One of DEFAULT_UPDATE. + pub webhook_code: WebhookCode, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +pub enum WebhookCode { + #[serde(rename = "DEFAULT_UPDATE")] + DefaultUpdate, +} + +impl + HttpSerialize> Endpoint for FireWebhookRequest { + type Response = FireWebhookResponse; + + fn path(&self) -> String { + "/sandbox/item/fire_webhook".into() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FireWebhookResponse { + pub webhook_fired: bool, + pub request_id: String, +} diff --git a/src/model/token.rs b/src/model/token.rs new file mode 100644 index 0000000..402fdd7 --- /dev/null +++ b/src/model/token.rs @@ -0,0 +1,157 @@ +use super::*; + +#[derive(Debug, Serialize)] +pub struct ExchangePublicTokenRequest> { + pub public_token: T, +} + +impl + HttpSerialize> Endpoint for ExchangePublicTokenRequest { + type Response = ExchangePublicTokenResponse; + + fn path(&self) -> String { + "/item/public_token/exchange".into() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ExchangePublicTokenResponse { + pub access_token: String, + pub item_id: String, + pub request_id: String, +} + +#[derive(Debug, Serialize, Default)] +pub struct CreateLinkTokenRequest<'a, T: AsRef> { + pub client_name: T, + pub language: T, + pub country_codes: &'a [T], + pub user: LinkUser, + pub products: &'a [T], + pub webhook: Option, + pub access_token: Option, + pub link_customization_name: Option, + pub redirect_uri: Option, + pub android_package_name: Option, + pub account_filters: Option>, + pub eu_config: Option, + pub payment_initiation: Option>, + pub deposit_switch: Option>, + pub income_verification: Option>, + pub auth: Option>, + pub institution_id: Option, +} + +impl + HttpSerialize> Endpoint for CreateLinkTokenRequest<'_, T> { + type Response = CreateLinkTokenResponse; + + fn path(&self) -> String { + "/link/token/create".into() + } +} + +#[derive(Debug, Serialize, Default)] +pub struct LinkAuth> { + flow_type: T, +} + +#[derive(Debug, Serialize, Default)] +pub struct IncomeVerification> { + income_verification_id: T, + asset_report_id: Option, +} + +#[derive(Debug, Serialize, Default)] +pub struct DepositSwitchOptions> { + deposit_switch_id: T, +} + +#[derive(Debug, Serialize, Default)] +pub struct PaymentInitiation> { + payment_id: T, +} + +#[derive(Debug, Serialize, Default)] +pub struct LinkUser> { + pub client_user_id: T, + pub legal_name: Option, + pub phone_number: Option, + pub phone_number_verified_time: Option, + pub email_address: Option, + pub email_address_verified_time: Option, + pub ssn: Option, + pub date_of_birth: Option, +} + +#[derive(Debug, Serialize, Default)] +pub struct AccountFilters<'a, T: AsRef> { + depository: Option>, + credit: Option>, + loan: Option>, + investment: Option>, +} + +#[derive(Debug, Serialize, Default)] +pub struct EUConfig { + headless: Option, +} + +#[derive(Debug, Serialize, Default)] +pub struct AccountFilter<'a, T: AsRef> { + account_subtypes: &'a [T], +} + +impl + Default> LinkUser { + pub fn new(user_id: T) -> Self { + Self { + client_user_id: user_id, + ..Self::default() + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CreateLinkTokenResponse { + pub link_token: String, + pub expiration: String, + pub request_id: String, +} + +#[derive(Debug, Serialize)] +pub struct GetLinkTokenRequest> { + pub link_token: T, +} + +impl + HttpSerialize> Endpoint for GetLinkTokenRequest { + type Response = GetLinkTokenResponse; + + fn path(&self) -> String { + "/link/token/get".into() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetLinkTokenResponse { + pub link_token: String, + pub expiration: Option, + pub created_at: Option, + pub request_id: String, +} + +#[derive(Debug, Serialize)] +pub struct InvalidateAccessTokenRequest> { + pub access_token: T, +} + +impl + HttpSerialize> Endpoint for InvalidateAccessTokenRequest { + type Response = InvalidateAccessTokenResponse; + + fn path(&self) -> String { + "/item/access_token/invalidate".into() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct InvalidateAccessTokenResponse { + pub new_access_token: String, + pub request_id: String, +} diff --git a/src/model/transactions.rs b/src/model/transactions.rs new file mode 100644 index 0000000..feee09f --- /dev/null +++ b/src/model/transactions.rs @@ -0,0 +1,161 @@ +use super::*; + +#[derive(Debug, Serialize, Copy, Clone)] +pub struct GetTransactionsRequest> { + pub access_token: T, + /// A string date with the format YYYY-MM-DD. Start date is inclusive. + pub start_date: T, + /// A string date with the format YYYY-MM-DD. End date is inclusive. + pub end_date: T, + pub options: Option>, +} + +impl + HttpSerialize> Endpoint for GetTransactionsRequest { + type Response = GetTransactionsResponse; + + fn path(&self) -> String { + "/transactions/get".into() + } +} + +#[derive(Debug, Serialize, Copy, Clone)] +pub struct GetTransactionsOptions> { + pub account_ids: Option, + pub count: Option, + pub offset: Option, + pub include_original_description: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetTransactionsResponse { + pub accounts: Vec, + pub transactions: Vec, + pub total_transactions: usize, + pub item: Item, + pub request_id: String, +} + +#[derive(Debug, Serialize, Copy, Clone)] +pub struct RefreshTransactionsRequest> { + pub access_token: T, +} + +impl + HttpSerialize> Endpoint for RefreshTransactionsRequest { + type Response = RefreshTransactionsResponse; + + fn path(&self) -> String { + "/transactions/refresh".into() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct RefreshTransactionsResponse { + pub request_id: String, +} + +#[derive(Debug, Serialize, Copy, Clone)] +pub struct GetCategoriesRequest {} + +impl Endpoint for GetCategoriesRequest { + type Response = GetCategoriesResponse; + + fn path(&self) -> String { + "/categories/get".into() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetCategoriesResponse { + pub categories: Vec, + pub request_id: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Category { + category_id: String, + group: String, + hierarchy: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Transaction { + /// DEPRECATED: do not depend on this type, it will be deleted in the future. + pub transaction_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_transaction_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub category_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub category: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_meta: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub account_owner: Option, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub original_description: Option, + pub account_id: String, + pub amount: f64, + #[serde(skip_serializing_if = "Option::is_none")] + pub iso_currency_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub unofficial_currency_code: Option, + pub date: String, + pub pending: bool, + pub transaction_id: String, + pub payment_channel: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub merchant_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub authorized_date: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub authorized_datetime: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub datetime: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub check_number: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_code: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct TransactionLocation { + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub region: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub postal_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub country: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub lat: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub lon: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub store_number: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct PaymentMetadata { + #[serde(skip_serializing_if = "Option::is_none")] + pub reference_number: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ppd_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payee: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub by_order_of: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_method: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_processor: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} diff --git a/src/model/webhooks.rs b/src/model/webhooks.rs new file mode 100644 index 0000000..0726ff1 --- /dev/null +++ b/src/model/webhooks.rs @@ -0,0 +1,23 @@ +use super::*; + +#[derive(Debug, Serialize, Copy, Clone)] +pub struct GetWebhookVerificationKeyRequest> { + pub key_id: T, +} + +impl + HttpSerialize> Endpoint for GetWebhookVerificationKeyRequest { + type Response = GetWebhookVerificationKeyResponse; + + fn path(&self) -> String { + "/webhook_verification_key/get".into() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetWebhookVerificationKeyResponse { + // TODO(allancalix): This is obviously not right, but maybe it's worth + // bringing in a real JWT type to return here? Creating a JWT type to + // return here doesn't feel like the right answer. + pub key: std::collections::HashMap, + pub request_id: String, +} diff --git a/src/snapshots/rplaid__client__tests__can_fetch_accounts_with_token.snap b/src/snapshots/rplaid__client__tests__can_fetch_accounts_with_token.snap new file mode 100644 index 0000000..ca49b9a --- /dev/null +++ b/src/snapshots/rplaid__client__tests__can_fetch_accounts_with_token.snap @@ -0,0 +1,142 @@ +--- +source: src/client.rs +expression: accounts + +--- +[ + { + "account_id": "[account_id]", + "balances": { + "available": 100.0, + "current": 110.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "0000", + "name": "Plaid Checking", + "official_name": "Plaid Gold Standard 0% Interest Checking", + "type": "depository", + "subtype": "checking" + }, + { + "account_id": "[account_id]", + "balances": { + "available": 200.0, + "current": 210.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "1111", + "name": "Plaid Saving", + "official_name": "Plaid Silver Standard 0.1% Interest Saving", + "type": "depository", + "subtype": "savings" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 1000.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "2222", + "name": "Plaid CD", + "official_name": "Plaid Bronze Standard 0.2% Interest CD", + "type": "depository", + "subtype": "cd" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 410.0, + "iso_current_code": null, + "limit": 2000.0, + "unofficial_currency_code": null + }, + "mask": "3333", + "name": "Plaid Credit Card", + "official_name": "Plaid Diamond 12.5% APR Interest Credit Card", + "type": "credit", + "subtype": "credit card" + }, + { + "account_id": "[account_id]", + "balances": { + "available": 43200.0, + "current": 43200.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "4444", + "name": "Plaid Money Market", + "official_name": "Plaid Platinum Standard 1.85% Interest Money Market", + "type": "depository", + "subtype": "money market" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 320.76, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "5555", + "name": "Plaid IRA", + "official_name": null, + "type": "investment", + "subtype": "ira" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 23631.9805, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "6666", + "name": "Plaid 401k", + "official_name": null, + "type": "investment", + "subtype": "401k" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 65262.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "7777", + "name": "Plaid Student Loan", + "official_name": null, + "type": "loan", + "subtype": "student" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 56302.06, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "8888", + "name": "Plaid Mortgage", + "official_name": null, + "type": "loan", + "subtype": "mortgage" + } +] diff --git a/src/snapshots/rplaid__client__tests__can_fetch_single_institution.snap b/src/snapshots/rplaid__client__tests__can_fetch_single_institution.snap new file mode 100644 index 0000000..87aa186 --- /dev/null +++ b/src/snapshots/rplaid__client__tests__can_fetch_single_institution.snap @@ -0,0 +1,24 @@ +--- +source: src/client.rs +expression: res + +--- +{ + "institution_id": "ins_129571", + "name": "Bank21", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity" + ], + "country_codes": [ + "US" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": false +} diff --git a/src/snapshots/rplaid__client__tests__can_get_multiple_institutions.snap b/src/snapshots/rplaid__client__tests__can_get_multiple_institutions.snap new file mode 100644 index 0000000..2a0c362 --- /dev/null +++ b/src/snapshots/rplaid__client__tests__can_get_multiple_institutions.snap @@ -0,0 +1,206 @@ +--- +source: src/client.rs +expression: res + +--- +[ + { + "institution_id": "ins_129571", + "name": "Bank21", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity" + ], + "country_codes": [ + "US" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": false + }, + { + "institution_id": "ins_122874", + "name": "Banque Populaire - Bourgogne Franche Comté", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122875", + "name": "Banque Populaire - Auvergne et Rhône-Alpes", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122876", + "name": "Banque Populaire - Rives de Paris", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122877", + "name": "Banque Populaire - Val de France", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122878", + "name": "Banque Populaire - du Nord", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122879", + "name": "Banque Populaire - Sud", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122880", + "name": "Banque Populaire - Aquitaine Centre Atlantique", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122881", + "name": "Banque Populaire - Alsace Lorraine Champagne", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122882", + "name": "Banque Populaire - Occitane", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + } +] diff --git a/src/snapshots/rplaid__client__tests__can_modify_items.snap b/src/snapshots/rplaid__client__tests__can_modify_items.snap new file mode 100644 index 0000000..9905cd1 --- /dev/null +++ b/src/snapshots/rplaid__client__tests__can_modify_items.snap @@ -0,0 +1,23 @@ +--- +source: src/client.rs +expression: item + +--- +{ + "item_id": "[item_id]", + "institution_id": "ins_129571", + "webhook": "", + "error": null, + "available_products": [ + "balance", + "identity", + "transactions" + ], + "billed_products": [ + "assets", + "auth" + ], + "consent_expiration_time": null, + "update_type": "background", + "status": null +} diff --git a/src/snapshots/rplaid__client__tests__can_read_auth.snap b/src/snapshots/rplaid__client__tests__can_read_auth.snap new file mode 100644 index 0000000..3289a88 --- /dev/null +++ b/src/snapshots/rplaid__client__tests__can_read_auth.snap @@ -0,0 +1,182 @@ +--- +source: src/client.rs +expression: res + +--- +{ + "accounts": [ + { + "account_id": "[account_id]", + "balances": { + "available": 100.0, + "current": 110.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "0000", + "name": "Plaid Checking", + "official_name": "Plaid Gold Standard 0% Interest Checking", + "type": "depository", + "subtype": "checking" + }, + { + "account_id": "[account_id]", + "balances": { + "available": 200.0, + "current": 210.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "1111", + "name": "Plaid Saving", + "official_name": "Plaid Silver Standard 0.1% Interest Saving", + "type": "depository", + "subtype": "savings" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 1000.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "2222", + "name": "Plaid CD", + "official_name": "Plaid Bronze Standard 0.2% Interest CD", + "type": "depository", + "subtype": "cd" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 410.0, + "iso_current_code": null, + "limit": 2000.0, + "unofficial_currency_code": null + }, + "mask": "3333", + "name": "Plaid Credit Card", + "official_name": "Plaid Diamond 12.5% APR Interest Credit Card", + "type": "credit", + "subtype": "credit card" + }, + { + "account_id": "[account_id]", + "balances": { + "available": 43200.0, + "current": 43200.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "4444", + "name": "Plaid Money Market", + "official_name": "Plaid Platinum Standard 1.85% Interest Money Market", + "type": "depository", + "subtype": "money market" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 320.76, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "5555", + "name": "Plaid IRA", + "official_name": null, + "type": "investment", + "subtype": "ira" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 23631.9805, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "6666", + "name": "Plaid 401k", + "official_name": null, + "type": "investment", + "subtype": "401k" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 65262.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "7777", + "name": "Plaid Student Loan", + "official_name": null, + "type": "loan", + "subtype": "student" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 56302.06, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "8888", + "name": "Plaid Mortgage", + "official_name": null, + "type": "loan", + "subtype": "mortgage" + } + ], + "numbers": { + "ach": [ + { + "account_id": "[ach_account_id]", + "account": "1111222233330000", + "routing": "011401533", + "wire_routing": "021000021" + }, + { + "account_id": "[ach_account_id]", + "account": "1111222233331111", + "routing": "011401533", + "wire_routing": "021000021" + } + ], + "eft": [], + "international": [], + "bacs": [] + }, + "item": { + "item_id": "[item_id]", + "institution_id": "ins_129571", + "webhook": "", + "error": null, + "available_products": [ + "balance", + "identity" + ], + "billed_products": [ + "assets", + "auth", + "transactions" + ], + "consent_expiration_time": null, + "update_type": "background", + "status": null + }, + "request_id": "[request_id]" +} diff --git a/src/snapshots/rplaid__client__tests__can_read_categories.snap b/src/snapshots/rplaid__client__tests__can_read_categories.snap new file mode 100644 index 0000000..a6d7dad --- /dev/null +++ b/src/snapshots/rplaid__client__tests__can_read_categories.snap @@ -0,0 +1,5151 @@ +--- +source: src/client.rs +expression: res.categories + +--- +[ + { + "category_id": "10000000", + "group": "special", + "hierarchy": [ + "Bank Fees" + ] + }, + { + "category_id": "10001000", + "group": "special", + "hierarchy": [ + "Bank Fees", + "Overdraft" + ] + }, + { + "category_id": "10002000", + "group": "special", + "hierarchy": [ + "Bank Fees", + "ATM" + ] + }, + { + "category_id": "10003000", + "group": "special", + "hierarchy": [ + "Bank Fees", + "Late Payment" + ] + }, + { + "category_id": "10004000", + "group": "special", + "hierarchy": [ + "Bank Fees", + "Fraud Dispute" + ] + }, + { + "category_id": "10005000", + "group": "special", + "hierarchy": [ + "Bank Fees", + "Foreign Transaction" + ] + }, + { + "category_id": "10006000", + "group": "special", + "hierarchy": [ + "Bank Fees", + "Wire Transfer" + ] + }, + { + "category_id": "10007000", + "group": "special", + "hierarchy": [ + "Bank Fees", + "Insufficient Funds" + ] + }, + { + "category_id": "10008000", + "group": "special", + "hierarchy": [ + "Bank Fees", + "Cash Advance" + ] + }, + { + "category_id": "10009000", + "group": "special", + "hierarchy": [ + "Bank Fees", + "Excess Activity" + ] + }, + { + "category_id": "11000000", + "group": "special", + "hierarchy": [ + "Cash Advance" + ] + }, + { + "category_id": "12000000", + "group": "place", + "hierarchy": [ + "Community" + ] + }, + { + "category_id": "12001000", + "group": "place", + "hierarchy": [ + "Community", + "Animal Shelter" + ] + }, + { + "category_id": "12002000", + "group": "place", + "hierarchy": [ + "Community", + "Assisted Living Services" + ] + }, + { + "category_id": "12002001", + "group": "place", + "hierarchy": [ + "Community", + "Assisted Living Services", + "Facilities and Nursing Homes" + ] + }, + { + "category_id": "12002002", + "group": "place", + "hierarchy": [ + "Community", + "Assisted Living Services", + "Caretakers" + ] + }, + { + "category_id": "12003000", + "group": "place", + "hierarchy": [ + "Community", + "Cemetery" + ] + }, + { + "category_id": "12004000", + "group": "place", + "hierarchy": [ + "Community", + "Courts" + ] + }, + { + "category_id": "12005000", + "group": "place", + "hierarchy": [ + "Community", + "Day Care and Preschools" + ] + }, + { + "category_id": "12006000", + "group": "place", + "hierarchy": [ + "Community", + "Disabled Persons Services" + ] + }, + { + "category_id": "12007000", + "group": "place", + "hierarchy": [ + "Community", + "Drug and Alcohol Services" + ] + }, + { + "category_id": "12008000", + "group": "place", + "hierarchy": [ + "Community", + "Education" + ] + }, + { + "category_id": "12008001", + "group": "place", + "hierarchy": [ + "Community", + "Education", + "Vocational Schools" + ] + }, + { + "category_id": "12008002", + "group": "place", + "hierarchy": [ + "Community", + "Education", + "Tutoring and Educational Services" + ] + }, + { + "category_id": "12008003", + "group": "place", + "hierarchy": [ + "Community", + "Education", + "Primary and Secondary Schools" + ] + }, + { + "category_id": "12008004", + "group": "place", + "hierarchy": [ + "Community", + "Education", + "Fraternities and Sororities" + ] + }, + { + "category_id": "12008005", + "group": "place", + "hierarchy": [ + "Community", + "Education", + "Driving Schools" + ] + }, + { + "category_id": "12008006", + "group": "place", + "hierarchy": [ + "Community", + "Education", + "Dance Schools" + ] + }, + { + "category_id": "12008007", + "group": "place", + "hierarchy": [ + "Community", + "Education", + "Culinary Lessons and Schools" + ] + }, + { + "category_id": "12008008", + "group": "place", + "hierarchy": [ + "Community", + "Education", + "Computer Training" + ] + }, + { + "category_id": "12008009", + "group": "place", + "hierarchy": [ + "Community", + "Education", + "Colleges and Universities" + ] + }, + { + "category_id": "12008010", + "group": "place", + "hierarchy": [ + "Community", + "Education", + "Art School" + ] + }, + { + "category_id": "12008011", + "group": "place", + "hierarchy": [ + "Community", + "Education", + "Adult Education" + ] + }, + { + "category_id": "12009000", + "group": "place", + "hierarchy": [ + "Community", + "Government Departments and Agencies" + ] + }, + { + "category_id": "12010000", + "group": "place", + "hierarchy": [ + "Community", + "Government Lobbyists" + ] + }, + { + "category_id": "12011000", + "group": "place", + "hierarchy": [ + "Community", + "Housing Assistance and Shelters" + ] + }, + { + "category_id": "12012000", + "group": "place", + "hierarchy": [ + "Community", + "Law Enforcement" + ] + }, + { + "category_id": "12012001", + "group": "place", + "hierarchy": [ + "Community", + "Law Enforcement", + "Police Stations" + ] + }, + { + "category_id": "12012002", + "group": "place", + "hierarchy": [ + "Community", + "Law Enforcement", + "Fire Stations" + ] + }, + { + "category_id": "12012003", + "group": "place", + "hierarchy": [ + "Community", + "Law Enforcement", + "Correctional Institutions" + ] + }, + { + "category_id": "12013000", + "group": "place", + "hierarchy": [ + "Community", + "Libraries" + ] + }, + { + "category_id": "12014000", + "group": "place", + "hierarchy": [ + "Community", + "Military" + ] + }, + { + "category_id": "12015000", + "group": "place", + "hierarchy": [ + "Community", + "Organizations and Associations" + ] + }, + { + "category_id": "12015001", + "group": "place", + "hierarchy": [ + "Community", + "Organizations and Associations", + "Youth Organizations" + ] + }, + { + "category_id": "12015002", + "group": "place", + "hierarchy": [ + "Community", + "Organizations and Associations", + "Environmental" + ] + }, + { + "category_id": "12015003", + "group": "place", + "hierarchy": [ + "Community", + "Organizations and Associations", + "Charities and Non-Profits" + ] + }, + { + "category_id": "12016000", + "group": "place", + "hierarchy": [ + "Community", + "Post Offices" + ] + }, + { + "category_id": "12017000", + "group": "place", + "hierarchy": [ + "Community", + "Public and Social Services" + ] + }, + { + "category_id": "12018000", + "group": "place", + "hierarchy": [ + "Community", + "Religious" + ] + }, + { + "category_id": "12018001", + "group": "place", + "hierarchy": [ + "Community", + "Religious", + "Temple" + ] + }, + { + "category_id": "12018002", + "group": "place", + "hierarchy": [ + "Community", + "Religious", + "Synagogues" + ] + }, + { + "category_id": "12018003", + "group": "place", + "hierarchy": [ + "Community", + "Religious", + "Mosques" + ] + }, + { + "category_id": "12018004", + "group": "place", + "hierarchy": [ + "Community", + "Religious", + "Churches" + ] + }, + { + "category_id": "12019000", + "group": "place", + "hierarchy": [ + "Community", + "Senior Citizen Services" + ] + }, + { + "category_id": "12019001", + "group": "place", + "hierarchy": [ + "Community", + "Senior Citizen Services", + "Retirement" + ] + }, + { + "category_id": "13000000", + "group": "place", + "hierarchy": [ + "Food and Drink" + ] + }, + { + "category_id": "13001000", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Bar" + ] + }, + { + "category_id": "13001001", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Bar", + "Wine Bar" + ] + }, + { + "category_id": "13001002", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Bar", + "Sports Bar" + ] + }, + { + "category_id": "13001003", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Bar", + "Hotel Lounge" + ] + }, + { + "category_id": "13002000", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Breweries" + ] + }, + { + "category_id": "13003000", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Internet Cafes" + ] + }, + { + "category_id": "13004000", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Nightlife" + ] + }, + { + "category_id": "13004001", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Nightlife", + "Strip Club" + ] + }, + { + "category_id": "13004002", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Nightlife", + "Night Clubs" + ] + }, + { + "category_id": "13004003", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Nightlife", + "Karaoke" + ] + }, + { + "category_id": "13004004", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Nightlife", + "Jazz and Blues Cafe" + ] + }, + { + "category_id": "13004005", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Nightlife", + "Hookah Lounges" + ] + }, + { + "category_id": "13004006", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Nightlife", + "Adult Entertainment" + ] + }, + { + "category_id": "13005000", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants" + ] + }, + { + "category_id": "13005001", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Winery" + ] + }, + { + "category_id": "13005002", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Vegan and Vegetarian" + ] + }, + { + "category_id": "13005003", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Turkish" + ] + }, + { + "category_id": "13005004", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Thai" + ] + }, + { + "category_id": "13005005", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Swiss" + ] + }, + { + "category_id": "13005006", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Sushi" + ] + }, + { + "category_id": "13005007", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Steakhouses" + ] + }, + { + "category_id": "13005008", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Spanish" + ] + }, + { + "category_id": "13005009", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Seafood" + ] + }, + { + "category_id": "13005010", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Scandinavian" + ] + }, + { + "category_id": "13005011", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Portuguese" + ] + }, + { + "category_id": "13005012", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Pizza" + ] + }, + { + "category_id": "13005013", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Moroccan" + ] + }, + { + "category_id": "13005014", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Middle Eastern" + ] + }, + { + "category_id": "13005015", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Mexican" + ] + }, + { + "category_id": "13005016", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Mediterranean" + ] + }, + { + "category_id": "13005017", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Latin American" + ] + }, + { + "category_id": "13005018", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Korean" + ] + }, + { + "category_id": "13005019", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Juice Bar" + ] + }, + { + "category_id": "13005020", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Japanese" + ] + }, + { + "category_id": "13005021", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Italian" + ] + }, + { + "category_id": "13005022", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Indonesian" + ] + }, + { + "category_id": "13005023", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Indian" + ] + }, + { + "category_id": "13005024", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Ice Cream" + ] + }, + { + "category_id": "13005025", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Greek" + ] + }, + { + "category_id": "13005026", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "German" + ] + }, + { + "category_id": "13005027", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Gastropub" + ] + }, + { + "category_id": "13005028", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "French" + ] + }, + { + "category_id": "13005029", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Food Truck" + ] + }, + { + "category_id": "13005030", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Fish and Chips" + ] + }, + { + "category_id": "13005031", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Filipino" + ] + }, + { + "category_id": "13005032", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Fast Food" + ] + }, + { + "category_id": "13005033", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Falafel" + ] + }, + { + "category_id": "13005034", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Ethiopian" + ] + }, + { + "category_id": "13005035", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Eastern European" + ] + }, + { + "category_id": "13005036", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Donuts" + ] + }, + { + "category_id": "13005037", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Distillery" + ] + }, + { + "category_id": "13005038", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Diners" + ] + }, + { + "category_id": "13005039", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Dessert" + ] + }, + { + "category_id": "13005040", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Delis" + ] + }, + { + "category_id": "13005041", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Cupcake Shop" + ] + }, + { + "category_id": "13005042", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Cuban" + ] + }, + { + "category_id": "13005043", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Coffee Shop" + ] + }, + { + "category_id": "13005044", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Chinese" + ] + }, + { + "category_id": "13005045", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Caribbean" + ] + }, + { + "category_id": "13005046", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Cajun" + ] + }, + { + "category_id": "13005047", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Cafe" + ] + }, + { + "category_id": "13005048", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Burrito" + ] + }, + { + "category_id": "13005049", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Burgers" + ] + }, + { + "category_id": "13005050", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Breakfast Spot" + ] + }, + { + "category_id": "13005051", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Brazilian" + ] + }, + { + "category_id": "13005052", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Barbecue" + ] + }, + { + "category_id": "13005053", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Bakery" + ] + }, + { + "category_id": "13005054", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Bagel Shop" + ] + }, + { + "category_id": "13005055", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Australian" + ] + }, + { + "category_id": "13005056", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Asian" + ] + }, + { + "category_id": "13005057", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "American" + ] + }, + { + "category_id": "13005058", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "African" + ] + }, + { + "category_id": "13005059", + "group": "place", + "hierarchy": [ + "Food and Drink", + "Restaurants", + "Afghan" + ] + }, + { + "category_id": "14000000", + "group": "place", + "hierarchy": [ + "Healthcare" + ] + }, + { + "category_id": "14001000", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services" + ] + }, + { + "category_id": "14001001", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Psychologists" + ] + }, + { + "category_id": "14001002", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Pregnancy and Sexual Health" + ] + }, + { + "category_id": "14001003", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Podiatrists" + ] + }, + { + "category_id": "14001004", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Physical Therapy" + ] + }, + { + "category_id": "14001005", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Optometrists" + ] + }, + { + "category_id": "14001006", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Nutritionists" + ] + }, + { + "category_id": "14001007", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Nurses" + ] + }, + { + "category_id": "14001008", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Mental Health" + ] + }, + { + "category_id": "14001009", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Medical Supplies and Labs" + ] + }, + { + "category_id": "14001010", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Hospitals, Clinics and Medical Centers" + ] + }, + { + "category_id": "14001011", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Emergency Services" + ] + }, + { + "category_id": "14001012", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Dentists" + ] + }, + { + "category_id": "14001013", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Counseling and Therapy" + ] + }, + { + "category_id": "14001014", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Chiropractors" + ] + }, + { + "category_id": "14001015", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Blood Banks and Centers" + ] + }, + { + "category_id": "14001016", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Alternative Medicine" + ] + }, + { + "category_id": "14001017", + "group": "place", + "hierarchy": [ + "Healthcare", + "Healthcare Services", + "Acupuncture" + ] + }, + { + "category_id": "14002000", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians" + ] + }, + { + "category_id": "14002001", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Urologists" + ] + }, + { + "category_id": "14002002", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Respiratory" + ] + }, + { + "category_id": "14002003", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Radiologists" + ] + }, + { + "category_id": "14002004", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Psychiatrists" + ] + }, + { + "category_id": "14002005", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Plastic Surgeons" + ] + }, + { + "category_id": "14002006", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Pediatricians" + ] + }, + { + "category_id": "14002007", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Pathologists" + ] + }, + { + "category_id": "14002008", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Orthopedic Surgeons" + ] + }, + { + "category_id": "14002009", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Ophthalmologists" + ] + }, + { + "category_id": "14002010", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Oncologists" + ] + }, + { + "category_id": "14002011", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Obstetricians and Gynecologists" + ] + }, + { + "category_id": "14002012", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Neurologists" + ] + }, + { + "category_id": "14002013", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Internal Medicine" + ] + }, + { + "category_id": "14002014", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "General Surgery" + ] + }, + { + "category_id": "14002015", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Gastroenterologists" + ] + }, + { + "category_id": "14002016", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Family Medicine" + ] + }, + { + "category_id": "14002017", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Ear, Nose and Throat" + ] + }, + { + "category_id": "14002018", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Dermatologists" + ] + }, + { + "category_id": "14002019", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Cardiologists" + ] + }, + { + "category_id": "14002020", + "group": "place", + "hierarchy": [ + "Healthcare", + "Physicians", + "Anesthesiologists" + ] + }, + { + "category_id": "15000000", + "group": "special", + "hierarchy": [ + "Interest" + ] + }, + { + "category_id": "15001000", + "group": "special", + "hierarchy": [ + "Interest", + "Interest Earned" + ] + }, + { + "category_id": "15002000", + "group": "special", + "hierarchy": [ + "Interest", + "Interest Charged" + ] + }, + { + "category_id": "16000000", + "group": "special", + "hierarchy": [ + "Payment" + ] + }, + { + "category_id": "16001000", + "group": "special", + "hierarchy": [ + "Payment", + "Credit Card" + ] + }, + { + "category_id": "16002000", + "group": "special", + "hierarchy": [ + "Payment", + "Rent" + ] + }, + { + "category_id": "16003000", + "group": "special", + "hierarchy": [ + "Payment", + "Loan" + ] + }, + { + "category_id": "17000000", + "group": "place", + "hierarchy": [ + "Recreation" + ] + }, + { + "category_id": "17001000", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment" + ] + }, + { + "category_id": "17001001", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Theatrical Productions" + ] + }, + { + "category_id": "17001002", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Symphony and Opera" + ] + }, + { + "category_id": "17001003", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Sports Venues" + ] + }, + { + "category_id": "17001004", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Social Clubs" + ] + }, + { + "category_id": "17001005", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Psychics and Astrologers" + ] + }, + { + "category_id": "17001006", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Party Centers" + ] + }, + { + "category_id": "17001007", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Music and Show Venues" + ] + }, + { + "category_id": "17001008", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Museums" + ] + }, + { + "category_id": "17001009", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Movie Theatres" + ] + }, + { + "category_id": "17001010", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Fairgrounds and Rodeos" + ] + }, + { + "category_id": "17001011", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Entertainment" + ] + }, + { + "category_id": "17001012", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Dance Halls and Saloons" + ] + }, + { + "category_id": "17001013", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Circuses and Carnivals" + ] + }, + { + "category_id": "17001014", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Casinos and Gaming" + ] + }, + { + "category_id": "17001015", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Bowling" + ] + }, + { + "category_id": "17001016", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Billiards and Pool" + ] + }, + { + "category_id": "17001017", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Art Dealers and Galleries" + ] + }, + { + "category_id": "17001018", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Arcades and Amusement Parks" + ] + }, + { + "category_id": "17001019", + "group": "place", + "hierarchy": [ + "Recreation", + "Arts and Entertainment", + "Aquarium" + ] + }, + { + "category_id": "17002000", + "group": "place", + "hierarchy": [ + "Recreation", + "Athletic Fields" + ] + }, + { + "category_id": "17003000", + "group": "place", + "hierarchy": [ + "Recreation", + "Baseball" + ] + }, + { + "category_id": "17004000", + "group": "place", + "hierarchy": [ + "Recreation", + "Basketball" + ] + }, + { + "category_id": "17005000", + "group": "place", + "hierarchy": [ + "Recreation", + "Batting Cages" + ] + }, + { + "category_id": "17006000", + "group": "place", + "hierarchy": [ + "Recreation", + "Boating" + ] + }, + { + "category_id": "17007000", + "group": "place", + "hierarchy": [ + "Recreation", + "Campgrounds and RV Parks" + ] + }, + { + "category_id": "17008000", + "group": "place", + "hierarchy": [ + "Recreation", + "Canoes and Kayaks" + ] + }, + { + "category_id": "17009000", + "group": "place", + "hierarchy": [ + "Recreation", + "Combat Sports" + ] + }, + { + "category_id": "17010000", + "group": "place", + "hierarchy": [ + "Recreation", + "Cycling" + ] + }, + { + "category_id": "17011000", + "group": "place", + "hierarchy": [ + "Recreation", + "Dance" + ] + }, + { + "category_id": "17012000", + "group": "place", + "hierarchy": [ + "Recreation", + "Equestrian" + ] + }, + { + "category_id": "17013000", + "group": "place", + "hierarchy": [ + "Recreation", + "Football" + ] + }, + { + "category_id": "17014000", + "group": "place", + "hierarchy": [ + "Recreation", + "Go Carts" + ] + }, + { + "category_id": "17015000", + "group": "place", + "hierarchy": [ + "Recreation", + "Golf" + ] + }, + { + "category_id": "17016000", + "group": "place", + "hierarchy": [ + "Recreation", + "Gun Ranges" + ] + }, + { + "category_id": "17017000", + "group": "place", + "hierarchy": [ + "Recreation", + "Gymnastics" + ] + }, + { + "category_id": "17018000", + "group": "place", + "hierarchy": [ + "Recreation", + "Gyms and Fitness Centers" + ] + }, + { + "category_id": "17019000", + "group": "place", + "hierarchy": [ + "Recreation", + "Hiking" + ] + }, + { + "category_id": "17020000", + "group": "place", + "hierarchy": [ + "Recreation", + "Hockey" + ] + }, + { + "category_id": "17021000", + "group": "place", + "hierarchy": [ + "Recreation", + "Hot Air Balloons" + ] + }, + { + "category_id": "17022000", + "group": "place", + "hierarchy": [ + "Recreation", + "Hunting and Fishing" + ] + }, + { + "category_id": "17023000", + "group": "place", + "hierarchy": [ + "Recreation", + "Landmarks" + ] + }, + { + "category_id": "17023001", + "group": "place", + "hierarchy": [ + "Recreation", + "Landmarks", + "Monuments and Memorials" + ] + }, + { + "category_id": "17023002", + "group": "place", + "hierarchy": [ + "Recreation", + "Landmarks", + "Historic Sites" + ] + }, + { + "category_id": "17023003", + "group": "place", + "hierarchy": [ + "Recreation", + "Landmarks", + "Gardens" + ] + }, + { + "category_id": "17023004", + "group": "place", + "hierarchy": [ + "Recreation", + "Landmarks", + "Buildings and Structures" + ] + }, + { + "category_id": "17024000", + "group": "place", + "hierarchy": [ + "Recreation", + "Miniature Golf" + ] + }, + { + "category_id": "17025000", + "group": "place", + "hierarchy": [ + "Recreation", + "Outdoors" + ] + }, + { + "category_id": "17025001", + "group": "place", + "hierarchy": [ + "Recreation", + "Outdoors", + "Rivers" + ] + }, + { + "category_id": "17025002", + "group": "place", + "hierarchy": [ + "Recreation", + "Outdoors", + "Mountains" + ] + }, + { + "category_id": "17025003", + "group": "place", + "hierarchy": [ + "Recreation", + "Outdoors", + "Lakes" + ] + }, + { + "category_id": "17025004", + "group": "place", + "hierarchy": [ + "Recreation", + "Outdoors", + "Forests" + ] + }, + { + "category_id": "17025005", + "group": "place", + "hierarchy": [ + "Recreation", + "Outdoors", + "Beaches" + ] + }, + { + "category_id": "17026000", + "group": "place", + "hierarchy": [ + "Recreation", + "Paintball" + ] + }, + { + "category_id": "17027000", + "group": "place", + "hierarchy": [ + "Recreation", + "Parks" + ] + }, + { + "category_id": "17027001", + "group": "place", + "hierarchy": [ + "Recreation", + "Parks", + "Playgrounds" + ] + }, + { + "category_id": "17027002", + "group": "place", + "hierarchy": [ + "Recreation", + "Parks", + "Picnic Areas" + ] + }, + { + "category_id": "17027003", + "group": "place", + "hierarchy": [ + "Recreation", + "Parks", + "Natural Parks" + ] + }, + { + "category_id": "17028000", + "group": "place", + "hierarchy": [ + "Recreation", + "Personal Trainers" + ] + }, + { + "category_id": "17029000", + "group": "place", + "hierarchy": [ + "Recreation", + "Race Tracks" + ] + }, + { + "category_id": "17030000", + "group": "place", + "hierarchy": [ + "Recreation", + "Racquet Sports" + ] + }, + { + "category_id": "17031000", + "group": "place", + "hierarchy": [ + "Recreation", + "Racquetball" + ] + }, + { + "category_id": "17032000", + "group": "place", + "hierarchy": [ + "Recreation", + "Rafting" + ] + }, + { + "category_id": "17033000", + "group": "place", + "hierarchy": [ + "Recreation", + "Recreation Centers" + ] + }, + { + "category_id": "17034000", + "group": "place", + "hierarchy": [ + "Recreation", + "Rock Climbing" + ] + }, + { + "category_id": "17035000", + "group": "place", + "hierarchy": [ + "Recreation", + "Running" + ] + }, + { + "category_id": "17036000", + "group": "place", + "hierarchy": [ + "Recreation", + "Scuba Diving" + ] + }, + { + "category_id": "17037000", + "group": "place", + "hierarchy": [ + "Recreation", + "Skating" + ] + }, + { + "category_id": "17038000", + "group": "place", + "hierarchy": [ + "Recreation", + "Skydiving" + ] + }, + { + "category_id": "17039000", + "group": "place", + "hierarchy": [ + "Recreation", + "Snow Sports" + ] + }, + { + "category_id": "17040000", + "group": "place", + "hierarchy": [ + "Recreation", + "Soccer" + ] + }, + { + "category_id": "17041000", + "group": "place", + "hierarchy": [ + "Recreation", + "Sports and Recreation Camps" + ] + }, + { + "category_id": "17042000", + "group": "place", + "hierarchy": [ + "Recreation", + "Sports Clubs" + ] + }, + { + "category_id": "17043000", + "group": "place", + "hierarchy": [ + "Recreation", + "Stadiums and Arenas" + ] + }, + { + "category_id": "17044000", + "group": "place", + "hierarchy": [ + "Recreation", + "Swimming" + ] + }, + { + "category_id": "17045000", + "group": "place", + "hierarchy": [ + "Recreation", + "Tennis" + ] + }, + { + "category_id": "17046000", + "group": "place", + "hierarchy": [ + "Recreation", + "Water Sports" + ] + }, + { + "category_id": "17047000", + "group": "place", + "hierarchy": [ + "Recreation", + "Yoga and Pilates" + ] + }, + { + "category_id": "17048000", + "group": "place", + "hierarchy": [ + "Recreation", + "Zoo" + ] + }, + { + "category_id": "18000000", + "group": "place", + "hierarchy": [ + "Service" + ] + }, + { + "category_id": "18001000", + "group": "place", + "hierarchy": [ + "Service", + "Advertising and Marketing" + ] + }, + { + "category_id": "18001001", + "group": "place", + "hierarchy": [ + "Service", + "Advertising and Marketing", + "Writing, Copywriting and Technical Writing" + ] + }, + { + "category_id": "18001002", + "group": "place", + "hierarchy": [ + "Service", + "Advertising and Marketing", + "Search Engine Marketing and Optimization" + ] + }, + { + "category_id": "18001003", + "group": "place", + "hierarchy": [ + "Service", + "Advertising and Marketing", + "Public Relations" + ] + }, + { + "category_id": "18001004", + "group": "place", + "hierarchy": [ + "Service", + "Advertising and Marketing", + "Promotional Items" + ] + }, + { + "category_id": "18001005", + "group": "place", + "hierarchy": [ + "Service", + "Advertising and Marketing", + "Print, TV, Radio and Outdoor Advertising" + ] + }, + { + "category_id": "18001006", + "group": "place", + "hierarchy": [ + "Service", + "Advertising and Marketing", + "Online Advertising" + ] + }, + { + "category_id": "18001007", + "group": "place", + "hierarchy": [ + "Service", + "Advertising and Marketing", + "Market Research and Consulting" + ] + }, + { + "category_id": "18001008", + "group": "place", + "hierarchy": [ + "Service", + "Advertising and Marketing", + "Direct Mail and Email Marketing Services" + ] + }, + { + "category_id": "18001009", + "group": "place", + "hierarchy": [ + "Service", + "Advertising and Marketing", + "Creative Services" + ] + }, + { + "category_id": "18001010", + "group": "place", + "hierarchy": [ + "Service", + "Advertising and Marketing", + "Advertising Agencies and Media Buyers" + ] + }, + { + "category_id": "18003000", + "group": "place", + "hierarchy": [ + "Service", + "Art Restoration" + ] + }, + { + "category_id": "18004000", + "group": "place", + "hierarchy": [ + "Service", + "Audiovisual" + ] + }, + { + "category_id": "18005000", + "group": "place", + "hierarchy": [ + "Service", + "Automation and Control Systems" + ] + }, + { + "category_id": "18006000", + "group": "place", + "hierarchy": [ + "Service", + "Automotive" + ] + }, + { + "category_id": "18006001", + "group": "place", + "hierarchy": [ + "Service", + "Automotive", + "Towing" + ] + }, + { + "category_id": "18006002", + "group": "place", + "hierarchy": [ + "Service", + "Automotive", + "Motorcycle, Moped and Scooter Repair" + ] + }, + { + "category_id": "18006003", + "group": "place", + "hierarchy": [ + "Service", + "Automotive", + "Maintenance and Repair" + ] + }, + { + "category_id": "18006004", + "group": "place", + "hierarchy": [ + "Service", + "Automotive", + "Car Wash and Detail" + ] + }, + { + "category_id": "18006005", + "group": "place", + "hierarchy": [ + "Service", + "Automotive", + "Car Appraisers" + ] + }, + { + "category_id": "18006006", + "group": "place", + "hierarchy": [ + "Service", + "Automotive", + "Auto Transmission" + ] + }, + { + "category_id": "18006007", + "group": "place", + "hierarchy": [ + "Service", + "Automotive", + "Auto Tires" + ] + }, + { + "category_id": "18006008", + "group": "place", + "hierarchy": [ + "Service", + "Automotive", + "Auto Smog Check" + ] + }, + { + "category_id": "18006009", + "group": "place", + "hierarchy": [ + "Service", + "Automotive", + "Auto Oil and Lube" + ] + }, + { + "category_id": "18007000", + "group": "place", + "hierarchy": [ + "Service", + "Business and Strategy Consulting" + ] + }, + { + "category_id": "18008000", + "group": "place", + "hierarchy": [ + "Service", + "Business Services" + ] + }, + { + "category_id": "18008001", + "group": "place", + "hierarchy": [ + "Service", + "Business Services", + "Printing and Publishing" + ] + }, + { + "category_id": "18009000", + "group": "special", + "hierarchy": [ + "Service", + "Cable" + ] + }, + { + "category_id": "18010000", + "group": "place", + "hierarchy": [ + "Service", + "Chemicals and Gasses" + ] + }, + { + "category_id": "18011000", + "group": "place", + "hierarchy": [ + "Service", + "Cleaning" + ] + }, + { + "category_id": "18012000", + "group": "place", + "hierarchy": [ + "Service", + "Computers" + ] + }, + { + "category_id": "18012001", + "group": "place", + "hierarchy": [ + "Service", + "Computers", + "Maintenance and Repair" + ] + }, + { + "category_id": "18012002", + "group": "place", + "hierarchy": [ + "Service", + "Computers", + "Software Development" + ] + }, + { + "category_id": "18013000", + "group": "place", + "hierarchy": [ + "Service", + "Construction" + ] + }, + { + "category_id": "18013001", + "group": "place", + "hierarchy": [ + "Service", + "Construction", + "Specialty" + ] + }, + { + "category_id": "18013002", + "group": "place", + "hierarchy": [ + "Service", + "Construction", + "Roofers" + ] + }, + { + "category_id": "18013003", + "group": "place", + "hierarchy": [ + "Service", + "Construction", + "Painting" + ] + }, + { + "category_id": "18013004", + "group": "place", + "hierarchy": [ + "Service", + "Construction", + "Masonry" + ] + }, + { + "category_id": "18013005", + "group": "place", + "hierarchy": [ + "Service", + "Construction", + "Infrastructure" + ] + }, + { + "category_id": "18013006", + "group": "place", + "hierarchy": [ + "Service", + "Construction", + "Heating, Ventilating and Air Conditioning" + ] + }, + { + "category_id": "18013007", + "group": "place", + "hierarchy": [ + "Service", + "Construction", + "Electricians" + ] + }, + { + "category_id": "18013008", + "group": "place", + "hierarchy": [ + "Service", + "Construction", + "Contractors" + ] + }, + { + "category_id": "18013009", + "group": "place", + "hierarchy": [ + "Service", + "Construction", + "Carpet and Flooring" + ] + }, + { + "category_id": "18013010", + "group": "place", + "hierarchy": [ + "Service", + "Construction", + "Carpenters" + ] + }, + { + "category_id": "18014000", + "group": "place", + "hierarchy": [ + "Service", + "Credit Counseling and Bankruptcy Services" + ] + }, + { + "category_id": "18015000", + "group": "place", + "hierarchy": [ + "Service", + "Dating and Escort" + ] + }, + { + "category_id": "18016000", + "group": "place", + "hierarchy": [ + "Service", + "Employment Agencies" + ] + }, + { + "category_id": "18017000", + "group": "place", + "hierarchy": [ + "Service", + "Engineering" + ] + }, + { + "category_id": "18018000", + "group": "place", + "hierarchy": [ + "Service", + "Entertainment" + ] + }, + { + "category_id": "18018001", + "group": "place", + "hierarchy": [ + "Service", + "Entertainment", + "Media" + ] + }, + { + "category_id": "18019000", + "group": "place", + "hierarchy": [ + "Service", + "Events and Event Planning" + ] + }, + { + "category_id": "18020000", + "group": "place", + "hierarchy": [ + "Service", + "Financial" + ] + }, + { + "category_id": "18020001", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Taxes" + ] + }, + { + "category_id": "18020002", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Student Aid and Grants" + ] + }, + { + "category_id": "18020003", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Stock Brokers" + ] + }, + { + "category_id": "18020004", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Loans and Mortgages" + ] + }, + { + "category_id": "18020005", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Holding and Investment Offices" + ] + }, + { + "category_id": "18020006", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Fund Raising" + ] + }, + { + "category_id": "18020007", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Financial Planning and Investments" + ] + }, + { + "category_id": "18020008", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Credit Reporting" + ] + }, + { + "category_id": "18020009", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Collections" + ] + }, + { + "category_id": "18020010", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Check Cashing" + ] + }, + { + "category_id": "18020011", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Business Brokers and Franchises" + ] + }, + { + "category_id": "18020012", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Banking and Finance" + ] + }, + { + "category_id": "18020013", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "ATMs" + ] + }, + { + "category_id": "18020014", + "group": "place", + "hierarchy": [ + "Service", + "Financial", + "Accounting and Bookkeeping" + ] + }, + { + "category_id": "18021000", + "group": "place", + "hierarchy": [ + "Service", + "Food and Beverage" + ] + }, + { + "category_id": "18021001", + "group": "place", + "hierarchy": [ + "Service", + "Food and Beverage", + "Distribution" + ] + }, + { + "category_id": "18021002", + "group": "place", + "hierarchy": [ + "Service", + "Food and Beverage", + "Catering" + ] + }, + { + "category_id": "18022000", + "group": "place", + "hierarchy": [ + "Service", + "Funeral Services" + ] + }, + { + "category_id": "18023000", + "group": "place", + "hierarchy": [ + "Service", + "Geological" + ] + }, + { + "category_id": "18024000", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement" + ] + }, + { + "category_id": "18024001", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Upholstery" + ] + }, + { + "category_id": "18024002", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Tree Service" + ] + }, + { + "category_id": "18024003", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Swimming Pool Maintenance and Services" + ] + }, + { + "category_id": "18024004", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Storage" + ] + }, + { + "category_id": "18024005", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Roofers" + ] + }, + { + "category_id": "18024006", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Pools and Spas" + ] + }, + { + "category_id": "18024007", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Plumbing" + ] + }, + { + "category_id": "18024008", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Pest Control" + ] + }, + { + "category_id": "18024009", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Painting" + ] + }, + { + "category_id": "18024010", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Movers" + ] + }, + { + "category_id": "18024011", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Mobile Homes" + ] + }, + { + "category_id": "18024012", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Lighting Fixtures" + ] + }, + { + "category_id": "18024013", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Landscaping and Gardeners" + ] + }, + { + "category_id": "18024014", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Kitchens" + ] + }, + { + "category_id": "18024015", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Interior Design" + ] + }, + { + "category_id": "18024016", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Housewares" + ] + }, + { + "category_id": "18024017", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Home Inspection Services" + ] + }, + { + "category_id": "18024018", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Home Appliances" + ] + }, + { + "category_id": "18024019", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Heating, Ventilation and Air Conditioning" + ] + }, + { + "category_id": "18024020", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Hardware and Services" + ] + }, + { + "category_id": "18024021", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Fences, Fireplaces and Garage Doors" + ] + }, + { + "category_id": "18024022", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Electricians" + ] + }, + { + "category_id": "18024023", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Doors and Windows" + ] + }, + { + "category_id": "18024024", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Contractors" + ] + }, + { + "category_id": "18024025", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Carpet and Flooring" + ] + }, + { + "category_id": "18024026", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Carpenters" + ] + }, + { + "category_id": "18024027", + "group": "place", + "hierarchy": [ + "Service", + "Home Improvement", + "Architects" + ] + }, + { + "category_id": "18025000", + "group": "place", + "hierarchy": [ + "Service", + "Household" + ] + }, + { + "category_id": "18026000", + "group": "place", + "hierarchy": [ + "Service", + "Human Resources" + ] + }, + { + "category_id": "18027000", + "group": "place", + "hierarchy": [ + "Service", + "Immigration" + ] + }, + { + "category_id": "18028000", + "group": "place", + "hierarchy": [ + "Service", + "Import and Export" + ] + }, + { + "category_id": "18029000", + "group": "place", + "hierarchy": [ + "Service", + "Industrial Machinery and Vehicles" + ] + }, + { + "category_id": "18030000", + "group": "special", + "hierarchy": [ + "Service", + "Insurance" + ] + }, + { + "category_id": "18031000", + "group": "digital", + "hierarchy": [ + "Service", + "Internet Services" + ] + }, + { + "category_id": "18032000", + "group": "place", + "hierarchy": [ + "Service", + "Leather" + ] + }, + { + "category_id": "18033000", + "group": "place", + "hierarchy": [ + "Service", + "Legal" + ] + }, + { + "category_id": "18034000", + "group": "place", + "hierarchy": [ + "Service", + "Logging and Sawmills" + ] + }, + { + "category_id": "18035000", + "group": "place", + "hierarchy": [ + "Service", + "Machine Shops" + ] + }, + { + "category_id": "18036000", + "group": "place", + "hierarchy": [ + "Service", + "Management" + ] + }, + { + "category_id": "18037000", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing" + ] + }, + { + "category_id": "18037001", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Apparel and Fabric Products" + ] + }, + { + "category_id": "18037002", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Chemicals and Gasses" + ] + }, + { + "category_id": "18037003", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Computers and Office Machines" + ] + }, + { + "category_id": "18037004", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Electrical Equipment and Components" + ] + }, + { + "category_id": "18037005", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Food and Beverage" + ] + }, + { + "category_id": "18037006", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Furniture and Fixtures" + ] + }, + { + "category_id": "18037007", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Glass Products" + ] + }, + { + "category_id": "18037008", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Industrial Machinery and Equipment" + ] + }, + { + "category_id": "18037009", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Leather Goods" + ] + }, + { + "category_id": "18037010", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Metal Products" + ] + }, + { + "category_id": "18037011", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Nonmetallic Mineral Products" + ] + }, + { + "category_id": "18037012", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Paper Products" + ] + }, + { + "category_id": "18037013", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Petroleum" + ] + }, + { + "category_id": "18037014", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Plastic Products" + ] + }, + { + "category_id": "18037015", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Rubber Products" + ] + }, + { + "category_id": "18037016", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Service Instruments" + ] + }, + { + "category_id": "18037017", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Textiles" + ] + }, + { + "category_id": "18037018", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Tobacco" + ] + }, + { + "category_id": "18037019", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Transportation Equipment" + ] + }, + { + "category_id": "18037020", + "group": "place", + "hierarchy": [ + "Service", + "Manufacturing", + "Wood Products" + ] + }, + { + "category_id": "18038000", + "group": "place", + "hierarchy": [ + "Service", + "Media Production" + ] + }, + { + "category_id": "18039000", + "group": "place", + "hierarchy": [ + "Service", + "Metals" + ] + }, + { + "category_id": "18040000", + "group": "place", + "hierarchy": [ + "Service", + "Mining" + ] + }, + { + "category_id": "18040001", + "group": "place", + "hierarchy": [ + "Service", + "Mining", + "Coal" + ] + }, + { + "category_id": "18040002", + "group": "place", + "hierarchy": [ + "Service", + "Mining", + "Metal" + ] + }, + { + "category_id": "18040003", + "group": "place", + "hierarchy": [ + "Service", + "Mining", + "Non-Metallic Minerals" + ] + }, + { + "category_id": "18041000", + "group": "place", + "hierarchy": [ + "Service", + "News Reporting" + ] + }, + { + "category_id": "18042000", + "group": "place", + "hierarchy": [ + "Service", + "Oil and Gas" + ] + }, + { + "category_id": "18043000", + "group": "place", + "hierarchy": [ + "Service", + "Packaging" + ] + }, + { + "category_id": "18044000", + "group": "place", + "hierarchy": [ + "Service", + "Paper" + ] + }, + { + "category_id": "18045000", + "group": "place", + "hierarchy": [ + "Service", + "Personal Care" + ] + }, + { + "category_id": "18045001", + "group": "place", + "hierarchy": [ + "Service", + "Personal Care", + "Tattooing" + ] + }, + { + "category_id": "18045002", + "group": "place", + "hierarchy": [ + "Service", + "Personal Care", + "Tanning Salons" + ] + }, + { + "category_id": "18045003", + "group": "place", + "hierarchy": [ + "Service", + "Personal Care", + "Spas" + ] + }, + { + "category_id": "18045004", + "group": "place", + "hierarchy": [ + "Service", + "Personal Care", + "Skin Care" + ] + }, + { + "category_id": "18045005", + "group": "place", + "hierarchy": [ + "Service", + "Personal Care", + "Piercing" + ] + }, + { + "category_id": "18045006", + "group": "place", + "hierarchy": [ + "Service", + "Personal Care", + "Massage Clinics and Therapists" + ] + }, + { + "category_id": "18045007", + "group": "place", + "hierarchy": [ + "Service", + "Personal Care", + "Manicures and Pedicures" + ] + }, + { + "category_id": "18045008", + "group": "place", + "hierarchy": [ + "Service", + "Personal Care", + "Laundry and Garment Services" + ] + }, + { + "category_id": "18045009", + "group": "place", + "hierarchy": [ + "Service", + "Personal Care", + "Hair Salons and Barbers" + ] + }, + { + "category_id": "18045010", + "group": "place", + "hierarchy": [ + "Service", + "Personal Care", + "Hair Removal" + ] + }, + { + "category_id": "18046000", + "group": "place", + "hierarchy": [ + "Service", + "Petroleum" + ] + }, + { + "category_id": "18047000", + "group": "place", + "hierarchy": [ + "Service", + "Photography" + ] + }, + { + "category_id": "18048000", + "group": "place", + "hierarchy": [ + "Service", + "Plastics" + ] + }, + { + "category_id": "18049000", + "group": "place", + "hierarchy": [ + "Service", + "Rail" + ] + }, + { + "category_id": "18050000", + "group": "place", + "hierarchy": [ + "Service", + "Real Estate" + ] + }, + { + "category_id": "18050001", + "group": "place", + "hierarchy": [ + "Service", + "Real Estate", + "Real Estate Development and Title Companies" + ] + }, + { + "category_id": "18050002", + "group": "place", + "hierarchy": [ + "Service", + "Real Estate", + "Real Estate Appraiser" + ] + }, + { + "category_id": "18050003", + "group": "place", + "hierarchy": [ + "Service", + "Real Estate", + "Real Estate Agents" + ] + }, + { + "category_id": "18050004", + "group": "place", + "hierarchy": [ + "Service", + "Real Estate", + "Property Management" + ] + }, + { + "category_id": "18050005", + "group": "place", + "hierarchy": [ + "Service", + "Real Estate", + "Corporate Housing" + ] + }, + { + "category_id": "18050006", + "group": "place", + "hierarchy": [ + "Service", + "Real Estate", + "Commercial Real Estate" + ] + }, + { + "category_id": "18050007", + "group": "place", + "hierarchy": [ + "Service", + "Real Estate", + "Building and Land Surveyors" + ] + }, + { + "category_id": "18050008", + "group": "place", + "hierarchy": [ + "Service", + "Real Estate", + "Boarding Houses" + ] + }, + { + "category_id": "18050009", + "group": "place", + "hierarchy": [ + "Service", + "Real Estate", + "Apartments, Condos and Houses" + ] + }, + { + "category_id": "18050010", + "group": "special", + "hierarchy": [ + "Service", + "Real Estate", + "Rent" + ] + }, + { + "category_id": "18051000", + "group": "place", + "hierarchy": [ + "Service", + "Refrigeration and Ice" + ] + }, + { + "category_id": "18052000", + "group": "place", + "hierarchy": [ + "Service", + "Renewable Energy" + ] + }, + { + "category_id": "18053000", + "group": "place", + "hierarchy": [ + "Service", + "Repair Services" + ] + }, + { + "category_id": "18054000", + "group": "place", + "hierarchy": [ + "Service", + "Research" + ] + }, + { + "category_id": "18055000", + "group": "place", + "hierarchy": [ + "Service", + "Rubber" + ] + }, + { + "category_id": "18056000", + "group": "place", + "hierarchy": [ + "Service", + "Scientific" + ] + }, + { + "category_id": "18057000", + "group": "place", + "hierarchy": [ + "Service", + "Security and Safety" + ] + }, + { + "category_id": "18058000", + "group": "place", + "hierarchy": [ + "Service", + "Shipping and Freight" + ] + }, + { + "category_id": "18059000", + "group": "place", + "hierarchy": [ + "Service", + "Software Development" + ] + }, + { + "category_id": "18060000", + "group": "place", + "hierarchy": [ + "Service", + "Storage" + ] + }, + { + "category_id": "18061000", + "group": "place", + "hierarchy": [ + "Service", + "Subscription" + ] + }, + { + "category_id": "18062000", + "group": "place", + "hierarchy": [ + "Service", + "Tailors" + ] + }, + { + "category_id": "18063000", + "group": "place", + "hierarchy": [ + "Service", + "Telecommunication Services" + ] + }, + { + "category_id": "18064000", + "group": "place", + "hierarchy": [ + "Service", + "Textiles" + ] + }, + { + "category_id": "18065000", + "group": "place", + "hierarchy": [ + "Service", + "Tourist Information and Services" + ] + }, + { + "category_id": "18066000", + "group": "place", + "hierarchy": [ + "Service", + "Transportation" + ] + }, + { + "category_id": "18067000", + "group": "place", + "hierarchy": [ + "Service", + "Travel Agents and Tour Operators" + ] + }, + { + "category_id": "18068000", + "group": "special", + "hierarchy": [ + "Service", + "Utilities" + ] + }, + { + "category_id": "18068001", + "group": "special", + "hierarchy": [ + "Service", + "Utilities", + "Water" + ] + }, + { + "category_id": "18068002", + "group": "special", + "hierarchy": [ + "Service", + "Utilities", + "Sanitary and Waste Management" + ] + }, + { + "category_id": "18068003", + "group": "place", + "hierarchy": [ + "Service", + "Utilities", + "Heating, Ventilating, and Air Conditioning" + ] + }, + { + "category_id": "18068004", + "group": "special", + "hierarchy": [ + "Service", + "Utilities", + "Gas" + ] + }, + { + "category_id": "18068005", + "group": "special", + "hierarchy": [ + "Service", + "Utilities", + "Electric" + ] + }, + { + "category_id": "18069000", + "group": "place", + "hierarchy": [ + "Service", + "Veterinarians" + ] + }, + { + "category_id": "18070000", + "group": "place", + "hierarchy": [ + "Service", + "Water and Waste Management" + ] + }, + { + "category_id": "18071000", + "group": "place", + "hierarchy": [ + "Service", + "Web Design and Development" + ] + }, + { + "category_id": "18072000", + "group": "place", + "hierarchy": [ + "Service", + "Welding" + ] + }, + { + "category_id": "18073000", + "group": "place", + "hierarchy": [ + "Service", + "Agriculture and Forestry" + ] + }, + { + "category_id": "18073001", + "group": "place", + "hierarchy": [ + "Service", + "Agriculture and Forestry", + "Crop Production" + ] + }, + { + "category_id": "18073002", + "group": "place", + "hierarchy": [ + "Service", + "Agriculture and Forestry", + "Forestry" + ] + }, + { + "category_id": "18073003", + "group": "place", + "hierarchy": [ + "Service", + "Agriculture and Forestry", + "Livestock and Animals" + ] + }, + { + "category_id": "18073004", + "group": "place", + "hierarchy": [ + "Service", + "Agriculture and Forestry", + "Services" + ] + }, + { + "category_id": "18074000", + "group": "place", + "hierarchy": [ + "Service", + "Art and Graphic Design" + ] + }, + { + "category_id": "19000000", + "group": "place", + "hierarchy": [ + "Shops" + ] + }, + { + "category_id": "19001000", + "group": "place", + "hierarchy": [ + "Shops", + "Adult" + ] + }, + { + "category_id": "19002000", + "group": "place", + "hierarchy": [ + "Shops", + "Antiques" + ] + }, + { + "category_id": "19003000", + "group": "place", + "hierarchy": [ + "Shops", + "Arts and Crafts" + ] + }, + { + "category_id": "19004000", + "group": "place", + "hierarchy": [ + "Shops", + "Auctions" + ] + }, + { + "category_id": "19005000", + "group": "place", + "hierarchy": [ + "Shops", + "Automotive" + ] + }, + { + "category_id": "19005001", + "group": "place", + "hierarchy": [ + "Shops", + "Automotive", + "Used Car Dealers" + ] + }, + { + "category_id": "19005002", + "group": "place", + "hierarchy": [ + "Shops", + "Automotive", + "Salvage Yards" + ] + }, + { + "category_id": "19005003", + "group": "place", + "hierarchy": [ + "Shops", + "Automotive", + "RVs and Motor Homes" + ] + }, + { + "category_id": "19005004", + "group": "place", + "hierarchy": [ + "Shops", + "Automotive", + "Motorcycles, Mopeds and Scooters" + ] + }, + { + "category_id": "19005005", + "group": "place", + "hierarchy": [ + "Shops", + "Automotive", + "Classic and Antique Car" + ] + }, + { + "category_id": "19005006", + "group": "place", + "hierarchy": [ + "Shops", + "Automotive", + "Car Parts and Accessories" + ] + }, + { + "category_id": "19005007", + "group": "place", + "hierarchy": [ + "Shops", + "Automotive", + "Car Dealers and Leasing" + ] + }, + { + "category_id": "19006000", + "group": "place", + "hierarchy": [ + "Shops", + "Beauty Products" + ] + }, + { + "category_id": "19007000", + "group": "place", + "hierarchy": [ + "Shops", + "Bicycles" + ] + }, + { + "category_id": "19008000", + "group": "place", + "hierarchy": [ + "Shops", + "Boat Dealers" + ] + }, + { + "category_id": "19009000", + "group": "place", + "hierarchy": [ + "Shops", + "Bookstores" + ] + }, + { + "category_id": "19010000", + "group": "place", + "hierarchy": [ + "Shops", + "Cards and Stationery" + ] + }, + { + "category_id": "19011000", + "group": "place", + "hierarchy": [ + "Shops", + "Children" + ] + }, + { + "category_id": "19012000", + "group": "place", + "hierarchy": [ + "Shops", + "Clothing and Accessories" + ] + }, + { + "category_id": "19012001", + "group": "place", + "hierarchy": [ + "Shops", + "Clothing and Accessories", + "Women's Store" + ] + }, + { + "category_id": "19012002", + "group": "place", + "hierarchy": [ + "Shops", + "Clothing and Accessories", + "Swimwear" + ] + }, + { + "category_id": "19012003", + "group": "place", + "hierarchy": [ + "Shops", + "Clothing and Accessories", + "Shoe Store" + ] + }, + { + "category_id": "19012004", + "group": "place", + "hierarchy": [ + "Shops", + "Clothing and Accessories", + "Men's Store" + ] + }, + { + "category_id": "19012005", + "group": "place", + "hierarchy": [ + "Shops", + "Clothing and Accessories", + "Lingerie Store" + ] + }, + { + "category_id": "19012006", + "group": "place", + "hierarchy": [ + "Shops", + "Clothing and Accessories", + "Kids' Store" + ] + }, + { + "category_id": "19012007", + "group": "place", + "hierarchy": [ + "Shops", + "Clothing and Accessories", + "Boutique" + ] + }, + { + "category_id": "19012008", + "group": "place", + "hierarchy": [ + "Shops", + "Clothing and Accessories", + "Accessories Store" + ] + }, + { + "category_id": "19013000", + "group": "place", + "hierarchy": [ + "Shops", + "Computers and Electronics" + ] + }, + { + "category_id": "19013001", + "group": "place", + "hierarchy": [ + "Shops", + "Computers and Electronics", + "Video Games" + ] + }, + { + "category_id": "19013002", + "group": "place", + "hierarchy": [ + "Shops", + "Computers and Electronics", + "Mobile Phones" + ] + }, + { + "category_id": "19013003", + "group": "place", + "hierarchy": [ + "Shops", + "Computers and Electronics", + "Cameras" + ] + }, + { + "category_id": "19014000", + "group": "place", + "hierarchy": [ + "Shops", + "Construction Supplies" + ] + }, + { + "category_id": "19015000", + "group": "place", + "hierarchy": [ + "Shops", + "Convenience Stores" + ] + }, + { + "category_id": "19016000", + "group": "place", + "hierarchy": [ + "Shops", + "Costumes" + ] + }, + { + "category_id": "19017000", + "group": "place", + "hierarchy": [ + "Shops", + "Dance and Music" + ] + }, + { + "category_id": "19018000", + "group": "place", + "hierarchy": [ + "Shops", + "Department Stores" + ] + }, + { + "category_id": "19019000", + "group": "digital", + "hierarchy": [ + "Shops", + "Digital Purchase" + ] + }, + { + "category_id": "19020000", + "group": "place", + "hierarchy": [ + "Shops", + "Discount Stores" + ] + }, + { + "category_id": "19021000", + "group": "place", + "hierarchy": [ + "Shops", + "Electrical Equipment" + ] + }, + { + "category_id": "19022000", + "group": "place", + "hierarchy": [ + "Shops", + "Equipment Rental" + ] + }, + { + "category_id": "19023000", + "group": "place", + "hierarchy": [ + "Shops", + "Flea Markets" + ] + }, + { + "category_id": "19024000", + "group": "place", + "hierarchy": [ + "Shops", + "Florists" + ] + }, + { + "category_id": "19025000", + "group": "place", + "hierarchy": [ + "Shops", + "Food and Beverage Store" + ] + }, + { + "category_id": "19025001", + "group": "place", + "hierarchy": [ + "Shops", + "Food and Beverage Store", + "Specialty" + ] + }, + { + "category_id": "19025002", + "group": "place", + "hierarchy": [ + "Shops", + "Food and Beverage Store", + "Health Food" + ] + }, + { + "category_id": "19025003", + "group": "place", + "hierarchy": [ + "Shops", + "Food and Beverage Store", + "Farmers Markets" + ] + }, + { + "category_id": "19025004", + "group": "place", + "hierarchy": [ + "Shops", + "Food and Beverage Store", + "Beer, Wine and Spirits" + ] + }, + { + "category_id": "19026000", + "group": "place", + "hierarchy": [ + "Shops", + "Fuel Dealer" + ] + }, + { + "category_id": "19027000", + "group": "place", + "hierarchy": [ + "Shops", + "Furniture and Home Decor" + ] + }, + { + "category_id": "19028000", + "group": "place", + "hierarchy": [ + "Shops", + "Gift and Novelty" + ] + }, + { + "category_id": "19029000", + "group": "place", + "hierarchy": [ + "Shops", + "Glasses and Optometrist" + ] + }, + { + "category_id": "19030000", + "group": "place", + "hierarchy": [ + "Shops", + "Hardware Store" + ] + }, + { + "category_id": "19031000", + "group": "place", + "hierarchy": [ + "Shops", + "Hobby and Collectibles" + ] + }, + { + "category_id": "19032000", + "group": "place", + "hierarchy": [ + "Shops", + "Industrial Supplies" + ] + }, + { + "category_id": "19033000", + "group": "place", + "hierarchy": [ + "Shops", + "Jewelry and Watches" + ] + }, + { + "category_id": "19034000", + "group": "place", + "hierarchy": [ + "Shops", + "Luggage" + ] + }, + { + "category_id": "19035000", + "group": "place", + "hierarchy": [ + "Shops", + "Marine Supplies" + ] + }, + { + "category_id": "19036000", + "group": "place", + "hierarchy": [ + "Shops", + "Music, Video and DVD" + ] + }, + { + "category_id": "19037000", + "group": "place", + "hierarchy": [ + "Shops", + "Musical Instruments" + ] + }, + { + "category_id": "19038000", + "group": "place", + "hierarchy": [ + "Shops", + "Newsstands" + ] + }, + { + "category_id": "19039000", + "group": "place", + "hierarchy": [ + "Shops", + "Office Supplies" + ] + }, + { + "category_id": "19040000", + "group": "place", + "hierarchy": [ + "Shops", + "Outlet" + ] + }, + { + "category_id": "19040001", + "group": "place", + "hierarchy": [ + "Shops", + "Outlet", + "Women's Store" + ] + }, + { + "category_id": "19040002", + "group": "place", + "hierarchy": [ + "Shops", + "Outlet", + "Swimwear" + ] + }, + { + "category_id": "19040003", + "group": "place", + "hierarchy": [ + "Shops", + "Outlet", + "Shoe Store" + ] + }, + { + "category_id": "19040004", + "group": "place", + "hierarchy": [ + "Shops", + "Outlet", + "Men's Store" + ] + }, + { + "category_id": "19040005", + "group": "place", + "hierarchy": [ + "Shops", + "Outlet", + "Lingerie Store" + ] + }, + { + "category_id": "19040006", + "group": "place", + "hierarchy": [ + "Shops", + "Outlet", + "Kids' Store" + ] + }, + { + "category_id": "19040007", + "group": "place", + "hierarchy": [ + "Shops", + "Outlet", + "Boutique" + ] + }, + { + "category_id": "19040008", + "group": "place", + "hierarchy": [ + "Shops", + "Outlet", + "Accessories Store" + ] + }, + { + "category_id": "19041000", + "group": "place", + "hierarchy": [ + "Shops", + "Pawn Shops" + ] + }, + { + "category_id": "19042000", + "group": "place", + "hierarchy": [ + "Shops", + "Pets" + ] + }, + { + "category_id": "19043000", + "group": "place", + "hierarchy": [ + "Shops", + "Pharmacies" + ] + }, + { + "category_id": "19044000", + "group": "place", + "hierarchy": [ + "Shops", + "Photos and Frames" + ] + }, + { + "category_id": "19045000", + "group": "place", + "hierarchy": [ + "Shops", + "Shopping Centers and Malls" + ] + }, + { + "category_id": "19046000", + "group": "place", + "hierarchy": [ + "Shops", + "Sporting Goods" + ] + }, + { + "category_id": "19047000", + "group": "place", + "hierarchy": [ + "Shops", + "Supermarkets and Groceries" + ] + }, + { + "category_id": "19048000", + "group": "place", + "hierarchy": [ + "Shops", + "Tobacco" + ] + }, + { + "category_id": "19049000", + "group": "place", + "hierarchy": [ + "Shops", + "Toys" + ] + }, + { + "category_id": "19050000", + "group": "place", + "hierarchy": [ + "Shops", + "Vintage and Thrift" + ] + }, + { + "category_id": "19051000", + "group": "place", + "hierarchy": [ + "Shops", + "Warehouses and Wholesale Stores" + ] + }, + { + "category_id": "19052000", + "group": "place", + "hierarchy": [ + "Shops", + "Wedding and Bridal" + ] + }, + { + "category_id": "19053000", + "group": "place", + "hierarchy": [ + "Shops", + "Wholesale" + ] + }, + { + "category_id": "19054000", + "group": "place", + "hierarchy": [ + "Shops", + "Lawn and Garden" + ] + }, + { + "category_id": "20000000", + "group": "special", + "hierarchy": [ + "Tax" + ] + }, + { + "category_id": "20001000", + "group": "special", + "hierarchy": [ + "Tax", + "Refund" + ] + }, + { + "category_id": "20002000", + "group": "special", + "hierarchy": [ + "Tax", + "Payment" + ] + }, + { + "category_id": "21000000", + "group": "special", + "hierarchy": [ + "Transfer" + ] + }, + { + "category_id": "21001000", + "group": "special", + "hierarchy": [ + "Transfer", + "Internal Account Transfer" + ] + }, + { + "category_id": "21002000", + "group": "special", + "hierarchy": [ + "Transfer", + "ACH" + ] + }, + { + "category_id": "21003000", + "group": "special", + "hierarchy": [ + "Transfer", + "Billpay" + ] + }, + { + "category_id": "21004000", + "group": "special", + "hierarchy": [ + "Transfer", + "Check" + ] + }, + { + "category_id": "21005000", + "group": "special", + "hierarchy": [ + "Transfer", + "Credit" + ] + }, + { + "category_id": "21006000", + "group": "special", + "hierarchy": [ + "Transfer", + "Debit" + ] + }, + { + "category_id": "21007000", + "group": "special", + "hierarchy": [ + "Transfer", + "Deposit" + ] + }, + { + "category_id": "21007001", + "group": "special", + "hierarchy": [ + "Transfer", + "Deposit", + "Check" + ] + }, + { + "category_id": "21007002", + "group": "special", + "hierarchy": [ + "Transfer", + "Deposit", + "ATM" + ] + }, + { + "category_id": "21008000", + "group": "special", + "hierarchy": [ + "Transfer", + "Keep the Change Savings Program" + ] + }, + { + "category_id": "21009000", + "group": "special", + "hierarchy": [ + "Transfer", + "Payroll" + ] + }, + { + "category_id": "21009001", + "group": "special", + "hierarchy": [ + "Transfer", + "Payroll", + "Benefits" + ] + }, + { + "category_id": "21010000", + "group": "special", + "hierarchy": [ + "Transfer", + "Third Party" + ] + }, + { + "category_id": "21010001", + "group": "special", + "hierarchy": [ + "Transfer", + "Third Party", + "Venmo" + ] + }, + { + "category_id": "21010002", + "group": "special", + "hierarchy": [ + "Transfer", + "Third Party", + "Square Cash" + ] + }, + { + "category_id": "21010003", + "group": "special", + "hierarchy": [ + "Transfer", + "Third Party", + "Square" + ] + }, + { + "category_id": "21010004", + "group": "special", + "hierarchy": [ + "Transfer", + "Third Party", + "PayPal" + ] + }, + { + "category_id": "21010005", + "group": "special", + "hierarchy": [ + "Transfer", + "Third Party", + "Dwolla" + ] + }, + { + "category_id": "21010006", + "group": "special", + "hierarchy": [ + "Transfer", + "Third Party", + "Coinbase" + ] + }, + { + "category_id": "21010007", + "group": "special", + "hierarchy": [ + "Transfer", + "Third Party", + "Chase QuickPay" + ] + }, + { + "category_id": "21010008", + "group": "special", + "hierarchy": [ + "Transfer", + "Third Party", + "Acorns" + ] + }, + { + "category_id": "21010009", + "group": "special", + "hierarchy": [ + "Transfer", + "Third Party", + "Digit" + ] + }, + { + "category_id": "21010010", + "group": "special", + "hierarchy": [ + "Transfer", + "Third Party", + "Betterment" + ] + }, + { + "category_id": "21010011", + "group": "special", + "hierarchy": [ + "Transfer", + "Third Party", + "Plaid" + ] + }, + { + "category_id": "21011000", + "group": "special", + "hierarchy": [ + "Transfer", + "Wire" + ] + }, + { + "category_id": "21012000", + "group": "special", + "hierarchy": [ + "Transfer", + "Withdrawal" + ] + }, + { + "category_id": "21012001", + "group": "special", + "hierarchy": [ + "Transfer", + "Withdrawal", + "Check" + ] + }, + { + "category_id": "21012002", + "group": "special", + "hierarchy": [ + "Transfer", + "Withdrawal", + "ATM" + ] + }, + { + "category_id": "21013000", + "group": "special", + "hierarchy": [ + "Transfer", + "Save As You Go" + ] + }, + { + "category_id": "22000000", + "group": "place", + "hierarchy": [ + "Travel" + ] + }, + { + "category_id": "22001000", + "group": "special", + "hierarchy": [ + "Travel", + "Airlines and Aviation Services" + ] + }, + { + "category_id": "22002000", + "group": "place", + "hierarchy": [ + "Travel", + "Airports" + ] + }, + { + "category_id": "22003000", + "group": "place", + "hierarchy": [ + "Travel", + "Boat" + ] + }, + { + "category_id": "22004000", + "group": "place", + "hierarchy": [ + "Travel", + "Bus Stations" + ] + }, + { + "category_id": "22005000", + "group": "place", + "hierarchy": [ + "Travel", + "Car and Truck Rentals" + ] + }, + { + "category_id": "22006000", + "group": "place", + "hierarchy": [ + "Travel", + "Car Service" + ] + }, + { + "category_id": "22006001", + "group": "special", + "hierarchy": [ + "Travel", + "Car Service", + "Ride Share" + ] + }, + { + "category_id": "22007000", + "group": "place", + "hierarchy": [ + "Travel", + "Charter Buses" + ] + }, + { + "category_id": "22008000", + "group": "special", + "hierarchy": [ + "Travel", + "Cruises" + ] + }, + { + "category_id": "22009000", + "group": "place", + "hierarchy": [ + "Travel", + "Gas Stations" + ] + }, + { + "category_id": "22010000", + "group": "place", + "hierarchy": [ + "Travel", + "Heliports" + ] + }, + { + "category_id": "22011000", + "group": "place", + "hierarchy": [ + "Travel", + "Limos and Chauffeurs" + ] + }, + { + "category_id": "22012000", + "group": "place", + "hierarchy": [ + "Travel", + "Lodging" + ] + }, + { + "category_id": "22012001", + "group": "place", + "hierarchy": [ + "Travel", + "Lodging", + "Resorts" + ] + }, + { + "category_id": "22012002", + "group": "place", + "hierarchy": [ + "Travel", + "Lodging", + "Lodges and Vacation Rentals" + ] + }, + { + "category_id": "22012003", + "group": "place", + "hierarchy": [ + "Travel", + "Lodging", + "Hotels and Motels" + ] + }, + { + "category_id": "22012004", + "group": "place", + "hierarchy": [ + "Travel", + "Lodging", + "Hostels" + ] + }, + { + "category_id": "22012005", + "group": "place", + "hierarchy": [ + "Travel", + "Lodging", + "Cottages and Cabins" + ] + }, + { + "category_id": "22012006", + "group": "place", + "hierarchy": [ + "Travel", + "Lodging", + "Bed and Breakfasts" + ] + }, + { + "category_id": "22013000", + "group": "place", + "hierarchy": [ + "Travel", + "Parking" + ] + }, + { + "category_id": "22014000", + "group": "place", + "hierarchy": [ + "Travel", + "Public Transportation Services" + ] + }, + { + "category_id": "22015000", + "group": "place", + "hierarchy": [ + "Travel", + "Rail" + ] + }, + { + "category_id": "22016000", + "group": "special", + "hierarchy": [ + "Travel", + "Taxi" + ] + }, + { + "category_id": "22017000", + "group": "special", + "hierarchy": [ + "Travel", + "Tolls and Fees" + ] + }, + { + "category_id": "22018000", + "group": "place", + "hierarchy": [ + "Travel", + "Transportation Centers" + ] + } +] diff --git a/src/snapshots/rplaid__client__tests__can_read_identity.snap b/src/snapshots/rplaid__client__tests__can_read_identity.snap new file mode 100644 index 0000000..ce6ee2e --- /dev/null +++ b/src/snapshots/rplaid__client__tests__can_read_identity.snap @@ -0,0 +1,163 @@ +--- +source: src/client.rs +expression: res + +--- +{ + "accounts": [ + { + "account_id": "[account_id]", + "balances": { + "available": 100.0, + "current": 110.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "0000", + "name": "Plaid Checking", + "official_name": "Plaid Gold Standard 0% Interest Checking", + "type": "depository", + "subtype": "checking" + }, + { + "account_id": "[account_id]", + "balances": { + "available": 200.0, + "current": 210.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "1111", + "name": "Plaid Saving", + "official_name": "Plaid Silver Standard 0.1% Interest Saving", + "type": "depository", + "subtype": "savings" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 1000.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "2222", + "name": "Plaid CD", + "official_name": "Plaid Bronze Standard 0.2% Interest CD", + "type": "depository", + "subtype": "cd" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 410.0, + "iso_current_code": null, + "limit": 2000.0, + "unofficial_currency_code": null + }, + "mask": "3333", + "name": "Plaid Credit Card", + "official_name": "Plaid Diamond 12.5% APR Interest Credit Card", + "type": "credit", + "subtype": "credit card" + }, + { + "account_id": "[account_id]", + "balances": { + "available": 43200.0, + "current": 43200.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "4444", + "name": "Plaid Money Market", + "official_name": "Plaid Platinum Standard 1.85% Interest Money Market", + "type": "depository", + "subtype": "money market" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 320.76, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "5555", + "name": "Plaid IRA", + "official_name": null, + "type": "investment", + "subtype": "ira" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 23631.9805, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "6666", + "name": "Plaid 401k", + "official_name": null, + "type": "investment", + "subtype": "401k" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 65262.0, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "7777", + "name": "Plaid Student Loan", + "official_name": null, + "type": "loan", + "subtype": "student" + }, + { + "account_id": "[account_id]", + "balances": { + "available": null, + "current": 56302.06, + "iso_current_code": null, + "limit": null, + "unofficial_currency_code": null + }, + "mask": "8888", + "name": "Plaid Mortgage", + "official_name": null, + "type": "loan", + "subtype": "mortgage" + } + ], + "item": { + "item_id": "[item_id]", + "institution_id": "ins_129571", + "webhook": "", + "error": null, + "available_products": [ + "balance" + ], + "billed_products": [ + "assets", + "auth", + "identity", + "transactions" + ], + "consent_expiration_time": null, + "update_type": "background", + "status": null + }, + "request_id": "[request_id]" +} diff --git a/src/snapshots/rplaid__client__tests__can_read_transactions.snap b/src/snapshots/rplaid__client__tests__can_read_transactions.snap new file mode 100644 index 0000000..eb075b4 --- /dev/null +++ b/src/snapshots/rplaid__client__tests__can_read_transactions.snap @@ -0,0 +1,124 @@ +--- +source: src/client.rs +expression: res.transactions + +--- +[ + { + "transaction_type": "place", + "category_id": "17018000", + "category": [ + "Recreation", + "Gyms and Fitness Centers" + ], + "location": {}, + "payment_meta": {}, + "name": "Touchstone Climbing", + "account_id": "[account_id]", + "amount": 78.5, + "iso_currency_code": "USD", + "date": "2021-09-05", + "pending": false, + "transaction_id": "[transaction_id]", + "payment_channel": "in store", + "merchant_name": "Touchstone" + }, + { + "transaction_type": "special", + "category_id": "22001000", + "category": [ + "Travel", + "Airlines and Aviation Services" + ], + "location": {}, + "payment_meta": {}, + "name": "United Airlines", + "account_id": "[account_id]", + "amount": -500.0, + "iso_currency_code": "USD", + "date": "2021-09-05", + "pending": false, + "transaction_id": "[transaction_id]", + "payment_channel": "other", + "merchant_name": "United Airlines" + }, + { + "transaction_type": "place", + "category_id": "13005032", + "category": [ + "Food and Drink", + "Restaurants", + "Fast Food" + ], + "location": { + "store_number": "3322" + }, + "payment_meta": {}, + "name": "McDonald's", + "account_id": "[account_id]", + "amount": 12.0, + "iso_currency_code": "USD", + "date": "2021-09-04", + "pending": false, + "transaction_id": "[transaction_id]", + "payment_channel": "in store", + "merchant_name": "McDonald's" + }, + { + "transaction_type": "place", + "category_id": "13005043", + "category": [ + "Food and Drink", + "Restaurants", + "Coffee Shop" + ], + "location": {}, + "payment_meta": {}, + "name": "Starbucks", + "account_id": "[account_id]", + "amount": 4.33, + "iso_currency_code": "USD", + "date": "2021-09-04", + "pending": false, + "transaction_id": "[transaction_id]", + "payment_channel": "in store", + "merchant_name": "Starbucks" + }, + { + "transaction_type": "place", + "category_id": "13005000", + "category": [ + "Food and Drink", + "Restaurants" + ], + "location": {}, + "payment_meta": {}, + "name": "SparkFun", + "account_id": "[account_id]", + "amount": 89.4, + "iso_currency_code": "USD", + "date": "2021-09-03", + "pending": false, + "transaction_id": "[transaction_id]", + "payment_channel": "in store", + "merchant_name": "Sparkfun" + }, + { + "transaction_type": "special", + "category_id": "21005000", + "category": [ + "Transfer", + "Credit" + ], + "location": {}, + "payment_meta": {}, + "name": "INTRST PYMNT", + "account_id": "[account_id]", + "amount": -4.22, + "iso_currency_code": "USD", + "date": "2021-09-02", + "pending": false, + "transaction_id": "[transaction_id]", + "payment_channel": "other" + } +] diff --git a/src/snapshots/rplaid__client__tests__can_search_institutions.snap b/src/snapshots/rplaid__client__tests__can_search_institutions.snap new file mode 100644 index 0000000..4b9dff8 --- /dev/null +++ b/src/snapshots/rplaid__client__tests__can_search_institutions.snap @@ -0,0 +1,207 @@ +--- +source: src/client.rs +expression: res + +--- +[ + { + "institution_id": "ins_122881", + "name": "Banque Populaire - Alsace Lorraine Champagne", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122880", + "name": "Banque Populaire - Aquitaine Centre Atlantique", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122875", + "name": "Banque Populaire - Auvergne et Rhône-Alpes", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122885", + "name": "Banque Populaire - Banque de Savoie", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122883", + "name": "Banque Populaire - Grand Ouest", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122884", + "name": "Banque Populaire - Méditerranée", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122882", + "name": "Banque Populaire - Occitane", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122876", + "name": "Banque Populaire - Rives de Paris", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122879", + "name": "Banque Populaire - Sud", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + }, + { + "institution_id": "ins_122878", + "name": "Banque Populaire - du Nord", + "products": [ + "assets", + "auth", + "balance", + "transactions", + "identity", + "payment_initiation" + ], + "country_codes": [ + "FR" + ], + "url": null, + "primary_color": null, + "logo": null, + "routing_numbers": [], + "oauth": true + } +]