use crate::{EntityType, PayrixClient, Result, SearchBuilder};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
pub struct BankAccount {
pub id: String,
pub token: String,
pub account_type: i32,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[repr(i32)]
pub enum PayoutSchedule {
#[default]
Single = 5,
Daily = 1,
Weekly = 2,
BiWeekly = 3,
Monthly = 4,
}
impl PayoutSchedule {
pub fn as_i32(&self) -> i32 {
*self as i32
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[repr(i32)]
pub enum PayoutUsageMethod {
Percentage = 1,
#[default]
Actual = 2,
}
impl PayoutUsageMethod {
pub fn as_i32(&self) -> i32 {
*self as i32
}
}
#[derive(Debug, Clone)]
pub struct PayoutConfig {
pub schedule: PayoutSchedule,
pub schedule_factor: i32,
pub usage_method: PayoutUsageMethod,
pub same_day: bool,
}
impl Default for PayoutConfig {
fn default() -> Self {
Self {
schedule: PayoutSchedule::Single,
schedule_factor: 1,
usage_method: PayoutUsageMethod::Actual,
same_day: false,
}
}
}
impl PayoutConfig {
pub fn single() -> Self {
Self::default()
}
pub fn single_same_day() -> Self {
Self {
same_day: true,
..Self::default()
}
}
pub fn recurring(schedule: PayoutSchedule, factor: i32) -> Self {
Self {
schedule,
schedule_factor: factor,
..Self::default()
}
}
}
pub async fn get_entity_by_custom_field(
client: &PayrixClient,
custom_value: &str,
) -> Result<serde_json::Value> {
let search = SearchBuilder::new().field("custom", custom_value).build();
let entities = client
.search::<serde_json::Value>(EntityType::Entities, &search)
.await?;
entities.into_iter().next().ok_or_else(|| {
crate::Error::NotFound(format!(
"No Payrix entity found with custom={}",
custom_value
))
})
}
pub async fn get_entity(
client: &PayrixClient,
entity_id: &str,
) -> Result<Option<serde_json::Value>> {
client
.get_one::<serde_json::Value>(EntityType::Entities, entity_id)
.await
}
pub async fn get_merchant_for_entity(
client: &PayrixClient,
entity_id: &str,
) -> Result<serde_json::Value> {
let search = SearchBuilder::new().field("entity", entity_id).build();
let merchants = client
.search::<serde_json::Value>(EntityType::Merchants, &search)
.await?;
merchants.into_iter().next().ok_or_else(|| {
crate::Error::NotFound(format!(
"No Payrix merchant found for entity {}",
entity_id
))
})
}
pub async fn get_merchant(
client: &PayrixClient,
merchant_id: &str,
) -> Result<Option<serde_json::Value>> {
client
.get_one::<serde_json::Value>(EntityType::Merchants, merchant_id)
.await
}
pub async fn get_accounts_for_entity(
client: &PayrixClient,
entity_id: &str,
) -> Result<Vec<BankAccount>> {
let search = SearchBuilder::new().field("entity", entity_id).build();
let accounts = client
.search::<serde_json::Value>(EntityType::Accounts, &search)
.await?;
let result: Vec<BankAccount> = accounts
.iter()
.map(|account| {
let account_type = account
.get("type")
.and_then(|t| t.as_i64())
.unwrap_or(0) as i32;
let id = account
.get("id")
.and_then(|i| i.as_str())
.unwrap_or("")
.to_string();
let token = account
.get("token")
.and_then(|t| t.as_str())
.unwrap_or("")
.to_string();
BankAccount {
id,
token,
account_type,
}
})
.collect();
Ok(result)
}
pub async fn get_account_by_type(
client: &PayrixClient,
entity_id: &str,
account_type: i32,
) -> Result<Option<BankAccount>> {
let accounts = get_accounts_for_entity(client, entity_id).await?;
Ok(accounts.into_iter().find(|a| a.account_type == account_type))
}
pub async fn get_funds_for_entity(client: &PayrixClient, entity_id: &str) -> Result<(i64, i64)> {
let search = SearchBuilder::new().field("entity", entity_id).build();
let funds = client
.search::<serde_json::Value>(EntityType::Funds, &search)
.await?;
let fund = funds.into_iter().next().ok_or_else(|| {
crate::Error::NotFound(
"No funds found - processor may not have processed any transactions yet".to_string(),
)
})?;
let available = fund
.get("available")
.and_then(|a| a.as_i64())
.unwrap_or(0);
let pending = fund.get("pending").and_then(|p| p.as_i64()).unwrap_or(0);
Ok((available, pending))
}
pub async fn get_payouts_for_entity(
client: &PayrixClient,
entity_id: &str,
start_date: Option<&str>,
end_date: Option<&str>,
) -> Result<Vec<serde_json::Value>> {
let mut search = SearchBuilder::new().field("entity", entity_id);
if let Some(start) = start_date {
search = search.field("created[greater]", start);
}
if let Some(end) = end_date {
search = search.field("created[less]", end);
}
let payouts = client
.search::<serde_json::Value>(EntityType::Payouts, &search.build())
.await?;
Ok(payouts)
}
pub async fn create_payout(
client: &PayrixClient,
entity_id: &str,
account_token: &str,
amount_cents: i64,
description: &str,
start_date: &str,
config: PayoutConfig,
) -> Result<serde_json::Value> {
let payout_data = serde_json::json!({
"entity": entity_id,
"account": account_token,
"amount": amount_cents,
"schedule": config.schedule.as_i32(),
"scheduleFactor": config.schedule_factor,
"start": start_date,
"description": description,
"um": config.usage_method.as_i32(),
"sameDay": if config.same_day { 1 } else { 0 },
});
let payout = client
.create::<_, serde_json::Value>(EntityType::Payouts, &payout_data)
.await?;
tracing::info!(
"Created payout {} for ${:.2} to account {}",
payout
.get("id")
.and_then(|i| i.as_str())
.unwrap_or("unknown"),
amount_cents as f64 / 100.0,
account_token
);
Ok(payout)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bank_account_struct() {
let account = BankAccount {
id: "acc_1".to_string(),
token: "tok_1".to_string(),
account_type: 1,
};
assert_eq!(account.id, "acc_1");
assert_eq!(account.token, "tok_1");
assert_eq!(account.account_type, 1);
}
#[test]
fn test_payout_schedule_values() {
assert_eq!(PayoutSchedule::Single.as_i32(), 5);
assert_eq!(PayoutSchedule::Daily.as_i32(), 1);
assert_eq!(PayoutSchedule::Weekly.as_i32(), 2);
assert_eq!(PayoutSchedule::BiWeekly.as_i32(), 3);
assert_eq!(PayoutSchedule::Monthly.as_i32(), 4);
}
#[test]
fn test_payout_usage_method_values() {
assert_eq!(PayoutUsageMethod::Percentage.as_i32(), 1);
assert_eq!(PayoutUsageMethod::Actual.as_i32(), 2);
}
#[test]
fn test_payout_config_default() {
let config = PayoutConfig::default();
assert_eq!(config.schedule.as_i32(), 5); assert_eq!(config.schedule_factor, 1);
assert_eq!(config.usage_method.as_i32(), 2); assert!(!config.same_day);
}
#[test]
fn test_payout_config_single() {
let config = PayoutConfig::single();
assert_eq!(config.schedule.as_i32(), 5);
assert!(!config.same_day);
}
#[test]
fn test_payout_config_single_same_day() {
let config = PayoutConfig::single_same_day();
assert_eq!(config.schedule.as_i32(), 5);
assert!(config.same_day);
}
#[test]
fn test_payout_config_recurring() {
let config = PayoutConfig::recurring(PayoutSchedule::Weekly, 2);
assert_eq!(config.schedule.as_i32(), 2); assert_eq!(config.schedule_factor, 2);
assert!(!config.same_day);
}
}