swift_mt_message/fields/
field20.rs

1use crate::SwiftField;
2use serde::{Deserialize, Serialize};
3
4/// # Field 20: Transaction Reference
5///
6/// ## Overview
7/// Field 20 contains the sender's reference to identify the related transaction uniquely.
8/// This field is mandatory in most SWIFT MT messages and serves as the primary identifier
9/// for transaction tracking, reconciliation, and reference purposes throughout the payment
10/// processing lifecycle.
11///
12/// ## Format Specification
13/// **Format**: `16x`
14/// - **16x**: Up to 16 alphanumeric characters
15/// - **Character set**: A-Z, a-z, 0-9, and limited special characters
16/// - **Case sensitivity**: Preserved as entered
17/// - **Padding**: No padding required (variable length up to 16 characters)
18///
19/// ## Structure and Content
20/// The transaction reference typically follows institutional naming conventions:
21/// ```text
22/// FT21234567890123
23/// │└─────────────┘
24/// │  Reference ID
25/// └── Transaction type prefix (optional)
26/// ```
27///
28/// ## Common Reference Patterns
29/// Different institutions use various patterns for transaction references:
30/// - **Sequential**: `FT000001`, `FT000002`, `FT000003`
31/// - **Date-based**: `FT20241201001`, `FT20241201002`
32/// - **Branch-based**: `NYC001234567`, `LON987654321`
33/// - **System-generated**: `TXN1234567890`, `REF0987654321`
34/// - **Customer-based**: `CUST12345678`, `CLI0000012345`
35///
36/// ## Usage Context
37/// Field 20 is used in numerous SWIFT MT message types:
38/// - **MT103**: Single Customer Credit Transfer
39/// - **MT202**: General Financial Institution Transfer
40/// - **MT202COV**: Cover for customer credit transfer
41/// - **MT900**: Confirmation of Debit
42/// - **MT910**: Confirmation of Credit
43/// - **MT950**: Statement Message
44/// - **MT940**: Customer Statement Message
45///
46/// ### Business Applications
47/// - **Transaction tracking**: Unique identification across systems
48/// - **Reconciliation**: Matching payments with confirmations
49/// - **Audit trails**: Regulatory compliance and investigation
50/// - **Customer service**: Reference for inquiries and disputes
51/// - **STP processing**: Automated transaction processing
52/// - **Nostro reconciliation**: Account statement matching
53///
54/// ## Validation Rules
55/// 1. **Length**: Maximum 16 characters
56/// 2. **Character set**: Alphanumeric characters and limited special characters
57/// 3. **Uniqueness**: Should be unique within sender's system for the day
58/// 4. **Non-empty**: Cannot be empty or contain only whitespace
59/// 5. **Format consistency**: Should follow institutional standards
60///
61/// ## Network Validated Rules (SWIFT Standards)
62/// - Transaction reference must not exceed 16 characters (Error: T13)
63/// - Must contain valid SWIFT character set (Error: T61)
64/// - Cannot be empty (Error: T18)
65/// - Should be unique per sender per day (Warning: recommended practice)
66///
67///
68/// ## Examples
69/// ```text
70/// :20:FT21234567890
71/// └─── Wire transfer reference
72///
73/// :20:TXN0000012345
74/// └─── System-generated transaction ID
75///
76/// :20:NYC20241201001
77/// └─── Branch and date-based reference
78///
79/// :20:CUST123456789A
80/// └─── Customer-based reference with check digit
81/// ```
82///
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, SwiftField)]
85#[format("16x")]
86pub struct Field20 {
87    /// Transaction reference value (maximum 16 characters)
88    ///
89    /// Contains the sender's unique reference for the transaction.
90    /// This value is used throughout the payment processing lifecycle
91    /// for tracking, reconciliation, and audit purposes.
92    ///
93    /// **Format**: Up to 16 alphanumeric characters
94    /// **Character set**: A-Z, a-z, 0-9, and limited special characters
95    /// **Case sensitivity**: Preserved as entered
96    /// **Uniqueness**: Should be unique within sender's system per day
97    ///
98    /// # Examples
99    /// - `"FT21234567890"` - Wire transfer reference
100    /// - `"TXN0000012345"` - System-generated ID
101    /// - `"NYC20241201001"` - Branch and date-based
102    /// - `"CUST123456789A"` - Customer-based with check digit
103    #[format("16x")]
104    pub transaction_reference: String,
105}
106
107impl Field20 {
108    /// Create a new Field20 with comprehensive validation
109    ///
110    /// Creates a new transaction reference field with the provided reference string.
111    /// The reference is validated for length and character set compliance.
112    ///
113    /// # Arguments
114    /// * `transaction_reference` - The transaction reference string (max 16 chars)
115    ///
116    /// # Examples
117    /// ```rust
118    /// use swift_mt_message::fields::Field20;
119    ///
120    /// // Standard wire transfer reference
121    /// let field = Field20::new("FT21234567890".to_string());
122    ///
123    /// // System-generated transaction ID
124    /// let field = Field20::new("TXN0000012345".to_string());
125    ///
126    /// // Date-based reference
127    /// let field = Field20::new("20241201001".to_string());
128    /// ```
129    ///
130    /// # Validation
131    /// The constructor performs basic validation but full validation
132    /// occurs when calling `validate()` method or during SWIFT message processing.
133    pub fn new(transaction_reference: String) -> Self {
134        Self {
135            transaction_reference,
136        }
137    }
138
139    /// Get the transaction reference value
140    ///
141    /// Returns the transaction reference string that uniquely identifies
142    /// this transaction within the sender's system.
143    ///
144    /// # Returns
145    /// A string slice containing the transaction reference
146    ///
147    /// # Example
148    /// ```rust
149    /// # use swift_mt_message::fields::Field20;
150    /// let field = Field20::new("FT21234567890".to_string());
151    /// assert_eq!(field.transaction_reference(), "FT21234567890");
152    /// ```
153    pub fn transaction_reference(&self) -> &str {
154        &self.transaction_reference
155    }
156
157    /// Check if the reference follows a common pattern
158    ///
159    /// Analyzes the transaction reference to determine if it follows
160    /// common institutional patterns for reference generation.
161    ///
162    /// # Returns
163    /// A string describing the detected pattern, or "Custom" if no pattern is detected
164    ///
165    /// # Example
166    /// ```rust
167    /// # use swift_mt_message::fields::Field20;
168    /// let ft_ref = Field20::new("FT21234567890".to_string());
169    /// assert_eq!(ft_ref.reference_pattern(), "Wire Transfer");
170    ///
171    /// let txn_ref = Field20::new("TXN0000012345".to_string());
172    /// assert_eq!(txn_ref.reference_pattern(), "Transaction ID");
173    /// ```
174    pub fn reference_pattern(&self) -> &'static str {
175        let ref_upper = self.transaction_reference.to_uppercase();
176
177        if ref_upper.starts_with("FT") {
178            "Wire Transfer"
179        } else if ref_upper.starts_with("TXN") {
180            "Transaction ID"
181        } else if ref_upper.starts_with("REF") {
182            "Reference ID"
183        } else if ref_upper.starts_with("CUST") {
184            "Customer Reference"
185        } else if ref_upper.starts_with("CLI") {
186            "Client Reference"
187        } else if self.is_date_based() {
188            "Date-based"
189        } else if ref_upper.chars().all(|c| c.is_ascii_digit()) {
190            "Numeric Sequential"
191        } else {
192            "Custom"
193        }
194    }
195
196    /// Check if the reference appears to be date-based
197    ///
198    /// Determines if the transaction reference contains date information
199    /// based on common date patterns in reference strings.
200    ///
201    /// # Returns
202    /// `true` if the reference appears to contain date information
203    ///
204    /// # Example
205    /// ```rust
206    /// # use swift_mt_message::fields::Field20;
207    /// let date_ref = Field20::new("20241201001".to_string());
208    /// assert!(date_ref.is_date_based());
209    ///
210    /// let simple_ref = Field20::new("FT12345".to_string());
211    /// assert!(!simple_ref.is_date_based());
212    /// ```
213    pub fn is_date_based(&self) -> bool {
214        let ref_str = &self.transaction_reference;
215
216        // Check for YYYYMMDD pattern (8 digits at start)
217        if ref_str.len() >= 8 {
218            let date_part = &ref_str[0..8];
219            if date_part.chars().all(|c| c.is_ascii_digit()) {
220                // Basic date validation (year 2000-2099, month 01-12, day 01-31)
221                if let (Ok(year), Ok(month), Ok(day)) = (
222                    date_part[0..4].parse::<u32>(),
223                    date_part[4..6].parse::<u32>(),
224                    date_part[6..8].parse::<u32>(),
225                ) {
226                    return (2000..=2099).contains(&year)
227                        && (1..=12).contains(&month)
228                        && (1..=31).contains(&day);
229                }
230            }
231        }
232
233        false
234    }
235
236    /// Extract potential sequence number from reference
237    ///
238    /// Attempts to extract a sequence number from the transaction reference,
239    /// which is commonly found at the end of structured references.
240    ///
241    /// # Returns
242    /// `Some(number)` if a sequence number is found, `None` otherwise
243    ///
244    /// # Example
245    /// ```rust
246    /// # use swift_mt_message::fields::Field20;
247    /// let seq_ref = Field20::new("FT001".to_string());
248    /// assert_eq!(seq_ref.sequence_number(), Some(1));
249    ///
250    /// let no_seq = Field20::new("CUSTOMREF".to_string());
251    /// assert_eq!(no_seq.sequence_number(), None);
252    /// ```
253    pub fn sequence_number(&self) -> Option<u32> {
254        let ref_str = &self.transaction_reference;
255
256        // Find the longest numeric suffix
257        let mut numeric_suffix = String::new();
258        for ch in ref_str.chars().rev() {
259            if ch.is_ascii_digit() {
260                numeric_suffix.insert(0, ch);
261            } else {
262                break;
263            }
264        }
265
266        if !numeric_suffix.is_empty() && numeric_suffix.len() <= 10 {
267            numeric_suffix.parse().ok()
268        } else {
269            None
270        }
271    }
272
273    /// Get a human-readable description of the transaction reference
274    ///
275    /// Returns a descriptive string explaining the transaction reference
276    /// format and detected patterns.
277    ///
278    /// # Returns
279    /// A descriptive string
280    ///
281    /// # Example
282    /// ```rust
283    /// # use swift_mt_message::fields::Field20;
284    /// let field = Field20::new("FT21234567890".to_string());
285    /// println!("{}", field.description());
286    /// ```
287    pub fn description(&self) -> String {
288        let pattern = self.reference_pattern();
289        let length = self.transaction_reference.len();
290
291        let mut desc = format!(
292            "Transaction Reference: {} (Pattern: {}, Length: {})",
293            self.transaction_reference, pattern, length
294        );
295
296        if self.is_date_based() {
297            desc.push_str(", Date-based");
298        }
299
300        if let Some(seq) = self.sequence_number() {
301            desc.push_str(&format!(", Sequence: {}", seq));
302        }
303
304        desc
305    }
306
307    /// Validate reference format according to institutional standards
308    ///
309    /// Performs additional validation beyond the standard SWIFT field validation,
310    /// checking for common institutional reference format requirements.
311    ///
312    /// # Returns
313    /// `true` if the reference follows good practices, `false` otherwise
314    ///
315    /// # Example
316    /// ```rust
317    /// # use swift_mt_message::fields::Field20;
318    /// let good_ref = Field20::new("FT21234567890".to_string());
319    /// assert!(good_ref.is_well_formed());
320    ///
321    /// let poor_ref = Field20::new("x".to_string());
322    /// assert!(!poor_ref.is_well_formed());
323    /// ```
324    pub fn is_well_formed(&self) -> bool {
325        let ref_str = &self.transaction_reference;
326
327        // Check minimum length (at least 3 characters for meaningful reference)
328        if ref_str.len() < 3 {
329            return false;
330        }
331
332        // Check for reasonable character distribution (not all same character)
333        let unique_chars: std::collections::HashSet<char> = ref_str.chars().collect();
334        if unique_chars.len() < 2 {
335            return false;
336        }
337
338        // Check for valid characters (alphanumeric and common special chars)
339        if !ref_str
340            .chars()
341            .all(|c| c.is_alphanumeric() || matches!(c, '-' | '_' | '/' | '.'))
342        {
343            return false;
344        }
345
346        true
347    }
348}
349
350impl std::fmt::Display for Field20 {
351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352        write!(f, "{}", self.transaction_reference)
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn test_field20_creation() {
362        let field = Field20::new("FT21234567890".to_string());
363        assert_eq!(field.transaction_reference(), "FT21234567890");
364    }
365
366    #[test]
367    fn test_field20_parse() {
368        let field = Field20::parse("FT21234567890").unwrap();
369        assert_eq!(field.transaction_reference, "FT21234567890");
370    }
371
372    #[test]
373    fn test_field20_parse_with_prefix() {
374        let field = Field20::parse(":20:FT21234567890").unwrap();
375        assert_eq!(field.transaction_reference, "FT21234567890");
376    }
377
378    #[test]
379    fn test_field20_to_swift_string() {
380        let field = Field20::new("FT21234567890".to_string());
381        assert_eq!(field.to_swift_string(), ":20:FT21234567890");
382    }
383
384    #[test]
385    fn test_field20_validation() {
386        let valid_field = Field20::new("FT12345".to_string());
387        let result = valid_field.validate();
388        assert!(result.is_valid);
389
390        let invalid_field = Field20::new("THIS_IS_TOO_LONG_FOR_FIELD20".to_string());
391        let result = invalid_field.validate();
392        assert!(!result.is_valid);
393    }
394
395    #[test]
396    fn test_field20_format_spec() {
397        assert_eq!(Field20::format_spec(), "16x");
398    }
399
400    #[test]
401    fn test_field20_reference_pattern_detection() {
402        // Test wire transfer pattern
403        let ft_ref = Field20::new("FT21234567890".to_string());
404        assert_eq!(ft_ref.reference_pattern(), "Wire Transfer");
405
406        // Test transaction ID pattern
407        let txn_ref = Field20::new("TXN0000012345".to_string());
408        assert_eq!(txn_ref.reference_pattern(), "Transaction ID");
409
410        // Test reference ID pattern
411        let ref_ref = Field20::new("REF987654321".to_string());
412        assert_eq!(ref_ref.reference_pattern(), "Reference ID");
413
414        // Test customer reference pattern
415        let cust_ref = Field20::new("CUST12345678".to_string());
416        assert_eq!(cust_ref.reference_pattern(), "Customer Reference");
417
418        // Test client reference pattern
419        let cli_ref = Field20::new("CLI0000012345".to_string());
420        assert_eq!(cli_ref.reference_pattern(), "Client Reference");
421
422        // Test date-based pattern
423        let date_ref = Field20::new("20241201001".to_string());
424        assert_eq!(date_ref.reference_pattern(), "Date-based");
425
426        // Test numeric sequential pattern
427        let num_ref = Field20::new("123456789".to_string());
428        assert_eq!(num_ref.reference_pattern(), "Numeric Sequential");
429
430        // Test custom pattern
431        let custom_ref = Field20::new("CUSTOM_REF".to_string());
432        assert_eq!(custom_ref.reference_pattern(), "Customer Reference"); // Starts with "CUST"
433    }
434
435    #[test]
436    fn test_field20_case_insensitive_pattern_detection() {
437        // Test lowercase patterns
438        let ft_lower = Field20::new("ft21234567890".to_string());
439        assert_eq!(ft_lower.reference_pattern(), "Wire Transfer");
440
441        let txn_lower = Field20::new("txn0000012345".to_string());
442        assert_eq!(txn_lower.reference_pattern(), "Transaction ID");
443
444        // Test mixed case patterns
445        let mixed_case = Field20::new("Ft21234567890".to_string());
446        assert_eq!(mixed_case.reference_pattern(), "Wire Transfer");
447    }
448
449    #[test]
450    fn test_field20_date_based_detection() {
451        // Test valid date-based references
452        let date_ref1 = Field20::new("20241201001".to_string());
453        assert!(date_ref1.is_date_based());
454
455        let date_ref2 = Field20::new("20230315999".to_string());
456        assert!(date_ref2.is_date_based());
457
458        let date_ref3 = Field20::new("20221231ABC".to_string());
459        assert!(date_ref3.is_date_based());
460
461        // Test invalid date-based references
462        let invalid_year = Field20::new("19991201001".to_string());
463        assert!(!invalid_year.is_date_based());
464
465        let invalid_month = Field20::new("20241301001".to_string());
466        assert!(!invalid_month.is_date_based());
467
468        let invalid_day = Field20::new("20241232001".to_string());
469        assert!(!invalid_day.is_date_based());
470
471        let too_short = Field20::new("2024120".to_string());
472        assert!(!too_short.is_date_based());
473
474        let non_numeric = Field20::new("FT241201001".to_string());
475        assert!(!non_numeric.is_date_based());
476    }
477
478    #[test]
479    fn test_field20_sequence_number_extraction() {
480        // Test sequence number extraction
481        let seq_ref1 = Field20::new("FT001".to_string());
482        assert_eq!(seq_ref1.sequence_number(), Some(1));
483
484        let seq_ref2 = Field20::new("TXN123456789".to_string());
485        assert_eq!(seq_ref2.sequence_number(), Some(123456789));
486
487        let seq_ref3 = Field20::new("REF000000042".to_string());
488        assert_eq!(seq_ref3.sequence_number(), Some(42));
489
490        // Test no sequence number
491        let no_seq1 = Field20::new("CUSTOMREF".to_string());
492        assert_eq!(no_seq1.sequence_number(), None);
493
494        let no_seq2 = Field20::new("FT_NO_NUM".to_string());
495        assert_eq!(no_seq2.sequence_number(), None);
496
497        // Test all numeric reference
498        let all_num = Field20::new("123456789".to_string());
499        assert_eq!(all_num.sequence_number(), Some(123456789));
500
501        // Test sequence too long (more than 10 digits)
502        let too_long = Field20::new("FT12345678901".to_string());
503        assert_eq!(too_long.sequence_number(), None);
504    }
505
506    #[test]
507    fn test_field20_description_generation() {
508        // Test wire transfer description
509        let ft_ref = Field20::new("FT21234567890".to_string());
510        let description = ft_ref.description();
511        assert!(description.contains("FT21234567890"));
512        assert!(description.contains("Wire Transfer"));
513        assert!(description.contains("Length: 13"));
514
515        // Test date-based (no sequence because entire string is numeric)
516        let date_seq_ref = Field20::new("20241201001".to_string());
517        let description = date_seq_ref.description();
518        assert!(description.contains("Date-based"));
519        assert!(!description.contains("Sequence:")); // No sequence for all-numeric
520
521        // Test custom reference
522        let custom_ref = Field20::new("MYREF123".to_string());
523        let description = custom_ref.description();
524        assert!(description.contains("Custom"));
525        assert!(description.contains("Sequence: 123"));
526    }
527
528    #[test]
529    fn test_field20_well_formed_validation() {
530        // Test well-formed references
531        let good_ref1 = Field20::new("FT21234567890".to_string());
532        assert!(good_ref1.is_well_formed());
533
534        let good_ref2 = Field20::new("TXN-123456".to_string());
535        assert!(good_ref2.is_well_formed());
536
537        let good_ref3 = Field20::new("REF_001.234".to_string());
538        assert!(good_ref3.is_well_formed());
539
540        let good_ref4 = Field20::new("CUST/12345".to_string());
541        assert!(good_ref4.is_well_formed());
542
543        // Test poorly formed references
544        let too_short = Field20::new("AB".to_string());
545        assert!(!too_short.is_well_formed());
546
547        let all_same = Field20::new("AAAAAAA".to_string());
548        assert!(!all_same.is_well_formed());
549
550        let invalid_chars = Field20::new("REF@123#".to_string());
551        assert!(!invalid_chars.is_well_formed());
552
553        let spaces = Field20::new("REF 123".to_string());
554        assert!(!spaces.is_well_formed());
555    }
556
557    #[test]
558    fn test_field20_display_formatting() {
559        let field = Field20::new("FT21234567890".to_string());
560        assert_eq!(format!("{}", field), "FT21234567890");
561
562        let field2 = Field20::new("TXN0000012345".to_string());
563        assert_eq!(format!("{}", field2), "TXN0000012345");
564    }
565
566    #[test]
567    fn test_field20_edge_cases() {
568        // Test minimum valid length
569        let min_ref = Field20::new("ABC".to_string());
570        assert_eq!(min_ref.transaction_reference(), "ABC");
571        assert!(min_ref.is_well_formed());
572
573        // Test maximum length
574        let max_ref = Field20::new("1234567890123456".to_string());
575        assert_eq!(max_ref.transaction_reference(), "1234567890123456");
576        assert_eq!(max_ref.transaction_reference().len(), 16);
577
578        // Test single character (should not be well-formed)
579        let single_char = Field20::new("A".to_string());
580        assert!(!single_char.is_well_formed());
581
582        // Test empty string (should not be well-formed)
583        let empty_ref = Field20::new("".to_string());
584        assert!(!empty_ref.is_well_formed());
585    }
586
587    #[test]
588    fn test_field20_real_world_examples() {
589        // Test realistic wire transfer reference
590        let wire_ref = Field20::new("FT001".to_string());
591        assert_eq!(wire_ref.reference_pattern(), "Wire Transfer");
592        assert!(!wire_ref.is_date_based());
593        assert_eq!(wire_ref.sequence_number(), Some(1));
594        assert!(wire_ref.is_well_formed());
595
596        // Test system-generated transaction ID
597        let sys_ref = Field20::new("TXN0000012345".to_string());
598        assert_eq!(sys_ref.reference_pattern(), "Transaction ID");
599        assert!(!sys_ref.is_date_based());
600        assert_eq!(sys_ref.sequence_number(), Some(12345));
601        assert!(sys_ref.is_well_formed());
602
603        // Test customer reference with check digit
604        let cust_ref = Field20::new("CUST123456789A".to_string());
605        assert_eq!(cust_ref.reference_pattern(), "Customer Reference");
606        assert!(!cust_ref.is_date_based());
607        assert_eq!(cust_ref.sequence_number(), None); // No sequence because it ends with 'A'
608        assert!(cust_ref.is_well_formed());
609
610        // Test branch-based reference
611        let branch_ref = Field20::new("NYC001234567".to_string());
612        assert_eq!(branch_ref.reference_pattern(), "Custom");
613        assert!(!branch_ref.is_date_based());
614        assert_eq!(branch_ref.sequence_number(), Some(1234567));
615        assert!(branch_ref.is_well_formed());
616    }
617
618    #[test]
619    fn test_field20_serialization() {
620        let field = Field20::new("FT21234567890".to_string());
621
622        // Test JSON serialization
623        let json = serde_json::to_string(&field).unwrap();
624        let deserialized: Field20 = serde_json::from_str(&json).unwrap();
625
626        assert_eq!(field, deserialized);
627        assert_eq!(
628            field.transaction_reference(),
629            deserialized.transaction_reference()
630        );
631    }
632
633    #[test]
634    fn test_field20_pattern_edge_cases() {
635        // Test patterns with minimum required characters
636        let ft_min = Field20::new("FT1".to_string());
637        assert_eq!(ft_min.reference_pattern(), "Wire Transfer");
638
639        let txn_min = Field20::new("TXN".to_string());
640        assert_eq!(txn_min.reference_pattern(), "Transaction ID");
641
642        // Test date pattern edge cases
643        let date_edge1 = Field20::new("20000101".to_string());
644        assert!(date_edge1.is_date_based());
645
646        let date_edge2 = Field20::new("20991231".to_string());
647        assert!(date_edge2.is_date_based());
648
649        let date_edge3 = Field20::new("20240229".to_string()); // Leap year
650        assert!(date_edge3.is_date_based());
651    }
652
653    #[test]
654    fn test_field20_sequence_extraction_edge_cases() {
655        // Test zero sequence
656        let zero_seq = Field20::new("FT000000000".to_string());
657        assert_eq!(zero_seq.sequence_number(), Some(0));
658
659        // Test single digit sequence
660        let single_digit = Field20::new("REF1".to_string());
661        assert_eq!(single_digit.sequence_number(), Some(1));
662
663        // Test maximum valid sequence (10 digits)
664        let max_seq = Field20::new("A1234567890".to_string());
665        assert_eq!(max_seq.sequence_number(), Some(1234567890));
666
667        // Test sequence with leading zeros
668        let leading_zeros = Field20::new("TXN0000000001".to_string());
669        assert_eq!(leading_zeros.sequence_number(), Some(1));
670    }
671
672    #[test]
673    fn test_field20_validation_comprehensive() {
674        // Test various validation scenarios
675        let valid_cases = vec![
676            "FT12345",
677            "TXN0000012345",
678            "20241201001",
679            "CUST123456789A",
680            "REF-123_456.78",
681            "ABC/DEF",
682        ];
683
684        for case in valid_cases {
685            let field = Field20::new(case.to_string());
686            let validation = field.validate();
687            assert!(validation.is_valid, "Failed validation for: {}", case);
688        }
689
690        // Test invalid cases
691        let invalid_cases = vec![
692            "THIS_IS_WAY_TOO_LONG_FOR_FIELD20_VALIDATION", // Too long
693        ];
694
695        for case in invalid_cases {
696            let field = Field20::new(case.to_string());
697            let validation = field.validate();
698            assert!(
699                !validation.is_valid,
700                "Should have failed validation for: {}",
701                case
702            );
703        }
704
705        // Test empty case separately (it's valid for Field20 creation but not well-formed)
706        let empty_field = Field20::new("".to_string());
707        let _validation = empty_field.validate();
708        // Empty field might be valid for Field20 but not well-formed
709        assert!(!empty_field.is_well_formed());
710    }
711}