swift_mt_message/fields/
field20.rs

1use super::swift_utils::{parse_max_length, parse_swift_chars};
2use crate::errors::ParseError;
3use crate::traits::SwiftField;
4use serde::{Deserialize, Serialize};
5
6/// **Field 20: Sender's Reference**
7///
8/// Transaction reference assigned by sender to identify the message.
9/// Must be unique for audit and payment tracking.
10///
11/// **Format:** `16x` (max 16 alphanumeric chars)
12/// **Constraints:** Cannot start/end with `/` or contain `//`
13///
14/// **Example:**
15/// ```text
16/// :20:PAYMENT123456
17/// ```
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct Field20 {
20    /// Sender's reference (max 16 chars, no leading/trailing slashes)
21    pub reference: String,
22}
23
24impl SwiftField for Field20 {
25    fn parse(input: &str) -> crate::Result<Self>
26    where
27        Self: Sized,
28    {
29        // Parse the reference with max length of 16
30        let reference = parse_max_length(input, 16, "Field 20 reference")?;
31
32        // Validate SWIFT character set
33        parse_swift_chars(&reference, "Field 20 reference")?;
34
35        // Additional validation: no leading/trailing slashes
36        if reference.starts_with('/') || reference.ends_with('/') {
37            return Err(ParseError::InvalidFormat {
38                message: "Field 20 reference cannot start or end with '/'".to_string(),
39            });
40        }
41
42        // Additional validation: no consecutive slashes
43        if reference.contains("//") {
44            return Err(ParseError::InvalidFormat {
45                message: "Field 20 reference cannot contain consecutive slashes '//'".to_string(),
46            });
47        }
48
49        Ok(Field20 { reference })
50    }
51
52    fn to_swift_string(&self) -> String {
53        format!(":20:{}", self.reference)
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn test_field20_parse_valid() {
63        let field = Field20::parse("PAYMENT123456").unwrap();
64        assert_eq!(field.reference, "PAYMENT123456");
65
66        let field = Field20::parse("INV2024001234").unwrap();
67        assert_eq!(field.reference, "INV2024001234");
68
69        let field = Field20::parse("TXN20240719001").unwrap();
70        assert_eq!(field.reference, "TXN20240719001");
71    }
72
73    #[test]
74    fn test_field20_max_length() {
75        // Exactly 16 characters should work
76        let field = Field20::parse("1234567890ABCDEF").unwrap();
77        assert_eq!(field.reference, "1234567890ABCDEF");
78
79        // 17 characters should fail
80        assert!(Field20::parse("1234567890ABCDEFG").is_err());
81    }
82
83    #[test]
84    fn test_field20_slash_validation() {
85        // Leading slash should fail
86        assert!(Field20::parse("/PAYMENT123").is_err());
87
88        // Trailing slash should fail
89        assert!(Field20::parse("PAYMENT123/").is_err());
90
91        // Consecutive slashes should fail
92        assert!(Field20::parse("PAY//MENT123").is_err());
93
94        // Single slash in middle should be ok
95        let field = Field20::parse("PAY/MENT123").unwrap();
96        assert_eq!(field.reference, "PAY/MENT123");
97    }
98
99    #[test]
100    fn test_field20_to_swift_string() {
101        let field = Field20 {
102            reference: "PAYMENT123456".to_string(),
103        };
104        assert_eq!(field.to_swift_string(), ":20:PAYMENT123456");
105    }
106}