Skip to main content

a2a_ap2/types/
payment_request.rs

1//! W3C Payment Request API types used by AP2.
2//!
3//! These types mirror the [W3C Payment Request API](https://www.w3.org/TR/payment-request/)
4//! as adopted by the official AP2 Python SDK.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use super::contact::ContactAddress;
10
11/// A monetary amount with currency code.
12///
13/// Follows the W3C `PaymentCurrencyAmount` dictionary.
14///
15/// See: <https://www.w3.org/TR/payment-request/#dom-paymentcurrencyamount>
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct PaymentCurrencyAmount {
18    /// Three-letter ISO 4217 currency code (e.g. `"USD"`).
19    pub currency: String,
20    /// Monetary value.
21    pub value: f64,
22}
23
24/// An item for purchase and the value asked for it.
25///
26/// See: <https://www.w3.org/TR/payment-request/#dom-paymentitem>
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct PaymentItem {
29    /// Human-readable description of the item.
30    pub label: String,
31    /// The monetary amount of the item.
32    pub amount: PaymentCurrencyAmount,
33    /// If `true`, indicates the amount is not yet final.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub pending: Option<bool>,
36    /// Refund duration for this item, in days. Defaults to 30.
37    #[serde(
38        default = "default_refund_period",
39        deserialize_with = "deserialize_refund_period"
40    )]
41    pub refund_period: i32,
42}
43
44fn default_refund_period() -> i32 {
45    30
46}
47
48fn deserialize_refund_period<'de, D>(deserializer: D) -> Result<i32, D::Error>
49where
50    D: serde::Deserializer<'de>,
51{
52    #[derive(Deserialize)]
53    #[serde(untagged)]
54    enum IntOrFloat {
55        Int(i32),
56        Float(f64),
57    }
58
59    match IntOrFloat::deserialize(deserializer)? {
60        IntOrFloat::Int(i) => Ok(i),
61        IntOrFloat::Float(f) => Ok(f as i32),
62    }
63}
64
65/// A shipping option with its cost.
66///
67/// See: <https://www.w3.org/TR/payment-request/#dom-paymentshippingoption>
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub struct PaymentShippingOption {
70    /// Unique identifier for the shipping option.
71    pub id: String,
72    /// Human-readable description.
73    pub label: String,
74    /// Cost of this shipping option.
75    pub amount: PaymentCurrencyAmount,
76    /// Whether this is the default selection.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub selected: Option<bool>,
79}
80
81/// Options controlling what payer information to collect.
82///
83/// See: <https://www.w3.org/TR/payment-request/#dom-paymentoptions>
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub struct PaymentOptions {
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub request_payer_name: Option<bool>,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub request_payer_email: Option<bool>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub request_payer_phone: Option<bool>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub request_shipping: Option<bool>,
94    /// One of `"shipping"`, `"delivery"`, or `"pickup"`.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub shipping_type: Option<String>,
97}
98
99impl Default for PaymentOptions {
100    fn default() -> Self {
101        Self {
102            request_payer_name: Some(false),
103            request_payer_email: Some(false),
104            request_payer_phone: Some(false),
105            request_shipping: Some(true),
106            shipping_type: None,
107        }
108    }
109}
110
111/// A supported payment method and its associated data.
112///
113/// See: <https://www.w3.org/TR/payment-request/#dom-paymentmethoddata>
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115pub struct PaymentMethodData {
116    /// Payment method identifier (e.g. `"CARD"`, `"google-pay"`).
117    pub supported_methods: String,
118    /// Payment-method-specific data.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub data: Option<HashMap<String, serde_json::Value>>,
121}
122
123/// A price modifier that applies when a specific payment method is selected.
124///
125/// See: <https://www.w3.org/TR/payment-request/#dom-paymentdetailsmodifier>
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127pub struct PaymentDetailsModifier {
128    /// The payment method ID this modifier applies to.
129    pub supported_methods: String,
130    /// Overrides the original item total for this method.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub total: Option<PaymentItem>,
133    /// Additional line items for this payment method.
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub additional_display_items: Option<Vec<PaymentItem>>,
136    /// Payment-method-specific data for the modifier.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub data: Option<HashMap<String, serde_json::Value>>,
139}
140
141/// Details of the payment being requested.
142///
143/// See: <https://www.w3.org/TR/payment-request/#dom-paymentdetailsinit>
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct PaymentDetailsInit {
146    /// Unique identifier for this payment request.
147    pub id: String,
148    /// Line items to display to the user.
149    pub display_items: Vec<PaymentItem>,
150    /// Available shipping options.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub shipping_options: Option<Vec<PaymentShippingOption>>,
153    /// Price modifiers for particular payment methods.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub modifiers: Option<Vec<PaymentDetailsModifier>>,
156    /// Total payment amount.
157    pub total: PaymentItem,
158}
159
160/// A request for payment.
161///
162/// See: <https://www.w3.org/TR/payment-request/#paymentrequest-interface>
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164pub struct PaymentRequest {
165    /// Supported payment methods.
166    pub method_data: Vec<PaymentMethodData>,
167    /// Financial details of the transaction.
168    pub details: PaymentDetailsInit,
169    /// Options for collecting payer information.
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub options: Option<PaymentOptions>,
172    /// The user's provided shipping address.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub shipping_address: Option<ContactAddress>,
175}
176
177/// Indicates the user has chosen a payment method and approved a payment request.
178///
179/// See: <https://www.w3.org/TR/payment-request/#paymentresponse-interface>
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181pub struct PaymentResponse {
182    /// Unique ID from the original `PaymentRequest`.
183    pub request_id: String,
184    /// Payment method chosen by the user.
185    pub method_name: String,
186    /// Payment-method-specific transaction data.
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub details: Option<HashMap<String, serde_json::Value>>,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub shipping_address: Option<ContactAddress>,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub shipping_option: Option<PaymentShippingOption>,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub payer_name: Option<String>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub payer_email: Option<String>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub payer_phone: Option<String>,
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn payment_currency_amount_roundtrip() {
207        let amt = PaymentCurrencyAmount {
208            currency: "USD".into(),
209            value: 120.0,
210        };
211        let json = serde_json::to_string(&amt).unwrap();
212        let back: PaymentCurrencyAmount = serde_json::from_str(&json).unwrap();
213        assert_eq!(amt, back);
214    }
215
216    #[test]
217    fn payment_item_default_refund_period() {
218        let json = r#"{"label":"Shoes","amount":{"currency":"USD","value":99.99}}"#;
219        let item: PaymentItem = serde_json::from_str(json).unwrap();
220        assert_eq!(item.refund_period, 30);
221    }
222
223    #[test]
224    fn payment_request_full_roundtrip() {
225        let req = PaymentRequest {
226            method_data: vec![PaymentMethodData {
227                supported_methods: "CARD".into(),
228                data: Some(HashMap::from([(
229                    "payment_processor_url".into(),
230                    serde_json::Value::String("http://example.com/pay".into()),
231                )])),
232            }],
233            details: PaymentDetailsInit {
234                id: "order_123".into(),
235                display_items: vec![PaymentItem {
236                    label: "Cool Shoes".into(),
237                    amount: PaymentCurrencyAmount {
238                        currency: "USD".into(),
239                        value: 120.0,
240                    },
241                    pending: None,
242                    refund_period: 30,
243                }],
244                shipping_options: None,
245                modifiers: None,
246                total: PaymentItem {
247                    label: "Total".into(),
248                    amount: PaymentCurrencyAmount {
249                        currency: "USD".into(),
250                        value: 120.0,
251                    },
252                    pending: None,
253                    refund_period: 30,
254                },
255            },
256            options: Some(PaymentOptions {
257                request_payer_name: Some(false),
258                request_payer_email: Some(false),
259                request_payer_phone: Some(false),
260                request_shipping: Some(true),
261                shipping_type: None,
262            }),
263            shipping_address: None,
264        };
265        let json = serde_json::to_string(&req).unwrap();
266        let back: PaymentRequest = serde_json::from_str(&json).unwrap();
267        assert_eq!(req, back);
268    }
269
270    #[test]
271    fn payment_response_minimal() {
272        let resp = PaymentResponse {
273            request_id: "order_123".into(),
274            method_name: "CARD".into(),
275            details: None,
276            shipping_address: None,
277            shipping_option: None,
278            payer_name: None,
279            payer_email: None,
280            payer_phone: None,
281        };
282        let json = serde_json::to_string(&resp).unwrap();
283        let back: PaymentResponse = serde_json::from_str(&json).unwrap();
284        assert_eq!(resp, back);
285    }
286
287    #[test]
288    fn payment_method_data_single_string() {
289        // The official SDK uses a single string, NOT an array
290        let json = r#"{"supported_methods":"CARD"}"#;
291        let pmd: PaymentMethodData = serde_json::from_str(json).unwrap();
292        assert_eq!(pmd.supported_methods, "CARD");
293    }
294}