swift_mt_message/fields/
field57c.rs

1use crate::{SwiftField, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 57C: Account With Institution (Option C)
5///
6/// ## Overview
7/// Field 57C identifies the account with institution in SWIFT payment messages using an account
8/// number or identifier. This field provides a direct account-based identification method when
9/// the beneficiary's bank is identified through an account number, clearing code, or other
10/// identifier system. This option is particularly useful in domestic payment systems or when
11/// specific account-based routing is required for the final credit destination.
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**: Beneficiary's bank account 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 57C 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 for beneficiary banks
43/// - **Account-based identification**: When BIC is not available or preferred
44/// - **Clearing system integration**: Interfacing with local clearing systems
45/// - **Direct account crediting**: Specifying exact account for final credit
46/// - **Cost optimization**: Reducing correspondent banking complexity
47/// - **Regional payments**: Supporting regional payment networks
48///
49/// ## Examples
50/// ```text
51/// :57C:/BENEFICIARYACCT123456
52/// └─── Beneficiary's bank account number
53///
54/// :57C:/CLRCODE987654321
55/// └─── Clearing code for beneficiary's bank
56///
57/// :57C:/FEDWIRE021000021
58/// └─── US Federal Reserve routing number
59///
60/// :57C:/SORTCODE654321
61/// └─── UK sort code for beneficiary's bank
62///
63/// :57C:/IBAN12345678901234567890
64/// └─── International Bank Account Number
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/// - **IBAN**: International Bank Account Numbers
76///
77/// ## Validation Rules
78/// 1. **Length**: Maximum 34 characters
79/// 2. **Format**: Must start with forward slash (/)
80/// 3. **Character set**: SWIFT character set only
81/// 4. **Content**: Cannot be empty after delimiter
82/// 5. **Special characters**: Limited to SWIFT-approved characters
83/// 6. **Control characters**: Not permitted
84///
85/// ## Network Validated Rules (SWIFT Standards)
86/// - Account number cannot exceed 34 characters (Error: T14)
87/// - Must use SWIFT character set only (Error: T61)
88/// - Leading slash is mandatory (Error: T26)
89/// - Account identifier cannot be empty (Error: T13)
90/// - Field 57C alternative to 57A/57B/57D (Error: C57)
91/// - Must be valid for receiving country's system (Error: T50)
92/// - Account format must be recognizable (Error: T51)
93///
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96pub struct Field57C {
97    /// Account number (up to 34 characters)
98    pub account_number: String,
99}
100
101impl Field57C {
102    /// Create a new Field57C with validation
103    pub fn new(account_number: impl Into<String>) -> Result<Self, crate::ParseError> {
104        let account_number = account_number.into().trim().to_string();
105
106        // Validate account number
107        if account_number.is_empty() {
108            return Err(crate::ParseError::InvalidFieldFormat {
109                field_tag: "57C".to_string(),
110                message: "Account number cannot be empty".to_string(),
111            });
112        }
113
114        if account_number.len() > 34 {
115            return Err(crate::ParseError::InvalidFieldFormat {
116                field_tag: "57C".to_string(),
117                message: "Account number cannot exceed 34 characters".to_string(),
118            });
119        }
120
121        if !account_number
122            .chars()
123            .all(|c| c.is_ascii() && !c.is_control())
124        {
125            return Err(crate::ParseError::InvalidFieldFormat {
126                field_tag: "57C".to_string(),
127                message: "Account number contains invalid characters".to_string(),
128            });
129        }
130
131        Ok(Field57C { account_number })
132    }
133
134    /// Get the account number
135    pub fn account_number(&self) -> &str {
136        &self.account_number
137    }
138
139    /// Get human-readable description
140    pub fn description(&self) -> String {
141        format!(
142            "Account With Institution (Account: {})",
143            self.account_number
144        )
145    }
146}
147
148impl SwiftField for Field57C {
149    fn parse(content: &str) -> crate::Result<Self> {
150        let content = content.trim();
151        if content.is_empty() {
152            return Err(crate::ParseError::InvalidFieldFormat {
153                field_tag: "57C".to_string(),
154                message: "Field content cannot be empty".to_string(),
155            });
156        }
157
158        let content = if let Some(stripped) = content.strip_prefix(":57C:") {
159            stripped
160        } else if let Some(stripped) = content.strip_prefix("57C:") {
161            stripped
162        } else {
163            content
164        };
165
166        // Remove leading slash if present
167        let account_number = if let Some(stripped) = content.strip_prefix('/') {
168            stripped
169        } else {
170            content
171        };
172
173        Field57C::new(account_number)
174    }
175
176    fn to_swift_string(&self) -> String {
177        format!(":57C:/{}", self.account_number)
178    }
179
180    fn validate(&self) -> ValidationResult {
181        // Validation is done in constructor
182        ValidationResult {
183            is_valid: true,
184            errors: Vec::new(),
185            warnings: Vec::new(),
186        }
187    }
188
189    fn format_spec() -> &'static str {
190        "/34x"
191    }
192}
193
194impl std::fmt::Display for Field57C {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        write!(f, "Account: {}", self.account_number)
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_field57c_creation() {
206        let field = Field57C::new("ACCT123456789").unwrap();
207        assert_eq!(field.account_number(), "ACCT123456789");
208    }
209
210    #[test]
211    fn test_field57c_parse_basic() {
212        let field = Field57C::parse("/ACCT123456789").unwrap();
213        assert_eq!(field.account_number(), "ACCT123456789");
214    }
215
216    #[test]
217    fn test_field57c_parse_without_slash() {
218        let field = Field57C::parse("ACCT123456789").unwrap();
219        assert_eq!(field.account_number(), "ACCT123456789");
220    }
221
222    #[test]
223    fn test_field57c_parse_with_tag() {
224        let field = Field57C::parse(":57C:/ACCT123456789").unwrap();
225        assert_eq!(field.account_number(), "ACCT123456789");
226    }
227
228    #[test]
229    fn test_field57c_to_swift_string() {
230        let field = Field57C::new("ACCT123456789").unwrap();
231        assert_eq!(field.to_swift_string(), ":57C:/ACCT123456789");
232    }
233
234    #[test]
235    fn test_field57c_display() {
236        let field = Field57C::new("ACCT123456789").unwrap();
237        assert_eq!(format!("{}", field), "Account: ACCT123456789");
238    }
239
240    #[test]
241    fn test_field57c_description() {
242        let field = Field57C::new("ACCT123456789").unwrap();
243        assert_eq!(
244            field.description(),
245            "Account With Institution (Account: ACCT123456789)"
246        );
247    }
248
249    #[test]
250    fn test_field57c_validation_empty() {
251        let result = Field57C::new("");
252        assert!(result.is_err());
253        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
254    }
255
256    #[test]
257    fn test_field57c_validation_too_long() {
258        let account = "A".repeat(35); // 35 characters, max is 34
259        let result = Field57C::new(account);
260        assert!(result.is_err());
261        assert!(
262            result
263                .unwrap_err()
264                .to_string()
265                .contains("cannot exceed 34 characters")
266        );
267    }
268
269    #[test]
270    fn test_field57c_validation_invalid_characters() {
271        let result = Field57C::new("ACCT\x00123"); // Contains null character
272        assert!(result.is_err());
273        assert!(
274            result
275                .unwrap_err()
276                .to_string()
277                .contains("invalid characters")
278        );
279    }
280
281    #[test]
282    fn test_field57c_validate() {
283        let field = Field57C::new("ACCT123456789").unwrap();
284        let validation = field.validate();
285        assert!(validation.is_valid);
286        assert!(validation.errors.is_empty());
287    }
288}