swift_mt_message/fields/
field52.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 52A: Ordering Institution (BIC with Party Identifier)**
8///
9/// Identifies the ordering customer's bank when different from message sender.
10/// Format: [/1!a][/34x] + BIC (8 or 11 chars)
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct Field52A {
13    /// Optional party identifier (max 34 chars, clearing/account ref)
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub party_identifier: Option<String>,
16
17    /// BIC code (8 or 11 chars)
18    pub bic: String,
19}
20
21impl SwiftField for Field52A {
22    fn parse(input: &str) -> crate::Result<Self>
23    where
24        Self: Sized,
25    {
26        let lines: Vec<&str> = input.lines().collect();
27
28        if lines.is_empty() {
29            return Err(ParseError::InvalidFormat {
30                message: "Field 52A cannot be empty".to_string(),
31            });
32        }
33
34        let mut party_identifier = None;
35        let mut bic_line_idx = 0;
36
37        // Check for optional party identifier on first line
38        if let Some(party_id) = parse_party_identifier(lines[0])? {
39            party_identifier = Some(party_id);
40            bic_line_idx = 1;
41        }
42
43        // Parse BIC
44        if bic_line_idx >= lines.len() {
45            return Err(ParseError::InvalidFormat {
46                message: "Field 52A missing BIC code".to_string(),
47            });
48        }
49
50        let bic = parse_bic(lines[bic_line_idx])?;
51
52        Ok(Field52A {
53            party_identifier,
54            bic,
55        })
56    }
57
58    fn to_swift_string(&self) -> String {
59        let mut lines = Vec::new();
60
61        if let Some(ref id) = self.party_identifier {
62            lines.push(format!("/{}", id));
63        }
64
65        lines.push(self.bic.clone());
66        format!(":52A:{}", lines.join("\n"))
67    }
68}
69
70/// **Field 52B: Ordering Institution (Party Identifier with Location)**
71///
72/// Domestic routing using party identifier and location.
73/// Format: [/1!a][/34x] + [35x]
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct Field52B {
76    /// Optional party identifier (max 34 chars)
77    pub party_identifier: Option<String>,
78
79    /// Location (max 35 chars)
80    pub location: Option<String>,
81}
82
83impl SwiftField for Field52B {
84    fn parse(input: &str) -> crate::Result<Self>
85    where
86        Self: Sized,
87    {
88        if input.is_empty() {
89            return Ok(Field52B {
90                party_identifier: None,
91                location: None,
92            });
93        }
94
95        let lines: Vec<&str> = input.lines().collect();
96        let mut party_identifier = None;
97        let mut location = None;
98        let mut current_idx = 0;
99
100        // Check for party identifier
101        if !lines.is_empty() && lines[0].starts_with('/') {
102            let line = &lines[0][1..]; // Remove leading /
103
104            // Check if it's /1!a/34x format
105            if let Some(slash_pos) = line.find('/') {
106                let code = &line[..slash_pos];
107                let id = &line[slash_pos + 1..];
108
109                if code.len() == 1
110                    && code.chars().all(|c| c.is_ascii_alphabetic())
111                    && id.len() <= 34
112                {
113                    parse_swift_chars(id, "Field 52B party identifier")?;
114                    party_identifier = Some(format!("{}/{}", code, id));
115                    current_idx = 1;
116                }
117            } else if line.len() <= 34 {
118                // Just /34x format
119                parse_swift_chars(line, "Field 52B party identifier")?;
120                party_identifier = Some(line.to_string());
121                current_idx = 1;
122            }
123        }
124
125        // Check for location
126        if current_idx < lines.len() {
127            let loc = lines[current_idx];
128            if !loc.is_empty() && loc.len() <= 35 {
129                parse_swift_chars(loc, "Field 52B location")?;
130                location = Some(loc.to_string());
131            }
132        }
133
134        Ok(Field52B {
135            party_identifier,
136            location,
137        })
138    }
139
140    fn to_swift_string(&self) -> String {
141        let mut result = Vec::new();
142
143        if let Some(ref id) = self.party_identifier {
144            result.push(format!("/{}", id));
145        }
146
147        if let Some(ref loc) = self.location {
148            result.push(loc.clone());
149        }
150
151        format!(":52B:{}", result.join("\n"))
152    }
153}
154
155/// **Field 52C: Ordering Institution (Party Identifier Only)**
156///
157/// Simplified institutional reference with party identifier only.
158/// Format: /34x (mandatory slash prefix)
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct Field52C {
161    /// Party identifier (1-34 chars, domestic/clearing codes)
162    pub party_identifier: String,
163}
164
165impl SwiftField for Field52C {
166    fn parse(input: &str) -> crate::Result<Self>
167    where
168        Self: Sized,
169    {
170        if !input.starts_with('/') {
171            return Err(ParseError::InvalidFormat {
172                message: "Field 52C must start with '/'".to_string(),
173            });
174        }
175
176        let identifier = &input[1..];
177
178        if identifier.is_empty() || identifier.len() > 34 {
179            return Err(ParseError::InvalidFormat {
180                message: "Field 52C party identifier must be 1-34 characters".to_string(),
181            });
182        }
183
184        parse_swift_chars(identifier, "Field 52C party identifier")?;
185
186        Ok(Field52C {
187            party_identifier: identifier.to_string(),
188        })
189    }
190
191    fn to_swift_string(&self) -> String {
192        format!(":52C:/{}", self.party_identifier)
193    }
194}
195
196/// **Field 52D: Ordering Institution (Party Identifier with Name and Address)**
197///
198/// Detailed institutional identification with full name and address.
199/// Format: [/1!a][/34x] + 4*35x
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201pub struct Field52D {
202    /// Optional party identifier (max 34 chars)
203    pub party_identifier: Option<String>,
204
205    /// Name and address (max 4 lines, 35 chars each)
206    pub name_and_address: Vec<String>,
207}
208
209impl SwiftField for Field52D {
210    fn parse(input: &str) -> crate::Result<Self>
211    where
212        Self: Sized,
213    {
214        let lines: Vec<&str> = input.lines().collect();
215
216        if lines.is_empty() {
217            return Err(ParseError::InvalidFormat {
218                message: "Field 52D must have at least one line".to_string(),
219            });
220        }
221
222        let mut party_identifier = None;
223        let mut start_idx = 0;
224
225        // Check for party identifier
226        if lines[0].starts_with('/') {
227            let line = &lines[0][1..]; // Remove leading /
228
229            // Check if it's /1!a/34x format
230            if let Some(slash_pos) = line.find('/') {
231                let code = &line[..slash_pos];
232                let id = &line[slash_pos + 1..];
233
234                if code.len() == 1
235                    && code.chars().all(|c| c.is_ascii_alphabetic())
236                    && id.len() <= 34
237                {
238                    parse_swift_chars(id, "Field 52D party identifier")?;
239                    party_identifier = Some(format!("{}/{}", code, id));
240                    start_idx = 1;
241                }
242            } else if line.len() <= 34 {
243                // Just /34x format
244                parse_swift_chars(line, "Field 52D party identifier")?;
245                party_identifier = Some(line.to_string());
246                start_idx = 1;
247            }
248        }
249
250        // Parse name and address lines
251        let name_and_address = parse_name_and_address(&lines, start_idx, "Field 52D")?;
252
253        Ok(Field52D {
254            party_identifier,
255            name_and_address,
256        })
257    }
258
259    fn to_swift_string(&self) -> String {
260        let mut lines = Vec::new();
261
262        if let Some(ref id) = self.party_identifier {
263            lines.push(format!("/{}", id));
264        }
265
266        for line in &self.name_and_address {
267            lines.push(line.clone());
268        }
269
270        format!(":52D:{}", lines.join("\n"))
271    }
272}
273
274/// Enum for Field52 Account Servicing Institution variants (A, C)
275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
276pub enum Field52AccountServicingInstitution {
277    #[serde(rename = "52A")]
278    A(Field52A),
279    #[serde(rename = "52C")]
280    C(Field52C),
281}
282
283impl SwiftField for Field52AccountServicingInstitution {
284    fn parse(input: &str) -> crate::Result<Self>
285    where
286        Self: Sized,
287    {
288        // Try Option A (BIC-based)
289        if let Ok(field) = Field52A::parse(input) {
290            return Ok(Field52AccountServicingInstitution::A(field));
291        }
292
293        // Try Option C (party identifier only)
294        if let Ok(field) = Field52C::parse(input) {
295            return Ok(Field52AccountServicingInstitution::C(field));
296        }
297
298        Err(ParseError::InvalidFormat {
299            message: "Field 52 Account Servicing Institution could not be parsed as option A or C"
300                .to_string(),
301        })
302    }
303
304    fn parse_with_variant(
305        value: &str,
306        variant: Option<&str>,
307        _field_tag: Option<&str>,
308    ) -> crate::Result<Self>
309    where
310        Self: Sized,
311    {
312        match variant {
313            Some("A") => {
314                let field = Field52A::parse(value)?;
315                Ok(Field52AccountServicingInstitution::A(field))
316            }
317            Some("C") => {
318                let field = Field52C::parse(value)?;
319                Ok(Field52AccountServicingInstitution::C(field))
320            }
321            _ => {
322                // No variant specified, fall back to default parse behavior
323                Self::parse(value)
324            }
325        }
326    }
327
328    fn to_swift_string(&self) -> String {
329        match self {
330            Field52AccountServicingInstitution::A(field) => field.to_swift_string(),
331            Field52AccountServicingInstitution::C(field) => field.to_swift_string(),
332        }
333    }
334}
335
336/// Enum for Field52 Ordering Institution variants (A, D)
337#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
338pub enum Field52OrderingInstitution {
339    #[serde(rename = "52A")]
340    A(Field52A),
341    #[serde(rename = "52D")]
342    D(Field52D),
343}
344
345impl SwiftField for Field52OrderingInstitution {
346    fn parse(input: &str) -> crate::Result<Self>
347    where
348        Self: Sized,
349    {
350        // Try Option A (BIC-based) first
351        if let Ok(field) = Field52A::parse(input) {
352            return Ok(Field52OrderingInstitution::A(field));
353        }
354
355        // Try Option D (party identifier with name/address)
356        if let Ok(field) = Field52D::parse(input) {
357            return Ok(Field52OrderingInstitution::D(field));
358        }
359
360        Err(ParseError::InvalidFormat {
361            message: "Field 52 Ordering Institution could not be parsed as option A or D"
362                .to_string(),
363        })
364    }
365
366    fn parse_with_variant(
367        value: &str,
368        variant: Option<&str>,
369        _field_tag: Option<&str>,
370    ) -> crate::Result<Self>
371    where
372        Self: Sized,
373    {
374        match variant {
375            Some("A") => {
376                let field = Field52A::parse(value)?;
377                Ok(Field52OrderingInstitution::A(field))
378            }
379            Some("D") => {
380                let field = Field52D::parse(value)?;
381                Ok(Field52OrderingInstitution::D(field))
382            }
383            _ => {
384                // No variant specified, fall back to default parse behavior
385                Self::parse(value)
386            }
387        }
388    }
389
390    fn to_swift_string(&self) -> String {
391        match self {
392            Field52OrderingInstitution::A(field) => field.to_swift_string(),
393            Field52OrderingInstitution::D(field) => field.to_swift_string(),
394        }
395    }
396
397    fn get_variant_tag(&self) -> Option<&'static str> {
398        match self {
399            Field52OrderingInstitution::A(_) => Some("A"),
400            Field52OrderingInstitution::D(_) => Some("D"),
401        }
402    }
403}
404
405/// Enum for Field52 Creditor Bank variants (A, C, D)
406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
407pub enum Field52CreditorBank {
408    #[serde(rename = "52A")]
409    A(Field52A),
410    #[serde(rename = "52C")]
411    C(Field52C),
412    #[serde(rename = "52D")]
413    D(Field52D),
414}
415
416impl SwiftField for Field52CreditorBank {
417    fn parse(input: &str) -> crate::Result<Self>
418    where
419        Self: Sized,
420    {
421        // Try Option A (BIC-based) first
422        if let Ok(field) = Field52A::parse(input) {
423            return Ok(Field52CreditorBank::A(field));
424        }
425
426        // Try Option C (party identifier only)
427        if input.starts_with('/')
428            && !input.contains('\n')
429            && let Ok(field) = Field52C::parse(input)
430        {
431            return Ok(Field52CreditorBank::C(field));
432        }
433
434        // Try Option D (party identifier with name/address)
435        if let Ok(field) = Field52D::parse(input) {
436            return Ok(Field52CreditorBank::D(field));
437        }
438
439        Err(ParseError::InvalidFormat {
440            message: "Field 52 Creditor Bank could not be parsed as option A, C or D".to_string(),
441        })
442    }
443
444    fn parse_with_variant(
445        value: &str,
446        variant: Option<&str>,
447        _field_tag: Option<&str>,
448    ) -> crate::Result<Self>
449    where
450        Self: Sized,
451    {
452        match variant {
453            Some("A") => {
454                let field = Field52A::parse(value)?;
455                Ok(Field52CreditorBank::A(field))
456            }
457            Some("C") => {
458                let field = Field52C::parse(value)?;
459                Ok(Field52CreditorBank::C(field))
460            }
461            Some("D") => {
462                let field = Field52D::parse(value)?;
463                Ok(Field52CreditorBank::D(field))
464            }
465            _ => {
466                // No variant specified, fall back to default parse behavior
467                Self::parse(value)
468            }
469        }
470    }
471
472    fn to_swift_string(&self) -> String {
473        match self {
474            Field52CreditorBank::A(field) => field.to_swift_string(),
475            Field52CreditorBank::C(field) => field.to_swift_string(),
476            Field52CreditorBank::D(field) => field.to_swift_string(),
477        }
478    }
479}
480
481/// Enum for Field52 Drawer Bank variants (A, B, D)
482#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
483pub enum Field52DrawerBank {
484    #[serde(rename = "52A")]
485    A(Field52A),
486    #[serde(rename = "52B")]
487    B(Field52B),
488    #[serde(rename = "52D")]
489    D(Field52D),
490}
491
492impl SwiftField for Field52DrawerBank {
493    fn parse(input: &str) -> crate::Result<Self>
494    where
495        Self: Sized,
496    {
497        // Try Option A (BIC-based) first
498        if let Ok(field) = Field52A::parse(input) {
499            return Ok(Field52DrawerBank::A(field));
500        }
501
502        // Try Option B (party identifier with location)
503        if let Ok(field) = Field52B::parse(input) {
504            return Ok(Field52DrawerBank::B(field));
505        }
506
507        // Try Option D (party identifier with name/address)
508        if let Ok(field) = Field52D::parse(input) {
509            return Ok(Field52DrawerBank::D(field));
510        }
511
512        Err(ParseError::InvalidFormat {
513            message: "Field 52 Drawer Bank could not be parsed as option A, B or D".to_string(),
514        })
515    }
516
517    fn parse_with_variant(
518        value: &str,
519        variant: Option<&str>,
520        _field_tag: Option<&str>,
521    ) -> crate::Result<Self>
522    where
523        Self: Sized,
524    {
525        match variant {
526            Some("A") => {
527                let field = Field52A::parse(value)?;
528                Ok(Field52DrawerBank::A(field))
529            }
530            Some("B") => {
531                let field = Field52B::parse(value)?;
532                Ok(Field52DrawerBank::B(field))
533            }
534            Some("D") => {
535                let field = Field52D::parse(value)?;
536                Ok(Field52DrawerBank::D(field))
537            }
538            _ => {
539                // No variant specified, fall back to default parse behavior
540                Self::parse(value)
541            }
542        }
543    }
544
545    fn to_swift_string(&self) -> String {
546        match self {
547            Field52DrawerBank::A(field) => field.to_swift_string(),
548            Field52DrawerBank::B(field) => field.to_swift_string(),
549            Field52DrawerBank::D(field) => field.to_swift_string(),
550        }
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    #[test]
559    fn test_field52a() {
560        // With party identifier
561        let field = Field52A::parse("/C/US123456\nDEUTDEFF").unwrap();
562        assert_eq!(field.party_identifier, Some("C/US123456".to_string()));
563        assert_eq!(field.bic, "DEUTDEFF");
564
565        // Without party identifier
566        let field = Field52A::parse("CHASUS33XXX").unwrap();
567        assert_eq!(field.party_identifier, None);
568        assert_eq!(field.bic, "CHASUS33XXX");
569    }
570
571    #[test]
572    fn test_field52b() {
573        // With party identifier and location
574        let field = Field52B::parse("/A/12345\nNEW YORK").unwrap();
575        assert_eq!(field.party_identifier, Some("A/12345".to_string()));
576        assert_eq!(field.location, Some("NEW YORK".to_string()));
577
578        // Empty
579        let field = Field52B::parse("").unwrap();
580        assert_eq!(field.party_identifier, None);
581        assert_eq!(field.location, None);
582    }
583
584    #[test]
585    fn test_field52c() {
586        let field = Field52C::parse("/UKCLEARING123").unwrap();
587        assert_eq!(field.party_identifier, "UKCLEARING123");
588        assert_eq!(field.to_swift_string(), ":52C:/UKCLEARING123");
589    }
590
591    #[test]
592    fn test_field52d() {
593        // With party identifier
594        let field = Field52D::parse("/D/DE123456\nDEUTSCHE BANK\nFRANKFURT").unwrap();
595        assert_eq!(field.party_identifier, Some("D/DE123456".to_string()));
596        assert_eq!(field.name_and_address.len(), 2);
597        assert_eq!(field.name_and_address[0], "DEUTSCHE BANK");
598
599        // Without party identifier
600        let field = Field52D::parse("ACME BANK\nLONDON").unwrap();
601        assert_eq!(field.party_identifier, None);
602        assert_eq!(field.name_and_address.len(), 2);
603    }
604
605    #[test]
606    fn test_field52_invalid() {
607        // Invalid BIC
608        assert!(Field52A::parse("INVALID").is_err());
609
610        // Missing slash in 52C
611        assert!(Field52C::parse("NOSLASH").is_err());
612
613        // Too many lines in 52D
614        assert!(Field52D::parse("LINE1\nLINE2\nLINE3\nLINE4\nLINE5").is_err());
615    }
616}