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}