swift_mt_message/fields/
field77b.rs

1use crate::{SwiftField, ValidationError, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 77B: Regulatory Reporting
5///
6/// ## Overview
7/// Field 77B contains regulatory reporting information in SWIFT payment messages, providing
8/// data required by regulatory authorities for compliance, monitoring, and statistical purposes.
9/// This field supports various regulatory requirements including anti-money laundering (AML),
10/// know your customer (KYC), foreign exchange reporting, and other jurisdiction-specific
11/// compliance obligations. The information helps authorities track cross-border payments
12/// and ensure compliance with local and international regulations.
13///
14/// ## Format Specification
15/// **Format**: `3*35x`
16/// - **3*35x**: Up to 3 lines of 35 characters each
17/// - **Line structure**: Structured regulatory codes and information
18/// - **Character set**: SWIFT character set (A-Z, 0-9, and limited special characters)
19/// - **Line separation**: Each line on separate row
20///
21/// ## Structure
22/// ```text
23/// /ORDERRES/US/1234567890123456
24/// /BENEFRES/DE/9876543210987654
25/// /PURP/TRADE
26/// │                              │
27/// └──────────────────────────────┘
28///        Up to 35 characters per line
29///        Maximum 3 lines
30/// ```
31///
32/// ## Field Components
33/// - **Ordering Country**: Country code of ordering customer
34/// - **Beneficiary Country**: Country code of beneficiary customer
35/// - **Purpose Code**: Transaction purpose or category
36/// - **Regulatory Codes**: Authority-specific reporting codes
37/// - **Additional Information**: Supplementary compliance data
38///
39/// ## Usage Context
40/// Field 77B is used in:
41/// - **MT103**: Single Customer Credit Transfer
42/// - **MT200**: Financial Institution Transfer
43/// - **MT202**: General Financial Institution Transfer
44/// - **MT202COV**: Cover for customer credit transfer
45/// - **MT205**: Financial Institution Transfer for its own account
46///
47/// ### Business Applications
48/// - **Regulatory compliance**: Meeting reporting requirements
49/// - **AML/KYC reporting**: Anti-money laundering compliance
50/// - **Foreign exchange reporting**: FX transaction monitoring
51/// - **Statistical reporting**: Economic and trade statistics
52/// - **Sanctions screening**: Compliance with sanctions regimes
53/// - **Tax reporting**: Supporting tax authority requirements
54///
55/// ## Common Regulatory Codes
56/// ### /ORDERRES/ - Ordering Customer Residence
57/// - **Format**: `/ORDERRES/CC/identifier`
58/// - **CC**: ISO 3166-1 two-letter country code
59/// - **identifier**: Customer identification number
60/// - **Purpose**: Identifies ordering customer's country of residence
61///
62/// ### /BENEFRES/ - Beneficiary Residence
63/// - **Format**: `/BENEFRES/CC/identifier`
64/// - **CC**: ISO 3166-1 two-letter country code
65/// - **identifier**: Beneficiary identification number
66/// - **Purpose**: Identifies beneficiary's country of residence
67///
68/// ### /PURP/ - Purpose Code
69/// - **Format**: `/PURP/code`
70/// - **code**: Transaction purpose code
71/// - **Examples**: TRADE, SALA, PENS, DIVI, LOAN
72/// - **Purpose**: Categorizes transaction purpose
73///
74/// ## Examples
75/// ```text
76/// :77B:/ORDERRES/US/1234567890
77/// └─── US ordering customer with ID
78///
79/// :77B:/ORDERRES/DE/9876543210
80/// /BENEFRES/GB/5555666677
81/// /PURP/TRADE
82/// └─── Complete regulatory reporting with purpose
83///
84/// :77B:/BENEFRES/JP/1111222233
85/// /PURP/SALA
86/// └─── Japanese beneficiary for salary payment
87///
88/// :77B:/ORDERRES/CH/7777888899
89/// /BENEFRES/FR/4444555566
90/// └─── Cross-border payment reporting
91/// ```
92///
93/// ## Purpose Codes
94/// - **TRADE**: Trade-related payments
95/// - **SALA**: Salary and wage payments
96/// - **PENS**: Pension payments
97/// - **DIVI**: Dividend payments
98/// - **LOAN**: Loan-related payments
99/// - **RENT**: Rental payments
100/// - **ROYALTY**: Royalty payments
101/// - **FEES**: Professional fees
102/// - **INSUR**: Insurance payments
103/// - **INVEST**: Investment-related payments
104///
105/// ## Country Code Guidelines
106/// - **ISO 3166-1**: Must use standard two-letter country codes
107/// - **Active codes**: Should use currently valid country codes
108/// - **Residence**: Based on customer's country of residence
109/// - **Jurisdiction**: May differ from bank location
110/// - **Compliance**: Must align with regulatory requirements
111///
112/// ## Validation Rules
113/// 1. **Line count**: Maximum 3 lines
114/// 2. **Line length**: Maximum 35 characters per line
115/// 3. **Character set**: SWIFT character set only
116/// 4. **Country codes**: Must be valid ISO 3166-1 codes
117/// 5. **Format structure**: Must follow structured format
118/// 6. **Content validation**: Codes must be meaningful
119/// 7. **Regulatory compliance**: Must meet jurisdiction requirements
120///
121/// ## Network Validated Rules (SWIFT Standards)
122/// - Maximum 3 lines allowed (Error: T26)
123/// - Each line maximum 35 characters (Error: T50)
124/// - Must use SWIFT character set only (Error: T61)
125/// - Country codes must be valid (Error: T52)
126/// - Format must follow regulatory structure (Error: T77)
127/// - Purpose codes should be recognized (Warning: W77)
128/// - Field required for certain jurisdictions (Error: M77)
129///
130
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
132pub struct Field77B {
133    /// Regulatory reporting information lines (up to 3 lines of 35 characters each)
134    pub information: Vec<String>,
135    /// Ordering country code
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub ordering_country: Option<String>,
138    /// Beneficiary country code
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub beneficiary_country: Option<String>,
141}
142
143impl Field77B {
144    /// Create a new Field77B with validation
145    pub fn new(information: Vec<String>) -> Result<Self, crate::ParseError> {
146        if information.is_empty() {
147            return Err(crate::ParseError::InvalidFieldFormat {
148                field_tag: "77B".to_string(),
149                message: "Regulatory reporting information cannot be empty".to_string(),
150            });
151        }
152
153        if information.len() > 3 {
154            return Err(crate::ParseError::InvalidFieldFormat {
155                field_tag: "77B".to_string(),
156                message: "Too many lines (max 3)".to_string(),
157            });
158        }
159
160        for (i, line) in information.iter().enumerate() {
161            if line.len() > 35 {
162                return Err(crate::ParseError::InvalidFieldFormat {
163                    field_tag: "77B".to_string(),
164                    message: format!("Line {} too long (max 35 characters)", i + 1),
165                });
166            }
167
168            // Validate characters (printable ASCII)
169            if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
170                return Err(crate::ParseError::InvalidFieldFormat {
171                    field_tag: "77B".to_string(),
172                    message: format!("Line {} contains invalid characters", i + 1),
173                });
174            }
175        }
176
177        // Extract country codes from the information lines
178        let mut ordering_country = None;
179        let mut beneficiary_country = None;
180
181        for line in &information {
182            if line.starts_with("/ORDERRES/") {
183                // Extract country code after /ORDERRES/
184                if let Some(country_part) = line.strip_prefix("/ORDERRES/") {
185                    // Take the first part before any additional slashes or content
186                    let country = country_part.split('/').next().unwrap_or("").to_string();
187                    if !country.is_empty() {
188                        ordering_country = Some(country);
189                    }
190                }
191            }
192            if line.starts_with("/BENEFRES/") {
193                // Extract country code after /BENEFRES/
194                if let Some(country_part) = line.strip_prefix("/BENEFRES/") {
195                    // Take the first part before any additional slashes or content
196                    let country = country_part.split('/').next().unwrap_or("").to_string();
197                    if !country.is_empty() {
198                        beneficiary_country = Some(country);
199                    }
200                }
201            }
202        }
203
204        Ok(Field77B {
205            information,
206            ordering_country,
207            beneficiary_country,
208        })
209    }
210
211    /// Create from a single string, splitting on newlines
212    pub fn from_string(content: impl Into<String>) -> Result<Self, crate::ParseError> {
213        let content = content.into();
214        let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
215        Self::new(lines)
216    }
217
218    /// Get the information lines
219    pub fn information(&self) -> &[String] {
220        &self.information
221    }
222
223    /// Get the number of lines
224    pub fn line_count(&self) -> usize {
225        self.information.len()
226    }
227
228    /// Get a specific line by index
229    pub fn line(&self, index: usize) -> Option<&str> {
230        self.information.get(index).map(|s| s.as_str())
231    }
232
233    /// Add a line of information
234    pub fn add_line(&mut self, line: String) -> Result<(), crate::ParseError> {
235        if self.information.len() >= 3 {
236            return Err(crate::ParseError::InvalidFieldFormat {
237                field_tag: "77B".to_string(),
238                message: "Cannot add more lines (max 3)".to_string(),
239            });
240        }
241
242        if line.len() > 35 {
243            return Err(crate::ParseError::InvalidFieldFormat {
244                field_tag: "77B".to_string(),
245                message: "Line too long (max 35 characters)".to_string(),
246            });
247        }
248
249        if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
250            return Err(crate::ParseError::InvalidFieldFormat {
251                field_tag: "77B".to_string(),
252                message: "Line contains invalid characters".to_string(),
253            });
254        }
255
256        self.information.push(line);
257        Ok(())
258    }
259
260    /// Check if this contains ordering country information
261    pub fn has_ordering_country(&self) -> bool {
262        self.ordering_country.is_some()
263    }
264
265    /// Check if this contains beneficiary country information
266    pub fn has_beneficiary_country(&self) -> bool {
267        self.beneficiary_country.is_some()
268    }
269
270    /// Extract ordering country code if present
271    pub fn ordering_country(&self) -> Option<&str> {
272        self.ordering_country.as_deref()
273    }
274
275    /// Extract beneficiary country code if present
276    pub fn beneficiary_country(&self) -> Option<&str> {
277        self.beneficiary_country.as_deref()
278    }
279
280    /// Get human-readable description
281    pub fn description(&self) -> String {
282        format!("Regulatory Reporting ({} lines)", self.line_count())
283    }
284}
285
286impl SwiftField for Field77B {
287    fn parse(value: &str) -> Result<Self, crate::ParseError> {
288        let content = if let Some(stripped) = value.strip_prefix(":77B:") {
289            stripped // Remove ":77B:" prefix
290        } else if let Some(stripped) = value.strip_prefix("77B:") {
291            stripped // Remove "77B:" prefix
292        } else {
293            value
294        };
295
296        let content = content.trim();
297
298        if content.is_empty() {
299            return Err(crate::ParseError::InvalidFieldFormat {
300                field_tag: "77B".to_string(),
301                message: "Field content cannot be empty".to_string(),
302            });
303        }
304
305        Self::from_string(content)
306    }
307
308    fn to_swift_string(&self) -> String {
309        format!(":77B:{}", self.information.join("\n"))
310    }
311
312    fn validate(&self) -> ValidationResult {
313        let mut errors = Vec::new();
314
315        // Validate line count
316        if self.information.is_empty() {
317            errors.push(ValidationError::ValueValidation {
318                field_tag: "77B".to_string(),
319                message: "Information cannot be empty".to_string(),
320            });
321        }
322
323        if self.information.len() > 3 {
324            errors.push(ValidationError::LengthValidation {
325                field_tag: "77B".to_string(),
326                expected: "max 3 lines".to_string(),
327                actual: self.information.len(),
328            });
329        }
330
331        // Validate each line
332        for (i, line) in self.information.iter().enumerate() {
333            if line.len() > 35 {
334                errors.push(ValidationError::LengthValidation {
335                    field_tag: "77B".to_string(),
336                    expected: format!("max 35 characters for line {}", i + 1),
337                    actual: line.len(),
338                });
339            }
340
341            if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
342                errors.push(ValidationError::FormatValidation {
343                    field_tag: "77B".to_string(),
344                    message: format!("Line {} contains invalid characters", i + 1),
345                });
346            }
347        }
348
349        ValidationResult {
350            is_valid: errors.is_empty(),
351            errors,
352            warnings: Vec::new(),
353        }
354    }
355
356    fn format_spec() -> &'static str {
357        "3*35x"
358    }
359}
360
361impl std::fmt::Display for Field77B {
362    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363        write!(f, "{}", self.information.join("\n"))
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn test_field77b_creation() {
373        let lines = vec!["/ORDERRES/DE".to_string(), "/BENEFRES/BE".to_string()];
374        let field = Field77B::new(lines.clone()).unwrap();
375        assert_eq!(field.information(), &lines);
376        assert_eq!(field.line_count(), 2);
377    }
378
379    #[test]
380    fn test_field77b_from_string() {
381        let content = "/ORDERRES/DE\n/BENEFRES/BE\nREGPORT123";
382        let field = Field77B::from_string(content).unwrap();
383        assert_eq!(field.line_count(), 3);
384        assert_eq!(field.line(0), Some("/ORDERRES/DE"));
385        assert_eq!(field.line(1), Some("/BENEFRES/BE"));
386        assert_eq!(field.line(2), Some("REGPORT123"));
387    }
388
389    #[test]
390    fn test_field77b_parse() {
391        let field = Field77B::parse("/ORDERRES/DE\n/BENEFRES/BE").unwrap();
392        assert_eq!(field.line_count(), 2);
393        assert_eq!(field.line(0), Some("/ORDERRES/DE"));
394        assert_eq!(field.line(1), Some("/BENEFRES/BE"));
395    }
396
397    #[test]
398    fn test_field77b_parse_with_prefix() {
399        let field = Field77B::parse(":77B:/ORDERRES/DE\n/BENEFRES/BE").unwrap();
400        assert_eq!(field.line_count(), 2);
401        assert_eq!(field.line(0), Some("/ORDERRES/DE"));
402    }
403
404    #[test]
405    fn test_field77b_to_swift_string() {
406        let lines = vec!["/ORDERRES/DE".to_string(), "/BENEFRES/BE".to_string()];
407        let field = Field77B::new(lines).unwrap();
408        assert_eq!(field.to_swift_string(), ":77B:/ORDERRES/DE\n/BENEFRES/BE");
409    }
410
411    #[test]
412    fn test_field77b_add_line() {
413        let mut field = Field77B::new(vec!["/ORDERRES/DE".to_string()]).unwrap();
414        field.add_line("/BENEFRES/BE".to_string()).unwrap();
415        assert_eq!(field.line_count(), 2);
416        assert_eq!(field.line(1), Some("/BENEFRES/BE"));
417    }
418
419    #[test]
420    fn test_field77b_country_extraction() {
421        let field =
422            Field77B::new(vec!["/ORDERRES/DE".to_string(), "/BENEFRES/BE".to_string()]).unwrap();
423
424        assert!(field.has_ordering_country());
425        assert!(field.has_beneficiary_country());
426        assert_eq!(field.ordering_country(), Some("DE"));
427        assert_eq!(field.beneficiary_country(), Some("BE"));
428    }
429
430    #[test]
431    fn test_field77b_country_extraction_with_additional_info() {
432        // Test the format from the backup version: "/ORDERRES/DE//REGULATORY INFO"
433        let field = Field77B::new(vec![
434            "/ORDERRES/DE//REGULATORY INFO".to_string(),
435            "SOFTWARE LICENSE COMPLIANCE".to_string(),
436            "TRADE RELATED TRANSACTION".to_string(),
437        ])
438        .unwrap();
439
440        assert!(field.has_ordering_country());
441        assert!(!field.has_beneficiary_country());
442        assert_eq!(field.ordering_country(), Some("DE"));
443        assert_eq!(field.beneficiary_country(), None);
444    }
445
446    #[test]
447    fn test_field77b_no_country_codes() {
448        let field = Field77B::new(vec![
449            "REGULATORY INFORMATION".to_string(),
450            "NO COUNTRY CODES HERE".to_string(),
451        ])
452        .unwrap();
453
454        assert!(!field.has_ordering_country());
455        assert!(!field.has_beneficiary_country());
456        assert_eq!(field.ordering_country(), None);
457        assert_eq!(field.beneficiary_country(), None);
458    }
459
460    #[test]
461    fn test_field77b_too_many_lines() {
462        let lines = vec![
463            "Line 1".to_string(),
464            "Line 2".to_string(),
465            "Line 3".to_string(),
466            "Line 4".to_string(), // Too many
467        ];
468        let result = Field77B::new(lines);
469        assert!(result.is_err());
470    }
471
472    #[test]
473    fn test_field77b_line_too_long() {
474        let lines = vec!["A".repeat(36)]; // 36 characters, max is 35
475        let result = Field77B::new(lines);
476        assert!(result.is_err());
477    }
478
479    #[test]
480    fn test_field77b_empty() {
481        let result = Field77B::new(vec![]);
482        assert!(result.is_err());
483    }
484
485    #[test]
486    fn test_field77b_validation() {
487        let field = Field77B::new(vec!["/ORDERRES/DE".to_string()]).unwrap();
488        let validation = field.validate();
489        assert!(validation.is_valid);
490        assert!(validation.errors.is_empty());
491    }
492
493    #[test]
494    fn test_field77b_display() {
495        let field =
496            Field77B::new(vec!["/ORDERRES/DE".to_string(), "/BENEFRES/BE".to_string()]).unwrap();
497        assert_eq!(format!("{}", field), "/ORDERRES/DE\n/BENEFRES/BE");
498    }
499
500    #[test]
501    fn test_field77b_description() {
502        let field =
503            Field77B::new(vec!["/ORDERRES/DE".to_string(), "/BENEFRES/BE".to_string()]).unwrap();
504        assert_eq!(field.description(), "Regulatory Reporting (2 lines)");
505    }
506
507    #[test]
508    fn test_field77b_add_line_max_reached() {
509        let mut field = Field77B::new(vec![
510            "Line 1".to_string(),
511            "Line 2".to_string(),
512            "Line 3".to_string(),
513        ])
514        .unwrap();
515
516        let result = field.add_line("Line 4".to_string());
517        assert!(result.is_err());
518    }
519}