swift_mt_message/fields/
field51a.rs

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