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::error::{Error, Result};
12use crate::message::agent::TapParticipant;
13use crate::message::tap_message_trait::{TapMessage as TapMessageTrait, TapMessageBody};
14use crate::message::{Agent, Party};
15use crate::TapMessage;
16
17/// Invoice reference that can be either a URL or an Invoice object
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(untagged)]
20pub enum InvoiceReference {
21    /// URL to an invoice
22    Url(String),
23    /// Structured invoice object
24    Object(Box<crate::message::Invoice>),
25}
26
27impl InvoiceReference {
28    /// Check if this is a URL reference
29    pub fn is_url(&self) -> bool {
30        matches!(self, InvoiceReference::Url(_))
31    }
32
33    /// Check if this is an object reference
34    pub fn is_object(&self) -> bool {
35        matches!(self, InvoiceReference::Object(_))
36    }
37
38    /// Get the URL if this is a URL reference
39    pub fn as_url(&self) -> Option<&str> {
40        match self {
41            InvoiceReference::Url(url) => Some(url),
42            _ => None,
43        }
44    }
45
46    /// Get the invoice object if this is an object reference
47    pub fn as_object(&self) -> Option<&crate::message::Invoice> {
48        match self {
49            InvoiceReference::Object(invoice) => Some(invoice.as_ref()),
50            _ => None,
51        }
52    }
53
54    /// Validate the invoice reference
55    pub fn validate(&self) -> Result<()> {
56        match self {
57            InvoiceReference::Url(url) => {
58                // Basic URL validation - just check it's not empty
59                if url.is_empty() {
60                    return Err(Error::Validation("Invoice URL cannot be empty".to_string()));
61                }
62                // Could add more URL validation here if needed
63                Ok(())
64            }
65            InvoiceReference::Object(invoice) => {
66                // Validate the invoice object
67                invoice.validate()
68            }
69        }
70    }
71}
72
73/// Payment message body (TAIP-14).
74///
75/// A Payment is a DIDComm message initiated by the merchant's agent and sent
76/// to the customer's agent to request a blockchain payment. It must include either
77/// an asset or a currency to denominate the payment, along with the amount and
78/// recipient information.
79#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
80#[tap(
81    message_type = "https://tap.rsvp/schema/1.0#Payment",
82    initiator,
83    authorizable,
84    transactable
85)]
86pub struct Payment {
87    /// Asset identifier (CAIP-19 format).
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub asset: Option<AssetId>,
90
91    /// Payment amount.
92    pub amount: String,
93
94    /// Currency code for fiat amounts (e.g., USD).
95    #[serde(rename = "currency", skip_serializing_if = "Option::is_none")]
96    pub currency_code: Option<String>,
97
98    /// Supported assets for this payment (when currency_code is specified)
99    #[serde(rename = "supportedAssets", skip_serializing_if = "Option::is_none")]
100    pub supported_assets: Option<Vec<AssetId>>,
101
102    /// Customer (payer) details.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    #[tap(participant)]
105    pub customer: Option<Party>,
106
107    /// Merchant (payee) details.
108    #[tap(participant)]
109    pub merchant: Party,
110
111    /// Transaction identifier (only available after creation).
112    #[serde(skip)]
113    #[tap(transaction_id)]
114    pub transaction_id: Option<String>,
115
116    /// Memo for the payment (optional).
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub memo: Option<String>,
119
120    /// Expiration time in ISO 8601 format (optional).
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub expiry: Option<String>,
123
124    /// Invoice details (optional) per TAIP-16 - can be either a URL or an Invoice object
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub invoice: Option<InvoiceReference>,
127
128    /// Other agents involved in the payment.
129    #[serde(default)]
130    #[tap(participant_list)]
131    pub agents: Vec<Agent>,
132
133    /// Connection ID for linking to Connect messages
134    #[serde(skip_serializing_if = "Option::is_none")]
135    #[tap(connection_id)]
136    pub connection_id: Option<String>,
137
138    /// Additional metadata (optional).
139    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
140    pub metadata: HashMap<String, serde_json::Value>,
141}
142
143/// Builder for Payment objects.
144#[derive(Default)]
145pub struct PaymentBuilder {
146    asset: Option<AssetId>,
147    amount: Option<String>,
148    currency_code: Option<String>,
149    supported_assets: Option<Vec<AssetId>>,
150    customer: Option<Party>,
151    merchant: Option<Party>,
152    transaction_id: Option<String>,
153    memo: Option<String>,
154    expiry: Option<String>,
155    invoice: Option<InvoiceReference>,
156    agents: Vec<Agent>,
157    metadata: HashMap<String, serde_json::Value>,
158}
159
160impl PaymentBuilder {
161    /// Set the asset for this payment
162    pub fn asset(mut self, asset: AssetId) -> Self {
163        self.asset = Some(asset);
164        self
165    }
166
167    /// Set the amount for this payment
168    pub fn amount(mut self, amount: String) -> Self {
169        self.amount = Some(amount);
170        self
171    }
172
173    /// Set the currency code for this payment
174    pub fn currency_code(mut self, currency_code: String) -> Self {
175        self.currency_code = Some(currency_code);
176        self
177    }
178
179    /// Set the supported assets for this payment
180    pub fn supported_assets(mut self, supported_assets: Vec<AssetId>) -> Self {
181        self.supported_assets = Some(supported_assets);
182        self
183    }
184
185    /// Add a supported asset for this payment
186    pub fn add_supported_asset(mut self, asset: AssetId) -> Self {
187        if let Some(assets) = &mut self.supported_assets {
188            assets.push(asset);
189        } else {
190            self.supported_assets = Some(vec![asset]);
191        }
192        self
193    }
194
195    /// Set the customer for this payment
196    pub fn customer(mut self, customer: Party) -> Self {
197        self.customer = Some(customer);
198        self
199    }
200
201    /// Set the merchant for this payment
202    pub fn merchant(mut self, merchant: Party) -> Self {
203        self.merchant = Some(merchant);
204        self
205    }
206
207    /// Set the transaction ID for this payment
208    pub fn transaction_id(mut self, transaction_id: String) -> Self {
209        self.transaction_id = Some(transaction_id);
210        self
211    }
212
213    /// Set the memo for this payment
214    pub fn memo(mut self, memo: String) -> Self {
215        self.memo = Some(memo);
216        self
217    }
218
219    /// Set the expiration time for this payment
220    pub fn expiry(mut self, expiry: String) -> Self {
221        self.expiry = Some(expiry);
222        self
223    }
224
225    /// Set the invoice for this payment with an Invoice object
226    pub fn invoice(mut self, invoice: crate::message::Invoice) -> Self {
227        self.invoice = Some(InvoiceReference::Object(Box::new(invoice)));
228        self
229    }
230
231    /// Set the invoice URL for this payment
232    pub fn invoice_url(mut self, url: String) -> Self {
233        self.invoice = Some(InvoiceReference::Url(url));
234        self
235    }
236
237    /// Add an agent to this payment
238    pub fn add_agent(mut self, agent: Agent) -> Self {
239        self.agents.push(agent);
240        self
241    }
242
243    /// Set all agents for this payment
244    pub fn agents(mut self, agents: Vec<Agent>) -> Self {
245        self.agents = agents;
246        self
247    }
248
249    /// Add a metadata field
250    pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
251        self.metadata.insert(key, value);
252        self
253    }
254
255    /// Set all metadata for this payment
256    pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
257        self.metadata = metadata;
258        self
259    }
260
261    /// Build the Payment object
262    ///
263    /// # Panics
264    ///
265    /// Panics if required fields are not set
266    pub fn build(self) -> Payment {
267        // Ensure either asset or currency_code is provided
268        if self.asset.is_none() && self.currency_code.is_none() {
269            panic!("Either asset or currency_code is required");
270        }
271
272        Payment {
273            asset: self.asset,
274            amount: self.amount.expect("Amount is required"),
275            currency_code: self.currency_code,
276            supported_assets: self.supported_assets,
277            customer: self.customer,
278            merchant: self.merchant.expect("Merchant is required"),
279            transaction_id: self.transaction_id,
280            memo: self.memo,
281            expiry: self.expiry,
282            invoice: self.invoice,
283            agents: self.agents,
284            connection_id: None,
285            metadata: self.metadata,
286        }
287    }
288}
289
290impl Payment {
291    /// Creates a new Payment with an asset
292    pub fn with_asset(asset: AssetId, amount: String, merchant: Party, agents: Vec<Agent>) -> Self {
293        Self {
294            asset: Some(asset),
295            amount,
296            currency_code: None,
297            supported_assets: None,
298            customer: None,
299            merchant,
300            transaction_id: None,
301            memo: None,
302            expiry: None,
303            invoice: None,
304            agents,
305            connection_id: None,
306            metadata: HashMap::new(),
307        }
308    }
309
310    /// Creates a new Payment with a currency
311    pub fn with_currency(
312        currency_code: String,
313        amount: String,
314        merchant: Party,
315        agents: Vec<Agent>,
316    ) -> Self {
317        Self {
318            asset: None,
319            amount,
320            currency_code: Some(currency_code),
321            supported_assets: None,
322            customer: None,
323            merchant,
324            transaction_id: None,
325            memo: None,
326            expiry: None,
327            invoice: None,
328            agents,
329            connection_id: None,
330            metadata: HashMap::new(),
331        }
332    }
333
334    /// Creates a new Payment with a currency and supported assets
335    pub fn with_currency_and_assets(
336        currency_code: String,
337        amount: String,
338        supported_assets: Vec<AssetId>,
339        merchant: Party,
340        agents: Vec<Agent>,
341    ) -> Self {
342        Self {
343            asset: None,
344            amount,
345            currency_code: Some(currency_code),
346            supported_assets: Some(supported_assets),
347            customer: None,
348            merchant,
349            transaction_id: None,
350            memo: None,
351            expiry: None,
352            invoice: None,
353            agents,
354            connection_id: None,
355            metadata: HashMap::new(),
356        }
357    }
358
359    /// Custom validation for Payment messages
360    pub fn validate(&self) -> Result<()> {
361        // Validate either asset or currency_code is provided
362        if self.asset.is_none() && self.currency_code.is_none() {
363            return Err(Error::Validation(
364                "Either asset or currency_code must be provided".to_string(),
365            ));
366        }
367
368        // Validate asset ID if provided
369        if let Some(asset) = &self.asset {
370            if asset.namespace().is_empty() || asset.reference().is_empty() {
371                return Err(Error::Validation("Asset ID is invalid".to_string()));
372            }
373        }
374
375        // Validate amount
376        if self.amount.is_empty() {
377            return Err(Error::Validation("Amount is required".to_string()));
378        }
379
380        // Validate amount is a positive number
381        match self.amount.parse::<f64>() {
382            Ok(amount) if amount <= 0.0 => {
383                return Err(Error::Validation("Amount must be positive".to_string()));
384            }
385            Err(_) => {
386                return Err(Error::Validation(
387                    "Amount must be a valid number".to_string(),
388                ));
389            }
390            _ => {}
391        }
392
393        // Validate merchant
394        if self.merchant.id().is_empty() {
395            return Err(Error::Validation("Merchant ID is required".to_string()));
396        }
397
398        // Validate supported_assets if provided
399        if let Some(supported_assets) = &self.supported_assets {
400            if supported_assets.is_empty() {
401                return Err(Error::Validation(
402                    "Supported assets list cannot be empty".to_string(),
403                ));
404            }
405
406            // Validate each asset ID in the supported_assets list
407            for (i, asset) in supported_assets.iter().enumerate() {
408                if asset.namespace().is_empty() || asset.reference().is_empty() {
409                    return Err(Error::Validation(format!(
410                        "Supported asset at index {} is invalid",
411                        i
412                    )));
413                }
414            }
415        }
416
417        // If invoice is provided, validate it
418        if let Some(invoice) = &self.invoice {
419            // Call the validate method on the invoice
420            if let Err(e) = invoice.validate() {
421                return Err(Error::Validation(format!(
422                    "Invoice validation failed: {}",
423                    e
424                )));
425            }
426        }
427
428        Ok(())
429    }
430}