swift_mt_message/fields/
field57b.rs

1use crate::{SwiftField, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 57B: Account With Institution (Option B)
5///
6/// ## Overview
7/// Field 57B identifies the account with institution in SWIFT payment messages using a party
8/// identifier. This field provides an alternative to BIC-based identification when the
9/// beneficiary's bank is identified through a party identifier system. This option is
10/// particularly useful for institutions that participate in specific clearing systems or
11/// have established party identifier arrangements within correspondent banking networks.
12///
13/// ## Format Specification
14/// **Format**: `[/1!a][/34x]35x`
15/// - **1!a**: Optional account line indicator (1 character)
16/// - **34x**: Optional account number (up to 34 characters)
17/// - **35x**: Party identifier (up to 35 characters)
18/// - **Structure**: Multi-line format with optional components
19///
20/// ## Structure
21/// ```text
22/// /D/1234567890123456789012345678901234
23/// PARTYIDENTIFIER123456789012345678901234
24/// │ │                                  │
25/// │ └─ Account number (optional)       │
26/// │                                    │
27/// └─ Account line indicator (optional) │
28///                                      │
29/// └─────────────────────────────────────
30///           Party identifier (required)
31/// ```
32///
33/// ## Field Components
34/// - **Account Line Indicator**: Single character indicator (optional)
35/// - **Account Number**: Beneficiary's account identifier (optional)
36/// - **Party Identifier**: Institution identification code (required)
37///   - Maximum 35 characters
38///   - Must comply with SWIFT character set
39///
40/// ## Usage Context
41/// Field 57B is used in:
42/// - **MT103**: Single Customer Credit Transfer
43/// - **MT200**: Financial Institution Transfer
44/// - **MT202**: General Financial Institution Transfer
45/// - **MT202COV**: Cover for customer credit transfer
46/// - **MT205**: Financial Institution Transfer for its own account
47///
48/// ### Business Applications
49/// - **Party identifier systems**: Using established identifier schemes
50/// - **Clearing system integration**: Interfacing with clearing networks
51/// - **Correspondent banking**: Party-based correspondent identification
52/// - **Regional payments**: Supporting regional identifier systems
53/// - **Cost optimization**: Reducing identification complexity
54/// - **System interoperability**: Bridging different identifier systems
55///
56/// ## Examples
57/// ```text
58/// :57B:ACCOUNTWITHPARTYID123
59/// └─── Simple party identifier for beneficiary's bank
60///
61/// :57B:/BENEFICIARYACCT987654321
62/// ACCOUNTWITHPARTYID123
63/// └─── Party identifier with beneficiary account
64///
65/// :57B:/D/SPECIALACCT123456
66/// PARTYID789012345
67/// └─── Party identifier with account line indicator and account
68///
69/// :57B:/IBAN12345678901234567890
70/// CLEARINGPARTYID456
71/// └─── Party identifier with IBAN account
72/// ```
73///
74/// ## Party Identifier Types
75/// - **Clearing codes**: National clearing system identifiers
76/// - **Member codes**: Clearing system member identifiers
77/// - **Registry codes**: Financial institution registry codes
78/// - **Network identifiers**: Payment network specific codes
79/// - **System codes**: Internal system identifiers
80/// - **Custom identifiers**: Bilateral agreement identifiers
81///
82/// ## Account Line Indicator Usage
83/// - **D**: Debit account indicator
84/// - **C**: Credit account indicator
85/// - **M**: Main account indicator
86/// - **S**: Settlement account indicator
87/// - **Other**: System-specific indicators
88///
89/// ## Validation Rules
90/// 1. **Party identifier**: Required, maximum 35 characters
91/// 2. **Account number**: Optional, maximum 34 characters
92/// 3. **Account line indicator**: Optional, exactly 1 character
93/// 4. **Character set**: SWIFT character set only
94/// 5. **Format structure**: Must follow multi-line format rules
95/// 6. **Content validation**: All components must be meaningful
96///
97/// ## Network Validated Rules (SWIFT Standards)
98/// - Party identifier cannot exceed 35 characters (Error: T50)
99/// - Account number cannot exceed 34 characters (Error: T14)
100/// - Account line indicator must be single character (Error: T26)
101/// - Must use SWIFT character set only (Error: T61)
102/// - Party identifier cannot be empty (Error: T13)
103/// - Field 57B alternative to 57A/57C/57D (Error: C57)
104/// - Party identifier must be valid for system (Error: T51)
105///
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108pub struct Field57B {
109    /// Account line indicator (optional, 1 character)
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub account_line_indicator: Option<String>,
112    /// Account number (optional, up to 34 characters)
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub account_number: Option<String>,
115    /// Party identifier (up to 35 characters)
116    pub party_identifier: String,
117}
118
119impl Field57B {
120    /// Create a new Field57B with validation
121    pub fn new(
122        account_line_indicator: Option<String>,
123        account_number: Option<String>,
124        party_identifier: impl Into<String>,
125    ) -> Result<Self, crate::ParseError> {
126        let party_identifier = party_identifier.into().trim().to_string();
127
128        // Validate party identifier
129        if party_identifier.is_empty() {
130            return Err(crate::ParseError::InvalidFieldFormat {
131                field_tag: "57B".to_string(),
132                message: "Party identifier cannot be empty".to_string(),
133            });
134        }
135
136        if party_identifier.len() > 35 {
137            return Err(crate::ParseError::InvalidFieldFormat {
138                field_tag: "57B".to_string(),
139                message: "Party identifier cannot exceed 35 characters".to_string(),
140            });
141        }
142
143        if !party_identifier
144            .chars()
145            .all(|c| c.is_ascii() && !c.is_control())
146        {
147            return Err(crate::ParseError::InvalidFieldFormat {
148                field_tag: "57B".to_string(),
149                message: "Party identifier contains invalid characters".to_string(),
150            });
151        }
152
153        // Validate account line indicator if present
154        if let Some(ref indicator) = account_line_indicator {
155            if indicator.is_empty() {
156                return Err(crate::ParseError::InvalidFieldFormat {
157                    field_tag: "57B".to_string(),
158                    message: "Account line indicator cannot be empty if specified".to_string(),
159                });
160            }
161
162            if indicator.len() != 1 {
163                return Err(crate::ParseError::InvalidFieldFormat {
164                    field_tag: "57B".to_string(),
165                    message: "Account line indicator must be exactly 1 character".to_string(),
166                });
167            }
168
169            if !indicator.chars().all(|c| c.is_ascii() && !c.is_control()) {
170                return Err(crate::ParseError::InvalidFieldFormat {
171                    field_tag: "57B".to_string(),
172                    message: "Account line indicator contains invalid characters".to_string(),
173                });
174            }
175        }
176
177        // Validate account number if present
178        if let Some(ref account) = account_number {
179            if account.is_empty() {
180                return Err(crate::ParseError::InvalidFieldFormat {
181                    field_tag: "57B".to_string(),
182                    message: "Account number cannot be empty if specified".to_string(),
183                });
184            }
185
186            if account.len() > 34 {
187                return Err(crate::ParseError::InvalidFieldFormat {
188                    field_tag: "57B".to_string(),
189                    message: "Account number cannot exceed 34 characters".to_string(),
190                });
191            }
192
193            if !account.chars().all(|c| c.is_ascii() && !c.is_control()) {
194                return Err(crate::ParseError::InvalidFieldFormat {
195                    field_tag: "57B".to_string(),
196                    message: "Account number contains invalid characters".to_string(),
197                });
198            }
199        }
200
201        Ok(Field57B {
202            account_line_indicator,
203            account_number,
204            party_identifier,
205        })
206    }
207
208    /// Get the account line indicator
209    pub fn account_line_indicator(&self) -> Option<&str> {
210        self.account_line_indicator.as_deref()
211    }
212
213    /// Get the account number
214    pub fn account_number(&self) -> Option<&str> {
215        self.account_number.as_deref()
216    }
217
218    /// Get the party identifier
219    pub fn party_identifier(&self) -> &str {
220        &self.party_identifier
221    }
222
223    /// Get human-readable description
224    pub fn description(&self) -> String {
225        format!(
226            "Account With Institution (Party ID: {})",
227            self.party_identifier
228        )
229    }
230}
231
232impl SwiftField for Field57B {
233    fn parse(content: &str) -> crate::Result<Self> {
234        let content = content.trim();
235        if content.is_empty() {
236            return Err(crate::ParseError::InvalidFieldFormat {
237                field_tag: "57B".to_string(),
238                message: "Field content cannot be empty".to_string(),
239            });
240        }
241
242        let content = if let Some(stripped) = content.strip_prefix(":57B:") {
243            stripped
244        } else if let Some(stripped) = content.strip_prefix("57B:") {
245            stripped
246        } else {
247            content
248        };
249
250        let mut account_line_indicator = None;
251        let mut account_number = None;
252        let mut party_identifier_content = content;
253
254        // Check for account line indicator (starts with /)
255        if content.starts_with('/') {
256            let lines: Vec<&str> = content.lines().collect();
257            if !lines.is_empty() {
258                let first_line = lines[0];
259
260                if first_line.len() == 2 && first_line.starts_with('/') {
261                    // Only account line indicator: /X
262                    account_line_indicator = Some(first_line[1..].to_string());
263                    party_identifier_content = if lines.len() > 1 { lines[1] } else { "" };
264                } else if first_line.len() > 2 && first_line.starts_with('/') {
265                    // Account line indicator + account number: /X/account or /account
266                    let parts: Vec<&str> = first_line[1..].split('/').collect();
267                    if parts.len() == 2 {
268                        // /X/account format
269                        account_line_indicator = Some(parts[0].to_string());
270                        account_number = Some(parts[1].to_string());
271                    } else {
272                        // /account format
273                        account_number = Some(parts[0].to_string());
274                    }
275                    party_identifier_content = if lines.len() > 1 { lines[1] } else { "" };
276                }
277            }
278        }
279
280        let party_identifier = party_identifier_content.trim().to_string();
281        if party_identifier.is_empty() {
282            return Err(crate::ParseError::InvalidFieldFormat {
283                field_tag: "57B".to_string(),
284                message: "Party identifier is required".to_string(),
285            });
286        }
287
288        Field57B::new(account_line_indicator, account_number, party_identifier)
289    }
290
291    fn to_swift_string(&self) -> String {
292        let mut result = String::new();
293
294        if let Some(ref indicator) = self.account_line_indicator {
295            result.push('/');
296            result.push_str(indicator);
297        }
298
299        if let Some(ref account) = self.account_number {
300            result.push('/');
301            result.push_str(account);
302        }
303
304        if !result.is_empty() {
305            result.push('\n');
306        }
307        result.push_str(&self.party_identifier);
308
309        format!(":57B:{}", result)
310    }
311
312    fn validate(&self) -> ValidationResult {
313        // Validation is done in constructor
314        ValidationResult {
315            is_valid: true,
316            errors: Vec::new(),
317            warnings: Vec::new(),
318        }
319    }
320
321    fn format_spec() -> &'static str {
322        "[/1!a][/34x]35x"
323    }
324}
325
326impl std::fmt::Display for Field57B {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        match (&self.account_line_indicator, &self.account_number) {
329            (Some(indicator), Some(account)) => write!(
330                f,
331                "Indicator: {}, Account: {}, Party: {}",
332                indicator, account, self.party_identifier
333            ),
334            (None, Some(account)) => {
335                write!(f, "Account: {}, Party: {}", account, self.party_identifier)
336            }
337            (Some(indicator), None) => write!(
338                f,
339                "Indicator: {}, Party: {}",
340                indicator, self.party_identifier
341            ),
342            (None, None) => write!(f, "Party: {}", self.party_identifier),
343        }
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_field57b_creation_party_only() {
353        let field = Field57B::new(None, None, "ACCOUNTWITHPARTYID123").unwrap();
354        assert_eq!(field.party_identifier(), "ACCOUNTWITHPARTYID123");
355        assert!(field.account_number().is_none());
356        assert!(field.account_line_indicator().is_none());
357    }
358
359    #[test]
360    fn test_field57b_creation_with_account() {
361        let field = Field57B::new(
362            None,
363            Some("ACCT987654321".to_string()),
364            "ACCOUNTWITHPARTYID123",
365        )
366        .unwrap();
367        assert_eq!(field.party_identifier(), "ACCOUNTWITHPARTYID123");
368        assert_eq!(field.account_number(), Some("ACCT987654321"));
369        assert!(field.account_line_indicator().is_none());
370    }
371
372    #[test]
373    fn test_field57b_creation_with_account_line_indicator() {
374        let field = Field57B::new(
375            Some("D".to_string()),
376            Some("ACCT987654321".to_string()),
377            "ACCOUNTWITHPARTYID123",
378        )
379        .unwrap();
380        assert_eq!(field.party_identifier(), "ACCOUNTWITHPARTYID123");
381        assert_eq!(field.account_number(), Some("ACCT987654321"));
382        assert_eq!(field.account_line_indicator(), Some("D"));
383    }
384
385    #[test]
386    fn test_field57b_parse_party_only() {
387        let field = Field57B::parse("ACCOUNTWITHPARTYID123").unwrap();
388        assert_eq!(field.party_identifier(), "ACCOUNTWITHPARTYID123");
389        assert!(field.account_number().is_none());
390    }
391
392    #[test]
393    fn test_field57b_parse_with_account() {
394        let field = Field57B::parse("/ACCT987654321\nACCOUNTWITHPARTYID123").unwrap();
395        assert_eq!(field.party_identifier(), "ACCOUNTWITHPARTYID123");
396        assert_eq!(field.account_number(), Some("ACCT987654321"));
397    }
398
399    #[test]
400    fn test_field57b_parse_with_tag() {
401        let field = Field57B::parse(":57B:ACCOUNTWITHPARTYID123").unwrap();
402        assert_eq!(field.party_identifier(), "ACCOUNTWITHPARTYID123");
403    }
404
405    #[test]
406    fn test_field57b_to_swift_string() {
407        let field = Field57B::new(None, None, "ACCOUNTWITHPARTYID123").unwrap();
408        assert_eq!(field.to_swift_string(), ":57B:ACCOUNTWITHPARTYID123");
409
410        let field = Field57B::new(
411            None,
412            Some("ACCT987654321".to_string()),
413            "ACCOUNTWITHPARTYID123",
414        )
415        .unwrap();
416        assert_eq!(
417            field.to_swift_string(),
418            ":57B:/ACCT987654321\nACCOUNTWITHPARTYID123"
419        );
420    }
421
422    #[test]
423    fn test_field57b_display() {
424        let field = Field57B::new(None, None, "ACCOUNTWITHPARTYID123").unwrap();
425        assert_eq!(format!("{}", field), "Party: ACCOUNTWITHPARTYID123");
426
427        let field = Field57B::new(
428            None,
429            Some("ACCT987654321".to_string()),
430            "ACCOUNTWITHPARTYID123",
431        )
432        .unwrap();
433        assert_eq!(
434            format!("{}", field),
435            "Account: ACCT987654321, Party: ACCOUNTWITHPARTYID123"
436        );
437    }
438
439    #[test]
440    fn test_field57b_description() {
441        let field = Field57B::new(None, None, "ACCOUNTWITHPARTYID123").unwrap();
442        assert_eq!(
443            field.description(),
444            "Account With Institution (Party ID: ACCOUNTWITHPARTYID123)"
445        );
446    }
447
448    #[test]
449    fn test_field57b_validation_empty_party() {
450        let result = Field57B::new(None, None, "");
451        assert!(result.is_err());
452        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
453    }
454
455    #[test]
456    fn test_field57b_validation_party_too_long() {
457        let party = "A".repeat(36); // 36 characters, max is 35
458        let result = Field57B::new(None, None, party);
459        assert!(result.is_err());
460        assert!(
461            result
462                .unwrap_err()
463                .to_string()
464                .contains("cannot exceed 35 characters")
465        );
466    }
467
468    #[test]
469    fn test_field57b_validation_invalid_characters() {
470        let result = Field57B::new(None, None, "PARTY\x00ID"); // Contains null character
471        assert!(result.is_err());
472        assert!(
473            result
474                .unwrap_err()
475                .to_string()
476                .contains("invalid characters")
477        );
478    }
479
480    #[test]
481    fn test_field57b_validation_account_too_long() {
482        let account = "A".repeat(35); // 35 characters, max is 34
483        let result = Field57B::new(None, Some(account), "ACCOUNTWITHPARTYID123");
484        assert!(result.is_err());
485        assert!(
486            result
487                .unwrap_err()
488                .to_string()
489                .contains("cannot exceed 34 characters")
490        );
491    }
492
493    #[test]
494    fn test_field57b_validate() {
495        let field = Field57B::new(None, None, "ACCOUNTWITHPARTYID123").unwrap();
496        let validation = field.validate();
497        assert!(validation.is_valid);
498        assert!(validation.errors.is_empty());
499    }
500}