diff --git a/crates/yttrium/src/pay/e2e_tests.rs b/crates/yttrium/src/pay/e2e_tests.rs index cafce3f4..479c3bed 100644 --- a/crates/yttrium/src/pay/e2e_tests.rs +++ b/crates/yttrium/src/pay/e2e_tests.rs @@ -249,11 +249,8 @@ async fn e2e_payment_options_only() { .expect("Failed to get payment options"); assert!(!response.payment_id.is_empty()); - println!( - "Payment {} has {} options", - response.payment_id, - response.options.len() - ); + let options = response.options.expect("Expected options to be present"); + println!("Payment {} has {} options", response.payment_id, options.len()); if let Some(info) = &response.info { println!( @@ -263,10 +260,7 @@ async fn e2e_payment_options_only() { println!("Merchant: {}", info.merchant.name); } - assert!( - !response.options.is_empty(), - "Expected at least one payment option" - ); + assert!(!options.is_empty(), "Expected at least one payment option"); } #[tokio::test] @@ -297,11 +291,12 @@ async fn e2e_payment_happy_path() { .await .expect("Failed to get payment options"); - println!("Got {} payment options", options_response.options.len()); - assert!( - !options_response.options.is_empty(), - "Expected at least one payment option" - ); + let options = options_response + .options + .as_ref() + .expect("Expected options to be present"); + println!("Got {} payment options", options.len()); + assert!(!options.is_empty(), "Expected at least one payment option"); if let Some(info) = &options_response.info { println!( @@ -312,13 +307,12 @@ async fn e2e_payment_happy_path() { } // Step 3: Select first available option (prefer Base chain) - let selected_option = options_response - .options + let selected_option = options .iter() .find(|opt| { opt.actions.iter().any(|a| a.wallet_rpc.chain_id == CHAIN_BASE) }) - .or_else(|| options_response.options.first()) + .or_else(|| options.first()) .expect("No payment option available"); println!( diff --git a/crates/yttrium/src/pay/mod.rs b/crates/yttrium/src/pay/mod.rs index 47adbb23..f3e5ce45 100644 --- a/crates/yttrium/src/pay/mod.rs +++ b/crates/yttrium/src/pay/mod.rs @@ -777,7 +777,7 @@ impl From for PaymentInfo { pub struct PaymentOptionsResponse { pub payment_id: String, pub info: Option, - pub options: Vec, + pub options: Option>, pub collect_data: Option, } @@ -984,12 +984,14 @@ impl WalletConnectPay { let api_response = response.into_inner(); pay_debug!( "get_payment_options: success, {} options", - api_response.options.len() + api_response.options.as_ref().map_or(0, Vec::len) ); // Cache the options with their raw actions let cached: Vec = api_response .options + .as_deref() + .unwrap_or_default() .iter() .map(|o| CachedPaymentOption { option_id: o.id.clone(), @@ -1006,7 +1008,9 @@ impl WalletConnectPay { Ok(PaymentOptionsResponse { payment_id, info: api_response.info.map(Into::into), - options: api_response.options.into_iter().map(Into::into).collect(), + options: api_response + .options + .map(|opts| opts.into_iter().map(Into::into).collect()), collect_data: api_response.collect_data.map(Into::into), }) } @@ -1839,18 +1843,15 @@ mod tests { assert!(result.is_ok()); let response = result.unwrap(); - assert_eq!(response.options.len(), 1); - assert_eq!(response.options[0].id, "opt_1"); + let options = response.options.expect("options"); + assert_eq!(options.len(), 1); + assert_eq!(options[0].id, "opt_1"); + assert_eq!(options[0].amount.unit, "caip19/eip155:8453/erc20:0xUSDC"); assert_eq!( - response.options[0].amount.unit, - "caip19/eip155:8453/erc20:0xUSDC" - ); - assert_eq!( - response.options[0].amount.display.network_name, + options[0].amount.display.network_name, Some("Base".to_string()) ); - let opt_cd = - response.options[0].collect_data.as_ref().expect("collect_data"); + let opt_cd = options[0].collect_data.as_ref().expect("collect_data"); assert_eq!(opt_cd.fields.len(), 1); assert_eq!(opt_cd.fields[0].id, "fullName"); assert_eq!(opt_cd.fields[0].field_type, CollectDataFieldType::Text); @@ -1923,6 +1924,57 @@ mod tests { )); } + #[tokio::test] + async fn test_get_payment_options_null_options_terminal_state() { + let mock_server = MockServer::start().await; + let mock_response = serde_json::json!({ + "info": { + "status": "succeeded", + "amount": { + "unit": "iso4217/USD", + "value": "1", + "display": { + "assetSymbol": "USD", + "assetName": "US Dollar", + "decimals": 2 + } + }, + "expiresAt": 1775490768_i64, + "merchant": { + "name": "Test Merchant", + "iconUrl": null + } + }, + "collectData": null, + "options": null + }); + + Mock::given(method("POST")) + .and(path("/v1/gateway/payment/pay_succeeded/options")) + .respond_with( + ResponseTemplate::new(200).set_body_json(&mock_response), + ) + .mount(&mock_server) + .await; + + let client = + WalletConnectPay::new(test_config(mock_server.uri())).unwrap(); + let result = client + .get_payment_options( + "pay_succeeded".to_string(), + vec!["eip155:8453:0x123".to_string()], + true, + ) + .await; + + assert!(result.is_ok()); + let response = result.unwrap(); + assert!(response.options.is_none()); + assert!(response.info.is_some()); + let info = response.info.unwrap(); + assert_eq!(info.status, PaymentStatus::Succeeded); + } + #[tokio::test] async fn test_extract_payment_id() { // pay.walletconnect.com with path segment @@ -2147,7 +2199,7 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.options.len(), 1); + assert_eq!(response.options.as_ref().map(Vec::len), Some(1)); let actions = client .get_required_payment_actions( @@ -2614,7 +2666,11 @@ mod tests { Some("https://data-collection.example.com/ic/pay_123".to_string()) ); assert!(data.schema.is_some()); - assert!(response.options[0].collect_data.is_none()); + assert!( + response.options.as_ref().expect("options")[0] + .collect_data + .is_none() + ); } #[tokio::test] diff --git a/crates/yttrium/src/pay/openapi.json b/crates/yttrium/src/pay/openapi.json index a9bec22d..44ce4775 100644 --- a/crates/yttrium/src/pay/openapi.json +++ b/crates/yttrium/src/pay/openapi.json @@ -1760,7 +1760,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/PaymentOption" - } + }, + "nullable": true } }, "example": {