swift_mt_message/fields/field21.rs
1use crate::SwiftField;
2use serde::{Deserialize, Serialize};
3
4/// # Field 21: Related Reference
5///
6/// ## Overview
7/// Field 21 contains the sender's reference to the related transaction or message.
8/// This field establishes a link between the current message and a previously sent
9/// message or transaction, enabling tracking of related operations and maintaining
10/// audit trails across multiple message exchanges in correspondent banking and
11/// payment processing workflows.
12///
13/// ## Format Specification
14/// **Format**: `16x`
15/// - **16x**: Up to 16 alphanumeric characters
16/// - **Character set**: A-Z, a-z, 0-9, and limited special characters
17/// - **Case sensitivity**: Preserved as entered
18/// - **Padding**: No padding required (variable length up to 16 characters)
19///
20/// ## Structure and Content
21/// The related reference typically links to:
22/// ```text
23/// FT21034567890123
24/// │└─────────────┘
25/// │ Related transaction reference
26/// └── Transaction type prefix (optional)
27/// ```
28///
29/// ## Common Reference Patterns
30/// Different institutions use various patterns for related references:
31/// - **Sequential linking**: `REL000001`, `REL000002`, `REL000003`
32/// - **Original reference**: Same as Field 20 from original message
33/// - **Cover references**: `COV001234567`, `COV987654321`
34/// - **Amendment references**: `AMD1234567890`, `AMD0987654321`
35/// - **Cancellation references**: `CAN12345678`, `CAN87654321`
36///
37/// ## Usage Context
38/// Field 21 is used in numerous SWIFT MT message types:
39/// - **MT202**: General Financial Institution Transfer
40/// - **MT202COV**: Cover for customer credit transfer
41/// - **MT205**: Financial Institution Transfer for its own account
42/// - **MT210**: Notice to Receive
43/// - **MT292**: Request for Cancellation
44/// - **MT296**: Answer to Amendment/Cancellation Request
45///
46/// ### Business Applications
47/// - **Transaction linking**: Connecting related messages and transactions
48/// - **Cover operations**: Linking cover messages to original customer transfers
49/// - **Amendment tracking**: Referencing original messages in amendments
50/// - **Cancellation requests**: Identifying transactions to be cancelled
51/// - **Reconciliation**: Matching related transactions across systems
52/// - **Audit trails**: Maintaining complete transaction history chains
53/// - **Regulatory reporting**: Providing transaction relationship information
54///
55/// ## Validation Rules
56/// 1. **Length**: Maximum 16 characters
57/// 2. **Character set**: Alphanumeric characters and limited special characters
58/// 3. **Non-empty**: Cannot be empty or contain only whitespace
59/// 4. **Format consistency**: Should follow institutional standards
60/// 5. **Relationship validity**: Should reference valid related transactions
61///
62/// ## Network Validated Rules (SWIFT Standards)
63/// - Related reference must not exceed 16 characters (Error: T13)
64/// - Must contain valid SWIFT character set (Error: T61)
65/// - Cannot be empty (Error: T18)
66/// - Should reference existing transaction when applicable (Warning: recommended practice)
67///
68/// ## Examples
69/// ```text
70/// :21:FT21234567890
71/// └─── Related wire transfer reference
72///
73/// :21:COV0000012345
74/// └─── Cover message reference
75///
76/// :21:NYC20241201001
77/// └─── Branch and date-based related reference
78///
79/// :21:AMD123456789A
80/// └─── Amendment reference with check digit
81/// ```
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, SwiftField)]
84#[format("16x")]
85pub struct Field21 {
86 /// Related reference value (maximum 16 characters)
87 ///
88 /// Contains the sender's reference to the related transaction or message.
89 /// This value establishes the link between the current message and a
90 /// previously sent message, enabling transaction tracking and audit trails.
91 ///
92 /// **Format**: Up to 16 alphanumeric characters
93 /// **Character set**: A-Z, a-z, 0-9, and limited special characters
94 /// **Case sensitivity**: Preserved as entered
95 /// **Purpose**: Links current message to related transaction or message
96 ///
97 /// # Examples
98 /// - `"FT21234567890"` - Related wire transfer reference
99 /// - `"COV0000012345"` - Cover message reference
100 /// - `"AMD20241201001"` - Amendment reference
101 /// - `"CAN123456789A"` - Cancellation reference with check digit
102 #[format("16x")]
103 pub related_reference: String,
104}
105
106impl Field21 {
107 /// Create a new Field21 with comprehensive validation
108 ///
109 /// Creates a new related reference field with the provided reference string.
110 /// The reference is validated for length and character set compliance.
111 ///
112 /// # Arguments
113 /// * `related_reference` - The related reference string (max 16 chars)
114 ///
115 /// # Examples
116 /// ```rust
117 /// use swift_mt_message::fields::Field21;
118 ///
119 /// // Standard related reference
120 /// let field = Field21::new("FT21234567890".to_string());
121 ///
122 /// // Cover message reference
123 /// let field = Field21::new("COV0000012345".to_string());
124 ///
125 /// // Amendment reference
126 /// let field = Field21::new("AMD20241201001".to_string());
127 /// ```
128 ///
129 /// # Validation
130 /// The constructor performs basic validation but full validation
131 /// occurs when calling `validate()` method or during SWIFT message processing.
132 pub fn new(related_reference: String) -> Self {
133 Self { related_reference }
134 }
135
136 /// Get the related reference value
137 ///
138 /// Returns the related reference string that links this message
139 /// to a previously sent message or transaction.
140 ///
141 /// # Returns
142 /// A string slice containing the related reference
143 ///
144 /// # Example
145 /// ```rust
146 /// # use swift_mt_message::fields::Field21;
147 /// let field = Field21::new("FT21234567890".to_string());
148 /// assert_eq!(field.related_reference(), "FT21234567890");
149 /// ```
150 pub fn related_reference(&self) -> &str {
151 &self.related_reference
152 }
153
154 /// Check if the reference follows a common pattern
155 ///
156 /// Analyzes the related reference to determine if it follows
157 /// common institutional patterns for reference generation.
158 ///
159 /// # Returns
160 /// A string describing the detected pattern, or "Custom" if no pattern is detected
161 ///
162 /// # Example
163 /// ```rust
164 /// # use swift_mt_message::fields::Field21;
165 /// let cover_ref = Field21::new("COV21234567890".to_string());
166 /// assert_eq!(cover_ref.reference_pattern(), "Cover Message");
167 ///
168 /// let amd_ref = Field21::new("AMD0000012345".to_string());
169 /// assert_eq!(amd_ref.reference_pattern(), "Amendment");
170 /// ```
171 pub fn reference_pattern(&self) -> &'static str {
172 let ref_upper = self.related_reference.to_uppercase();
173
174 if ref_upper.starts_with("COV") {
175 "Cover Message"
176 } else if ref_upper.starts_with("AMD") {
177 "Amendment"
178 } else if ref_upper.starts_with("CAN") {
179 "Cancellation"
180 } else if ref_upper.starts_with("REL") {
181 "Related Transaction"
182 } else if ref_upper.starts_with("FT") {
183 "Wire Transfer"
184 } else if ref_upper.starts_with("TXN") {
185 "Transaction ID"
186 } else if ref_upper.starts_with("REF") {
187 "Reference ID"
188 } else if self.is_date_based() {
189 "Date-based"
190 } else if ref_upper.chars().all(|c| c.is_ascii_digit()) {
191 "Numeric Sequential"
192 } else {
193 "Custom"
194 }
195 }
196
197 /// Check if the reference appears to be date-based
198 ///
199 /// Determines if the related reference contains date information
200 /// based on common date patterns in reference strings.
201 ///
202 /// # Returns
203 /// `true` if the reference appears to contain date information
204 ///
205 /// # Example
206 /// ```rust
207 /// # use swift_mt_message::fields::Field21;
208 /// let date_ref = Field21::new("20241201001".to_string());
209 /// assert!(date_ref.is_date_based());
210 ///
211 /// let simple_ref = Field21::new("COV12345".to_string());
212 /// assert!(!simple_ref.is_date_based());
213 /// ```
214 pub fn is_date_based(&self) -> bool {
215 let ref_str = &self.related_reference;
216
217 // Check for YYYYMMDD pattern (8 digits at start)
218 if ref_str.len() >= 8 {
219 let date_part = &ref_str[0..8];
220 if date_part.chars().all(|c| c.is_ascii_digit()) {
221 // Basic date validation (year 2000-2099, month 01-12, day 01-31)
222 if let (Ok(year), Ok(month), Ok(day)) = (
223 date_part[0..4].parse::<u32>(),
224 date_part[4..6].parse::<u32>(),
225 date_part[6..8].parse::<u32>(),
226 ) {
227 if (2000..=2099).contains(&year)
228 && (1..=12).contains(&month)
229 && (1..=31).contains(&day)
230 {
231 return true;
232 }
233 }
234 }
235 }
236
237 // Check for YYYYMMDD pattern embedded in the string (after prefix)
238 if ref_str.len() >= 8 {
239 for i in 0..=ref_str.len() - 8 {
240 let date_part = &ref_str[i..i + 8];
241 if date_part.chars().all(|c| c.is_ascii_digit()) {
242 // Basic date validation (year 2000-2099, month 01-12, day 01-31)
243 if let (Ok(year), Ok(month), Ok(day)) = (
244 date_part[0..4].parse::<u32>(),
245 date_part[4..6].parse::<u32>(),
246 date_part[6..8].parse::<u32>(),
247 ) {
248 if (2000..=2099).contains(&year)
249 && (1..=12).contains(&month)
250 && (1..=31).contains(&day)
251 {
252 return true;
253 }
254 }
255 }
256 }
257 }
258
259 false
260 }
261
262 /// Extract potential sequence number from reference
263 ///
264 /// Attempts to extract a sequence number from the related reference,
265 /// which is commonly found at the end of structured references.
266 ///
267 /// # Returns
268 /// `Some(number)` if a sequence number is found, `None` otherwise
269 ///
270 /// # Example
271 /// ```rust
272 /// # use swift_mt_message::fields::Field21;
273 /// let seq_ref1 = Field21::new("COV001".to_string());
274 /// assert_eq!(seq_ref1.sequence_number(), Some(1));
275 ///
276 /// let seq_ref2 = Field21::new("AMD123456789".to_string());
277 /// assert_eq!(seq_ref2.sequence_number(), Some(123456789));
278 ///
279 /// let seq_ref3 = Field21::new("CAN000000042".to_string());
280 /// assert_eq!(seq_ref3.sequence_number(), Some(42));
281 ///
282 /// let no_seq1 = Field21::new("CUSTOMREF".to_string());
283 /// assert_eq!(no_seq1.sequence_number(), None);
284 ///
285 /// let no_seq2 = Field21::new("COV_NO_NUM".to_string());
286 /// assert_eq!(no_seq2.sequence_number(), None);
287 ///
288 /// let all_num = Field21::new("123456789".to_string());
289 /// assert_eq!(all_num.sequence_number(), Some(123456789));
290 ///
291 /// let too_long = Field21::new("AMD12345678901".to_string());
292 /// assert_eq!(too_long.sequence_number(), None);
293 /// ```
294 pub fn sequence_number(&self) -> Option<u32> {
295 let ref_str = &self.related_reference;
296
297 // Find the longest numeric suffix
298 let mut numeric_suffix = String::new();
299 for ch in ref_str.chars().rev() {
300 if ch.is_ascii_digit() {
301 numeric_suffix.insert(0, ch);
302 } else {
303 break;
304 }
305 }
306
307 if numeric_suffix.is_empty() {
308 return None;
309 }
310
311 // If the numeric suffix is too long, it might contain a date pattern
312 // Try to extract just the sequence part after the date
313 if numeric_suffix.len() > 10 {
314 // Check if this looks like a date followed by a sequence number
315 // Common patterns: YYYYMMDD + sequence (8 + up to 3 digits)
316 if numeric_suffix.len() >= 8 {
317 let potential_date = &numeric_suffix[0..8];
318 let potential_seq = &numeric_suffix[8..];
319
320 // Validate if the first 8 digits could be a date
321 if let (Ok(year), Ok(month), Ok(day)) = (
322 potential_date[0..4].parse::<u32>(),
323 potential_date[4..6].parse::<u32>(),
324 potential_date[6..8].parse::<u32>(),
325 ) {
326 if (2000..=2099).contains(&year)
327 && (1..=12).contains(&month)
328 && (1..=31).contains(&day)
329 && !potential_seq.is_empty()
330 && potential_seq.len() <= 10
331 {
332 return potential_seq.parse().ok();
333 }
334 }
335 }
336 return None;
337 }
338
339 // Normal case: numeric suffix is reasonable length
340 if numeric_suffix.len() <= 10 {
341 numeric_suffix.parse().ok()
342 } else {
343 None
344 }
345 }
346
347 /// Check if this is a cover message reference
348 ///
349 /// Determines if the related reference indicates this is related
350 /// to a cover message operation.
351 ///
352 /// # Returns
353 /// `true` if the reference appears to be for a cover message
354 ///
355 /// # Example
356 /// ```rust
357 /// # use swift_mt_message::fields::Field21;
358 /// let cover_ref = Field21::new("COV21234567890".to_string());
359 /// assert!(cover_ref.is_cover_reference());
360 ///
361 /// let regular_ref = Field21::new("FT12345".to_string());
362 /// assert!(!regular_ref.is_cover_reference());
363 /// ```
364 pub fn is_cover_reference(&self) -> bool {
365 self.related_reference.to_uppercase().starts_with("COV")
366 }
367
368 /// Check if this is an amendment reference
369 ///
370 /// Determines if the related reference indicates this is related
371 /// to an amendment operation.
372 ///
373 /// # Returns
374 /// `true` if the reference appears to be for an amendment
375 ///
376 /// # Example
377 /// ```rust
378 /// # use swift_mt_message::fields::Field21;
379 /// let amd_ref = Field21::new("AMD21234567890".to_string());
380 /// assert!(amd_ref.is_amendment_reference());
381 ///
382 /// let regular_ref = Field21::new("FT12345".to_string());
383 /// assert!(!regular_ref.is_amendment_reference());
384 /// ```
385 pub fn is_amendment_reference(&self) -> bool {
386 self.related_reference.to_uppercase().starts_with("AMD")
387 }
388
389 /// Check if this is a cancellation reference
390 ///
391 /// Determines if the related reference indicates this is related
392 /// to a cancellation operation.
393 ///
394 /// # Returns
395 /// `true` if the reference appears to be for a cancellation
396 ///
397 /// # Example
398 /// ```rust
399 /// # use swift_mt_message::fields::Field21;
400 /// let can_ref = Field21::new("CAN21234567890".to_string());
401 /// assert!(can_ref.is_cancellation_reference());
402 ///
403 /// let regular_ref = Field21::new("FT12345".to_string());
404 /// assert!(!regular_ref.is_cancellation_reference());
405 /// ```
406 pub fn is_cancellation_reference(&self) -> bool {
407 self.related_reference.to_uppercase().starts_with("CAN")
408 }
409
410 /// Get a human-readable description of the related reference
411 ///
412 /// Returns a descriptive string explaining the related reference
413 /// format and detected patterns.
414 ///
415 /// # Returns
416 /// A descriptive string
417 ///
418 /// # Example
419 /// ```rust
420 /// # use swift_mt_message::fields::Field21;
421 /// let field = Field21::new("COV21234567890".to_string());
422 /// println!("{}", field.description());
423 /// ```
424 pub fn description(&self) -> String {
425 let pattern = self.reference_pattern();
426 let length = self.related_reference.len();
427
428 let mut desc = format!(
429 "Related Reference: {} (Pattern: {}, Length: {})",
430 self.related_reference, pattern, length
431 );
432
433 if self.is_cover_reference() {
434 desc.push_str(", Cover Message");
435 } else if self.is_amendment_reference() {
436 desc.push_str(", Amendment");
437 } else if self.is_cancellation_reference() {
438 desc.push_str(", Cancellation");
439 }
440
441 if self.is_date_based() {
442 desc.push_str(", Date-based");
443 }
444
445 if let Some(seq) = self.sequence_number() {
446 desc.push_str(&format!(", Sequence: {}", seq));
447 }
448
449 desc
450 }
451
452 /// Validate reference format according to institutional standards
453 ///
454 /// Performs additional validation beyond the standard SWIFT field validation,
455 /// checking for common institutional reference format requirements.
456 ///
457 /// # Returns
458 /// `true` if the reference follows good practices, `false` otherwise
459 ///
460 /// # Example
461 /// ```rust
462 /// # use swift_mt_message::fields::Field21;
463 /// let good_ref1 = Field21::new("COV21234567890".to_string());
464 /// assert!(good_ref1.is_well_formed());
465 ///
466 /// let good_ref2 = Field21::new("AMD-123456".to_string());
467 /// assert!(good_ref2.is_well_formed());
468 ///
469 /// let good_ref3 = Field21::new("REL_001.234".to_string());
470 /// assert!(good_ref3.is_well_formed());
471 ///
472 /// let good_ref4 = Field21::new("CAN/12345".to_string());
473 /// assert!(good_ref4.is_well_formed());
474 ///
475 /// let too_short = Field21::new("AB".to_string());
476 /// assert!(!too_short.is_well_formed());
477 ///
478 /// let all_same = Field21::new("AAAAAAA".to_string());
479 /// assert!(!all_same.is_well_formed());
480 ///
481 /// let invalid_chars = Field21::new("REF@123#".to_string());
482 /// assert!(!invalid_chars.is_well_formed());
483 ///
484 /// let spaces = Field21::new("REF 123".to_string());
485 /// assert!(!spaces.is_well_formed());
486 /// ```
487 pub fn is_well_formed(&self) -> bool {
488 let ref_str = &self.related_reference;
489
490 // Check minimum length (at least 3 characters for meaningful reference)
491 if ref_str.len() < 3 {
492 return false;
493 }
494
495 // Check for reasonable character distribution (not all same character)
496 let unique_chars: std::collections::HashSet<char> = ref_str.chars().collect();
497 if unique_chars.len() < 2 {
498 return false;
499 }
500
501 // Check for valid characters (alphanumeric and common special chars)
502 if !ref_str
503 .chars()
504 .all(|c| c.is_alphanumeric() || matches!(c, '-' | '_' | '/' | '.'))
505 {
506 return false;
507 }
508
509 true
510 }
511}
512
513impl std::fmt::Display for Field21 {
514 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515 write!(f, "{}", self.related_reference)
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 #[test]
524 fn test_field21_creation() {
525 let field = Field21::new("COV21234567890".to_string());
526 assert_eq!(field.related_reference(), "COV21234567890");
527 }
528
529 #[test]
530 fn test_field21_parse() {
531 let field = Field21::parse("AMD21234567890").unwrap();
532 assert_eq!(field.related_reference, "AMD21234567890");
533 }
534
535 #[test]
536 fn test_field21_parse_with_prefix() {
537 let field = Field21::parse(":21:CAN21234567890").unwrap();
538 assert_eq!(field.related_reference, "CAN21234567890");
539 }
540
541 #[test]
542 fn test_field21_to_swift_string() {
543 let field = Field21::new("REL21234567890".to_string());
544 assert_eq!(field.to_swift_string(), ":21:REL21234567890");
545 }
546
547 #[test]
548 fn test_field21_validation() {
549 let valid_field = Field21::new("COV12345".to_string());
550 let result = valid_field.validate();
551 assert!(result.is_valid);
552
553 let invalid_field = Field21::new("THIS_IS_TOO_LONG_FOR_FIELD21".to_string());
554 let result = invalid_field.validate();
555 assert!(!result.is_valid);
556 }
557
558 #[test]
559 fn test_field21_format_spec() {
560 assert_eq!(Field21::format_spec(), "16x");
561 }
562
563 #[test]
564 fn test_field21_reference_pattern_detection() {
565 // Test cover message pattern
566 let cov_ref = Field21::new("COV21234567890".to_string());
567 assert_eq!(cov_ref.reference_pattern(), "Cover Message");
568
569 // Test amendment pattern
570 let amd_ref = Field21::new("AMD0000012345".to_string());
571 assert_eq!(amd_ref.reference_pattern(), "Amendment");
572
573 // Test cancellation pattern
574 let can_ref = Field21::new("CAN987654321".to_string());
575 assert_eq!(can_ref.reference_pattern(), "Cancellation");
576
577 // Test related transaction pattern
578 let rel_ref = Field21::new("REL12345678".to_string());
579 assert_eq!(rel_ref.reference_pattern(), "Related Transaction");
580
581 // Test wire transfer pattern
582 let ft_ref = Field21::new("FT21234567890".to_string());
583 assert_eq!(ft_ref.reference_pattern(), "Wire Transfer");
584
585 // Test transaction ID pattern
586 let txn_ref = Field21::new("TXN0000012345".to_string());
587 assert_eq!(txn_ref.reference_pattern(), "Transaction ID");
588
589 // Test reference ID pattern
590 let ref_ref = Field21::new("REF987654321".to_string());
591 assert_eq!(ref_ref.reference_pattern(), "Reference ID");
592
593 // Test date-based pattern
594 let date_ref = Field21::new("20241201001".to_string());
595 assert_eq!(date_ref.reference_pattern(), "Date-based");
596
597 // Test numeric sequential pattern
598 let num_ref = Field21::new("123456789".to_string());
599 assert_eq!(num_ref.reference_pattern(), "Numeric Sequential");
600
601 // Test custom pattern
602 let custom_ref = Field21::new("CUSTOM_REF".to_string());
603 assert_eq!(custom_ref.reference_pattern(), "Custom");
604 }
605
606 #[test]
607 fn test_field21_case_insensitive_pattern_detection() {
608 // Test lowercase patterns
609 let cov_lower = Field21::new("cov21234567890".to_string());
610 assert_eq!(cov_lower.reference_pattern(), "Cover Message");
611
612 let amd_lower = Field21::new("amd0000012345".to_string());
613 assert_eq!(amd_lower.reference_pattern(), "Amendment");
614
615 // Test mixed case patterns
616 let mixed_case = Field21::new("Can21234567890".to_string());
617 assert_eq!(mixed_case.reference_pattern(), "Cancellation");
618 }
619
620 #[test]
621 fn test_field21_date_based_detection() {
622 // Test valid date-based references
623 let date_ref1 = Field21::new("20241201001".to_string());
624 assert!(date_ref1.is_date_based());
625
626 let date_ref2 = Field21::new("20230315999".to_string());
627 assert!(date_ref2.is_date_based());
628
629 let date_ref3 = Field21::new("20221231ABC".to_string());
630 assert!(date_ref3.is_date_based());
631
632 // Test invalid date-based references
633 let invalid_year = Field21::new("19991201001".to_string());
634 assert!(!invalid_year.is_date_based());
635
636 let invalid_month = Field21::new("20241301001".to_string());
637 assert!(!invalid_month.is_date_based());
638
639 let invalid_day = Field21::new("20241232001".to_string());
640 assert!(!invalid_day.is_date_based());
641
642 let too_short = Field21::new("2024120".to_string());
643 assert!(!too_short.is_date_based());
644
645 let non_numeric = Field21::new("COV241201001".to_string());
646 assert!(!non_numeric.is_date_based());
647 }
648
649 #[test]
650 fn test_field21_sequence_number_extraction() {
651 // Test sequence number extraction
652 let seq_ref1 = Field21::new("COV001".to_string());
653 assert_eq!(seq_ref1.sequence_number(), Some(1));
654
655 let seq_ref2 = Field21::new("AMD123456789".to_string());
656 assert_eq!(seq_ref2.sequence_number(), Some(123456789));
657
658 let seq_ref3 = Field21::new("CAN000000042".to_string());
659 assert_eq!(seq_ref3.sequence_number(), Some(42));
660
661 // Test no sequence number
662 let no_seq1 = Field21::new("CUSTOMREF".to_string());
663 assert_eq!(no_seq1.sequence_number(), None);
664
665 let no_seq2 = Field21::new("COV_NO_NUM".to_string());
666 assert_eq!(no_seq2.sequence_number(), None);
667
668 // Test all numeric reference
669 let all_num = Field21::new("123456789".to_string());
670 assert_eq!(all_num.sequence_number(), Some(123456789));
671
672 // Test sequence too long (more than 10 digits)
673 let too_long = Field21::new("AMD12345678901".to_string());
674 assert_eq!(too_long.sequence_number(), None);
675 }
676
677 #[test]
678 fn test_field21_operation_type_detection() {
679 // Test cover reference detection
680 let cov_ref = Field21::new("COV21234567890".to_string());
681 assert!(cov_ref.is_cover_reference());
682 assert!(!cov_ref.is_amendment_reference());
683 assert!(!cov_ref.is_cancellation_reference());
684
685 // Test amendment reference detection
686 let amd_ref = Field21::new("AMD21234567890".to_string());
687 assert!(!amd_ref.is_cover_reference());
688 assert!(amd_ref.is_amendment_reference());
689 assert!(!amd_ref.is_cancellation_reference());
690
691 // Test cancellation reference detection
692 let can_ref = Field21::new("CAN21234567890".to_string());
693 assert!(!can_ref.is_cover_reference());
694 assert!(!can_ref.is_amendment_reference());
695 assert!(can_ref.is_cancellation_reference());
696
697 // Test regular reference
698 let reg_ref = Field21::new("FT12345".to_string());
699 assert!(!reg_ref.is_cover_reference());
700 assert!(!reg_ref.is_amendment_reference());
701 assert!(!reg_ref.is_cancellation_reference());
702
703 // Test case insensitive detection
704 let cov_lower = Field21::new("cov12345".to_string());
705 assert!(cov_lower.is_cover_reference());
706 }
707
708 #[test]
709 fn test_field21_description_generation() {
710 // Test cover message description
711 let cov_ref = Field21::new("COV21234567890".to_string());
712 let description = cov_ref.description();
713 assert!(description.contains("COV21234567890"));
714 assert!(description.contains("Cover Message"));
715 assert!(description.contains("Length: 14"));
716
717 // Test amendment description
718 let amd_seq_ref = Field21::new("AMD20241201001".to_string());
719 let description = amd_seq_ref.description();
720 assert!(description.contains("Amendment"));
721 assert!(description.contains("Date-based"));
722
723 // Test custom reference
724 let custom_ref = Field21::new("MYREF123".to_string());
725 let description = custom_ref.description();
726 assert!(description.contains("Custom"));
727 assert!(description.contains("Sequence: 123"));
728 }
729
730 #[test]
731 fn test_field21_well_formed_validation() {
732 // Test well-formed references
733 let good_ref1 = Field21::new("COV21234567890".to_string());
734 assert!(good_ref1.is_well_formed());
735
736 let good_ref2 = Field21::new("AMD-123456".to_string());
737 assert!(good_ref2.is_well_formed());
738
739 let good_ref3 = Field21::new("REL_001.234".to_string());
740 assert!(good_ref3.is_well_formed());
741
742 let good_ref4 = Field21::new("CAN/12345".to_string());
743 assert!(good_ref4.is_well_formed());
744
745 // Test poorly formed references
746 let too_short = Field21::new("AB".to_string());
747 assert!(!too_short.is_well_formed());
748
749 let all_same = Field21::new("AAAAAAA".to_string());
750 assert!(!all_same.is_well_formed());
751
752 let invalid_chars = Field21::new("REF@123#".to_string());
753 assert!(!invalid_chars.is_well_formed());
754
755 let spaces = Field21::new("REF 123".to_string());
756 assert!(!spaces.is_well_formed());
757 }
758
759 #[test]
760 fn test_field21_display_formatting() {
761 let field = Field21::new("COV21234567890".to_string());
762 assert_eq!(format!("{}", field), "COV21234567890");
763
764 let field2 = Field21::new("AMD0000012345".to_string());
765 assert_eq!(format!("{}", field2), "AMD0000012345");
766 }
767
768 #[test]
769 fn test_field21_edge_cases() {
770 // Test minimum valid length
771 let min_ref = Field21::new("COV".to_string());
772 assert_eq!(min_ref.related_reference(), "COV");
773 assert!(min_ref.is_well_formed());
774
775 // Test maximum length
776 let max_ref = Field21::new("1234567890123456".to_string());
777 assert_eq!(max_ref.related_reference(), "1234567890123456");
778 assert_eq!(max_ref.related_reference().len(), 16);
779
780 // Test single character (should not be well-formed)
781 let single_char = Field21::new("A".to_string());
782 assert!(!single_char.is_well_formed());
783
784 // Test empty string (should not be well-formed)
785 let empty_ref = Field21::new("".to_string());
786 assert!(!empty_ref.is_well_formed());
787 }
788
789 #[test]
790 fn test_field21_real_world_examples() {
791 // Test realistic cover reference
792 let cover_ref = Field21::new("COV001".to_string());
793 assert_eq!(cover_ref.reference_pattern(), "Cover Message");
794 assert!(!cover_ref.is_date_based());
795 assert_eq!(cover_ref.sequence_number(), Some(1));
796 assert!(cover_ref.is_well_formed());
797 assert!(cover_ref.is_cover_reference());
798
799 // Test amendment reference
800 let amd_ref = Field21::new("AMD0000012345".to_string());
801 assert_eq!(amd_ref.reference_pattern(), "Amendment");
802 assert!(!amd_ref.is_date_based());
803 assert_eq!(amd_ref.sequence_number(), Some(12345));
804 assert!(amd_ref.is_well_formed());
805 assert!(amd_ref.is_amendment_reference());
806
807 // Test cancellation reference with date
808 let can_ref = Field21::new("CAN20241201001".to_string());
809 assert_eq!(can_ref.reference_pattern(), "Cancellation");
810 assert!(can_ref.is_date_based());
811 assert_eq!(can_ref.sequence_number(), Some(1));
812 assert!(can_ref.is_well_formed());
813 assert!(can_ref.is_cancellation_reference());
814
815 // Test related transaction reference
816 let rel_ref = Field21::new("REL001234567".to_string());
817 assert_eq!(rel_ref.reference_pattern(), "Related Transaction");
818 assert!(!rel_ref.is_date_based());
819 assert_eq!(rel_ref.sequence_number(), Some(1234567));
820 assert!(rel_ref.is_well_formed());
821 }
822
823 #[test]
824 fn test_field21_serialization() {
825 let field = Field21::new("COV21234567890".to_string());
826
827 // Test JSON serialization
828 let json = serde_json::to_string(&field).unwrap();
829 let deserialized: Field21 = serde_json::from_str(&json).unwrap();
830
831 assert_eq!(field, deserialized);
832 assert_eq!(field.related_reference(), deserialized.related_reference());
833 }
834
835 #[test]
836 fn test_field21_comprehensive_validation() {
837 // Test various validation scenarios
838 let valid_cases = vec![
839 "COV12345",
840 "AMD0000012345",
841 "CAN20241201001",
842 "REL123456789A",
843 "FT-123_456.78",
844 "ABC/DEF",
845 ];
846
847 for case in valid_cases {
848 let field = Field21::new(case.to_string());
849 let validation = field.validate();
850 assert!(validation.is_valid, "Failed validation for: {}", case);
851 }
852
853 // Test invalid cases
854 let invalid_cases = vec![
855 "THIS_IS_WAY_TOO_LONG_FOR_FIELD21_VALIDATION", // Too long
856 ];
857
858 for case in invalid_cases {
859 let field = Field21::new(case.to_string());
860 let validation = field.validate();
861 assert!(
862 !validation.is_valid,
863 "Should have failed validation for: {}",
864 case
865 );
866 }
867
868 // Test empty case separately (it's valid for Field21 creation but not well-formed)
869 let empty_field = Field21::new("".to_string());
870 let _validation = empty_field.validate();
871 // Empty field might be valid for Field21 but not well-formed
872 assert!(!empty_field.is_well_formed());
873 }
874}