use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::client::Client;
use crate::errors::Result;
#[derive(Serialize, Debug, Clone)]
pub struct LinkTokenUser<'a> {
pub client_user_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub legal_name: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phone_number: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phone_number_verified_time: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email_address: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email_address_verified_time: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ssn: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date_of_birth: Option<&'a str>,
}
impl Default for LinkTokenUser<'_> {
fn default() -> Self {
Self {
client_user_id: "",
legal_name: None,
phone_number: None,
phone_number_verified_time: None,
email_address: None,
email_address_verified_time: None,
ssn: None,
date_of_birth: None,
}
}
}
#[derive(Debug, Clone)]
pub struct LinkTokenConfigs<'a> {
pub user: LinkTokenUser<'a>,
pub client_name: &'a str,
pub language: &'a str,
pub country_codes: &'a [&'a str],
pub products: Option<&'a [&'a str]>,
pub webhook: Option<&'a str>,
pub access_token: Option<&'a str>,
pub link_customization_name: Option<&'a str>,
pub account_filters: Option<HashMap<&'a str, HashMap<&'a str, Vec<&'a str>>>>,
pub redirect_uri: Option<&'a str>,
pub android_package_name: Option<&'a str>,
}
#[derive(Serialize)]
struct CreateLinkTokenRequest<'a> {
client_id: &'a str,
secret: &'a str,
client_name: &'a str,
language: &'a str,
country_codes: &'a [&'a str],
user: LinkTokenUser<'a>,
#[serde(skip_serializing_if = "Option::is_none")]
products: Option<&'a [&'a str]>,
#[serde(skip_serializing_if = "Option::is_none")]
webhook: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
access_token: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
link_customization_name: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
account_filters: Option<HashMap<&'a str, HashMap<&'a str, Vec<&'a str>>>>,
#[serde(skip_serializing_if = "Option::is_none")]
redirect_uri: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
android_package_name: Option<&'a str>,
}
impl Default for LinkTokenConfigs<'_> {
fn default() -> Self {
Self {
user: Default::default(),
client_name: "",
language: "en",
country_codes: &["US"],
products: None,
webhook: None,
access_token: None,
link_customization_name: None,
account_filters: None,
redirect_uri: None,
android_package_name: None,
}
}
}
#[derive(Serialize)]
struct GetLinkTokenRequest<'a> {
client_id: &'a str,
secret: &'a str,
link_token: &'a str,
}
#[derive(Deserialize, Debug)]
pub struct CreateLinkTokenResponse {
pub request_id: String,
pub link_token: String,
pub expiration: DateTime<Utc>,
}
#[derive(Deserialize, Debug)]
pub struct GetLinkTokenMetadataResponse {
pub initial_products: Vec<String>,
pub webhook: Option<String>,
pub country_codes: Vec<String>,
pub language: Option<String>,
pub account_filters: HashMap<String, HashMap<String, Vec<String>>>,
pub redirect_uri: Option<String>,
pub client_name: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct GetLinkTokenResponse {
pub request_id: String,
pub link_token: String,
pub created_at: Option<DateTime<Utc>>,
pub expiration: Option<DateTime<Utc>>,
pub metadata: GetLinkTokenMetadataResponse,
}
impl Client {
pub async fn create_link_token<'a>(
&self,
configs: LinkTokenConfigs<'a>,
) -> Result<CreateLinkTokenResponse> {
self.send_request(
"link/token/create",
&CreateLinkTokenRequest {
client_id: &self.client_id,
secret: &self.secret,
client_name: configs.client_name,
language: configs.language,
country_codes: configs.country_codes,
user: configs.user,
products: configs.products,
webhook: configs.webhook,
access_token: configs.access_token,
link_customization_name: configs.link_customization_name,
account_filters: configs.account_filters,
redirect_uri: configs.redirect_uri,
android_package_name: configs.android_package_name,
},
)
.await
}
pub async fn get_link_token(&self, link_token: &str) -> Result<GetLinkTokenResponse> {
self.send_request(
"link/token/get",
&GetLinkTokenRequest {
client_id: &self.client_id,
secret: &self.secret,
link_token,
},
)
.await
}
}
#[cfg(test)]
mod tests {
use crate::client::tests::get_test_client;
use super::*;
#[tokio::test]
async fn test_create_link_token_required() {
let client = get_test_client();
let time_now = Utc::now().to_rfc3339();
let resp = client
.create_link_token(LinkTokenConfigs {
user: LinkTokenUser {
client_user_id: &time_now,
..Default::default()
},
client_name: "Plaid Test",
products: Some(&["auth"]),
country_codes: &["US"],
language: "en",
..Default::default()
})
.await
.unwrap();
assert!(resp.link_token.starts_with("link-sandbox"));
assert_ne!(resp.expiration.timestamp(), 0);
}
#[tokio::test]
async fn test_create_link_token_optional() {
let client = get_test_client();
let time_now = Utc::now().to_rfc3339();
let resp = client
.create_link_token(LinkTokenConfigs {
user: LinkTokenUser {
client_user_id: &time_now,
legal_name: Some("Legal Name"),
phone_number: Some("2025550165"),
email_address: Some("test@email.com"),
phone_number_verified_time: Some(Utc::now()),
email_address_verified_time: Some(Utc::now()),
ssn: None,
date_of_birth: None,
},
client_name: "Plaid Test",
products: Some(&["auth"]),
country_codes: &["US"],
language: "en",
webhook: Some("https://webhook-uri.com"),
link_customization_name: Some("default"),
account_filters: Some(
vec![(
"depository",
vec![("account_subtypes", vec!["checking", "savings"])]
.into_iter()
.collect(),
)]
.into_iter()
.collect(),
),
..Default::default()
})
.await
.unwrap();
assert!(resp.link_token.starts_with("link-sandbox"));
assert_ne!(resp.expiration.timestamp(), 0);
}
#[tokio::test]
async fn test_create_link_token_then_get() {
let client = get_test_client();
let time_now = Utc::now().to_rfc3339();
let create_resp = client
.create_link_token(LinkTokenConfigs {
user: LinkTokenUser {
client_user_id: &time_now,
legal_name: Some("Legal Name"),
phone_number: Some("2025550165"),
email_address: Some("test@email.com"),
phone_number_verified_time: Some(Utc::now()),
email_address_verified_time: Some(Utc::now()),
ssn: None,
date_of_birth: None,
},
client_name: "Plaid Test",
products: Some(&["auth"]),
country_codes: &["US"],
language: "en",
webhook: Some("https://webhook-uri.com"),
link_customization_name: Some("default"),
account_filters: Some(
vec![(
"depository",
vec![("account_subtypes", vec!["checking", "savings"])]
.into_iter()
.collect(),
)]
.into_iter()
.collect(),
),
..Default::default()
})
.await
.unwrap();
assert!(create_resp.link_token.starts_with("link-sandbox"));
assert_ne!(create_resp.expiration.timestamp(), 0);
let get_resp = client
.get_link_token(&create_resp.link_token)
.await
.unwrap();
assert_eq!(create_resp.link_token, get_resp.link_token);
assert_eq!(get_resp.metadata.initial_products, &["auth"]);
assert_eq!(
get_resp.metadata.webhook,
Some("https://webhook-uri.com".to_string())
);
assert_eq!(get_resp.metadata.country_codes, &["US"]);
assert_eq!(get_resp.metadata.language, Some("en".to_string()));
assert_eq!(get_resp.metadata.account_filters.len(), 1);
assert_eq!(
get_resp.metadata.client_name,
Some("Plaid Test".to_string())
);
assert_ne!(get_resp.expiration.unwrap().timestamp(), 0);
assert_ne!(get_resp.created_at.unwrap().timestamp(), 0);
}
}