swift_mt_message/fields/
field54.rs

1use super::field_utils::{parse_name_and_address, parse_party_identifier};
2use super::swift_utils::{parse_bic, parse_max_length};
3use crate::errors::ParseError;
4use crate::traits::SwiftField;
5use serde::{Deserialize, Serialize};
6
7/// **Field 54A: Receiver's Correspondent (BIC with Party Identifier)**
8///
9/// Specifies the receiver's correspondent bank for fund availability.
10/// Format: [/1!a][/34x] + BIC (8 or 11 chars)
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct Field54A {
13    /// Optional party identifier (max 34 chars, vostro account ref)
14    pub party_identifier: Option<String>,
15
16    /// BIC code (8 or 11 chars)
17    pub bic: String,
18}
19
20impl SwiftField for Field54A {
21    fn parse(input: &str) -> crate::Result<Self>
22    where
23        Self: Sized,
24    {
25        let lines: Vec<&str> = input.split('\n').collect();
26
27        if lines.is_empty() {
28            return Err(ParseError::InvalidFormat {
29                message: "Field 54A requires input".to_string(),
30            });
31        }
32
33        let mut line_idx = 0;
34        let mut party_identifier = None;
35
36        // Check for optional party identifier on first line
37        if let Some(party_id) = parse_party_identifier(lines[0])? {
38            party_identifier = Some(format!("/{}", party_id));
39            line_idx = 1;
40        }
41
42        // Ensure we have a BIC line
43        if line_idx >= lines.len() {
44            return Err(ParseError::InvalidFormat {
45                message: "Field 54A requires BIC code after party identifier".to_string(),
46            });
47        }
48
49        // Parse BIC code
50        let bic = parse_bic(lines[line_idx])?;
51
52        Ok(Field54A {
53            party_identifier,
54            bic,
55        })
56    }
57
58    fn to_swift_string(&self) -> String {
59        let mut result = String::from(":54A:");
60        if let Some(ref party_id) = self.party_identifier {
61            result.push_str(party_id);
62            result.push('\n');
63        }
64        result.push_str(&self.bic);
65        result
66    }
67}
68
69/// **Field 54B: Receiver's Correspondent (Party Identifier with Location)**
70///
71/// Domestic receiver correspondent routing with party identifier and location.
72/// Format: [/1!a][/34x] + [35x]
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74pub struct Field54B {
75    /// Optional party identifier (max 34 chars, vostro account)
76    pub party_identifier: Option<String>,
77
78    /// Location (max 35 chars)
79    pub location: Option<String>,
80}
81
82impl SwiftField for Field54B {
83    fn parse(input: &str) -> crate::Result<Self>
84    where
85        Self: Sized,
86    {
87        if input.is_empty() {
88            return Ok(Field54B {
89                party_identifier: None,
90                location: None,
91            });
92        }
93
94        let lines: Vec<&str> = input.split('\n').collect();
95        let mut party_identifier = None;
96        let mut location = None;
97        let mut line_idx = 0;
98
99        // Check for party identifier on first line
100        if !lines.is_empty() && lines[0].starts_with('/') {
101            party_identifier = Some(lines[0].to_string());
102            line_idx = 1;
103        }
104
105        // Remaining line is location
106        if line_idx < lines.len() && !lines[line_idx].is_empty() {
107            location = Some(parse_max_length(lines[line_idx], 35, "Field54B location")?);
108        }
109
110        Ok(Field54B {
111            party_identifier,
112            location,
113        })
114    }
115
116    fn to_swift_string(&self) -> String {
117        let mut result = String::from(":54B:");
118        if let Some(ref party_id) = self.party_identifier {
119            result.push_str(party_id);
120            if self.location.is_some() {
121                result.push('\n');
122            }
123        }
124        if let Some(ref loc) = self.location {
125            result.push_str(loc);
126        }
127        result
128    }
129}
130
131/// **Field 54D: Receiver's Correspondent (Party Identifier with Name and Address)**
132///
133/// Detailed receiver correspondent identification with name and address.
134/// Format: [/1!a][/34x] + 4*35x
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub struct Field54D {
137    /// Optional party identifier (max 34 chars, vostro account)
138    pub party_identifier: Option<String>,
139
140    /// Name and address (max 4 lines, 35 chars each)
141    pub name_and_address: Vec<String>,
142}
143
144impl SwiftField for Field54D {
145    fn parse(input: &str) -> crate::Result<Self>
146    where
147        Self: Sized,
148    {
149        let lines: Vec<&str> = input.split('\n').collect();
150
151        if lines.is_empty() {
152            return Err(ParseError::InvalidFormat {
153                message: "Field 54D requires at least one line".to_string(),
154            });
155        }
156
157        let mut party_identifier = None;
158        let mut start_idx = 0;
159
160        // Check for party identifier on first line
161        if let Some(party_id) = parse_party_identifier(lines[0])? {
162            party_identifier = Some(format!("/{}", party_id));
163            start_idx = 1;
164        }
165
166        // Parse remaining lines as name and address
167        let name_and_address = parse_name_and_address(&lines, start_idx, "Field54D")?;
168
169        Ok(Field54D {
170            party_identifier,
171            name_and_address,
172        })
173    }
174
175    fn to_swift_string(&self) -> String {
176        let mut result = String::from(":54D:");
177        if let Some(ref party_id) = self.party_identifier {
178            result.push_str(party_id);
179            result.push('\n');
180        }
181        for (i, line) in self.name_and_address.iter().enumerate() {
182            if i > 0 {
183                result.push('\n');
184            }
185            result.push_str(line);
186        }
187        result
188    }
189}
190
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub enum Field54ReceiverCorrespondent {
193    #[serde(rename = "54A")]
194    A(Field54A),
195    #[serde(rename = "54B")]
196    B(Field54B),
197    #[serde(rename = "54D")]
198    D(Field54D),
199}
200
201impl SwiftField for Field54ReceiverCorrespondent {
202    fn parse(input: &str) -> crate::Result<Self>
203    where
204        Self: Sized,
205    {
206        // Try parsing as each variant
207        // A: Has BIC code (8 or 11 uppercase letters/digits)
208        // B: Has optional party identifier and/or location
209        // D: Has party identifier and/or multiple lines of name/address
210
211        let lines: Vec<&str> = input.split('\n').collect();
212        let last_line = lines.last().unwrap_or(&"");
213
214        // Check if last line looks like a BIC code
215        if (8..=11).contains(&last_line.len())
216            && last_line
217                .chars()
218                .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
219        {
220            // Try parsing as Field54A
221            if let Ok(field) = Field54A::parse(input) {
222                return Ok(Field54ReceiverCorrespondent::A(field));
223            }
224        }
225
226        // Check for multiple lines suggesting D format
227        if lines.len() > 2 || (lines.len() == 2 && !lines[0].starts_with('/')) {
228            // Try parsing as Field54D (multiple lines of name/address)
229            if let Ok(field) = Field54D::parse(input) {
230                return Ok(Field54ReceiverCorrespondent::D(field));
231            }
232        }
233
234        // Try parsing as Field54B (simpler format)
235        if let Ok(field) = Field54B::parse(input) {
236            return Ok(Field54ReceiverCorrespondent::B(field));
237        }
238
239        // If all fail, try in order
240        if let Ok(field) = Field54A::parse(input) {
241            return Ok(Field54ReceiverCorrespondent::A(field));
242        }
243        if let Ok(field) = Field54D::parse(input) {
244            return Ok(Field54ReceiverCorrespondent::D(field));
245        }
246
247        Err(ParseError::InvalidFormat {
248            message: "Field 54 could not be parsed as any valid option (A, B, or D)".to_string(),
249        })
250    }
251
252    fn to_swift_string(&self) -> String {
253        match self {
254            Field54ReceiverCorrespondent::A(field) => field.to_swift_string(),
255            Field54ReceiverCorrespondent::B(field) => field.to_swift_string(),
256            Field54ReceiverCorrespondent::D(field) => field.to_swift_string(),
257        }
258    }
259
260    fn get_variant_tag(&self) -> Option<&'static str> {
261        match self {
262            Field54ReceiverCorrespondent::A(_) => Some("A"),
263            Field54ReceiverCorrespondent::B(_) => Some("B"),
264            Field54ReceiverCorrespondent::D(_) => Some("D"),
265        }
266    }
267}
268
269// Type alias for backward compatibility and simplicity
270pub type Field54 = Field54ReceiverCorrespondent;
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_field54a_valid() {
278        // Without party identifier
279        let field = Field54A::parse("DEUTDEFFXXX").unwrap();
280        assert_eq!(field.bic, "DEUTDEFFXXX");
281        assert_eq!(field.party_identifier, None);
282        assert_eq!(field.to_swift_string(), ":54A:DEUTDEFFXXX");
283
284        // With party identifier
285        let field = Field54A::parse("/A/987654321\nDEUTDEFF").unwrap();
286        assert_eq!(field.bic, "DEUTDEFF");
287        assert_eq!(field.party_identifier, Some("/A/987654321".to_string()));
288        assert_eq!(field.to_swift_string(), ":54A:/A/987654321\nDEUTDEFF");
289    }
290
291    #[test]
292    fn test_field54b_valid() {
293        // Only location
294        let field = Field54B::parse("FRANKFURT BRANCH").unwrap();
295        assert_eq!(field.location, Some("FRANKFURT BRANCH".to_string()));
296        assert_eq!(field.party_identifier, None);
297
298        // With party identifier
299        let field = Field54B::parse("/B/11223344\nFRANKFURT").unwrap();
300        assert_eq!(field.party_identifier, Some("/B/11223344".to_string()));
301        assert_eq!(field.location, Some("FRANKFURT".to_string()));
302
303        // Empty
304        let field = Field54B::parse("").unwrap();
305        assert_eq!(field.party_identifier, None);
306        assert_eq!(field.location, None);
307    }
308
309    #[test]
310    fn test_field54d_valid() {
311        // With party identifier and name/address
312        let field = Field54D::parse("/A/987654321\nRECEIVER BANK\n456 BANK ST\nFRANKFURT\nGERMANY")
313            .unwrap();
314        assert_eq!(field.party_identifier, Some("/A/987654321".to_string()));
315        assert_eq!(field.name_and_address.len(), 4);
316        assert_eq!(field.name_and_address[0], "RECEIVER BANK");
317        assert_eq!(field.name_and_address[3], "GERMANY");
318
319        // Without party identifier
320        let field = Field54D::parse("RECEIVER BANK\nFRANKFURT").unwrap();
321        assert_eq!(field.party_identifier, None);
322        assert_eq!(field.name_and_address.len(), 2);
323    }
324
325    #[test]
326    fn test_field54_enum() {
327        // Parse as A
328        let field = Field54ReceiverCorrespondent::parse("DEUTDEFFXXX").unwrap();
329        assert!(matches!(field, Field54ReceiverCorrespondent::A(_)));
330
331        // Parse as B
332        let field = Field54ReceiverCorrespondent::parse("FRANKFURT BRANCH").unwrap();
333        assert!(matches!(field, Field54ReceiverCorrespondent::B(_)));
334
335        // Parse as D
336        let field = Field54ReceiverCorrespondent::parse("BANK NAME\nADDRESS LINE 1\nCITY").unwrap();
337        assert!(matches!(field, Field54ReceiverCorrespondent::D(_)));
338    }
339}