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(default = "default_refund_period")]
38    pub refund_period: i32,
39}
40
41fn default_refund_period() -> i32 {
42    30
43}
44
45/// A shipping option with its cost.
46///
47/// See: <https://www.w3.org/TR/payment-request/#dom-paymentshippingoption>
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct PaymentShippingOption {
50    /// Unique identifier for the shipping option.
51    pub id: String,
52    /// Human-readable description.
53    pub label: String,
54    /// Cost of this shipping option.
55    pub amount: PaymentCurrencyAmount,
56    /// Whether this is the default selection.
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub selected: Option<bool>,
59}
60
61/// Options controlling what payer information to collect.
62///
63/// See: <https://www.w3.org/TR/payment-request/#dom-paymentoptions>
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct PaymentOptions {
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub request_payer_name: Option<bool>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub request_payer_email: Option<bool>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub request_payer_phone: Option<bool>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub request_shipping: Option<bool>,
74    /// One of `"shipping"`, `"delivery"`, or `"pickup"`.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub shipping_type: Option<String>,
77}
78
79impl Default for PaymentOptions {
80    fn default() -> Self {
81        Self {
82            request_payer_name: Some(false),
83            request_payer_email: Some(false),
84            request_payer_phone: Some(false),
85            request_shipping: Some(true),
86            shipping_type: None,
87        }
88    }
89}
90
91/// A supported payment method and its associated data.
92///
93/// See: <https://www.w3.org/TR/payment-request/#dom-paymentmethoddata>
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct PaymentMethodData {
96    /// Payment method identifier (e.g. `"CARD"`, `"google-pay"`).
97    pub supported_methods: String,
98    /// Payment-method-specific data.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub data: Option<HashMap<String, serde_json::Value>>,
101}
102
103/// A price modifier that applies when a specific payment method is selected.
104///
105/// See: <https://www.w3.org/TR/payment-request/#dom-paymentdetailsmodifier>
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107pub struct PaymentDetailsModifier {
108    /// The payment method ID this modifier applies to.
109    pub supported_methods: String,
110    /// Overrides the original item total for this method.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub total: Option<PaymentItem>,
113    /// Additional line items for this payment method.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub additional_display_items: Option<Vec<PaymentItem>>,
116    /// Payment-method-specific data for the modifier.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub data: Option<HashMap<String, serde_json::Value>>,
119}
120
121/// Details of the payment being requested.
122///
123/// See: <https://www.w3.org/TR/payment-request/#dom-paymentdetailsinit>
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
125pub struct PaymentDetailsInit {
126    /// Unique identifier for this payment request.
127    pub id: String,
128    /// Line items to display to the user.
129    pub display_items: Vec<PaymentItem>,
130    /// Available shipping options.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub shipping_options: Option<Vec<PaymentShippingOption>>,
133    /// Price modifiers for particular payment methods.
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub modifiers: Option<Vec<PaymentDetailsModifier>>,
136    /// Total payment amount.
137    pub total: PaymentItem,
138}
139
140/// A request for payment.
141///
142/// See: <https://www.w3.org/TR/payment-request/#paymentrequest-interface>
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144pub struct PaymentRequest {
145    /// Supported payment methods.
146    pub method_data: Vec<PaymentMethodData>,
147    /// Financial details of the transaction.
148    pub details: PaymentDetailsInit,
149    /// Options for collecting payer information.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub options: Option<PaymentOptions>,
152    /// The user's provided shipping address.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub shipping_address: Option<ContactAddress>,
155}
156
157/// Indicates the user has chosen a payment method and approved a payment request.
158///
159/// See: <https://www.w3.org/TR/payment-request/#paymentresponse-interface>
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161pub struct PaymentResponse {
162    /// Unique ID from the original `PaymentRequest`.
163    pub request_id: String,
164    /// Payment method chosen by the user.
165    pub method_name: String,
166    /// Payment-method-specific transaction data.
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub details: Option<HashMap<String, serde_json::Value>>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub shipping_address: Option<ContactAddress>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub shipping_option: Option<PaymentShippingOption>,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub payer_name: Option<String>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub payer_email: Option<String>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub payer_phone: Option<String>,
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn payment_currency_amount_roundtrip() {
187        let amt = PaymentCurrencyAmount {
188            currency: "USD".into(),
189            value: 120.0,
190        };
191        let json = serde_json::to_string(&amt).unwrap();
192        let back: PaymentCurrencyAmount = serde_json::from_str(&json).unwrap();
193        assert_eq!(amt, back);
194    }
195
196    #[test]
197    fn payment_item_default_refund_period() {
198        let json = r#"{"label":"Shoes","amount":{"currency":"USD","value":99.99}}"#;
199        let item: PaymentItem = serde_json::from_str(json).unwrap();
200        assert_eq!(item.refund_period, 30);
201    }
202
203    #[test]
204    fn payment_request_full_roundtrip() {
205        let req = PaymentRequest {
206            method_data: vec![PaymentMethodData {
207                supported_methods: "CARD".into(),
208                data: Some(HashMap::from([(
209                    "payment_processor_url".into(),
210                    serde_json::Value::String("http://example.com/pay".into()),
211                )])),
212            }],
213            details: PaymentDetailsInit {
214                id: "order_123".into(),
215                display_items: vec![PaymentItem {
216                    label: "Cool Shoes".into(),
217                    amount: PaymentCurrencyAmount {
218                        currency: "USD".into(),
219                        value: 120.0,
220                    },
221                    pending: None,
222                    refund_period: 30,
223                }],
224                shipping_options: None,
225                modifiers: None,
226                total: PaymentItem {
227                    label: "Total".into(),
228                    amount: PaymentCurrencyAmount {
229                        currency: "USD".into(),
230                        value: 120.0,
231                    },
232                    pending: None,
233                    refund_period: 30,
234                },
235            },
236            options: Some(PaymentOptions {
237                request_payer_name: Some(false),
238                request_payer_email: Some(false),
239                request_payer_phone: Some(false),
240                request_shipping: Some(true),
241                shipping_type: None,
242            }),
243            shipping_address: None,
244        };
245        let json = serde_json::to_string(&req).unwrap();
246        let back: PaymentRequest = serde_json::from_str(&json).unwrap();
247        assert_eq!(req, back);
248    }
249
250    #[test]
251    fn payment_response_minimal() {
252        let resp = PaymentResponse {
253            request_id: "order_123".into(),
254            method_name: "CARD".into(),
255            details: None,
256            shipping_address: None,
257            shipping_option: None,
258            payer_name: None,
259            payer_email: None,
260            payer_phone: None,
261        };
262        let json = serde_json::to_string(&resp).unwrap();
263        let back: PaymentResponse = serde_json::from_str(&json).unwrap();
264        assert_eq!(resp, back);
265    }
266
267    #[test]
268    fn payment_method_data_single_string() {
269        // The official SDK uses a single string, NOT an array
270        let json = r#"{"supported_methods":"CARD"}"#;
271        let pmd: PaymentMethodData = serde_json::from_str(json).unwrap();
272        assert_eq!(pmd.supported_methods, "CARD");
273    }
274}