swift_mt_message/fields/
field54a.rs

1use crate::common::BIC;
2use crate::{SwiftField, ValidationError, ValidationResult};
3use serde::{Deserialize, Serialize};
4
5/// # Field 54A: Receiver's Correspondent
6///
7/// ## Overview
8/// Field 54A identifies the receiver's correspondent institution in SWIFT payment messages.
9/// This field specifies the financial institution that acts as a correspondent for the
10/// message receiver, facilitating the payment routing and settlement process on the
11/// receiving side. The correspondent relationship is essential for cross-border payments
12/// and international correspondent banking arrangements.
13///
14/// ## Format Specification
15/// **Format**: `[/34x]4!a2!a2!c[3!c]`
16/// - **34x**: Optional account number (up to 34 characters)
17/// - **4!a2!a2!c[3!c]**: BIC code (8 or 11 characters)
18///   - **4!a**: Bank code (4 alphabetic characters)
19///   - **2!a**: Country code (2 alphabetic characters, ISO 3166-1)
20///   - **2!c**: Location code (2 alphanumeric characters)
21///   - **3!c**: Optional branch code (3 alphanumeric characters)
22///
23/// ## Structure
24/// ```text
25/// /1234567890123456789012345678901234
26/// DEUTDEFF500
27/// │       │││
28/// │       │└┴─ Branch code (optional, 500)
29/// │       └┴── Location code (2 chars, FF)
30/// │     └┴──── Country code (2 chars, DE)
31/// │ └┴┴┴────── Bank code (4 chars, DEUT)
32/// └─────────── Account number (optional)
33/// ```
34///
35/// ## Field Components
36/// - **Account Number**: Institution's account for settlement (optional)
37/// - **BIC Code**: Business Identifier Code for institution identification
38/// - **Bank Code**: 4-letter code identifying the bank
39/// - **Country Code**: 2-letter ISO country code
40/// - **Location Code**: 2-character location identifier
41/// - **Branch Code**: 3-character branch identifier (optional)
42///
43/// ## Usage Context
44/// Field 54A is used in:
45/// - **MT103**: Single Customer Credit Transfer
46/// - **MT200**: Financial Institution Transfer
47/// - **MT202**: General Financial Institution Transfer
48/// - **MT202COV**: Cover for customer credit transfer
49/// - **MT205**: Financial Institution Transfer for its own account
50///
51/// ### Business Applications
52/// - **Correspondent banking**: Establishing correspondent relationships
53/// - **Payment routing**: Directing payments through correspondent networks
54/// - **Settlement coordination**: Managing settlement between institutions
55/// - **Cross-border payments**: Facilitating international transfers
56/// - **Liquidity management**: Optimizing correspondent account balances
57/// - **Risk management**: Identifying counterparty institutions
58///
59/// ## Examples
60/// ```text
61/// :54A:CHASUS33
62/// └─── JPMorgan Chase Bank, New York (BIC only)
63///
64/// :54A:/1234567890123456789012345678901234
65/// DEUTDEFF500
66/// └─── Deutsche Bank AG, Frankfurt with account number
67///
68/// :54A:BARCGB22
69/// └─── Barclays Bank PLC, London (8-character BIC)
70///
71/// :54A:/CORRESPONDENT001
72/// BNPAFRPP
73/// └─── BNP Paribas, Paris with correspondent account
74/// ```
75///
76/// ## BIC Code Structure
77/// - **8-character BIC**: BANKCCLL (Bank-Country-Location)
78/// - **11-character BIC**: BANKCCLLBBB (Bank-Country-Location-Branch)
79/// - **Bank Code**: 4 letters identifying the institution
80/// - **Country Code**: 2 letters (ISO 3166-1 alpha-2)
81/// - **Location Code**: 2 alphanumeric characters
82/// - **Branch Code**: 3 alphanumeric characters (optional)
83///
84/// ## Account Number Guidelines
85/// - **Format**: Up to 34 alphanumeric characters
86/// - **Content**: Correspondent account number or identifier
87/// - **Usage**: When specific account designation is required
88/// - **Omission**: When only institution identification is needed
89///
90/// ## Validation Rules
91/// 1. **BIC format**: Must be valid 8 or 11 character BIC code
92/// 2. **Bank code**: Must be 4 alphabetic characters
93/// 3. **Country code**: Must be 2 alphabetic characters
94/// 4. **Location code**: Must be 2 alphanumeric characters
95/// 5. **Branch code**: Must be 3 alphanumeric characters (if present)
96/// 6. **Account number**: Maximum 34 characters (if present)
97/// 7. **Character validation**: All components must be printable ASCII
98///
99/// ## Network Validated Rules (SWIFT Standards)
100/// - BIC must be valid and registered in SWIFT network (Error: T10)
101/// - BIC format must comply with ISO 13616 standards (Error: T11)
102/// - Account number cannot exceed 34 characters (Error: T14)
103/// - Bank code must be alphabetic only (Error: T15)
104/// - Country code must be valid ISO 3166-1 code (Error: T16)
105/// - Location code must be alphanumeric (Error: T17)
106/// - Branch code must be alphanumeric if present (Error: T18)
107/// - Field 54A alternative to 54B/54D (Error: C54)
108///
109
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
111pub struct Field54A {
112    /// Account line indicator (optional, 1 character)
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub account_line_indicator: Option<String>,
115    /// Account number (optional, up to 34 characters)
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub account_number: Option<String>,
118    /// BIC code (8 or 11 characters)
119    #[serde(flatten)]
120    pub bic: BIC,
121}
122
123impl Field54A {
124    /// Create a new Field54A with validation
125    pub fn new(
126        account_line_indicator: Option<String>,
127        account_number: Option<String>,
128        bic: impl Into<String>,
129    ) -> Result<Self, crate::ParseError> {
130        let bic = bic.into().to_uppercase();
131
132        // Validate account line indicator if present
133        if let Some(ref indicator) = account_line_indicator {
134            if indicator.is_empty() {
135                return Err(crate::ParseError::InvalidFieldFormat {
136                    field_tag: "54A".to_string(),
137                    message: "Account line indicator cannot be empty if specified".to_string(),
138                });
139            }
140
141            if indicator.len() != 1 {
142                return Err(crate::ParseError::InvalidFieldFormat {
143                    field_tag: "54A".to_string(),
144                    message: "Account line indicator must be exactly 1 character".to_string(),
145                });
146            }
147
148            if !indicator.chars().all(|c| c.is_ascii() && !c.is_control()) {
149                return Err(crate::ParseError::InvalidFieldFormat {
150                    field_tag: "54A".to_string(),
151                    message: "Account line indicator contains invalid characters".to_string(),
152                });
153            }
154        }
155
156        // Validate account number if present
157        if let Some(ref account) = account_number {
158            if account.is_empty() {
159                return Err(crate::ParseError::InvalidFieldFormat {
160                    field_tag: "54A".to_string(),
161                    message: "Account number cannot be empty if specified".to_string(),
162                });
163            }
164
165            if account.len() > 34 {
166                return Err(crate::ParseError::InvalidFieldFormat {
167                    field_tag: "54A".to_string(),
168                    message: "Account number too long (max 34 characters)".to_string(),
169                });
170            }
171
172            if !account.chars().all(|c| c.is_ascii() && !c.is_control()) {
173                return Err(crate::ParseError::InvalidFieldFormat {
174                    field_tag: "54A".to_string(),
175                    message: "Account number contains invalid characters".to_string(),
176                });
177            }
178        }
179
180        // Parse and validate BIC using the common structure
181        let parsed_bic = BIC::parse(&bic, Some("54A"))?;
182
183        Ok(Field54A {
184            account_line_indicator,
185            account_number,
186            bic: parsed_bic,
187        })
188    }
189
190    /// Get the account line indicator
191    pub fn account_line_indicator(&self) -> Option<&str> {
192        self.account_line_indicator.as_deref()
193    }
194
195    /// Get the account number
196    pub fn account_number(&self) -> Option<&str> {
197        self.account_number.as_deref()
198    }
199
200    /// Get the BIC code
201    pub fn bic(&self) -> &str {
202        self.bic.value()
203    }
204
205    /// Check if this is a full BIC (11 characters) or short BIC (8 characters)
206    pub fn is_full_bic(&self) -> bool {
207        self.bic.is_full_bic()
208    }
209
210    /// Get human-readable description
211    pub fn description(&self) -> String {
212        match &self.account_number {
213            Some(account) => format!("Receiver's Correspondent: {} ({})", self.bic, account),
214            None => format!("Receiver's Correspondent: {}", self.bic),
215        }
216    }
217}
218
219impl SwiftField for Field54A {
220    fn parse(value: &str) -> Result<Self, crate::ParseError> {
221        let content = if let Some(stripped) = value.strip_prefix(":54A:") {
222            stripped
223        } else if let Some(stripped) = value.strip_prefix("54A:") {
224            stripped
225        } else {
226            value
227        };
228
229        let content = content.trim();
230
231        if content.is_empty() {
232            return Err(crate::ParseError::InvalidFieldFormat {
233                field_tag: "54A".to_string(),
234                message: "Field content cannot be empty".to_string(),
235            });
236        }
237
238        let mut account_line_indicator = None;
239        let mut account_number = None;
240        let bic;
241
242        if content.starts_with('/') {
243            let lines: Vec<&str> = content.lines().collect();
244
245            if lines.len() == 1 {
246                let parts: Vec<&str> = lines[0].splitn(2, ' ').collect();
247                if parts.len() == 2 {
248                    account_line_indicator = Some(parts[0][1..].to_string());
249                    account_number = Some(parts[1].to_string());
250                    bic = parts[1].to_string();
251                } else {
252                    return Err(crate::ParseError::InvalidFieldFormat {
253                        field_tag: "54A".to_string(),
254                        message: "Invalid format: expected account and BIC".to_string(),
255                    });
256                }
257            } else if lines.len() == 2 {
258                account_line_indicator = Some(lines[0][1..].to_string());
259                account_number = Some(lines[1].to_string());
260                bic = lines[1].to_string();
261            } else {
262                return Err(crate::ParseError::InvalidFieldFormat {
263                    field_tag: "54A".to_string(),
264                    message: "Invalid format: too many lines".to_string(),
265                });
266            }
267        } else {
268            bic = content.to_string();
269        }
270
271        let parsed_bic = BIC::parse(&bic, Some("54A"))?;
272
273        Ok(Field54A {
274            account_line_indicator,
275            account_number,
276            bic: parsed_bic,
277        })
278    }
279
280    fn to_swift_string(&self) -> String {
281        match &self.account_number {
282            Some(account) => format!(":54A:/{}\n{}", account, self.bic.value()),
283            None => format!(":54A:{}", self.bic.value()),
284        }
285    }
286
287    fn validate(&self) -> ValidationResult {
288        let mut errors = Vec::new();
289
290        if let Some(ref account) = self.account_number {
291            if account.is_empty() {
292                errors.push(ValidationError::ValueValidation {
293                    field_tag: "54A".to_string(),
294                    message: "Account number cannot be empty if specified".to_string(),
295                });
296            }
297
298            if account.len() > 34 {
299                errors.push(ValidationError::LengthValidation {
300                    field_tag: "54A".to_string(),
301                    expected: "max 34 characters".to_string(),
302                    actual: account.len(),
303                });
304            }
305        }
306
307        // Validate BIC format using the common BIC validation
308        let bic_validation = self.bic.validate();
309        if !bic_validation.is_valid {
310            errors.extend(bic_validation.errors);
311        }
312
313        ValidationResult {
314            is_valid: errors.is_empty(),
315            errors,
316            warnings: Vec::new(),
317        }
318    }
319
320    fn format_spec() -> &'static str {
321        "[/34x]4!a2!a2!c[3!c]"
322    }
323}
324
325impl std::fmt::Display for Field54A {
326    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327        match &self.account_number {
328            Some(account) => write!(f, "/{} {}", account, self.bic.value()),
329            None => write!(f, "{}", self.bic.value()),
330        }
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn test_field54a_creation() {
340        let field = Field54A::new(None, None, "DEUTDEFF").unwrap();
341        assert_eq!(field.bic(), "DEUTDEFF");
342        assert!(field.account_number().is_none());
343    }
344
345    #[test]
346    fn test_field54a_with_account() {
347        let field = Field54A::new(None, Some("1234567890".to_string()), "DEUTDEFF500").unwrap();
348        assert_eq!(field.bic(), "DEUTDEFF500");
349        assert_eq!(field.account_number(), Some("1234567890"));
350        assert!(field.is_full_bic());
351    }
352
353    #[test]
354    fn test_field54a_parse() {
355        let field = Field54A::parse("CHASUS33").unwrap();
356        assert_eq!(field.bic(), "CHASUS33");
357    }
358
359    #[test]
360    fn test_field54a_to_swift_string() {
361        let field = Field54A::new(None, None, "DEUTDEFF").unwrap();
362        assert_eq!(field.to_swift_string(), ":54A:DEUTDEFF");
363    }
364
365    #[test]
366    fn test_field54a_validation() {
367        let field = Field54A::new(None, Some("1234567890".to_string()), "DEUTDEFF").unwrap();
368        let validation = field.validate();
369        assert!(validation.is_valid);
370    }
371}