swift_mt_message/fields/
field55b.rs

1use crate::{SwiftField, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 55B: Third Reimbursement Institution (Option B)
5///
6/// ## Overview
7/// Field 55B identifies the third reimbursement institution using a party identifier
8/// rather than a BIC code. This option is used when the third institution in the
9/// reimbursement chain needs to be identified through an alternative identification
10/// scheme, such as a national bank code, clearing code, or proprietary identifier
11/// system. This field is essential for complex correspondent arrangements involving
12/// multiple institutions in the settlement process.
13///
14/// ## Format Specification
15/// **Format**: `[/1!a][/34x]35x`
16/// - **1!a**: Optional account line indicator (1 character)
17/// - **34x**: Optional account number (up to 34 characters)
18/// - **35x**: Party identifier (up to 35 characters)
19///
20/// ## Structure
21/// ```text
22/// /C/1234567890
23/// FEDWIRE021000021
24/// │││ │         │
25/// │││ │         └─ Party identifier (routing number)
26/// │││ └─────────── Account number
27/// ││└─────────────── Account separator
28/// │└──────────────── Account line indicator
29/// └───────────────── Field separator
30/// ```
31///
32/// ## Field Components
33/// - **Account Line Indicator**: Optional qualifier for account type
34/// - **Account Number**: Institution's account for reimbursement
35/// - **Party Identifier**: Alternative identification code or number
36///
37/// ## Usage Context
38/// Field 55B is used in:
39/// - **MT202**: General Financial Institution Transfer (when BIC not available)
40/// - **MT202COV**: Cover for customer credit transfer
41/// - **MT205**: Financial Institution Transfer for its own account
42/// - **MT103**: Single Customer Credit Transfer (in complex routing)
43/// - **MT200**: Financial Institution Transfer
44///
45/// ### Business Applications
46/// - **Non-SWIFT institutions**: Identifying institutions without BIC codes
47/// - **Domestic clearing**: Using national clearing codes or bank numbers
48/// - **Regional networks**: Supporting regional payment network identifiers
49/// - **Legacy systems**: Interfacing with older identification schemes
50/// - **Regulatory requirements**: Meeting local identification standards
51/// - **Reimbursement routing**: Directing reimbursement through specific institutions
52/// - **Multi-hop settlements**: Managing complex correspondent chains
53///
54/// ## Examples
55/// ```text
56/// :55B:FEDWIRE021000021
57/// └─── US Federal Reserve routing number
58///
59/// :55B:/C/1234567890
60/// UKSC123456
61/// └─── UK Sort Code with reimbursement account
62///
63/// :55B:/S/SETTLEMENT001234567890
64/// CANCLEAR001234
65/// └─── Canadian clearing number with settlement account
66///
67/// :55B:CHIPS0789
68/// └─── CHIPS participant identifier
69/// ```
70///
71/// ## Party Identifier Types
72/// Common party identifier formats for third reimbursement institutions:
73/// - **FEDWIRE**: US Federal Reserve routing numbers (9 digits)
74/// - **UKSC**: UK Sort Codes (6 digits)
75/// - **CANCLEAR**: Canadian clearing numbers
76/// - **CHIPS**: Clearing House Interbank Payments System IDs
77/// - **TARGET2**: European TARGET2 participant codes
78/// - **CNAPS**: China National Advanced Payment System codes
79/// - **RTGS**: Real-time gross settlement system codes
80/// - **ACH**: Automated Clearing House routing numbers
81///
82/// ## Account Line Indicators
83/// Common indicators for third reimbursement institution accounts:
84/// - **C**: Correspondent account (checking)
85/// - **D**: Deposit account
86/// - **S**: Settlement account
87/// - **N**: Nostro account (our account with them)
88/// - **V**: Vostro account (their account with us)
89/// - **L**: Liquidity management account
90/// - **R**: Reserve account
91/// - **T**: Third party account
92///
93/// ## Reimbursement Chain Context
94/// In multi-institution reimbursement chains:
95/// - **Field 53A/B/D**: Sender's correspondent (first institution)
96/// - **Field 54A/B/D**: Receiver's correspondent (second institution)
97/// - **Field 55A/B/D**: Third reimbursement institution (third institution)
98/// - **Field 56A/C/D**: Intermediary institution (fourth institution)
99/// - **Field 57A/B/C/D**: Account with institution (final institution)
100///
101/// ## Validation Rules
102/// 1. **Party identifier**: Cannot be empty, max 35 characters
103/// 2. **Account line indicator**: If present, exactly 1 character
104/// 3. **Account number**: If present, max 34 characters
105/// 4. **Character validation**: All components must be printable ASCII
106/// 5. **Content requirement**: Must contain meaningful identification
107/// 6. **Format consistency**: Components must be properly structured
108///
109/// ## Network Validated Rules (SWIFT Standards)
110/// - Party identifier cannot be empty (Error: T11)
111/// - Party identifier cannot exceed 35 characters (Error: T14)
112/// - Account line indicator must be single character (Error: T12)
113/// - Account number cannot exceed 34 characters (Error: T15)
114/// - Characters must be from SWIFT character set (Error: T61)
115/// - Field 55B alternative to 55A when BIC not available (Error: C55)
116/// - Party identifier must be recognizable by institutions (Error: C56)
117/// - Institution must be in reimbursement chain (Error: C57)
118///
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
121pub struct Field55B {
122    /// Account line indicator (optional, 1 character)
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub account_line_indicator: Option<String>,
125    /// Account number (optional, up to 34 characters)
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub account_number: Option<String>,
128    /// Party identifier (up to 35 characters)
129    pub party_identifier: String,
130}
131
132impl Field55B {
133    /// Create a new Field55B with validation
134    pub fn new(
135        account_line_indicator: Option<String>,
136        account_number: Option<String>,
137        party_identifier: impl Into<String>,
138    ) -> Result<Self, crate::ParseError> {
139        let party_identifier = party_identifier.into().trim().to_string();
140
141        // Validate party identifier
142        if party_identifier.is_empty() {
143            return Err(crate::ParseError::InvalidFieldFormat {
144                field_tag: "55B".to_string(),
145                message: "Party identifier cannot be empty".to_string(),
146            });
147        }
148
149        if party_identifier.len() > 35 {
150            return Err(crate::ParseError::InvalidFieldFormat {
151                field_tag: "55B".to_string(),
152                message: "Party identifier cannot exceed 35 characters".to_string(),
153            });
154        }
155
156        if !party_identifier
157            .chars()
158            .all(|c| c.is_ascii() && !c.is_control())
159        {
160            return Err(crate::ParseError::InvalidFieldFormat {
161                field_tag: "55B".to_string(),
162                message: "Party identifier contains invalid characters".to_string(),
163            });
164        }
165
166        // Validate account line indicator if present
167        if let Some(ref indicator) = account_line_indicator {
168            if indicator.is_empty() {
169                return Err(crate::ParseError::InvalidFieldFormat {
170                    field_tag: "55B".to_string(),
171                    message: "Account line indicator cannot be empty if specified".to_string(),
172                });
173            }
174
175            if indicator.len() != 1 {
176                return Err(crate::ParseError::InvalidFieldFormat {
177                    field_tag: "55B".to_string(),
178                    message: "Account line indicator must be exactly 1 character".to_string(),
179                });
180            }
181
182            if !indicator.chars().all(|c| c.is_ascii() && !c.is_control()) {
183                return Err(crate::ParseError::InvalidFieldFormat {
184                    field_tag: "55B".to_string(),
185                    message: "Account line indicator contains invalid characters".to_string(),
186                });
187            }
188        }
189
190        // Validate account number if present
191        if let Some(ref account) = account_number {
192            if account.is_empty() {
193                return Err(crate::ParseError::InvalidFieldFormat {
194                    field_tag: "55B".to_string(),
195                    message: "Account number cannot be empty if specified".to_string(),
196                });
197            }
198
199            if account.len() > 34 {
200                return Err(crate::ParseError::InvalidFieldFormat {
201                    field_tag: "55B".to_string(),
202                    message: "Account number cannot exceed 34 characters".to_string(),
203                });
204            }
205
206            if !account.chars().all(|c| c.is_ascii() && !c.is_control()) {
207                return Err(crate::ParseError::InvalidFieldFormat {
208                    field_tag: "55B".to_string(),
209                    message: "Account number contains invalid characters".to_string(),
210                });
211            }
212        }
213
214        Ok(Field55B {
215            account_line_indicator,
216            account_number,
217            party_identifier,
218        })
219    }
220
221    /// Get the account line indicator
222    pub fn account_line_indicator(&self) -> Option<&str> {
223        self.account_line_indicator.as_deref()
224    }
225
226    /// Get the account number
227    pub fn account_number(&self) -> Option<&str> {
228        self.account_number.as_deref()
229    }
230
231    /// Get the party identifier
232    pub fn party_identifier(&self) -> &str {
233        &self.party_identifier
234    }
235
236    /// Get human-readable description
237    pub fn description(&self) -> String {
238        format!(
239            "Third Reimbursement Institution (Party ID: {})",
240            self.party_identifier
241        )
242    }
243}
244
245impl SwiftField for Field55B {
246    fn parse(content: &str) -> crate::Result<Self> {
247        let content = content.trim();
248        if content.is_empty() {
249            return Err(crate::ParseError::InvalidFieldFormat {
250                field_tag: "55B".to_string(),
251                message: "Field content cannot be empty".to_string(),
252            });
253        }
254
255        let content = if let Some(stripped) = content.strip_prefix(":55B:") {
256            stripped
257        } else if let Some(stripped) = content.strip_prefix("55B:") {
258            stripped
259        } else {
260            content
261        };
262
263        let mut account_line_indicator = None;
264        let mut account_number = None;
265        let mut party_identifier_content = content;
266
267        // Check for account line indicator (starts with /)
268        if content.starts_with('/') {
269            let lines: Vec<&str> = content.lines().collect();
270            if !lines.is_empty() {
271                let first_line = lines[0];
272
273                if first_line.len() == 2 && first_line.starts_with('/') {
274                    // Only account line indicator: /X
275                    account_line_indicator = Some(first_line[1..].to_string());
276                    party_identifier_content = if lines.len() > 1 { lines[1] } else { "" };
277                } else if first_line.len() > 2 && first_line.starts_with('/') {
278                    // Account line indicator + account number: /X/account or /account
279                    let parts: Vec<&str> = first_line[1..].split('/').collect();
280                    if parts.len() == 2 {
281                        // /X/account format
282                        account_line_indicator = Some(parts[0].to_string());
283                        account_number = Some(parts[1].to_string());
284                    } else {
285                        // /account format
286                        account_number = Some(parts[0].to_string());
287                    }
288                    party_identifier_content = if lines.len() > 1 { lines[1] } else { "" };
289                }
290            }
291        }
292
293        let party_identifier = party_identifier_content.trim().to_string();
294        if party_identifier.is_empty() {
295            return Err(crate::ParseError::InvalidFieldFormat {
296                field_tag: "55B".to_string(),
297                message: "Party identifier is required".to_string(),
298            });
299        }
300
301        Field55B::new(account_line_indicator, account_number, party_identifier)
302    }
303
304    fn to_swift_string(&self) -> String {
305        let mut result = String::new();
306
307        if let Some(ref indicator) = self.account_line_indicator {
308            result.push('/');
309            result.push_str(indicator);
310        }
311
312        if let Some(ref account) = self.account_number {
313            result.push('/');
314            result.push_str(account);
315        }
316
317        if !result.is_empty() {
318            result.push('\n');
319        }
320        result.push_str(&self.party_identifier);
321
322        format!(":55B:{}", result)
323    }
324
325    fn validate(&self) -> ValidationResult {
326        // Validation is done in constructor
327        ValidationResult {
328            is_valid: true,
329            errors: Vec::new(),
330            warnings: Vec::new(),
331        }
332    }
333
334    fn format_spec() -> &'static str {
335        "[/1!a][/34x]35x"
336    }
337}
338
339impl std::fmt::Display for Field55B {
340    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341        match (&self.account_line_indicator, &self.account_number) {
342            (Some(indicator), Some(account)) => write!(
343                f,
344                "Indicator: {}, Account: {}, Party: {}",
345                indicator, account, self.party_identifier
346            ),
347            (None, Some(account)) => {
348                write!(f, "Account: {}, Party: {}", account, self.party_identifier)
349            }
350            (Some(indicator), None) => write!(
351                f,
352                "Indicator: {}, Party: {}",
353                indicator, self.party_identifier
354            ),
355            (None, None) => write!(f, "Party: {}", self.party_identifier),
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_field55b_creation_party_only() {
366        let field = Field55B::new(None, None, "THIRDPARTYID123").unwrap();
367        assert_eq!(field.party_identifier(), "THIRDPARTYID123");
368        assert!(field.account_number().is_none());
369        assert!(field.account_line_indicator().is_none());
370    }
371
372    #[test]
373    fn test_field55b_creation_with_account() {
374        let field = Field55B::new(None, Some("1234567890".to_string()), "THIRDPARTYID123").unwrap();
375        assert_eq!(field.party_identifier(), "THIRDPARTYID123");
376        assert_eq!(field.account_number(), Some("1234567890"));
377        assert!(field.account_line_indicator().is_none());
378    }
379
380    #[test]
381    fn test_field55b_creation_with_account_line_indicator() {
382        let field = Field55B::new(
383            Some("C".to_string()),
384            Some("1234567890".to_string()),
385            "THIRDPARTYID123",
386        )
387        .unwrap();
388        assert_eq!(field.party_identifier(), "THIRDPARTYID123");
389        assert_eq!(field.account_number(), Some("1234567890"));
390        assert_eq!(field.account_line_indicator(), Some("C"));
391    }
392
393    #[test]
394    fn test_field55b_parse_party_only() {
395        let field = Field55B::parse("THIRDPARTYID123").unwrap();
396        assert_eq!(field.party_identifier(), "THIRDPARTYID123");
397        assert!(field.account_number().is_none());
398    }
399
400    #[test]
401    fn test_field55b_parse_with_account() {
402        let field = Field55B::parse("/1234567890\nTHIRDPARTYID123").unwrap();
403        assert_eq!(field.party_identifier(), "THIRDPARTYID123");
404        assert_eq!(field.account_number(), Some("1234567890"));
405    }
406
407    #[test]
408    fn test_field55b_parse_with_tag() {
409        let field = Field55B::parse(":55B:THIRDPARTYID123").unwrap();
410        assert_eq!(field.party_identifier(), "THIRDPARTYID123");
411    }
412
413    #[test]
414    fn test_field55b_to_swift_string() {
415        let field = Field55B::new(None, None, "THIRDPARTYID123").unwrap();
416        assert_eq!(field.to_swift_string(), ":55B:THIRDPARTYID123");
417
418        let field = Field55B::new(None, Some("1234567890".to_string()), "THIRDPARTYID123").unwrap();
419        assert_eq!(field.to_swift_string(), ":55B:/1234567890\nTHIRDPARTYID123");
420    }
421
422    #[test]
423    fn test_field55b_display() {
424        let field = Field55B::new(None, None, "THIRDPARTYID123").unwrap();
425        assert_eq!(format!("{}", field), "Party: THIRDPARTYID123");
426
427        let field = Field55B::new(None, Some("1234567890".to_string()), "THIRDPARTYID123").unwrap();
428        assert_eq!(
429            format!("{}", field),
430            "Account: 1234567890, Party: THIRDPARTYID123"
431        );
432    }
433
434    #[test]
435    fn test_field55b_description() {
436        let field = Field55B::new(None, None, "THIRDPARTYID123").unwrap();
437        assert_eq!(
438            field.description(),
439            "Third Reimbursement Institution (Party ID: THIRDPARTYID123)"
440        );
441    }
442
443    #[test]
444    fn test_field55b_validation_empty_party() {
445        let result = Field55B::new(None, None, "");
446        assert!(result.is_err());
447        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
448    }
449
450    #[test]
451    fn test_field55b_validation_party_too_long() {
452        let party = "A".repeat(36); // 36 characters, max is 35
453        let result = Field55B::new(None, None, party);
454        assert!(result.is_err());
455        assert!(
456            result
457                .unwrap_err()
458                .to_string()
459                .contains("cannot exceed 35 characters")
460        );
461    }
462
463    #[test]
464    fn test_field55b_validation_invalid_characters() {
465        let result = Field55B::new(None, None, "PARTY\x00ID"); // Contains null character
466        assert!(result.is_err());
467        assert!(
468            result
469                .unwrap_err()
470                .to_string()
471                .contains("invalid characters")
472        );
473    }
474
475    #[test]
476    fn test_field55b_validation_account_too_long() {
477        let account = "A".repeat(35); // 35 characters, max is 34
478        let result = Field55B::new(None, Some(account), "THIRDPARTYID123");
479        assert!(result.is_err());
480        assert!(
481            result
482                .unwrap_err()
483                .to_string()
484                .contains("cannot exceed 34 characters")
485        );
486    }
487
488    #[test]
489    fn test_field55b_validate() {
490        let field = Field55B::new(None, None, "THIRDPARTYID123").unwrap();
491        let validation = field.validate();
492        assert!(validation.is_valid);
493        assert!(validation.errors.is_empty());
494    }
495}