From 71ca7626e28125f073cee0474aa96564162211c0 Mon Sep 17 00:00:00 2001 From: Josh Rotenberg Date: Fri, 15 May 2026 11:45:45 -0700 Subject: [PATCH] fix(account): PaymentMethod.credit_card_ends_with should be String, not i32 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OpenAPI example for `PaymentMethod` shows `creditCardEndsWith` as a string, and real responses commonly include leading-zero card tails (e.g. `"0042"`) that lose information when typed as `i32` — or fail to deserialize entirely if the value contains non-numeric formatting. Strengthens `test_get_account_payment_methods` from a vacuous `account_id.is_some()` check to a realistic round-trip that includes a `paymentMethods` array with a leading-zero card tail and asserts the field comes through intact. Breaking change to a public field type. The previous typing would silently mis-handle real responses, so no production caller can have been relying on the numeric form. Refs #40 (spec deltas tracking) Closes #77 --- src/account.rs | 8 ++++++-- tests/account_tests.rs | 26 ++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/account.rs b/src/account.rs index 2363a16..bc9fd0c 100644 --- a/src/account.rs +++ b/src/account.rs @@ -239,9 +239,13 @@ pub struct PaymentMethod { #[serde(skip_serializing_if = "Option::is_none")] pub r#type: Option, - /// Last 4 digits of the credit card + /// Last digits of the credit card as a masked string. + /// + /// Typed as `Option` because the OpenAPI example shows a string + /// value and real responses can include leading zeros (`"0042"`) or + /// non-numeric formatting that would be lost or fail to parse as `i32`. #[serde(skip_serializing_if = "Option::is_none")] - pub credit_card_ends_with: Option, + pub credit_card_ends_with: Option, /// Name on the card #[serde(skip_serializing_if = "Option::is_none")] diff --git a/tests/account_tests.rs b/tests/account_tests.rs index 5b0ebb0..3f0c7cc 100644 --- a/tests/account_tests.rs +++ b/tests/account_tests.rs @@ -182,12 +182,26 @@ async fn test_get_supported_regions() { async fn test_get_account_payment_methods() { let mock_server = MockServer::start().await; + // Realistic shape including a paymentMethods array entry with a + // string `creditCardEndsWith` containing leading zeros — the previous + // `Option` typing would either drop the leading zero or fail + // outright. Mock::given(method("GET")) .and(path("/payment-methods")) .and(header("x-api-key", "test-key")) .and(header("x-api-secret-key", "test-secret")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "accountId": 123 + "accountId": 123, + "paymentMethods": [ + { + "id": 555, + "type": "Visa", + "creditCardEndsWith": "0042", + "nameOnCard": "Alex Example", + "expirationMonth": 9, + "expirationYear": 2030 + } + ] }))) .mount(&mock_server) .await; @@ -202,7 +216,15 @@ async fn test_get_account_payment_methods() { let handler = AccountHandler::new(client); let result = handler.get_account_payment_methods().await.unwrap(); - assert!(result.account_id.is_some()); + assert_eq!(result.account_id, Some(123)); + let methods = result + .payment_methods + .expect("response should include paymentMethods"); + assert_eq!(methods.len(), 1); + assert_eq!(methods[0].id, Some(555)); + assert_eq!(methods[0].r#type.as_deref(), Some("Visa")); + // Regression guard for #77: leading-zero card tails survive the round-trip. + assert_eq!(methods[0].credit_card_ends_with.as_deref(), Some("0042")); } #[tokio::test]