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