swift_mt_message/fields/
field56c.rs

1use crate::{SwiftField, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 56C: Intermediary Institution (Option C)
5///
6/// ## Overview
7/// Field 56C identifies an intermediary institution in SWIFT payment messages using an account
8/// number or identifier. This field provides an alternative to BIC-based identification when
9/// the intermediary institution is identified through an account number, clearing code, or
10/// other identifier system. This option is particularly useful in domestic payment systems
11/// or when specific account-based routing is required.
12///
13/// ## Format Specification
14/// **Format**: `/34x`
15/// - **34x**: Account number or identifier (up to 34 characters)
16/// - **Leading slash**: Required field delimiter
17/// - **Character set**: SWIFT character set (A-Z, 0-9, and limited special characters)
18///
19/// ## Structure
20/// ```text
21/// /1234567890123456789012345678901234
22/// │└─────────────────────────────────┘
23/// │              Account number
24/// └─ Required delimiter
25/// ```
26///
27/// ## Field Components
28/// - **Account Number**: Intermediary institution identifier
29///   - Can be account number, clearing code, or routing identifier
30///   - Maximum 34 characters
31///   - Must comply with SWIFT character set
32///
33/// ## Usage Context
34/// Field 56C is used in:
35/// - **MT103**: Single Customer Credit Transfer
36/// - **MT200**: Financial Institution Transfer
37/// - **MT202**: General Financial Institution Transfer
38/// - **MT202COV**: Cover for customer credit transfer
39/// - **MT205**: Financial Institution Transfer for its own account
40///
41/// ### Business Applications
42/// - **Domestic routing**: Using national clearing codes
43/// - **Account-based identification**: When BIC is not available or preferred
44/// - **Clearing system integration**: Interfacing with local clearing systems
45/// - **Correspondent banking**: Account-based correspondent identification
46/// - **Cost optimization**: Reducing correspondent banking fees
47/// - **Regional payments**: Supporting regional payment networks
48///
49/// ## Examples
50/// ```text
51/// :56C:/INTERMEDIARYACCT123456
52/// └─── Intermediary account number
53///
54/// :56C:/CLRCODE123456789
55/// └─── Clearing code identifier
56///
57/// :56C:/FEDWIRE021000021
58/// └─── US Federal Reserve routing number
59///
60/// :56C:/SORTCODE123456
61/// └─── UK sort code based identifier
62///
63/// :56C:/IFSC0001234
64/// └─── Indian Financial System Code
65/// ```
66///
67/// ## Account Number Types
68/// - **Bank account numbers**: Direct account identification
69/// - **Clearing codes**: National clearing system codes
70/// - **Routing numbers**: US Federal Reserve routing numbers
71/// - **Sort codes**: UK banking sort codes
72/// - **IFSC codes**: Indian Financial System Codes
73/// - **BSB numbers**: Australian Bank State Branch numbers
74/// - **Transit numbers**: Canadian transit numbers
75///
76/// ## Validation Rules
77/// 1. **Length**: Maximum 34 characters
78/// 2. **Format**: Must start with forward slash (/)
79/// 3. **Character set**: SWIFT character set only
80/// 4. **Content**: Cannot be empty after delimiter
81/// 5. **Special characters**: Limited to SWIFT-approved characters
82/// 6. **Control characters**: Not permitted
83///
84/// ## Network Validated Rules (SWIFT Standards)
85/// - Account number cannot exceed 34 characters (Error: T14)
86/// - Must use SWIFT character set only (Error: T61)
87/// - Leading slash is mandatory (Error: T26)
88/// - Account identifier cannot be empty (Error: T13)
89/// - Field 56C alternative to 56A/56D (Error: C56)
90/// - Must be valid for receiving country's system (Error: T50)
91///
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
94pub struct Field56C {
95    /// Account number (up to 34 characters)
96    pub account_number: String,
97}
98
99impl Field56C {
100    /// Create a new Field56C with validation
101    pub fn new(account_number: impl Into<String>) -> Result<Self, crate::ParseError> {
102        let account_number = account_number.into().trim().to_string();
103
104        // Validate account number
105        if account_number.is_empty() {
106            return Err(crate::ParseError::InvalidFieldFormat {
107                field_tag: "56C".to_string(),
108                message: "Account number cannot be empty".to_string(),
109            });
110        }
111
112        if account_number.len() > 34 {
113            return Err(crate::ParseError::InvalidFieldFormat {
114                field_tag: "56C".to_string(),
115                message: "Account number cannot exceed 34 characters".to_string(),
116            });
117        }
118
119        if !account_number
120            .chars()
121            .all(|c| c.is_ascii() && !c.is_control())
122        {
123            return Err(crate::ParseError::InvalidFieldFormat {
124                field_tag: "56C".to_string(),
125                message: "Account number contains invalid characters".to_string(),
126            });
127        }
128
129        Ok(Field56C { account_number })
130    }
131
132    /// Get the account number
133    pub fn account_number(&self) -> &str {
134        &self.account_number
135    }
136
137    /// Get human-readable description
138    pub fn description(&self) -> String {
139        format!(
140            "Intermediary Institution (Account: {})",
141            self.account_number
142        )
143    }
144}
145
146impl SwiftField for Field56C {
147    fn parse(content: &str) -> crate::Result<Self> {
148        let content = content.trim();
149        if content.is_empty() {
150            return Err(crate::ParseError::InvalidFieldFormat {
151                field_tag: "56C".to_string(),
152                message: "Field content cannot be empty".to_string(),
153            });
154        }
155
156        let content = if let Some(stripped) = content.strip_prefix(":56C:") {
157            stripped
158        } else if let Some(stripped) = content.strip_prefix("56C:") {
159            stripped
160        } else {
161            content
162        };
163
164        // Remove leading slash if present
165        let account_number = if let Some(stripped) = content.strip_prefix('/') {
166            stripped
167        } else {
168            content
169        };
170
171        if account_number.is_empty() {
172            return Err(crate::ParseError::InvalidFieldFormat {
173                field_tag: "56C".to_string(),
174                message: "Account number is required".to_string(),
175            });
176        }
177
178        Field56C::new(account_number)
179    }
180
181    fn to_swift_string(&self) -> String {
182        format!(":56C:/{}", self.account_number)
183    }
184
185    fn validate(&self) -> ValidationResult {
186        // Validation is done in constructor
187        ValidationResult {
188            is_valid: true,
189            errors: Vec::new(),
190            warnings: Vec::new(),
191        }
192    }
193
194    fn format_spec() -> &'static str {
195        "/34x"
196    }
197}
198
199impl std::fmt::Display for Field56C {
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201        write!(f, "Account: {}", self.account_number)
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_field56c_creation() {
211        let field = Field56C::new("INTERMEDIARYACCT123456").unwrap();
212        assert_eq!(field.account_number(), "INTERMEDIARYACCT123456");
213    }
214
215    #[test]
216    fn test_field56c_parse() {
217        let field = Field56C::parse("/INTERMEDIARYACCT123456").unwrap();
218        assert_eq!(field.account_number(), "INTERMEDIARYACCT123456");
219    }
220
221    #[test]
222    fn test_field56c_parse_without_slash() {
223        let field = Field56C::parse("INTERMEDIARYACCT123456").unwrap();
224        assert_eq!(field.account_number(), "INTERMEDIARYACCT123456");
225    }
226
227    #[test]
228    fn test_field56c_parse_with_tag() {
229        let field = Field56C::parse(":56C:/INTERMEDIARYACCT123456").unwrap();
230        assert_eq!(field.account_number(), "INTERMEDIARYACCT123456");
231    }
232
233    #[test]
234    fn test_field56c_to_swift_string() {
235        let field = Field56C::new("INTERMEDIARYACCT123456").unwrap();
236        assert_eq!(field.to_swift_string(), ":56C:/INTERMEDIARYACCT123456");
237    }
238
239    #[test]
240    fn test_field56c_display() {
241        let field = Field56C::new("INTERMEDIARYACCT123456").unwrap();
242        assert_eq!(format!("{}", field), "Account: INTERMEDIARYACCT123456");
243    }
244
245    #[test]
246    fn test_field56c_description() {
247        let field = Field56C::new("INTERMEDIARYACCT123456").unwrap();
248        assert_eq!(
249            field.description(),
250            "Intermediary Institution (Account: INTERMEDIARYACCT123456)"
251        );
252    }
253
254    #[test]
255    fn test_field56c_validation_empty_account() {
256        let result = Field56C::new("");
257        assert!(result.is_err());
258        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
259    }
260
261    #[test]
262    fn test_field56c_validation_account_too_long() {
263        let account = "A".repeat(35); // 35 characters, max is 34
264        let result = Field56C::new(account);
265        assert!(result.is_err());
266        assert!(
267            result
268                .unwrap_err()
269                .to_string()
270                .contains("cannot exceed 34 characters")
271        );
272    }
273
274    #[test]
275    fn test_field56c_validation_invalid_characters() {
276        let result = Field56C::new("ACCOUNT\x00ID"); // Contains null character
277        assert!(result.is_err());
278        assert!(
279            result
280                .unwrap_err()
281                .to_string()
282                .contains("invalid characters")
283        );
284    }
285
286    #[test]
287    fn test_field56c_validate() {
288        let field = Field56C::new("INTERMEDIARYACCT123456").unwrap();
289        let validation = field.validate();
290        assert!(validation.is_valid);
291        assert!(validation.errors.is_empty());
292    }
293}