swift_mt_message/fields/
field59.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 59F: Party ID + Numbered Name/Address**
8///
9/// Structured beneficiary identification with numbered lines.
10/// Format: [/34x]4*(1!n/33x)
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct Field59F {
13    /// Optional party ID (max 34 chars)
14    pub party_identifier: Option<String>,
15    /// Numbered name/address lines (e.g., "1/ACME CORP")
16    pub name_and_address: Vec<String>,
17}
18
19/// **Field 59A: Account + BIC**
20///
21/// BIC-based beneficiary identification (STP-preferred).
22/// Format: [/34x] + BIC (8 or 11 chars)
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct Field59A {
25    /// Optional account (max 34 chars, IBAN or domestic)
26    pub account: Option<String>,
27    /// BIC code (8 or 11 chars)
28    pub bic: String,
29}
30
31/// **Field 59 (No Option): Account + Free-Format Name/Address**
32///
33/// Most common variant. Flexible beneficiary identification.
34/// Format: [/34x]4*35x
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct Field59NoOption {
37    /// Optional account (max 34 chars)
38    pub account: Option<String>,
39    /// Name/address (max 4 lines, 35 chars each)
40    pub name_and_address: Vec<String>,
41}
42
43/// **Field 59: Beneficiary Customer**
44///
45/// Final recipient of payment funds.
46///
47/// **Variants:**
48/// - **A:** Account + BIC (STP-preferred)
49/// - **F:** Party ID + numbered name/address
50/// - **No Option:** Account + free-format name/address (most common)
51///
52/// **Example:**
53/// ```text
54/// :59:/GB82WEST12345698765432
55/// JOHN SMITH
56/// 456 RESIDENTIAL AVENUE
57/// ```
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub enum Field59 {
60    #[serde(rename = "59A")]
61    A(Field59A),
62    #[serde(rename = "59F")]
63    F(Field59F),
64    #[serde(rename = "59")]
65    NoOption(Field59NoOption),
66}
67
68impl SwiftField for Field59F {
69    fn parse(input: &str) -> crate::Result<Self>
70    where
71        Self: Sized,
72    {
73        let lines: Vec<&str> = input.lines().collect();
74
75        if lines.is_empty() {
76            return Err(ParseError::InvalidFormat {
77                message: "Field 59F cannot be empty".to_string(),
78            });
79        }
80
81        let mut party_identifier = None;
82        let mut start_idx = 0;
83
84        // Check for party identifier on first line
85        if let Some(party_id) = parse_party_identifier(lines[0])? {
86            party_identifier = Some(party_id);
87            start_idx = 1;
88        }
89
90        // Parse name and address lines with line number format: 1!n/33x
91        let mut name_and_address = Vec::new();
92        for (i, line) in lines.iter().enumerate().skip(start_idx) {
93            // Check for line number format (1!n/33x)
94            let mut chars = line.chars();
95            let first_char = chars.next();
96            let second_char = chars.next();
97            if line.len() < 2 || !first_char.unwrap().is_ascii_digit() || second_char != Some('/') {
98                return Err(ParseError::InvalidFormat {
99                    message: format!(
100                        "Field 59F line {} must start with line number and slash (e.g., '1/')",
101                        i - start_idx + 1
102                    ),
103                });
104            }
105
106            let line_num = first_char.unwrap().to_digit(10).unwrap() as usize;
107            let expected_line_num = i - start_idx + 1;
108
109            if line_num != expected_line_num {
110                return Err(ParseError::InvalidFormat {
111                    message: format!(
112                        "Field 59F line number {} doesn't match expected {}",
113                        line_num, expected_line_num
114                    ),
115                });
116            }
117
118            let content = &line[2..];
119            if content.len() > 33 {
120                return Err(ParseError::InvalidFormat {
121                    message: format!("Field 59F line {} content exceeds 33 characters", line_num),
122                });
123            }
124
125            parse_swift_chars(content, &format!("Field 59F line {}", line_num))?;
126            name_and_address.push(line.to_string());
127        }
128
129        if name_and_address.is_empty() {
130            return Err(ParseError::InvalidFormat {
131                message: "Field 59F must have at least one name/address line".to_string(),
132            });
133        }
134
135        if name_and_address.len() > 4 {
136            return Err(ParseError::InvalidFormat {
137                message: format!(
138                    "Field 59F cannot have more than 4 name/address lines, found {}",
139                    name_and_address.len()
140                ),
141            });
142        }
143
144        Ok(Field59F {
145            party_identifier,
146            name_and_address,
147        })
148    }
149
150    fn to_swift_string(&self) -> String {
151        let mut result = String::from(":59F:");
152
153        if let Some(ref id) = self.party_identifier {
154            result.push_str(&format!("/{}\n", id));
155        }
156
157        for (i, line) in self.name_and_address.iter().enumerate() {
158            if i > 0 || self.party_identifier.is_some() {
159                result.push('\n');
160            }
161            result.push_str(line);
162        }
163
164        result
165    }
166}
167
168impl SwiftField for Field59A {
169    fn parse(input: &str) -> crate::Result<Self>
170    where
171        Self: Sized,
172    {
173        let lines: Vec<&str> = input.lines().collect();
174
175        if lines.is_empty() {
176            return Err(ParseError::InvalidFormat {
177                message: "Field 59A cannot be empty".to_string(),
178            });
179        }
180
181        let mut account = None;
182        let bic_line_idx;
183
184        // Check if first line is account (/...)
185        if lines[0].starts_with('/') {
186            let identifier = &lines[0][1..];
187            if identifier.len() <= 34 {
188                parse_swift_chars(identifier, "Field 59A account")?;
189                account = Some(identifier.to_string());
190                bic_line_idx = 1;
191            } else {
192                bic_line_idx = 0;
193            }
194        } else {
195            bic_line_idx = 0;
196        }
197
198        // Parse BIC
199        if bic_line_idx >= lines.len() {
200            return Err(ParseError::InvalidFormat {
201                message: "Field 59A missing BIC code".to_string(),
202            });
203        }
204
205        let bic = parse_bic(lines[bic_line_idx])?;
206
207        Ok(Field59A { account, bic })
208    }
209
210    fn to_swift_string(&self) -> String {
211        let mut result = String::from(":59A:");
212
213        if let Some(ref acc) = self.account {
214            result.push_str(&format!("/{}\n", acc));
215        }
216
217        result.push_str(&self.bic);
218        result
219    }
220}
221
222impl SwiftField for Field59NoOption {
223    fn parse(input: &str) -> crate::Result<Self>
224    where
225        Self: Sized,
226    {
227        let lines: Vec<&str> = input.lines().collect();
228
229        if lines.is_empty() {
230            return Err(ParseError::InvalidFormat {
231                message: "Field 59 (No Option) cannot be empty".to_string(),
232            });
233        }
234
235        let mut account = None;
236        let mut start_idx = 0;
237
238        // Check for account
239        if lines[0].starts_with('/') {
240            let identifier = &lines[0][1..];
241            if identifier.len() <= 34 {
242                parse_swift_chars(identifier, "Field 59 account")?;
243                account = Some(identifier.to_string());
244                start_idx = 1;
245            }
246        }
247
248        // Parse remaining lines as name and address
249        let name_and_address = parse_name_and_address(&lines, start_idx, "Field59NoOption")?;
250
251        Ok(Field59NoOption {
252            account,
253            name_and_address,
254        })
255    }
256
257    fn to_swift_string(&self) -> String {
258        let mut result = String::from(":59:");
259
260        if let Some(ref acc) = self.account {
261            result.push_str(&format!("/{}", acc));
262        }
263
264        for (i, line) in self.name_and_address.iter().enumerate() {
265            if i > 0 || (i == 0 && self.account.is_some()) {
266                result.push('\n');
267            }
268            result.push_str(line);
269        }
270
271        result
272    }
273}
274
275impl SwiftField for Field59 {
276    fn parse(input: &str) -> crate::Result<Self>
277    where
278        Self: Sized,
279    {
280        // Try Option A (BIC-based) first
281        if let Ok(field) = Field59A::parse(input) {
282            return Ok(Field59::A(field));
283        }
284
285        // Try Option F (structured name/address with line numbers)
286        // This is identifiable by the line number format (1/content, 2/content, etc.)
287        let lines: Vec<&str> = input.lines().collect();
288        if !lines.is_empty() {
289            // Check if any line (after optional account) has line number format
290            let check_start = if lines[0].starts_with('/') { 1 } else { 0 };
291            if check_start < lines.len() {
292                let test_line = lines[check_start];
293                let mut chars = test_line.chars();
294                if test_line.len() >= 2
295                    && chars.next().unwrap().is_ascii_digit()
296                    && chars.next() == Some('/')
297                    && let Ok(field) = Field59F::parse(input)
298                {
299                    return Ok(Field59::F(field));
300                }
301            }
302        }
303
304        // Try No Option (account + name/address)
305        if let Ok(field) = Field59NoOption::parse(input) {
306            return Ok(Field59::NoOption(field));
307        }
308
309        Err(ParseError::InvalidFormat {
310            message: "Field 59 could not be parsed as option A, F, or No Option".to_string(),
311        })
312    }
313
314    fn parse_with_variant(
315        value: &str,
316        variant: Option<&str>,
317        _field_tag: Option<&str>,
318    ) -> crate::Result<Self>
319    where
320        Self: Sized,
321    {
322        match variant {
323            None => {
324                let field = Field59NoOption::parse(value)?;
325                Ok(Field59::NoOption(field))
326            }
327            Some("A") => {
328                let field = Field59A::parse(value)?;
329                Ok(Field59::A(field))
330            }
331            Some("F") => {
332                let field = Field59F::parse(value)?;
333                Ok(Field59::F(field))
334            }
335            _ => {
336                // Unknown variant, fall back to default parse behavior
337                Self::parse(value)
338            }
339        }
340    }
341
342    fn to_swift_string(&self) -> String {
343        match self {
344            Field59::A(field) => field.to_swift_string(),
345            Field59::F(field) => field.to_swift_string(),
346            Field59::NoOption(field) => field.to_swift_string(),
347        }
348    }
349
350    fn get_variant_tag(&self) -> Option<&'static str> {
351        match self {
352            Field59::A(_) => Some("A"),
353            Field59::F(_) => Some("F"),
354            Field59::NoOption(_) => None, // No option doesn't have a variant letter
355        }
356    }
357}
358
359impl SwiftField for Field59Debtor {
360    fn parse(input: &str) -> crate::Result<Self>
361    where
362        Self: Sized,
363    {
364        // Try Option A (BIC-based) first
365        if let Ok(field) = Field59A::parse(input) {
366            return Ok(Field59Debtor::A(field));
367        }
368
369        // Try No Option (account + name/address)
370        if let Ok(field) = Field59NoOption::parse(input) {
371            return Ok(Field59Debtor::NoOption(field));
372        }
373
374        Err(ParseError::InvalidFormat {
375            message: "Field 59 Debtor could not be parsed as option A or No Option".to_string(),
376        })
377    }
378
379    fn parse_with_variant(
380        value: &str,
381        variant: Option<&str>,
382        _field_tag: Option<&str>,
383    ) -> crate::Result<Self>
384    where
385        Self: Sized,
386    {
387        match variant {
388            None => {
389                let field = Field59NoOption::parse(value)?;
390                Ok(Field59Debtor::NoOption(field))
391            }
392            Some("A") => {
393                let field = Field59A::parse(value)?;
394                Ok(Field59Debtor::A(field))
395            }
396            _ => {
397                // Unknown variant, fall back to default parse behavior
398                Self::parse(value)
399            }
400        }
401    }
402
403    fn to_swift_string(&self) -> String {
404        match self {
405            Field59Debtor::A(field) => field.to_swift_string(),
406            Field59Debtor::NoOption(field) => field.to_swift_string(),
407        }
408    }
409}
410
411#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
412pub enum Field59Debtor {
413    #[serde(rename = "59A")]
414    A(Field59A),
415    #[serde(rename = "59")]
416    NoOption(Field59NoOption),
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    #[test]
424    fn test_field59f() {
425        // With party identifier
426        let field = Field59F::parse("/GB82WEST12345698765432\n1/ACME CORPORATION LIMITED\n2/INTERNATIONAL TRADE DIVISION\n3/123 BUSINESS PARK AVENUE\n4/LONDON EC1A 1BB UNITED KINGDOM").unwrap();
427        assert_eq!(
428            field.party_identifier,
429            Some("GB82WEST12345698765432".to_string())
430        );
431        assert_eq!(field.name_and_address.len(), 4);
432        assert_eq!(field.name_and_address[0], "1/ACME CORPORATION LIMITED");
433
434        // Without party identifier
435        let field =
436            Field59F::parse("1/JOHN SMITH\n2/123 MAIN STREET\n3/LONDON\n4/UNITED KINGDOM").unwrap();
437        assert_eq!(field.party_identifier, None);
438        assert_eq!(field.name_and_address.len(), 4);
439    }
440
441    #[test]
442    fn test_field59a() {
443        // With account
444        let field = Field59A::parse("/GB82WEST12345698765432\nMIDLGB22XXX").unwrap();
445        assert_eq!(field.account, Some("GB82WEST12345698765432".to_string()));
446        assert_eq!(field.bic, "MIDLGB22XXX");
447
448        // Without account
449        let field = Field59A::parse("CHASUS33XXX").unwrap();
450        assert_eq!(field.account, None);
451        assert_eq!(field.bic, "CHASUS33XXX");
452    }
453
454    #[test]
455    fn test_field59_no_option() {
456        // With account
457        let field = Field59NoOption::parse("/GB82WEST12345698765432\nJOHN SMITH\n456 RESIDENTIAL AVENUE\nMANCHESTER M1 1AA\nUNITED KINGDOM").unwrap();
458        assert_eq!(field.account, Some("GB82WEST12345698765432".to_string()));
459        assert_eq!(field.name_and_address.len(), 4);
460        assert_eq!(field.name_and_address[0], "JOHN SMITH");
461
462        // Without account
463        let field = Field59NoOption::parse("JANE DOE\n789 MAIN STREET\nLONDON").unwrap();
464        assert_eq!(field.account, None);
465        assert_eq!(field.name_and_address.len(), 3);
466    }
467
468    #[test]
469    fn test_field59_invalid() {
470        // Invalid BIC
471        assert!(Field59A::parse("INVALID").is_err());
472
473        // Invalid line number in 59F
474        assert!(Field59F::parse("2/WRONG LINE NUMBER").is_err());
475
476        // Too many lines
477        assert!(Field59NoOption::parse("LINE1\nLINE2\nLINE3\nLINE4\nLINE5").is_err());
478    }
479}