From 51a3c747981702c6523da0581575deb59aba77e0 Mon Sep 17 00:00:00 2001 From: sswaminathan Date: Thu, 22 Oct 2026 11:14:07 -0700 Subject: [PATCH] fix(shopping): preserve API payment descriptions --- .../openapi/shopping.oas3.json | 3 + rust/src/shopping/common.rs | 3 + rust/src/shopping/human.rs | 69 +++++++++++++++++++ .../generate-api-catalog/src/shopping_spec.rs | 10 ++- 4 files changed, 84 insertions(+), 1 deletion(-) diff --git a/rust/shopping-client/openapi/shopping.oas3.json b/rust/shopping-client/openapi/shopping.oas3.json index 1521df74..99996372 100644 --- a/rust/shopping-client/openapi/shopping.oas3.json +++ b/rust/shopping-client/openapi/shopping.oas3.json @@ -1903,6 +1903,9 @@ "id": { "type": "string" }, + "rich_text_description": { + "type": "string" + }, "selected": { "type": "boolean" }, diff --git a/rust/src/shopping/common.rs b/rust/src/shopping/common.rs index 1f1dd9cf..5ea0f813 100644 --- a/rust/src/shopping/common.rs +++ b/rust/src/shopping/common.rs @@ -328,6 +328,7 @@ fn preserved_payment_body( // an unchanged checkout update must echo them back unchanged. handler_id: payment.handler_id.clone(), type_: payment.type_.clone(), + ..Default::default() }], })) } @@ -581,6 +582,7 @@ mod tests { selected: Some(true), handler_id: Some("com.godaddy.payments".to_owned()), type_: Some("card".to_owned()), + rich_text_description: Some("CREDIT_CARD/VISA 4242".to_owned()), ..Default::default() }], }), @@ -605,6 +607,7 @@ mod tests { "an unchanged selected instrument must keep its handler routing metadata" ); assert_eq!(instrument.type_, Some("card".to_owned())); + assert!(instrument.rich_text_description.is_none()); } #[test] diff --git a/rust/src/shopping/human.rs b/rust/src/shopping/human.rs index 5fd783f9..dd9b30f7 100644 --- a/rust/src/shopping/human.rs +++ b/rust/src/shopping/human.rs @@ -688,6 +688,7 @@ pub(crate) fn checkout_completion_response(completion: &Value) -> Value { "order_id": completion.pointer("/order/id").and_then(Value::as_str), "order_permalink": completion.pointer("/order/permalink_url").and_then(Value::as_str), "total": money::format_total(completion.get("totals"), completion.get("currency").and_then(Value::as_str)), + "selected_payment": selected_payment_instrument(completion).map(|_| selected_payment(completion)), }) } @@ -712,6 +713,9 @@ fn render_checkout_completion(completion: &Value) -> String { if let Some(total) = completion.get("total").and_then(Value::as_str) { output.push_str(&format!("Total: {total}\n")); } + if let Some(payment) = completion.get("selected_payment").and_then(Value::as_str) { + output.push_str(&format!("Selected payment: {payment}\n")); + } output } @@ -799,6 +803,71 @@ fn optional_text(value: &Value, key: &str, default: &str) -> String { mod tests { use super::*; + #[test] + fn typed_checkout_responses_keep_payment_descriptions_in_json_and_human_views() { + let payload = json!({ + "id": "checkout-1", + "status": "ready_for_complete", + "payment": {"instruments": [ + {"id": "wallet-1", "selected": true, + "rich_text_description": "E_WALLET/PAYPAL PayPal Agreement example"}, + {"id": "card-1", "rich_text_description": "CREDIT_CARD/VISA 4242", + "brand": "visa", "last_digits": "4242", "expiry_month": 12, "expiry_year": 2030} + ]} + }); + macro_rules! round_trip { + ($response:ty) => {{ + let typed: $response = + serde_json::from_value(payload.clone()).expect("decode response"); + serde_json::to_value(typed).expect("encode response") + }}; + } + for response in [ + round_trip!(shopping_client::types::CreateCheckoutResponse), + round_trip!(shopping_client::types::GetCheckoutResponse), + round_trip!(shopping_client::types::UpdateCheckoutResponse), + round_trip!(shopping_client::types::CompleteCheckoutResponse), + ] { + let instruments = &response["payment"]["instruments"]; + for index in 0..2 { + assert_eq!( + instruments[index]["rich_text_description"], + payload["payment"]["instruments"][index]["rich_text_description"] + ); + } + for field in ["brand", "last_digits", "expiry_month", "expiry_year"] { + assert!(instruments[1].get(field).is_none(), "unexpected {field}"); + } + let cart = checkout_response(&response, false); + let human = render_checkout(&cart); + assert!(human.contains( + "Selected payment: E_WALLET/PAYPAL PayPal Agreement example (ID: wallet-1)" + )); + assert!(human.contains("- CREDIT_CARD/VISA 4242 (ID: card-1)")); + let completion = checkout_completion_response(&response); + let human = render_checkout_completion(&completion); + assert!(human.contains( + "Selected payment: E_WALLET/PAYPAL PayPal Agreement example (ID: wallet-1)" + )); + assert!(!human.contains("CREDIT_CARD/VISA")); + } + } + + #[test] + fn completion_payment_summary_handles_missing_description_and_selection() { + let response = checkout_completion_response(&json!({ + "payment": {"instruments": [{"id": "card-1", "selected": true}]} + })); + assert!( + render_checkout_completion(&response) + .contains("Selected payment: Saved payment method (ID: card-1)") + ); + for payment in [Value::Null, json!({"instruments": [{"id": "card-1"}]})] { + let response = checkout_completion_response(&json!({"payment": payment})); + assert!(!render_checkout_completion(&response).contains("Selected payment:")); + } + } + fn catalog_response() -> Value { json!({ "pagination": {"total_count": 1}, diff --git a/rust/tools/generate-api-catalog/src/shopping_spec.rs b/rust/tools/generate-api-catalog/src/shopping_spec.rs index 486624d8..2ae77212 100644 --- a/rust/tools/generate-api-catalog/src/shopping_spec.rs +++ b/rust/tools/generate-api-catalog/src/shopping_spec.rs @@ -266,6 +266,11 @@ fn preserve_payment_instrument_fields(spec: &mut Value) -> Result<()> { serde_json::json!({"type": "string"}), ); properties.insert("type".to_owned(), serde_json::json!({"type": "string"})); + // The API builds this display label from the saved payment profile. + properties.insert( + "rich_text_description".to_owned(), + serde_json::json!({"type": "string"}), + ); Ok(()) } @@ -657,7 +662,7 @@ mod tests { } #[test] - fn preserving_payment_instrument_fields_adds_handler_routing_metadata() { + fn preserving_payment_instrument_fields_adds_routing_metadata_and_display_text() { let mut spec = json!({ "components": { "schemas": { @@ -693,6 +698,9 @@ mod tests { "type must survive so a checkout update can echo the selected instrument's routing metadata back unchanged" ); assert_eq!(properties["selected"]["type"], "boolean"); + assert_eq!(properties["rich_text_description"]["type"], "string"); + assert!(properties.get("expiry_month").is_none()); + assert!(properties.get("expiry_year").is_none()); } #[test]