use crate::{EntityType, PayrixClient, Result};
#[derive(Debug, Clone)]
pub struct ContactName {
pub first: String,
pub middle: String,
pub last: String,
}
pub async fn refund_credit_card(
client: &PayrixClient,
transaction_id: &str,
amount_cents: i64,
description: &str,
client_ip: &str,
) -> Result<serde_json::Value> {
let refund_data = serde_json::json!({
"fortxn": transaction_id,
"clientIp": client_ip,
"type": 3, "amount": amount_cents,
"description": description,
});
let refund = client
.create::<_, serde_json::Value>(EntityType::Txns, &refund_data)
.await?;
tracing::info!(
"Refunded credit card transaction {} - ${:.2}",
transaction_id,
amount_cents as f64 / 100.0
);
Ok(refund)
}
pub async fn refund_bank_account(
client: &PayrixClient,
transaction_id: &str,
amount_cents: i64,
description: &str,
contact: &ContactName,
client_ip: &str,
) -> Result<serde_json::Value> {
let refund_data = serde_json::json!({
"fortxn": transaction_id,
"clientIp": client_ip,
"type": 4, "amount": amount_cents,
"first": contact.first,
"middle": contact.middle,
"last": contact.last,
"description": description,
});
let refund = client
.create::<_, serde_json::Value>(EntityType::Txns, &refund_data)
.await?;
tracing::info!(
"Refunded bank account transaction {} - ${:.2}",
transaction_id,
amount_cents as f64 / 100.0
);
Ok(refund)
}
pub async fn void_transaction(client: &PayrixClient, transaction_id: &str) -> Result<()> {
let transaction = client
.get_one::<serde_json::Value>(EntityType::Txns, transaction_id)
.await?
.ok_or_else(|| crate::Error::NotFound(format!("Transaction {} not found", transaction_id)))?;
let batch_id = transaction
.get("batch")
.and_then(|b| b.as_str())
.ok_or_else(|| {
crate::Error::BadRequest("Transaction has no batch - cannot void".to_string())
})?;
let batch = client
.get_one::<serde_json::Value>(EntityType::Batches, batch_id)
.await?
.ok_or_else(|| crate::Error::NotFound(format!("Batch {} not found", batch_id)))?;
let batch_status = batch
.get("status")
.and_then(|s| s.as_str())
.unwrap_or("closed");
if batch_status != "open" {
return Err(crate::Error::BadRequest(format!(
"Cannot void transaction {} - batch is {} (must be open)",
transaction_id, batch_status
)));
}
let update = serde_json::json!({ "batch": null });
client
.update::<_, serde_json::Value>(EntityType::Txns, transaction_id, &update)
.await?;
tracing::info!(
"Voided transaction {} (removed from open batch)",
transaction_id
);
Ok(())
}
pub async fn can_void_transaction(client: &PayrixClient, transaction_id: &str) -> Result<bool> {
let transaction = client
.get_one::<serde_json::Value>(EntityType::Txns, transaction_id)
.await?
.ok_or_else(|| crate::Error::NotFound(format!("Transaction {} not found", transaction_id)))?;
let batch_id = match transaction.get("batch").and_then(|b| b.as_str()) {
Some(id) => id,
None => return Ok(false),
};
let batch = client
.get_one::<serde_json::Value>(EntityType::Batches, batch_id)
.await?
.ok_or_else(|| crate::Error::NotFound(format!("Batch {} not found", batch_id)))?;
let batch_status = batch
.get("status")
.and_then(|s| s.as_str())
.unwrap_or("closed");
Ok(batch_status == "open")
}
pub async fn cancel_transaction(
client: &PayrixClient,
transaction_id: &str,
description: &str,
client_ip: &str,
contact: Option<&ContactName>,
) -> Result<serde_json::Value> {
let transaction = client
.get_one::<serde_json::Value>(EntityType::Txns, transaction_id)
.await?
.ok_or_else(|| crate::Error::NotFound(format!("Transaction {} not found", transaction_id)))?;
if let Some(batch_id) = transaction.get("batch").and_then(|b| b.as_str()) {
let batch = client
.get_one::<serde_json::Value>(EntityType::Batches, batch_id)
.await?
.ok_or_else(|| crate::Error::NotFound(format!("Batch {} not found", batch_id)))?;
let batch_status = batch
.get("status")
.and_then(|s| s.as_str())
.unwrap_or("open");
if batch_status == "open" {
let update = serde_json::json!({ "batch": null });
client
.update::<_, serde_json::Value>(EntityType::Txns, transaction_id, &update)
.await?;
tracing::info!(
"Voided transaction {} (removed from open batch)",
transaction_id
);
return Ok(transaction);
}
}
let amount_cents = transaction
.get("total")
.and_then(|t| t.as_i64())
.unwrap_or(0);
let txn_type = transaction
.get("type")
.and_then(|t| t.as_i64())
.unwrap_or(0);
tracing::info!(
"Cannot void settled transaction {}, issuing refund instead",
transaction_id
);
if txn_type == 1 {
refund_credit_card(client, transaction_id, amount_cents, description, client_ip).await
} else {
let contact = contact.ok_or_else(|| {
crate::Error::BadRequest(
"Contact name required for bank account refund".to_string(),
)
})?;
refund_bank_account(
client,
transaction_id,
amount_cents,
description,
contact,
client_ip,
)
.await
}
}
pub async fn get_transaction(
client: &PayrixClient,
transaction_id: &str,
) -> Result<Option<serde_json::Value>> {
client
.get_one::<serde_json::Value>(EntityType::Txns, transaction_id)
.await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contact_name_creation() {
let contact = ContactName {
first: "John".to_string(),
middle: "Q".to_string(),
last: "Doe".to_string(),
};
assert_eq!(contact.first, "John");
assert_eq!(contact.last, "Doe");
}
}