swift_mt_message/fields/
field52a.rs

1use crate::common::BIC;
2use crate::{SwiftField, ValidationError, ValidationResult};
3use serde::{Deserialize, Serialize};
4
5/// # Field 52A: Ordering Institution
6///
7/// ## Overview
8/// Field 52A identifies the ordering institution in SWIFT payment messages. This field
9/// specifies the financial institution that is acting on behalf of the ordering customer
10/// (Field 50) to initiate the payment. It represents the first institution in the payment
11/// chain and is crucial for routing, settlement, and compliance purposes.
12///
13/// ## Format Specification
14/// **Format**: `[/1!a][/34x]4!a2!a2!c[3!c]`
15/// - **1!a**: 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/// DEUTDEFF500
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**: Single character qualifier for account number type
30/// - **Account Number**: Institution's account number for settlement
31/// - **BIC**: Bank Identifier Code uniquely identifying the institution
32///
33/// ## Usage Context
34/// Field 52A 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/// - **Payment routing**: Identifying the institution to route payment through
42/// - **Settlement**: Providing account information for settlement processes
43/// - **Compliance**: Meeting regulatory requirements for institution identification
44/// - **Correspondent banking**: Managing relationships between correspondent banks
45/// - **Risk management**: Assessing counterparty risk and limits
46///
47/// ## Examples
48/// ```text
49/// :52A:DEUTDEFFXXX
50/// └─── Deutsche Bank Frankfurt (no account information)
51///
52/// :52A:/C/1234567890
53/// CHASUS33XXX
54/// └─── JPMorgan Chase New York with checking account 1234567890
55///
56/// :52A:/A/GB12ABCD12345678901234
57/// ABCDEFGHJKL
58/// └─── Bank with account line indicator A and IBAN account
59///
60/// :52A:/S/SWIFT001234567890
61/// BNPAFRPPXXX
62/// └─── BNP Paribas with SWIFT account identifier
63/// ```
64///
65/// ## Account Line Indicators
66/// Common account line indicators include:
67/// - **A**: Account identifier (generic)
68/// - **B**: Beneficiary account
69/// - **C**: Checking account
70/// - **D**: Deposit account
71/// - **S**: SWIFT account identifier
72/// - **T**: Trust account
73///
74/// ## Validation Rules
75/// 1. **BIC format**: Must be valid 8 or 11 character BIC
76/// 2. **BIC structure**: 4!a2!a2!c[3!c] format required
77/// 3. **Account line indicator**: If present, exactly 1 character
78/// 4. **Account number**: If present, max 34 characters
79/// 5. **Character validation**: All components must use valid character sets
80///
81/// ## Network Validated Rules (SWIFT Standards)
82/// - BIC must be valid format and registered (Error: T27)
83/// - Account line indicator must be single character (Error: T12)
84/// - Account number cannot exceed 34 characters (Error: T15)
85/// - BIC country code must be valid ISO country code (Error: T28)
86/// - Characters must be from SWIFT character set (Error: T61)
87/// - Field 52A is mandatory in most payment message types (Error: C52)
88///
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
91pub struct Field52A {
92    /// Account line indicator (optional, 1 character)
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub account_line_indicator: Option<String>,
95    /// Account number (optional, up to 34 characters)
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub account_number: Option<String>,
98    /// BIC code (8 or 11 characters)
99    #[serde(flatten)]
100    pub bic: BIC,
101}
102
103impl Field52A {
104    /// Create a new Field52A with validation
105    pub fn new(
106        account_line_indicator: Option<String>,
107        account_number: Option<String>,
108        bic: impl Into<String>,
109    ) -> Result<Self, crate::ParseError> {
110        // Parse and validate BIC using the common structure
111        let bic = BIC::parse(&bic.into(), Some("52A"))?;
112
113        // Validate account line indicator if present
114        if let Some(ref indicator) = account_line_indicator {
115            if indicator.is_empty() {
116                return Err(crate::ParseError::InvalidFieldFormat {
117                    field_tag: "52A".to_string(),
118                    message: "Account line indicator cannot be empty".to_string(),
119                });
120            }
121            if indicator.len() != 1 {
122                return Err(crate::ParseError::InvalidFieldFormat {
123                    field_tag: "52A".to_string(),
124                    message: "Account line indicator must be exactly 1 character".to_string(),
125                });
126            }
127            if !indicator.chars().all(|c| c.is_ascii() && !c.is_control()) {
128                return Err(crate::ParseError::InvalidFieldFormat {
129                    field_tag: "52A".to_string(),
130                    message: "Account line indicator contains invalid characters".to_string(),
131                });
132            }
133        }
134
135        // Validate account number if present
136        if let Some(ref account) = account_number {
137            if account.is_empty() {
138                return Err(crate::ParseError::InvalidFieldFormat {
139                    field_tag: "52A".to_string(),
140                    message: "Account number cannot be empty".to_string(),
141                });
142            }
143            if account.len() > 34 {
144                return Err(crate::ParseError::InvalidFieldFormat {
145                    field_tag: "52A".to_string(),
146                    message: "Account number cannot exceed 34 characters".to_string(),
147                });
148            }
149            if !account.chars().all(|c| c.is_ascii() && !c.is_control()) {
150                return Err(crate::ParseError::InvalidFieldFormat {
151                    field_tag: "52A".to_string(),
152                    message: "Account number contains invalid characters".to_string(),
153                });
154            }
155        }
156
157        Ok(Field52A {
158            account_line_indicator,
159            account_number,
160            bic,
161        })
162    }
163
164    /// Get the account line indicator
165    pub fn account_line_indicator(&self) -> Option<&str> {
166        self.account_line_indicator.as_deref()
167    }
168
169    /// Get the account number
170    pub fn account_number(&self) -> Option<&str> {
171        self.account_number.as_deref()
172    }
173
174    /// Get the BIC code
175    pub fn bic(&self) -> &str {
176        self.bic.value()
177    }
178
179    /// Check if this is a full BIC (11 characters)
180    pub fn is_full_bic(&self) -> bool {
181        self.bic.is_full_bic()
182    }
183}
184
185impl SwiftField for Field52A {
186    fn parse(content: &str) -> crate::Result<Self> {
187        let content = content.trim();
188        if content.is_empty() {
189            return Err(crate::ParseError::InvalidFieldFormat {
190                field_tag: "52A".to_string(),
191                message: "Field content cannot be empty".to_string(),
192            });
193        }
194
195        let content = if let Some(stripped) = content.strip_prefix(":52A:") {
196            stripped // Remove ":52A:" prefix
197        } else if let Some(stripped) = content.strip_prefix("52A:") {
198            stripped // Remove "52A:" prefix
199        } else {
200            content
201        };
202
203        let mut account_line_indicator = None;
204        let mut account_number = None;
205        let mut bic_content = content;
206
207        // Check for account line indicator (starts with /)
208        if content.starts_with('/') {
209            let lines: Vec<&str> = content.lines().collect();
210            if !lines.is_empty() {
211                let first_line = lines[0];
212
213                if first_line.len() == 2 && first_line.starts_with('/') {
214                    // Only account line indicator: /X
215                    account_line_indicator = Some(first_line[1..].to_string());
216                    bic_content = if lines.len() > 1 { lines[1] } else { "" };
217                } else if first_line.len() > 2 && first_line.starts_with('/') {
218                    // Account line indicator + account number: /X/account or /account
219                    let parts: Vec<&str> = first_line[1..].split('/').collect();
220                    if parts.len() == 2 {
221                        // /X/account format
222                        account_line_indicator = Some(parts[0].to_string());
223                        account_number = Some(parts[1].to_string());
224                    } else {
225                        // /account format
226                        account_number = Some(parts[0].to_string());
227                    }
228                    bic_content = if lines.len() > 1 { lines[1] } else { "" };
229                }
230            }
231        }
232
233        let bic_str = bic_content.trim();
234        if bic_str.is_empty() {
235            return Err(crate::ParseError::InvalidFieldFormat {
236                field_tag: "52A".to_string(),
237                message: "BIC code is required".to_string(),
238            });
239        }
240
241        let bic = BIC::parse(bic_str, Some("52A"))?;
242
243        Ok(Field52A {
244            account_line_indicator,
245            account_number,
246            bic,
247        })
248    }
249
250    fn to_swift_string(&self) -> String {
251        let mut result = String::new();
252
253        if let Some(ref indicator) = self.account_line_indicator {
254            result.push('/');
255            result.push_str(indicator);
256        }
257
258        if let Some(ref account) = self.account_number {
259            result.push('/');
260            result.push_str(account);
261        }
262
263        if !result.is_empty() {
264            result.push('\n');
265        }
266        result.push_str(self.bic.value());
267
268        format!(":52A:{}", result)
269    }
270
271    fn validate(&self) -> ValidationResult {
272        let mut errors = Vec::new();
273
274        // Validate BIC format using the common BIC validation
275        let bic_validation = self.bic.validate();
276        if !bic_validation.is_valid {
277            errors.extend(bic_validation.errors);
278        }
279
280        // Additional validation for account components
281        if let Some(indicator) = &self.account_line_indicator {
282            if indicator.len() != 1 {
283                errors.push(ValidationError::LengthValidation {
284                    field_tag: "52A".to_string(),
285                    expected: "1 character".to_string(),
286                    actual: indicator.len(),
287                });
288            }
289        }
290
291        if let Some(account) = &self.account_number {
292            if account.len() > 34 {
293                errors.push(ValidationError::LengthValidation {
294                    field_tag: "52A".to_string(),
295                    expected: "max 34 characters".to_string(),
296                    actual: account.len(),
297                });
298            }
299        }
300
301        ValidationResult {
302            is_valid: errors.is_empty(),
303            errors,
304            warnings: Vec::new(),
305        }
306    }
307
308    fn format_spec() -> &'static str {
309        "[/1!a][/34x]4!a2!a2!c[3!c]"
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn test_field52a_creation_bic_only() {
319        let field = Field52A::new(None, None, "BNPAFRPPXXX").unwrap();
320        assert_eq!(field.bic(), "BNPAFRPPXXX");
321        assert!(field.account_number().is_none());
322        assert!(field.account_line_indicator().is_none());
323        assert!(field.is_full_bic());
324    }
325
326    #[test]
327    fn test_field52a_creation_with_account() {
328        let field = Field52A::new(None, Some("1234567890".to_string()), "BNPAFRPPXXX").unwrap();
329        assert_eq!(field.bic(), "BNPAFRPPXXX");
330        assert_eq!(field.account_number(), Some("1234567890"));
331        assert!(field.account_line_indicator().is_none());
332    }
333
334    #[test]
335    fn test_field52a_creation_with_account_line_indicator() {
336        let field = Field52A::new(
337            Some("A".to_string()),
338            Some("1234567890".to_string()),
339            "BNPAFRPPXXX",
340        )
341        .unwrap();
342        assert_eq!(field.bic(), "BNPAFRPPXXX");
343        assert_eq!(field.account_number(), Some("1234567890"));
344        assert_eq!(field.account_line_indicator(), Some("A"));
345    }
346
347    #[test]
348    fn test_field52a_parse_bic_only() {
349        let field = Field52A::parse("BNPAFRPPXXX").unwrap();
350        assert_eq!(field.bic(), "BNPAFRPPXXX");
351        assert!(field.account_number().is_none());
352    }
353
354    #[test]
355    fn test_field52a_parse_with_account() {
356        let field = Field52A::parse("/1234567890\nBNPAFRPPXXX").unwrap();
357        assert_eq!(field.bic(), "BNPAFRPPXXX");
358        assert_eq!(field.account_number(), Some("1234567890"));
359    }
360
361    #[test]
362    fn test_field52a_to_swift_string() {
363        let field = Field52A::new(
364            Some("A".to_string()),
365            Some("1234567890".to_string()),
366            "BNPAFRPPXXX",
367        )
368        .unwrap();
369        assert_eq!(field.to_swift_string(), ":52A:/A/1234567890\nBNPAFRPPXXX");
370    }
371}