swift_mt_message/fields/
field52d.rs

1use crate::{SwiftField, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 52D: Ordering Institution (Option D)
5///
6/// ## Overview
7/// Field 52D identifies the ordering institution in SWIFT payment messages using name
8/// and address information rather than a BIC code. This option is used when the ordering
9/// institution does not have a BIC or when full name and address details are required
10/// for regulatory, compliance, or routing purposes.
11///
12/// ## Format Specification
13/// **Format**: `4*35x`
14/// - **4*35x**: Up to 4 lines of name and address information
15/// - **Line length**: Maximum 35 characters per line
16/// - **Character set**: SWIFT character set (printable ASCII)
17/// - **Content**: Institution name, street address, city, country
18///
19/// ## Structure
20/// ```text
21/// Line 1: Institution Name
22/// Line 2: Street Address
23/// Line 3: City, State/Province, Postal Code
24/// Line 4: Country (optional)
25/// ```
26///
27/// ## Usage Context
28/// Field 52D is used in:
29/// - **MT103**: Single Customer Credit Transfer (when 52A not available)
30/// - **MT200**: Financial Institution Transfer
31/// - **MT202**: General Financial Institution Transfer  
32/// - **MT202COV**: Cover for customer credit transfer
33///
34/// ### Business Applications
35/// - **Non-SWIFT institutions**: Identifying institutions without BIC codes
36/// - **Regulatory compliance**: Providing full address for compliance requirements
37/// - **Local banks**: Identifying smaller banks or credit unions
38/// - **Correspondent banking**: When full address details are required
39/// - **Sanctions screening**: Enabling comprehensive name/address screening
40///
41/// ## Examples
42/// ```text
43/// :52D:FIRST NATIONAL BANK
44/// 123 MAIN STREET
45/// ANYTOWN NY 12345
46/// UNITED STATES
47/// └─── US regional bank with full address
48///
49/// :52D:BANQUE REGIONALE SARL
50/// 45 RUE DE LA PAIX
51/// PARIS 75001 FRANCE
52/// └─── French regional bank (3 lines)
53///
54/// :52D:CREDIT UNION COOPERATIVE
55/// 789 COMMUNITY DRIVE
56/// SMALLTOWN CA 90210
57/// └─── Credit union (minimal address)
58/// ```
59///
60/// ## Address Format Guidelines
61/// - **Line 1**: Institution legal name (required)
62/// - **Line 2**: Street address/building number (recommended)
63/// - **Line 3**: City, state/province, postal code (recommended)
64/// - **Line 4**: Country name (optional but recommended for international)
65///
66/// ### Address Standards
67/// - Use standard postal abbreviations
68/// - Include postal/ZIP codes when available
69/// - Spell out country names in full
70/// - Avoid special characters and diacritical marks
71/// - Use standard address formatting conventions
72///
73/// ## Validation Rules
74/// 1. **Minimum content**: At least 1 line required
75/// 2. **Maximum lines**: No more than 4 lines
76/// 3. **Line length**: Each line maximum 35 characters
77/// 4. **Character validation**: Only printable ASCII characters
78/// 5. **Content requirement**: Must contain meaningful institution information
79///
80/// ## Network Validated Rules (SWIFT Standards)
81/// - Minimum 1 line, maximum 4 lines allowed (Error: C54)
82/// - Each line cannot exceed 35 characters (Error: T14)
83/// - Characters must be from SWIFT character set (Error: T61)
84/// - Lines cannot be empty (Error: T11)
85/// - Must contain institution name in first line (Error: C55)
86/// - Field 52D alternative to 52A (Error: C52)
87///
88
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
90pub struct Field52D {
91    /// Name and address lines (up to 4 lines of 35 characters each)
92    pub name_and_address: Vec<String>,
93}
94
95impl Field52D {
96    /// Create a new Field52D with validation
97    pub fn new(name_and_address: Vec<String>) -> Result<Self, crate::ParseError> {
98        if name_and_address.is_empty() {
99            return Err(crate::ParseError::InvalidFieldFormat {
100                field_tag: "52D".to_string(),
101                message: "Name and address cannot be empty".to_string(),
102            });
103        }
104
105        if name_and_address.len() > 4 {
106            return Err(crate::ParseError::InvalidFieldFormat {
107                field_tag: "52D".to_string(),
108                message: "Too many name/address lines (max 4)".to_string(),
109            });
110        }
111
112        for (i, line) in name_and_address.iter().enumerate() {
113            if line.len() > 35 {
114                return Err(crate::ParseError::InvalidFieldFormat {
115                    field_tag: "52D".to_string(),
116                    message: format!("Line {} too long (max 35 characters)", i + 1),
117                });
118            }
119
120            // Validate characters (printable ASCII)
121            if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
122                return Err(crate::ParseError::InvalidFieldFormat {
123                    field_tag: "52D".to_string(),
124                    message: format!("Line {} contains invalid characters", i + 1),
125                });
126            }
127        }
128
129        Ok(Field52D { name_and_address })
130    }
131
132    /// Create from a single string, splitting on newlines
133    pub fn from_string(content: impl Into<String>) -> Result<Self, crate::ParseError> {
134        let content = content.into();
135        let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
136        Self::new(lines)
137    }
138
139    /// Get the name and address lines
140    pub fn name_and_address(&self) -> &[String] {
141        &self.name_and_address
142    }
143
144    /// Get the number of lines
145    pub fn line_count(&self) -> usize {
146        self.name_and_address.len()
147    }
148
149    /// Get a specific line by index
150    pub fn line(&self, index: usize) -> Option<&str> {
151        self.name_and_address.get(index).map(|s| s.as_str())
152    }
153
154    /// Add a line of name/address information
155    pub fn add_line(&mut self, line: String) -> Result<(), crate::ParseError> {
156        if self.name_and_address.len() >= 4 {
157            return Err(crate::ParseError::InvalidFieldFormat {
158                field_tag: "52D".to_string(),
159                message: "Cannot add more lines (max 4)".to_string(),
160            });
161        }
162
163        if line.len() > 35 {
164            return Err(crate::ParseError::InvalidFieldFormat {
165                field_tag: "52D".to_string(),
166                message: "Line too long (max 35 characters)".to_string(),
167            });
168        }
169
170        if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
171            return Err(crate::ParseError::InvalidFieldFormat {
172                field_tag: "52D".to_string(),
173                message: "Line contains invalid characters".to_string(),
174            });
175        }
176
177        self.name_and_address.push(line);
178        Ok(())
179    }
180
181    /// Get human-readable description
182    pub fn description(&self) -> String {
183        format!("Ordering Institution ({} lines)", self.line_count())
184    }
185}
186
187impl SwiftField for Field52D {
188    fn parse(content: &str) -> crate::Result<Self> {
189        let content = content.trim();
190        if content.is_empty() {
191            return Err(crate::ParseError::InvalidFieldFormat {
192                field_tag: "52D".to_string(),
193                message: "Field content cannot be empty".to_string(),
194            });
195        }
196
197        let content = if let Some(stripped) = content.strip_prefix(":52D:") {
198            stripped
199        } else if let Some(stripped) = content.strip_prefix("52D:") {
200            stripped
201        } else {
202            content
203        };
204
205        let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
206
207        Field52D::new(lines)
208    }
209
210    fn to_swift_string(&self) -> String {
211        format!(":52D:{}", self.name_and_address.join("\n"))
212    }
213
214    fn validate(&self) -> ValidationResult {
215        use crate::errors::ValidationError;
216
217        let mut errors = Vec::new();
218
219        if self.name_and_address.is_empty() {
220            errors.push(ValidationError::ValueValidation {
221                field_tag: "52D".to_string(),
222                message: "Name and address cannot be empty".to_string(),
223            });
224        }
225
226        if self.name_and_address.len() > 4 {
227            errors.push(ValidationError::LengthValidation {
228                field_tag: "52D".to_string(),
229                expected: "max 4 lines".to_string(),
230                actual: self.name_and_address.len(),
231            });
232        }
233
234        for (i, line) in self.name_and_address.iter().enumerate() {
235            if line.len() > 35 {
236                errors.push(ValidationError::LengthValidation {
237                    field_tag: "52D".to_string(),
238                    expected: format!("max 35 characters for line {}", i + 1),
239                    actual: line.len(),
240                });
241            }
242
243            if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
244                errors.push(ValidationError::FormatValidation {
245                    field_tag: "52D".to_string(),
246                    message: format!("Line {} contains invalid characters", i + 1),
247                });
248            }
249        }
250
251        ValidationResult {
252            is_valid: errors.is_empty(),
253            errors,
254            warnings: Vec::new(),
255        }
256    }
257
258    fn format_spec() -> &'static str {
259        "4*35x"
260    }
261}
262
263impl std::fmt::Display for Field52D {
264    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        write!(f, "{}", self.name_and_address.join("\n"))
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_field52d_creation() {
275        let lines = vec![
276            "ABC BANK".to_string(),
277            "123 MAIN STREET".to_string(),
278            "NEW YORK NY 10001".to_string(),
279        ];
280        let field = Field52D::new(lines.clone()).unwrap();
281        assert_eq!(field.name_and_address(), &lines);
282        assert_eq!(field.line_count(), 3);
283        assert_eq!(field.line(0), Some("ABC BANK"));
284        assert_eq!(field.line(1), Some("123 MAIN STREET"));
285        assert_eq!(field.line(2), Some("NEW YORK NY 10001"));
286        assert_eq!(field.line(3), None);
287    }
288
289    #[test]
290    fn test_field52d_creation_single_line() {
291        let lines = vec!["ABC BANK".to_string()];
292        let field = Field52D::new(lines.clone()).unwrap();
293        assert_eq!(field.name_and_address(), &lines);
294        assert_eq!(field.line_count(), 1);
295    }
296
297    #[test]
298    fn test_field52d_from_string() {
299        let content = "ABC BANK\n123 MAIN STREET\nNEW YORK NY 10001";
300        let field = Field52D::from_string(content).unwrap();
301        assert_eq!(field.line_count(), 3);
302        assert_eq!(field.line(0), Some("ABC BANK"));
303        assert_eq!(field.line(1), Some("123 MAIN STREET"));
304        assert_eq!(field.line(2), Some("NEW YORK NY 10001"));
305    }
306
307    #[test]
308    fn test_field52d_parse() {
309        let field = Field52D::parse("ABC BANK\n123 MAIN STREET").unwrap();
310        assert_eq!(field.line_count(), 2);
311        assert_eq!(field.line(0), Some("ABC BANK"));
312        assert_eq!(field.line(1), Some("123 MAIN STREET"));
313    }
314
315    #[test]
316    fn test_field52d_parse_with_tag() {
317        let field = Field52D::parse(":52D:ABC BANK\n123 MAIN STREET").unwrap();
318        assert_eq!(field.line_count(), 2);
319        assert_eq!(field.line(0), Some("ABC BANK"));
320        assert_eq!(field.line(1), Some("123 MAIN STREET"));
321    }
322
323    #[test]
324    fn test_field52d_to_swift_string() {
325        let lines = vec!["ABC BANK".to_string(), "123 MAIN STREET".to_string()];
326        let field = Field52D::new(lines).unwrap();
327        assert_eq!(field.to_swift_string(), ":52D:ABC BANK\n123 MAIN STREET");
328    }
329
330    #[test]
331    fn test_field52d_display() {
332        let lines = vec!["ABC BANK".to_string(), "123 MAIN STREET".to_string()];
333        let field = Field52D::new(lines).unwrap();
334        assert_eq!(format!("{}", field), "ABC BANK\n123 MAIN STREET");
335    }
336
337    #[test]
338    fn test_field52d_description() {
339        let lines = vec!["ABC BANK".to_string(), "123 MAIN STREET".to_string()];
340        let field = Field52D::new(lines).unwrap();
341        assert_eq!(field.description(), "Ordering Institution (2 lines)");
342    }
343
344    #[test]
345    fn test_field52d_add_line() {
346        let lines = vec!["ABC BANK".to_string()];
347        let mut field = Field52D::new(lines).unwrap();
348
349        field.add_line("123 MAIN STREET".to_string()).unwrap();
350        assert_eq!(field.line_count(), 2);
351        assert_eq!(field.line(1), Some("123 MAIN STREET"));
352
353        field.add_line("NEW YORK NY 10001".to_string()).unwrap();
354        assert_eq!(field.line_count(), 3);
355
356        field.add_line("USA".to_string()).unwrap();
357        assert_eq!(field.line_count(), 4);
358
359        // Should fail when trying to add 5th line
360        let result = field.add_line("TOO MANY LINES".to_string());
361        assert!(result.is_err());
362    }
363
364    #[test]
365    fn test_field52d_validation_empty() {
366        let result = Field52D::new(vec![]);
367        assert!(result.is_err());
368        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
369    }
370
371    #[test]
372    fn test_field52d_validation_too_many_lines() {
373        let lines = vec![
374            "Line 1".to_string(),
375            "Line 2".to_string(),
376            "Line 3".to_string(),
377            "Line 4".to_string(),
378            "Line 5".to_string(), // Too many
379        ];
380        let result = Field52D::new(lines);
381        assert!(result.is_err());
382        assert!(result.unwrap_err().to_string().contains("max 4"));
383    }
384
385    #[test]
386    fn test_field52d_validation_line_too_long() {
387        let lines = vec!["A".repeat(36)]; // 36 characters, max is 35
388        let result = Field52D::new(lines);
389        assert!(result.is_err());
390        assert!(result.unwrap_err().to_string().contains("too long"));
391    }
392
393    #[test]
394    fn test_field52d_validation_invalid_characters() {
395        let lines = vec!["ABC BANK\x00".to_string()]; // Contains null character
396        let result = Field52D::new(lines);
397        assert!(result.is_err());
398        assert!(
399            result
400                .unwrap_err()
401                .to_string()
402                .contains("invalid characters")
403        );
404    }
405
406    #[test]
407    fn test_field52d_validate() {
408        let lines = vec!["ABC BANK".to_string(), "123 MAIN STREET".to_string()];
409        let field = Field52D::new(lines).unwrap();
410        let validation = field.validate();
411        assert!(validation.is_valid);
412        assert!(validation.errors.is_empty());
413    }
414
415    #[test]
416    fn test_field52d_validate_errors() {
417        let lines = vec!["A".repeat(36)]; // Line too long
418        let field = Field52D {
419            name_and_address: lines,
420        };
421        let validation = field.validate();
422        assert!(!validation.is_valid);
423        assert!(!validation.errors.is_empty());
424    }
425}