agentic_payments/ap2/
mandates.rs

1//! Mandate Types for AP2 Protocol
2//!
3//! Three types of mandates:
4//! 1. Intent Mandates - User authorization for agent actions
5//! 2. Cart Mandates - Explicit purchase authorization
6//! 3. Payment Mandates - Payment network signaling
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12/// Mandate Type Enumeration
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14#[serde(rename_all = "snake_case")]
15pub enum MandateType {
16    Intent,
17    Cart,
18    Payment,
19}
20
21/// Mandate Status
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23#[serde(rename_all = "snake_case")]
24pub enum MandateStatus {
25    Pending,
26    Active,
27    Completed,
28    Cancelled,
29    Expired,
30}
31
32/// Base Mandate Trait
33pub trait Mandate {
34    fn mandate_type(&self) -> MandateType;
35    fn issuer(&self) -> &str;
36    fn created_at(&self) -> DateTime<Utc>;
37    fn expires_at(&self) -> Option<DateTime<Utc>>;
38    fn status(&self) -> MandateStatus;
39    fn is_valid(&self) -> bool {
40        let now = Utc::now();
41        match self.expires_at() {
42            Some(expiry) => now < expiry && self.status() == MandateStatus::Active,
43            None => self.status() == MandateStatus::Active,
44        }
45    }
46}
47
48/// Intent Mandate - User authorizes agent to perform specific actions
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct IntentMandate {
51    pub id: String,
52    pub issuer: String,           // User DID
53    pub subject_agent: String,    // Agent DID
54    pub intent_description: String,
55    pub permissions: Vec<Permission>,
56    pub constraints: HashMap<String, serde_json::Value>,
57    pub created_at: DateTime<Utc>,
58    pub expires_at: Option<DateTime<Utc>>,
59    pub status: MandateStatus,
60    pub metadata: HashMap<String, String>,
61}
62
63/// Permission for agent actions
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
65pub struct Permission {
66    pub action: String,
67    pub resource: String,
68    pub conditions: Vec<String>,
69}
70
71impl IntentMandate {
72    pub fn new(issuer: String, subject_agent: String, intent_description: String) -> Self {
73        Self {
74            id: format!("intent:{}", uuid::Uuid::new_v4()),
75            issuer,
76            subject_agent,
77            intent_description,
78            permissions: Vec::new(),
79            constraints: HashMap::new(),
80            created_at: Utc::now(),
81            expires_at: Some(Utc::now() + chrono::Duration::hours(24)),
82            status: MandateStatus::Active,
83            metadata: HashMap::new(),
84        }
85    }
86
87    pub fn add_permission(&mut self, permission: Permission) {
88        self.permissions.push(permission);
89    }
90
91    pub fn add_constraint(&mut self, key: String, value: serde_json::Value) {
92        self.constraints.insert(key, value);
93    }
94
95    pub fn has_permission(&self, action: &str, resource: &str) -> bool {
96        self.permissions
97            .iter()
98            .any(|p| p.action == action && p.resource == resource)
99    }
100
101    pub fn with_expiration(mut self, expires_at: DateTime<Utc>) -> Self {
102        self.expires_at = Some(expires_at);
103        self
104    }
105
106    pub fn cancel(&mut self) {
107        self.status = MandateStatus::Cancelled;
108    }
109}
110
111impl Mandate for IntentMandate {
112    fn mandate_type(&self) -> MandateType {
113        MandateType::Intent
114    }
115
116    fn issuer(&self) -> &str {
117        &self.issuer
118    }
119
120    fn created_at(&self) -> DateTime<Utc> {
121        self.created_at
122    }
123
124    fn expires_at(&self) -> Option<DateTime<Utc>> {
125        self.expires_at
126    }
127
128    fn status(&self) -> MandateStatus {
129        self.status.clone()
130    }
131}
132
133/// Cart Mandate - Explicit purchase authorization with itemized cart
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct CartMandate {
136    pub id: String,
137    pub issuer: String,        // User DID
138    pub merchant: String,      // Merchant DID
139    pub items: Vec<CartItem>,
140    pub total_amount: u64,     // Amount in smallest currency unit (e.g., cents)
141    pub currency: String,
142    pub tax_amount: Option<u64>,
143    pub shipping_amount: Option<u64>,
144    pub discount_amount: Option<u64>,
145    pub created_at: DateTime<Utc>,
146    pub expires_at: Option<DateTime<Utc>>,
147    pub status: MandateStatus,
148    pub metadata: HashMap<String, String>,
149}
150
151/// Cart Item representation
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct CartItem {
154    pub id: String,
155    pub name: String,
156    pub description: Option<String>,
157    pub quantity: u32,
158    pub unit_price: u64,
159    pub total_price: u64,
160    pub metadata: HashMap<String, String>,
161}
162
163impl CartItem {
164    pub fn new(id: String, name: String, quantity: u32, unit_price: u64) -> Self {
165        Self {
166            id,
167            name,
168            description: None,
169            quantity,
170            unit_price,
171            total_price: unit_price * quantity as u64,
172            metadata: HashMap::new(),
173        }
174    }
175
176    pub fn with_description(mut self, description: String) -> Self {
177        self.description = Some(description);
178        self
179    }
180
181    pub fn with_metadata(mut self, key: String, value: String) -> Self {
182        self.metadata.insert(key, value);
183        self
184    }
185}
186
187impl CartMandate {
188    pub fn new(issuer: String, items: Vec<CartItem>, total_amount: u64, currency: String) -> Self {
189        Self {
190            id: format!("cart:{}", uuid::Uuid::new_v4()),
191            issuer,
192            merchant: String::new(),
193            items,
194            total_amount,
195            currency,
196            tax_amount: None,
197            shipping_amount: None,
198            discount_amount: None,
199            created_at: Utc::now(),
200            expires_at: Some(Utc::now() + chrono::Duration::hours(1)),
201            status: MandateStatus::Active,
202            metadata: HashMap::new(),
203        }
204    }
205
206    pub fn calculate_total(&self) -> u64 {
207        let items_total: u64 = self.items.iter().map(|item| item.total_price).sum();
208        let tax = self.tax_amount.unwrap_or(0);
209        let shipping = self.shipping_amount.unwrap_or(0);
210        let discount = self.discount_amount.unwrap_or(0);
211
212        items_total + tax + shipping - discount
213    }
214
215    pub fn verify_total(&self) -> bool {
216        self.calculate_total() == self.total_amount
217    }
218
219    pub fn with_merchant(mut self, merchant: String) -> Self {
220        self.merchant = merchant;
221        self
222    }
223
224    pub fn with_tax(mut self, tax_amount: u64) -> Self {
225        self.tax_amount = Some(tax_amount);
226        self
227    }
228
229    pub fn with_shipping(mut self, shipping_amount: u64) -> Self {
230        self.shipping_amount = Some(shipping_amount);
231        self
232    }
233
234    pub fn with_discount(mut self, discount_amount: u64) -> Self {
235        self.discount_amount = Some(discount_amount);
236        self
237    }
238
239    pub fn complete(&mut self) {
240        self.status = MandateStatus::Completed;
241    }
242
243    pub fn cancel(&mut self) {
244        self.status = MandateStatus::Cancelled;
245    }
246}
247
248impl Mandate for CartMandate {
249    fn mandate_type(&self) -> MandateType {
250        MandateType::Cart
251    }
252
253    fn issuer(&self) -> &str {
254        &self.issuer
255    }
256
257    fn created_at(&self) -> DateTime<Utc> {
258        self.created_at
259    }
260
261    fn expires_at(&self) -> Option<DateTime<Utc>> {
262        self.expires_at
263    }
264
265    fn status(&self) -> MandateStatus {
266        self.status.clone()
267    }
268}
269
270/// Payment Mandate - Payment network signaling for actual transaction
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct PaymentMandate {
273    pub id: String,
274    pub issuer: String,         // Payer DID
275    pub recipient: String,      // Payee DID
276    pub amount: u64,
277    pub currency: String,
278    pub payment_method: PaymentMethod,
279    pub payment_network: Option<String>,
280    pub reference: Option<String>,
281    pub cart_mandate_id: Option<String>,
282    pub created_at: DateTime<Utc>,
283    pub expires_at: Option<DateTime<Utc>>,
284    pub status: MandateStatus,
285    pub metadata: HashMap<String, String>,
286}
287
288/// Payment Method Types
289#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(rename_all = "snake_case")]
291pub enum PaymentMethod {
292    CreditCard { last_four: String },
293    BankTransfer { account_id: String },
294    Cryptocurrency { chain: String, token: String },
295    DigitalWallet { provider: String },
296    Other { method_type: String },
297}
298
299impl PaymentMandate {
300    pub fn new(
301        issuer: String,
302        recipient: String,
303        amount: u64,
304        currency: String,
305        payment_method_type: String,
306    ) -> Self {
307        Self {
308            id: format!("payment:{}", uuid::Uuid::new_v4()),
309            issuer,
310            recipient,
311            amount,
312            currency,
313            payment_method: PaymentMethod::Other {
314                method_type: payment_method_type,
315            },
316            payment_network: None,
317            reference: None,
318            cart_mandate_id: None,
319            created_at: Utc::now(),
320            expires_at: Some(Utc::now() + chrono::Duration::minutes(30)),
321            status: MandateStatus::Pending,
322            metadata: HashMap::new(),
323        }
324    }
325
326    pub fn with_payment_method(mut self, method: PaymentMethod) -> Self {
327        self.payment_method = method;
328        self
329    }
330
331    pub fn with_payment_network(mut self, network: String) -> Self {
332        self.payment_network = Some(network);
333        self
334    }
335
336    pub fn with_reference(mut self, reference: String) -> Self {
337        self.reference = Some(reference);
338        self
339    }
340
341    pub fn link_cart_mandate(mut self, cart_mandate_id: String) -> Self {
342        self.cart_mandate_id = Some(cart_mandate_id);
343        self
344    }
345
346    pub fn activate(&mut self) {
347        self.status = MandateStatus::Active;
348    }
349
350    pub fn complete(&mut self) {
351        self.status = MandateStatus::Completed;
352    }
353
354    pub fn cancel(&mut self) {
355        self.status = MandateStatus::Cancelled;
356    }
357}
358
359impl Mandate for PaymentMandate {
360    fn mandate_type(&self) -> MandateType {
361        MandateType::Payment
362    }
363
364    fn issuer(&self) -> &str {
365        &self.issuer
366    }
367
368    fn created_at(&self) -> DateTime<Utc> {
369        self.created_at
370    }
371
372    fn expires_at(&self) -> Option<DateTime<Utc>> {
373        self.expires_at
374    }
375
376    fn status(&self) -> MandateStatus {
377        self.status.clone()
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_intent_mandate_creation() {
387        let mandate = IntentMandate::new(
388            "did:example:user".to_string(),
389            "did:example:agent".to_string(),
390            "Purchase items on behalf of user".to_string(),
391        );
392
393        assert_eq!(mandate.mandate_type(), MandateType::Intent);
394        assert_eq!(mandate.status(), MandateStatus::Active);
395        assert!(mandate.is_valid());
396    }
397
398    #[test]
399    fn test_cart_mandate_total_calculation() {
400        let items = vec![
401            CartItem::new("item1".to_string(), "Product A".to_string(), 2, 1000),
402            CartItem::new("item2".to_string(), "Product B".to_string(), 1, 1500),
403        ];
404
405        let mut mandate = CartMandate::new(
406            "did:example:user".to_string(),
407            items,
408            3500,
409            "USD".to_string(),
410        );
411
412        assert!(mandate.verify_total());
413
414        mandate = mandate.with_tax(350);
415        assert_eq!(mandate.calculate_total(), 3850);
416    }
417
418    #[test]
419    fn test_payment_mandate_lifecycle() {
420        let mut mandate = PaymentMandate::new(
421            "did:example:payer".to_string(),
422            "did:example:payee".to_string(),
423            5000,
424            "USD".to_string(),
425            "credit_card".to_string(),
426        );
427
428        assert_eq!(mandate.status(), MandateStatus::Pending);
429
430        mandate.activate();
431        assert_eq!(mandate.status(), MandateStatus::Active);
432
433        mandate.complete();
434        assert_eq!(mandate.status(), MandateStatus::Completed);
435    }
436
437    #[test]
438    fn test_cart_item_calculation() {
439        let item = CartItem::new("item1".to_string(), "Test Item".to_string(), 3, 500);
440        assert_eq!(item.total_price, 1500);
441    }
442}