swift_mt_message/fields/
field53a.rs

1use crate::common::BIC;
2use crate::{SwiftField, ValidationError, ValidationResult};
3use serde::{Deserialize, Serialize};
4
5/// # Field 53A: Sender's Correspondent
6///
7/// ## Overview
8/// Field 53A identifies the sender's correspondent institution in SWIFT payment messages.
9/// This field specifies the financial institution that acts as a correspondent for the
10/// message sender, facilitating the payment routing and settlement process. The correspondent
11/// relationship is crucial for cross-border payments and correspondent banking arrangements.
12///
13/// ## Format Specification
14/// **Format**: `[/1!c][/34x]4!a2!a2!c[3!c]`
15/// - **1!c**: Optional account line indicator (1 character)
16/// - **34x**: Optional account number (up to 34 characters)
17/// - **4!a2!a2!c[3!c]**: BIC code (8 or 11 characters)
18///
19/// ### BIC Structure
20/// ```text
21/// CHASUS33XXX
22/// ││││││││└┴┴─ Branch Code (3 characters, optional)
23/// ││││││└┴──── Location Code (2 characters)
24/// ││││└┴────── Country Code (2 letters)
25/// └┴┴┴──────── Bank Code (4 letters)
26/// ```
27///
28/// ## Field Components
29/// - **Account Line Indicator**: Optional qualifier for account type or purpose
30/// - **Account Number**: Correspondent account number for settlement
31/// - **BIC**: Bank Identifier Code of the correspondent institution
32///
33/// ## Usage Context
34/// Field 53A is used in:
35/// - **MT103**: Single Customer Credit Transfer
36/// - **MT200**: Financial Institution Transfer
37/// - **MT202**: General Financial Institution Transfer
38/// - **MT202COV**: Cover for customer credit transfer
39///
40/// ### Business Applications
41/// - **Correspondent banking**: Identifying correspondent bank relationships
42/// - **Payment routing**: Providing routing instructions for payment processing
43/// - **Settlement**: Facilitating settlement through correspondent accounts
44/// - **Risk management**: Managing correspondent banking exposure and limits
45/// - **Compliance**: Meeting regulatory requirements for correspondent relationships
46///
47/// ## Examples
48/// ```text
49/// :53A:CHASUS33XXX
50/// └─── JPMorgan Chase New York as correspondent
51///
52/// :53A:/C/1234567890
53/// DEUTDEFFXXX
54/// └─── Deutsche Bank with checking account 1234567890
55///
56/// :53A:/N/LORO12345678901234567890
57/// BNPAFRPPXXX
58/// └─── BNP Paribas with nostro account identifier
59///
60/// :53A:/V/VOSTRO001234567890123456
61/// ABCDEFGHJKL
62/// └─── Correspondent with vostro account reference
63/// ```
64///
65/// ## Account Line Indicators
66/// Common account line indicators for correspondent relationships:
67/// - **C**: Correspondent account (checking)
68/// - **D**: Deposit account
69/// - **L**: Loan account
70/// - **N**: Nostro account (our account with them)
71/// - **S**: Settlement account
72/// - **V**: Vostro account (their account with us)
73///
74/// ## BIC Components Analysis
75/// ### Bank Code (Characters 1-4)
76/// - Must be 4 alphabetic characters
77/// - Identifies the specific financial institution
78/// - Assigned by SWIFT registration authority
79///
80/// ### Country Code (Characters 5-6)
81/// - Must be valid ISO 3166-1 alpha-2 country code
82/// - Identifies the country of the institution
83/// - Must match BIC registration country
84///
85/// ### Location Code (Characters 7-8)
86/// - Alphanumeric characters identifying location within country
87/// - Often represents city or administrative division
88/// - Used for routing within correspondent networks
89///
90/// ### Branch Code (Characters 9-11)
91/// - Optional 3-character branch identifier
92/// - Identifies specific branch or department
93/// - XXX indicates head office if present
94///
95/// ## Validation Rules
96/// 1. **BIC format**: Must be valid 8 or 11 character BIC
97/// 2. **BIC structure**: 4!a2!a2!c[3!c] format required
98/// 3. **Account line indicator**: If present, exactly 1 character
99/// 4. **Account number**: If present, max 34 characters
100/// 5. **Character validation**: All components must use valid character sets
101///
102/// ## Network Validated Rules (SWIFT Standards)
103/// - BIC must be valid format and registered (Error: T27)
104/// - Account line indicator must be single character (Error: T12)
105/// - Account number cannot exceed 34 characters (Error: T15)
106/// - BIC country code must be valid ISO country code (Error: T28)
107/// - Characters must be from SWIFT character set (Error: T61)
108/// - Field 53A is conditional based on message type (Error: C53)
109///
110
111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
112pub struct Field53A {
113    /// Account line indicator (optional, 1 character)
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub account_line_indicator: Option<String>,
116    /// Account number (optional, up to 34 characters)
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub account_number: Option<String>,
119    /// BIC code (8 or 11 characters)
120    #[serde(flatten)]
121    pub bic: BIC,
122}
123
124impl Field53A {
125    /// Create a new Field53A with validation
126    pub fn new(
127        account_line_indicator: Option<String>,
128        account_number: Option<String>,
129        bic: impl Into<String>,
130    ) -> Result<Self, crate::ParseError> {
131        let bic = bic.into().to_uppercase();
132
133        // Validate account line indicator if present
134        if let Some(ref indicator) = account_line_indicator {
135            if indicator.is_empty() {
136                return Err(crate::ParseError::InvalidFieldFormat {
137                    field_tag: "53A".to_string(),
138                    message: "Account line indicator cannot be empty if specified".to_string(),
139                });
140            }
141
142            if indicator.len() != 1 {
143                return Err(crate::ParseError::InvalidFieldFormat {
144                    field_tag: "53A".to_string(),
145                    message: "Account line indicator must be exactly 1 character".to_string(),
146                });
147            }
148
149            if !indicator.chars().all(|c| c.is_ascii() && !c.is_control()) {
150                return Err(crate::ParseError::InvalidFieldFormat {
151                    field_tag: "53A".to_string(),
152                    message: "Account line indicator contains invalid characters".to_string(),
153                });
154            }
155        }
156
157        // Validate account number if present
158        if let Some(ref account) = account_number {
159            if account.is_empty() {
160                return Err(crate::ParseError::InvalidFieldFormat {
161                    field_tag: "53A".to_string(),
162                    message: "Account number cannot be empty if specified".to_string(),
163                });
164            }
165
166            if account.len() > 34 {
167                return Err(crate::ParseError::InvalidFieldFormat {
168                    field_tag: "53A".to_string(),
169                    message: "Account number too long (max 34 characters)".to_string(),
170                });
171            }
172
173            if !account.chars().all(|c| c.is_ascii() && !c.is_control()) {
174                return Err(crate::ParseError::InvalidFieldFormat {
175                    field_tag: "53A".to_string(),
176                    message: "Account number contains invalid characters".to_string(),
177                });
178            }
179        }
180
181        // Parse and validate BIC using the common structure
182        let parsed_bic = BIC::parse(&bic, Some("53A"))?;
183
184        Ok(Field53A {
185            account_line_indicator,
186            account_number,
187            bic: parsed_bic,
188        })
189    }
190
191    /// Get the account line indicator
192    pub fn account_line_indicator(&self) -> Option<&str> {
193        self.account_line_indicator.as_deref()
194    }
195
196    /// Get the account number
197    pub fn account_number(&self) -> Option<&str> {
198        self.account_number.as_deref()
199    }
200
201    /// Get the BIC code
202    pub fn bic(&self) -> &str {
203        self.bic.value()
204    }
205
206    /// Check if this is a full BIC (11 characters) or short BIC (8 characters)
207    pub fn is_full_bic(&self) -> bool {
208        self.bic.is_full_bic()
209    }
210
211    /// Get the bank code (first 4 characters of BIC)
212    pub fn bank_code(&self) -> &str {
213        self.bic.bank_code()
214    }
215
216    /// Get the country code (characters 5-6 of BIC)
217    pub fn country_code(&self) -> &str {
218        self.bic.country_code()
219    }
220
221    /// Get the location code (characters 7-8 of BIC)
222    pub fn location_code(&self) -> &str {
223        self.bic.location_code()
224    }
225
226    /// Get the branch code (characters 9-11 of BIC, if present)
227    pub fn branch_code(&self) -> Option<&str> {
228        self.bic.branch_code()
229    }
230
231    /// Get human-readable description
232    pub fn description(&self) -> String {
233        match &self.account_number {
234            Some(account) => format!("Sender's Correspondent: {} ({})", self.bic, account),
235            None => format!("Sender's Correspondent: {}", self.bic),
236        }
237    }
238}
239
240impl SwiftField for Field53A {
241    fn parse(value: &str) -> Result<Self, crate::ParseError> {
242        let content = if let Some(stripped) = value.strip_prefix(":53A:") {
243            stripped // Remove ":53A:" prefix
244        } else if let Some(stripped) = value.strip_prefix("53A:") {
245            stripped // Remove "53A:" prefix
246        } else {
247            value
248        };
249
250        let content = content.trim();
251
252        if content.is_empty() {
253            return Err(crate::ParseError::InvalidFieldFormat {
254                field_tag: "53A".to_string(),
255                message: "Field content cannot be empty".to_string(),
256            });
257        }
258
259        // Parse account and BIC
260        let mut account_number = None;
261        let bic;
262
263        if content.starts_with('/') {
264            // Has account number
265            let lines: Vec<&str> = content.lines().collect();
266
267            if lines.len() == 1 {
268                // Account and BIC on same line: "/account BIC" or "/account\nBIC"
269                let parts: Vec<&str> = lines[0].splitn(2, ' ').collect();
270                if parts.len() == 2 {
271                    account_number = Some(parts[0][1..].to_string()); // Remove leading '/'
272                    bic = parts[1].to_string();
273                } else {
274                    return Err(crate::ParseError::InvalidFieldFormat {
275                        field_tag: "53A".to_string(),
276                        message: "Invalid format: expected account and BIC".to_string(),
277                    });
278                }
279            } else if lines.len() == 2 {
280                // Account and BIC on separate lines
281                account_number = Some(lines[0][1..].to_string()); // Remove leading '/'
282                bic = lines[1].to_string();
283            } else {
284                return Err(crate::ParseError::InvalidFieldFormat {
285                    field_tag: "53A".to_string(),
286                    message: "Invalid format: too many lines".to_string(),
287                });
288            }
289        } else {
290            // No account number, just BIC
291            bic = content.to_string();
292        }
293
294        let parsed_bic = BIC::parse(&bic, Some("53A"))?;
295
296        Ok(Field53A {
297            account_line_indicator: None,
298            account_number,
299            bic: parsed_bic,
300        })
301    }
302
303    fn to_swift_string(&self) -> String {
304        match &self.account_number {
305            Some(account) => format!(":53A:/{}\n{}", account, self.bic.value()),
306            None => format!(":53A:{}", self.bic.value()),
307        }
308    }
309
310    fn validate(&self) -> ValidationResult {
311        let mut errors = Vec::new();
312
313        // Validate account line indicator if present
314        if let Some(ref indicator) = self.account_line_indicator {
315            if indicator.is_empty() {
316                errors.push(ValidationError::ValueValidation {
317                    field_tag: "53A".to_string(),
318                    message: "Account line indicator cannot be empty if specified".to_string(),
319                });
320            }
321
322            if indicator.len() != 1 {
323                errors.push(ValidationError::LengthValidation {
324                    field_tag: "53A".to_string(),
325                    expected: "exactly 1 character".to_string(),
326                    actual: indicator.len(),
327                });
328            }
329
330            if !indicator.chars().all(|c| c.is_ascii() && !c.is_control()) {
331                errors.push(ValidationError::FormatValidation {
332                    field_tag: "53A".to_string(),
333                    message: "Account line indicator contains invalid characters".to_string(),
334                });
335            }
336        }
337
338        // Validate account number if present
339        if let Some(ref account) = self.account_number {
340            if account.is_empty() {
341                errors.push(ValidationError::ValueValidation {
342                    field_tag: "53A".to_string(),
343                    message: "Account number cannot be empty if specified".to_string(),
344                });
345            }
346
347            if account.len() > 34 {
348                errors.push(ValidationError::LengthValidation {
349                    field_tag: "53A".to_string(),
350                    expected: "max 34 characters".to_string(),
351                    actual: account.len(),
352                });
353            }
354
355            if !account.chars().all(|c| c.is_ascii() && !c.is_control()) {
356                errors.push(ValidationError::FormatValidation {
357                    field_tag: "53A".to_string(),
358                    message: "Account number contains invalid characters".to_string(),
359                });
360            }
361        }
362
363        // Validate BIC format using the common BIC validation
364        let bic_validation = self.bic.validate();
365        if !bic_validation.is_valid {
366            errors.extend(bic_validation.errors);
367        }
368
369        ValidationResult {
370            is_valid: errors.is_empty(),
371            errors,
372            warnings: Vec::new(),
373        }
374    }
375
376    fn format_spec() -> &'static str {
377        "[/1!c][/34x]BIC"
378    }
379}
380
381impl std::fmt::Display for Field53A {
382    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383        match (&self.account_line_indicator, &self.account_number) {
384            (Some(indicator), Some(account)) => write!(f, "/{}{} {}", indicator, account, self.bic),
385            (None, Some(account)) => write!(f, "/{} {}", account, self.bic),
386            _ => write!(f, "{}", self.bic),
387        }
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    #[test]
396    fn test_field53a_creation_bic_only() {
397        let field = Field53A::new(None, None, "DEUTDEFF").unwrap();
398        assert_eq!(field.bic(), "DEUTDEFF");
399        assert!(field.account_number().is_none());
400        assert!(!field.is_full_bic());
401    }
402
403    #[test]
404    fn test_field53a_creation_with_account() {
405        let field = Field53A::new(None, Some("1234567890".to_string()), "DEUTDEFF500").unwrap();
406        assert_eq!(field.bic(), "DEUTDEFF500");
407        assert_eq!(field.account_number(), Some("1234567890"));
408        assert!(field.is_full_bic());
409    }
410
411    #[test]
412    fn test_field53a_parse_bic_only() {
413        let field = Field53A::parse("CHASUS33").unwrap();
414        assert_eq!(field.bic(), "CHASUS33");
415        assert!(field.account_number().is_none());
416    }
417
418    #[test]
419    fn test_field53a_parse_with_account_same_line() {
420        let field = Field53A::parse("/1234567890 DEUTDEFF").unwrap();
421        assert_eq!(field.bic(), "DEUTDEFF");
422        assert_eq!(field.account_number(), Some("1234567890"));
423    }
424
425    #[test]
426    fn test_field53a_parse_with_account_separate_lines() {
427        let field = Field53A::parse("/1234567890\nDEUTDEFF500").unwrap();
428        assert_eq!(field.bic(), "DEUTDEFF500");
429        assert_eq!(field.account_number(), Some("1234567890"));
430    }
431
432    #[test]
433    fn test_field53a_parse_with_prefix() {
434        let field = Field53A::parse(":53A:CHASUS33").unwrap();
435        assert_eq!(field.bic(), "CHASUS33");
436    }
437
438    #[test]
439    fn test_field53a_to_swift_string_bic_only() {
440        let field = Field53A::new(None, None, "DEUTDEFF").unwrap();
441        assert_eq!(field.to_swift_string(), ":53A:DEUTDEFF");
442    }
443
444    #[test]
445    fn test_field53a_to_swift_string_with_account() {
446        let field = Field53A::new(None, Some("1234567890".to_string()), "DEUTDEFF500").unwrap();
447        assert_eq!(field.to_swift_string(), ":53A:/1234567890\nDEUTDEFF500");
448    }
449
450    #[test]
451    fn test_field53a_bic_components() {
452        let field = Field53A::new(None, None, "DEUTDEFF500").unwrap();
453        assert_eq!(field.bank_code(), "DEUT");
454        assert_eq!(field.country_code(), "DE");
455        assert_eq!(field.location_code(), "FF");
456        assert_eq!(field.branch_code(), Some("500"));
457    }
458
459    #[test]
460    fn test_field53a_short_bic_components() {
461        let field = Field53A::new(None, None, "CHASUS33").unwrap();
462        assert_eq!(field.bank_code(), "CHAS");
463        assert_eq!(field.country_code(), "US");
464        assert_eq!(field.location_code(), "33");
465        assert_eq!(field.branch_code(), None);
466    }
467
468    #[test]
469    fn test_field53a_invalid_bic_length() {
470        let result = Field53A::new(None, None, "DEUT");
471        assert!(result.is_err());
472
473        let result = Field53A::new(None, None, "DEUTDEFF5001");
474        assert!(result.is_err());
475    }
476
477    #[test]
478    fn test_field53a_invalid_bic_format() {
479        let result = Field53A::new(None, None, "123TDEFF");
480        assert!(result.is_err());
481
482        let result = Field53A::new(None, None, "DEUT12FF");
483        assert!(result.is_err());
484
485        let result = Field53A::new(None, None, "DEUTDE@F");
486        assert!(result.is_err());
487    }
488
489    #[test]
490    fn test_field53a_invalid_account() {
491        let result = Field53A::new(None, Some("".to_string()), "DEUTDEFF");
492        assert!(result.is_err());
493
494        let result = Field53A::new(None, Some("A".repeat(35)), "DEUTDEFF");
495        assert!(result.is_err());
496    }
497
498    #[test]
499    fn test_field53a_validation() {
500        let field = Field53A::new(None, Some("1234567890".to_string()), "DEUTDEFF").unwrap();
501        let validation = field.validate();
502        assert!(validation.is_valid);
503        assert!(validation.errors.is_empty());
504    }
505
506    #[test]
507    fn test_field53a_display() {
508        let field1 = Field53A::new(None, None, "DEUTDEFF").unwrap();
509        assert_eq!(format!("{}", field1), "DEUTDEFF");
510
511        let field2 = Field53A::new(None, Some("1234567890".to_string()), "DEUTDEFF").unwrap();
512        assert_eq!(format!("{}", field2), "/1234567890 DEUTDEFF");
513    }
514
515    #[test]
516    fn test_field53a_description() {
517        let field1 = Field53A::new(None, None, "DEUTDEFF").unwrap();
518        assert_eq!(field1.description(), "Sender's Correspondent: DEUTDEFF");
519
520        let field2 = Field53A::new(None, Some("1234567890".to_string()), "DEUTDEFF").unwrap();
521        assert_eq!(
522            field2.description(),
523            "Sender's Correspondent: DEUTDEFF (1234567890)"
524        );
525    }
526
527    #[test]
528    fn test_field53a_case_normalization() {
529        let field = Field53A::new(None, None, "deutdeff").unwrap();
530        assert_eq!(field.bic(), "DEUTDEFF");
531    }
532}