swift_mt_message/fields/
field55.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 55A: Third Reimbursement Institution (BIC with Party Identifier)**
8///
9/// Specifies the receiver's branch in complex reimbursement chains.
10/// Format: [/1!a][/34x] + BIC (8 or 11 chars)
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct Field55A {
13    /// Optional party identifier (max 34 chars, specialized account)
14    pub party_identifier: Option<String>,
15
16    /// BIC code (8 or 11 chars)
17    pub bic: String,
18}
19
20impl SwiftField for Field55A {
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 55A 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 55A 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(Field55A {
53            party_identifier,
54            bic,
55        })
56    }
57
58    fn to_swift_string(&self) -> String {
59        let mut result = String::from(":55A:");
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 55B: Third Reimbursement Institution (Party Identifier with Location)**
70///
71/// Domestic third institution routing with party identifier and location.
72/// Format: [/1!a][/34x] + [35x]
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74pub struct Field55B {
75    /// Optional party identifier (max 34 chars)
76    pub party_identifier: Option<String>,
77
78    /// Location (max 35 chars)
79    pub location: Option<String>,
80}
81
82impl SwiftField for Field55B {
83    fn parse(input: &str) -> crate::Result<Self>
84    where
85        Self: Sized,
86    {
87        if input.is_empty() {
88            return Ok(Field55B {
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, "Field55B location")?);
108        }
109
110        Ok(Field55B {
111            party_identifier,
112            location,
113        })
114    }
115
116    fn to_swift_string(&self) -> String {
117        let mut result = String::from(":55B:");
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 55D: Third Reimbursement Institution (Party Identifier with Name and Address)**
132///
133/// Detailed third institution identification with name and address.
134/// Format: [/1!a][/34x] + 4*35x
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub struct Field55D {
137    /// Optional party identifier (max 34 chars)
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 Field55D {
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 55D 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, "Field55D")?;
168
169        Ok(Field55D {
170            party_identifier,
171            name_and_address,
172        })
173    }
174
175    fn to_swift_string(&self) -> String {
176        let mut result = String::from(":55D:");
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 Field55ThirdReimbursementInstitution {
193    #[serde(rename = "55A")]
194    A(Field55A),
195    #[serde(rename = "55B")]
196    B(Field55B),
197    #[serde(rename = "55D")]
198    D(Field55D),
199}
200
201impl SwiftField for Field55ThirdReimbursementInstitution {
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 Field55A
221            if let Ok(field) = Field55A::parse(input) {
222                return Ok(Field55ThirdReimbursementInstitution::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 Field55D (multiple lines of name/address)
229            if let Ok(field) = Field55D::parse(input) {
230                return Ok(Field55ThirdReimbursementInstitution::D(field));
231            }
232        }
233
234        // Try parsing as Field55B (simpler format)
235        if let Ok(field) = Field55B::parse(input) {
236            return Ok(Field55ThirdReimbursementInstitution::B(field));
237        }
238
239        // If all fail, try in order
240        if let Ok(field) = Field55A::parse(input) {
241            return Ok(Field55ThirdReimbursementInstitution::A(field));
242        }
243        if let Ok(field) = Field55D::parse(input) {
244            return Ok(Field55ThirdReimbursementInstitution::D(field));
245        }
246
247        Err(ParseError::InvalidFormat {
248            message: "Field 55 could not be parsed as any valid option (A, B, or D)".to_string(),
249        })
250    }
251
252    fn parse_with_variant(
253        value: &str,
254        variant: Option<&str>,
255        _field_tag: Option<&str>,
256    ) -> crate::Result<Self>
257    where
258        Self: Sized,
259    {
260        match variant {
261            Some("A") => {
262                let field = Field55A::parse(value)?;
263                Ok(Field55ThirdReimbursementInstitution::A(field))
264            }
265            Some("B") => {
266                let field = Field55B::parse(value)?;
267                Ok(Field55ThirdReimbursementInstitution::B(field))
268            }
269            Some("D") => {
270                let field = Field55D::parse(value)?;
271                Ok(Field55ThirdReimbursementInstitution::D(field))
272            }
273            _ => {
274                // No variant specified, fall back to default parse behavior
275                Self::parse(value)
276            }
277        }
278    }
279
280    fn to_swift_string(&self) -> String {
281        match self {
282            Field55ThirdReimbursementInstitution::A(field) => field.to_swift_string(),
283            Field55ThirdReimbursementInstitution::B(field) => field.to_swift_string(),
284            Field55ThirdReimbursementInstitution::D(field) => field.to_swift_string(),
285        }
286    }
287
288    fn get_variant_tag(&self) -> Option<&'static str> {
289        match self {
290            Field55ThirdReimbursementInstitution::A(_) => Some("A"),
291            Field55ThirdReimbursementInstitution::B(_) => Some("B"),
292            Field55ThirdReimbursementInstitution::D(_) => Some("D"),
293        }
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_field55a_valid() {
303        // Without party identifier
304        let field = Field55A::parse("BNPAFRPPXXX").unwrap();
305        assert_eq!(field.bic, "BNPAFRPPXXX");
306        assert_eq!(field.party_identifier, None);
307        assert_eq!(field.to_swift_string(), ":55A:BNPAFRPPXXX");
308
309        // With party identifier
310        let field = Field55A::parse("/E/55566677\nBNPAFRPP").unwrap();
311        assert_eq!(field.bic, "BNPAFRPP");
312        assert_eq!(field.party_identifier, Some("/E/55566677".to_string()));
313        assert_eq!(field.to_swift_string(), ":55A:/E/55566677\nBNPAFRPP");
314    }
315
316    #[test]
317    fn test_field55b_valid() {
318        // Only location
319        let field = Field55B::parse("PARIS BRANCH").unwrap();
320        assert_eq!(field.location, Some("PARIS BRANCH".to_string()));
321        assert_eq!(field.party_identifier, None);
322
323        // With party identifier
324        let field = Field55B::parse("/F/99887766\nPARIS").unwrap();
325        assert_eq!(field.party_identifier, Some("/F/99887766".to_string()));
326        assert_eq!(field.location, Some("PARIS".to_string()));
327
328        // Empty
329        let field = Field55B::parse("").unwrap();
330        assert_eq!(field.party_identifier, None);
331        assert_eq!(field.location, None);
332    }
333
334    #[test]
335    fn test_field55d_valid() {
336        // With party identifier and name/address
337        let field =
338            Field55D::parse("/E/55566677\nTHIRD BANK\n789 THIRD ST\nPARIS\nFRANCE").unwrap();
339        assert_eq!(field.party_identifier, Some("/E/55566677".to_string()));
340        assert_eq!(field.name_and_address.len(), 4);
341        assert_eq!(field.name_and_address[0], "THIRD BANK");
342        assert_eq!(field.name_and_address[3], "FRANCE");
343
344        // Without party identifier
345        let field = Field55D::parse("THIRD BANK\nPARIS").unwrap();
346        assert_eq!(field.party_identifier, None);
347        assert_eq!(field.name_and_address.len(), 2);
348    }
349
350    #[test]
351    fn test_field55_enum() {
352        // Parse as A
353        let field = Field55ThirdReimbursementInstitution::parse("BNPAFRPPXXX").unwrap();
354        assert!(matches!(field, Field55ThirdReimbursementInstitution::A(_)));
355
356        // Parse as B
357        let field = Field55ThirdReimbursementInstitution::parse("PARIS BRANCH").unwrap();
358        assert!(matches!(field, Field55ThirdReimbursementInstitution::B(_)));
359
360        // Parse as D
361        let field =
362            Field55ThirdReimbursementInstitution::parse("BANK NAME\nADDRESS LINE 1\nCITY").unwrap();
363        assert!(matches!(field, Field55ThirdReimbursementInstitution::D(_)));
364    }
365}