swift_mt_message/fields/
field51a.rs

1use crate::common::BIC;
2use crate::{SwiftField, ValidationError, ValidationResult};
3use serde::{Deserialize, Serialize};
4
5/// # Field 51A: Sending Institution
6///
7/// ## Overview
8/// Field 51A identifies the sending institution in SWIFT payment messages using a BIC code.
9/// This field specifies the financial institution that is sending the payment message,
10/// typically used in correspondent banking arrangements and institutional transfers.
11/// The sending institution is distinct from the ordering institution and represents
12/// the actual message sender in the SWIFT network, providing crucial information for
13/// message routing, settlement processing, and regulatory compliance.
14///
15/// ## Format Specification
16/// **Format**: `[/1!a][/34x]4!a2!a2!c[3!c]`
17/// - **1!a**: Optional account line indicator (1 character)
18/// - **34x**: Optional account number (up to 34 characters)
19/// - **4!a2!a2!c[3!c]**: BIC code (8 or 11 characters)
20///
21/// ### BIC Structure
22/// ```text
23/// CHASUS33XXX
24/// ││││││││└┴┴─ Branch Code (3 characters, optional)
25/// ││││││└┴──── Location Code (2 characters)
26/// ││││└┴────── Country Code (2 letters)
27/// └┴┴┴──────── Bank Code (4 letters)
28/// ```
29///
30/// ## Field Components
31/// - **Account Line Indicator**: Optional qualifier for account type or purpose
32/// - **Account Number**: Institution's account number for settlement
33/// - **BIC**: Bank Identifier Code uniquely identifying the sending institution
34///
35/// ## Usage Context
36/// Field 51A is used in:
37/// - **MT103**: Single Customer Credit Transfer (optional)
38/// - **MT103.REMIT**: Single Customer Credit Transfer with Remittance (optional)
39/// - **MT200**: Financial Institution Transfer
40/// - **MT202**: General Financial Institution Transfer
41/// - **MT202COV**: Cover for customer credit transfer
42///
43/// ### Business Applications
44/// - **Correspondent banking**: Identifying the actual message sender
45/// - **Settlement**: Providing account information for settlement processes
46/// - **Compliance**: Meeting regulatory requirements for sender identification
47/// - **Audit trails**: Maintaining clear sender identification records
48/// - **Message routing**: Supporting proper SWIFT network routing
49/// - **Risk management**: Enabling counterparty risk assessment
50///
51/// ## MT103 Variant Support
52/// - **MT103 Core**: Optional field
53/// - **MT103.STP**: **Not allowed** (STP compliance restriction)
54/// - **MT103.REMIT**: Optional field
55///
56/// ## Examples
57/// ```rust
58/// use swift_mt_message::fields::Field51A;
59///
60/// // BIC only
61/// let field = Field51A::new(None, None, "CHASUS33XXX").unwrap();
62///
63/// // With account number
64/// let field = Field51A::new(None, Some("1234567890".to_string()), "DEUTDEFF500").unwrap();
65///
66/// // With account line indicator and account number
67/// let field = Field51A::new(Some("C".to_string()), Some("1234567890".to_string()), "HSBCHKHH").unwrap();
68/// ```
69///
70/// ## Account Line Indicators
71/// Common account line indicators include:
72/// - **A**: Account identifier (generic)
73/// - **B**: Beneficiary account
74/// - **C**: Checking account
75/// - **D**: Deposit account
76/// - **S**: SWIFT account identifier
77/// - **T**: Trust account
78/// - **N**: Nostro account
79/// - **V**: Vostro account
80///
81/// ## Validation Rules
82/// 1. **BIC format**: Must be valid 8 or 11 character BIC code
83/// 2. **BIC structure**: 4!a2!a2!c[3!c] format required
84/// 3. **Bank code**: Must be 4 alphabetic characters
85/// 4. **Country code**: Must be 2 alphabetic characters
86/// 5. **Location code**: Must be 2 alphanumeric characters
87/// 6. **Branch code**: Must be 3 alphanumeric characters (if present)
88/// 7. **Account line indicator**: If present, exactly 1 character
89/// 8. **Account number**: If present, max 34 characters
90/// 9. **Character validation**: All components must use valid character sets
91///
92/// ## Network Validated Rules (SWIFT Standards)
93/// - BIC must be valid format and registered (Error: T27)
94/// - BIC format must comply with ISO 13616 standards (Error: T11)
95/// - Account line indicator must be single character (Error: T12)
96/// - Account number cannot exceed 34 characters (Error: T14)
97/// - Bank code must be alphabetic only (Error: T15)
98/// - Country code must be valid ISO 3166-1 code (Error: T16)
99/// - Location code must be alphanumeric (Error: T17)
100/// - Branch code must be alphanumeric if present (Error: T18)
101/// - Characters must be from SWIFT character set (Error: T61)
102/// - Field 51A not allowed in MT103.STP messages (Error: C51)
103/// - Field 51A optional in MT103 Core and MT103.REMIT (Warning: W51)
104///
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107pub struct Field51A {
108    /// Account line indicator (optional, 1 character)
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub account_line_indicator: Option<String>,
111
112    /// Account number (optional, up to 34 characters)
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub account_number: Option<String>,
115
116    /// BIC code (8 or 11 characters)
117    #[serde(flatten)]
118    pub bic: BIC,
119}
120
121impl SwiftField for Field51A {
122    fn parse(value: &str) -> Result<Self, crate::ParseError> {
123        let content = if let Some(stripped) = value.strip_prefix(":51A:") {
124            stripped
125        } else if let Some(stripped) = value.strip_prefix("51A:") {
126            stripped
127        } else {
128            value
129        };
130
131        if content.is_empty() {
132            return Err(crate::ParseError::InvalidFieldFormat {
133                field_tag: "51A".to_string(),
134                message: "Field content cannot be empty".to_string(),
135            });
136        }
137
138        let lines: Vec<&str> = content.lines().collect();
139
140        if lines.is_empty() {
141            return Err(crate::ParseError::InvalidFieldFormat {
142                field_tag: "51A".to_string(),
143                message: "No content found".to_string(),
144            });
145        }
146
147        let mut account_line_indicator = None;
148        let mut account_number = None;
149        let mut bic_line = lines[0];
150
151        // Check if first line contains account information
152        if bic_line.starts_with('/') {
153            // Parse account information from first line
154            let account_part = &bic_line[1..]; // Remove leading '/'
155
156            if let Some(second_slash) = account_part.find('/') {
157                // Format: /indicator/account
158                account_line_indicator = Some(account_part[..second_slash].to_string());
159                account_number = Some(account_part[second_slash + 1..].to_string());
160            } else {
161                // Format: /account
162                account_number = Some(account_part.to_string());
163            }
164
165            // BIC should be on second line
166            if lines.len() < 2 {
167                return Err(crate::ParseError::InvalidFieldFormat {
168                    field_tag: "51A".to_string(),
169                    message: "BIC code missing after account information".to_string(),
170                });
171            }
172            bic_line = lines[1];
173        }
174
175        let bic = BIC::parse(bic_line.trim(), Some("51A"))?;
176
177        Ok(Self {
178            account_line_indicator,
179            account_number,
180            bic,
181        })
182    }
183
184    fn to_swift_string(&self) -> String {
185        let mut result = ":51A:".to_string();
186
187        if self.account_line_indicator.is_some() || self.account_number.is_some() {
188            result.push('/');
189
190            if let Some(indicator) = &self.account_line_indicator {
191                result.push_str(indicator);
192                result.push('/');
193            }
194
195            if let Some(account) = &self.account_number {
196                result.push_str(account);
197            }
198
199            result.push('\n');
200        }
201
202        result.push_str(self.bic.value());
203        result
204    }
205
206    fn validate(&self) -> ValidationResult {
207        let mut errors = Vec::new();
208
209        // Validate BIC format using the common BIC validation
210        let bic_validation = self.bic.validate();
211        if !bic_validation.is_valid {
212            errors.extend(bic_validation.errors);
213        }
214
215        // Validate account line indicator if present
216        if let Some(indicator) = &self.account_line_indicator {
217            if indicator.len() != 1 {
218                errors.push(ValidationError::LengthValidation {
219                    field_tag: "51A".to_string(),
220                    expected: "1 character".to_string(),
221                    actual: indicator.len(),
222                });
223            }
224
225            if !indicator.chars().all(|c| c.is_ascii_alphanumeric()) {
226                errors.push(ValidationError::FormatValidation {
227                    field_tag: "51A".to_string(),
228                    message: "Account line indicator must be alphanumeric".to_string(),
229                });
230            }
231        }
232
233        // Validate account number if present
234        if let Some(account) = &self.account_number {
235            if account.len() > 34 {
236                errors.push(ValidationError::LengthValidation {
237                    field_tag: "51A".to_string(),
238                    expected: "max 34 characters".to_string(),
239                    actual: account.len(),
240                });
241            }
242
243            if !account.chars().all(|c| c.is_ascii() && !c.is_control()) {
244                errors.push(ValidationError::FormatValidation {
245                    field_tag: "51A".to_string(),
246                    message: "Account number contains invalid characters".to_string(),
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        "[/1!a][/34x]4!a2!a2!c[3!c]"
260    }
261}
262
263impl Field51A {
264    /// Create a new Field51A with validation
265    ///
266    /// # Arguments
267    /// * `account_line_indicator` - Optional account line indicator (1 character)
268    /// * `account_number` - Optional account number (up to 34 characters)
269    /// * `bic` - BIC code (8 or 11 characters)
270    ///
271    /// # Examples
272    /// ```rust
273    /// use swift_mt_message::fields::Field51A;
274    ///
275    /// // BIC only
276    /// let field = Field51A::new(None, None, "CHASUS33XXX").unwrap();
277    ///
278    /// // With account number
279    /// let field = Field51A::new(None, Some("1234567890".to_string()), "DEUTDEFF500").unwrap();
280    ///
281    /// // With account line indicator and account number
282    /// let field = Field51A::new(Some("C".to_string()), Some("1234567890".to_string()), "HSBCHKHH").unwrap();
283    /// ```
284    pub fn new(
285        account_line_indicator: Option<String>,
286        account_number: Option<String>,
287        bic: impl Into<String>,
288    ) -> crate::Result<Self> {
289        let account_line_indicator = account_line_indicator.map(|s| s.trim().to_string());
290        let account_number = account_number.map(|s| s.trim().to_string());
291
292        // Parse and validate BIC using the common structure
293        let bic = BIC::parse(&bic.into(), Some("51A"))?;
294
295        // Validate account line indicator if present
296        if let Some(ref indicator) = account_line_indicator {
297            if indicator.len() != 1 {
298                return Err(crate::ParseError::InvalidFieldFormat {
299                    field_tag: "51A".to_string(),
300                    message: "Account line indicator must be exactly 1 character".to_string(),
301                });
302            }
303
304            if !indicator.chars().all(|c| c.is_ascii_alphanumeric()) {
305                return Err(crate::ParseError::InvalidFieldFormat {
306                    field_tag: "51A".to_string(),
307                    message: "Account line indicator must be alphanumeric".to_string(),
308                });
309            }
310        }
311
312        // Validate account number if present
313        if let Some(ref account) = account_number {
314            if account.len() > 34 {
315                return Err(crate::ParseError::InvalidFieldFormat {
316                    field_tag: "51A".to_string(),
317                    message: "Account number cannot exceed 34 characters".to_string(),
318                });
319            }
320
321            if !account.chars().all(|c| c.is_ascii() && !c.is_control()) {
322                return Err(crate::ParseError::InvalidFieldFormat {
323                    field_tag: "51A".to_string(),
324                    message: "Account number contains invalid characters".to_string(),
325                });
326            }
327        }
328
329        Ok(Field51A {
330            account_line_indicator,
331            account_number,
332            bic,
333        })
334    }
335
336    /// Get the BIC code
337    pub fn bic(&self) -> &str {
338        self.bic.value()
339    }
340
341    /// Get the account line indicator if present
342    pub fn account_line_indicator(&self) -> Option<&str> {
343        self.account_line_indicator.as_deref()
344    }
345
346    /// Get the account number if present
347    pub fn account_number(&self) -> Option<&str> {
348        self.account_number.as_deref()
349    }
350
351    /// Check if this field is allowed in MT103.STP messages
352    ///
353    /// Field 51A is NOT allowed in MT103.STP messages according to SWIFT standards
354    pub fn is_stp_allowed(&self) -> bool {
355        false
356    }
357
358    /// Get bank code from BIC
359    pub fn bank_code(&self) -> &str {
360        self.bic.bank_code()
361    }
362
363    /// Get country code from BIC
364    pub fn country_code(&self) -> &str {
365        self.bic.country_code()
366    }
367
368    /// Get location code from BIC
369    pub fn location_code(&self) -> &str {
370        self.bic.location_code()
371    }
372
373    /// Get branch code from BIC if present
374    pub fn branch_code(&self) -> Option<&str> {
375        self.bic.branch_code()
376    }
377}
378
379impl std::fmt::Display for Field51A {
380    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381        if let Some(indicator) = &self.account_line_indicator {
382            if let Some(account) = &self.account_number {
383                write!(f, "/{}/{} {}", indicator, account, self.bic)
384            } else {
385                write!(f, "/{} {}", indicator, self.bic)
386            }
387        } else if let Some(account) = &self.account_number {
388            write!(f, "/{} {}", account, self.bic)
389        } else {
390            write!(f, "{}", self.bic)
391        }
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    #[test]
400    fn test_field51a_creation() {
401        let field = Field51A::new(None, None, "CHASUS33XXX").unwrap();
402        assert_eq!(field.bic(), "CHASUS33XXX");
403        assert_eq!(field.account_line_indicator(), None);
404        assert_eq!(field.account_number(), None);
405    }
406
407    #[test]
408    fn test_field51a_with_account() {
409        let field = Field51A::new(None, Some("1234567890".to_string()), "DEUTDEFF500").unwrap();
410        assert_eq!(field.account_number(), Some("1234567890"));
411        assert_eq!(field.bic(), "DEUTDEFF500");
412        assert!(field.account_line_indicator().is_none());
413    }
414
415    #[test]
416    fn test_field51a_with_indicator_and_account() {
417        let field = Field51A::new(
418            Some("C".to_string()),
419            Some("1234567890".to_string()),
420            "HSBCHKHH",
421        )
422        .unwrap();
423        assert_eq!(field.account_line_indicator(), Some("C"));
424        assert_eq!(field.account_number(), Some("1234567890"));
425        assert_eq!(field.bic(), "HSBCHKHH");
426    }
427
428    #[test]
429    fn test_field51a_parse() {
430        let field = Field51A::parse("CHASUS33XXX").unwrap();
431        assert_eq!(field.bic(), "CHASUS33XXX");
432
433        let field = Field51A::parse("/1234567890\nDEUTDEFF500").unwrap();
434        assert_eq!(field.bic(), "DEUTDEFF500");
435        assert_eq!(field.account_number(), Some("1234567890"));
436
437        let field = Field51A::parse("/C/1234567890\nHSBCHKHH").unwrap();
438        assert_eq!(field.bic(), "HSBCHKHH");
439        assert_eq!(field.account_line_indicator(), Some("C"));
440        assert_eq!(field.account_number(), Some("1234567890"));
441    }
442
443    #[test]
444    fn test_field51a_parse_with_prefix() {
445        let field = Field51A::parse(":51A:CHASUS33XXX").unwrap();
446        assert_eq!(field.bic(), "CHASUS33XXX");
447
448        let field = Field51A::parse("51A:/1234567890\nDEUTDEFF500").unwrap();
449        assert_eq!(field.bic(), "DEUTDEFF500");
450        assert_eq!(field.account_number(), Some("1234567890"));
451    }
452
453    #[test]
454    fn test_field51a_invalid_bic() {
455        let result = Field51A::new(None, None, "INVALID");
456        assert!(result.is_err());
457
458        let result = Field51A::new(None, None, "CHAS1233");
459        assert!(result.is_err());
460    }
461
462    #[test]
463    fn test_field51a_invalid_account() {
464        let result = Field51A::new(None, Some("a".repeat(35)), "CHASUS33XXX");
465        assert!(result.is_err());
466
467        let result = Field51A::new(Some("AB".to_string()), None, "CHASUS33XXX");
468        assert!(result.is_err());
469    }
470
471    #[test]
472    fn test_field51a_to_swift_string() {
473        let field = Field51A::new(None, None, "CHASUS33XXX").unwrap();
474        assert_eq!(field.to_swift_string(), ":51A:CHASUS33XXX");
475
476        let field = Field51A::new(None, Some("1234567890".to_string()), "DEUTDEFF500").unwrap();
477        assert_eq!(field.to_swift_string(), ":51A:/1234567890\nDEUTDEFF500");
478
479        let field = Field51A::new(
480            Some("C".to_string()),
481            Some("1234567890".to_string()),
482            "HSBCHKHH",
483        )
484        .unwrap();
485        assert_eq!(field.to_swift_string(), ":51A:/C/1234567890\nHSBCHKHH");
486    }
487
488    #[test]
489    fn test_field51a_validation() {
490        let field = Field51A::new(None, None, "CHASUS33XXX").unwrap();
491        let result = field.validate();
492        assert!(result.is_valid);
493
494        let invalid_field = Field51A {
495            account_line_indicator: None,
496            account_number: None,
497            bic: BIC::new_unchecked("INVALID"),
498        };
499        let result = invalid_field.validate();
500        assert!(!result.is_valid);
501    }
502
503    #[test]
504    fn test_field51a_stp_compliance() {
505        let field = Field51A::new(None, None, "CHASUS33XXX").unwrap();
506        assert!(!field.is_stp_allowed()); // Field 51A not allowed in STP
507    }
508
509    #[test]
510    fn test_field51a_bic_components() {
511        let field = Field51A::new(None, None, "CHASUS33XXX").unwrap();
512        assert_eq!(field.bank_code(), "CHAS");
513        assert_eq!(field.country_code(), "US");
514        assert_eq!(field.location_code(), "33");
515        assert_eq!(field.branch_code(), Some("XXX"));
516
517        let field = Field51A::new(None, None, "DEUTDEFF").unwrap();
518        assert_eq!(field.bank_code(), "DEUT");
519        assert_eq!(field.country_code(), "DE");
520        assert_eq!(field.location_code(), "FF");
521        assert_eq!(field.branch_code(), None);
522    }
523
524    #[test]
525    fn test_field51a_display() {
526        let field = Field51A::new(None, None, "CHASUS33XXX").unwrap();
527        assert_eq!(format!("{}", field), "CHASUS33XXX");
528
529        let field = Field51A::new(None, Some("1234567890".to_string()), "DEUTDEFF500").unwrap();
530        assert_eq!(format!("{}", field), "/1234567890 DEUTDEFF500");
531
532        let field = Field51A::new(
533            Some("C".to_string()),
534            Some("1234567890".to_string()),
535            "HSBCHKHH",
536        )
537        .unwrap();
538        assert_eq!(format!("{}", field), "/C/1234567890 HSBCHKHH");
539    }
540
541    #[test]
542    fn test_field51a_format_spec() {
543        assert_eq!(Field51A::format_spec(), "[/1!a][/34x]4!a2!a2!c[3!c]");
544    }
545}