swift_mt_message/fields/
field57.rs

1use super::field_utils::{parse_name_and_address, parse_party_identifier};
2use super::swift_utils::{parse_bic, parse_swift_chars};
3use crate::errors::ParseError;
4use crate::traits::SwiftField;
5use serde::{Deserialize, Serialize};
6
7/// **Field 57A: Account With Institution (BIC with Party Identifier)**
8///
9/// Specifies beneficiary's bank where account is maintained.
10///
11/// **Format:** [/1!a][/34x] + BIC (8 or 11 chars)
12/// **Payment Method Codes:** //FW (Fedwire), //RT (RTGS), //AU, //IN
13///
14/// **Example:**
15/// ```text
16/// :57A:CHASUS33XXX
17/// :57A://FW
18/// DEUTDEFF
19/// ```
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct Field57A {
22    /// Optional party identifier (max 34 chars, may include //FW, //RT, //AU, //IN codes)
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub party_identifier: Option<String>,
25
26    /// BIC code (8 or 11 chars)
27    pub bic: String,
28}
29
30impl SwiftField for Field57A {
31    fn parse(input: &str) -> crate::Result<Self>
32    where
33        Self: Sized,
34    {
35        let lines: Vec<&str> = input.lines().collect();
36
37        if lines.is_empty() {
38            return Err(ParseError::InvalidFormat {
39                message: "Field 57A cannot be empty".to_string(),
40            });
41        }
42
43        let mut party_identifier = None;
44        let mut bic_line_idx = 0;
45
46        // Check for optional party identifier on first line
47        if let Some(party_id) = parse_party_identifier(lines[0])? {
48            party_identifier = Some(party_id);
49            bic_line_idx = 1;
50        }
51
52        // Ensure we have a BIC line
53        if bic_line_idx >= lines.len() {
54            return Err(ParseError::InvalidFormat {
55                message: "Field 57A missing BIC code".to_string(),
56            });
57        }
58
59        let bic = parse_bic(lines[bic_line_idx])?;
60
61        Ok(Field57A {
62            party_identifier,
63            bic,
64        })
65    }
66
67    fn to_swift_string(&self) -> String {
68        let mut result = Vec::new();
69
70        if let Some(ref id) = self.party_identifier {
71            result.push(format!("/{}", id));
72        }
73
74        result.push(self.bic.clone());
75        format!(":57A:{}", result.join("\n"))
76    }
77}
78
79/// **Field 57B: Account With Institution (Party Identifier with Location)**
80///
81/// Domestic routing with party identifier and location.
82/// Format: [/1!a][/34x] + [35x]
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub struct Field57B {
85    /// Optional party identifier (max 34 chars)
86    pub party_identifier: Option<String>,
87
88    /// Location (max 35 chars)
89    pub location: Option<String>,
90}
91
92impl SwiftField for Field57B {
93    fn parse(input: &str) -> crate::Result<Self>
94    where
95        Self: Sized,
96    {
97        if input.is_empty() {
98            return Ok(Field57B {
99                party_identifier: None,
100                location: None,
101            });
102        }
103
104        let lines: Vec<&str> = input.lines().collect();
105        let mut party_identifier = None;
106        let mut location = None;
107        let mut current_idx = 0;
108
109        // Check for party identifier
110        if !lines.is_empty()
111            && let Some(party_id) = parse_party_identifier(lines[0])?
112        {
113            party_identifier = Some(party_id);
114            current_idx = 1;
115        }
116
117        // Check for location
118        if current_idx < lines.len() {
119            let loc = lines[current_idx];
120            if !loc.is_empty() && loc.len() <= 35 {
121                parse_swift_chars(loc, "Field 57B location")?;
122                location = Some(loc.to_string());
123            }
124        }
125
126        Ok(Field57B {
127            party_identifier,
128            location,
129        })
130    }
131
132    fn to_swift_string(&self) -> String {
133        let mut result = Vec::new();
134
135        if let Some(ref id) = self.party_identifier {
136            result.push(format!("/{}", id));
137        }
138
139        if let Some(ref loc) = self.location {
140            result.push(loc.clone());
141        }
142
143        format!(":57B:{}", result.join("\n"))
144    }
145}
146
147/// **Field 57C: Account With Institution (Party Identifier Only)**
148///
149/// Simplified institutional reference with party identifier only.
150/// Format: /34x (mandatory slash prefix)
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152pub struct Field57C {
153    /// Party identifier (1-34 chars, domestic/clearing codes)
154    pub party_identifier: String,
155}
156
157impl SwiftField for Field57C {
158    fn parse(input: &str) -> crate::Result<Self>
159    where
160        Self: Sized,
161    {
162        if !input.starts_with('/') {
163            return Err(ParseError::InvalidFormat {
164                message: "Field 57C must start with '/'".to_string(),
165            });
166        }
167
168        let identifier = &input[1..];
169
170        if identifier.is_empty() || identifier.len() > 34 {
171            return Err(ParseError::InvalidFormat {
172                message: "Field 57C party identifier must be 1-34 characters".to_string(),
173            });
174        }
175
176        parse_swift_chars(identifier, "Field 57C party identifier")?;
177
178        Ok(Field57C {
179            party_identifier: identifier.to_string(),
180        })
181    }
182
183    fn to_swift_string(&self) -> String {
184        format!(":57C:/{}", self.party_identifier)
185    }
186}
187
188/// **Field 57D: Account With Institution (Party Identifier with Name and Address)**
189///
190/// Detailed institutional identification with name and address.
191/// Format: [/1!a][/34x] + 4*35x
192#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
193pub struct Field57D {
194    /// Optional party identifier (max 34 chars, routing codes)
195    pub party_identifier: Option<String>,
196
197    /// Name and address (max 4 lines, 35 chars each)
198    pub name_and_address: Vec<String>,
199}
200
201impl SwiftField for Field57D {
202    fn parse(input: &str) -> crate::Result<Self>
203    where
204        Self: Sized,
205    {
206        let lines: Vec<&str> = input.lines().collect();
207
208        if lines.is_empty() {
209            return Err(ParseError::InvalidFormat {
210                message: "Field 57D must have at least one line".to_string(),
211            });
212        }
213
214        let mut party_identifier = None;
215        let mut start_idx = 0;
216
217        // Check for party identifier on first line
218        if let Some(party_id) = parse_party_identifier(lines[0])? {
219            party_identifier = Some(party_id);
220            start_idx = 1;
221        }
222
223        // Parse remaining lines as name and address
224        let name_and_address = parse_name_and_address(&lines, start_idx, "Field57D")?;
225
226        Ok(Field57D {
227            party_identifier,
228            name_and_address,
229        })
230    }
231
232    fn to_swift_string(&self) -> String {
233        let mut result = Vec::new();
234
235        if let Some(ref id) = self.party_identifier {
236            result.push(format!("/{}", id));
237        }
238
239        for line in &self.name_and_address {
240            result.push(line.clone());
241        }
242
243        format!(":57D:{}", result.join("\n"))
244    }
245}
246
247/// Enum for Field57 Account With Institution variants (A, B, C, D)
248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
249pub enum Field57 {
250    #[serde(rename = "57A")]
251    A(Field57A),
252    #[serde(rename = "57B")]
253    B(Field57B),
254    #[serde(rename = "57C")]
255    C(Field57C),
256    #[serde(rename = "57D")]
257    D(Field57D),
258}
259
260impl SwiftField for Field57 {
261    fn parse(input: &str) -> crate::Result<Self>
262    where
263        Self: Sized,
264    {
265        // Try Option A (BIC-based) first
266        if let Ok(field) = Field57A::parse(input) {
267            return Ok(Field57::A(field));
268        }
269
270        // Try Option C (party identifier only)
271        if input.starts_with('/')
272            && !input.contains('\n')
273            && let Ok(field) = Field57C::parse(input)
274        {
275            return Ok(Field57::C(field));
276        }
277
278        // Try Option B (party identifier with location)
279        if let Ok(field) = Field57B::parse(input) {
280            return Ok(Field57::B(field));
281        }
282
283        // Try Option D (party identifier with name/address)
284        if let Ok(field) = Field57D::parse(input) {
285            return Ok(Field57::D(field));
286        }
287
288        Err(ParseError::InvalidFormat {
289            message: "Field 57 could not be parsed as option A, B, C or D".to_string(),
290        })
291    }
292
293    fn parse_with_variant(
294        value: &str,
295        variant: Option<&str>,
296        _field_tag: Option<&str>,
297    ) -> crate::Result<Self>
298    where
299        Self: Sized,
300    {
301        match variant {
302            Some("A") => {
303                let field = Field57A::parse(value)?;
304                Ok(Field57::A(field))
305            }
306            Some("B") => {
307                let field = Field57B::parse(value)?;
308                Ok(Field57::B(field))
309            }
310            Some("C") => {
311                let field = Field57C::parse(value)?;
312                Ok(Field57::C(field))
313            }
314            Some("D") => {
315                let field = Field57D::parse(value)?;
316                Ok(Field57::D(field))
317            }
318            _ => {
319                // No variant specified, fall back to default parse behavior
320                Self::parse(value)
321            }
322        }
323    }
324
325    fn to_swift_string(&self) -> String {
326        match self {
327            Field57::A(field) => field.to_swift_string(),
328            Field57::B(field) => field.to_swift_string(),
329            Field57::C(field) => field.to_swift_string(),
330            Field57::D(field) => field.to_swift_string(),
331        }
332    }
333
334    fn get_variant_tag(&self) -> Option<&'static str> {
335        match self {
336            Field57::A(_) => Some("A"),
337            Field57::B(_) => Some("B"),
338            Field57::C(_) => Some("C"),
339            Field57::D(_) => Some("D"),
340        }
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_field57a() {
350        // With payment method code
351        let field = Field57A::parse("//FW123456\nDEUTDEFF").unwrap();
352        assert_eq!(field.party_identifier, Some("/FW123456".to_string()));
353        assert_eq!(field.bic, "DEUTDEFF");
354
355        // With standard party identifier
356        let field = Field57A::parse("/C/US123456\nDEUTDEFF").unwrap();
357        assert_eq!(field.party_identifier, Some("C/US123456".to_string()));
358        assert_eq!(field.bic, "DEUTDEFF");
359
360        // Without party identifier
361        let field = Field57A::parse("CHASUS33XXX").unwrap();
362        assert_eq!(field.party_identifier, None);
363        assert_eq!(field.bic, "CHASUS33XXX");
364    }
365
366    #[test]
367    fn test_field57b() {
368        // With party identifier and location
369        let field = Field57B::parse("/A/12345\nNEW YORK").unwrap();
370        assert_eq!(field.party_identifier, Some("A/12345".to_string()));
371        assert_eq!(field.location, Some("NEW YORK".to_string()));
372
373        // Empty
374        let field = Field57B::parse("").unwrap();
375        assert_eq!(field.party_identifier, None);
376        assert_eq!(field.location, None);
377    }
378
379    #[test]
380    fn test_field57c() {
381        let field = Field57C::parse("/UKCLEARING123").unwrap();
382        assert_eq!(field.party_identifier, "UKCLEARING123");
383        assert_eq!(field.to_swift_string(), ":57C:/UKCLEARING123");
384    }
385
386    #[test]
387    fn test_field57d() {
388        // With payment method code
389        let field = Field57D::parse("//FW\nCHASE BANK\nNEW YORK").unwrap();
390        assert_eq!(field.party_identifier, Some("/FW".to_string()));
391        assert_eq!(field.name_and_address.len(), 2);
392        assert_eq!(field.name_and_address[0], "CHASE BANK");
393
394        // Without party identifier
395        let field = Field57D::parse("BENEFICIARY BANK\nLONDON").unwrap();
396        assert_eq!(field.party_identifier, None);
397        assert_eq!(field.name_and_address.len(), 2);
398    }
399
400    #[test]
401    fn test_field57_payment_method_codes() {
402        // Fedwire code
403        let field = Field57A::parse("//FW\nCHASUS33").unwrap();
404        assert_eq!(field.party_identifier, Some("/FW".to_string()));
405
406        // RTGS code
407        let field = Field57A::parse("//RT\nDEUTDEFF").unwrap();
408        assert_eq!(field.party_identifier, Some("/RT".to_string()));
409
410        // Australian code
411        let field = Field57A::parse("//AU\nANZBAU3M").unwrap();
412        assert_eq!(field.party_identifier, Some("/AU".to_string()));
413
414        // Indian code
415        let field = Field57A::parse("//IN\nHDFCINBB").unwrap();
416        assert_eq!(field.party_identifier, Some("/IN".to_string()));
417    }
418
419    #[test]
420    fn test_field57_invalid() {
421        // Invalid BIC
422        assert!(Field57A::parse("INVALID").is_err());
423
424        // Missing slash in 57C
425        assert!(Field57C::parse("NOSLASH").is_err());
426
427        // Too many lines in 57D
428        assert!(Field57D::parse("LINE1\nLINE2\nLINE3\nLINE4\nLINE5").is_err());
429    }
430}
431
432// Type aliases for backward compatibility
433pub type Field57AccountWithInstitution = Field57;
434pub type Field57DebtorBank = Field57;
435
436/// Field57DebtInstitution: Account With Institution for MT200 and similar messages
437///
438/// Restricted enum supporting only variants A, B, and D per SWIFT specification.
439/// Used in MT200 where Field 57 is mandatory and limited to these options.
440#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
441pub enum Field57DebtInstitution {
442    #[serde(rename = "57A")]
443    A(Field57A),
444    #[serde(rename = "57B")]
445    B(Field57B),
446    #[serde(rename = "57D")]
447    D(Field57D),
448}
449
450impl SwiftField for Field57DebtInstitution {
451    fn parse(input: &str) -> crate::Result<Self>
452    where
453        Self: Sized,
454    {
455        // Try parsing as 57A (party identifier + BIC)
456        if let Ok(field) = Field57A::parse(input) {
457            return Ok(Field57DebtInstitution::A(field));
458        }
459
460        // Try parsing as 57B (party identifier only)
461        if let Ok(field) = Field57B::parse(input) {
462            return Ok(Field57DebtInstitution::B(field));
463        }
464
465        // Try parsing as 57D (name and address)
466        if let Ok(field) = Field57D::parse(input) {
467            return Ok(Field57DebtInstitution::D(field));
468        }
469
470        Err(ParseError::InvalidFormat {
471            message: "Field 57 must be one of formats: 57A (party + BIC), 57B (party + location), or 57D (name + address)".to_string(),
472        })
473    }
474
475    fn parse_with_variant(
476        value: &str,
477        variant: Option<&str>,
478        _field_tag: Option<&str>,
479    ) -> crate::Result<Self>
480    where
481        Self: Sized,
482    {
483        match variant {
484            Some("A") => {
485                let field = Field57A::parse(value)?;
486                Ok(Field57DebtInstitution::A(field))
487            }
488            Some("B") => {
489                let field = Field57B::parse(value)?;
490                Ok(Field57DebtInstitution::B(field))
491            }
492            Some("D") => {
493                let field = Field57D::parse(value)?;
494                Ok(Field57DebtInstitution::D(field))
495            }
496            _ => {
497                // No variant specified, fall back to default parse behavior
498                Self::parse(value)
499            }
500        }
501    }
502
503    fn to_swift_string(&self) -> String {
504        match self {
505            Field57DebtInstitution::A(field) => field.to_swift_string(),
506            Field57DebtInstitution::B(field) => field.to_swift_string(),
507            Field57DebtInstitution::D(field) => field.to_swift_string(),
508        }
509    }
510}
511
512/// Field57AccountWithABD: Account With Institution for MT291 and similar messages
513///
514/// Restricted enum supporting only variants A, B, and D per SWIFT specification.
515/// Used in MT291 where Field 57a is optional and limited to these options.
516/// This is an alias for Field57DebtInstitution with a more descriptive name.
517pub type Field57AccountWithABD = Field57DebtInstitution;