wave-api 0.1.0

Typed Rust client for the Wave Accounting GraphQL API
Documentation
use wave_claw::error::{GraphqlError, InputError};
use wave_claw::models::*;
use wave_claw::pagination::Connection;

fn load_fixture(name: &str) -> serde_json::Value {
    let path = format!("tests/fixtures/{}.json", name);
    let content = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("fixture {}: {}", path, e));
    serde_json::from_str(&content).unwrap()
}

#[test]
fn deserialize_user() {
    let data = load_fixture("get_user");
    let user: User = serde_json::from_value(data["data"]["user"].clone()).unwrap();
    assert_eq!(user.id, "VXNlcjoxMjM0NTY3ODk=");
    assert_eq!(user.default_email.as_deref(), Some("user@example.com"));
    assert_eq!(user.first_name.as_deref(), Some("Jane"));
    assert_eq!(user.last_name.as_deref(), Some("Doe"));
}

#[test]
fn deserialize_businesses_connection() {
    let data = load_fixture("list_businesses");
    let conn: Connection<Business> =
        serde_json::from_value(data["data"]["businesses"].clone()).unwrap();
    let page = conn.into_page();

    assert_eq!(page.current_page, 1);
    assert_eq!(page.total_pages, Some(1));
    assert_eq!(page.total_count, Some(2));
    assert_eq!(page.items.len(), 2);
    assert!(!page.has_next_page());

    let personal = &page.items[0];
    assert_eq!(personal.name, "Personal");
    assert!(personal.is_personal);
    assert!(personal.address.is_none());

    let consulting = &page.items[1];
    assert_eq!(consulting.name, "Smith Consulting");
    assert!(!consulting.is_personal);
    assert_eq!(consulting.phone.as_deref(), Some("416-555-1234"));
    let addr = consulting.address.as_ref().unwrap();
    assert_eq!(addr.city.as_deref(), Some("Toronto"));
    assert_eq!(addr.postal_code.as_deref(), Some("M5V 1A1"));
}

#[test]
fn deserialize_customer_create_success() {
    let data = load_fixture("customer_create");
    let result = &data["data"]["customerCreate"];

    assert_eq!(result["didSucceed"].as_bool(), Some(true));

    let customer: Customer = serde_json::from_value(result["customer"].clone()).unwrap();
    assert_eq!(customer.name, "Acme Corp");
    assert_eq!(customer.email.as_deref(), Some("billing@acme.com"));
    assert_eq!(customer.first_name.as_deref(), Some("John"));

    let addr = customer.address.as_ref().unwrap();
    assert_eq!(addr.city.as_deref(), Some("San Francisco"));
}

#[test]
fn deserialize_mutation_failed() {
    let data = load_fixture("mutation_failed");
    let result = &data["data"]["customerCreate"];

    assert_eq!(result["didSucceed"].as_bool(), Some(false));

    let errors: Vec<InputError> =
        serde_json::from_value(result["inputErrors"].clone()).unwrap();
    assert_eq!(errors.len(), 1);
    assert_eq!(errors[0].code.as_deref(), Some("REQUIRED"));
    assert_eq!(errors[0].message.as_deref(), Some("This field is required."));
    assert_eq!(
        errors[0].path.as_ref().unwrap(),
        &["input".to_string(), "name".to_string()]
    );
}

#[test]
fn deserialize_graphql_error() {
    let json = r#"{
        "message": "Unknown argument \"name\" on field \"business\" of type \"Query\".",
        "path": [],
        "extensions": {
            "id": "e486c5f4-49b0-456f-897c-6d21bdabcde7",
            "code": "GRAPHQL_VALIDATION_FAILED"
        }
    }"#;
    let error: GraphqlError = serde_json::from_str(json).unwrap();
    assert_eq!(
        error.extensions.as_ref().unwrap().code.as_deref(),
        Some("GRAPHQL_VALIDATION_FAILED")
    );
}

#[test]
fn serialize_customer_create_input() {
    use wave_claw::inputs::{AddressInput, CustomerCreateInput};
    use wave_claw::enums::{CountryCode, CurrencyCode};

    let input = CustomerCreateInput::new("biz-123", "Acme Corp")
        .first_name("John")
        .email("john@acme.com")
        .address(
            AddressInput::new()
                .city("Toronto")
                .country_code(CountryCode::CA)
                .postal_code("M5V 1A1"),
        )
        .currency(CurrencyCode::CAD);

    let json = serde_json::to_value(&input).unwrap();
    assert_eq!(json["businessId"], "biz-123");
    assert_eq!(json["name"], "Acme Corp");
    assert_eq!(json["firstName"], "John");
    assert_eq!(json["email"], "john@acme.com");
    assert_eq!(json["address"]["city"], "Toronto");
    assert_eq!(json["address"]["countryCode"], "CA");
    assert_eq!(json["currency"], "CAD");
    // Optional fields that weren't set should be absent.
    assert!(json.get("phone").is_none());
    assert!(json.get("mobile").is_none());
}

#[test]
fn serialize_invoice_create_input() {
    use rust_decimal_macros::dec;
    use wave_claw::inputs::{InvoiceCreateInput, InvoiceCreateItemInput};

    let input = InvoiceCreateInput::new("biz-123", "cust-456")
        .items(vec![
            InvoiceCreateItemInput::new("prod-789")
                .quantity(dec!(2))
                .unit_price(dec!(49.99)),
        ])
        .memo("Thanks!");

    let json = serde_json::to_value(&input).unwrap();
    assert_eq!(json["businessId"], "biz-123");
    assert_eq!(json["customerId"], "cust-456");
    assert_eq!(json["memo"], "Thanks!");
    let items = json["items"].as_array().unwrap();
    assert_eq!(items.len(), 1);
    assert_eq!(items[0]["productId"], "prod-789");
    assert_eq!(items[0]["quantity"], "2");
    assert_eq!(items[0]["unitPrice"], "49.99");
}

#[test]
fn serialize_money_transaction_input() {
    use chrono::NaiveDate;
    use rust_decimal_macros::dec;
    use wave_claw::enums::{BalanceType, TransactionDirection};
    use wave_claw::inputs::{
        MoneyTransactionAnchorInput, MoneyTransactionCreateInput,
        MoneyTransactionLineItemInput,
    };

    let input = MoneyTransactionCreateInput::new(
        "biz-123",
        "ext-001",
        NaiveDate::from_ymd_opt(2025, 3, 1).unwrap(),
        "Test transaction",
        MoneyTransactionAnchorInput {
            account_id: "acct-bank".into(),
            amount: dec!(100),
            direction: TransactionDirection::Deposit,
        },
        vec![MoneyTransactionLineItemInput::new(
            "acct-income",
            dec!(100),
            BalanceType::Increase,
        )],
    );

    let json = serde_json::to_value(&input).unwrap();
    assert_eq!(json["businessId"], "biz-123");
    assert_eq!(json["externalId"], "ext-001");
    assert_eq!(json["date"], "2025-03-01");
    assert_eq!(json["anchor"]["direction"], "DEPOSIT");
    assert_eq!(json["lineItems"][0]["balance"], "INCREASE");
}