swift_mt_message/fields/
field53d.rs

1use crate::{SwiftField, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 53D: Sender's Correspondent (Option D)
5///
6/// ## Overview
7/// Field 53D identifies the sender's correspondent institution using name and address
8/// information rather than a BIC code or party identifier. This option provides the
9/// most detailed identification method and is used when full institutional details
10/// are required for regulatory compliance, routing, or when other identification
11/// methods are not available or sufficient.
12///
13/// ## Format Specification
14/// **Format**: `4*35x`
15/// - **4*35x**: Up to 4 lines of name and address information
16/// - **Line length**: Maximum 35 characters per line
17/// - **Character set**: SWIFT character set (printable ASCII)
18/// - **Content**: Institution name, street address, city, postal code, country
19///
20/// ## Structure
21/// ```text
22/// Line 1: Institution Name (required)
23/// Line 2: Street Address/Building Number
24/// Line 3: City, State/Province, Postal Code
25/// Line 4: Country (recommended for international)
26/// ```
27///
28/// ## Usage Context
29/// Field 53D is used in:
30/// - **MT103**: Single Customer Credit Transfer (when 53A/53B not applicable)
31/// - **MT200**: Financial Institution Transfer
32/// - **MT202**: General Financial Institution Transfer
33/// - **MT202COV**: Cover for customer credit transfer
34///
35/// ### Business Applications
36/// - **Non-SWIFT institutions**: Identifying institutions without BIC codes
37/// - **Regulatory compliance**: Providing complete address for compliance screening
38/// - **Small institutions**: Local banks, credit unions, or regional institutions
39/// - **Enhanced due diligence**: Meeting KYC requirements for correspondent details
40/// - **Sanctions screening**: Enabling comprehensive name/address verification
41/// - **Audit trails**: Maintaining detailed correspondent institution records
42///
43/// ## Examples
44/// ```text
45/// :53D:REGIONAL TRUST BANK
46/// 456 CORRESPONDENT AVENUE
47/// MIDDLETOWN ST 54321
48/// UNITED STATES
49/// └─── US regional bank with full address
50///
51/// :53D:BANQUE LOCALE COOPERATIVE
52/// 78 RUE DU COMMERCE
53/// LYON 69001 FRANCE
54/// └─── French cooperative bank (3 lines)
55///
56/// :53D:COMMUNITY SAVINGS BANK
57/// 321 MAIN STREET
58/// SMALLVILLE TX 75001
59/// └─── Small community bank (minimal address)
60///
61/// :53D:CREDIT AGRICOLE REGIONAL
62/// SUCCURSALE DE PROVENCE
63/// 123 AVENUE DE LA REPUBLIQUE
64/// MARSEILLE 13001 FRANCE
65/// └─── Regional branch with detailed address
66/// ```
67///
68/// ## Address Format Guidelines
69/// ### Line 1: Institution Name (Required)
70/// - Full legal name of the correspondent institution
71/// - Include organizational form (Bank, Credit Union, Trust, etc.)
72/// - Avoid abbreviations when possible
73/// - Maximum 35 characters
74///
75/// ### Line 2: Street Address (Recommended)
76/// - Building number and street name
77/// - Suite/floor information if applicable
78/// - PO Box if street address not available
79/// - Maximum 35 characters
80///
81/// ### Line 3: City and Postal Information (Recommended)
82/// - City name, state/province abbreviation
83/// - Postal code or ZIP code
84/// - Administrative district if required
85/// - Maximum 35 characters
86///
87/// ### Line 4: Country (Optional but Recommended)
88/// - Full country name (preferred) or ISO code
89/// - Required for international correspondent relationships
90/// - Helps with routing and compliance screening
91/// - Maximum 35 characters
92///
93/// ## Address Standards
94/// - Use standard postal abbreviations for states/provinces
95/// - Include postal/ZIP codes when available
96/// - Spell out country names in full when possible
97/// - Avoid special characters and diacritical marks
98/// - Follow local address formatting conventions
99/// - Ensure consistency with official institution records
100///
101/// ## Validation Rules
102/// 1. **Minimum content**: At least 1 line required
103/// 2. **Maximum lines**: No more than 4 lines allowed
104/// 3. **Line length**: Each line maximum 35 characters
105/// 4. **Character validation**: Only printable ASCII characters
106/// 5. **Content requirement**: Must contain meaningful institution information
107/// 6. **Line ordering**: Institution name should be in first line
108///
109/// ## Network Validated Rules (SWIFT Standards)
110/// - Minimum 1 line, maximum 4 lines allowed (Error: C54)
111/// - Each line cannot exceed 35 characters (Error: T14)
112/// - Characters must be from SWIFT character set (Error: T61)
113/// - Lines cannot be empty (Error: T11)
114/// - Must contain institution name in first line (Error: C55)
115/// - Field 53D alternative to 53A/53B (Error: C53)
116/// - Address should be verifiable institution address (Error: C56)
117///
118
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
120pub struct Field53D {
121    /// Name and address lines (up to 4 lines of 35 characters each)
122    pub name_and_address: Vec<String>,
123}
124
125impl Field53D {
126    /// Create a new Field53D with validation
127    pub fn new(name_and_address: Vec<String>) -> Result<Self, crate::ParseError> {
128        if name_and_address.is_empty() {
129            return Err(crate::ParseError::InvalidFieldFormat {
130                field_tag: "53D".to_string(),
131                message: "Name and address cannot be empty".to_string(),
132            });
133        }
134
135        if name_and_address.len() > 4 {
136            return Err(crate::ParseError::InvalidFieldFormat {
137                field_tag: "53D".to_string(),
138                message: "Too many name/address lines (max 4)".to_string(),
139            });
140        }
141
142        for (i, line) in name_and_address.iter().enumerate() {
143            if line.len() > 35 {
144                return Err(crate::ParseError::InvalidFieldFormat {
145                    field_tag: "53D".to_string(),
146                    message: format!("Line {} too long (max 35 characters)", i + 1),
147                });
148            }
149
150            // Validate characters (printable ASCII)
151            if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
152                return Err(crate::ParseError::InvalidFieldFormat {
153                    field_tag: "53D".to_string(),
154                    message: format!("Line {} contains invalid characters", i + 1),
155                });
156            }
157        }
158
159        Ok(Field53D { name_and_address })
160    }
161
162    /// Create from a single string, splitting on newlines
163    pub fn from_string(content: impl Into<String>) -> Result<Self, crate::ParseError> {
164        let content = content.into();
165        let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
166        Self::new(lines)
167    }
168
169    /// Get the name and address lines
170    pub fn name_and_address(&self) -> &[String] {
171        &self.name_and_address
172    }
173
174    /// Get the number of lines
175    pub fn line_count(&self) -> usize {
176        self.name_and_address.len()
177    }
178
179    /// Get a specific line by index
180    pub fn line(&self, index: usize) -> Option<&str> {
181        self.name_and_address.get(index).map(|s| s.as_str())
182    }
183
184    /// Add a line of name/address information
185    pub fn add_line(&mut self, line: String) -> Result<(), crate::ParseError> {
186        if self.name_and_address.len() >= 4 {
187            return Err(crate::ParseError::InvalidFieldFormat {
188                field_tag: "53D".to_string(),
189                message: "Cannot add more lines (max 4)".to_string(),
190            });
191        }
192
193        if line.len() > 35 {
194            return Err(crate::ParseError::InvalidFieldFormat {
195                field_tag: "53D".to_string(),
196                message: "Line too long (max 35 characters)".to_string(),
197            });
198        }
199
200        if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
201            return Err(crate::ParseError::InvalidFieldFormat {
202                field_tag: "53D".to_string(),
203                message: "Line contains invalid characters".to_string(),
204            });
205        }
206
207        self.name_and_address.push(line);
208        Ok(())
209    }
210
211    /// Get human-readable description
212    pub fn description(&self) -> String {
213        format!("Sender's Correspondent ({} lines)", self.line_count())
214    }
215}
216
217impl SwiftField for Field53D {
218    fn parse(content: &str) -> crate::Result<Self> {
219        let content = content.trim();
220        if content.is_empty() {
221            return Err(crate::ParseError::InvalidFieldFormat {
222                field_tag: "53D".to_string(),
223                message: "Field content cannot be empty".to_string(),
224            });
225        }
226
227        let content = if let Some(stripped) = content.strip_prefix(":53D:") {
228            stripped
229        } else if let Some(stripped) = content.strip_prefix("53D:") {
230            stripped
231        } else {
232            content
233        };
234
235        let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
236
237        Field53D::new(lines)
238    }
239
240    fn to_swift_string(&self) -> String {
241        format!(":53D:{}", self.name_and_address.join("\n"))
242    }
243
244    fn validate(&self) -> ValidationResult {
245        use crate::errors::ValidationError;
246
247        let mut errors = Vec::new();
248
249        if self.name_and_address.is_empty() {
250            errors.push(ValidationError::ValueValidation {
251                field_tag: "53D".to_string(),
252                message: "Name and address cannot be empty".to_string(),
253            });
254        }
255
256        if self.name_and_address.len() > 4 {
257            errors.push(ValidationError::LengthValidation {
258                field_tag: "53D".to_string(),
259                expected: "max 4 lines".to_string(),
260                actual: self.name_and_address.len(),
261            });
262        }
263
264        for (i, line) in self.name_and_address.iter().enumerate() {
265            if line.len() > 35 {
266                errors.push(ValidationError::LengthValidation {
267                    field_tag: "53D".to_string(),
268                    expected: format!("max 35 characters for line {}", i + 1),
269                    actual: line.len(),
270                });
271            }
272
273            if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
274                errors.push(ValidationError::FormatValidation {
275                    field_tag: "53D".to_string(),
276                    message: format!("Line {} contains invalid characters", i + 1),
277                });
278            }
279        }
280
281        ValidationResult {
282            is_valid: errors.is_empty(),
283            errors,
284            warnings: Vec::new(),
285        }
286    }
287
288    fn format_spec() -> &'static str {
289        "4*35x"
290    }
291}
292
293impl std::fmt::Display for Field53D {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        write!(f, "{}", self.name_and_address.join("\n"))
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_field53d_creation() {
305        let lines = vec![
306            "CORRESPONDENT BANK".to_string(),
307            "456 FINANCE STREET".to_string(),
308            "LONDON EC2V 8RF".to_string(),
309            "UNITED KINGDOM".to_string(),
310        ];
311        let field = Field53D::new(lines.clone()).unwrap();
312        assert_eq!(field.name_and_address(), &lines);
313        assert_eq!(field.line_count(), 4);
314        assert_eq!(field.line(0), Some("CORRESPONDENT BANK"));
315        assert_eq!(field.line(1), Some("456 FINANCE STREET"));
316        assert_eq!(field.line(2), Some("LONDON EC2V 8RF"));
317        assert_eq!(field.line(3), Some("UNITED KINGDOM"));
318        assert_eq!(field.line(4), None);
319    }
320
321    #[test]
322    fn test_field53d_creation_single_line() {
323        let lines = vec!["CORRESPONDENT BANK".to_string()];
324        let field = Field53D::new(lines.clone()).unwrap();
325        assert_eq!(field.name_and_address(), &lines);
326        assert_eq!(field.line_count(), 1);
327    }
328
329    #[test]
330    fn test_field53d_from_string() {
331        let content = "CORRESPONDENT BANK\n456 FINANCE STREET\nLONDON EC2V 8RF\nUNITED KINGDOM";
332        let field = Field53D::from_string(content).unwrap();
333        assert_eq!(field.line_count(), 4);
334        assert_eq!(field.line(0), Some("CORRESPONDENT BANK"));
335        assert_eq!(field.line(1), Some("456 FINANCE STREET"));
336        assert_eq!(field.line(2), Some("LONDON EC2V 8RF"));
337        assert_eq!(field.line(3), Some("UNITED KINGDOM"));
338    }
339
340    #[test]
341    fn test_field53d_parse() {
342        let field = Field53D::parse("CORRESPONDENT BANK\n456 FINANCE STREET").unwrap();
343        assert_eq!(field.line_count(), 2);
344        assert_eq!(field.line(0), Some("CORRESPONDENT BANK"));
345        assert_eq!(field.line(1), Some("456 FINANCE STREET"));
346    }
347
348    #[test]
349    fn test_field53d_parse_with_tag() {
350        let field = Field53D::parse(":53D:CORRESPONDENT BANK\n456 FINANCE STREET").unwrap();
351        assert_eq!(field.line_count(), 2);
352        assert_eq!(field.line(0), Some("CORRESPONDENT BANK"));
353        assert_eq!(field.line(1), Some("456 FINANCE STREET"));
354    }
355
356    #[test]
357    fn test_field53d_to_swift_string() {
358        let lines = vec![
359            "CORRESPONDENT BANK".to_string(),
360            "456 FINANCE STREET".to_string(),
361        ];
362        let field = Field53D::new(lines).unwrap();
363        assert_eq!(
364            field.to_swift_string(),
365            ":53D:CORRESPONDENT BANK\n456 FINANCE STREET"
366        );
367    }
368
369    #[test]
370    fn test_field53d_display() {
371        let lines = vec![
372            "CORRESPONDENT BANK".to_string(),
373            "456 FINANCE STREET".to_string(),
374        ];
375        let field = Field53D::new(lines).unwrap();
376        assert_eq!(
377            format!("{}", field),
378            "CORRESPONDENT BANK\n456 FINANCE STREET"
379        );
380    }
381
382    #[test]
383    fn test_field53d_description() {
384        let lines = vec![
385            "CORRESPONDENT BANK".to_string(),
386            "456 FINANCE STREET".to_string(),
387        ];
388        let field = Field53D::new(lines).unwrap();
389        assert_eq!(field.description(), "Sender's Correspondent (2 lines)");
390    }
391
392    #[test]
393    fn test_field53d_add_line() {
394        let lines = vec!["CORRESPONDENT BANK".to_string()];
395        let mut field = Field53D::new(lines).unwrap();
396
397        field.add_line("456 FINANCE STREET".to_string()).unwrap();
398        assert_eq!(field.line_count(), 2);
399        assert_eq!(field.line(1), Some("456 FINANCE STREET"));
400
401        field.add_line("LONDON EC2V 8RF".to_string()).unwrap();
402        assert_eq!(field.line_count(), 3);
403
404        field.add_line("UNITED KINGDOM".to_string()).unwrap();
405        assert_eq!(field.line_count(), 4);
406
407        // Should fail when trying to add 5th line
408        let result = field.add_line("TOO MANY LINES".to_string());
409        assert!(result.is_err());
410    }
411
412    #[test]
413    fn test_field53d_validation_empty() {
414        let result = Field53D::new(vec![]);
415        assert!(result.is_err());
416        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
417    }
418
419    #[test]
420    fn test_field53d_validation_too_many_lines() {
421        let lines = vec![
422            "Line 1".to_string(),
423            "Line 2".to_string(),
424            "Line 3".to_string(),
425            "Line 4".to_string(),
426            "Line 5".to_string(), // Too many
427        ];
428        let result = Field53D::new(lines);
429        assert!(result.is_err());
430        assert!(result.unwrap_err().to_string().contains("max 4"));
431    }
432
433    #[test]
434    fn test_field53d_validation_line_too_long() {
435        let lines = vec!["A".repeat(36)]; // 36 characters, max is 35
436        let result = Field53D::new(lines);
437        assert!(result.is_err());
438        assert!(result.unwrap_err().to_string().contains("too long"));
439    }
440
441    #[test]
442    fn test_field53d_validation_invalid_characters() {
443        let lines = vec!["CORRESPONDENT BANK\x00".to_string()]; // Contains null character
444        let result = Field53D::new(lines);
445        assert!(result.is_err());
446        assert!(
447            result
448                .unwrap_err()
449                .to_string()
450                .contains("invalid characters")
451        );
452    }
453
454    #[test]
455    fn test_field53d_validate() {
456        let lines = vec![
457            "CORRESPONDENT BANK".to_string(),
458            "456 FINANCE STREET".to_string(),
459        ];
460        let field = Field53D::new(lines).unwrap();
461        let validation = field.validate();
462        assert!(validation.is_valid);
463        assert!(validation.errors.is_empty());
464    }
465
466    #[test]
467    fn test_field53d_validate_errors() {
468        let lines = vec!["A".repeat(36)]; // Line too long
469        let field = Field53D {
470            name_and_address: lines,
471        };
472        let validation = field.validate();
473        assert!(!validation.is_valid);
474        assert!(!validation.errors.is_empty());
475    }
476}