Skip to main content

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