swift_mt_message/fields/
field58.rs

1use super::swift_utils::{parse_bic, parse_swift_chars};
2use crate::errors::ParseError;
3use crate::traits::SwiftField;
4use serde::{Deserialize, Serialize};
5
6/// **Field 58A: Beneficiary Institution (BIC with Party Identifier)**
7///
8/// Specifies beneficiary institution for institutional transfers.
9///
10/// **Format:** [/1!a][/34x] + BIC (8 or 11 chars)
11/// **Usage:** Central bank operations, institutional transfers, market infrastructure
12///
13/// **Example:**
14/// ```text
15/// :58A:DEUTDEFF
16/// :58A:/CHGS123456
17/// DEUTDEFF
18/// ```
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct Field58A {
21    /// Optional party identifier (max 34 chars, institutional account)
22    pub party_identifier: Option<String>,
23
24    /// BIC code (8 or 11 chars)
25    pub bic: String,
26}
27
28/// **Field 58D: Beneficiary Institution (Party Identifier with Name and Address)**
29///
30/// Detailed institutional beneficiary identification with name and address.
31///
32/// **Format:** [/1!a][/34x] + 4*35x
33/// **Extended Clearing Codes:** CH (CHIPS), CP (CHIPS Participant), FW (Fedwire), RU (Russian), SW (Swiss)
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct Field58D {
36    /// Optional party identifier (max 34 chars, may include CH, CP, FW, RU, SW codes)
37    pub party_identifier: Option<String>,
38
39    /// Name and address (max 4 lines, 35 chars each)
40    pub name_and_address: Vec<String>,
41}
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44pub enum Field58 {
45    #[serde(rename = "58A")]
46    A(Field58A),
47    #[serde(rename = "58D")]
48    D(Field58D),
49}
50
51impl SwiftField for Field58A {
52    fn parse(input: &str) -> crate::Result<Self>
53    where
54        Self: Sized,
55    {
56        let lines: Vec<&str> = input.lines().collect();
57
58        let mut party_identifier = None;
59        let mut bic_line_idx = 0;
60
61        // Check for optional party identifier on first line
62        if !lines.is_empty() && lines[0].starts_with('/') {
63            party_identifier = Some(lines[0][1..].to_string()); // Strip the leading / (format prefix)
64            bic_line_idx = 1;
65        }
66
67        // Ensure we have a BIC line
68        if bic_line_idx >= lines.len() {
69            return Err(ParseError::InvalidFormat {
70                message: "Field 58A missing BIC code".to_string(),
71            });
72        }
73
74        let bic = parse_bic(lines[bic_line_idx])?;
75
76        Ok(Field58A {
77            party_identifier,
78            bic,
79        })
80    }
81
82    fn to_swift_string(&self) -> String {
83        let mut result = ":58A:".to_string();
84
85        if let Some(ref party_id) = self.party_identifier {
86            result.push('/'); // Add format prefix
87            result.push_str(party_id);
88            result.push('\n');
89        }
90
91        result.push_str(&self.bic);
92        result
93    }
94}
95
96impl SwiftField for Field58D {
97    fn parse(input: &str) -> crate::Result<Self>
98    where
99        Self: Sized,
100    {
101        let mut lines = input.lines().collect::<Vec<_>>();
102        let mut party_identifier = None;
103
104        // Check if first line is a party identifier
105        // Party identifier can be on its own line (starting with /)
106        // If first line is short and there are more lines, it's likely a party identifier
107        if let Some(first_line) = lines.first() {
108            // Party identifier should start with / and be short (≤35 chars to account for the /)
109            if first_line.starts_with('/') && first_line.len() <= 35 && lines.len() > 1 {
110                // Entire first line is party identifier (strip the leading / format prefix)
111                party_identifier = Some(first_line[1..].to_string());
112                lines.remove(0);
113            }
114        }
115
116        // Parse name and address lines (max 4 lines, max 35 chars each)
117        let mut name_and_address = Vec::new();
118        for (i, line) in lines.iter().enumerate() {
119            if i >= 4 {
120                break;
121            }
122            if line.len() > 35 {
123                return Err(ParseError::InvalidFormat {
124                    message: format!("Field 58D line {} exceeds 35 characters", i + 1),
125                });
126            }
127            parse_swift_chars(line, &format!("Field 58D line {}", i + 1))?;
128            name_and_address.push(line.to_string());
129        }
130
131        if name_and_address.is_empty() {
132            return Err(ParseError::InvalidFormat {
133                message: "Field 58D must contain name and address information".to_string(),
134            });
135        }
136
137        Ok(Field58D {
138            party_identifier,
139            name_and_address,
140        })
141    }
142
143    fn to_swift_string(&self) -> String {
144        let mut result = ":58D:".to_string();
145
146        if let Some(ref party_id) = self.party_identifier {
147            result.push('/'); // Add format prefix
148            result.push_str(party_id);
149            result.push('\n');
150        }
151
152        result.push_str(&self.name_and_address.join("\n"));
153        result
154    }
155}
156
157impl SwiftField for Field58 {
158    fn parse(input: &str) -> crate::Result<Self>
159    where
160        Self: Sized,
161    {
162        // Try parsing as Field58A first (BIC-based)
163        if let Ok(field) = Field58A::parse(input) {
164            return Ok(Field58::A(field));
165        }
166
167        // Try parsing as Field58D (name and address)
168        if let Ok(field) = Field58D::parse(input) {
169            return Ok(Field58::D(field));
170        }
171
172        Err(ParseError::InvalidFormat {
173            message: "Field 58 could not be parsed as either option A or D".to_string(),
174        })
175    }
176
177    fn parse_with_variant(
178        value: &str,
179        variant: Option<&str>,
180        _field_tag: Option<&str>,
181    ) -> crate::Result<Self>
182    where
183        Self: Sized,
184    {
185        match variant {
186            Some("A") => {
187                let field = Field58A::parse(value)?;
188                Ok(Field58::A(field))
189            }
190            Some("D") => {
191                let field = Field58D::parse(value)?;
192                Ok(Field58::D(field))
193            }
194            _ => {
195                // No variant specified, fall back to default parse behavior
196                Self::parse(value)
197            }
198        }
199    }
200
201    fn to_swift_string(&self) -> String {
202        match self {
203            Field58::A(field) => field.to_swift_string(),
204            Field58::D(field) => field.to_swift_string(),
205        }
206    }
207
208    fn get_variant_tag(&self) -> Option<&'static str> {
209        match self {
210            Field58::A(_) => Some("A"),
211            Field58::D(_) => Some("D"),
212        }
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_field58a_parse_with_bic_only() {
222        let field = Field58A::parse("DEUTDEFF").unwrap();
223        assert_eq!(field.party_identifier, None);
224        assert_eq!(field.bic, "DEUTDEFF");
225    }
226
227    #[test]
228    fn test_field58a_parse_with_party_identifier() {
229        let field = Field58A::parse("/CHGS123456\nDEUTDEFF").unwrap();
230        assert_eq!(field.party_identifier, Some("CHGS123456".to_string()));
231        assert_eq!(field.bic, "DEUTDEFF");
232    }
233
234    #[test]
235    fn test_field58a_to_swift_string() {
236        let field = Field58A {
237            party_identifier: Some("CHGS123456".to_string()),
238            bic: "DEUTDEFF".to_string(),
239        };
240        assert_eq!(field.to_swift_string(), ":58A:/CHGS123456\nDEUTDEFF");
241    }
242
243    #[test]
244    fn test_field58d_parse_with_name_only() {
245        let input = "DEUTSCHE BANK AG\nFRANKFURT AM MAIN\nGERMANY";
246        let field = Field58D::parse(input).unwrap();
247        assert_eq!(field.party_identifier, None);
248        assert_eq!(field.name_and_address.len(), 3);
249        assert_eq!(field.name_and_address[0], "DEUTSCHE BANK AG");
250        assert_eq!(field.name_and_address[1], "FRANKFURT AM MAIN");
251        assert_eq!(field.name_and_address[2], "GERMANY");
252    }
253
254    #[test]
255    fn test_field58d_parse_with_party_identifier() {
256        let input = "/CH123456\nDEUTSCHE BANK AG\nFRANKFURT AM MAIN";
257        let field = Field58D::parse(input).unwrap();
258        assert_eq!(field.party_identifier, Some("CH123456".to_string()));
259        assert_eq!(field.name_and_address.len(), 2);
260        assert_eq!(field.name_and_address[0], "DEUTSCHE BANK AG");
261        assert_eq!(field.name_and_address[1], "FRANKFURT AM MAIN");
262    }
263
264    #[test]
265    fn test_field58d_line_too_long() {
266        let input = "THIS BANK NAME IS MUCH TOO LONG TO BE ACCEPTED IN FIELD 58D";
267        assert!(Field58D::parse(input).is_err());
268    }
269
270    #[test]
271    fn test_field58_enum_to_swift_string() {
272        let field_a = Field58::A(Field58A {
273            party_identifier: None,
274            bic: "DEUTDEFF".to_string(),
275        });
276        assert_eq!(field_a.to_swift_string(), ":58A:DEUTDEFF");
277
278        let field_d = Field58::D(Field58D {
279            party_identifier: None,
280            name_and_address: vec!["DEUTSCHE BANK AG".to_string()],
281        });
282        assert_eq!(field_d.to_swift_string(), ":58D:DEUTSCHE BANK AG");
283    }
284}