use crate::{EntityType, PayrixClient, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[repr(i32)]
pub enum TransactionOrigin {
CardPresent = 1,
CardNotPresent = 2,
#[default]
Ecommerce = 3,
}
impl TransactionOrigin {
pub fn as_i32(&self) -> i32 {
*self as i32
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[repr(i32)]
pub enum CardOnFileType {
#[default]
SingleUse = 1,
InitialStorage = 2,
SubsequentUse = 3,
MerchantInitiated = 4,
}
impl CardOnFileType {
pub fn as_i32(&self) -> i32 {
*self as i32
}
}
#[derive(Debug, Clone)]
pub struct TransactionConfig {
pub origin: TransactionOrigin,
pub cof_type: CardOnFileType,
pub allow_partial: bool,
}
impl Default for TransactionConfig {
fn default() -> Self {
Self {
origin: TransactionOrigin::Ecommerce,
cof_type: CardOnFileType::SingleUse,
allow_partial: false,
}
}
}
impl TransactionConfig {
pub fn ecommerce() -> Self {
Self::default()
}
pub fn card_present() -> Self {
Self {
origin: TransactionOrigin::CardPresent,
..Self::default()
}
}
pub fn recurring() -> Self {
Self {
cof_type: CardOnFileType::SubsequentUse,
..Self::default()
}
}
pub fn initial_storage() -> Self {
Self {
cof_type: CardOnFileType::InitialStorage,
..Self::default()
}
}
pub fn with_partial(mut self) -> Self {
self.allow_partial = true;
self
}
}
pub async fn create_credit_card_transaction(
client: &PayrixClient,
merchant_id: &str,
token: &str,
amount_cents: i64,
description: &str,
client_ip: &str,
config: TransactionConfig,
) -> Result<serde_json::Value> {
let txn_data = serde_json::json!({
"merchant": merchant_id,
"token": token,
"clientIp": client_ip,
"type": 1, "origin": config.origin.as_i32(),
"cofType": config.cof_type.as_i32(),
"allowPartial": if config.allow_partial { 1 } else { 0 },
"total": amount_cents,
"description": description,
});
let transaction = client
.create::<_, serde_json::Value>(EntityType::Txns, &txn_data)
.await?;
tracing::info!(
"Created credit card transaction {} for ${:.2}",
transaction
.get("id")
.and_then(|i| i.as_str())
.unwrap_or("unknown"),
amount_cents as f64 / 100.0
);
Ok(transaction)
}
pub async fn create_bank_transaction(
client: &PayrixClient,
merchant_id: &str,
token: &str,
amount_cents: i64,
description: &str,
client_ip: &str,
config: TransactionConfig,
) -> Result<serde_json::Value> {
let txn_data = serde_json::json!({
"merchant": merchant_id,
"token": token,
"clientIp": client_ip,
"type": 2, "origin": config.origin.as_i32(),
"cofType": config.cof_type.as_i32(),
"allowPartial": if config.allow_partial { 1 } else { 0 },
"total": amount_cents,
"description": description,
});
let transaction = client
.create::<_, serde_json::Value>(EntityType::Txns, &txn_data)
.await?;
tracing::info!(
"Created bank account transaction {} for ${:.2}",
transaction
.get("id")
.and_then(|i| i.as_str())
.unwrap_or("unknown"),
amount_cents as f64 / 100.0
);
Ok(transaction)
}
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_amount_conversion() {
let dollars = 10.99_f64;
let cents = (dollars * 100.0).ceil() as i64;
assert_eq!(cents, 1099);
}
#[test]
fn test_amount_conversion_rounding() {
let dollars = 10.999_f64;
let cents = (dollars * 100.0).ceil() as i64;
assert_eq!(cents, 1100); }
#[test]
fn test_transaction_origin_values() {
assert_eq!(TransactionOrigin::CardPresent.as_i32(), 1);
assert_eq!(TransactionOrigin::CardNotPresent.as_i32(), 2);
assert_eq!(TransactionOrigin::Ecommerce.as_i32(), 3);
}
#[test]
fn test_card_on_file_type_values() {
assert_eq!(CardOnFileType::SingleUse.as_i32(), 1);
assert_eq!(CardOnFileType::InitialStorage.as_i32(), 2);
assert_eq!(CardOnFileType::SubsequentUse.as_i32(), 3);
assert_eq!(CardOnFileType::MerchantInitiated.as_i32(), 4);
}
#[test]
fn test_transaction_config_default() {
let config = TransactionConfig::default();
assert_eq!(config.origin.as_i32(), 3); assert_eq!(config.cof_type.as_i32(), 1); assert!(!config.allow_partial);
}
#[test]
fn test_transaction_config_ecommerce() {
let config = TransactionConfig::ecommerce();
assert_eq!(config.origin.as_i32(), 3);
}
#[test]
fn test_transaction_config_card_present() {
let config = TransactionConfig::card_present();
assert_eq!(config.origin.as_i32(), 1);
}
#[test]
fn test_transaction_config_recurring() {
let config = TransactionConfig::recurring();
assert_eq!(config.cof_type.as_i32(), 3); }
#[test]
fn test_transaction_config_with_partial() {
let config = TransactionConfig::ecommerce().with_partial();
assert!(config.allow_partial);
}
}