tap-msg 0.7.0

Core message processing library for the Transaction Authorization Protocol
Documentation
//! Settle message type for the Transaction Authorization Protocol.
//!
//! This module defines the Settle message type, which is used
//! for settling transactions in the TAP protocol.

use crate::error::{Error, Result};
use crate::TapMessage;
use serde::{Deserialize, Serialize};

/// Settle message body (TAIP-4).
#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
#[tap(message_type = "https://tap.rsvp/schema/1.0#Settle", custom_validation)]
pub struct Settle {
    /// ID of the transaction being settled.
    #[tap(thread_id)]
    pub transaction_id: String,

    /// Settlement ID (CAIP-220 identifier of the underlying settlement transaction).
    #[serde(
        rename = "settlementId",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub settlement_id: Option<String>,

    /// Optional amount settled. If specified, must be less than or equal to the original amount.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub amount: Option<String>,
}

impl Settle {
    /// Create a new Settle message
    pub fn new(transaction_id: &str, settlement_id: &str) -> Self {
        Self {
            transaction_id: transaction_id.to_string(),
            settlement_id: Some(settlement_id.to_string()),
            amount: None,
        }
    }

    /// Create a new Settle message with an amount
    pub fn with_amount(transaction_id: &str, settlement_id: &str, amount: &str) -> Self {
        Self {
            transaction_id: transaction_id.to_string(),
            settlement_id: Some(settlement_id.to_string()),
            amount: Some(amount.to_string()),
        }
    }

    /// Create a minimal Settle message (for testing/special cases)
    pub fn minimal(transaction_id: &str) -> Self {
        Self {
            transaction_id: transaction_id.to_string(),
            settlement_id: None,
            amount: None,
        }
    }
}

impl Settle {
    /// Custom validation for Settle messages
    pub fn validate_settle(&self) -> Result<()> {
        if self.transaction_id.is_empty() {
            return Err(Error::Validation(
                "Transaction ID is required in Settle".to_string(),
            ));
        }

        // Note: settlement_id is now optional to support minimal test cases
        // In production use, settlement_id should typically be provided
        if let Some(ref settlement_id) = self.settlement_id {
            if settlement_id.is_empty() {
                return Err(Error::Validation(
                    "Settlement ID cannot be empty when provided".to_string(),
                ));
            }

            // Validate CAIP-220 format: namespace:chain_id:tx_type/tx_hash
            // Example: eip155:1:tx/0x3edb98c24d46d148eb926c714f4fbaa117c47b0c0821f38bfce9763604457c33

            // First check if it starts with 0x (common mistake - raw hex without CAIP format)
            if settlement_id.starts_with("0x") && !settlement_id.contains(':') {
                return Err(Error::Validation(
                    "Invalid format for 'settlementId', CAIP-220 block address expected"
                        .to_string(),
                ));
            }

            let parts: Vec<&str> = settlement_id.split(':').collect();
            if parts.len() < 3 {
                return Err(Error::Validation(
                    "Invalid format for 'settlementId', CAIP-220 block address expected"
                        .to_string(),
                ));
            }

            // Check if the third part contains tx_type/tx_hash
            if let Some(tx_part) = parts.get(2) {
                if !tx_part.contains('/') {
                    return Err(Error::Validation(
                        "Invalid format for 'settlementId', CAIP-220 block address expected"
                            .to_string(),
                    ));
                }
            }
        }

        if let Some(amount) = &self.amount {
            if amount.is_empty() {
                return Err(Error::Validation(
                    "Amount must be a valid number".to_string(),
                ));
            }

            // Validate amount is a finite positive number if provided
            match amount.parse::<f64>() {
                Ok(amount) if !amount.is_finite() => {
                    return Err(Error::Validation(
                        "Amount must be a finite number".to_string(),
                    ));
                }
                Ok(amount) if amount <= 0.0 => {
                    return Err(Error::Validation("Amount must be positive".to_string()));
                }
                Err(_) => {
                    return Err(Error::Validation(
                        "Amount must be a valid number".to_string(),
                    ));
                }
                _ => {}
            }
        }

        Ok(())
    }
}