swift_mt_message/fields/
field53b.rs

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