swift_mt_message/fields/
field57d.rs

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