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