Skip to main content

tap_msg/message/
party.rs

1//! Party types for TAP messages (TAIP-6).
2//!
3//! This module defines the structure of party information used in TAP messages.
4//! Parties are the real-world entities involved with a transaction - legal or natural persons.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::message::agent::TapParticipant;
10use crate::utils::NameHashable;
11
12/// Party in a transaction (TAIP-6).
13///
14/// Parties are identified using an IRI as the @id attribute in a JSON-LD object.
15/// They represent real-world entities (legal or natural persons) that are parties to a transaction.
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17pub struct Party {
18    /// IRI of the party (DID, email, phone number, etc).
19    #[serde(rename = "@id")]
20    pub id: String,
21
22    /// Additional JSON-LD metadata for the party.
23    /// This allows for extensible metadata like country codes, LEI codes, MCC codes, etc.
24    /// Example: {"https://schema.org/addressCountry": "de", "lei": "..."}
25    #[serde(flatten)]
26    pub metadata: HashMap<String, serde_json::Value>,
27}
28
29impl TapParticipant for Party {
30    fn id(&self) -> &str {
31        &self.id
32    }
33}
34
35impl Party {
36    /// Create a new party with the given IRI.
37    pub fn new(id: &str) -> Self {
38        Self {
39            id: id.to_string(),
40            metadata: HashMap::new(),
41        }
42    }
43
44    /// Create a new party with metadata.
45    pub fn with_metadata(id: &str, metadata: HashMap<String, serde_json::Value>) -> Self {
46        Self {
47            id: id.to_string(),
48            metadata,
49        }
50    }
51
52    /// Add a metadata field to the party.
53    pub fn add_metadata(&mut self, key: String, value: serde_json::Value) {
54        self.metadata.insert(key, value);
55    }
56
57    /// Add metadata using the builder pattern.
58    pub fn with_metadata_field(mut self, key: String, value: serde_json::Value) -> Self {
59        self.metadata.insert(key, value);
60        self
61    }
62
63    /// Add country code metadata.
64    pub fn with_country(mut self, country_code: &str) -> Self {
65        self.metadata.insert(
66            "https://schema.org/addressCountry".to_string(),
67            serde_json::Value::String(country_code.to_string()),
68        );
69        self
70    }
71
72    /// Add LEI code metadata.
73    pub fn with_lei(mut self, lei_code: &str) -> Self {
74        self.metadata.insert(
75            "https://schema.org/leiCode".to_string(),
76            serde_json::Value::String(lei_code.to_string()),
77        );
78        self
79    }
80
81    /// Add merchant category code (MCC) metadata.
82    pub fn with_mcc(mut self, mcc: &str) -> Self {
83        self.metadata.insert(
84            "mcc".to_string(),
85            serde_json::Value::String(mcc.to_string()),
86        );
87        self
88    }
89
90    /// Get a metadata value by key.
91    pub fn get_metadata(&self, key: &str) -> Option<&serde_json::Value> {
92        self.metadata.get(key)
93    }
94
95    /// Get country code if present.
96    pub fn country(&self) -> Option<String> {
97        self.get_metadata("https://schema.org/addressCountry")
98            .and_then(|v| v.as_str())
99            .map(|s| s.to_string())
100    }
101
102    /// Get LEI code if present.
103    pub fn lei_code(&self) -> Option<String> {
104        self.get_metadata("https://schema.org/leiCode")
105            .and_then(|v| v.as_str())
106            .map(|s| s.to_string())
107    }
108
109    /// Get MCC code if present.
110    pub fn mcc(&self) -> Option<String> {
111        self.get_metadata("mcc")
112            .and_then(|v| v.as_str())
113            .map(|s| s.to_string())
114    }
115
116    /// Add name hash metadata according to TAIP-12.
117    pub fn with_name_hash(mut self, name: &str) -> Self {
118        let hash = Self::hash_name(name);
119        self.metadata
120            .insert("nameHash".to_string(), serde_json::Value::String(hash));
121        self
122    }
123
124    /// Get name hash if present.
125    pub fn name_hash(&self) -> Option<String> {
126        self.get_metadata("nameHash")
127            .and_then(|v| v.as_str())
128            .map(|s| s.to_string())
129    }
130
131    /// Set name hash directly.
132    pub fn set_name_hash(&mut self, hash: String) {
133        self.metadata
134            .insert("nameHash".to_string(), serde_json::Value::String(hash));
135    }
136
137    // Schema.org Organization field accessors and builders
138
139    /// Add a name field (schema.org/Organization or schema.org/Person).
140    pub fn with_name(mut self, name: &str) -> Self {
141        self.metadata.insert(
142            "name".to_string(),
143            serde_json::Value::String(name.to_string()),
144        );
145        self
146    }
147
148    /// Get the name field if present.
149    pub fn name(&self) -> Option<&str> {
150        self.metadata.get("name").and_then(|v| v.as_str())
151    }
152
153    /// Add a URL field (schema.org/Organization).
154    pub fn with_url(mut self, url: &str) -> Self {
155        self.metadata.insert(
156            "url".to_string(),
157            serde_json::Value::String(url.to_string()),
158        );
159        self
160    }
161
162    /// Get the URL field if present.
163    pub fn url(&self) -> Option<&str> {
164        self.metadata.get("url").and_then(|v| v.as_str())
165    }
166
167    /// Add a logo field (schema.org/Organization).
168    pub fn with_logo(mut self, logo: &str) -> Self {
169        self.metadata.insert(
170            "logo".to_string(),
171            serde_json::Value::String(logo.to_string()),
172        );
173        self
174    }
175
176    /// Get the logo field if present.
177    pub fn logo(&self) -> Option<&str> {
178        self.metadata.get("logo").and_then(|v| v.as_str())
179    }
180
181    /// Add a description field (schema.org/Organization).
182    pub fn with_description(mut self, description: &str) -> Self {
183        self.metadata.insert(
184            "description".to_string(),
185            serde_json::Value::String(description.to_string()),
186        );
187        self
188    }
189
190    /// Get the description field if present.
191    pub fn description(&self) -> Option<&str> {
192        self.metadata.get("description").and_then(|v| v.as_str())
193    }
194
195    /// Add an email field (schema.org/Organization or schema.org/Person).
196    pub fn with_email(mut self, email: &str) -> Self {
197        self.metadata.insert(
198            "email".to_string(),
199            serde_json::Value::String(email.to_string()),
200        );
201        self
202    }
203
204    /// Get the email field if present.
205    pub fn email(&self) -> Option<&str> {
206        self.metadata.get("email").and_then(|v| v.as_str())
207    }
208
209    /// Add a telephone field (schema.org/Organization or schema.org/Person).
210    pub fn with_telephone(mut self, telephone: &str) -> Self {
211        self.metadata.insert(
212            "telephone".to_string(),
213            serde_json::Value::String(telephone.to_string()),
214        );
215        self
216    }
217
218    /// Get the telephone field if present.
219    pub fn telephone(&self) -> Option<&str> {
220        self.metadata.get("telephone").and_then(|v| v.as_str())
221    }
222}
223
224// Implement NameHashable for Party
225impl NameHashable for Party {}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use serde_json;
231
232    #[test]
233    fn test_party_creation() {
234        let party = Party::new("did:example:alice");
235        assert_eq!(party.id, "did:example:alice");
236        assert!(party.metadata.is_empty());
237    }
238
239    #[test]
240    fn test_party_with_metadata() {
241        let mut metadata = HashMap::new();
242        metadata.insert(
243            "name".to_string(),
244            serde_json::Value::String("Alice".to_string()),
245        );
246
247        let party = Party::with_metadata("did:example:alice", metadata);
248        assert_eq!(party.id, "did:example:alice");
249        assert_eq!(
250            party.get_metadata("name").unwrap().as_str().unwrap(),
251            "Alice"
252        );
253    }
254
255    #[test]
256    fn test_party_with_country() {
257        let party = Party::new("did:example:alice").with_country("de");
258        assert_eq!(party.country().unwrap(), "de");
259    }
260
261    #[test]
262    fn test_party_with_lei() {
263        let party = Party::new("did:web:example.com").with_lei("LEI123456789");
264        assert_eq!(party.lei_code().unwrap(), "LEI123456789");
265    }
266
267    #[test]
268    fn test_party_with_mcc() {
269        let party = Party::new("did:web:merchant.com").with_mcc("5812");
270        assert_eq!(party.mcc().unwrap(), "5812");
271    }
272
273    #[test]
274    fn test_party_serialization() {
275        let party = Party::new("did:example:alice")
276            .with_country("de")
277            .with_lei("LEI123");
278
279        let json = serde_json::to_string(&party).unwrap();
280        let deserialized: Party = serde_json::from_str(&json).unwrap();
281
282        assert_eq!(party, deserialized);
283        assert_eq!(deserialized.country().unwrap(), "de");
284        assert_eq!(deserialized.lei_code().unwrap(), "LEI123");
285    }
286
287    #[test]
288    fn test_party_json_ld_format() {
289        let party = Party::new("did:example:alice").with_country("de");
290        let json = serde_json::to_value(&party).unwrap();
291
292        assert_eq!(json["@id"], "did:example:alice");
293        assert_eq!(json["https://schema.org/addressCountry"], "de");
294    }
295
296    #[test]
297    fn test_party_with_name_hash() {
298        let party = Party::new("did:example:alice").with_name_hash("Alice Lee");
299        assert_eq!(
300            party.name_hash().unwrap(),
301            "b117f44426c9670da91b563db728cd0bc8bafa7d1a6bb5e764d1aad2ca25032e"
302        );
303    }
304
305    #[test]
306    fn test_party_name_hash_serialization() {
307        let party = Party::new("did:example:alice")
308            .with_name_hash("Alice Lee")
309            .with_country("US");
310
311        let json = serde_json::to_value(&party).unwrap();
312        assert_eq!(json["@id"], "did:example:alice");
313        assert_eq!(
314            json["nameHash"],
315            "b117f44426c9670da91b563db728cd0bc8bafa7d1a6bb5e764d1aad2ca25032e"
316        );
317        assert_eq!(json["https://schema.org/addressCountry"], "US");
318    }
319
320    // Schema.org Organization field tests
321
322    #[test]
323    fn test_party_with_name_field() {
324        let party = Party::new("did:example:alice").with_name("Alice Corporation");
325
326        assert_eq!(party.name(), Some("Alice Corporation"));
327
328        // Test serialization
329        let json = serde_json::to_value(&party).unwrap();
330        assert_eq!(json["name"], "Alice Corporation");
331
332        // Test deserialization
333        let deserialized: Party = serde_json::from_value(json).unwrap();
334        assert_eq!(deserialized.name(), Some("Alice Corporation"));
335    }
336
337    #[test]
338    fn test_party_with_url_field() {
339        let party = Party::new("did:example:alice").with_url("https://alice.example.com");
340
341        assert_eq!(party.url(), Some("https://alice.example.com"));
342
343        let json = serde_json::to_value(&party).unwrap();
344        assert_eq!(json["url"], "https://alice.example.com");
345    }
346
347    #[test]
348    fn test_party_with_logo_field() {
349        let party = Party::new("did:example:alice").with_logo("https://alice.example.com/logo.png");
350
351        assert_eq!(party.logo(), Some("https://alice.example.com/logo.png"));
352
353        let json = serde_json::to_value(&party).unwrap();
354        assert_eq!(json["logo"], "https://alice.example.com/logo.png");
355    }
356
357    #[test]
358    fn test_party_with_description_field() {
359        let party =
360            Party::new("did:example:alice").with_description("A trusted financial institution");
361
362        assert_eq!(party.description(), Some("A trusted financial institution"));
363
364        let json = serde_json::to_value(&party).unwrap();
365        assert_eq!(json["description"], "A trusted financial institution");
366    }
367
368    #[test]
369    fn test_party_with_email_field() {
370        let party = Party::new("did:example:alice").with_email("contact@alice.example.com");
371
372        assert_eq!(party.email(), Some("contact@alice.example.com"));
373
374        let json = serde_json::to_value(&party).unwrap();
375        assert_eq!(json["email"], "contact@alice.example.com");
376    }
377
378    #[test]
379    fn test_party_with_telephone_field() {
380        let party = Party::new("did:example:alice").with_telephone("+1-555-0200");
381
382        assert_eq!(party.telephone(), Some("+1-555-0200"));
383
384        let json = serde_json::to_value(&party).unwrap();
385        assert_eq!(json["telephone"], "+1-555-0200");
386    }
387
388    #[test]
389    fn test_party_with_multiple_organization_fields() {
390        let party = Party::new("did:example:alice")
391            .with_name("Alice Corporation")
392            .with_url("https://alice.example.com")
393            .with_logo("https://alice.example.com/logo.png")
394            .with_description("A trusted financial institution")
395            .with_email("contact@alice.example.com")
396            .with_telephone("+1-555-0200")
397            .with_country("US")
398            .with_lei("123456789012345678");
399
400        assert_eq!(party.name(), Some("Alice Corporation"));
401        assert_eq!(party.url(), Some("https://alice.example.com"));
402        assert_eq!(party.logo(), Some("https://alice.example.com/logo.png"));
403        assert_eq!(party.description(), Some("A trusted financial institution"));
404        assert_eq!(party.email(), Some("contact@alice.example.com"));
405        assert_eq!(party.telephone(), Some("+1-555-0200"));
406        assert_eq!(party.country(), Some("US".to_string()));
407        assert_eq!(party.lei_code(), Some("123456789012345678".to_string()));
408
409        // Test JSON serialization includes all fields
410        let json = serde_json::to_value(&party).unwrap();
411        assert_eq!(json["@id"], "did:example:alice");
412        assert_eq!(json["name"], "Alice Corporation");
413        assert_eq!(json["url"], "https://alice.example.com");
414        assert_eq!(json["logo"], "https://alice.example.com/logo.png");
415        assert_eq!(json["description"], "A trusted financial institution");
416        assert_eq!(json["email"], "contact@alice.example.com");
417        assert_eq!(json["telephone"], "+1-555-0200");
418        assert_eq!(json["https://schema.org/addressCountry"], "US");
419        assert_eq!(json["https://schema.org/leiCode"], "123456789012345678");
420
421        // Test deserialization preserves all fields
422        let deserialized: Party = serde_json::from_value(json).unwrap();
423        assert_eq!(deserialized.name(), Some("Alice Corporation"));
424        assert_eq!(deserialized.url(), Some("https://alice.example.com"));
425        assert_eq!(deserialized.country(), Some("US".to_string()));
426    }
427
428    #[test]
429    fn test_party_json_ld_compliance_with_organization_fields() {
430        let party = Party::new("did:example:alice")
431            .with_name("Alice Corporation")
432            .with_metadata_field(
433                "ivms101".to_string(),
434                serde_json::json!({
435                    "naturalPerson": {
436                        "name": {
437                            "primaryIdentifier": "Smith",
438                            "secondaryIdentifier": "Alice"
439                        }
440                    }
441                }),
442            );
443
444        let json = serde_json::to_value(&party).unwrap();
445
446        // Verify JSON-LD structure
447        assert_eq!(json["@id"], "did:example:alice");
448        assert_eq!(json["name"], "Alice Corporation");
449        assert!(json["ivms101"]["naturalPerson"]["name"]["primaryIdentifier"].is_string());
450
451        // Fields should be at root level, not nested under metadata
452        assert!(json.get("metadata").is_none());
453    }
454
455    #[test]
456    fn test_party_with_ivms101_and_schema_org_fields() {
457        // Test that IVMS101 data can coexist with schema.org fields
458        let party = Party::new("did:example:alice")
459            .with_name("Alice Corporation")
460            .with_country("US")
461            .with_metadata_field(
462                "ivms101".to_string(),
463                serde_json::json!({
464                    "legalPerson": {
465                        "name": {
466                            "nameIdentifier": [{
467                                "legalPersonName": "Alice Corporation",
468                                "legalPersonNameIdentifierType": "LEGL"
469                            }]
470                        },
471                        "nationalIdentification": {
472                            "nationalIdentifier": "123456789",
473                            "nationalIdentifierType": "LEIX"
474                        }
475                    }
476                }),
477            );
478
479        assert_eq!(party.name(), Some("Alice Corporation"));
480        assert_eq!(party.country(), Some("US".to_string()));
481
482        let json = serde_json::to_value(&party).unwrap();
483        assert_eq!(json["name"], "Alice Corporation");
484        assert_eq!(json["https://schema.org/addressCountry"], "US");
485        assert!(json["ivms101"]["legalPerson"]["name"]["nameIdentifier"].is_array());
486    }
487}