swift_mt_message/fields/
field54b.rs

1use crate::{SwiftField, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 54B: Receiver's Correspondent (Option B)
5///
6/// ## Overview
7/// Field 54B identifies the receiver's correspondent institution using a party identifier
8/// rather than a BIC code. This option is used when the receiver's correspondent institution
9/// needs to be identified through an alternative identification scheme, such as a national
10/// bank code, clearing code, or proprietary identifier system. This field is essential for
11/// routing payments through correspondent networks when BIC codes are not available or sufficient.
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///
19/// ## Structure
20/// ```text
21/// /C/9876543210
22/// FEDWIRE021000021
23/// │││ │         │
24/// │││ │         └─ Party identifier (routing number)
25/// │││ └─────────── Account number
26/// ││└─────────────── Account separator
27/// │└──────────────── Account line indicator
28/// └───────────────── Field separator
29/// ```
30///
31/// ## Field Components
32/// - **Account Line Indicator**: Optional qualifier for account type
33/// - **Account Number**: Institution's account for settlement
34/// - **Party Identifier**: Alternative identification code or number
35///
36/// ## Usage Context
37/// Field 54B is used in:
38/// - **MT103**: Single Customer Credit Transfer (when BIC not available)
39/// - **MT200**: Financial Institution Transfer
40/// - **MT202**: General Financial Institution Transfer
41/// - **MT202COV**: Cover for customer credit transfer
42/// - **MT205**: Financial Institution Transfer for its own account
43///
44/// ### Business Applications
45/// - **Non-SWIFT institutions**: Identifying institutions without BIC codes
46/// - **Domestic clearing**: Using national clearing codes or bank numbers
47/// - **Regional networks**: Supporting regional payment network identifiers
48/// - **Legacy systems**: Interfacing with older identification schemes
49/// - **Regulatory requirements**: Meeting local identification standards
50/// - **Correspondent routing**: Directing payments through specific correspondents
51///
52/// ## Examples
53/// ```text
54/// :54B:FEDWIRE021000021
55/// └─── US Federal Reserve routing number
56///
57/// :54B:/C/9876543210
58/// UKSC123456
59/// └─── UK Sort Code with correspondent account
60///
61/// :54B:/S/SETTLEMENT009876543210
62/// CANCLEAR005678
63/// └─── Canadian clearing number with settlement account
64///
65/// :54B:CHIPS0456
66/// └─── CHIPS participant identifier
67/// ```
68///
69/// ## Party Identifier Types
70/// Common party identifier formats for receiver's correspondents:
71/// - **FEDWIRE**: US Federal Reserve routing numbers (9 digits)
72/// - **UKSC**: UK Sort Codes (6 digits)
73/// - **CANCLEAR**: Canadian clearing numbers
74/// - **CHIPS**: Clearing House Interbank Payments System IDs
75/// - **TARGET2**: European TARGET2 participant codes
76/// - **CNAPS**: China National Advanced Payment System codes
77/// - **RTGS**: Real-time gross settlement system codes
78///
79/// ## Account Line Indicators
80/// Common indicators for receiver's correspondent accounts:
81/// - **C**: Correspondent account (checking)
82/// - **D**: Deposit account
83/// - **S**: Settlement account
84/// - **N**: Nostro account (our account with them)
85/// - **V**: Vostro account (their account with us)
86/// - **L**: Liquidity management account
87/// - **R**: Reserve account
88///
89/// ## Validation Rules
90/// 1. **Party identifier**: Cannot be empty, max 35 characters
91/// 2. **Account line indicator**: If present, exactly 1 character
92/// 3. **Account number**: If present, max 34 characters
93/// 4. **Character validation**: All components must be printable ASCII
94/// 5. **Content requirement**: Must contain meaningful identification
95/// 6. **Format consistency**: Components must be properly structured
96///
97/// ## Network Validated Rules (SWIFT Standards)
98/// - Party identifier cannot be empty (Error: T11)
99/// - Party identifier cannot exceed 35 characters (Error: T14)
100/// - Account line indicator must be single character (Error: T12)
101/// - Account number cannot exceed 34 characters (Error: T15)
102/// - Characters must be from SWIFT character set (Error: T61)
103/// - Field 54B alternative to 54A when BIC not available (Error: C54)
104/// - Party identifier must be recognizable by receiver (Error: C55)
105///
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108pub struct Field54B {
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 Field54B {
120    /// Create a new Field54B 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: "54B".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: "54B".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: "54B".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: "54B".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: "54B".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: "54B".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: "54B".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: "54B".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: "54B".to_string(),
196                    message: "Account number contains invalid characters".to_string(),
197                });
198            }
199        }
200
201        Ok(Field54B {
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            "Receiver's Correspondent (Party ID: {})",
227            self.party_identifier
228        )
229    }
230}
231
232impl SwiftField for Field54B {
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: "54B".to_string(),
238                message: "Field content cannot be empty".to_string(),
239            });
240        }
241
242        let content = if let Some(stripped) = content.strip_prefix(":54B:") {
243            stripped
244        } else if let Some(stripped) = content.strip_prefix("54B:") {
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: "54B".to_string(),
284                message: "Party identifier is required".to_string(),
285            });
286        }
287
288        Field54B::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!(":54B:{}", 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 Field54B {
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_field54b_creation_party_only() {
353        let field = Field54B::new(None, None, "RCVRPARTYID123").unwrap();
354        assert_eq!(field.party_identifier(), "RCVRPARTYID123");
355        assert!(field.account_number().is_none());
356        assert!(field.account_line_indicator().is_none());
357    }
358
359    #[test]
360    fn test_field54b_creation_with_account() {
361        let field = Field54B::new(None, Some("9876543210".to_string()), "RCVRPARTYID123").unwrap();
362        assert_eq!(field.party_identifier(), "RCVRPARTYID123");
363        assert_eq!(field.account_number(), Some("9876543210"));
364        assert!(field.account_line_indicator().is_none());
365    }
366
367    #[test]
368    fn test_field54b_creation_with_account_line_indicator() {
369        let field = Field54B::new(
370            Some("B".to_string()),
371            Some("9876543210".to_string()),
372            "RCVRPARTYID123",
373        )
374        .unwrap();
375        assert_eq!(field.party_identifier(), "RCVRPARTYID123");
376        assert_eq!(field.account_number(), Some("9876543210"));
377        assert_eq!(field.account_line_indicator(), Some("B"));
378    }
379
380    #[test]
381    fn test_field54b_parse_party_only() {
382        let field = Field54B::parse("RCVRPARTYID123").unwrap();
383        assert_eq!(field.party_identifier(), "RCVRPARTYID123");
384        assert!(field.account_number().is_none());
385    }
386
387    #[test]
388    fn test_field54b_parse_with_account() {
389        let field = Field54B::parse("/9876543210\nRCVRPARTYID123").unwrap();
390        assert_eq!(field.party_identifier(), "RCVRPARTYID123");
391        assert_eq!(field.account_number(), Some("9876543210"));
392    }
393
394    #[test]
395    fn test_field54b_parse_with_tag() {
396        let field = Field54B::parse(":54B:RCVRPARTYID123").unwrap();
397        assert_eq!(field.party_identifier(), "RCVRPARTYID123");
398    }
399
400    #[test]
401    fn test_field54b_to_swift_string() {
402        let field = Field54B::new(None, None, "RCVRPARTYID123").unwrap();
403        assert_eq!(field.to_swift_string(), ":54B:RCVRPARTYID123");
404
405        let field = Field54B::new(None, Some("9876543210".to_string()), "RCVRPARTYID123").unwrap();
406        assert_eq!(field.to_swift_string(), ":54B:/9876543210\nRCVRPARTYID123");
407    }
408
409    #[test]
410    fn test_field54b_display() {
411        let field = Field54B::new(None, None, "RCVRPARTYID123").unwrap();
412        assert_eq!(format!("{}", field), "Party: RCVRPARTYID123");
413
414        let field = Field54B::new(None, Some("9876543210".to_string()), "RCVRPARTYID123").unwrap();
415        assert_eq!(
416            format!("{}", field),
417            "Account: 9876543210, Party: RCVRPARTYID123"
418        );
419    }
420
421    #[test]
422    fn test_field54b_description() {
423        let field = Field54B::new(None, None, "RCVRPARTYID123").unwrap();
424        assert_eq!(
425            field.description(),
426            "Receiver's Correspondent (Party ID: RCVRPARTYID123)"
427        );
428    }
429
430    #[test]
431    fn test_field54b_validation_empty_party() {
432        let result = Field54B::new(None, None, "");
433        assert!(result.is_err());
434        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
435    }
436
437    #[test]
438    fn test_field54b_validation_party_too_long() {
439        let party = "A".repeat(36); // 36 characters, max is 35
440        let result = Field54B::new(None, None, party);
441        assert!(result.is_err());
442        assert!(
443            result
444                .unwrap_err()
445                .to_string()
446                .contains("cannot exceed 35 characters")
447        );
448    }
449
450    #[test]
451    fn test_field54b_validation_invalid_characters() {
452        let result = Field54B::new(None, None, "PARTY\x00ID"); // Contains null character
453        assert!(result.is_err());
454        assert!(
455            result
456                .unwrap_err()
457                .to_string()
458                .contains("invalid characters")
459        );
460    }
461
462    #[test]
463    fn test_field54b_validation_account_too_long() {
464        let account = "A".repeat(35); // 35 characters, max is 34
465        let result = Field54B::new(None, Some(account), "RCVRPARTYID123");
466        assert!(result.is_err());
467        assert!(
468            result
469                .unwrap_err()
470                .to_string()
471                .contains("cannot exceed 34 characters")
472        );
473    }
474
475    #[test]
476    fn test_field54b_validate() {
477        let field = Field54B::new(None, None, "RCVRPARTYID123").unwrap();
478        let validation = field.validate();
479        assert!(validation.is_valid);
480        assert!(validation.errors.is_empty());
481    }
482}