use payrix_macros::PayrixEntity;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use super::{bool_from_int_default_false, deserialize_string_or_int, MerchantType, PayrixId, TaxIdStatus};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum EinType {
Ssn,
Tin,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GlobalBusinessType {
#[serde(rename = "ssn")]
Ssn,
#[serde(rename = "tin")]
Tin,
#[serde(rename = "other")]
Other,
#[serde(rename = "CD")]
FederalCanada,
#[serde(rename = "AB")]
Alberta,
#[serde(rename = "BC")]
BritishColumbia,
#[serde(rename = "MB")]
Manitoba,
#[serde(rename = "NB")]
NewBrunswick,
#[serde(rename = "NL")]
NewfoundlandLabrador,
#[serde(rename = "NT")]
NorthwestTerritories,
#[serde(rename = "NS")]
NovaScotia,
#[serde(rename = "NU")]
Nunavut,
#[serde(rename = "ON")]
Ontario,
#[serde(rename = "PE")]
PrinceEdwardIsland,
#[serde(rename = "QC")]
Quebec,
#[serde(rename = "SK")]
Saskatchewan,
#[serde(rename = "YT")]
Yukon,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PendingRiskCheck {
#[default]
Pending,
Successful,
Failed,
Manual,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EntityCheckStage {
#[default]
CreateEntity,
Underwriting,
Preboard,
Postboard,
Txn,
TxnVolume,
Payout,
PayoutVolume,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize_repr, Deserialize_repr)]
#[repr(i32)]
pub enum EntityReserved {
#[default]
NoReserve = 0,
Block = 1,
Reserved = 2,
Hold = 3,
Reserve = 4,
BlockActivity = 5,
Passed = 6,
NoPolicies = 7,
PostReviewOnly = 8,
ScheduledRelease = 9,
HoldAutoRelease = 10,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize_repr, Deserialize_repr)]
#[repr(i32)]
pub enum EntityPublic {
#[default]
Private = 0,
Public = 1,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, PayrixEntity)]
#[payrix(create = CreateEntity, update = UpdateEntity)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
#[serde(rename_all = "camelCase")]
pub struct Entity {
#[payrix(readonly)]
pub id: PayrixId,
#[payrix(readonly)]
#[serde(default)]
pub created: Option<String>,
#[payrix(readonly)]
#[serde(default)]
pub modified: Option<String>,
#[payrix(readonly)]
#[serde(default)]
pub creator: Option<PayrixId>,
#[payrix(readonly)]
#[serde(default)]
pub modifier: Option<PayrixId>,
#[payrix(readonly)]
#[serde(default)]
pub ip_created: Option<String>,
#[payrix(readonly)]
#[serde(default)]
pub ip_modified: Option<String>,
#[serde(default)]
pub client_ip: Option<String>,
#[serde(default)]
pub login: Option<PayrixId>,
#[serde(default)]
pub parameter: Option<String>,
#[serde(default)]
pub total_credit_disbursements: Option<i32>,
#[payrix(mutable)]
#[serde(default, rename = "type")]
pub entity_type: Option<MerchantType>,
#[payrix(mutable)]
#[serde(default)]
pub name: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub display_name: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub address1: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub address2: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub city: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub state: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub zip: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub country: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub timezone: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub website: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub phone: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub customer_phone: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub fax: Option<String>,
#[payrix(mutable)]
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub ein: Option<String>,
#[serde(default)]
pub ein_type: Option<EinType>,
#[serde(default)]
pub global_business_id: Option<String>,
#[serde(default)]
pub global_business_type: Option<GlobalBusinessType>,
#[serde(default)]
pub irs_filing_name: Option<String>,
#[serde(default)]
pub tin_status: Option<TaxIdStatus>,
#[serde(default)]
pub locations: Option<i32>,
#[serde(default)]
pub industry: Option<String>,
#[serde(default)]
pub payout_secondary_descriptor: Option<String>,
#[serde(default)]
pub currency: Option<String>,
#[serde(default)]
pub tc_version: Option<String>,
#[serde(default)]
pub tc_date: Option<String>,
#[serde(default)]
pub tc_ip: Option<String>,
#[serde(default, deserialize_with = "deserialize_string_or_int")]
pub tc_accept_date: Option<String>,
#[serde(default)]
pub tc_accept_ip: Option<String>,
#[serde(default)]
pub public: Option<EntityPublic>,
#[serde(default)]
pub reserved: Option<EntityReserved>,
#[serde(default)]
pub pending_risk_check: Option<PendingRiskCheck>,
#[serde(default)]
pub check_stage: Option<EntityCheckStage>,
#[serde(default)]
pub debit_grace_period: Option<i32>,
#[serde(default)]
pub negative_balance_limit: Option<i64>,
#[serde(default)]
pub negative_balance_email: Option<i32>,
#[serde(default)]
pub positive_balance_email: Option<i32>,
#[serde(default)]
pub pass_token_enabled: Option<i32>,
#[serde(default)]
pub txn_decision_microservice_enabled: Option<i32>,
#[payrix(mutable)]
#[serde(default)]
pub custom: Option<String>,
#[payrix(mutable)]
#[serde(default, with = "bool_from_int_default_false")]
pub inactive: bool,
#[payrix(mutable)]
#[serde(default, with = "bool_from_int_default_false")]
pub frozen: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entity_deserialize_full() {
let json = r#"{
"id": "t1_ent_12345678901234567890123",
"created": "2024-01-01 00:00:00.0000",
"modified": "2024-04-01 12:00:00.0000",
"creator": "t1_lgn_creator1234567890123456",
"modifier": "t1_lgn_modifier123456789012345",
"ipCreated": "192.168.1.1",
"ipModified": "192.168.1.2",
"clientIp": "10.0.0.1",
"login": "t1_lgn_12345678901234567890123",
"parameter": "param123",
"totalCreditDisbursements": -50000,
"type": 1,
"name": "Acme Corporation",
"displayName": "Acme Corp",
"address1": "123 Main St",
"address2": "Suite 100",
"city": "Springfield",
"state": "IL",
"zip": "62701",
"country": "USA",
"timezone": "cst",
"website": "https://acme.com",
"phone": "5551234567",
"customerPhone": "5559876543",
"fax": "5551112222",
"email": "info@acme.com",
"ein": "123456789",
"einType": "tin",
"globalBusinessId": "123456789",
"globalBusinessType": "tin",
"irsFilingName": "Acme Corporation Inc",
"tinStatus": 1,
"locations": 5,
"industry": "Software",
"payoutSecondaryDescriptor": "ACME PAYOUT",
"currency": "USD",
"tcVersion": "2.0",
"tcDate": "2024-01-15 10:30:00",
"tcIp": "192.168.1.100",
"tcAcceptDate": "202401151030",
"tcAcceptIp": "192.168.1.100",
"public": 0,
"reserved": 0,
"pendingRiskCheck": "successful",
"checkStage": "postboard",
"custom": "custom data here",
"inactive": 0,
"frozen": 0
}"#;
let entity: Entity = serde_json::from_str(json).unwrap();
assert_eq!(entity.id.as_str(), "t1_ent_12345678901234567890123");
assert_eq!(entity.ip_created.as_deref(), Some("192.168.1.1"));
assert_eq!(entity.client_ip.as_deref(), Some("10.0.0.1"));
assert_eq!(entity.entity_type, Some(MerchantType::Corporation));
assert_eq!(entity.name.as_deref(), Some("Acme Corporation"));
assert_eq!(entity.display_name.as_deref(), Some("Acme Corp"));
assert_eq!(entity.ein_type, Some(EinType::Tin));
assert_eq!(entity.global_business_type, Some(GlobalBusinessType::Tin));
assert_eq!(entity.tin_status, Some(TaxIdStatus::Valid));
assert_eq!(entity.locations, Some(5));
assert_eq!(entity.public, Some(EntityPublic::Private));
assert_eq!(entity.reserved, Some(EntityReserved::NoReserve));
assert_eq!(entity.pending_risk_check, Some(PendingRiskCheck::Successful));
assert_eq!(entity.check_stage, Some(EntityCheckStage::Postboard));
assert!(!entity.inactive);
assert!(!entity.frozen);
}
#[test]
fn entity_deserialize_minimal() {
let json = r#"{"id": "t1_ent_12345678901234567890123"}"#;
let entity: Entity = serde_json::from_str(json).unwrap();
assert_eq!(entity.id.as_str(), "t1_ent_12345678901234567890123");
assert!(entity.name.is_none());
assert!(entity.entity_type.is_none());
assert!(!entity.inactive);
assert!(!entity.frozen);
}
#[test]
fn entity_ein_type_values() {
let test_cases = vec![
("ssn", EinType::Ssn),
("tin", EinType::Tin),
("other", EinType::Other),
];
for (val, expected) in test_cases {
let json = format!(
r#"{{"id": "t1_ent_12345678901234567890123", "einType": "{}"}}"#,
val
);
let entity: Entity = serde_json::from_str(&json).unwrap();
assert_eq!(entity.ein_type, Some(expected));
}
}
#[test]
fn entity_pending_risk_check_values() {
let test_cases = vec![
("pending", PendingRiskCheck::Pending),
("successful", PendingRiskCheck::Successful),
("failed", PendingRiskCheck::Failed),
("manual", PendingRiskCheck::Manual),
];
for (val, expected) in test_cases {
let json = format!(
r#"{{"id": "t1_ent_12345678901234567890123", "pendingRiskCheck": "{}"}}"#,
val
);
let entity: Entity = serde_json::from_str(&json).unwrap();
assert_eq!(entity.pending_risk_check, Some(expected));
}
}
#[test]
fn entity_check_stage_values() {
let test_cases = vec![
("createEntity", EntityCheckStage::CreateEntity),
("underwriting", EntityCheckStage::Underwriting),
("preboard", EntityCheckStage::Preboard),
("postboard", EntityCheckStage::Postboard),
("txn", EntityCheckStage::Txn),
("txnVolume", EntityCheckStage::TxnVolume),
("payout", EntityCheckStage::Payout),
("payoutVolume", EntityCheckStage::PayoutVolume),
];
for (val, expected) in test_cases {
let json = format!(
r#"{{"id": "t1_ent_12345678901234567890123", "checkStage": "{}"}}"#,
val
);
let entity: Entity = serde_json::from_str(&json).unwrap();
assert_eq!(entity.check_stage, Some(expected));
}
}
#[test]
fn entity_reserved_values() {
let test_cases = vec![
(0, EntityReserved::NoReserve),
(1, EntityReserved::Block),
(3, EntityReserved::Hold),
(4, EntityReserved::Reserve),
(5, EntityReserved::BlockActivity),
(6, EntityReserved::Passed),
];
for (val, expected) in test_cases {
let json = format!(
r#"{{"id": "t1_ent_12345678901234567890123", "reserved": {}}}"#,
val
);
let entity: Entity = serde_json::from_str(&json).unwrap();
assert_eq!(entity.reserved, Some(expected));
}
}
#[test]
fn entity_global_business_type_canada() {
let test_cases = vec![
("CD", GlobalBusinessType::FederalCanada),
("ON", GlobalBusinessType::Ontario),
("QC", GlobalBusinessType::Quebec),
("BC", GlobalBusinessType::BritishColumbia),
];
for (val, expected) in test_cases {
let json = format!(
r#"{{"id": "t1_ent_12345678901234567890123", "globalBusinessType": "{}"}}"#,
val
);
let entity: Entity = serde_json::from_str(&json).unwrap();
assert_eq!(entity.global_business_type, Some(expected));
}
}
#[test]
fn entity_serialize_roundtrip() {
let json = r#"{
"id": "t1_ent_12345678901234567890123",
"name": "Test Entity",
"type": 2,
"einType": "tin",
"pendingRiskCheck": "successful"
}"#;
let entity: Entity = serde_json::from_str(json).unwrap();
let serialized = serde_json::to_string(&entity).unwrap();
let deserialized: Entity = serde_json::from_str(&serialized).unwrap();
assert_eq!(entity.id, deserialized.id);
assert_eq!(entity.name, deserialized.name);
assert_eq!(entity.entity_type, deserialized.entity_type);
assert_eq!(entity.ein_type, deserialized.ein_type);
}
#[test]
fn entity_deserialize_with_disbursement_settings() {
let json = r#"{
"id": "t1_ent_12345678901234567890123",
"debitGracePeriod": 30,
"negativeBalanceLimit": -500000,
"negativeBalanceEmail": 1,
"positiveBalanceEmail": 0
}"#;
let entity: Entity = serde_json::from_str(json).unwrap();
assert_eq!(entity.debit_grace_period, Some(30));
assert_eq!(entity.negative_balance_limit, Some(-500000));
assert_eq!(entity.negative_balance_email, Some(1));
assert_eq!(entity.positive_balance_email, Some(0));
}
#[test]
fn entity_deserialize_debit_grace_period_max() {
let json = r#"{
"id": "t1_ent_12345678901234567890123",
"debitGracePeriod": 99
}"#;
let entity: Entity = serde_json::from_str(json).unwrap();
assert_eq!(entity.debit_grace_period, Some(99));
}
#[test]
fn entity_deserialize_with_feature_flags() {
let json = r#"{
"id": "t1_ent_12345678901234567890123",
"passTokenEnabled": 1,
"txnDecisionMicroserviceEnabled": 0
}"#;
let entity: Entity = serde_json::from_str(json).unwrap();
assert_eq!(entity.pass_token_enabled, Some(1));
assert_eq!(entity.txn_decision_microservice_enabled, Some(0));
}
#[test]
fn entity_deserialize_new_fields_absent() {
let json = r#"{"id": "t1_ent_12345678901234567890123"}"#;
let entity: Entity = serde_json::from_str(json).unwrap();
assert!(entity.debit_grace_period.is_none());
assert!(entity.negative_balance_limit.is_none());
assert!(entity.negative_balance_email.is_none());
assert!(entity.positive_balance_email.is_none());
assert!(entity.pass_token_enabled.is_none());
assert!(entity.txn_decision_microservice_enabled.is_none());
}
#[test]
fn entity_bool_from_int() {
let json = r#"{"id": "t1_ent_12345678901234567890123", "inactive": 1, "frozen": 0}"#;
let entity: Entity = serde_json::from_str(json).unwrap();
assert!(entity.inactive);
assert!(!entity.frozen);
}
}