use crate::{EntityType, PayrixClient, Result, SearchBuilder};
use serde::Serialize;
pub use super::customer_management::Address;
#[derive(Debug, Clone)]
pub struct CardData {
pub number: String,
pub exp_month: String,
pub exp_year: String,
pub cvv: String,
pub address: Address,
pub name: String,
}
#[derive(Debug, Clone)]
pub struct BankData {
pub account_number: String,
pub routing_number: String,
pub account_type: BankAccountType,
pub name: String,
}
#[derive(Debug, Clone, Copy)]
pub enum BankAccountType {
Checking,
Savings,
}
#[derive(Debug, Serialize)]
pub struct PaymentMethod {
pub id: String,
pub payment_token: String,
pub payment_type: PaymentType,
pub number: String,
pub routing: Option<String>,
pub exp_month: Option<String>,
pub exp_year: Option<String>,
pub card_type: Option<String>,
pub account_type: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum PaymentType {
Card,
Bank,
}
pub async fn tokenize_credit_card(
client: &PayrixClient,
customer_id: &str,
login_id: &str,
card_data: &CardData,
description: &str,
) -> Result<serde_json::Value> {
let card_type = detect_card_type(&card_data.number)?;
let exp_month_padded = format!("{:02}", card_data.exp_month.parse::<u32>().unwrap_or(0));
let exp_year_short = if card_data.exp_year.len() == 4 {
&card_data.exp_year[2..]
} else {
&card_data.exp_year
};
let expiration = format!("{}{}", exp_month_padded, exp_year_short);
let token_data = serde_json::json!({
"customer": customer_id,
"payment": {
"method": card_type,
"number": card_data.number,
"expiration": expiration,
"cvv": card_data.cvv,
},
"name": card_data.name,
"login": login_id,
"description": description,
"inactive": 0,
"frozen": 0,
});
let token = client
.create::<_, serde_json::Value>(EntityType::Tokens, &token_data)
.await?;
tracing::info!(
"Tokenized credit card {}",
token
.get("id")
.and_then(|i| i.as_str())
.unwrap_or("unknown")
);
Ok(token)
}
pub async fn tokenize_bank_account(
client: &PayrixClient,
customer_id: &str,
login_id: &str,
bank_data: &BankData,
description: &str,
) -> Result<serde_json::Value> {
let method = match bank_data.account_type {
BankAccountType::Checking => 100,
BankAccountType::Savings => 101,
};
let token_data = serde_json::json!({
"customer": customer_id,
"payment": {
"method": method,
"number": bank_data.account_number,
"routing": bank_data.routing_number,
},
"name": bank_data.name,
"login": login_id,
"description": description,
"inactive": 0,
"frozen": 0,
});
let token = client
.create::<_, serde_json::Value>(EntityType::Tokens, &token_data)
.await?;
tracing::info!(
"Tokenized bank account {}",
token
.get("id")
.and_then(|i| i.as_str())
.unwrap_or("unknown")
);
Ok(token)
}
pub async fn get_tokens_for_customer(
client: &PayrixClient,
customer_id: &str,
) -> Result<Vec<serde_json::Value>> {
let search = SearchBuilder::new()
.field("customer", customer_id)
.expand("payment")
.build();
let tokens = client
.search::<serde_json::Value>(EntityType::Tokens, &search)
.await?;
Ok(tokens)
}
pub async fn get_token(
client: &PayrixClient,
token_id: &str,
) -> Result<Option<serde_json::Value>> {
client
.get_one::<serde_json::Value>(EntityType::Tokens, token_id)
.await
}
pub async fn deactivate_token(
client: &PayrixClient,
token_id: &str,
) -> Result<serde_json::Value> {
let update = serde_json::json!({ "inactive": 1 });
let token = client
.update::<_, serde_json::Value>(EntityType::Tokens, token_id, &update)
.await?;
tracing::info!("Deactivated token {}", token_id);
Ok(token)
}
fn detect_card_type(card_number: &str) -> Result<i32> {
let first_digit = card_number.chars().next().unwrap_or('0');
let first_two: i32 = card_number[..2.min(card_number.len())]
.parse()
.unwrap_or(0);
match first_digit {
'4' => Ok(1), '5' => Ok(2), '3' => {
if first_two == 34 || first_two == 37 {
Ok(3) } else {
Ok(6) }
}
'6' => Ok(4), _ => Err(crate::Error::BadRequest(format!(
"Unsupported card type for card starting with {}",
first_digit
))),
}
}
pub fn get_card_type_name(method: i64) -> String {
match method {
1 => "Visa".to_string(),
2 => "Mastercard".to_string(),
3 => "American Express".to_string(),
4 => "Discover".to_string(),
5 => "Diners Club".to_string(),
6 => "JCB".to_string(),
_ => "Unknown".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_visa() {
assert_eq!(detect_card_type("4111111111111111").unwrap(), 1);
}
#[test]
fn test_detect_mastercard() {
assert_eq!(detect_card_type("5555555555554444").unwrap(), 2);
}
#[test]
fn test_detect_amex() {
assert_eq!(detect_card_type("378282246310005").unwrap(), 3);
assert_eq!(detect_card_type("341234567890123").unwrap(), 3);
}
#[test]
fn test_detect_discover() {
assert_eq!(detect_card_type("6011111111111117").unwrap(), 4);
}
#[test]
fn test_detect_jcb() {
assert_eq!(detect_card_type("3530111333300000").unwrap(), 6);
}
#[test]
fn test_card_type_names() {
assert_eq!(get_card_type_name(1), "Visa");
assert_eq!(get_card_type_name(2), "Mastercard");
assert_eq!(get_card_type_name(3), "American Express");
assert_eq!(get_card_type_name(4), "Discover");
assert_eq!(get_card_type_name(6), "JCB");
}
#[test]
fn test_bank_account_types() {
let checking = BankAccountType::Checking;
let savings = BankAccountType::Savings;
match checking {
BankAccountType::Checking => assert!(true),
BankAccountType::Savings => panic!("Wrong type"),
}
match savings {
BankAccountType::Savings => assert!(true),
BankAccountType::Checking => panic!("Wrong type"),
}
}
}