swift_mt_message/fields/
field55a.rs

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