swift_mt_message/fields/common/
bic_field.rs

1use serde::{Deserialize, Serialize};
2
3/// Generic BIC Field
4///
5/// Used for institution fields with BIC code and optional account information.
6/// Format: BIC[/ACCOUNT] where BIC is 8 or 11 characters and ACCOUNT is optional
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct GenericBicField {
9    /// BIC code (8 or 11 characters)
10    pub bic: BIC,
11    /// Account number (optional)
12    pub account: Option<String>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub struct BIC {
17    pub raw: String,
18
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub bank_code: Option<String>,
21
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub country_code: Option<String>,
24
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub location_code: Option<String>,
27
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub branch_code: Option<String>,
30}
31
32impl std::fmt::Display for BIC {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(f, "{}", self.raw)
35    }
36}
37
38impl std::str::FromStr for BIC {
39    type Err = String;
40
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        if s.len() < 8 {
43            return Err("BIC must be at least 8 characters".to_string());
44        }
45
46        if s.len() > 11 {
47            // For compatibility with test data, allow longer BIC codes but truncate to 11 characters
48            let truncated = &s[..11];
49            return BIC::from_str(truncated);
50        }
51
52        Ok(BIC {
53            raw: s.to_string(),
54            bank_code: if s.len() > 4 {
55                Some(s[0..4].to_string())
56            } else {
57                None
58            },
59            country_code: if s.len() > 6 {
60                Some(s[4..6].to_string())
61            } else {
62                None
63            },
64            location_code: if s.len() > 8 {
65                Some(s[6..8].to_string())
66            } else {
67                None
68            },
69            branch_code: if s.len() == 11 {
70                Some(s[8..11].to_string())
71            } else {
72                None
73            },
74        })
75    }
76}
77
78// Custom SwiftField implementation for GenericBicField
79impl crate::SwiftField for GenericBicField {
80    fn parse(value: &str) -> crate::Result<Self> {
81        let content = value.trim();
82
83        // Remove field tag prefix if present
84        let content = Self::remove_field_tag_prefix(content);
85
86        // Split by '/' to separate BIC and account
87        if let Some(slash_pos) = content.find('/') {
88            // Format: BIC/ACCOUNT
89            let bic_str = &content[..slash_pos];
90            let account_str = &content[slash_pos + 1..];
91
92            let bic =
93                bic_str
94                    .parse()
95                    .map_err(|e: String| crate::ParseError::InvalidFieldFormat {
96                        field_tag: "GENERICBICFIELD".to_string(),
97                        message: format!("Failed to parse BIC: {e}"),
98                    })?;
99
100            let account = if account_str.is_empty() {
101                None
102            } else {
103                Some(account_str.to_string())
104            };
105
106            Ok(GenericBicField { bic, account })
107        } else {
108            // Format: BIC only
109            let bic =
110                content
111                    .parse()
112                    .map_err(|e: String| crate::ParseError::InvalidFieldFormat {
113                        field_tag: "GENERICBICFIELD".to_string(),
114                        message: format!("Failed to parse BIC: {e}"),
115                    })?;
116
117            Ok(GenericBicField { bic, account: None })
118        }
119    }
120
121    fn to_swift_string(&self) -> String {
122        if let Some(ref account) = self.account {
123            format!("{}/{}", self.bic, account)
124        } else {
125            self.bic.to_string()
126        }
127    }
128
129    fn validate(&self) -> crate::ValidationResult {
130        let mut errors = Vec::new();
131        let mut warnings = Vec::new();
132
133        // Validate BIC length (more lenient validation)
134        if self.bic.raw.len() != 8 && self.bic.raw.len() != 11 {
135            errors.push(crate::ValidationError::FormatValidation {
136                field_tag: "GENERICBICFIELD".to_string(),
137                message: format!("BIC must be 8 or 11 characters, got {}", self.bic.raw.len()),
138            });
139        }
140
141        // Validate account if present
142        if let Some(ref account) = self.account {
143            if account.len() > 35 {
144                errors.push(crate::ValidationError::LengthValidation {
145                    field_tag: "GENERICBICFIELD".to_string(),
146                    expected: "≤35".to_string(),
147                    actual: account.len(),
148                });
149            }
150            if account.is_empty() {
151                warnings.push("Empty account number provided".to_string());
152            }
153        }
154
155        crate::ValidationResult {
156            is_valid: errors.is_empty(),
157            errors,
158            warnings,
159        }
160    }
161
162    fn format_spec() -> &'static str {
163        "4!a2!a2!c[3!c][/35x]"
164    }
165}
166
167impl GenericBicField {
168    /// Remove field tag prefix using generic regex pattern
169    /// Handles patterns like ":52A:", "52A:", ":56A:", etc.
170    fn remove_field_tag_prefix(value: &str) -> &str {
171        use std::sync::OnceLock;
172        static FIELD_TAG_REGEX: OnceLock<regex::Regex> = OnceLock::new();
173
174        let regex = FIELD_TAG_REGEX.get_or_init(|| {
175            // Pattern matches: optional colon + field identifier + mandatory colon
176            // Field identifier: 1-3 digits optionally followed by 1-2 letters
177            regex::Regex::new(r"^:?([0-9]{1,3}[A-Z]{0,2}):").unwrap()
178        });
179
180        if let Some(captures) = regex.find(value) {
181            &value[captures.end()..]
182        } else {
183            value
184        }
185    }
186}
187
188impl std::fmt::Display for GenericBicField {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        if let Some(ref account) = self.account {
191            write!(f, "{}/{}", self.bic, account)
192        } else {
193            write!(f, "{}", self.bic)
194        }
195    }
196}