use serde::{Deserialize, Serialize};
use super::payment_request::{PaymentItem, PaymentRequest, PaymentResponse};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IntentMandate {
#[serde(default = "default_true")]
pub user_cart_confirmation_required: bool,
pub natural_language_description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub merchants: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skus: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_refundability: Option<bool>,
pub intent_expiry: String,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CartContents {
pub id: String,
pub user_cart_confirmation_required: bool,
pub payment_request: PaymentRequest,
pub cart_expiry: String,
pub merchant_name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CartMandate {
pub contents: CartContents,
#[serde(skip_serializing_if = "Option::is_none")]
pub merchant_authorization: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PaymentMandateContents {
pub payment_mandate_id: String,
pub payment_details_id: String,
pub payment_details_total: PaymentItem,
pub payment_response: PaymentResponse,
pub merchant_agent: String,
pub timestamp: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PaymentMandate {
pub payment_mandate_contents: PaymentMandateContents,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_authorization: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::payment_request::{
PaymentCurrencyAmount, PaymentDetailsInit, PaymentMethodData,
};
fn sample_payment_request() -> PaymentRequest {
PaymentRequest {
method_data: vec![PaymentMethodData {
supported_methods: "CARD".into(),
data: None,
}],
details: PaymentDetailsInit {
id: "order_123".into(),
display_items: vec![PaymentItem {
label: "Shoes".into(),
amount: PaymentCurrencyAmount {
currency: "USD".into(),
value: 120.0,
},
pending: None,
refund_period: 30,
}],
shipping_options: None,
modifiers: None,
total: PaymentItem {
label: "Total".into(),
amount: PaymentCurrencyAmount {
currency: "USD".into(),
value: 120.0,
},
pending: None,
refund_period: 30,
},
},
options: None,
shipping_address: None,
}
}
#[test]
fn intent_mandate_default_confirmation() {
let json = r#"{
"natural_language_description": "Buy red shoes",
"intent_expiry": "2026-12-31T23:59:59Z"
}"#;
let im: IntentMandate = serde_json::from_str(json).unwrap();
assert!(im.user_cart_confirmation_required);
}
#[test]
fn intent_mandate_roundtrip() {
let im = IntentMandate {
user_cart_confirmation_required: false,
natural_language_description: "Cool red shoes".into(),
merchants: Some(vec!["nike".into()]),
skus: None,
requires_refundability: Some(true),
intent_expiry: "2026-09-16T15:00:00Z".into(),
};
let json = serde_json::to_string(&im).unwrap();
let back: IntentMandate = serde_json::from_str(&json).unwrap();
assert_eq!(im, back);
}
#[test]
fn cart_mandate_roundtrip() {
let cm = CartMandate {
contents: CartContents {
id: "cart_123".into(),
user_cart_confirmation_required: false,
payment_request: sample_payment_request(),
cart_expiry: "2026-12-31T23:59:59Z".into(),
merchant_name: "Cool Shoe Store".into(),
},
merchant_authorization: Some("eyJhbGci...".into()),
};
let json = serde_json::to_string(&cm).unwrap();
let back: CartMandate = serde_json::from_str(&json).unwrap();
assert_eq!(cm, back);
}
#[test]
fn payment_mandate_roundtrip() {
let pm = PaymentMandate {
payment_mandate_contents: PaymentMandateContents {
payment_mandate_id: "pm_123".into(),
payment_details_id: "order_123".into(),
payment_details_total: PaymentItem {
label: "Total".into(),
amount: PaymentCurrencyAmount {
currency: "USD".into(),
value: 120.0,
},
pending: None,
refund_period: 30,
},
payment_response: crate::types::payment_request::PaymentResponse {
request_id: "order_123".into(),
method_name: "CARD".into(),
details: None,
shipping_address: None,
shipping_option: None,
payer_name: None,
payer_email: None,
payer_phone: None,
},
merchant_agent: "MerchantAgent".into(),
timestamp: "2025-08-26T19:36:36Z".into(),
},
user_authorization: Some("eyJhbGci...".into()),
};
let json = serde_json::to_string(&pm).unwrap();
let back: PaymentMandate = serde_json::from_str(&json).unwrap();
assert_eq!(pm, back);
}
#[test]
fn compat_with_python_sdk_intent_message() {
let json = r#"{
"user_cart_confirmation_required": false,
"natural_language_description": "I'd like some cool red shoes in my size",
"merchants": null,
"skus": null,
"required_refundability": true,
"intent_expiry": "2025-09-16T15:00:00Z"
}"#;
let _im: IntentMandate = serde_json::from_str(json).unwrap();
}
#[test]
fn compat_with_python_sdk_cart_artifact() {
let json = r#"{
"contents": {
"id": "cart_shoes_123",
"user_cart_confirmation_required": false,
"user_signature_required": false,
"payment_request": {
"method_data": [{"supported_methods": "CARD", "data": {"payment_processor_url": "http://example.com/pay"}}],
"details": {
"id": "order_shoes_123",
"display_items": [{"label": "Cool Shoes Max", "amount": {"currency": "USD", "value": 120.0}}],
"total": {"label": "Total", "amount": {"currency": "USD", "value": 120.0}}
},
"options": {
"request_payer_name": false,
"request_payer_email": false,
"request_payer_phone": false,
"request_shipping": true
}
},
"cart_expiry": "2026-12-31T23:59:59Z",
"merchant_name": "Cool Shoe Store"
},
"merchant_authorization": "sig_merchant_shoes_abc1"
}"#;
let cm: CartMandate = serde_json::from_str(json).unwrap();
assert_eq!(cm.contents.id, "cart_shoes_123");
}
}