use serde::{Deserialize, Serialize};
use crate::client::PayrixClient;
use crate::entity::EntityType;
use crate::error::Result;
use crate::types::{
Account, AccountHolderType, AccountType, DateYmd, Entity, Member, MemberType, Merchant,
MerchantEnvironment, MerchantStatus, MerchantType,
};
#[derive(Debug, Clone)]
pub struct OnboardMerchantRequest {
pub business: BusinessInfo,
pub merchant: MerchantConfig,
pub accounts: Vec<BankAccountInfo>,
pub members: Vec<MemberInfo>,
pub terms_acceptance: TermsAcceptance,
}
#[derive(Debug, Clone)]
pub struct BusinessInfo {
pub business_type: MerchantType,
pub legal_name: String,
pub address: Address,
pub phone: String,
pub email: String,
pub website: Option<String>,
pub ein: String,
}
#[derive(Debug, Clone)]
pub struct MerchantConfig {
pub dba: String,
pub mcc: String,
pub environment: MerchantEnvironment,
pub annual_cc_sales: i64,
pub avg_ticket: i64,
pub established: DateYmd,
pub is_new_business: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BankAccountMethod {
#[default]
Checking,
Savings,
}
#[derive(Clone)]
pub struct BankAccountInfo {
pub name: Option<String>,
pub routing_number: Option<String>,
pub account_number: Option<String>,
pub holder_type: AccountHolderType,
pub account_method: BankAccountMethod,
pub transaction_type: AccountType,
pub currency: Option<String>,
pub is_primary: bool,
pub plaid_public_token: Option<String>,
}
#[derive(Clone)]
pub struct MemberInfo {
pub member_type: MemberType,
pub first_name: String,
pub last_name: String,
pub title: Option<String>,
pub ownership_percentage: i32,
pub date_of_birth: String,
pub ssn: String,
pub email: String,
pub phone: String,
pub address: Address,
}
#[derive(Debug, Clone)]
pub struct Address {
pub line1: String,
pub line2: Option<String>,
pub city: String,
pub state: String,
pub zip: String,
pub country: String,
}
#[derive(Debug, Clone)]
pub struct TermsAcceptance {
pub version: String,
pub accepted_at: String,
}
#[derive(Debug, Clone)]
pub struct OnboardMerchantResult {
pub entity_id: String,
pub merchant_id: String,
pub boarding_status: BoardingStatus,
pub entity: Entity,
pub merchant: Merchant,
pub accounts: Vec<Account>,
pub members: Vec<Member>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoardingStatus {
NotReady,
Submitted,
Boarded,
ManualReview,
Closed,
Incomplete,
Pending,
}
impl From<MerchantStatus> for BoardingStatus {
fn from(status: MerchantStatus) -> Self {
match status {
MerchantStatus::NotReady => BoardingStatus::NotReady,
MerchantStatus::Ready => BoardingStatus::Submitted,
MerchantStatus::Boarded => BoardingStatus::Boarded,
MerchantStatus::Manual => BoardingStatus::ManualReview,
MerchantStatus::Closed => BoardingStatus::Closed,
MerchantStatus::Incomplete => BoardingStatus::Incomplete,
MerchantStatus::Pending => BoardingStatus::Pending,
}
}
}
impl std::fmt::Display for BoardingStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BoardingStatus::NotReady => write!(f, "Not Ready"),
BoardingStatus::Submitted => write!(f, "Submitted"),
BoardingStatus::Boarded => write!(f, "Boarded"),
BoardingStatus::ManualReview => write!(f, "Manual Review"),
BoardingStatus::Closed => write!(f, "Closed"),
BoardingStatus::Incomplete => write!(f, "Incomplete"),
BoardingStatus::Pending => write!(f, "Pending"),
}
}
}
#[derive(Debug, Clone)]
pub struct BoardingStatusResult {
pub status: BoardingStatus,
pub merchant_id: String,
pub entity_id: String,
pub boarded_date: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct PayrixOnboardingPayload {
#[serde(rename = "type")]
entity_type: MerchantType,
name: String,
address1: String,
#[serde(skip_serializing_if = "Option::is_none")]
address2: Option<String>,
city: String,
state: String,
zip: String,
country: String,
phone: String,
email: String,
#[serde(skip_serializing_if = "Option::is_none")]
website: Option<String>,
ein: String,
tc_version: String,
tc_date: String,
tc_attestation: i32,
accounts: Vec<PayrixAccountPayload>,
merchant: PayrixMerchantPayload,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct PayrixAccountPayload {
primary: i32,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(rename = "type")]
transaction_type: AccountType,
#[serde(skip_serializing_if = "Option::is_none")]
currency: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
account: Option<PayrixAccountDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
public_token: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct PayrixAccountDetails {
method: i32,
number: String,
routing: String,
holder_type: AccountHolderType,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct PayrixMerchantPayload {
dba: String,
new: i32,
mcc: String,
status: i32,
environment: MerchantEnvironment,
annual_cc_sales: i64,
avg_ticket: i64,
established: String,
members: Vec<PayrixMemberPayload>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct PayrixMemberPayload {
#[serde(rename = "type")]
member_type: MemberType,
first: String,
last: String,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
ownership: i32,
dob: String,
ssn: String,
email: String,
phone: String,
address1: String,
#[serde(skip_serializing_if = "Option::is_none")]
address2: Option<String>,
city: String,
state: String,
zip: String,
country: String,
}
fn mask_sensitive(value: &str) -> String {
if value.len() <= 4 {
"*".repeat(value.len())
} else {
format!("{}{}", "*".repeat(value.len() - 4), &value[value.len() - 4..])
}
}
impl std::fmt::Debug for BankAccountInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BankAccountInfo")
.field("name", &self.name)
.field("routing_number", &self.routing_number.as_ref().map(|s| mask_sensitive(s)))
.field("account_number", &self.account_number.as_ref().map(|s| mask_sensitive(s)))
.field("holder_type", &self.holder_type)
.field("account_method", &self.account_method)
.field("transaction_type", &self.transaction_type)
.field("currency", &self.currency)
.field("is_primary", &self.is_primary)
.field("plaid_public_token", &self.plaid_public_token.as_ref().map(|_| "[REDACTED]"))
.finish()
}
}
impl std::fmt::Debug for MemberInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemberInfo")
.field("member_type", &self.member_type)
.field("first_name", &self.first_name)
.field("last_name", &self.last_name)
.field("title", &self.title)
.field("ownership_percentage", &self.ownership_percentage)
.field("date_of_birth", &self.date_of_birth)
.field("ssn", &mask_sensitive(&self.ssn))
.field("email", &self.email)
.field("phone", &self.phone)
.field("address", &self.address)
.finish()
}
}
impl std::fmt::Debug for PayrixAccountDetails {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PayrixAccountDetails")
.field("method", &self.method)
.field("number", &mask_sensitive(&self.number))
.field("routing", &mask_sensitive(&self.routing))
.field("holder_type", &self.holder_type)
.finish()
}
}
impl std::fmt::Debug for PayrixMemberPayload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PayrixMemberPayload")
.field("member_type", &self.member_type)
.field("first", &self.first)
.field("last", &self.last)
.field("title", &self.title)
.field("ownership", &self.ownership)
.field("dob", &self.dob)
.field("ssn", &mask_sensitive(&self.ssn))
.field("email", &self.email)
.field("phone", &self.phone)
.field("address1", &self.address1)
.field("address2", &self.address2)
.field("city", &self.city)
.field("state", &self.state)
.field("zip", &self.zip)
.field("country", &self.country)
.finish()
}
}
impl From<OnboardMerchantRequest> for PayrixOnboardingPayload {
fn from(request: OnboardMerchantRequest) -> Self {
PayrixOnboardingPayload {
entity_type: request.business.business_type,
name: request.business.legal_name,
address1: request.business.address.line1,
address2: request.business.address.line2,
city: request.business.address.city,
state: request.business.address.state,
zip: request.business.address.zip,
country: request.business.address.country,
phone: request.business.phone,
email: request.business.email,
website: request.business.website,
ein: request.business.ein,
tc_version: request.terms_acceptance.version,
tc_date: request.terms_acceptance.accepted_at,
tc_attestation: 1,
accounts: request.accounts.into_iter().map(|a| a.into()).collect(),
merchant: PayrixMerchantPayload {
dba: request.merchant.dba,
new: if request.merchant.is_new_business { 1 } else { 0 },
mcc: request.merchant.mcc,
status: 1, environment: request.merchant.environment,
annual_cc_sales: request.merchant.annual_cc_sales,
avg_ticket: request.merchant.avg_ticket,
established: request.merchant.established.as_str().to_string(),
members: request.members.into_iter().map(|m| m.into()).collect(),
},
}
}
}
impl From<BankAccountInfo> for PayrixAccountPayload {
fn from(account: BankAccountInfo) -> Self {
let account_details = match (&account.routing_number, &account.account_number) {
(Some(routing), Some(number)) => {
let method = match (account.holder_type, account.account_method) {
(AccountHolderType::Individual, BankAccountMethod::Checking) => 8,
(AccountHolderType::Individual, BankAccountMethod::Savings) => 9,
(AccountHolderType::Business, BankAccountMethod::Checking) => 10,
(AccountHolderType::Business, BankAccountMethod::Savings) => 11,
};
Some(PayrixAccountDetails {
method,
number: number.clone(),
routing: routing.clone(),
holder_type: account.holder_type,
})
}
_ => None,
};
PayrixAccountPayload {
primary: if account.is_primary { 1 } else { 0 },
name: account.name,
transaction_type: account.transaction_type,
currency: account.currency,
account: account_details,
public_token: account.plaid_public_token,
}
}
}
impl From<MemberInfo> for PayrixMemberPayload {
fn from(member: MemberInfo) -> Self {
PayrixMemberPayload {
member_type: member.member_type,
first: member.first_name,
last: member.last_name,
title: member.title,
ownership: member.ownership_percentage,
dob: member.date_of_birth,
ssn: member.ssn,
email: member.email,
phone: member.phone,
address1: member.address.line1,
address2: member.address.line2,
city: member.address.city,
state: member.address.state,
zip: member.address.zip,
country: member.address.country,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct PayrixOnboardingResponse {
id: String,
#[serde(default)]
merchant: Option<MerchantInResponse>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct MerchantInResponse {
id: String,
#[serde(default)]
status: Option<MerchantStatus>,
#[serde(default)]
entity: Option<String>,
#[serde(default)]
boarded: Option<String>,
#[serde(default)]
members: Option<Vec<Member>>,
}
fn validate_request(request: &OnboardMerchantRequest) -> Result<()> {
if request.accounts.is_empty() {
return Err(crate::error::Error::Config(
"At least one bank account is required".into(),
));
}
if !request.accounts.iter().any(|a| a.is_primary) {
return Err(crate::error::Error::Config(
"One account must be marked as primary".into(),
));
}
for (i, account) in request.accounts.iter().enumerate() {
let has_manual = account.routing_number.is_some() && account.account_number.is_some();
let has_plaid = account.plaid_public_token.is_some();
if !has_manual && !has_plaid {
return Err(crate::error::Error::Config(format!(
"Account {} requires either routing/account numbers or a Plaid token",
i + 1
)));
}
if let Some(ref routing) = account.routing_number {
if routing.len() != 9 || !routing.chars().all(|c| c.is_ascii_digit()) {
return Err(crate::error::Error::Config(format!(
"Account {} routing number must be exactly 9 digits",
i + 1
)));
}
}
}
if request.members.is_empty() {
return Err(crate::error::Error::Config(
"At least one member (owner or control person) is required".into(),
));
}
for (i, member) in request.members.iter().enumerate() {
if member.ssn.len() != 9 || !member.ssn.chars().all(|c| c.is_ascii_digit()) {
return Err(crate::error::Error::Config(format!(
"Member {} SSN must be exactly 9 digits (no dashes)",
i + 1
)));
}
if member.date_of_birth.len() != 8
|| !member.date_of_birth.chars().all(|c| c.is_ascii_digit())
{
return Err(crate::error::Error::Config(format!(
"Member {} date of birth must be in YYYYMMDD format (8 digits)",
i + 1
)));
}
if member.ownership_percentage < 0 || member.ownership_percentage > 100 {
return Err(crate::error::Error::Config(format!(
"Member {} ownership percentage must be between 0 and 100",
i + 1
)));
}
}
if request.business.ein.len() != 9
|| !request.business.ein.chars().all(|c| c.is_ascii_digit())
{
return Err(crate::error::Error::Config(
"EIN must be exactly 9 digits (no dashes)".into(),
));
}
Ok(())
}
pub async fn onboard_merchant(
client: &PayrixClient,
request: OnboardMerchantRequest,
) -> Result<OnboardMerchantResult> {
validate_request(&request)?;
let payload: PayrixOnboardingPayload = request.into();
let response: PayrixOnboardingResponse = client.create(EntityType::Entities, &payload).await?;
let merchant_response = response.merchant.ok_or_else(|| {
crate::error::Error::Internal("API response missing merchant data".into())
})?;
if merchant_response.id.is_empty() {
return Err(crate::error::Error::Internal(
"API response contains empty merchant ID".into(),
));
}
let boarding_status = merchant_response
.status
.map(BoardingStatus::from)
.unwrap_or(BoardingStatus::NotReady);
let entity: Entity = client
.get_one(EntityType::Entities, &response.id)
.await?
.ok_or_else(|| crate::error::Error::Internal("Entity not found after creation".into()))?;
let merchant: Merchant = client
.get_one(EntityType::Merchants, &merchant_response.id)
.await?
.ok_or_else(|| crate::error::Error::Internal("Merchant not found after creation".into()))?;
let accounts: Vec<Account> = client
.search(
EntityType::Accounts,
&format!("entity[equals]={}", response.id),
)
.await?;
let members: Vec<Member> = client
.search(
EntityType::Members,
&format!("merchant[equals]={}", merchant_response.id),
)
.await?;
Ok(OnboardMerchantResult {
entity_id: response.id,
merchant_id: merchant_response.id,
boarding_status,
entity,
merchant,
accounts,
members,
})
}
pub async fn check_boarding_status(
client: &PayrixClient,
merchant_id: &str,
) -> Result<BoardingStatusResult> {
let merchant: Merchant = client
.get_one(EntityType::Merchants, merchant_id)
.await?
.ok_or_else(|| crate::error::Error::NotFound(format!("Merchant not found: {}", merchant_id)))?;
let status = merchant
.status
.map(BoardingStatus::from)
.unwrap_or(BoardingStatus::NotReady);
Ok(BoardingStatusResult {
status,
merchant_id: merchant.id.as_str().to_string(),
entity_id: merchant
.entity
.map(|e| e.as_str().to_string())
.unwrap_or_default(),
boarded_date: merchant.boarded.map(|d| d.as_str().to_string()),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mask_sensitive_full_ssn() {
let result = mask_sensitive("123456789");
assert_eq!(result, "*****6789");
}
#[test]
fn test_mask_sensitive_short_value() {
let result = mask_sensitive("1234");
assert_eq!(result, "****");
}
#[test]
fn test_mask_sensitive_empty() {
let result = mask_sensitive("");
assert_eq!(result, "");
}
#[test]
fn test_bank_account_debug_masks_sensitive() {
let account = BankAccountInfo {
name: Some("Test Account".to_string()),
routing_number: Some("123456789".to_string()),
account_number: Some("9876543210".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: Some("USD".to_string()),
is_primary: true,
plaid_public_token: None,
};
let debug_str = format!("{:?}", account);
assert!(!debug_str.contains("123456789"));
assert!(!debug_str.contains("9876543210"));
assert!(debug_str.contains("*****6789"));
assert!(debug_str.contains("******3210"));
}
#[test]
fn test_member_info_debug_masks_ssn() {
let member = MemberInfo {
member_type: MemberType::Owner,
first_name: "John".to_string(),
last_name: "Doe".to_string(),
title: Some("CEO".to_string()),
ownership_percentage: 100,
date_of_birth: "19800115".to_string(),
ssn: "123456789".to_string(),
email: "john@example.com".to_string(),
phone: "5551234567".to_string(),
address: Address {
line1: "123 Main St".to_string(),
line2: None,
city: "Chicago".to_string(),
state: "IL".to_string(),
zip: "60601".to_string(),
country: "USA".to_string(),
},
};
let debug_str = format!("{:?}", member);
assert!(!debug_str.contains("123456789"));
assert!(debug_str.contains("*****6789"));
assert!(debug_str.contains("John"));
}
fn valid_request() -> OnboardMerchantRequest {
OnboardMerchantRequest {
business: BusinessInfo {
business_type: MerchantType::LimitedLiabilityCorporation,
legal_name: "Test LLC".to_string(),
address: Address {
line1: "123 Main St".to_string(),
line2: None,
city: "Chicago".to_string(),
state: "IL".to_string(),
zip: "60601".to_string(),
country: "USA".to_string(),
},
phone: "5551234567".to_string(),
email: "test@example.com".to_string(),
website: None,
ein: "123456789".to_string(),
},
merchant: MerchantConfig {
dba: "Test DBA".to_string(),
mcc: "5999".to_string(),
environment: MerchantEnvironment::Ecommerce,
annual_cc_sales: 100000,
avg_ticket: 5000,
established: DateYmd::new("20200101").unwrap(),
is_new_business: false,
},
accounts: vec![BankAccountInfo {
name: Some("Operating".to_string()),
routing_number: Some("123456789".to_string()),
account_number: Some("987654321".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: Some("USD".to_string()),
is_primary: true,
plaid_public_token: None,
}],
members: vec![MemberInfo {
member_type: MemberType::Owner,
first_name: "John".to_string(),
last_name: "Doe".to_string(),
title: Some("CEO".to_string()),
ownership_percentage: 100,
date_of_birth: "19800115".to_string(),
ssn: "123456789".to_string(),
email: "john@example.com".to_string(),
phone: "5551234567".to_string(),
address: Address {
line1: "456 Oak Ave".to_string(),
line2: None,
city: "Chicago".to_string(),
state: "IL".to_string(),
zip: "60602".to_string(),
country: "USA".to_string(),
},
}],
terms_acceptance: TermsAcceptance {
version: "4.21".to_string(),
accepted_at: "2024-01-15 10:30:00".to_string(),
},
}
}
#[test]
fn test_validate_valid_request() {
let request = valid_request();
assert!(validate_request(&request).is_ok());
}
#[test]
fn test_validate_empty_accounts() {
let mut request = valid_request();
request.accounts = vec![];
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("bank account"));
}
#[test]
fn test_validate_no_primary_account() {
let mut request = valid_request();
request.accounts[0].is_primary = false;
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("primary"));
}
#[test]
fn test_validate_account_missing_routing_and_plaid() {
let mut request = valid_request();
request.accounts[0].routing_number = None;
request.accounts[0].account_number = None;
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("routing/account numbers or a Plaid token"));
}
#[test]
fn test_validate_invalid_routing_number() {
let mut request = valid_request();
request.accounts[0].routing_number = Some("12345".to_string()); let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("routing number"));
}
#[test]
fn test_validate_routing_number_with_dashes() {
let mut request = valid_request();
request.accounts[0].routing_number = Some("123-456-789".to_string());
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("routing number"));
}
#[test]
fn test_validate_empty_members() {
let mut request = valid_request();
request.members = vec![];
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("member"));
}
#[test]
fn test_validate_invalid_ssn() {
let mut request = valid_request();
request.members[0].ssn = "123-45-6789".to_string(); let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("SSN"));
}
#[test]
fn test_validate_ssn_too_short() {
let mut request = valid_request();
request.members[0].ssn = "12345".to_string();
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("SSN"));
}
#[test]
fn test_validate_invalid_dob() {
let mut request = valid_request();
request.members[0].date_of_birth = "1980-01-15".to_string(); let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("date of birth"));
}
#[test]
fn test_validate_ownership_over_100() {
let mut request = valid_request();
request.members[0].ownership_percentage = 150;
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("ownership"));
}
#[test]
fn test_validate_negative_ownership() {
let mut request = valid_request();
request.members[0].ownership_percentage = -10;
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("ownership"));
}
#[test]
fn test_validate_invalid_ein() {
let mut request = valid_request();
request.business.ein = "12-3456789".to_string(); let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("EIN"));
}
#[test]
fn test_validate_ein_too_short() {
let mut request = valid_request();
request.business.ein = "12345".to_string();
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("EIN"));
}
#[test]
fn test_validate_plaid_token_valid() {
let mut request = valid_request();
request.accounts[0].routing_number = None;
request.accounts[0].account_number = None;
request.accounts[0].plaid_public_token = Some("public-token-xxx".to_string());
assert!(validate_request(&request).is_ok());
}
#[test]
fn test_validate_routing_number_with_letters() {
let mut request = valid_request();
request.accounts[0].routing_number = Some("12345678A".to_string());
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("routing number"));
}
#[test]
fn test_validate_ssn_with_letters() {
let mut request = valid_request();
request.members[0].ssn = "12345678A".to_string();
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("SSN"));
}
#[test]
fn test_validate_dob_too_short() {
let mut request = valid_request();
request.members[0].date_of_birth = "1980".to_string();
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("date of birth"));
}
#[test]
fn test_validate_ein_with_letters() {
let mut request = valid_request();
request.business.ein = "12345678A".to_string();
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("EIN"));
}
#[test]
fn test_validate_ownership_zero_valid() {
let mut request = valid_request();
request.members[0].ownership_percentage = 0;
assert!(validate_request(&request).is_ok());
}
#[test]
fn test_validate_ownership_100_valid() {
let mut request = valid_request();
request.members[0].ownership_percentage = 100;
assert!(validate_request(&request).is_ok());
}
#[test]
fn test_validate_second_account_fails() {
let mut request = valid_request();
request.accounts.push(BankAccountInfo {
name: Some("Second".to_string()),
routing_number: Some("invalid".to_string()), account_number: Some("123456".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::Credit,
currency: None,
is_primary: false,
plaid_public_token: None,
});
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("Account 2"));
}
#[test]
fn test_validate_second_member_fails() {
let mut request = valid_request();
request.members.push(MemberInfo {
member_type: MemberType::Owner,
first_name: "Jane".to_string(),
last_name: "Doe".to_string(),
title: None,
ownership_percentage: 50,
date_of_birth: "19850520".to_string(),
ssn: "invalid".to_string(), email: "jane@example.com".to_string(),
phone: "5559876543".to_string(),
address: Address {
line1: "789 Pine St".to_string(),
line2: None,
city: "Chicago".to_string(),
state: "IL".to_string(),
zip: "60603".to_string(),
country: "USA".to_string(),
},
});
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("Member 2"));
}
#[test]
fn test_boarding_status_from_merchant_status() {
assert_eq!(
BoardingStatus::from(MerchantStatus::NotReady),
BoardingStatus::NotReady
);
assert_eq!(
BoardingStatus::from(MerchantStatus::Ready),
BoardingStatus::Submitted
);
assert_eq!(
BoardingStatus::from(MerchantStatus::Boarded),
BoardingStatus::Boarded
);
assert_eq!(
BoardingStatus::from(MerchantStatus::Manual),
BoardingStatus::ManualReview
);
assert_eq!(
BoardingStatus::from(MerchantStatus::Closed),
BoardingStatus::Closed
);
assert_eq!(
BoardingStatus::from(MerchantStatus::Incomplete),
BoardingStatus::Incomplete
);
assert_eq!(
BoardingStatus::from(MerchantStatus::Pending),
BoardingStatus::Pending
);
}
#[test]
fn test_boarding_status_display() {
assert_eq!(format!("{}", BoardingStatus::NotReady), "Not Ready");
assert_eq!(format!("{}", BoardingStatus::Submitted), "Submitted");
assert_eq!(format!("{}", BoardingStatus::Boarded), "Boarded");
assert_eq!(format!("{}", BoardingStatus::ManualReview), "Manual Review");
assert_eq!(format!("{}", BoardingStatus::Closed), "Closed");
assert_eq!(format!("{}", BoardingStatus::Incomplete), "Incomplete");
assert_eq!(format!("{}", BoardingStatus::Pending), "Pending");
}
#[test]
fn test_onboarding_payload_serialization() {
let request = OnboardMerchantRequest {
business: BusinessInfo {
business_type: MerchantType::LimitedLiabilityCorporation,
legal_name: "Test Business LLC".to_string(),
address: Address {
line1: "123 Main St".to_string(),
line2: Some("Suite 100".to_string()),
city: "Springfield".to_string(),
state: "IL".to_string(),
zip: "62701".to_string(),
country: "USA".to_string(),
},
phone: "5551234567".to_string(),
email: "test@example.com".to_string(),
website: Some("https://example.com".to_string()),
ein: "123456789".to_string(),
},
merchant: MerchantConfig {
dba: "Test DBA".to_string(),
mcc: "5999".to_string(),
environment: MerchantEnvironment::Ecommerce,
annual_cc_sales: 50000000,
avg_ticket: 5000,
established: DateYmd::new("20200101").unwrap(),
is_new_business: false,
},
accounts: vec![BankAccountInfo {
name: Some("Test Account".to_string()),
routing_number: Some("123456789".to_string()),
account_number: Some("987654321".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: Some("USD".to_string()),
is_primary: true,
plaid_public_token: None,
}],
members: vec![MemberInfo {
member_type: MemberType::Owner,
first_name: "John".to_string(),
last_name: "Doe".to_string(),
title: Some("CEO".to_string()),
ownership_percentage: 100,
date_of_birth: "19800115".to_string(),
ssn: "123456789".to_string(),
email: "john@example.com".to_string(),
phone: "5559876543".to_string(),
address: Address {
line1: "456 Oak Ave".to_string(),
line2: None,
city: "Springfield".to_string(),
state: "IL".to_string(),
zip: "62702".to_string(),
country: "USA".to_string(),
},
}],
terms_acceptance: TermsAcceptance {
version: "4.21".to_string(),
accepted_at: "2024-01-15 10:30:00".to_string(),
},
};
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string_pretty(&payload).unwrap();
assert!(json.contains("\"type\": 2"), "Expected LLC type (2) in JSON: {}", json);
assert!(json.contains("\"name\": \"Test Business LLC\""));
assert!(json.contains("\"address1\": \"123 Main St\""));
assert!(json.contains("\"tcVersion\": \"4.21\""));
assert!(json.contains("\"tcAttestation\": 1"));
assert!(json.contains("\"dba\": \"Test DBA\""));
assert!(json.contains("\"mcc\": \"5999\""));
assert!(json.contains("\"status\": 1")); assert!(json.contains("\"primary\": 1"));
assert!(json.contains("\"routing\": \"123456789\""));
assert!(json.contains("\"first\": \"John\""));
assert!(json.contains("\"ownership\": 100"));
}
#[test]
fn test_account_payload_conversion() {
let account = BankAccountInfo {
name: Some("Operating Account".to_string()),
routing_number: Some("123456789".to_string()),
account_number: Some("987654321".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: Some("USD".to_string()),
is_primary: true,
plaid_public_token: None,
};
let payload: PayrixAccountPayload = account.into();
assert_eq!(payload.primary, 1);
assert_eq!(payload.name, Some("Operating Account".to_string()));
assert_eq!(payload.transaction_type, AccountType::All);
assert_eq!(payload.currency, Some("USD".to_string()));
assert!(payload.account.is_some());
let account_details = payload.account.unwrap();
assert_eq!(account_details.routing, "123456789");
assert_eq!(account_details.number, "987654321");
assert_eq!(account_details.method, 10); assert_eq!(account_details.holder_type, AccountHolderType::Business);
}
#[test]
fn test_account_payload_with_plaid() {
let account = BankAccountInfo {
name: Some("Plaid Account".to_string()),
routing_number: None,
account_number: None,
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::Credit, currency: Some("USD".to_string()),
is_primary: false,
plaid_public_token: Some("public-sandbox-xxx".to_string()),
};
let payload: PayrixAccountPayload = account.into();
assert_eq!(payload.primary, 0);
assert_eq!(payload.transaction_type, AccountType::Credit);
assert!(payload.account.is_none()); assert_eq!(payload.public_token, Some("public-sandbox-xxx".to_string()));
}
#[test]
fn test_member_payload_conversion() {
let member = MemberInfo {
member_type: MemberType::Owner,
first_name: "Jane".to_string(),
last_name: "Smith".to_string(),
title: Some("President".to_string()),
ownership_percentage: 50,
date_of_birth: "19850620".to_string(),
ssn: "987654321".to_string(),
email: "jane@example.com".to_string(),
phone: "5551112222".to_string(),
address: Address {
line1: "789 Pine Rd".to_string(),
line2: None,
city: "Chicago".to_string(),
state: "IL".to_string(),
zip: "60601".to_string(),
country: "USA".to_string(),
},
};
let payload: PayrixMemberPayload = member.into();
assert_eq!(payload.first, "Jane");
assert_eq!(payload.last, "Smith");
assert_eq!(payload.title, Some("President".to_string()));
assert_eq!(payload.ownership, 50);
assert_eq!(payload.dob, "19850620");
assert_eq!(payload.ssn, "987654321");
}
#[test]
fn test_trust_and_operating_account_scenario() {
let operating_account = BankAccountInfo {
name: Some("Operating Account".to_string()),
routing_number: Some("123456789".to_string()),
account_number: Some("111111111".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All, currency: Some("USD".to_string()),
is_primary: true, plaid_public_token: None,
};
let trust_account = BankAccountInfo {
name: Some("Client Trust Account".to_string()),
routing_number: Some("123456789".to_string()),
account_number: Some("222222222".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::Credit, currency: Some("USD".to_string()),
is_primary: false, plaid_public_token: None,
};
let operating_payload: PayrixAccountPayload = operating_account.into();
let trust_payload: PayrixAccountPayload = trust_account.into();
assert_eq!(operating_payload.primary, 1);
assert_eq!(operating_payload.transaction_type, AccountType::All);
assert_eq!(operating_payload.name, Some("Operating Account".to_string()));
let operating_details = operating_payload.account.unwrap();
assert_eq!(operating_details.number, "111111111");
assert_eq!(trust_payload.primary, 0);
assert_eq!(trust_payload.transaction_type, AccountType::Credit);
assert_eq!(trust_payload.name, Some("Client Trust Account".to_string()));
let trust_details = trust_payload.account.unwrap();
assert_eq!(trust_details.number, "222222222");
let accounts = vec![
BankAccountInfo {
name: Some("Operating Account".to_string()),
routing_number: Some("123456789".to_string()),
account_number: Some("111111111".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: Some("USD".to_string()),
is_primary: true,
plaid_public_token: None,
},
BankAccountInfo {
name: Some("Client Trust Account".to_string()),
routing_number: Some("123456789".to_string()),
account_number: Some("222222222".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::Credit,
currency: Some("USD".to_string()),
is_primary: false,
plaid_public_token: None,
},
];
let payloads: Vec<PayrixAccountPayload> = accounts.into_iter().map(Into::into).collect();
let json = serde_json::to_string_pretty(&payloads).unwrap();
assert!(json.contains("\"number\": \"111111111\""), "Operating account number should be in JSON");
assert!(json.contains("\"number\": \"222222222\""), "Trust account number should be in JSON");
assert!(json.contains("\"type\": \"all\""), "Operating account should have type 'all'");
assert!(json.contains("\"type\": \"credit\""), "Trust account should have type 'credit'");
}
fn create_test_request() -> OnboardMerchantRequest {
OnboardMerchantRequest {
business: BusinessInfo {
business_type: MerchantType::LimitedLiabilityCorporation,
legal_name: "Test Business LLC".to_string(),
address: Address {
line1: "123 Main St".to_string(),
line2: None,
city: "Springfield".to_string(),
state: "IL".to_string(),
zip: "62701".to_string(),
country: "USA".to_string(),
},
phone: "5551234567".to_string(),
email: "test@example.com".to_string(),
website: None,
ein: "123456789".to_string(),
},
merchant: MerchantConfig {
dba: "Test DBA".to_string(),
mcc: "5999".to_string(),
environment: MerchantEnvironment::Ecommerce,
annual_cc_sales: 50000000,
avg_ticket: 5000,
established: DateYmd::new("20200101").unwrap(),
is_new_business: false,
},
accounts: vec![BankAccountInfo {
name: None,
routing_number: Some("123456789".to_string()),
account_number: Some("987654321".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: None,
is_primary: true,
plaid_public_token: None,
}],
members: vec![MemberInfo {
member_type: MemberType::Owner,
first_name: "John".to_string(),
last_name: "Doe".to_string(),
title: None,
ownership_percentage: 100,
date_of_birth: "19800115".to_string(),
ssn: "123456789".to_string(),
email: "john@example.com".to_string(),
phone: "5559876543".to_string(),
address: Address {
line1: "456 Oak Ave".to_string(),
line2: None,
city: "Springfield".to_string(),
state: "IL".to_string(),
zip: "62702".to_string(),
country: "USA".to_string(),
},
}],
terms_acceptance: TermsAcceptance {
version: "4.21".to_string(),
accepted_at: "2024-01-15 10:30:00".to_string(),
},
}
}
#[test]
fn test_payload_contains_all_required_entity_fields() {
let request = create_test_request();
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"type\":"), "Missing required field: type (entity_type)");
assert!(json.contains("\"name\":"), "Missing required field: name");
assert!(json.contains("\"address1\":"), "Missing required field: address1");
assert!(json.contains("\"city\":"), "Missing required field: city");
assert!(json.contains("\"state\":"), "Missing required field: state");
assert!(json.contains("\"zip\":"), "Missing required field: zip");
assert!(json.contains("\"country\":"), "Missing required field: country");
assert!(json.contains("\"phone\":"), "Missing required field: phone");
assert!(json.contains("\"email\":"), "Missing required field: email");
assert!(json.contains("\"ein\":"), "Missing required field: ein");
assert!(json.contains("\"tcVersion\":"), "Missing required field: tcVersion");
assert!(json.contains("\"tcDate\":"), "Missing required field: tcDate");
assert!(json.contains("\"tcAttestation\":"), "Missing required field: tcAttestation");
assert!(json.contains("\"accounts\":"), "Missing required field: accounts");
assert!(json.contains("\"merchant\":"), "Missing required field: merchant");
}
#[test]
fn test_payload_contains_all_required_merchant_fields() {
let request = create_test_request();
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let merchant = value.get("merchant").expect("merchant field missing");
assert!(merchant.get("dba").is_some(), "Missing required merchant field: dba");
assert!(merchant.get("mcc").is_some(), "Missing required merchant field: mcc");
assert!(merchant.get("status").is_some(), "Missing required merchant field: status");
assert!(merchant.get("environment").is_some(), "Missing required merchant field: environment");
assert!(merchant.get("annualCcSales").is_some(), "Missing required merchant field: annualCcSales");
assert!(merchant.get("avgTicket").is_some(), "Missing required merchant field: avgTicket");
assert!(merchant.get("established").is_some(), "Missing required merchant field: established");
assert!(merchant.get("new").is_some(), "Missing required merchant field: new (is_new_business)");
assert!(merchant.get("members").is_some(), "Missing required merchant field: members");
}
#[test]
fn test_payload_contains_all_required_account_fields() {
let request = create_test_request();
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let accounts = value.get("accounts").expect("accounts field missing").as_array().unwrap();
assert!(!accounts.is_empty(), "accounts array should not be empty");
let account = &accounts[0];
assert!(account.get("primary").is_some(), "Missing required account field: primary");
assert!(account.get("type").is_some(), "Missing required account field: type");
let account_details = account.get("account").expect("Missing account.account for manual entry");
assert!(account_details.get("method").is_some(), "Missing required field: account.method");
assert!(account_details.get("number").is_some(), "Missing required field: account.number");
assert!(account_details.get("routing").is_some(), "Missing required field: account.routing");
assert!(account_details.get("holderType").is_some(), "Missing required field: account.holderType");
}
#[test]
fn test_payload_contains_all_required_member_fields() {
let request = create_test_request();
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let members = value
.get("merchant").expect("merchant missing")
.get("members").expect("members missing")
.as_array().unwrap();
assert!(!members.is_empty(), "members array should not be empty");
let member = &members[0];
assert!(member.get("type").is_some(), "Missing required member field: type");
assert!(member.get("first").is_some(), "Missing required member field: first");
assert!(member.get("last").is_some(), "Missing required member field: last");
assert!(member.get("ownership").is_some(), "Missing required member field: ownership");
assert!(member.get("dob").is_some(), "Missing required member field: dob");
assert!(member.get("ssn").is_some(), "Missing required member field: ssn");
assert!(member.get("email").is_some(), "Missing required member field: email");
assert!(member.get("phone").is_some(), "Missing required member field: phone");
assert!(member.get("address1").is_some(), "Missing required member field: address1");
assert!(member.get("city").is_some(), "Missing required member field: city");
assert!(member.get("state").is_some(), "Missing required member field: state");
assert!(member.get("zip").is_some(), "Missing required member field: zip");
assert!(member.get("country").is_some(), "Missing required member field: country");
}
#[test]
fn test_payload_excludes_entity_readonly_fields() {
let request = create_test_request();
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(!json.contains("\"id\":"), "Read-only field 'id' should not be serialized");
assert!(!json.contains("\"created\":"), "Read-only field 'created' should not be serialized");
assert!(!json.contains("\"modified\":"), "Read-only field 'modified' should not be serialized");
assert!(!json.contains("\"login\":"), "Read-only field 'login' should not be serialized");
assert!(!json.contains("\"frozen\":"), "Read-only field 'frozen' should not be serialized");
assert!(!json.contains("\"inactive\":"), "Read-only field 'inactive' should not be serialized");
}
#[test]
fn test_payload_excludes_account_readonly_fields() {
let account = BankAccountInfo {
name: Some("Test Account".to_string()),
routing_number: Some("123456789".to_string()),
account_number: Some("987654321".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: Some("USD".to_string()),
is_primary: true,
plaid_public_token: None,
};
let payload: PayrixAccountPayload = account.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(!json.contains("\"id\":"), "Read-only field 'id' should not be in account payload");
assert!(!json.contains("\"entity\":"), "Read-only field 'entity' should not be in account payload");
assert!(!json.contains("\"merchant\":"), "Read-only field 'merchant' should not be in account payload");
assert!(!json.contains("\"login\":"), "Read-only field 'login' should not be in account payload");
assert!(!json.contains("\"last4\":"), "Read-only field 'last4' should not be in account payload");
assert!(!json.contains("\"status\":"), "Read-only field 'status' should not be in account payload");
assert!(!json.contains("\"verified\":"), "Read-only field 'verified' should not be in account payload");
assert!(!json.contains("\"created\":"), "Read-only field 'created' should not be in account payload");
assert!(!json.contains("\"modified\":"), "Read-only field 'modified' should not be in account payload");
assert!(!json.contains("\"frozen\":"), "Read-only field 'frozen' should not be in account payload");
assert!(!json.contains("\"inactive\":"), "Read-only field 'inactive' should not be in account payload");
}
#[test]
fn test_payload_excludes_member_readonly_fields() {
let member = MemberInfo {
member_type: MemberType::Owner,
first_name: "John".to_string(),
last_name: "Doe".to_string(),
title: None,
ownership_percentage: 100,
date_of_birth: "19800115".to_string(),
ssn: "123456789".to_string(),
email: "john@example.com".to_string(),
phone: "5559876543".to_string(),
address: Address {
line1: "456 Oak Ave".to_string(),
line2: None,
city: "Springfield".to_string(),
state: "IL".to_string(),
zip: "62702".to_string(),
country: "USA".to_string(),
},
};
let payload: PayrixMemberPayload = member.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(!json.contains("\"id\":"), "Read-only field 'id' should not be in member payload");
assert!(!json.contains("\"entity\":"), "Read-only field 'entity' should not be in member payload");
assert!(!json.contains("\"merchant\":"), "Read-only field 'merchant' should not be in member payload");
assert!(!json.contains("\"login\":"), "Read-only field 'login' should not be in member payload");
assert!(!json.contains("\"created\":"), "Read-only field 'created' should not be in member payload");
assert!(!json.contains("\"modified\":"), "Read-only field 'modified' should not be in member payload");
assert!(!json.contains("\"frozen\":"), "Read-only field 'frozen' should not be in member payload");
assert!(!json.contains("\"inactive\":"), "Read-only field 'inactive' should not be in member payload");
}
#[test]
fn test_payload_excludes_merchant_readonly_fields() {
let request = create_test_request();
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let merchant = value.get("merchant").expect("merchant field missing");
let merchant_json = serde_json::to_string(merchant).unwrap();
assert!(!merchant_json.contains("\"id\":"), "Read-only field 'id' should not be in merchant payload");
assert!(!merchant_json.contains("\"entity\":"), "Read-only field 'entity' should not be in merchant payload");
assert!(!merchant_json.contains("\"login\":"), "Read-only field 'login' should not be in merchant payload");
assert!(!merchant_json.contains("\"created\":"), "Read-only field 'created' should not be in merchant payload");
assert!(!merchant_json.contains("\"modified\":"), "Read-only field 'modified' should not be in merchant payload");
assert!(!merchant_json.contains("\"frozen\":"), "Read-only field 'frozen' should not be in merchant payload");
assert!(!merchant_json.contains("\"inactive\":"), "Read-only field 'inactive' should not be in merchant payload");
assert!(!merchant_json.contains("\"boarded\":"), "Read-only field 'boarded' should not be in merchant payload");
}
#[test]
fn test_payload_uses_camel_case() {
let request = create_test_request();
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"tcVersion\":"), "Should use camelCase: tcVersion");
assert!(json.contains("\"tcDate\":"), "Should use camelCase: tcDate");
assert!(json.contains("\"tcAttestation\":"), "Should use camelCase: tcAttestation");
assert!(json.contains("\"annualCcSales\":"), "Should use camelCase: annualCcSales");
assert!(json.contains("\"avgTicket\":"), "Should use camelCase: avgTicket");
assert!(json.contains("\"holderType\":"), "Should use camelCase: holderType");
assert!(!json.contains("\"tc_version\":"), "Should not use snake_case");
assert!(!json.contains("\"tc_date\":"), "Should not use snake_case");
assert!(!json.contains("\"annual_cc_sales\":"), "Should not use snake_case");
assert!(!json.contains("\"avg_ticket\":"), "Should not use snake_case");
assert!(!json.contains("\"holder_type\":"), "Should not use snake_case");
}
#[test]
fn test_payload_skips_none_optional_fields() {
let request = create_test_request();
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(!json.contains("\"website\":"), "Optional None field 'website' should not be serialized");
assert!(!json.contains("\"address2\":"), "Optional None field 'address2' should not be serialized");
}
#[test]
fn test_payload_includes_some_optional_fields() {
let mut request = create_test_request();
request.business.website = Some("https://example.com".to_string());
request.business.address.line2 = Some("Suite 100".to_string());
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"website\":\"https://example.com\""), "Optional Some field 'website' should be serialized");
assert!(json.contains("\"address2\":\"Suite 100\""), "Optional Some field 'address2' should be serialized");
}
#[test]
fn test_boarding_status_is_board_immediately() {
let request = create_test_request();
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let merchant = value.get("merchant").expect("merchant missing");
let status = merchant.get("status").expect("status missing").as_i64().unwrap();
assert_eq!(status, 1, "Merchant status should be 1 (Board Immediately)");
}
#[test]
fn test_tc_attestation_is_one() {
let request = create_test_request();
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let tc_attestation = value.get("tcAttestation").expect("tcAttestation missing").as_i64().unwrap();
assert_eq!(tc_attestation, 1, "tcAttestation should be 1");
}
#[test]
fn test_primary_account_flag_serialization() {
let primary_account = BankAccountInfo {
name: None,
routing_number: Some("123456789".to_string()),
account_number: Some("111111111".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: None,
is_primary: true,
plaid_public_token: None,
};
let primary_payload: PayrixAccountPayload = primary_account.into();
assert_eq!(primary_payload.primary, 1, "Primary account should have primary=1");
let secondary_account = BankAccountInfo {
name: None,
routing_number: Some("123456789".to_string()),
account_number: Some("222222222".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::Credit,
currency: None,
is_primary: false,
plaid_public_token: None,
};
let secondary_payload: PayrixAccountPayload = secondary_account.into();
assert_eq!(secondary_payload.primary, 0, "Non-primary account should have primary=0");
}
#[test]
fn test_new_business_flag_serialization() {
let mut request = create_test_request();
request.merchant.is_new_business = false;
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let new_flag = value.get("merchant").unwrap().get("new").unwrap().as_i64().unwrap();
assert_eq!(new_flag, 0, "Established business should have new=0");
let mut request = create_test_request();
request.merchant.is_new_business = true;
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let new_flag = value.get("merchant").unwrap().get("new").unwrap().as_i64().unwrap();
assert_eq!(new_flag, 1, "New business should have new=1");
}
#[test]
fn test_account_method_business_checking() {
let account = BankAccountInfo {
name: None,
routing_number: Some("123456789".to_string()),
account_number: Some("987654321".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: None,
is_primary: true,
plaid_public_token: None,
};
let payload: PayrixAccountPayload = account.into();
let account_details = payload.account.expect("account details missing");
assert_eq!(account_details.method, 10, "Business checking should be method 10");
}
#[test]
fn test_account_method_business_savings() {
let account = BankAccountInfo {
name: None,
routing_number: Some("123456789".to_string()),
account_number: Some("987654321".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Savings,
transaction_type: AccountType::All,
currency: None,
is_primary: true,
plaid_public_token: None,
};
let payload: PayrixAccountPayload = account.into();
let account_details = payload.account.expect("account details missing");
assert_eq!(account_details.method, 11, "Business savings should be method 11");
}
#[test]
fn test_account_method_individual_checking() {
let account = BankAccountInfo {
name: None,
routing_number: Some("123456789".to_string()),
account_number: Some("987654321".to_string()),
holder_type: AccountHolderType::Individual,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: None,
is_primary: true,
plaid_public_token: None,
};
let payload: PayrixAccountPayload = account.into();
let account_details = payload.account.expect("account details missing");
assert_eq!(account_details.method, 8, "Individual checking should be method 8");
}
#[test]
fn test_account_method_individual_savings() {
let account = BankAccountInfo {
name: None,
routing_number: Some("123456789".to_string()),
account_number: Some("987654321".to_string()),
holder_type: AccountHolderType::Individual,
account_method: BankAccountMethod::Savings,
transaction_type: AccountType::All,
currency: None,
is_primary: true,
plaid_public_token: None,
};
let payload: PayrixAccountPayload = account.into();
let account_details = payload.account.expect("account details missing");
assert_eq!(account_details.method, 9, "Individual savings should be method 9");
}
#[test]
fn test_account_method_exhaustive_combinations() {
let test_cases: &[(AccountHolderType, BankAccountMethod, i32, &str)] = &[
(AccountHolderType::Individual, BankAccountMethod::Checking, 8, "Individual+Checking"),
(AccountHolderType::Individual, BankAccountMethod::Savings, 9, "Individual+Savings"),
(AccountHolderType::Business, BankAccountMethod::Checking, 10, "Business+Checking"),
(AccountHolderType::Business, BankAccountMethod::Savings, 11, "Business+Savings"),
];
for (holder_type, account_method, expected_method, description) in test_cases {
let account = BankAccountInfo {
name: None,
routing_number: Some("123456789".to_string()),
account_number: Some("987654321".to_string()),
holder_type: *holder_type,
account_method: *account_method,
transaction_type: AccountType::All,
currency: None,
is_primary: true,
plaid_public_token: None,
};
let payload: PayrixAccountPayload = account.into();
let account_details = payload.account.expect("account details missing");
assert_eq!(
account_details.method, *expected_method,
"Failed for {}: expected method {}, got {}",
description, expected_method, account_details.method
);
}
}
#[test]
fn test_response_missing_merchant_field() {
let json = r#"{
"id": "t1_ent_12345678901234567890123"
}"#;
let response: PayrixOnboardingResponse = serde_json::from_str(json).unwrap();
assert!(response.merchant.is_none(), "merchant should be None when missing from response");
}
#[test]
fn test_response_with_empty_merchant_id() {
let json = r#"{
"id": "t1_ent_12345678901234567890123",
"merchant": {
"id": "",
"status": 1
}
}"#;
let response: PayrixOnboardingResponse = serde_json::from_str(json).unwrap();
let merchant = response.merchant.unwrap();
assert!(merchant.id.is_empty(), "merchant ID should be empty");
}
#[test]
fn test_response_with_null_status() {
let json = r#"{
"id": "t1_ent_12345678901234567890123",
"merchant": {
"id": "t1_mer_12345678901234567890123"
}
}"#;
let response: PayrixOnboardingResponse = serde_json::from_str(json).unwrap();
let merchant = response.merchant.unwrap();
assert!(merchant.status.is_none(), "status should be None when missing");
}
#[test]
fn test_boarding_status_defaults_to_not_ready_when_missing() {
let status: Option<MerchantStatus> = None;
let boarding_status = status
.map(BoardingStatus::from)
.unwrap_or(BoardingStatus::NotReady);
assert_eq!(boarding_status, BoardingStatus::NotReady);
}
#[test]
fn test_bank_account_method_default() {
assert_eq!(BankAccountMethod::default(), BankAccountMethod::Checking);
}
#[test]
fn test_validation_catches_empty_routing_with_account_number() {
let mut request = valid_request();
request.accounts[0].routing_number = None;
request.accounts[0].account_number = Some("123456789".to_string());
request.accounts[0].plaid_public_token = None;
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("routing/account numbers or a Plaid token"));
}
#[test]
fn test_validation_catches_routing_without_account_number() {
let mut request = valid_request();
request.accounts[0].routing_number = Some("123456789".to_string());
request.accounts[0].account_number = None;
request.accounts[0].plaid_public_token = None;
let err = validate_request(&request).unwrap_err();
assert!(err.to_string().contains("routing/account numbers or a Plaid token"));
}
#[test]
fn test_entity_type_serialization() {
let mut request = create_test_request();
request.business.business_type = MerchantType::LimitedLiabilityCorporation;
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"type\":2"), "LLC should serialize to type=2, got: {}", json);
}
#[test]
fn test_member_type_serialization() {
let mut member = MemberInfo {
member_type: MemberType::Owner,
first_name: "John".to_string(),
last_name: "Doe".to_string(),
title: None,
ownership_percentage: 100,
date_of_birth: "19800115".to_string(),
ssn: "123456789".to_string(),
email: "john@example.com".to_string(),
phone: "5559876543".to_string(),
address: Address {
line1: "456 Oak Ave".to_string(),
line2: None,
city: "Springfield".to_string(),
state: "IL".to_string(),
zip: "62702".to_string(),
country: "USA".to_string(),
},
};
let payload: PayrixMemberPayload = member.clone().into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"type\":1"), "Owner should serialize to type=1");
member.member_type = MemberType::ControlPerson;
let payload: PayrixMemberPayload = member.clone().into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"type\":2"), "ControlPerson should serialize to type=2");
member.member_type = MemberType::Principal;
let payload: PayrixMemberPayload = member.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"type\":3"), "Principal should serialize to type=3");
}
#[test]
fn test_account_type_serialization() {
let mut account = BankAccountInfo {
name: None,
routing_number: Some("123456789".to_string()),
account_number: Some("987654321".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: None,
is_primary: true,
plaid_public_token: None,
};
let payload: PayrixAccountPayload = account.clone().into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"type\":\"all\""), "AccountType::All should serialize to 'all'");
account.transaction_type = AccountType::Credit;
let payload: PayrixAccountPayload = account.clone().into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"type\":\"credit\""), "AccountType::Credit should serialize to 'credit'");
account.transaction_type = AccountType::Debit;
let payload: PayrixAccountPayload = account.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"type\":\"debit\""), "AccountType::Debit should serialize to 'debit'");
}
#[test]
fn test_account_holder_type_serialization() {
let mut account = BankAccountInfo {
name: None,
routing_number: Some("123456789".to_string()),
account_number: Some("987654321".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: None,
is_primary: true,
plaid_public_token: None,
};
let payload: PayrixAccountPayload = account.clone().into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"holderType\":2"), "AccountHolderType::Business should serialize to 2, got: {}", json);
account.holder_type = AccountHolderType::Individual;
let payload: PayrixAccountPayload = account.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"holderType\":1"), "AccountHolderType::Individual should serialize to 1, got: {}", json);
}
#[test]
fn test_environment_serialization() {
let mut request = create_test_request();
request.merchant.environment = MerchantEnvironment::Ecommerce;
let payload: PayrixOnboardingPayload = request.clone().into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let env = value.get("merchant").unwrap().get("environment").unwrap().as_str().unwrap();
assert_eq!(env, "ecommerce", "Ecommerce should serialize to 'ecommerce'");
request.merchant.environment = MerchantEnvironment::CardPresent;
let payload: PayrixOnboardingPayload = request.clone().into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let env = value.get("merchant").unwrap().get("environment").unwrap().as_str().unwrap();
assert_eq!(env, "cardPresent", "CardPresent should serialize to 'cardPresent'");
request.merchant.environment = MerchantEnvironment::Restaurant;
let payload: PayrixOnboardingPayload = request.clone().into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let env = value.get("merchant").unwrap().get("environment").unwrap().as_str().unwrap();
assert_eq!(env, "restaurant", "Restaurant should serialize to 'restaurant'");
request.merchant.environment = MerchantEnvironment::MailOrTelephoneOrder;
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let env = value.get("merchant").unwrap().get("environment").unwrap().as_str().unwrap();
assert_eq!(env, "moto", "MailOrTelephoneOrder should serialize to 'moto'");
}
#[test]
fn test_plaid_account_omits_manual_details() {
let account = BankAccountInfo {
name: Some("Plaid Verified Account".to_string()),
routing_number: None, account_number: None, holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::All,
currency: Some("USD".to_string()),
is_primary: true,
plaid_public_token: Some("public-sandbox-token".to_string()),
};
let payload: PayrixAccountPayload = account.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"publicToken\":\"public-sandbox-token\""), "Should include publicToken");
assert!(!json.contains("\"account\":"), "Should not include nested account when using Plaid, got: {}", json);
assert!(!json.contains("\"routing\":"), "Should not include routing when using Plaid");
assert!(!json.contains("\"number\":"), "Should not include number when using Plaid");
}
#[test]
fn test_multiple_members_serialization() {
let mut request = create_test_request();
request.members.push(MemberInfo {
member_type: MemberType::ControlPerson,
first_name: "Jane".to_string(),
last_name: "Smith".to_string(),
title: Some("CFO".to_string()),
ownership_percentage: 0, date_of_birth: "19850620".to_string(),
ssn: "987654321".to_string(),
email: "jane@example.com".to_string(),
phone: "5551112222".to_string(),
address: Address {
line1: "789 Pine Rd".to_string(),
line2: None,
city: "Chicago".to_string(),
state: "IL".to_string(),
zip: "60601".to_string(),
country: "USA".to_string(),
},
});
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let members = value.get("merchant").unwrap().get("members").unwrap().as_array().unwrap();
assert_eq!(members.len(), 2, "Should have 2 members");
assert_eq!(members[0].get("first").unwrap().as_str().unwrap(), "John");
assert_eq!(members[0].get("type").unwrap().as_i64().unwrap(), 1);
assert_eq!(members[1].get("first").unwrap().as_str().unwrap(), "Jane");
assert_eq!(members[1].get("type").unwrap().as_i64().unwrap(), 2); assert_eq!(members[1].get("title").unwrap().as_str().unwrap(), "CFO");
}
#[test]
fn test_multiple_accounts_serialization() {
let mut request = create_test_request();
request.accounts.push(BankAccountInfo {
name: Some("Trust Account".to_string()),
routing_number: Some("123456789".to_string()),
account_number: Some("222222222".to_string()),
holder_type: AccountHolderType::Business,
account_method: BankAccountMethod::Checking,
transaction_type: AccountType::Credit, currency: Some("USD".to_string()),
is_primary: false,
plaid_public_token: None,
});
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let accounts = value.get("accounts").unwrap().as_array().unwrap();
assert_eq!(accounts.len(), 2, "Should have 2 accounts");
assert_eq!(accounts[0].get("primary").unwrap().as_i64().unwrap(), 1);
assert_eq!(accounts[0].get("type").unwrap().as_str().unwrap(), "all");
assert_eq!(accounts[1].get("primary").unwrap().as_i64().unwrap(), 0);
assert_eq!(accounts[1].get("type").unwrap().as_str().unwrap(), "credit");
assert_eq!(accounts[1].get("name").unwrap().as_str().unwrap(), "Trust Account");
}
#[test]
fn test_payrix_onboarding_response_deserialize() {
let json = r#"{
"id": "t1_ent_12345678901234567890123",
"merchant": {
"id": "t1_mer_23456789012345678901234",
"status": 2,
"entity": "t1_ent_12345678901234567890123",
"boarded": "20240115"
}
}"#;
let response: PayrixOnboardingResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.id, "t1_ent_12345678901234567890123");
let merchant = response.merchant.unwrap();
assert_eq!(merchant.id, "t1_mer_23456789012345678901234");
assert_eq!(merchant.status, Some(MerchantStatus::Boarded));
assert_eq!(merchant.boarded, Some("20240115".to_string()));
}
#[test]
fn test_payrix_onboarding_response_minimal() {
let json = r#"{
"id": "t1_ent_12345678901234567890123"
}"#;
let response: PayrixOnboardingResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.id, "t1_ent_12345678901234567890123");
assert!(response.merchant.is_none());
}
#[test]
fn test_merchant_in_response_deserialize() {
let json = r#"{
"id": "t1_mer_12345678901234567890123",
"status": 6,
"entity": "t1_ent_23456789012345678901234"
}"#;
let merchant: MerchantInResponse = serde_json::from_str(json).unwrap();
assert_eq!(merchant.id, "t1_mer_12345678901234567890123");
assert_eq!(merchant.status, Some(MerchantStatus::Pending));
assert_eq!(merchant.entity, Some("t1_ent_23456789012345678901234".to_string()));
assert!(merchant.boarded.is_none());
assert!(merchant.members.is_none());
}
#[test]
fn test_merchant_in_response_with_members() {
let json = r#"{
"id": "t1_mer_12345678901234567890123",
"status": 2,
"members": [
{
"id": "t1_mem_34567890123456789012345",
"first": "John",
"last": "Doe"
}
]
}"#;
let merchant: MerchantInResponse = serde_json::from_str(json).unwrap();
assert_eq!(merchant.id, "t1_mer_12345678901234567890123");
let members = merchant.members.unwrap();
assert_eq!(members.len(), 1);
assert_eq!(members[0].first, Some("John".to_string()));
assert_eq!(members[0].last, Some("Doe".to_string()));
}
#[test]
fn test_boarding_status_all_variants() {
let test_cases = vec![
(MerchantStatus::NotReady, BoardingStatus::NotReady),
(MerchantStatus::Ready, BoardingStatus::Submitted),
(MerchantStatus::Boarded, BoardingStatus::Boarded),
(MerchantStatus::Manual, BoardingStatus::ManualReview),
(MerchantStatus::Closed, BoardingStatus::Closed),
(MerchantStatus::Incomplete, BoardingStatus::Incomplete),
(MerchantStatus::Pending, BoardingStatus::Pending),
];
for (merchant_status, expected) in test_cases {
let actual = BoardingStatus::from(merchant_status);
assert_eq!(actual, expected, "MerchantStatus::{:?} should map to BoardingStatus::{:?}", merchant_status, expected);
}
}
#[test]
fn test_boarding_status_equality() {
assert_eq!(BoardingStatus::Boarded, BoardingStatus::Boarded);
assert_ne!(BoardingStatus::Boarded, BoardingStatus::Pending);
}
#[test]
fn test_boarding_status_clone() {
let status = BoardingStatus::ManualReview;
let cloned = status; assert_eq!(status, cloned);
}
#[test]
fn test_boarding_status_copy() {
let status = BoardingStatus::Boarded;
let copied = status; assert_eq!(status, copied);
}
#[test]
fn test_onboard_merchant_result_fields() {
let entity_json = r#"{"id": "t1_ent_12345678901234567890123"}"#;
let merchant_json = r#"{"id": "t1_mer_23456789012345678901234"}"#;
let entity: Entity = serde_json::from_str(entity_json).unwrap();
let merchant: Merchant = serde_json::from_str(merchant_json).unwrap();
let result = OnboardMerchantResult {
entity_id: entity.id.as_str().to_string(),
merchant_id: merchant.id.as_str().to_string(),
boarding_status: BoardingStatus::Boarded,
entity,
merchant,
accounts: vec![],
members: vec![],
};
assert_eq!(result.entity_id, "t1_ent_12345678901234567890123");
assert_eq!(result.merchant_id, "t1_mer_23456789012345678901234");
assert_eq!(result.boarding_status, BoardingStatus::Boarded);
assert!(result.accounts.is_empty());
assert!(result.members.is_empty());
}
#[test]
fn test_boarding_status_result_structure() {
let result = BoardingStatusResult {
status: BoardingStatus::Pending,
merchant_id: "t1_mer_12345678901234567890123".to_string(),
entity_id: "t1_ent_23456789012345678901234".to_string(),
boarded_date: None,
};
assert_eq!(result.status, BoardingStatus::Pending);
assert_eq!(result.merchant_id, "t1_mer_12345678901234567890123");
assert!(result.boarded_date.is_none());
}
#[test]
fn test_boarding_status_result_with_boarded_date() {
let result = BoardingStatusResult {
status: BoardingStatus::Boarded,
merchant_id: "t1_mer_12345678901234567890123".to_string(),
entity_id: "t1_ent_23456789012345678901234".to_string(),
boarded_date: Some("20240115".to_string()),
};
assert_eq!(result.status, BoardingStatus::Boarded);
assert_eq!(result.boarded_date, Some("20240115".to_string()));
}
#[test]
fn test_empty_accounts_serialization() {
let mut request = create_test_request();
request.accounts = vec![];
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"accounts\":[]"), "Empty accounts should serialize to empty array");
}
#[test]
fn test_empty_members_serialization() {
let mut request = create_test_request();
request.members = vec![];
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let merchant = value.get("merchant").unwrap();
let members = merchant.get("members").unwrap().as_array().unwrap();
assert!(members.is_empty(), "Empty members should serialize to empty array");
}
#[test]
fn test_special_characters_in_strings() {
let mut request = create_test_request();
request.business.legal_name = "O'Reilly & Sons, LLC \"Test\"".to_string();
request.business.address.line1 = "123 Main St. #456".to_string();
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("O'Reilly"), "Single quote should be preserved");
assert!(json.contains("& Sons"), "Ampersand should be preserved");
assert!(json.contains("#456"), "Hash should be preserved");
let _: serde_json::Value = serde_json::from_str(&json)
.expect("JSON with special characters should be valid");
}
#[test]
fn test_unicode_in_strings() {
let mut request = create_test_request();
request.business.legal_name = "Café München LLC".to_string();
request.members[0].first_name = "José".to_string();
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("Café"), "Unicode é should be preserved");
assert!(json.contains("München"), "Unicode ü should be preserved");
assert!(json.contains("José"), "Unicode é in name should be preserved");
}
#[test]
fn test_max_ownership_percentage() {
let mut request = create_test_request();
request.members[0].ownership_percentage = 100;
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let members = value.get("merchant").unwrap().get("members").unwrap().as_array().unwrap();
let ownership = members[0].get("ownership").unwrap().as_i64().unwrap();
assert_eq!(ownership, 100);
}
#[test]
fn test_zero_ownership_percentage() {
let mut request = create_test_request();
request.members[0].member_type = MemberType::ControlPerson;
request.members[0].ownership_percentage = 0;
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let members = value.get("merchant").unwrap().get("members").unwrap().as_array().unwrap();
let ownership = members[0].get("ownership").unwrap().as_i64().unwrap();
assert_eq!(ownership, 0);
}
#[test]
fn test_large_annual_sales() {
let mut request = create_test_request();
request.merchant.annual_cc_sales = 10_000_000_000;
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let annual_cc_sales = value.get("merchant").unwrap().get("annualCcSales").unwrap().as_i64().unwrap();
assert_eq!(annual_cc_sales, 10_000_000_000);
}
#[test]
fn test_address_line2_with_apartment() {
let mut request = create_test_request();
request.business.address.line2 = Some("Apt 4B, Floor 12".to_string());
request.members[0].address.line2 = Some("Unit #789".to_string());
let payload: PayrixOnboardingPayload = request.into();
let json = serde_json::to_string(&payload).unwrap();
assert!(json.contains("\"address2\":\"Apt 4B, Floor 12\""), "Business address2 should be present");
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let members = value.get("merchant").unwrap().get("members").unwrap().as_array().unwrap();
let member_address2 = members[0].get("address2").unwrap().as_str().unwrap();
assert_eq!(member_address2, "Unit #789");
}
}