swift_mt_message/fields/
field21.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 21 NoOption: Transaction Reference**
7///
8/// Basic transaction reference for customer payment instructions.
9///
10/// **Format:** `16x` (max 16 chars)
11/// **Constraints:** No leading/trailing slashes, no consecutive slashes
12///
13/// **Example:**
14/// ```text
15/// :21:REF20240719001
16/// ```
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct Field21NoOption {
19    /// Transaction reference (max 16 chars, no slashes at start/end or consecutive)
20    pub reference: String,
21}
22
23impl SwiftField for Field21NoOption {
24    fn parse(input: &str) -> crate::Result<Self>
25    where
26        Self: Sized,
27    {
28        // Parse the reference with max length of 16
29        let reference = parse_max_length(input, 16, "Field 21 reference")?;
30
31        // Validate SWIFT character set
32        parse_swift_chars(&reference, "Field 21 reference")?;
33
34        // Additional validation: no leading/trailing slashes
35        if reference.starts_with('/') || reference.ends_with('/') {
36            return Err(ParseError::InvalidFormat {
37                message: "Field 21 reference cannot start or end with '/'".to_string(),
38            });
39        }
40
41        // Additional validation: no consecutive slashes
42        if reference.contains("//") {
43            return Err(ParseError::InvalidFormat {
44                message: "Field 21 reference cannot contain consecutive slashes '//'".to_string(),
45            });
46        }
47
48        Ok(Field21NoOption { reference })
49    }
50
51    fn to_swift_string(&self) -> String {
52        format!(":21:{}", self.reference)
53    }
54}
55
56/// **Field 21C: Customer-Specific Reference**
57///
58/// Extended reference for customer-specific transactions and treasury operations.
59///
60/// **Format:** `35x` (max 35 chars)
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62pub struct Field21C {
63    /// Customer reference (max 35 chars)
64    pub reference: String,
65}
66
67impl SwiftField for Field21C {
68    fn parse(input: &str) -> crate::Result<Self>
69    where
70        Self: Sized,
71    {
72        let reference = parse_max_length(input, 35, "Field 21C reference")?;
73        parse_swift_chars(&reference, "Field 21C reference")?;
74
75        if reference.starts_with('/') || reference.ends_with('/') {
76            return Err(ParseError::InvalidFormat {
77                message: "Field 21C reference cannot start or end with '/'".to_string(),
78            });
79        }
80
81        if reference.contains("//") {
82            return Err(ParseError::InvalidFormat {
83                message: "Field 21C reference cannot contain consecutive slashes '//'".to_string(),
84            });
85        }
86
87        Ok(Field21C { reference })
88    }
89
90    fn to_swift_string(&self) -> String {
91        format!(":21C:{}", self.reference)
92    }
93}
94
95/// **Field 21D: Deal Reference**
96///
97/// Deal reference for treasury and money market transactions.
98///
99/// **Format:** `35x` (max 35 chars)
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub struct Field21D {
102    /// Deal reference (max 35 chars)
103    pub reference: String,
104}
105
106impl SwiftField for Field21D {
107    fn parse(input: &str) -> crate::Result<Self>
108    where
109        Self: Sized,
110    {
111        let reference = parse_max_length(input, 35, "Field 21D reference")?;
112        parse_swift_chars(&reference, "Field 21D reference")?;
113
114        if reference.starts_with('/') || reference.ends_with('/') {
115            return Err(ParseError::InvalidFormat {
116                message: "Field 21D reference cannot start or end with '/'".to_string(),
117            });
118        }
119
120        if reference.contains("//") {
121            return Err(ParseError::InvalidFormat {
122                message: "Field 21D reference cannot contain consecutive slashes '//'".to_string(),
123            });
124        }
125
126        Ok(Field21D { reference })
127    }
128
129    fn to_swift_string(&self) -> String {
130        format!(":21D:{}", self.reference)
131    }
132}
133
134/// **Field 21E: Related Reference**
135///
136/// Reference to related transaction or instruction for linking operations.
137///
138/// **Format:** `35x` (max 35 chars)
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub struct Field21E {
141    /// Related reference (max 35 chars)
142    pub reference: String,
143}
144
145impl SwiftField for Field21E {
146    fn parse(input: &str) -> crate::Result<Self>
147    where
148        Self: Sized,
149    {
150        let reference = parse_max_length(input, 35, "Field 21E reference")?;
151        parse_swift_chars(&reference, "Field 21E reference")?;
152
153        if reference.starts_with('/') || reference.ends_with('/') {
154            return Err(ParseError::InvalidFormat {
155                message: "Field 21E reference cannot start or end with '/'".to_string(),
156            });
157        }
158
159        if reference.contains("//") {
160            return Err(ParseError::InvalidFormat {
161                message: "Field 21E reference cannot contain consecutive slashes '//'".to_string(),
162            });
163        }
164
165        Ok(Field21E { reference })
166    }
167
168    fn to_swift_string(&self) -> String {
169        format!(":21E:{}", self.reference)
170    }
171}
172
173/// **Field 21F: File Reference**
174///
175/// File reference for batch payment operations and MT102 messages.
176///
177/// **Format:** `16x` (max 16 chars)
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179pub struct Field21F {
180    /// File reference (max 16 chars)
181    pub reference: String,
182}
183
184impl SwiftField for Field21F {
185    fn parse(input: &str) -> crate::Result<Self>
186    where
187        Self: Sized,
188    {
189        let reference = parse_max_length(input, 16, "Field 21F reference")?;
190        parse_swift_chars(&reference, "Field 21F reference")?;
191
192        if reference.starts_with('/') || reference.ends_with('/') {
193            return Err(ParseError::InvalidFormat {
194                message: "Field 21F reference cannot start or end with '/'".to_string(),
195            });
196        }
197
198        if reference.contains("//") {
199            return Err(ParseError::InvalidFormat {
200                message: "Field 21F reference cannot contain consecutive slashes '//'".to_string(),
201            });
202        }
203
204        Ok(Field21F { reference })
205    }
206
207    fn to_swift_string(&self) -> String {
208        format!(":21F:{}", self.reference)
209    }
210}
211
212/// **Field 21R: Related File Reference**
213///
214/// Reference to related file for linking batch operations.
215///
216/// **Format:** `16x` (max 16 chars)
217#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
218pub struct Field21R {
219    /// Related file reference (max 16 chars)
220    pub reference: String,
221}
222
223impl SwiftField for Field21R {
224    fn parse(input: &str) -> crate::Result<Self>
225    where
226        Self: Sized,
227    {
228        let reference = parse_max_length(input, 16, "Field 21R reference")?;
229        parse_swift_chars(&reference, "Field 21R reference")?;
230
231        if reference.starts_with('/') || reference.ends_with('/') {
232            return Err(ParseError::InvalidFormat {
233                message: "Field 21R reference cannot start or end with '/'".to_string(),
234            });
235        }
236
237        if reference.contains("//") {
238            return Err(ParseError::InvalidFormat {
239                message: "Field 21R reference cannot contain consecutive slashes '//'".to_string(),
240            });
241        }
242
243        Ok(Field21R { reference })
244    }
245
246    fn to_swift_string(&self) -> String {
247        format!(":21R:{}", self.reference)
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn test_field21_no_option() {
257        let field = Field21NoOption::parse("REF20240719001").unwrap();
258        assert_eq!(field.reference, "REF20240719001");
259        assert_eq!(field.to_swift_string(), ":21:REF20240719001");
260
261        // Test max length
262        assert!(Field21NoOption::parse("1234567890ABCDEF").is_ok());
263        assert!(Field21NoOption::parse("1234567890ABCDEFG").is_err());
264
265        // Test slash validation
266        assert!(Field21NoOption::parse("/REF123").is_err());
267        assert!(Field21NoOption::parse("REF123/").is_err());
268        assert!(Field21NoOption::parse("REF//123").is_err());
269    }
270
271    #[test]
272    fn test_field21c() {
273        let field = Field21C::parse("TREASURY/SWAP/2024/07/19/001").unwrap();
274        assert_eq!(field.reference, "TREASURY/SWAP/2024/07/19/001");
275
276        // Test max length (35 chars)
277        let long_ref = "12345678901234567890123456789012345";
278        assert!(Field21C::parse(long_ref).is_ok());
279        assert!(Field21C::parse(&format!("{}X", long_ref)).is_err());
280    }
281
282    #[test]
283    fn test_field21d() {
284        let field = Field21D::parse("FX-DEAL-20240719-EUR-USD").unwrap();
285        assert_eq!(field.reference, "FX-DEAL-20240719-EUR-USD");
286        assert_eq!(field.to_swift_string(), ":21D:FX-DEAL-20240719-EUR-USD");
287    }
288
289    #[test]
290    fn test_field21e() {
291        let field = Field21E::parse("ORIGINAL-REF-123456").unwrap();
292        assert_eq!(field.reference, "ORIGINAL-REF-123456");
293    }
294
295    #[test]
296    fn test_field21f() {
297        let field = Field21F::parse("BATCH-20240719").unwrap();
298        assert_eq!(field.reference, "BATCH-20240719");
299
300        // Test 16 char limit
301        assert!(Field21F::parse("1234567890ABCDEF").is_ok());
302        assert!(Field21F::parse("1234567890ABCDEFG").is_err());
303    }
304
305    #[test]
306    fn test_field21r() {
307        let field = Field21R::parse("FILE-REF-001").unwrap();
308        assert_eq!(field.reference, "FILE-REF-001");
309        assert_eq!(field.to_swift_string(), ":21R:FILE-REF-001");
310    }
311}