tap_msg/message/
payment.rs

1//! Payment types for TAP messages.
2//!
3//! This module defines the structure of payment messages and related types
4//! used in the Transaction Authorization Protocol (TAP).
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use tap_caip::AssetId;
10
11use crate::didcomm::PlainMessage;
12use crate::error::{Error, Result};
13use crate::impl_tap_message;
14use crate::message::tap_message_trait::{Authorizable, Connectable, TapMessageBody};
15use crate::message::{Authorize, Participant, Policy, RemoveAgent, ReplaceAgent, UpdatePolicies};
16use chrono::Utc;
17
18/// Payment message body (TAIP-14).
19///
20/// A Payment is a DIDComm message initiated by the merchant's agent and sent
21/// to the customer's agent to request a blockchain payment. It must include either
22/// an asset or a currency to denominate the payment, along with the amount and
23/// recipient information.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Payment {
26    /// Asset identifier (CAIP-19 format).
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub asset: Option<AssetId>,
29
30    /// Payment amount.
31    pub amount: String,
32
33    /// Currency code for fiat amounts (e.g., USD).
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub currency_code: Option<String>,
36
37    /// Supported assets for this payment (when currency_code is specified)
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub supported_assets: Option<Vec<AssetId>>,
40
41    /// Customer (payer) details.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub customer: Option<Participant>,
44
45    /// Merchant (payee) details.
46    pub merchant: Participant,
47
48    /// Transaction identifier.
49    pub transaction_id: String,
50
51    /// Memo for the payment (optional).
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub memo: Option<String>,
54
55    /// Expiration time in ISO 8601 format (optional).
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub expiry: Option<String>,
58
59    /// Invoice details (optional) per TAIP-16
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub invoice: Option<crate::message::Invoice>,
62
63    /// Other agents involved in the payment.
64    #[serde(default)]
65    pub agents: Vec<Participant>,
66
67    /// Additional metadata (optional).
68    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
69    pub metadata: HashMap<String, serde_json::Value>,
70}
71
72/// Builder for Payment objects.
73#[derive(Default)]
74pub struct PaymentBuilder {
75    asset: Option<AssetId>,
76    amount: Option<String>,
77    currency_code: Option<String>,
78    supported_assets: Option<Vec<AssetId>>,
79    customer: Option<Participant>,
80    merchant: Option<Participant>,
81    transaction_id: Option<String>,
82    memo: Option<String>,
83    expiry: Option<String>,
84    invoice: Option<crate::message::Invoice>,
85    agents: Vec<Participant>,
86    metadata: HashMap<String, serde_json::Value>,
87}
88
89impl PaymentBuilder {
90    /// Set the asset for this payment
91    pub fn asset(mut self, asset: AssetId) -> Self {
92        self.asset = Some(asset);
93        self
94    }
95
96    /// Set the amount for this payment
97    pub fn amount(mut self, amount: String) -> Self {
98        self.amount = Some(amount);
99        self
100    }
101
102    /// Set the currency code for this payment
103    pub fn currency_code(mut self, currency_code: String) -> Self {
104        self.currency_code = Some(currency_code);
105        self
106    }
107
108    /// Set the supported assets for this payment
109    pub fn supported_assets(mut self, supported_assets: Vec<AssetId>) -> Self {
110        self.supported_assets = Some(supported_assets);
111        self
112    }
113
114    /// Add a supported asset for this payment
115    pub fn add_supported_asset(mut self, asset: AssetId) -> Self {
116        if let Some(assets) = &mut self.supported_assets {
117            assets.push(asset);
118        } else {
119            self.supported_assets = Some(vec![asset]);
120        }
121        self
122    }
123
124    /// Set the customer for this payment
125    pub fn customer(mut self, customer: Participant) -> Self {
126        self.customer = Some(customer);
127        self
128    }
129
130    /// Set the merchant for this payment
131    pub fn merchant(mut self, merchant: Participant) -> Self {
132        self.merchant = Some(merchant);
133        self
134    }
135
136    /// Set the transaction ID for this payment
137    pub fn transaction_id(mut self, transaction_id: String) -> Self {
138        self.transaction_id = Some(transaction_id);
139        self
140    }
141
142    /// Set the memo for this payment
143    pub fn memo(mut self, memo: String) -> Self {
144        self.memo = Some(memo);
145        self
146    }
147
148    /// Set the expiration time for this payment
149    pub fn expiry(mut self, expiry: String) -> Self {
150        self.expiry = Some(expiry);
151        self
152    }
153
154    /// Set the invoice for this payment
155    pub fn invoice(mut self, invoice: crate::message::Invoice) -> Self {
156        self.invoice = Some(invoice);
157        self
158    }
159
160    /// Add an agent to this payment
161    pub fn add_agent(mut self, agent: Participant) -> Self {
162        self.agents.push(agent);
163        self
164    }
165
166    /// Set all agents for this payment
167    pub fn agents(mut self, agents: Vec<Participant>) -> Self {
168        self.agents = agents;
169        self
170    }
171
172    /// Add a metadata field
173    pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
174        self.metadata.insert(key, value);
175        self
176    }
177
178    /// Set all metadata for this payment
179    pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
180        self.metadata = metadata;
181        self
182    }
183
184    /// Build the Payment object
185    ///
186    /// # Panics
187    ///
188    /// Panics if required fields are not set
189    pub fn build(self) -> Payment {
190        // Ensure either asset or currency_code is provided
191        if self.asset.is_none() && self.currency_code.is_none() {
192            panic!("Either asset or currency_code is required");
193        }
194
195        Payment {
196            asset: self.asset,
197            amount: self.amount.expect("Amount is required"),
198            currency_code: self.currency_code,
199            supported_assets: self.supported_assets,
200            customer: self.customer,
201            merchant: self.merchant.expect("Merchant is required"),
202            transaction_id: self
203                .transaction_id
204                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
205            memo: self.memo,
206            expiry: self.expiry,
207            invoice: self.invoice,
208            agents: self.agents,
209            metadata: self.metadata,
210        }
211    }
212}
213
214impl Connectable for Payment {
215    fn with_connection(&mut self, connect_id: &str) -> &mut Self {
216        // Store the connect_id in metadata
217        self.metadata.insert(
218            "connect_id".to_string(),
219            serde_json::Value::String(connect_id.to_string()),
220        );
221        self
222    }
223
224    fn has_connection(&self) -> bool {
225        self.metadata.contains_key("connect_id")
226    }
227
228    fn connection_id(&self) -> Option<&str> {
229        self.metadata.get("connect_id").and_then(|v| v.as_str())
230    }
231}
232
233impl TapMessageBody for Payment {
234    fn message_type() -> &'static str {
235        "https://tap.rsvp/schema/1.0#payment"
236    }
237
238    fn validate(&self) -> Result<()> {
239        // Validate either asset or currency_code is provided
240        if self.asset.is_none() && self.currency_code.is_none() {
241            return Err(Error::Validation(
242                "Either asset or currency_code must be provided".to_string(),
243            ));
244        }
245
246        // Validate asset ID if provided
247        if let Some(asset) = &self.asset {
248            if asset.namespace().is_empty() || asset.reference().is_empty() {
249                return Err(Error::Validation("Asset ID is invalid".to_string()));
250            }
251        }
252
253        // Validate amount
254        if self.amount.is_empty() {
255            return Err(Error::Validation("Amount is required".to_string()));
256        }
257
258        // Validate amount is a positive number
259        match self.amount.parse::<f64>() {
260            Ok(amount) if amount <= 0.0 => {
261                return Err(Error::Validation("Amount must be positive".to_string()));
262            }
263            Err(_) => {
264                return Err(Error::Validation(
265                    "Amount must be a valid number".to_string(),
266                ));
267            }
268            _ => {}
269        }
270
271        // Validate merchant
272        if self.merchant.id.is_empty() {
273            return Err(Error::Validation("Merchant ID is required".to_string()));
274        }
275
276        // Validate supported_assets if provided
277        if let Some(supported_assets) = &self.supported_assets {
278            if supported_assets.is_empty() {
279                return Err(Error::Validation(
280                    "Supported assets list cannot be empty".to_string(),
281                ));
282            }
283
284            // Validate each asset ID in the supported_assets list
285            for (i, asset) in supported_assets.iter().enumerate() {
286                if asset.namespace().is_empty() || asset.reference().is_empty() {
287                    return Err(Error::Validation(format!(
288                        "Supported asset at index {} is invalid",
289                        i
290                    )));
291                }
292            }
293        }
294
295        // If invoice is provided, validate it
296        if let Some(invoice) = &self.invoice {
297            // Call the validate method on the invoice
298            if let Err(e) = invoice.validate() {
299                return Err(Error::Validation(format!(
300                    "Invoice validation failed: {}",
301                    e
302                )));
303            }
304        }
305
306        Ok(())
307    }
308
309    fn to_didcomm(&self, from_did: &str) -> Result<PlainMessage> {
310        // Serialize the Payment to a JSON value
311        let mut body_json =
312            serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
313
314        // Ensure the @type field is correctly set in the body
315        if let Some(body_obj) = body_json.as_object_mut() {
316            body_obj.insert(
317                "@type".to_string(),
318                serde_json::Value::String(Self::message_type().to_string()),
319            );
320        }
321
322        // Extract agent DIDs directly from the message
323        let mut agent_dids = Vec::new();
324
325        // Add merchant DID
326        agent_dids.push(self.merchant.id.clone());
327
328        // Add customer DID if present
329        if let Some(customer) = &self.customer {
330            agent_dids.push(customer.id.clone());
331        }
332
333        // Add DIDs from agents array
334        for agent in &self.agents {
335            agent_dids.push(agent.id.clone());
336        }
337
338        // Remove duplicates
339        agent_dids.sort();
340        agent_dids.dedup();
341
342        // Remove the sender from the recipients list to avoid sending to self
343        agent_dids.retain(|did| did != from_did);
344
345        let now = Utc::now().timestamp() as u64;
346
347        // Get the connection ID if this message is connected to a previous message
348        let pthid = self
349            .connection_id()
350            .map(|connect_id| connect_id.to_string());
351
352        // Set expires_time based on the expiry field if provided
353        let expires_time = self.expiry.as_ref().and_then(|expiry| {
354            // Try to parse ISO 8601 date to epoch seconds
355            if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(expiry) {
356                Some(dt.timestamp() as u64)
357            } else {
358                None
359            }
360        });
361
362        // Create a new Message with required fields
363        let message = PlainMessage {
364            id: uuid::Uuid::new_v4().to_string(),
365            typ: "application/didcomm-plain+json".to_string(),
366            type_: Self::message_type().to_string(),
367            body: body_json,
368            from: from_did.to_string(),
369            to: agent_dids,
370            thid: Some(self.transaction_id.clone()),
371            pthid,
372            created_time: Some(now),
373            expires_time,
374            extra_headers: std::collections::HashMap::new(),
375            from_prior: None,
376            attachments: None,
377        };
378
379        Ok(message)
380    }
381
382    fn from_didcomm(message: &PlainMessage) -> Result<Self> {
383        // Validate message type
384        if message.type_ != Self::message_type() {
385            return Err(Error::InvalidMessageType(format!(
386                "Expected {} but got {}",
387                Self::message_type(),
388                message.type_
389            )));
390        }
391
392        // Extract fields from message body
393        let payment: Payment = serde_json::from_value(message.body.clone())
394            .map_err(|e| Error::SerializationError(e.to_string()))?;
395
396        Ok(payment)
397    }
398}
399
400impl Authorizable for Payment {
401    fn authorize(&self, note: Option<String>) -> Authorize {
402        Authorize {
403            transaction_id: self.transaction_id.clone(),
404            note,
405        }
406    }
407
408    fn update_policies(&self, transaction_id: String, policies: Vec<Policy>) -> UpdatePolicies {
409        UpdatePolicies {
410            transaction_id,
411            policies,
412        }
413    }
414
415    fn replace_agent(
416        &self,
417        transaction_id: String,
418        original_agent: String,
419        replacement: Participant,
420    ) -> ReplaceAgent {
421        ReplaceAgent {
422            transaction_id,
423            original: original_agent,
424            replacement,
425        }
426    }
427
428    fn remove_agent(&self, transaction_id: String, agent: String) -> RemoveAgent {
429        RemoveAgent {
430            transaction_id,
431            agent,
432        }
433    }
434}
435
436impl Payment {
437    /// Creates a new Payment with an asset
438    pub fn with_asset(
439        asset: AssetId,
440        amount: String,
441        merchant: Participant,
442        agents: Vec<Participant>,
443    ) -> Self {
444        Self {
445            asset: Some(asset),
446            amount,
447            currency_code: None,
448            supported_assets: None,
449            customer: None,
450            merchant,
451            transaction_id: uuid::Uuid::new_v4().to_string(),
452            memo: None,
453            expiry: None,
454            invoice: None,
455            agents,
456            metadata: HashMap::new(),
457        }
458    }
459
460    /// Creates a new Payment with a currency
461    pub fn with_currency(
462        currency_code: String,
463        amount: String,
464        merchant: Participant,
465        agents: Vec<Participant>,
466    ) -> Self {
467        Self {
468            asset: None,
469            amount,
470            currency_code: Some(currency_code),
471            supported_assets: None,
472            customer: None,
473            merchant,
474            transaction_id: uuid::Uuid::new_v4().to_string(),
475            memo: None,
476            expiry: None,
477            invoice: None,
478            agents,
479            metadata: HashMap::new(),
480        }
481    }
482
483    /// Creates a new Payment with a currency and supported assets
484    pub fn with_currency_and_assets(
485        currency_code: String,
486        amount: String,
487        supported_assets: Vec<AssetId>,
488        merchant: Participant,
489        agents: Vec<Participant>,
490    ) -> Self {
491        Self {
492            asset: None,
493            amount,
494            currency_code: Some(currency_code),
495            supported_assets: Some(supported_assets),
496            customer: None,
497            merchant,
498            transaction_id: uuid::Uuid::new_v4().to_string(),
499            memo: None,
500            expiry: None,
501            invoice: None,
502            agents,
503            metadata: HashMap::new(),
504        }
505    }
506}
507
508impl_tap_message!(Payment);