swift_mt_message/fields/
field53.rs

1use super::field_utils::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 53A: Sender's Correspondent (BIC with Party Identifier)**
8///
9/// Specifies the sender's correspondent bank for reimbursement.
10/// Format: [/1!a][/34x] + BIC (8 or 11 chars)
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct Field53A {
13    /// Optional party identifier (max 34 chars, nostro 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 Field53A {
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 53A 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 53A 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(Field53A {
53            party_identifier,
54            bic,
55        })
56    }
57
58    fn to_swift_string(&self) -> String {
59        let mut result = String::new();
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        format!(":53A:{}", result)
66    }
67}
68
69/// **Field 53B: Sender's Correspondent (Party Identifier with Location)**
70///
71/// Domestic correspondent routing with party identifier and location.
72/// Format: [/1!a][/34x] + [35x]
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74pub struct Field53B {
75    /// Optional party identifier (max 34 chars, nostro account)
76    pub party_identifier: Option<String>,
77
78    /// Location (max 35 chars)
79    pub location: Option<String>,
80}
81
82impl SwiftField for Field53B {
83    fn parse(input: &str) -> crate::Result<Self>
84    where
85        Self: Sized,
86    {
87        if input.is_empty() {
88            return Ok(Field53B {
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
98        // Field 53B format:
99        // If 2 lines: Line 1 = party_identifier, Line 2 = location
100        // If 1 line: Could be party_identifier OR location - use heuristics:
101        //   - Starts with '/' -> party_identifier
102        //   - Looks like BIC (8-11 uppercase alphanumeric) -> party_identifier
103        //   - Otherwise -> location
104        if lines.len() >= 2 {
105            // Two lines: first is party_identifier, second is location
106            if !lines[0].is_empty() {
107                party_identifier =
108                    Some(parse_max_length(lines[0], 34, "Field53B party_identifier")?);
109            }
110            if !lines[1].is_empty() {
111                location = Some(parse_max_length(lines[1], 35, "Field53B location")?);
112            }
113        } else if lines.len() == 1 && !lines[0].is_empty() {
114            let line = lines[0];
115
116            // Determine if single line is party_identifier or location
117            let is_party_identifier = line.starts_with('/')
118                || ((8..=11).contains(&line.len())
119                    && line
120                        .chars()
121                        .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit()));
122
123            if is_party_identifier {
124                party_identifier = Some(parse_max_length(line, 34, "Field53B party_identifier")?);
125            } else {
126                location = Some(parse_max_length(line, 35, "Field53B location")?);
127            }
128        }
129
130        Ok(Field53B {
131            party_identifier,
132            location,
133        })
134    }
135
136    fn to_swift_string(&self) -> String {
137        let mut result = String::new();
138        if let Some(ref party_id) = self.party_identifier {
139            result.push_str(party_id);
140            if self.location.is_some() {
141                result.push('\n');
142            }
143        }
144        if let Some(ref loc) = self.location {
145            result.push_str(loc);
146        }
147        format!(":53B:{}", result)
148    }
149}
150
151/// **Field 53D: Sender's Correspondent (Party Identifier with Name and Address)**
152///
153/// Detailed correspondent identification with name and address.
154/// Format: [/1!a][/34x] + 4*35x
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub struct Field53D {
157    /// Optional party identifier (max 34 chars, nostro account)
158    pub party_identifier: Option<String>,
159
160    /// Name and address (max 4 lines, 35 chars each)
161    pub name_and_address: Vec<String>,
162}
163
164impl SwiftField for Field53D {
165    fn parse(input: &str) -> crate::Result<Self>
166    where
167        Self: Sized,
168    {
169        let mut lines = input.split('\n').collect::<Vec<_>>();
170
171        if lines.is_empty() {
172            return Err(ParseError::InvalidFormat {
173                message: "Field 53D requires at least one line".to_string(),
174            });
175        }
176
177        let mut party_identifier = None;
178
179        // Check if first line is a party identifier
180        // Party identifier can be on its own line (with or without leading /)
181        // If first line starts with '/' and is short, it's a party identifier
182        if let Some(first_line) = lines.first() {
183            // If it starts with '/' or looks like an account identifier (short alphanumeric)
184            let looks_like_party_id = first_line.starts_with('/')
185                || (first_line.len() <= 34
186                    && !first_line.contains(' ')
187                    && first_line.chars().any(|c| c.is_ascii_digit()));
188
189            if looks_like_party_id && !first_line.is_empty() && lines.len() > 1 {
190                // Entire first line is party identifier
191                party_identifier = Some(first_line.to_string());
192                lines.remove(0);
193            }
194        }
195
196        // Parse remaining lines as name and address (max 4 lines, max 35 chars each)
197        let mut name_and_address = Vec::new();
198        for (i, line) in lines.iter().enumerate() {
199            if i >= 4 {
200                break;
201            }
202            if line.len() > 35 {
203                return Err(ParseError::InvalidFormat {
204                    message: format!("Field 53D line {} exceeds 35 characters", i + 1),
205                });
206            }
207            name_and_address.push(line.to_string());
208        }
209
210        if name_and_address.is_empty() {
211            return Err(ParseError::InvalidFormat {
212                message: "Field 53D must contain name and address information".to_string(),
213            });
214        }
215
216        Ok(Field53D {
217            party_identifier,
218            name_and_address,
219        })
220    }
221
222    fn to_swift_string(&self) -> String {
223        let mut result = String::new();
224        if let Some(ref party_id) = self.party_identifier {
225            result.push_str(party_id);
226            result.push('\n');
227        }
228        for (i, line) in self.name_and_address.iter().enumerate() {
229            if i > 0 {
230                result.push('\n');
231            }
232            result.push_str(line);
233        }
234        format!(":53D:{}", result)
235    }
236}
237
238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239pub enum Field53SenderCorrespondent {
240    #[serde(rename = "53A")]
241    A(Field53A),
242    #[serde(rename = "53B")]
243    B(Field53B),
244    #[serde(rename = "53D")]
245    D(Field53D),
246}
247
248impl SwiftField for Field53SenderCorrespondent {
249    fn parse(input: &str) -> crate::Result<Self>
250    where
251        Self: Sized,
252    {
253        // Try parsing as each variant
254        // A: Has BIC code (8 or 11 uppercase letters/digits)
255        // B: Has optional party identifier and/or location
256        // D: Has party identifier and/or multiple lines of name/address
257
258        let lines: Vec<&str> = input.split('\n').collect();
259        let last_line = lines.last().unwrap_or(&"");
260
261        // Check if last line looks like a BIC code
262        if (8..=11).contains(&last_line.len())
263            && last_line
264                .chars()
265                .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
266        {
267            // Try parsing as Field53A
268            if let Ok(field) = Field53A::parse(input) {
269                return Ok(Field53SenderCorrespondent::A(field));
270            }
271        }
272
273        // Check for multiple lines suggesting D format
274        if lines.len() > 2 || (lines.len() == 2 && !lines[0].starts_with('/')) {
275            // Try parsing as Field53D (multiple lines of name/address)
276            if let Ok(field) = Field53D::parse(input) {
277                return Ok(Field53SenderCorrespondent::D(field));
278            }
279        }
280
281        // Try parsing as Field53B (simpler format)
282        if let Ok(field) = Field53B::parse(input) {
283            return Ok(Field53SenderCorrespondent::B(field));
284        }
285
286        // If all fail, try in order
287        if let Ok(field) = Field53A::parse(input) {
288            return Ok(Field53SenderCorrespondent::A(field));
289        }
290        if let Ok(field) = Field53D::parse(input) {
291            return Ok(Field53SenderCorrespondent::D(field));
292        }
293
294        Err(ParseError::InvalidFormat {
295            message: "Field 53 could not be parsed as any valid option (A, B, or D)".to_string(),
296        })
297    }
298
299    fn parse_with_variant(
300        value: &str,
301        variant: Option<&str>,
302        _field_tag: Option<&str>,
303    ) -> crate::Result<Self>
304    where
305        Self: Sized,
306    {
307        match variant {
308            Some("A") => {
309                let field = Field53A::parse(value)?;
310                Ok(Field53SenderCorrespondent::A(field))
311            }
312            Some("B") => {
313                let field = Field53B::parse(value)?;
314                Ok(Field53SenderCorrespondent::B(field))
315            }
316            Some("D") => {
317                let field = Field53D::parse(value)?;
318                Ok(Field53SenderCorrespondent::D(field))
319            }
320            _ => {
321                // No variant specified, fall back to default parse behavior
322                Self::parse(value)
323            }
324        }
325    }
326
327    fn to_swift_string(&self) -> String {
328        match self {
329            Field53SenderCorrespondent::A(field) => field.to_swift_string(),
330            Field53SenderCorrespondent::B(field) => field.to_swift_string(),
331            Field53SenderCorrespondent::D(field) => field.to_swift_string(),
332        }
333    }
334
335    fn get_variant_tag(&self) -> Option<&'static str> {
336        match self {
337            Field53SenderCorrespondent::A(_) => Some("A"),
338            Field53SenderCorrespondent::B(_) => Some("B"),
339            Field53SenderCorrespondent::D(_) => Some("D"),
340        }
341    }
342}
343
344// Type alias for backward compatibility and simplicity
345pub type Field53 = Field53SenderCorrespondent;
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_field53a_valid() {
353        // Without party identifier
354        let field = Field53A::parse("CHASUS33XXX").unwrap();
355        assert_eq!(field.bic, "CHASUS33XXX");
356        assert_eq!(field.party_identifier, None);
357        assert_eq!(field.to_swift_string(), ":53A:CHASUS33XXX");
358
359        // With party identifier
360        let field = Field53A::parse("/C/12345678\nCHASUS33").unwrap();
361        assert_eq!(field.bic, "CHASUS33");
362        assert_eq!(field.party_identifier, Some("/C/12345678".to_string()));
363        assert_eq!(field.to_swift_string(), ":53A:/C/12345678\nCHASUS33");
364    }
365
366    #[test]
367    fn test_field53b_valid() {
368        // Only location
369        let field = Field53B::parse("NEW YORK BRANCH").unwrap();
370        assert_eq!(field.location, Some("NEW YORK BRANCH".to_string()));
371        assert_eq!(field.party_identifier, None);
372
373        // With party identifier
374        let field = Field53B::parse("/D/98765432\nNEW YORK").unwrap();
375        assert_eq!(field.party_identifier, Some("/D/98765432".to_string()));
376        assert_eq!(field.location, Some("NEW YORK".to_string()));
377
378        // Empty
379        let field = Field53B::parse("").unwrap();
380        assert_eq!(field.party_identifier, None);
381        assert_eq!(field.location, None);
382    }
383
384    #[test]
385    fn test_field53d_valid() {
386        // With party identifier and name/address
387        let field =
388            Field53D::parse("/C/12345678\nCORRESPONDENT BANK\n123 MAIN ST\nNEW YORK\nUSA").unwrap();
389        assert_eq!(field.party_identifier, Some("/C/12345678".to_string()));
390        assert_eq!(field.name_and_address.len(), 4);
391        assert_eq!(field.name_and_address[0], "CORRESPONDENT BANK");
392        assert_eq!(field.name_and_address[3], "USA");
393
394        // Without party identifier
395        let field = Field53D::parse("CORRESPONDENT BANK\nNEW YORK").unwrap();
396        assert_eq!(field.party_identifier, None);
397        assert_eq!(field.name_and_address.len(), 2);
398    }
399
400    #[test]
401    fn test_field53_enum() {
402        // Parse as A
403        let field = Field53SenderCorrespondent::parse("CHASUS33XXX").unwrap();
404        assert!(matches!(field, Field53SenderCorrespondent::A(_)));
405
406        // Parse as B
407        let field = Field53SenderCorrespondent::parse("NEW YORK BRANCH").unwrap();
408        assert!(matches!(field, Field53SenderCorrespondent::B(_)));
409
410        // Parse as D
411        let field = Field53SenderCorrespondent::parse("BANK NAME\nADDRESS LINE 1\nCITY").unwrap();
412        assert!(matches!(field, Field53SenderCorrespondent::D(_)));
413    }
414}