tap_msg/message/
settle.rs

1//! Settle message type for the Transaction Authorization Protocol.
2//!
3//! This module defines the Settle message type, which is used
4//! for settling transactions in the TAP protocol.
5
6use crate::didcomm::PlainMessage;
7use crate::error::{Error, Result};
8use crate::impl_tap_message;
9use crate::message::tap_message_trait::TapMessageBody;
10use chrono::Utc;
11use serde::{Deserialize, Serialize};
12
13/// Settle message body (TAIP-4).
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Settle {
16    /// ID of the transaction being settled.
17    pub transaction_id: String,
18
19    /// Settlement ID (CAIP-220 identifier of the underlying settlement transaction).
20    pub settlement_id: String,
21
22    /// Optional amount settled. If specified, must be less than or equal to the original amount.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub amount: Option<String>,
25}
26
27impl Settle {
28    /// Create a new Settle message
29    pub fn new(transaction_id: &str, settlement_id: &str) -> Self {
30        Self {
31            transaction_id: transaction_id.to_string(),
32            settlement_id: settlement_id.to_string(),
33            amount: None,
34        }
35    }
36
37    /// Create a new Settle message with an amount
38    pub fn with_amount(transaction_id: &str, settlement_id: &str, amount: &str) -> Self {
39        Self {
40            transaction_id: transaction_id.to_string(),
41            settlement_id: settlement_id.to_string(),
42            amount: Some(amount.to_string()),
43        }
44    }
45}
46
47impl TapMessageBody for Settle {
48    fn message_type() -> &'static str {
49        "https://tap.rsvp/schema/1.0#settle"
50    }
51
52    fn validate(&self) -> Result<()> {
53        if self.transaction_id.is_empty() {
54            return Err(Error::Validation(
55                "Transaction ID is required in Settle".to_string(),
56            ));
57        }
58
59        if self.settlement_id.is_empty() {
60            return Err(Error::Validation(
61                "Settlement ID is required in Settle".to_string(),
62            ));
63        }
64
65        if let Some(amount) = &self.amount {
66            if amount.is_empty() {
67                return Err(Error::Validation(
68                    "Amount must be a valid number".to_string(),
69                ));
70            }
71
72            // Validate amount is a positive number if provided
73            match amount.parse::<f64>() {
74                Ok(amount) if amount <= 0.0 => {
75                    return Err(Error::Validation("Amount must be positive".to_string()));
76                }
77                Err(_) => {
78                    return Err(Error::Validation(
79                        "Amount must be a valid number".to_string(),
80                    ));
81                }
82                _ => {}
83            }
84        }
85
86        Ok(())
87    }
88
89    fn to_didcomm(&self, from_did: &str) -> Result<PlainMessage> {
90        // Create a JSON representation of self with explicit type field
91        let mut body_json =
92            serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
93
94        // Ensure the @type field is correctly set in the body
95        if let Some(body_obj) = body_json.as_object_mut() {
96            // Add or update the @type field with the message type
97            body_obj.insert(
98                "@type".to_string(),
99                serde_json::Value::String(Self::message_type().to_string()),
100            );
101        }
102
103        // Create a new message with a random ID
104        let id = uuid::Uuid::new_v4().to_string();
105        let created_time = Utc::now().timestamp() as u64;
106
107        // Create the message
108        let message = PlainMessage {
109            id,
110            typ: "application/didcomm-plain+json".to_string(),
111            type_: Self::message_type().to_string(),
112            from: from_did.to_string(),
113            to: Vec::new(), // Empty recipients, will be determined by the framework later
114            thid: Some(self.transaction_id.clone()),
115            pthid: None,
116            created_time: Some(created_time),
117            expires_time: None,
118            extra_headers: std::collections::HashMap::new(),
119            from_prior: None,
120            body: body_json,
121            attachments: None,
122        };
123
124        Ok(message)
125    }
126}
127
128impl_tap_message!(Settle);