swift_mt_message/fields/
field77t.rs

1use crate::{SwiftField, ValidationError, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 77T: Envelope Contents
5///
6/// ## Overview
7/// Field 77T contains envelope contents information in SWIFT MT103.REMIT messages, providing
8/// structured data about the remittance information envelope. This field is mandatory for
9/// MT103.REMIT messages and contains codes that identify the type and format of remittance
10/// information being transmitted. It supports the Extended Remittance Registration (ERR)
11/// framework for structured remittance data exchange.
12///
13/// ## Format Specification
14/// **Format**: `1!a1!a/35x`
15/// - **1!a**: Envelope type code (1 alphabetic character)
16/// - **1!a**: Envelope format code (1 alphabetic character)
17/// - **/**: Separator
18/// - **35x**: Envelope identifier (up to 35 characters)
19///
20/// ## Structure
21/// ```text
22/// RD/REMITTANCE-2024-001234567890
23/// ││ │
24/// ││ └─ Envelope identifier (up to 35 chars)
25/// │└─── Format code (D = Detailed)
26/// └──── Type code (R = Remittance)
27/// ```
28///
29/// ## Field Components
30/// - **Envelope Type**: Single character indicating envelope content type
31/// - **Envelope Format**: Single character indicating data format
32/// - **Envelope Identifier**: Unique identifier for the envelope contents
33///
34/// ## Usage Context
35/// Field 77T is used in:
36/// - **MT103.REMIT**: Single Customer Credit Transfer with Remittance (mandatory)
37///
38/// ### Business Applications
39/// - **Structured remittance**: Supporting ISO 20022 remittance data
40/// - **Extended remittance**: Enabling detailed payment information
41/// - **Regulatory compliance**: Meeting remittance reporting requirements
42/// - **Automated processing**: Supporting straight-through remittance processing
43/// - **Invoice matching**: Facilitating automated accounts receivable processing
44///
45/// ## Envelope Type Codes
46/// ### R - Remittance Information
47/// - **Description**: Contains structured remittance data
48/// - **Usage**: Standard remittance information envelope
49/// - **Content**: Invoice details, payment references, structured data
50///
51/// ### S - Supplementary Information
52/// - **Description**: Additional supporting information
53/// - **Usage**: Supplementary data beyond basic remittance
54/// - **Content**: Extended commercial details, regulatory data
55///
56/// ### T - Trade Information
57/// - **Description**: Trade finance related information
58/// - **Usage**: Trade settlement and documentary credit data
59/// - **Content**: LC references, trade documents, commercial terms
60///
61/// ## Envelope Format Codes
62/// ### D - Detailed Format
63/// - **Description**: Comprehensive structured format
64/// - **Usage**: Full remittance data with all available fields
65/// - **Content**: Complete invoice and payment details
66///
67/// ### S - Summary Format
68/// - **Description**: Condensed format with key information
69/// - **Usage**: Essential remittance data only
70/// - **Content**: Basic payment references and amounts
71///
72/// ### C - Custom Format
73/// - **Description**: Institution-specific format
74/// - **Usage**: Proprietary or specialized data structures
75/// - **Content**: Custom remittance information layout
76///
77/// ## Examples
78/// ```text
79/// :77T:RD/REMITTANCE-2024-001234567890
80/// └─── Detailed remittance envelope
81///
82/// :77T:SS/SUPP-INFO-2024-03-15-001
83/// └─── Summary supplementary information
84///
85/// :77T:TC/TRADE-LC-2024-567890123
86/// └─── Custom trade information envelope
87///
88/// :77T:RD/INV-2024-001234-PAYMENT-REF
89/// └─── Invoice-based remittance envelope
90/// ```
91///
92/// ## Envelope Identifier Guidelines
93/// - **Uniqueness**: Should be unique within sender's system
94/// - **Traceability**: Enable tracking and reconciliation
95/// - **Format**: Alphanumeric with limited special characters
96/// - **Length**: Maximum 35 characters
97/// - **Content**: Meaningful identifier for envelope contents
98///
99/// ## Common Identifier Patterns
100/// - **REMITTANCE-YYYY-NNNNNNNNNN**: Standard remittance pattern
101/// - **INV-YYYY-NNNNNN-REF**: Invoice-based identifier
102/// - **TRADE-LC-YYYY-NNNNNN**: Trade finance identifier
103/// - **SUPP-INFO-YYYY-MM-DD-NNN**: Supplementary information pattern
104///
105/// ## Validation Rules
106/// 1. **Envelope type**: Must be single alphabetic character
107/// 2. **Envelope format**: Must be single alphabetic character
108/// 3. **Separator**: Must be forward slash (/)
109/// 4. **Identifier length**: Maximum 35 characters
110/// 5. **Character set**: SWIFT character set only
111/// 6. **Content validation**: Identifier must be meaningful
112/// 7. **Uniqueness**: Should be unique within context
113///
114/// ## Network Validated Rules (SWIFT Standards)
115/// - Envelope type must be alphabetic (Error: T15)
116/// - Envelope format must be alphabetic (Error: T15)
117/// - Separator must be forward slash (Error: T77)
118/// - Identifier cannot exceed 35 characters (Error: T50)
119/// - Characters must be from SWIFT character set (Error: T61)
120/// - Field 77T mandatory in MT103.REMIT (Error: M77)
121/// - Field 77T not used in MT103 Core/STP (Error: C77)
122///
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
125pub struct Field77T {
126    /// Envelope type code (1 alphabetic character)
127    pub envelope_type: String,
128
129    /// Envelope format code (1 alphabetic character)
130    pub envelope_format: String,
131
132    /// Envelope identifier (up to 35 characters)
133    pub envelope_identifier: String,
134}
135
136impl SwiftField for Field77T {
137    fn parse(value: &str) -> Result<Self, crate::ParseError> {
138        let content = if let Some(stripped) = value.strip_prefix(":77T:") {
139            stripped
140        } else if let Some(stripped) = value.strip_prefix("77T:") {
141            stripped
142        } else {
143            value
144        };
145
146        if content.is_empty() {
147            return Err(crate::ParseError::InvalidFieldFormat {
148                field_tag: "77T".to_string(),
149                message: "Field content cannot be empty".to_string(),
150            });
151        }
152
153        let content = content.trim();
154
155        // Expected format: XY/identifier
156        if content.len() < 4 {
157            return Err(crate::ParseError::InvalidFieldFormat {
158                field_tag: "77T".to_string(),
159                message: "Content too short (minimum format: XY/id)".to_string(),
160            });
161        }
162
163        let envelope_type = content.chars().nth(0).unwrap().to_string();
164        let envelope_format = content.chars().nth(1).unwrap().to_string();
165
166        if content.chars().nth(2) != Some('/') {
167            return Err(crate::ParseError::InvalidFieldFormat {
168                field_tag: "77T".to_string(),
169                message: "Missing separator '/' after envelope codes".to_string(),
170            });
171        }
172
173        let envelope_identifier = content[3..].to_string();
174
175        Self::new(envelope_type, envelope_format, envelope_identifier)
176    }
177
178    fn to_swift_string(&self) -> String {
179        format!(
180            ":77T:{}{}/{}",
181            self.envelope_type, self.envelope_format, self.envelope_identifier
182        )
183    }
184
185    fn validate(&self) -> ValidationResult {
186        let mut errors = Vec::new();
187
188        // Validate envelope type (1 alphabetic character)
189        if self.envelope_type.len() != 1 {
190            errors.push(ValidationError::LengthValidation {
191                field_tag: "77T".to_string(),
192                expected: "1 character".to_string(),
193                actual: self.envelope_type.len(),
194            });
195        } else if !self.envelope_type.chars().all(|c| c.is_ascii_alphabetic()) {
196            errors.push(ValidationError::FormatValidation {
197                field_tag: "77T".to_string(),
198                message: "Envelope type must be alphabetic".to_string(),
199            });
200        }
201
202        // Validate envelope format (1 alphabetic character)
203        if self.envelope_format.len() != 1 {
204            errors.push(ValidationError::LengthValidation {
205                field_tag: "77T".to_string(),
206                expected: "1 character".to_string(),
207                actual: self.envelope_format.len(),
208            });
209        } else if !self
210            .envelope_format
211            .chars()
212            .all(|c| c.is_ascii_alphabetic())
213        {
214            errors.push(ValidationError::FormatValidation {
215                field_tag: "77T".to_string(),
216                message: "Envelope format must be alphabetic".to_string(),
217            });
218        }
219
220        // Validate envelope identifier
221        if self.envelope_identifier.is_empty() {
222            errors.push(ValidationError::ValueValidation {
223                field_tag: "77T".to_string(),
224                message: "Envelope identifier cannot be empty".to_string(),
225            });
226        }
227
228        if self.envelope_identifier.len() > 35 {
229            errors.push(ValidationError::LengthValidation {
230                field_tag: "77T".to_string(),
231                expected: "max 35 characters".to_string(),
232                actual: self.envelope_identifier.len(),
233            });
234        }
235
236        // Validate character set (SWIFT character set)
237        if !self
238            .envelope_identifier
239            .chars()
240            .all(|c| c.is_ascii() && !c.is_control())
241        {
242            errors.push(ValidationError::FormatValidation {
243                field_tag: "77T".to_string(),
244                message: "Envelope identifier contains invalid characters".to_string(),
245            });
246        }
247
248        ValidationResult {
249            is_valid: errors.is_empty(),
250            errors,
251            warnings: Vec::new(),
252        }
253    }
254
255    fn format_spec() -> &'static str {
256        "1!a1!a/35x"
257    }
258}
259
260impl Field77T {
261    /// Create a new Field77T with validation
262    ///
263    /// # Arguments
264    /// * `envelope_type` - Envelope type code (1 alphabetic character)
265    /// * `envelope_format` - Envelope format code (1 alphabetic character)
266    /// * `envelope_identifier` - Envelope identifier (up to 35 characters)
267    ///
268    /// # Examples
269    /// ```rust
270    /// use swift_mt_message::fields::Field77T;
271    ///
272    /// // Detailed remittance envelope
273    /// let field = Field77T::new("R", "D", "REMITTANCE-2024-001234567890").unwrap();
274    ///
275    /// // Summary supplementary information
276    /// let field = Field77T::new("S", "S", "SUPP-INFO-2024-03-15-001").unwrap();
277    ///
278    /// // Custom trade information
279    /// let field = Field77T::new("T", "C", "TRADE-LC-2024-567890123").unwrap();
280    /// ```
281    pub fn new(
282        envelope_type: impl Into<String>,
283        envelope_format: impl Into<String>,
284        envelope_identifier: impl Into<String>,
285    ) -> crate::Result<Self> {
286        let envelope_type = envelope_type.into().trim().to_uppercase();
287        let envelope_format = envelope_format.into().trim().to_uppercase();
288        let envelope_identifier = envelope_identifier.into().trim().to_string();
289
290        // Validate envelope type
291        if envelope_type.len() != 1 {
292            return Err(crate::ParseError::InvalidFieldFormat {
293                field_tag: "77T".to_string(),
294                message: "Envelope type must be exactly 1 character".to_string(),
295            });
296        }
297
298        if !envelope_type.chars().all(|c| c.is_ascii_alphabetic()) {
299            return Err(crate::ParseError::InvalidFieldFormat {
300                field_tag: "77T".to_string(),
301                message: "Envelope type must be alphabetic".to_string(),
302            });
303        }
304
305        // Validate envelope format
306        if envelope_format.len() != 1 {
307            return Err(crate::ParseError::InvalidFieldFormat {
308                field_tag: "77T".to_string(),
309                message: "Envelope format must be exactly 1 character".to_string(),
310            });
311        }
312
313        if !envelope_format.chars().all(|c| c.is_ascii_alphabetic()) {
314            return Err(crate::ParseError::InvalidFieldFormat {
315                field_tag: "77T".to_string(),
316                message: "Envelope format must be alphabetic".to_string(),
317            });
318        }
319
320        // Validate envelope identifier
321        if envelope_identifier.is_empty() {
322            return Err(crate::ParseError::InvalidFieldFormat {
323                field_tag: "77T".to_string(),
324                message: "Envelope identifier cannot be empty".to_string(),
325            });
326        }
327
328        if envelope_identifier.len() > 35 {
329            return Err(crate::ParseError::InvalidFieldFormat {
330                field_tag: "77T".to_string(),
331                message: "Envelope identifier cannot exceed 35 characters".to_string(),
332            });
333        }
334
335        if !envelope_identifier
336            .chars()
337            .all(|c| c.is_ascii() && !c.is_control())
338        {
339            return Err(crate::ParseError::InvalidFieldFormat {
340                field_tag: "77T".to_string(),
341                message: "Envelope identifier contains invalid characters".to_string(),
342            });
343        }
344
345        Ok(Field77T {
346            envelope_type,
347            envelope_format,
348            envelope_identifier,
349        })
350    }
351
352    /// Get the envelope type code
353    pub fn envelope_type(&self) -> &str {
354        &self.envelope_type
355    }
356
357    /// Get the envelope format code
358    pub fn envelope_format(&self) -> &str {
359        &self.envelope_format
360    }
361
362    /// Get the envelope identifier
363    pub fn envelope_identifier(&self) -> &str {
364        &self.envelope_identifier
365    }
366
367    /// Check if this is a remittance information envelope
368    pub fn is_remittance_envelope(&self) -> bool {
369        self.envelope_type == "R"
370    }
371
372    /// Check if this is a supplementary information envelope
373    pub fn is_supplementary_envelope(&self) -> bool {
374        self.envelope_type == "S"
375    }
376
377    /// Check if this is a trade information envelope
378    pub fn is_trade_envelope(&self) -> bool {
379        self.envelope_type == "T"
380    }
381
382    /// Check if this uses detailed format
383    pub fn is_detailed_format(&self) -> bool {
384        self.envelope_format == "D"
385    }
386
387    /// Check if this uses summary format
388    pub fn is_summary_format(&self) -> bool {
389        self.envelope_format == "S"
390    }
391
392    /// Check if this uses custom format
393    pub fn is_custom_format(&self) -> bool {
394        self.envelope_format == "C"
395    }
396
397    /// Get a description of the envelope type and format
398    pub fn description(&self) -> String {
399        let type_desc = match self.envelope_type.as_str() {
400            "R" => "Remittance Information",
401            "S" => "Supplementary Information",
402            "T" => "Trade Information",
403            _ => "Unknown Type",
404        };
405
406        let format_desc = match self.envelope_format.as_str() {
407            "D" => "Detailed Format",
408            "S" => "Summary Format",
409            "C" => "Custom Format",
410            _ => "Unknown Format",
411        };
412
413        format!(
414            "{} - {} ({})",
415            type_desc, format_desc, self.envelope_identifier
416        )
417    }
418
419    /// Check if this field is required for MT103.REMIT
420    pub fn is_required_for_remit(&self) -> bool {
421        true // Field 77T is mandatory for MT103.REMIT
422    }
423
424    /// Check if this field is allowed in MT103 Core/STP
425    pub fn is_allowed_in_core_stp(&self) -> bool {
426        false // Field 77T is not used in MT103 Core/STP
427    }
428}
429
430impl std::fmt::Display for Field77T {
431    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432        write!(
433            f,
434            "{}{}/{}",
435            self.envelope_type, self.envelope_format, self.envelope_identifier
436        )
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn test_field77t_creation() {
446        let field = Field77T::new("R", "D", "REMITTANCE-2024-001234567890").unwrap();
447        assert_eq!(field.envelope_type(), "R");
448        assert_eq!(field.envelope_format(), "D");
449        assert_eq!(field.envelope_identifier(), "REMITTANCE-2024-001234567890");
450    }
451
452    #[test]
453    fn test_field77t_parse() {
454        let field = Field77T::parse("RD/REMITTANCE-2024-001234567890").unwrap();
455        assert_eq!(field.envelope_type(), "R");
456        assert_eq!(field.envelope_format(), "D");
457        assert_eq!(field.envelope_identifier(), "REMITTANCE-2024-001234567890");
458    }
459
460    #[test]
461    fn test_field77t_parse_with_prefix() {
462        let field = Field77T::parse(":77T:SS/SUPP-INFO-2024-03-15-001").unwrap();
463        assert_eq!(field.envelope_type(), "S");
464        assert_eq!(field.envelope_format(), "S");
465        assert_eq!(field.envelope_identifier(), "SUPP-INFO-2024-03-15-001");
466
467        let field = Field77T::parse("77T:TC/TRADE-LC-2024-567890123").unwrap();
468        assert_eq!(field.envelope_type(), "T");
469        assert_eq!(field.envelope_format(), "C");
470        assert_eq!(field.envelope_identifier(), "TRADE-LC-2024-567890123");
471    }
472
473    #[test]
474    fn test_field77t_case_normalization() {
475        let field = Field77T::new("r", "d", "remittance-2024-001").unwrap();
476        assert_eq!(field.envelope_type(), "R");
477        assert_eq!(field.envelope_format(), "D");
478        assert_eq!(field.envelope_identifier(), "remittance-2024-001");
479    }
480
481    #[test]
482    fn test_field77t_invalid_envelope_type() {
483        let result = Field77T::new("", "D", "REMITTANCE-2024-001");
484        assert!(result.is_err());
485
486        let result = Field77T::new("AB", "D", "REMITTANCE-2024-001");
487        assert!(result.is_err());
488
489        let result = Field77T::new("1", "D", "REMITTANCE-2024-001");
490        assert!(result.is_err());
491    }
492
493    #[test]
494    fn test_field77t_invalid_envelope_format() {
495        let result = Field77T::new("R", "", "REMITTANCE-2024-001");
496        assert!(result.is_err());
497
498        let result = Field77T::new("R", "AB", "REMITTANCE-2024-001");
499        assert!(result.is_err());
500
501        let result = Field77T::new("R", "1", "REMITTANCE-2024-001");
502        assert!(result.is_err());
503    }
504
505    #[test]
506    fn test_field77t_invalid_identifier() {
507        let result = Field77T::new("R", "D", "");
508        assert!(result.is_err());
509
510        let result = Field77T::new("R", "D", "a".repeat(36));
511        assert!(result.is_err());
512    }
513
514    #[test]
515    fn test_field77t_invalid_format() {
516        let result = Field77T::parse("RD");
517        assert!(result.is_err());
518
519        let result = Field77T::parse("R/IDENTIFIER");
520        assert!(result.is_err());
521
522        let result = Field77T::parse("RDIDENTIFIER");
523        assert!(result.is_err());
524    }
525
526    #[test]
527    fn test_field77t_to_swift_string() {
528        let field = Field77T::new("R", "D", "REMITTANCE-2024-001234567890").unwrap();
529        assert_eq!(
530            field.to_swift_string(),
531            ":77T:RD/REMITTANCE-2024-001234567890"
532        );
533
534        let field = Field77T::new("S", "S", "SUPP-INFO-2024-03-15-001").unwrap();
535        assert_eq!(field.to_swift_string(), ":77T:SS/SUPP-INFO-2024-03-15-001");
536    }
537
538    #[test]
539    fn test_field77t_validation() {
540        let field = Field77T::new("R", "D", "REMITTANCE-2024-001234567890").unwrap();
541        let result = field.validate();
542        assert!(result.is_valid);
543
544        let invalid_field = Field77T {
545            envelope_type: "".to_string(),
546            envelope_format: "D".to_string(),
547            envelope_identifier: "REMITTANCE-2024-001".to_string(),
548        };
549        let result = invalid_field.validate();
550        assert!(!result.is_valid);
551    }
552
553    #[test]
554    fn test_field77t_type_checks() {
555        let remittance_field = Field77T::new("R", "D", "REMITTANCE-2024-001").unwrap();
556        assert!(remittance_field.is_remittance_envelope());
557        assert!(!remittance_field.is_supplementary_envelope());
558        assert!(!remittance_field.is_trade_envelope());
559
560        let supp_field = Field77T::new("S", "S", "SUPP-INFO-2024-001").unwrap();
561        assert!(!supp_field.is_remittance_envelope());
562        assert!(supp_field.is_supplementary_envelope());
563        assert!(!supp_field.is_trade_envelope());
564
565        let trade_field = Field77T::new("T", "C", "TRADE-LC-2024-001").unwrap();
566        assert!(!trade_field.is_remittance_envelope());
567        assert!(!trade_field.is_supplementary_envelope());
568        assert!(trade_field.is_trade_envelope());
569    }
570
571    #[test]
572    fn test_field77t_format_checks() {
573        let detailed_field = Field77T::new("R", "D", "REMITTANCE-2024-001").unwrap();
574        assert!(detailed_field.is_detailed_format());
575        assert!(!detailed_field.is_summary_format());
576        assert!(!detailed_field.is_custom_format());
577
578        let summary_field = Field77T::new("S", "S", "SUPP-INFO-2024-001").unwrap();
579        assert!(!summary_field.is_detailed_format());
580        assert!(summary_field.is_summary_format());
581        assert!(!summary_field.is_custom_format());
582
583        let custom_field = Field77T::new("T", "C", "TRADE-LC-2024-001").unwrap();
584        assert!(!custom_field.is_detailed_format());
585        assert!(!custom_field.is_summary_format());
586        assert!(custom_field.is_custom_format());
587    }
588
589    #[test]
590    fn test_field77t_compliance_checks() {
591        let field = Field77T::new("R", "D", "REMITTANCE-2024-001").unwrap();
592        assert!(field.is_required_for_remit());
593        assert!(!field.is_allowed_in_core_stp());
594    }
595
596    #[test]
597    fn test_field77t_description() {
598        let field = Field77T::new("R", "D", "REMITTANCE-2024-001234567890").unwrap();
599        let description = field.description();
600        assert!(description.contains("Remittance Information"));
601        assert!(description.contains("Detailed Format"));
602        assert!(description.contains("REMITTANCE-2024-001234567890"));
603    }
604
605    #[test]
606    fn test_field77t_display() {
607        let field = Field77T::new("R", "D", "REMITTANCE-2024-001234567890").unwrap();
608        assert_eq!(format!("{}", field), "RD/REMITTANCE-2024-001234567890");
609
610        let field = Field77T::new("S", "S", "SUPP-INFO-2024-03-15-001").unwrap();
611        assert_eq!(format!("{}", field), "SS/SUPP-INFO-2024-03-15-001");
612    }
613
614    #[test]
615    fn test_field77t_format_spec() {
616        assert_eq!(Field77T::format_spec(), "1!a1!a/35x");
617    }
618
619    #[test]
620    fn test_field77t_real_world_examples() {
621        // Standard remittance envelope
622        let remittance = Field77T::new("R", "D", "REMITTANCE-2024-001234567890").unwrap();
623        assert_eq!(
624            remittance.to_swift_string(),
625            ":77T:RD/REMITTANCE-2024-001234567890"
626        );
627        assert!(remittance.is_remittance_envelope());
628        assert!(remittance.is_detailed_format());
629
630        // Invoice-based identifier
631        let invoice = Field77T::new("R", "D", "INV-2024-001234-PAYMENT-REF").unwrap();
632        assert_eq!(
633            invoice.to_swift_string(),
634            ":77T:RD/INV-2024-001234-PAYMENT-REF"
635        );
636
637        // Trade finance envelope
638        let trade = Field77T::new("T", "C", "TRADE-LC-2024-567890123").unwrap();
639        assert_eq!(trade.to_swift_string(), ":77T:TC/TRADE-LC-2024-567890123");
640        assert!(trade.is_trade_envelope());
641        assert!(trade.is_custom_format());
642
643        // Supplementary information
644        let supp = Field77T::new("S", "S", "SUPP-INFO-2024-03-15-001").unwrap();
645        assert_eq!(supp.to_swift_string(), ":77T:SS/SUPP-INFO-2024-03-15-001");
646        assert!(supp.is_supplementary_envelope());
647        assert!(supp.is_summary_format());
648    }
649}