swift_mt_message/fields/
field72.rs

1use crate::{SwiftField, ValidationError, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 72: Sender to Receiver Information
5///
6/// ## Overview
7/// Field 72 contains sender to receiver information in SWIFT payment messages, providing
8/// additional instructions, codes, or information that the sending institution wants to
9/// communicate to the receiving institution. This field serves as a communication channel
10/// for operational instructions, regulatory codes, special handling requirements, and other
11/// relevant information that supports proper payment processing and compliance.
12///
13/// ## Format Specification
14/// **Format**: `6*35x`
15/// - **6*35x**: Up to 6 lines of 35 characters each
16/// - **Line structure**: Free-form text for instructions and information
17/// - **Character set**: SWIFT character set (A-Z, 0-9, and limited special characters)
18/// - **Line separation**: Each line on separate row
19///
20/// ## Structure
21/// ```text
22/// /ACC/BENEFICIARY ACCOUNT DETAILS
23/// /BNF/ADDITIONAL BENEFICIARY INFO
24/// /INS/SPECIAL HANDLING REQUIRED
25/// /REC/REGULATORY REPORTING CODE
26/// /FEE/CHARGE INSTRUCTIONS
27/// /REM/ADDITIONAL REMITTANCE INFO
28/// │                              │
29/// └──────────────────────────────┘
30///        Up to 35 characters per line
31///        Maximum 6 lines
32/// ```
33///
34/// ## Field Components
35/// - **Instruction Codes**: Structured codes for specific instructions
36/// - **Regulatory Information**: Compliance and reporting codes
37/// - **Processing Instructions**: Special handling requirements
38/// - **Additional Details**: Supplementary payment information
39/// - **Communication Messages**: Bank-to-bank communications
40///
41/// ## Usage Context
42/// Field 72 is used in:
43/// - **MT103**: Single Customer Credit Transfer
44/// - **MT200**: Financial Institution Transfer
45/// - **MT202**: General Financial Institution Transfer
46/// - **MT202COV**: Cover for customer credit transfer
47/// - **MT205**: Financial Institution Transfer for its own account
48///
49/// ### Business Applications
50/// - **Operational instructions**: Special processing requirements
51/// - **Regulatory compliance**: Required reporting codes and information
52/// - **Payment routing**: Additional routing or handling instructions
53/// - **Fee instructions**: Charge allocation and billing details
54/// - **Beneficiary information**: Additional beneficiary details
55/// - **Investigation support**: Information for payment inquiries
56///
57/// ## Common Instruction Codes
58/// ### /ACC/ - Account Information
59/// - Additional account details or instructions
60/// - Alternative account numbers or references
61/// - Account-specific processing requirements
62///
63/// ### /BNF/ - Beneficiary Information
64/// - Additional beneficiary identification
65/// - Alternative beneficiary details
66/// - Beneficiary-specific instructions
67///
68/// ### /INS/ - Special Instructions
69/// - Processing instructions for receiving bank
70/// - Handling requirements or restrictions
71/// - Operational guidance
72///
73/// ### /REC/ - Regulatory Information
74/// - Regulatory reporting codes
75/// - Compliance-related information
76/// - Authority-required data
77///
78/// ### /FEE/ - Fee Instructions
79/// - Charge allocation instructions
80/// - Fee payment arrangements
81/// - Billing-related information
82///
83/// ### /REM/ - Additional Remittance
84/// - Supplementary remittance information
85/// - Extended payment descriptions
86/// - Reference details
87///
88/// ## Examples
89/// ```text
90/// :72:/ACC/CREDIT TO ACCOUNT 123456789
91/// └─── Account crediting instruction
92///
93/// :72:/BNF/JOHN DOE TRADING COMPANY
94/// /INS/URGENT PROCESSING REQUIRED
95/// /REC/REGULATORY CODE ABC123
96/// └─── Multi-line instructions with codes
97///
98/// :72:/FEE/CHARGES TO BE SHARED
99/// /REM/INVOICE REF INV-2024-001
100/// └─── Fee and remittance instructions
101///
102/// :72:CORRESPONDENT BANK CHARGES APPLY
103/// BENEFICIARY BANK TO DEDUCT FEES
104/// PAYMENT FOR TRADE SETTLEMENT
105/// └─── Free-form instructions without codes
106/// ```
107///
108/// ## Information Categories
109/// - **Processing instructions**: How to handle the payment
110/// - **Regulatory codes**: Required compliance information
111/// - **Routing details**: Additional routing or correspondent instructions
112/// - **Charge information**: Fee allocation and payment instructions
113/// - **Reference data**: Additional reference numbers or codes
114/// - **Special requirements**: Urgent processing, holds, or restrictions
115///
116/// ## Validation Rules
117/// 1. **Line count**: Maximum 6 lines
118/// 2. **Line length**: Maximum 35 characters per line
119/// 3. **Character set**: SWIFT character set only
120/// 4. **Content**: Should contain meaningful instructions or information
121/// 5. **Empty lines**: Generally avoided for clarity
122/// 6. **Control characters**: Not allowed
123/// 7. **Special characters**: Limited to SWIFT-approved set
124///
125/// ## Network Validated Rules (SWIFT Standards)
126/// - Maximum 6 lines allowed (Error: T26)
127/// - Each line maximum 35 characters (Error: T50)
128/// - Must use SWIFT character set only (Error: T61)
129/// - Content should be meaningful (Error: T40)
130/// - No control characters permitted (Error: T35)
131/// - Field is optional but widely used (Warning: W72)
132/// - Structured codes should follow conventions (Warning: W73)
133///
134
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
136pub struct Field72 {
137    /// Information lines (up to 6 lines of 35 characters each)
138    pub information: Vec<String>,
139}
140
141impl Field72 {
142    /// Create a new Field72 with validation
143    pub fn new(information: Vec<String>) -> Result<Self, crate::ParseError> {
144        if information.is_empty() {
145            return Err(crate::ParseError::InvalidFieldFormat {
146                field_tag: "72".to_string(),
147                message: "Sender to receiver information cannot be empty".to_string(),
148            });
149        }
150
151        if information.len() > 6 {
152            return Err(crate::ParseError::InvalidFieldFormat {
153                field_tag: "72".to_string(),
154                message: "Too many lines (max 6)".to_string(),
155            });
156        }
157
158        for (i, line) in information.iter().enumerate() {
159            if line.len() > 35 {
160                return Err(crate::ParseError::InvalidFieldFormat {
161                    field_tag: "72".to_string(),
162                    message: format!("Line {} too long (max 35 characters)", i + 1),
163                });
164            }
165
166            // Validate characters (printable ASCII)
167            if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
168                return Err(crate::ParseError::InvalidFieldFormat {
169                    field_tag: "72".to_string(),
170                    message: format!("Line {} contains invalid characters", i + 1),
171                });
172            }
173        }
174
175        Ok(Field72 { information })
176    }
177
178    /// Create from a single string, splitting on newlines
179    pub fn from_string(content: impl Into<String>) -> Result<Self, crate::ParseError> {
180        let content = content.into();
181        let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
182        Self::new(lines)
183    }
184
185    /// Get the information lines
186    pub fn information(&self) -> &[String] {
187        &self.information
188    }
189
190    /// Get the number of lines
191    pub fn line_count(&self) -> usize {
192        self.information.len()
193    }
194
195    /// Get a specific line by index
196    pub fn line(&self, index: usize) -> Option<&str> {
197        self.information.get(index).map(|s| s.as_str())
198    }
199
200    /// Add a line of information
201    pub fn add_line(&mut self, line: String) -> Result<(), crate::ParseError> {
202        if self.information.len() >= 6 {
203            return Err(crate::ParseError::InvalidFieldFormat {
204                field_tag: "72".to_string(),
205                message: "Cannot add more lines (max 6)".to_string(),
206            });
207        }
208
209        if line.len() > 35 {
210            return Err(crate::ParseError::InvalidFieldFormat {
211                field_tag: "72".to_string(),
212                message: "Line too long (max 35 characters)".to_string(),
213            });
214        }
215
216        if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
217            return Err(crate::ParseError::InvalidFieldFormat {
218                field_tag: "72".to_string(),
219                message: "Line contains invalid characters".to_string(),
220            });
221        }
222
223        self.information.push(line);
224        Ok(())
225    }
226
227    /// Get human-readable description
228    pub fn description(&self) -> String {
229        format!(
230            "Sender to Receiver Information ({} lines)",
231            self.line_count()
232        )
233    }
234}
235
236impl SwiftField for Field72 {
237    fn parse(value: &str) -> Result<Self, crate::ParseError> {
238        let content = if let Some(stripped) = value.strip_prefix(":72:") {
239            stripped // Remove ":72:" prefix
240        } else if let Some(stripped) = value.strip_prefix("72:") {
241            stripped // Remove "72:" prefix
242        } else {
243            value
244        };
245
246        let content = content.trim();
247
248        if content.is_empty() {
249            return Err(crate::ParseError::InvalidFieldFormat {
250                field_tag: "72".to_string(),
251                message: "Field content cannot be empty".to_string(),
252            });
253        }
254
255        Self::from_string(content)
256    }
257
258    fn to_swift_string(&self) -> String {
259        format!(":72:{}", self.information.join("\n"))
260    }
261
262    fn validate(&self) -> ValidationResult {
263        let mut errors = Vec::new();
264
265        // Validate line count
266        if self.information.is_empty() {
267            errors.push(ValidationError::ValueValidation {
268                field_tag: "72".to_string(),
269                message: "Information cannot be empty".to_string(),
270            });
271        }
272
273        if self.information.len() > 6 {
274            errors.push(ValidationError::LengthValidation {
275                field_tag: "72".to_string(),
276                expected: "max 6 lines".to_string(),
277                actual: self.information.len(),
278            });
279        }
280
281        // Validate each line
282        for (i, line) in self.information.iter().enumerate() {
283            if line.len() > 35 {
284                errors.push(ValidationError::LengthValidation {
285                    field_tag: "72".to_string(),
286                    expected: format!("max 35 characters for line {}", i + 1),
287                    actual: line.len(),
288                });
289            }
290
291            if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
292                errors.push(ValidationError::FormatValidation {
293                    field_tag: "72".to_string(),
294                    message: format!("Line {} contains invalid characters", i + 1),
295                });
296            }
297        }
298
299        ValidationResult {
300            is_valid: errors.is_empty(),
301            errors,
302            warnings: Vec::new(),
303        }
304    }
305
306    fn format_spec() -> &'static str {
307        "6*35x"
308    }
309}
310
311impl std::fmt::Display for Field72 {
312    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313        write!(f, "{}", self.information.join("\n"))
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_field72_creation() {
323        let lines = vec!["/INS/CHQS".to_string(), "/BENEFRES/BE".to_string()];
324        let field = Field72::new(lines.clone()).unwrap();
325        assert_eq!(field.information(), &lines);
326        assert_eq!(field.line_count(), 2);
327    }
328
329    #[test]
330    fn test_field72_from_string() {
331        let content = "/INS/CHQS\n/BENEFRES/BE\n/ORDERRES/DE";
332        let field = Field72::from_string(content).unwrap();
333        assert_eq!(field.line_count(), 3);
334        assert_eq!(field.line(0), Some("/INS/CHQS"));
335        assert_eq!(field.line(1), Some("/BENEFRES/BE"));
336        assert_eq!(field.line(2), Some("/ORDERRES/DE"));
337    }
338
339    #[test]
340    fn test_field72_parse() {
341        let field = Field72::parse("/INS/CHQS\n/BENEFRES/BE").unwrap();
342        assert_eq!(field.line_count(), 2);
343        assert_eq!(field.line(0), Some("/INS/CHQS"));
344        assert_eq!(field.line(1), Some("/BENEFRES/BE"));
345    }
346
347    #[test]
348    fn test_field72_parse_with_prefix() {
349        let field = Field72::parse(":72:/INS/CHQS\n/BENEFRES/BE").unwrap();
350        assert_eq!(field.line_count(), 2);
351        assert_eq!(field.line(0), Some("/INS/CHQS"));
352    }
353
354    #[test]
355    fn test_field72_to_swift_string() {
356        let lines = vec!["/INS/CHQS".to_string(), "/BENEFRES/BE".to_string()];
357        let field = Field72::new(lines).unwrap();
358        assert_eq!(field.to_swift_string(), ":72:/INS/CHQS\n/BENEFRES/BE");
359    }
360
361    #[test]
362    fn test_field72_add_line() {
363        let mut field = Field72::new(vec!["/INS/CHQS".to_string()]).unwrap();
364        field.add_line("/BENEFRES/BE".to_string()).unwrap();
365        assert_eq!(field.line_count(), 2);
366        assert_eq!(field.line(1), Some("/BENEFRES/BE"));
367    }
368
369    #[test]
370    fn test_field72_too_many_lines() {
371        let lines = vec![
372            "Line 1".to_string(),
373            "Line 2".to_string(),
374            "Line 3".to_string(),
375            "Line 4".to_string(),
376            "Line 5".to_string(),
377            "Line 6".to_string(),
378            "Line 7".to_string(), // Too many
379        ];
380        let result = Field72::new(lines);
381        assert!(result.is_err());
382    }
383
384    #[test]
385    fn test_field72_line_too_long() {
386        let lines = vec!["A".repeat(36)]; // 36 characters, max is 35
387        let result = Field72::new(lines);
388        assert!(result.is_err());
389    }
390
391    #[test]
392    fn test_field72_empty() {
393        let result = Field72::new(vec![]);
394        assert!(result.is_err());
395    }
396
397    #[test]
398    fn test_field72_validation() {
399        let field = Field72::new(vec!["/INS/CHQS".to_string()]).unwrap();
400        let validation = field.validate();
401        assert!(validation.is_valid);
402        assert!(validation.errors.is_empty());
403    }
404
405    #[test]
406    fn test_field72_display() {
407        let field =
408            Field72::new(vec!["/INS/CHQS".to_string(), "/BENEFRES/BE".to_string()]).unwrap();
409        assert_eq!(format!("{}", field), "/INS/CHQS\n/BENEFRES/BE");
410    }
411
412    #[test]
413    fn test_field72_description() {
414        let field =
415            Field72::new(vec!["/INS/CHQS".to_string(), "/BENEFRES/BE".to_string()]).unwrap();
416        assert_eq!(
417            field.description(),
418            "Sender to Receiver Information (2 lines)"
419        );
420    }
421
422    #[test]
423    fn test_field72_line_access() {
424        let field = Field72::new(vec!["/INS/CHQS".to_string()]).unwrap();
425        assert_eq!(field.line(0), Some("/INS/CHQS"));
426        assert_eq!(field.line(1), None);
427    }
428
429    #[test]
430    fn test_field72_add_line_max_reached() {
431        let mut field = Field72::new(vec![
432            "Line 1".to_string(),
433            "Line 2".to_string(),
434            "Line 3".to_string(),
435            "Line 4".to_string(),
436            "Line 5".to_string(),
437            "Line 6".to_string(),
438        ])
439        .unwrap();
440
441        let result = field.add_line("Line 7".to_string());
442        assert!(result.is_err());
443    }
444}