swift_mt_message/fields/
field56d.rs

1use crate::{SwiftField, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 56D: Intermediary Institution (Option D)
5///
6/// ## Overview
7/// Field 56D identifies an intermediary institution in SWIFT payment messages using name and
8/// address information. This field provides an alternative identification method when BIC codes
9/// or account numbers are not available or suitable. It allows for detailed specification of
10/// the intermediary institution through structured name and address data, supporting various
11/// international payment routing scenarios where traditional identifiers may not be sufficient.
12///
13/// ## Format Specification
14/// **Format**: `4*35x`
15/// - **4*35x**: Up to 4 lines of 35 characters each
16/// - **Line structure**: Free-form text for name and address
17/// - **Character set**: SWIFT character set (A-Z, 0-9, and limited special characters)
18/// - **Line separation**: Each line on separate row
19///
20/// ## Structure
21/// ```text
22/// INTERMEDIARY BANK NAME LIMITED
23/// 123 FINANCIAL DISTRICT AVENUE
24/// NEW YORK NY 10005
25/// UNITED STATES OF AMERICA
26/// │                              │
27/// └──────────────────────────────┘
28///        Up to 35 characters per line
29///        Maximum 4 lines
30/// ```
31///
32/// ## Field Components
33/// - **Institution Name**: Official name of intermediary institution
34/// - **Street Address**: Physical address details
35/// - **City/State**: Location information
36/// - **Country**: Country of domicile
37/// - **Additional Info**: Branch details, postal codes, etc.
38///
39/// ## Usage Context
40/// Field 56D is used in:
41/// - **MT103**: Single Customer Credit Transfer
42/// - **MT200**: Financial Institution Transfer
43/// - **MT202**: General Financial Institution Transfer
44/// - **MT202COV**: Cover for customer credit transfer
45/// - **MT205**: Financial Institution Transfer for its own account
46///
47/// ### Business Applications
48/// - **Non-BIC institutions**: Identifying institutions without BIC codes
49/// - **Regional banks**: Supporting local and regional financial institutions
50/// - **Correspondent banking**: Detailed intermediary identification
51/// - **Cross-border payments**: International payment routing
52/// - **Regulatory compliance**: Meeting identification requirements
53/// - **Payment transparency**: Providing clear intermediary details
54///
55/// ## Examples
56/// ```text
57/// :56D:INTERMEDIARY BANK LIMITED
58/// 456 BANKING STREET
59/// LONDON EC2V 8RF
60/// UNITED KINGDOM
61/// └─── UK intermediary bank with full address
62///
63/// :56D:REGIONAL COOPERATIVE BANK
64/// HAUPTSTRASSE 123
65/// 60311 FRANKFURT AM MAIN
66/// GERMANY
67/// └─── German regional bank identification
68///
69/// :56D:CITY COMMERCIAL BANK
70/// FINANCIAL DISTRICT
71/// MUMBAI 400001
72/// INDIA
73/// └─── Indian commercial bank details
74///
75/// :56D:PROVINCIAL SAVINGS BANK
76/// RUE DE LA BANQUE 789
77/// 75001 PARIS
78/// FRANCE
79/// └─── French provincial bank with address
80/// ```
81///
82/// ## Name and Address Guidelines
83/// - **Line 1**: Institution name (required)
84/// - **Line 2**: Street address or building details
85/// - **Line 3**: City, state/province, postal code
86/// - **Line 4**: Country name
87/// - **Formatting**: Clear, unambiguous identification
88/// - **Language**: English preferred for international payments
89///
90/// ## Validation Rules
91/// 1. **Line count**: Minimum 1, maximum 4 lines
92/// 2. **Line length**: Maximum 35 characters per line
93/// 3. **Character set**: SWIFT character set only
94/// 4. **Content**: Each line must contain meaningful information
95/// 5. **Empty lines**: Not permitted
96/// 6. **Control characters**: Not allowed
97/// 7. **Special characters**: Limited to SWIFT-approved set
98///
99/// ## Network Validated Rules (SWIFT Standards)
100/// - Maximum 4 lines allowed (Error: T26)
101/// - Each line maximum 35 characters (Error: T50)
102/// - Must use SWIFT character set only (Error: T61)
103/// - At least one line required (Error: T13)
104/// - No empty lines permitted (Error: T40)
105/// - Field 56D alternative to 56A/56C (Error: C56)
106/// - Institution must be identifiable (Error: T51)
107///
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110pub struct Field56D {
111    /// Name and address lines (up to 4 lines of 35 characters each)
112    pub name_and_address: Vec<String>,
113}
114
115impl Field56D {
116    /// Create a new Field56D with validation
117    pub fn new(name_and_address: Vec<String>) -> Result<Self, crate::ParseError> {
118        if name_and_address.is_empty() {
119            return Err(crate::ParseError::InvalidFieldFormat {
120                field_tag: "56D".to_string(),
121                message: "Name and address cannot be empty".to_string(),
122            });
123        }
124
125        if name_and_address.len() > 4 {
126            return Err(crate::ParseError::InvalidFieldFormat {
127                field_tag: "56D".to_string(),
128                message: "Too many name/address lines (max 4)".to_string(),
129            });
130        }
131
132        for (i, line) in name_and_address.iter().enumerate() {
133            if line.len() > 35 {
134                return Err(crate::ParseError::InvalidFieldFormat {
135                    field_tag: "56D".to_string(),
136                    message: format!("Line {} too long (max 35 characters)", i + 1),
137                });
138            }
139
140            // Validate characters (printable ASCII)
141            if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
142                return Err(crate::ParseError::InvalidFieldFormat {
143                    field_tag: "56D".to_string(),
144                    message: format!("Line {} contains invalid characters", i + 1),
145                });
146            }
147        }
148
149        Ok(Field56D { name_and_address })
150    }
151
152    /// Create from a single string, splitting on newlines
153    pub fn from_string(content: impl Into<String>) -> Result<Self, crate::ParseError> {
154        let content = content.into();
155        let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
156        Self::new(lines)
157    }
158
159    /// Get the name and address lines
160    pub fn name_and_address(&self) -> &[String] {
161        &self.name_and_address
162    }
163
164    /// Get the number of lines
165    pub fn line_count(&self) -> usize {
166        self.name_and_address.len()
167    }
168
169    /// Get a specific line by index
170    pub fn line(&self, index: usize) -> Option<&str> {
171        self.name_and_address.get(index).map(|s| s.as_str())
172    }
173
174    /// Add a line of name/address information
175    pub fn add_line(&mut self, line: String) -> Result<(), crate::ParseError> {
176        if self.name_and_address.len() >= 4 {
177            return Err(crate::ParseError::InvalidFieldFormat {
178                field_tag: "56D".to_string(),
179                message: "Cannot add more lines (max 4)".to_string(),
180            });
181        }
182
183        if line.len() > 35 {
184            return Err(crate::ParseError::InvalidFieldFormat {
185                field_tag: "56D".to_string(),
186                message: "Line too long (max 35 characters)".to_string(),
187            });
188        }
189
190        if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
191            return Err(crate::ParseError::InvalidFieldFormat {
192                field_tag: "56D".to_string(),
193                message: "Line contains invalid characters".to_string(),
194            });
195        }
196
197        self.name_and_address.push(line);
198        Ok(())
199    }
200
201    /// Get human-readable description
202    pub fn description(&self) -> String {
203        format!("Intermediary Institution ({} lines)", self.line_count())
204    }
205}
206
207impl SwiftField for Field56D {
208    fn parse(content: &str) -> crate::Result<Self> {
209        let content = content.trim();
210        if content.is_empty() {
211            return Err(crate::ParseError::InvalidFieldFormat {
212                field_tag: "56D".to_string(),
213                message: "Field content cannot be empty".to_string(),
214            });
215        }
216
217        let content = if let Some(stripped) = content.strip_prefix(":56D:") {
218            stripped
219        } else if let Some(stripped) = content.strip_prefix("56D:") {
220            stripped
221        } else {
222            content
223        };
224
225        let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
226
227        Field56D::new(lines)
228    }
229
230    fn to_swift_string(&self) -> String {
231        format!(":56D:{}", self.name_and_address.join("\n"))
232    }
233
234    fn validate(&self) -> ValidationResult {
235        use crate::errors::ValidationError;
236
237        let mut errors = Vec::new();
238
239        if self.name_and_address.is_empty() {
240            errors.push(ValidationError::ValueValidation {
241                field_tag: "56D".to_string(),
242                message: "Name and address cannot be empty".to_string(),
243            });
244        }
245
246        if self.name_and_address.len() > 4 {
247            errors.push(ValidationError::LengthValidation {
248                field_tag: "56D".to_string(),
249                expected: "max 4 lines".to_string(),
250                actual: self.name_and_address.len(),
251            });
252        }
253
254        for (i, line) in self.name_and_address.iter().enumerate() {
255            if line.len() > 35 {
256                errors.push(ValidationError::LengthValidation {
257                    field_tag: "56D".to_string(),
258                    expected: format!("max 35 characters for line {}", i + 1),
259                    actual: line.len(),
260                });
261            }
262
263            if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
264                errors.push(ValidationError::FormatValidation {
265                    field_tag: "56D".to_string(),
266                    message: format!("Line {} contains invalid characters", i + 1),
267                });
268            }
269        }
270
271        ValidationResult {
272            is_valid: errors.is_empty(),
273            errors,
274            warnings: Vec::new(),
275        }
276    }
277
278    fn format_spec() -> &'static str {
279        "4*35x"
280    }
281}
282
283impl std::fmt::Display for Field56D {
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        write!(f, "{}", self.name_and_address.join("\n"))
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn test_field56d_creation() {
295        let lines = vec![
296            "INTERMEDIARY BANK".to_string(),
297            "123 INTERMEDIARY AVENUE".to_string(),
298            "NEW YORK NY 10001".to_string(),
299            "UNITED STATES".to_string(),
300        ];
301        let field = Field56D::new(lines.clone()).unwrap();
302        assert_eq!(field.name_and_address(), &lines);
303        assert_eq!(field.line_count(), 4);
304        assert_eq!(field.line(0), Some("INTERMEDIARY BANK"));
305        assert_eq!(field.line(1), Some("123 INTERMEDIARY AVENUE"));
306        assert_eq!(field.line(2), Some("NEW YORK NY 10001"));
307        assert_eq!(field.line(3), Some("UNITED STATES"));
308        assert_eq!(field.line(4), None);
309    }
310
311    #[test]
312    fn test_field56d_creation_single_line() {
313        let lines = vec!["INTERMEDIARY BANK".to_string()];
314        let field = Field56D::new(lines.clone()).unwrap();
315        assert_eq!(field.name_and_address(), &lines);
316        assert_eq!(field.line_count(), 1);
317    }
318
319    #[test]
320    fn test_field56d_from_string() {
321        let content =
322            "INTERMEDIARY BANK\n123 INTERMEDIARY AVENUE\nNEW YORK NY 10001\nUNITED STATES";
323        let field = Field56D::from_string(content).unwrap();
324        assert_eq!(field.line_count(), 4);
325        assert_eq!(field.line(0), Some("INTERMEDIARY BANK"));
326        assert_eq!(field.line(1), Some("123 INTERMEDIARY AVENUE"));
327        assert_eq!(field.line(2), Some("NEW YORK NY 10001"));
328        assert_eq!(field.line(3), Some("UNITED STATES"));
329    }
330
331    #[test]
332    fn test_field56d_parse() {
333        let field = Field56D::parse("INTERMEDIARY BANK\n123 INTERMEDIARY AVENUE").unwrap();
334        assert_eq!(field.line_count(), 2);
335        assert_eq!(field.line(0), Some("INTERMEDIARY BANK"));
336        assert_eq!(field.line(1), Some("123 INTERMEDIARY AVENUE"));
337    }
338
339    #[test]
340    fn test_field56d_parse_with_tag() {
341        let field = Field56D::parse(":56D:INTERMEDIARY BANK\n123 INTERMEDIARY AVENUE").unwrap();
342        assert_eq!(field.line_count(), 2);
343        assert_eq!(field.line(0), Some("INTERMEDIARY BANK"));
344        assert_eq!(field.line(1), Some("123 INTERMEDIARY AVENUE"));
345    }
346
347    #[test]
348    fn test_field56d_to_swift_string() {
349        let lines = vec![
350            "INTERMEDIARY BANK".to_string(),
351            "123 INTERMEDIARY AVENUE".to_string(),
352        ];
353        let field = Field56D::new(lines).unwrap();
354        assert_eq!(
355            field.to_swift_string(),
356            ":56D:INTERMEDIARY BANK\n123 INTERMEDIARY AVENUE"
357        );
358    }
359
360    #[test]
361    fn test_field56d_display() {
362        let lines = vec![
363            "INTERMEDIARY BANK".to_string(),
364            "123 INTERMEDIARY AVENUE".to_string(),
365        ];
366        let field = Field56D::new(lines).unwrap();
367        assert_eq!(
368            format!("{}", field),
369            "INTERMEDIARY BANK\n123 INTERMEDIARY AVENUE"
370        );
371    }
372
373    #[test]
374    fn test_field56d_description() {
375        let lines = vec![
376            "INTERMEDIARY BANK".to_string(),
377            "123 INTERMEDIARY AVENUE".to_string(),
378        ];
379        let field = Field56D::new(lines).unwrap();
380        assert_eq!(field.description(), "Intermediary Institution (2 lines)");
381    }
382
383    #[test]
384    fn test_field56d_add_line() {
385        let lines = vec!["INTERMEDIARY BANK".to_string()];
386        let mut field = Field56D::new(lines).unwrap();
387
388        field
389            .add_line("123 INTERMEDIARY AVENUE".to_string())
390            .unwrap();
391        assert_eq!(field.line_count(), 2);
392        assert_eq!(field.line(1), Some("123 INTERMEDIARY AVENUE"));
393
394        field.add_line("NEW YORK NY 10001".to_string()).unwrap();
395        assert_eq!(field.line_count(), 3);
396
397        field.add_line("UNITED STATES".to_string()).unwrap();
398        assert_eq!(field.line_count(), 4);
399
400        // Should fail when trying to add 5th line
401        let result = field.add_line("TOO MANY LINES".to_string());
402        assert!(result.is_err());
403    }
404
405    #[test]
406    fn test_field56d_validation_empty() {
407        let result = Field56D::new(vec![]);
408        assert!(result.is_err());
409        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
410    }
411
412    #[test]
413    fn test_field56d_validation_too_many_lines() {
414        let lines = vec![
415            "Line 1".to_string(),
416            "Line 2".to_string(),
417            "Line 3".to_string(),
418            "Line 4".to_string(),
419            "Line 5".to_string(), // Too many
420        ];
421        let result = Field56D::new(lines);
422        assert!(result.is_err());
423        assert!(result.unwrap_err().to_string().contains("max 4"));
424    }
425
426    #[test]
427    fn test_field56d_validation_line_too_long() {
428        let lines = vec!["A".repeat(36)]; // 36 characters, max is 35
429        let result = Field56D::new(lines);
430        assert!(result.is_err());
431        assert!(result.unwrap_err().to_string().contains("too long"));
432    }
433
434    #[test]
435    fn test_field56d_validation_invalid_characters() {
436        let lines = vec!["INTERMEDIARY BANK\x00".to_string()]; // Contains null character
437        let result = Field56D::new(lines);
438        assert!(result.is_err());
439        assert!(
440            result
441                .unwrap_err()
442                .to_string()
443                .contains("invalid characters")
444        );
445    }
446
447    #[test]
448    fn test_field56d_validate() {
449        let lines = vec![
450            "INTERMEDIARY BANK".to_string(),
451            "123 INTERMEDIARY AVENUE".to_string(),
452        ];
453        let field = Field56D::new(lines).unwrap();
454        let validation = field.validate();
455        assert!(validation.is_valid);
456        assert!(validation.errors.is_empty());
457    }
458
459    #[test]
460    fn test_field56d_validate_errors() {
461        let lines = vec!["A".repeat(36)]; // Line too long
462        let field = Field56D {
463            name_and_address: lines,
464        };
465        let validation = field.validate();
466        assert!(!validation.is_valid);
467        assert!(!validation.errors.is_empty());
468    }
469}