swift_mt_message/fields/
field23.rs

1use super::swift_utils::{parse_exact_length, parse_swift_chars, parse_uppercase};
2use crate::errors::ParseError;
3use crate::traits::SwiftField;
4use serde::{Deserialize, Serialize};
5
6/// **Field 23: Further Identification**
7///
8/// Transaction categorization for money market and deposit operations.
9///
10/// **Format:** `3!a[2!n]11x` (function code + optional days + reference)
11/// **Function codes:** BASE, CALL, COMMERCIAL, CURRENT, DEPOSIT, NOTICE, PRIME
12/// **Days field:** Required only for NOTICE (1-99)
13///
14/// **Example:**
15/// ```text
16/// :23:NOT15REF123
17/// :23:BASREFERENCE
18/// ```
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct Field23 {
21    /// Function code (3 chars, uppercase)
22    pub function_code: String,
23    /// Days (1-99, only for NOTICE)
24    pub days: Option<u32>,
25    /// Reference (max 11 chars)
26    pub reference: String,
27}
28
29impl SwiftField for Field23 {
30    fn parse(input: &str) -> crate::Result<Self>
31    where
32        Self: Sized,
33    {
34        if input.len() < 4 {
35            // Minimum: 3 char function code + 1 char reference
36            return Err(ParseError::InvalidFormat {
37                message: format!(
38                    "Field 23 must be at least 4 characters, found {}",
39                    input.len()
40                ),
41            });
42        }
43
44        // Parse function code (first 3 characters)
45        let function_code = parse_exact_length(&input[0..3], 3, "Field 23 function code")?;
46        parse_uppercase(&function_code, "Field 23 function code")?;
47
48        // Check if days field is present (next 2 characters could be numeric)
49        let (days, reference_start) = if input.len() >= 5 {
50            let potential_days = &input[3..5];
51            if potential_days.chars().all(|c| c.is_numeric()) {
52                let days_value =
53                    potential_days
54                        .parse::<u32>()
55                        .map_err(|_| ParseError::InvalidFormat {
56                            message: "Invalid days value in Field 23".to_string(),
57                        })?;
58
59                // Validate days range (1-99)
60                if days_value == 0 || days_value > 99 {
61                    return Err(ParseError::InvalidFormat {
62                        message: format!(
63                            "Field 23 days must be between 1 and 99, found {}",
64                            days_value
65                        ),
66                    });
67                }
68
69                // NOTICE function code requires days field
70                if function_code != "NOT" && function_code != "NOTICE" {
71                    return Err(ParseError::InvalidFormat {
72                        message: format!(
73                            "Days field only allowed for NOTICE function code, found {}",
74                            function_code
75                        ),
76                    });
77                }
78
79                (Some(days_value), 5)
80            } else {
81                (None, 3)
82            }
83        } else {
84            (None, 3)
85        };
86
87        // Parse reference (remaining characters, max 11)
88        let reference = if input.len() > reference_start {
89            let ref_str = &input[reference_start..];
90            if ref_str.len() > 11 {
91                return Err(ParseError::InvalidFormat {
92                    message: format!(
93                        "Field 23 reference must be at most 11 characters, found {}",
94                        ref_str.len()
95                    ),
96                });
97            }
98            parse_swift_chars(ref_str, "Field 23 reference")?;
99            ref_str.to_string()
100        } else {
101            return Err(ParseError::InvalidFormat {
102                message: "Field 23 reference is required".to_string(),
103            });
104        };
105
106        Ok(Field23 {
107            function_code,
108            days,
109            reference,
110        })
111    }
112
113    fn to_swift_string(&self) -> String {
114        let mut result = String::from(":23:");
115        result.push_str(&self.function_code);
116        if let Some(days) = self.days {
117            result.push_str(&format!("{:02}", days));
118        }
119        result.push_str(&self.reference);
120        result
121    }
122}
123
124/// **Field 23B: Bank Operation Code**
125///
126/// Service level and processing type for payment instructions.
127/// Affects STP routing, priority, and settlement timing.
128///
129/// **Format:** `4!c` (exactly 4 uppercase chars)
130/// **Common codes:** CRED, CRTS, SPAY, SPRI, SSTD, URGP
131///
132/// **Example:**
133/// ```text
134/// :23B:SSTD
135/// ```
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137pub struct Field23B {
138    /// Bank operation code (4 chars, uppercase)
139    pub instruction_code: String,
140}
141
142impl SwiftField for Field23B {
143    fn parse(input: &str) -> crate::Result<Self>
144    where
145        Self: Sized,
146    {
147        // Must be exactly 4 characters
148        let instruction_code = parse_exact_length(input, 4, "Field 23B instruction code")?;
149
150        // Must be uppercase alphabetic
151        parse_uppercase(&instruction_code, "Field 23B instruction code")?;
152
153        // Validate against known codes
154        // Common codes: CRED (Customer Transfer), CRTS (Credit Transfer System),
155        // SPAY (STP), SPRI (Priority), SSTD (Standard), URGP (Urgent Payment),
156        // SDVA (Same Day Value), TELB (Telecommunication Bulk)
157        const VALID_CODES: &[&str] = &[
158            "CRED", "CRTS", "SPAY", "SPRI", "SSTD", "URGP", "SDVA", "TELB", "PHON", "PHOB", "PHOI",
159            "TELE", "REPA", "CORT", "INTC", "HOLD",
160        ];
161        if !VALID_CODES.contains(&instruction_code.as_str()) {
162            return Err(ParseError::InvalidFormat {
163                message: format!(
164                    "Field 23B instruction code must be one of {:?}, found {}",
165                    VALID_CODES, instruction_code
166                ),
167            });
168        }
169
170        Ok(Field23B { instruction_code })
171    }
172
173    fn to_swift_string(&self) -> String {
174        format!(":23B:{}", self.instruction_code)
175    }
176}
177
178/// **Field 23E: Instruction Code**
179///
180/// Additional processing instructions and compliance indicators.
181///
182/// **Format:** `4!c[/35x]` (code + optional details)
183/// **Common codes:** CHQB, CORT, HOLD, PHON, REPA, SDVA, TELB, URGP
184///
185/// **Example:**
186/// ```text
187/// :23E:HOLD/COMPLIANCE REVIEW
188/// :23E:URGP
189/// ```
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191pub struct Field23E {
192    /// Instruction code (4 chars, uppercase)
193    pub instruction_code: String,
194    /// Additional info (max 35 chars)
195    pub additional_info: Option<String>,
196}
197
198impl SwiftField for Field23E {
199    fn parse(input: &str) -> crate::Result<Self>
200    where
201        Self: Sized,
202    {
203        if input.len() < 4 {
204            return Err(ParseError::InvalidFormat {
205                message: format!(
206                    "Field 23E must be at least 4 characters, found {}",
207                    input.len()
208                ),
209            });
210        }
211
212        // Parse instruction code (first 4 characters)
213        let instruction_code = parse_exact_length(&input[0..4], 4, "Field 23E instruction code")?;
214        parse_uppercase(&instruction_code, "Field 23E instruction code")?;
215
216        // Check for optional additional information after slash
217        let additional_info = if input.len() > 4 {
218            if !input[4..].starts_with('/') {
219                return Err(ParseError::InvalidFormat {
220                    message: "Field 23E additional information must start with '/'".to_string(),
221                });
222            }
223
224            let info = &input[5..];
225            if info.len() > 35 {
226                return Err(ParseError::InvalidFormat {
227                    message: format!(
228                        "Field 23E additional information must be at most 35 characters, found {}",
229                        info.len()
230                    ),
231                });
232            }
233
234            parse_swift_chars(info, "Field 23E additional information")?;
235            Some(info.to_string())
236        } else {
237            None
238        };
239
240        Ok(Field23E {
241            instruction_code,
242            additional_info,
243        })
244    }
245
246    fn to_swift_string(&self) -> String {
247        let mut result = String::from(":23E:");
248        result.push_str(&self.instruction_code);
249        if let Some(ref info) = self.additional_info {
250            result.push('/');
251            result.push_str(info);
252        }
253        result
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_field23() {
263        // Test with days field (NOTICE)
264        let field = Field23::parse("NOT02REFERENCE1").unwrap();
265        assert_eq!(field.function_code, "NOT");
266        assert_eq!(field.days, Some(2));
267        assert_eq!(field.reference, "REFERENCE1");
268
269        // Test without days field
270        let field = Field23::parse("BASREFERENCE").unwrap();
271        assert_eq!(field.function_code, "BAS");
272        assert_eq!(field.days, None);
273        assert_eq!(field.reference, "REFERENCE");
274
275        // Test to_swift_string
276        let field = Field23 {
277            function_code: "NOT".to_string(),
278            days: Some(15),
279            reference: "REF123".to_string(),
280        };
281        assert_eq!(field.to_swift_string(), ":23:NOT15REF123");
282    }
283
284    #[test]
285    fn test_field23b() {
286        // Test valid codes
287        let field = Field23B::parse("SSTD").unwrap();
288        assert_eq!(field.instruction_code, "SSTD");
289
290        let field = Field23B::parse("SPRI").unwrap();
291        assert_eq!(field.instruction_code, "SPRI");
292
293        // Test invalid length
294        assert!(Field23B::parse("SST").is_err());
295        assert!(Field23B::parse("SSTDD").is_err());
296
297        // Test invalid code
298        assert!(Field23B::parse("XXXX").is_err());
299
300        // Test lowercase (should fail)
301        assert!(Field23B::parse("sstd").is_err());
302    }
303
304    #[test]
305    fn test_field23e() {
306        // Test with additional info
307        let field = Field23E::parse("CHQB/CHECK NUMBER 12345").unwrap();
308        assert_eq!(field.instruction_code, "CHQB");
309        assert_eq!(
310            field.additional_info,
311            Some("CHECK NUMBER 12345".to_string())
312        );
313
314        // Test without additional info
315        let field = Field23E::parse("URGP").unwrap();
316        assert_eq!(field.instruction_code, "URGP");
317        assert_eq!(field.additional_info, None);
318
319        // Test to_swift_string
320        let field = Field23E {
321            instruction_code: "HOLD".to_string(),
322            additional_info: Some("COMPLIANCE REVIEW".to_string()),
323        };
324        assert_eq!(field.to_swift_string(), ":23E:HOLD/COMPLIANCE REVIEW");
325
326        // Test max length additional info
327        let long_info = "A".repeat(35);
328        let input = format!("CODE/{}", long_info);
329        assert!(Field23E::parse(&input).is_ok());
330
331        // Test too long additional info
332        let too_long = "A".repeat(36);
333        let input = format!("CODE/{}", too_long);
334        assert!(Field23E::parse(&input).is_err());
335    }
336}