Skip to main content

tap_msg/message/
connection.rs

1//! Connection types for TAP messages.
2//!
3//! This module defines the structure of connection messages and related types
4//! used in the Transaction Authorization Protocol (TAP).
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::error::{Error, Result};
10use crate::message::agent::TapParticipant;
11use crate::message::tap_message_trait::{TapMessage as TapMessageTrait, TapMessageBody};
12use crate::message::{Agent, Party};
13use crate::TapMessage;
14
15/// Agent structure specific to Connect messages (legacy).
16/// In TAIP-15 v2, standard Agent objects from TAIP-5 are used instead.
17/// This type is kept for backward compatibility with older messages.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ConnectAgent {
20    /// DID of the agent.
21    #[serde(rename = "@id")]
22    pub id: String,
23
24    /// Name of the agent (optional).
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub name: Option<String>,
27
28    /// Type of the agent (optional).
29    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
30    pub agent_type: Option<String>,
31
32    /// Service URL for the agent (optional).
33    #[serde(rename = "serviceUrl", skip_serializing_if = "Option::is_none")]
34    pub service_url: Option<String>,
35
36    /// Additional metadata.
37    #[serde(flatten)]
38    pub metadata: HashMap<String, serde_json::Value>,
39}
40
41impl TapParticipant for ConnectAgent {
42    fn id(&self) -> &str {
43        &self.id
44    }
45}
46
47impl ConnectAgent {
48    /// Create a new ConnectAgent with just an ID.
49    pub fn new(id: &str) -> Self {
50        Self {
51            id: id.to_string(),
52            name: None,
53            agent_type: None,
54            service_url: None,
55            metadata: HashMap::new(),
56        }
57    }
58
59    /// Convert to a regular Agent by adding a for_party.
60    pub fn to_agent(&self, for_party: &str) -> Agent {
61        let mut agent = Agent::new_without_role(&self.id, for_party);
62
63        // Copy metadata fields
64        if let Some(name) = &self.name {
65            agent
66                .metadata
67                .insert("name".to_string(), serde_json::Value::String(name.clone()));
68        }
69        if let Some(agent_type) = &self.agent_type {
70            agent.metadata.insert(
71                "type".to_string(),
72                serde_json::Value::String(agent_type.clone()),
73            );
74        }
75        if let Some(service_url) = &self.service_url {
76            agent.metadata.insert(
77                "serviceUrl".to_string(),
78                serde_json::Value::String(service_url.clone()),
79            );
80        }
81
82        // Copy any additional metadata
83        for (k, v) in &self.metadata {
84            agent.metadata.insert(k.clone(), v.clone());
85        }
86
87        agent
88    }
89}
90
91/// Transaction limits for connection constraints (TAIP-15).
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct TransactionLimits {
94    /// Maximum amount per transaction.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub per_transaction: Option<String>,
97
98    /// Maximum daily amount.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub per_day: Option<String>,
101
102    /// Maximum weekly amount.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub per_week: Option<String>,
105
106    /// Maximum monthly amount.
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub per_month: Option<String>,
109
110    /// Maximum yearly amount.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub per_year: Option<String>,
113
114    /// Currency for the limits (ISO 4217). Required when limits are specified.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub currency: Option<String>,
117}
118
119/// Connection constraints for the Connect message (TAIP-15).
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ConnectionConstraints {
122    /// Allowed TAIP-13 purpose codes.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub purposes: Option<Vec<String>>,
125
126    /// Allowed TAIP-13 category purpose codes.
127    #[serde(rename = "categoryPurposes", skip_serializing_if = "Option::is_none")]
128    pub category_purposes: Option<Vec<String>>,
129
130    /// Transaction limits.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub limits: Option<TransactionLimits>,
133
134    /// Allowed beneficiary parties (TAIP-6 Party objects).
135    #[serde(
136        rename = "allowedBeneficiaries",
137        skip_serializing_if = "Option::is_none"
138    )]
139    pub allowed_beneficiaries: Option<Vec<Party>>,
140
141    /// Allowed settlement addresses (CAIP-10 format).
142    #[serde(
143        rename = "allowedSettlementAddresses",
144        skip_serializing_if = "Option::is_none"
145    )]
146    pub allowed_settlement_addresses: Option<Vec<String>>,
147
148    /// Allowed asset identifiers (CAIP-19 format).
149    #[serde(rename = "allowedAssets", skip_serializing_if = "Option::is_none")]
150    pub allowed_assets: Option<Vec<String>>,
151}
152
153/// Connect message body (TAIP-15).
154#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
155#[tap(
156    message_type = "https://tap.rsvp/schema/1.0#Connect",
157    initiator,
158    authorizable
159)]
160pub struct Connect {
161    /// Transaction ID (only available after creation).
162    #[serde(skip)]
163    #[tap(transaction_id)]
164    pub transaction_id: Option<String>,
165
166    /// Requester party (TAIP-15 v2, required for new messages).
167    #[serde(skip_serializing_if = "Option::is_none")]
168    #[tap(participant)]
169    pub requester: Option<Party>,
170
171    /// Principal party this connection is for.
172    #[serde(skip_serializing_if = "Option::is_none")]
173    #[tap(participant)]
174    pub principal: Option<Party>,
175
176    /// Agents involved in the connection (TAIP-5 agents).
177    #[serde(default)]
178    #[tap(participant_list)]
179    pub agents: Vec<Agent>,
180
181    /// Connection constraints (required per TAIP-15).
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub constraints: Option<ConnectionConstraints>,
184
185    /// URL pointing to terms of service or agreement.
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub agreement: Option<String>,
188
189    /// Expiration time in ISO 8601 format.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub expiry: Option<String>,
192
193    // --- Legacy fields for backward compatibility ---
194    /// Agent DID (legacy, use agents array instead).
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub agent_id: Option<String>,
197
198    /// Legacy agent object (use agents array instead).
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub agent: Option<ConnectAgent>,
201
202    /// Legacy entity this connection is for (use principal instead).
203    #[serde(rename = "for", skip_serializing_if = "Option::is_none", default)]
204    pub for_: Option<String>,
205
206    /// Legacy role field (use agents with roles instead).
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub role: Option<String>,
209}
210
211impl Connect {
212    /// Create a new Connect message with requester, principal, and agents (TAIP-15 v2).
213    pub fn new_v2(
214        requester: Party,
215        principal: Party,
216        agents: Vec<Agent>,
217        constraints: ConnectionConstraints,
218    ) -> Self {
219        Self {
220            transaction_id: None,
221            requester: Some(requester),
222            principal: Some(principal),
223            agents,
224            constraints: Some(constraints),
225            agreement: None,
226            expiry: None,
227            agent_id: None,
228            agent: None,
229            for_: None,
230            role: None,
231        }
232    }
233
234    /// Create a new Connect message (legacy backward compatible).
235    pub fn new(transaction_id: &str, agent_id: &str, for_id: &str, role: Option<&str>) -> Self {
236        Self {
237            transaction_id: Some(transaction_id.to_string()),
238            requester: None,
239            principal: None,
240            agents: vec![],
241            constraints: None,
242            agreement: None,
243            expiry: None,
244            agent_id: Some(agent_id.to_string()),
245            agent: None,
246            for_: Some(for_id.to_string()),
247            role: role.map(|s| s.to_string()),
248        }
249    }
250
251    /// Create a new Connect message with Agent and Principal.
252    pub fn new_with_agent_and_principal(
253        transaction_id: &str,
254        agent: ConnectAgent,
255        principal: Party,
256    ) -> Self {
257        Self {
258            transaction_id: Some(transaction_id.to_string()),
259            requester: None,
260            principal: Some(principal),
261            agents: vec![],
262            constraints: None,
263            agreement: None,
264            expiry: None,
265            agent_id: None,
266            agent: Some(agent),
267            for_: None,
268            role: None,
269        }
270    }
271
272    /// Add constraints to the Connect message.
273    pub fn with_constraints(mut self, constraints: ConnectionConstraints) -> Self {
274        self.constraints = Some(constraints);
275        self
276    }
277
278    /// Set the agreement URL.
279    pub fn with_agreement(mut self, agreement: String) -> Self {
280        self.agreement = Some(agreement);
281        self
282    }
283
284    /// Set the expiry timestamp.
285    pub fn with_expiry(mut self, expiry: String) -> Self {
286        self.expiry = Some(expiry);
287        self
288    }
289}
290
291impl Connect {
292    /// Custom validation for Connect messages
293    pub fn validate_connect(&self) -> Result<()> {
294        // New TAIP-15 v2 validation: if requester is present, use new validation
295        if self.requester.is_some() {
296            if self.principal.is_none() {
297                return Err(Error::Validation("principal is required".to_string()));
298            }
299            if self.agents.is_empty() {
300                return Err(Error::Validation(
301                    "at least one agent is required".to_string(),
302                ));
303            }
304            if self.constraints.is_none() {
305                return Err(Error::Validation(
306                    "Connection request must include constraints".to_string(),
307                ));
308            }
309            return Ok(());
310        }
311
312        // Legacy validation
313        if self.agent_id.is_none() && self.agent.is_none() {
314            return Err(Error::Validation(
315                "either agent_id or agent is required".to_string(),
316            ));
317        }
318
319        let for_empty = self.for_.as_ref().is_none_or(|s| s.is_empty());
320        if for_empty && self.principal.is_none() {
321            return Err(Error::Validation(
322                "either for or principal is required".to_string(),
323            ));
324        }
325
326        if self.constraints.is_none() {
327            return Err(Error::Validation(
328                "Connection request must include constraints".to_string(),
329            ));
330        }
331
332        Ok(())
333    }
334
335    /// Validation method that will be called by TapMessageBody trait
336    pub fn validate(&self) -> Result<()> {
337        self.validate_connect()
338    }
339}
340
341/// Out of Band invitation for TAP connections.
342#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
343#[tap(message_type = "https://tap.rsvp/schema/1.0#OutOfBand")]
344pub struct OutOfBand {
345    /// The goal code for this invitation.
346    pub goal_code: String,
347
348    /// The goal for this invitation.
349    pub goal: String,
350
351    /// The public DID or endpoint URL for the inviter.
352    pub service: String,
353
354    /// Accept media types.
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub accept: Option<Vec<String>>,
357
358    /// Handshake protocols supported.
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub handshake_protocols: Option<Vec<String>>,
361
362    /// Additional metadata.
363    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
364    pub metadata: HashMap<String, serde_json::Value>,
365}
366
367impl OutOfBand {
368    /// Create a new OutOfBand message.
369    pub fn new(goal_code: String, goal: String, service: String) -> Self {
370        Self {
371            goal_code,
372            goal,
373            service,
374            accept: None,
375            handshake_protocols: None,
376            metadata: HashMap::new(),
377        }
378    }
379}
380
381/// Authorization Required message body (TAIP-4, TAIP-15).
382///
383/// Indicates that authorization is required to proceed with a transaction or connection.
384#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
385#[tap(message_type = "https://tap.rsvp/schema/1.0#AuthorizationRequired")]
386pub struct AuthorizationRequired {
387    /// Authorization URL where the user can authorize the transaction.
388    #[serde(rename = "authorizationUrl")]
389    pub authorization_url: String,
390
391    /// ISO 8601 timestamp when the authorization URL expires (REQUIRED per TAIP-4).
392    pub expires: String,
393
394    /// Optional party type (e.g., "customer", "principal", "originator") that is required to open the URL.
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub from: Option<String>,
397
398    /// Additional metadata.
399    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
400    pub metadata: HashMap<String, serde_json::Value>,
401}
402
403impl AuthorizationRequired {
404    /// Create a new AuthorizationRequired message.
405    pub fn new(authorization_url: String, expires: String) -> Self {
406        Self {
407            authorization_url,
408            expires,
409            from: None,
410            metadata: HashMap::new(),
411        }
412    }
413
414    /// Create a new AuthorizationRequired message with a specified party type.
415    pub fn new_with_from(authorization_url: String, expires: String, from: String) -> Self {
416        Self {
417            authorization_url,
418            expires,
419            from: Some(from),
420            metadata: HashMap::new(),
421        }
422    }
423
424    /// Set the party type that is required to open the URL.
425    pub fn with_from(mut self, from: String) -> Self {
426        self.from = Some(from);
427        self
428    }
429
430    /// Add metadata to the message.
431    pub fn add_metadata(mut self, key: &str, value: serde_json::Value) -> Self {
432        self.metadata.insert(key.to_string(), value);
433        self
434    }
435}
436
437impl OutOfBand {
438    /// Custom validation for OutOfBand messages
439    pub fn validate_out_of_band(&self) -> Result<()> {
440        if self.goal_code.is_empty() {
441            return Err(Error::Validation("Goal code is required".to_string()));
442        }
443
444        if self.service.is_empty() {
445            return Err(Error::Validation("Service is required".to_string()));
446        }
447
448        Ok(())
449    }
450
451    /// Validation method that will be called by TapMessageBody trait
452    pub fn validate(&self) -> Result<()> {
453        self.validate_out_of_band()
454    }
455}
456
457impl AuthorizationRequired {
458    /// Custom validation for AuthorizationRequired messages
459    pub fn validate_authorization_required(&self) -> Result<()> {
460        if self.authorization_url.is_empty() {
461            return Err(Error::Validation(
462                "Authorization URL is required".to_string(),
463            ));
464        }
465
466        if self.expires.is_empty() {
467            return Err(Error::Validation(
468                "Expires timestamp is required".to_string(),
469            ));
470        }
471
472        if !self.expires.contains('T') || !self.expires.contains(':') {
473            return Err(Error::Validation(
474                "Invalid expiry date format. Expected ISO8601/RFC3339 format".to_string(),
475            ));
476        }
477
478        if let Some(ref from) = self.from {
479            let valid_from_values = ["customer", "principal", "originator", "beneficiary"];
480            if !valid_from_values.contains(&from.as_str()) {
481                return Err(Error::Validation(
482                    format!("Invalid 'from' value '{}'. Expected one of: customer, principal, originator, beneficiary", from),
483                ));
484            }
485        }
486
487        Ok(())
488    }
489
490    /// Validation method that will be called by TapMessageBody trait
491    pub fn validate(&self) -> Result<()> {
492        self.validate_authorization_required()
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499    use serde_json;
500
501    #[test]
502    fn test_connect_v2_creation() {
503        let requester = Party::new("did:example:b2b-service");
504        let principal = Party::new("did:example:customer");
505        let agent = Agent::new_without_role("did:example:b2b-service", "did:example:b2b-service");
506        let constraints = ConnectionConstraints {
507            purposes: Some(vec!["BEXP".to_string()]),
508            category_purposes: None,
509            limits: Some(TransactionLimits {
510                per_transaction: Some("10000.00".to_string()),
511                per_day: Some("50000.00".to_string()),
512                per_week: None,
513                per_month: None,
514                per_year: None,
515                currency: Some("USD".to_string()),
516            }),
517            allowed_beneficiaries: None,
518            allowed_settlement_addresses: None,
519            allowed_assets: None,
520        };
521
522        let connect = Connect::new_v2(requester, principal, vec![agent], constraints)
523            .with_agreement("https://example.com/terms".to_string())
524            .with_expiry("2024-03-22T15:00:00Z".to_string());
525
526        assert!(connect.requester.is_some());
527        assert!(connect.principal.is_some());
528        assert_eq!(connect.agents.len(), 1);
529        assert!(connect.constraints.is_some());
530        assert_eq!(
531            connect.agreement,
532            Some("https://example.com/terms".to_string())
533        );
534        assert_eq!(connect.expiry, Some("2024-03-22T15:00:00Z".to_string()));
535        assert!(connect.validate().is_ok());
536    }
537
538    #[test]
539    fn test_connect_v2_serialization() {
540        let requester = Party::new("did:example:b2b-service");
541        let principal = Party::new("did:example:customer");
542        let agent = Agent::new_without_role("did:example:b2b-service", "did:example:b2b-service");
543        let constraints = ConnectionConstraints {
544            purposes: Some(vec!["BEXP".to_string()]),
545            category_purposes: None,
546            limits: Some(TransactionLimits {
547                per_transaction: Some("10000.00".to_string()),
548                per_day: Some("50000.00".to_string()),
549                per_week: None,
550                per_month: None,
551                per_year: None,
552                currency: Some("USD".to_string()),
553            }),
554            allowed_beneficiaries: Some(vec![Party::new("did:example:vendor-1")]),
555            allowed_settlement_addresses: Some(vec![
556                "eip155:1:0x742d35Cc6e4dfE2eDFaD2C0b91A8b0780EDAEb58".to_string(),
557            ]),
558            allowed_assets: Some(vec!["eip155:1/slip44:60".to_string()]),
559        };
560
561        let connect = Connect::new_v2(requester, principal, vec![agent], constraints);
562        let json = serde_json::to_value(&connect).unwrap();
563
564        assert!(json.get("requester").is_some());
565        assert!(json.get("principal").is_some());
566        assert!(json.get("agents").is_some());
567        assert!(json.get("constraints").is_some());
568
569        let constraints = json.get("constraints").unwrap();
570        assert!(constraints.get("allowedBeneficiaries").is_some());
571        assert!(constraints.get("allowedSettlementAddresses").is_some());
572        assert!(constraints.get("allowedAssets").is_some());
573
574        let limits = constraints.get("limits").unwrap();
575        assert_eq!(limits.get("per_day").unwrap(), "50000.00");
576    }
577
578    #[test]
579    fn test_connect_v2_validation_missing_principal() {
580        let connect = Connect {
581            transaction_id: None,
582            requester: Some(Party::new("did:example:service")),
583            principal: None,
584            agents: vec![Agent::new_without_role(
585                "did:example:service",
586                "did:example:service",
587            )],
588            constraints: Some(ConnectionConstraints {
589                purposes: Some(vec!["BEXP".to_string()]),
590                category_purposes: None,
591                limits: None,
592                allowed_beneficiaries: None,
593                allowed_settlement_addresses: None,
594                allowed_assets: None,
595            }),
596            agreement: None,
597            expiry: None,
598            agent_id: None,
599            agent: None,
600            for_: None,
601            role: None,
602        };
603
604        let result = connect.validate();
605        assert!(result.is_err());
606        assert!(result
607            .unwrap_err()
608            .to_string()
609            .contains("principal is required"));
610    }
611
612    #[test]
613    fn test_connect_v2_validation_no_agents() {
614        let connect = Connect {
615            transaction_id: None,
616            requester: Some(Party::new("did:example:service")),
617            principal: Some(Party::new("did:example:customer")),
618            agents: vec![],
619            constraints: Some(ConnectionConstraints {
620                purposes: Some(vec!["BEXP".to_string()]),
621                category_purposes: None,
622                limits: None,
623                allowed_beneficiaries: None,
624                allowed_settlement_addresses: None,
625                allowed_assets: None,
626            }),
627            agreement: None,
628            expiry: None,
629            agent_id: None,
630            agent: None,
631            for_: None,
632            role: None,
633        };
634
635        let result = connect.validate();
636        assert!(result.is_err());
637        assert!(result
638            .unwrap_err()
639            .to_string()
640            .contains("at least one agent"));
641    }
642
643    #[test]
644    fn test_authorization_required_creation() {
645        let auth_req = AuthorizationRequired::new(
646            "https://vasp.com/authorize?request=abc123".to_string(),
647            "2024-12-31T23:59:59Z".to_string(),
648        );
649
650        assert_eq!(
651            auth_req.authorization_url,
652            "https://vasp.com/authorize?request=abc123"
653        );
654        assert_eq!(auth_req.expires, "2024-12-31T23:59:59Z");
655        assert!(auth_req.from.is_none());
656        assert!(auth_req.metadata.is_empty());
657    }
658
659    #[test]
660    fn test_authorization_required_with_from() {
661        let auth_req = AuthorizationRequired::new_with_from(
662            "https://vasp.com/authorize".to_string(),
663            "2024-12-31T23:59:59Z".to_string(),
664            "customer".to_string(),
665        );
666
667        assert_eq!(auth_req.from, Some("customer".to_string()));
668    }
669
670    #[test]
671    fn test_authorization_required_builder_pattern() {
672        let auth_req = AuthorizationRequired::new(
673            "https://vasp.com/authorize".to_string(),
674            "2024-12-31T23:59:59Z".to_string(),
675        )
676        .with_from("principal".to_string())
677        .add_metadata("custom_field", serde_json::json!("value"));
678
679        assert_eq!(auth_req.from, Some("principal".to_string()));
680        assert_eq!(
681            auth_req.metadata.get("custom_field"),
682            Some(&serde_json::json!("value"))
683        );
684    }
685
686    #[test]
687    fn test_authorization_required_serialization() {
688        let auth_req = AuthorizationRequired::new_with_from(
689            "https://vasp.com/authorize?request=abc123".to_string(),
690            "2024-12-31T23:59:59Z".to_string(),
691            "customer".to_string(),
692        );
693
694        let json = serde_json::to_value(&auth_req).unwrap();
695
696        assert_eq!(
697            json["authorizationUrl"],
698            "https://vasp.com/authorize?request=abc123"
699        );
700        assert_eq!(json["expires"], "2024-12-31T23:59:59Z");
701        assert_eq!(json["from"], "customer");
702
703        let deserialized: AuthorizationRequired = serde_json::from_value(json).unwrap();
704        assert_eq!(deserialized.authorization_url, auth_req.authorization_url);
705        assert_eq!(deserialized.expires, auth_req.expires);
706        assert_eq!(deserialized.from, auth_req.from);
707    }
708
709    #[test]
710    fn test_authorization_required_validation_success() {
711        let auth_req = AuthorizationRequired::new(
712            "https://vasp.com/authorize".to_string(),
713            "2024-12-31T23:59:59Z".to_string(),
714        );
715
716        assert!(auth_req.validate().is_ok());
717    }
718
719    #[test]
720    fn test_authorization_required_validation_with_valid_from() {
721        let valid_from_values = ["customer", "principal", "originator", "beneficiary"];
722
723        for from_value in &valid_from_values {
724            let auth_req = AuthorizationRequired::new_with_from(
725                "https://vasp.com/authorize".to_string(),
726                "2024-12-31T23:59:59Z".to_string(),
727                from_value.to_string(),
728            );
729
730            assert!(
731                auth_req.validate().is_ok(),
732                "Validation failed for from value: {}",
733                from_value
734            );
735        }
736    }
737
738    #[test]
739    fn test_authorization_required_validation_empty_url() {
740        let auth_req =
741            AuthorizationRequired::new("".to_string(), "2024-12-31T23:59:59Z".to_string());
742
743        let result = auth_req.validate();
744        assert!(result.is_err());
745        assert!(result
746            .unwrap_err()
747            .to_string()
748            .contains("Authorization URL is required"));
749    }
750
751    #[test]
752    fn test_authorization_required_validation_empty_expires() {
753        let auth_req = AuthorizationRequired {
754            authorization_url: "https://vasp.com/authorize".to_string(),
755            expires: "".to_string(),
756            from: None,
757            metadata: HashMap::new(),
758        };
759
760        let result = auth_req.validate();
761        assert!(result.is_err());
762        assert!(result
763            .unwrap_err()
764            .to_string()
765            .contains("Expires timestamp is required"));
766    }
767
768    #[test]
769    fn test_authorization_required_validation_invalid_expires_format() {
770        let auth_req = AuthorizationRequired::new(
771            "https://vasp.com/authorize".to_string(),
772            "2024-12-31".to_string(),
773        );
774
775        let result = auth_req.validate();
776        assert!(result.is_err());
777        assert!(result
778            .unwrap_err()
779            .to_string()
780            .contains("Invalid expiry date format"));
781    }
782
783    #[test]
784    fn test_authorization_required_validation_invalid_from() {
785        let auth_req = AuthorizationRequired::new_with_from(
786            "https://vasp.com/authorize".to_string(),
787            "2024-12-31T23:59:59Z".to_string(),
788            "invalid_party".to_string(),
789        );
790
791        let result = auth_req.validate();
792        assert!(result.is_err());
793        assert!(result
794            .unwrap_err()
795            .to_string()
796            .contains("Invalid 'from' value"));
797    }
798
799    #[test]
800    fn test_authorization_required_json_compliance_with_taip4() {
801        let auth_req = AuthorizationRequired::new_with_from(
802            "https://beneficiary.vasp/authorize?request=abc123".to_string(),
803            "2024-01-01T12:00:00Z".to_string(),
804            "customer".to_string(),
805        );
806
807        let json = serde_json::to_value(&auth_req).unwrap();
808
809        assert!(json.get("authorizationUrl").is_some());
810        assert!(json.get("expires").is_some());
811        assert!(json.get("from").is_some());
812
813        assert!(json.get("authorization_url").is_none());
814        assert!(json.get("url").is_none());
815    }
816}