payrix 0.3.0

Rust client for the Payrix payment processing API
// Customer Management Workflow
//
// This workflow handles customer CRUD operations in Payrix.
//
// Key features:
// - Create, read, update, delete customers
// - Find customers by custom field value
// - Generic API - callers control their own metadata

use crate::{EntityType, PayrixClient, Result, SearchBuilder};
use serde::{Deserialize, Serialize};

/// Address information for customer creation
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Address {
    /// Street address line 1
    pub address1: Option<String>,
    /// Street address line 2
    pub address2: Option<String>,
    /// City name
    pub city: Option<String>,
    /// State or province code
    pub state: Option<String>,
    /// Postal code
    pub zip: Option<String>,
    /// Country code
    pub country: Option<String>,
}

/// Customer data for creation
#[derive(Debug, Clone, Serialize)]
pub struct CustomerData {
    /// Customer first name
    pub first: Option<String>,
    /// Customer middle name
    pub middle: Option<String>,
    /// Customer last name
    pub last: Option<String>,
    /// Customer email address
    pub email: String,
    /// Customer phone number
    pub phone: String,
    #[serde(flatten)]
    /// Customer address information
    pub address: Address,
    /// Custom field - caller can store any identifier they need
    pub custom: Option<String>,
}

/// Find customers by custom field value
///
/// Searches for customers using the `custom` field which can store
/// any application-specific identifier.
///
/// # Arguments
/// * `client` - Payrix API client
/// * `custom_value` - Value to search for in the custom field
///
/// # Returns
/// Vector of all Payrix customers with matching custom field
pub async fn find_customers_by_custom_field(
    client: &PayrixClient,
    custom_value: &str,
) -> Result<Vec<serde_json::Value>> {
    let search = SearchBuilder::new().field("custom", custom_value).build();

    let customers = client
        .search::<serde_json::Value>(EntityType::Customers, &search)
        .await?;

    Ok(customers)
}

/// Create a new Payrix customer
///
/// # Arguments
/// * `client` - Payrix API client
/// * `merchant_id` - Payrix merchant ID
/// * `login_id` - Payrix login ID
/// * `customer_data` - Customer information
///
/// # Returns
/// The created Payrix customer
pub async fn create_customer(
    client: &PayrixClient,
    merchant_id: &str,
    login_id: &str,
    customer_data: &CustomerData,
) -> Result<serde_json::Value> {
    // Clean phone number (remove non-numeric characters)
    let clean_phone = customer_data
        .phone
        .chars()
        .filter(|c| c.is_numeric())
        .collect::<String>();

    let data = serde_json::json!({
        "login": login_id,
        "merchant": merchant_id,
        "first": customer_data.first,
        "middle": customer_data.middle,
        "last": customer_data.last,
        "email": customer_data.email,
        "custom": customer_data.custom,
        "address1": customer_data.address.address1,
        "address2": customer_data.address.address2,
        "city": customer_data.address.city,
        "state": customer_data.address.state,
        "zip": customer_data.address.zip,
        "country": customer_data.address.country,
        "phone": clean_phone,
        "inactive": 0,
        "frozen": 0,
    });

    let customer = client
        .create::<_, serde_json::Value>(EntityType::Customers, &data)
        .await?;

    tracing::info!(
        "Created Payrix customer {}",
        customer
            .get("id")
            .and_then(|i| i.as_str())
            .unwrap_or("unknown")
    );

    Ok(customer)
}

/// Get a customer by ID
///
/// # Arguments
/// * `client` - Payrix API client
/// * `customer_id` - Payrix customer ID
///
/// # Returns
/// The customer if found
pub async fn get_customer(
    client: &PayrixClient,
    customer_id: &str,
) -> Result<Option<serde_json::Value>> {
    client
        .get_one::<serde_json::Value>(EntityType::Customers, customer_id)
        .await
}

/// Update a customer with new fields
///
/// # Arguments
/// * `client` - Payrix API client
/// * `customer_id` - Payrix customer ID
/// * `updates` - JSON object with fields to update
///
/// # Returns
/// The updated customer
pub async fn update_customer(
    client: &PayrixClient,
    customer_id: &str,
    updates: &serde_json::Value,
) -> Result<serde_json::Value> {
    let customer = client
        .update::<_, serde_json::Value>(EntityType::Customers, customer_id, updates)
        .await?;

    tracing::info!("Updated Payrix customer {}", customer_id);

    Ok(customer)
}

/// Delete a customer
///
/// # Arguments
/// * `client` - Payrix API client
/// * `customer_id` - Payrix customer ID
///
/// # Returns
/// Ok(()) if customer was deleted successfully
pub async fn delete_customer(client: &PayrixClient, customer_id: &str) -> Result<()> {
    let _: serde_json::Value = client.remove(EntityType::Customers, customer_id).await?;
    tracing::info!("Deleted Payrix customer {}", customer_id);
    Ok(())
}

/// Delete all customers with a specific custom field value
///
/// # Arguments
/// * `client` - Payrix API client
/// * `custom_value` - Custom field value to match
///
/// # Returns
/// Ok(()) if all customers were deleted successfully
pub async fn delete_customers_by_custom_field(
    client: &PayrixClient,
    custom_value: &str,
) -> Result<()> {
    let customers = find_customers_by_custom_field(client, custom_value).await?;

    for customer in customers {
        if let Some(id) = customer.get("id").and_then(|i| i.as_str()) {
            let _: serde_json::Value = client.remove(EntityType::Customers, id).await?;
            tracing::info!("Deleted Payrix customer {}", id);
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_customer_data_creation() {
        let customer = CustomerData {
            first: Some("John".to_string()),
            middle: Some("Q".to_string()),
            last: Some("Doe".to_string()),
            email: "john@example.com".to_string(),
            phone: "555-123-4567".to_string(),
            address: Address {
                address1: Some("123 Main St".to_string()),
                address2: None,
                city: Some("Boston".to_string()),
                state: Some("MA".to_string()),
                zip: Some("02101".to_string()),
                country: Some("US".to_string()),
            },
            custom: Some("my-custom-id".to_string()),
        };

        assert_eq!(customer.first.unwrap(), "John");
        assert_eq!(customer.custom.unwrap(), "my-custom-id");
    }

    #[test]
    fn test_address_creation() {
        let address = Address {
            address1: Some("123 Main St".to_string()),
            address2: None,
            city: Some("Boston".to_string()),
            state: Some("MA".to_string()),
            zip: Some("02101".to_string()),
            country: Some("US".to_string()),
        };

        assert_eq!(address.zip.unwrap(), "02101");
    }
}