swift_mt_message/fields/
field23e.rs

1use crate::{SwiftField, ValidationError, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// Valid instruction codes for Field 23E
5pub const VALID_INSTRUCTION_CODES: &[&str] = &[
6    "CHQB", "HOLD", "INTC", "PHOB", "PHOI", "PHON", "REPA", "SDVA", "TELB", "TELE", "TELI",
7];
8
9/// # Field 23E: Instruction Code
10///
11/// ## Overview
12/// Field 23E contains instruction codes that specify how the transaction should be processed
13/// by the receiving financial institution. These codes provide additional processing instructions
14/// beyond the basic operation code in Field 23B, enabling more granular control over payment
15/// handling, timing, and communication requirements.
16///
17/// ## Format Specification
18/// **Format**: `4!c[/30x]`
19/// - **4!c**: Exactly 4 alphanumeric characters (instruction code)
20/// - **[/30x]**: Optional additional information (up to 30 characters after slash)
21/// - **Character set**: A-Z, 0-9 for instruction code; printable ASCII for additional info
22/// - **Case handling**: Instruction codes normalized to uppercase
23/// - **Validation**: Must use recognized instruction codes
24///
25/// ## Standard Instruction Codes
26/// The SWIFT-recognized instruction codes and their meanings:
27///
28/// ### Payment Method Instructions
29/// - **CHQB**: Pay by cheque/banker's draft - Physical payment instrument required
30/// - **REPA**: Reimbursement payment - Payment for reimbursement purposes
31///
32/// ### Communication Instructions
33/// - **PHOB**: Phone ordering customer before payment - Contact beneficiary before processing
34/// - **PHOI**: Phone intermediary bank before payment - Contact intermediary institution
35/// - **PHON**: Phone all parties before payment - Contact all relevant parties
36/// - **TELB**: Telex beneficiary before payment - Send telex to beneficiary
37/// - **TELE**: Telex all parties before payment - Send telex to all parties
38/// - **TELI**: Telex intermediary bank before payment - Send telex to intermediary
39///
40/// ### Processing Instructions
41/// - **HOLD**: Hold payment until further notice - Suspend processing pending instructions
42/// - **INTC**: Intracompany payment - Internal company transfer
43/// - **SDVA**: Same day value - Ensure same-day value dating
44///
45/// ## Usage Context
46/// Field 23E is used in conjunction with Field 23B in various MT message types:
47/// - **MT103**: Single Customer Credit Transfer
48/// - **MT202**: General Financial Institution Transfer
49/// - **MT202COV**: Cover for customer credit transfer
50/// - **MT205**: Financial Institution Transfer for its Own Account
51///
52/// ### Business Applications
53/// - **Payment timing**: Control when payments are processed (SDVA, HOLD)
54/// - **Communication protocols**: Specify required notifications (PHOB, TELB, etc.)
55/// - **Payment methods**: Indicate specific payment instruments (CHQB)
56/// - **Risk management**: Enable additional verification steps (PHON, HOLD)
57/// - **Compliance**: Support regulatory and internal control requirements
58/// - **Customer service**: Ensure proper communication with beneficiaries
59///
60/// ## Business Rules and Restrictions
61/// Field 23E usage is restricted based on the operation code in Field 23B:
62///
63/// ### Field 23B = SPRI (Special Priority)
64/// Only the following 23E codes are permitted:
65/// - **SDVA**: Same day value
66/// - **TELB**: Telex beneficiary before payment
67/// - **PHOB**: Phone ordering customer before payment
68/// - **INTC**: Intracompany payment
69///
70/// ### Field 23B = SSTD or SPAY
71/// Field 23E **must not** be present when 23B contains these codes.
72///
73/// ### Other 23B Values
74/// Any valid instruction code may be used with other operation codes.
75///
76/// ## Validation Rules
77/// 1. **Instruction code**: Must be exactly 4 alphanumeric characters
78/// 2. **Valid codes**: Must be from the recognized instruction code list
79/// 3. **Additional info**: Optional, max 30 characters if present
80/// 4. **Character set**: Printable ASCII characters only
81/// 5. **Business rules**: Must comply with Field 23B restrictions
82/// 6. **Format**: Additional info must follow slash separator
83///
84/// ## Network Validated Rules (SWIFT Standards)
85/// - Instruction code must be exactly 4 characters (Error: T26)
86/// - Must be a recognized instruction code (Error: T18)
87/// - Additional information max 30 characters (Error: T13)
88/// - Must comply with Field 23B business rules (Error: T40)
89/// - Character set validation (Error: T61)
90///
91///
92/// ## Examples
93/// ```text
94/// :23E:CHQB
95/// └─── Pay by cheque/banker's draft
96///
97/// :23E:HOLD/COMPLIANCE CHECK
98/// └─── Hold payment with additional information
99///
100/// :23E:PHOB/CALL BEFORE 5PM
101/// └─── Phone beneficiary with specific timing
102///
103/// :23E:SDVA
104/// └─── Same day value dating required
105/// ```
106///
107
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109pub struct Field23E {
110    /// Instruction code (exactly 4 alphanumeric characters)
111    ///
112    /// Specifies the processing instruction for the transaction.
113    /// Must be one of the recognized SWIFT instruction codes.
114    ///
115    /// **Format**: Exactly 4 alphanumeric characters (A-Z, 0-9)
116    /// **Case handling**: Automatically normalized to uppercase
117    /// **Validation**: Must be from the standard instruction code list
118    ///
119    /// # Standard Codes
120    /// - `"CHQB"` - Pay by cheque/banker's draft
121    /// - `"HOLD"` - Hold payment until further notice
122    /// - `"INTC"` - Intracompany payment
123    /// - `"PHOB"` - Phone ordering customer before payment
124    /// - `"PHOI"` - Phone intermediary bank before payment
125    /// - `"PHON"` - Phone all parties before payment
126    /// - `"REPA"` - Reimbursement payment
127    /// - `"SDVA"` - Same day value
128    /// - `"TELB"` - Telex beneficiary before payment
129    /// - `"TELE"` - Telex all parties before payment
130    /// - `"TELI"` - Telex intermediary bank before payment
131    pub instruction_code: String,
132
133    /// Additional information (optional, up to 30 characters)
134    ///
135    /// Provides supplementary details about the instruction code.
136    /// This field is optional and should only be used when additional
137    /// clarification or specific details are required.
138    ///
139    /// **Format**: Up to 30 printable ASCII characters
140    /// **Separator**: Must be preceded by "/" in SWIFT format
141    /// **Validation**: Cannot be empty if specified
142    ///
143    /// # Examples
144    /// - `"COMPLIANCE CHECK"` - For HOLD instructions
145    /// - `"CALL BEFORE 5PM"` - For phone instructions
146    /// - `"WEEKLY PAYMENT"` - For REPA instructions
147    /// - `"URGENT PROCESSING"` - For time-sensitive instructions
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub additional_info: Option<String>,
150}
151
152impl SwiftField for Field23E {
153    fn parse(value: &str) -> Result<Self, crate::ParseError> {
154        let content = if let Some(stripped) = value.strip_prefix(":23E:") {
155            stripped // Remove ":23E:" prefix
156        } else if let Some(stripped) = value.strip_prefix("23E:") {
157            stripped // Remove "23E:" prefix
158        } else {
159            value
160        };
161
162        if content.is_empty() {
163            return Err(crate::ParseError::InvalidFieldFormat {
164                field_tag: "23E".to_string(),
165                message: "Field content cannot be empty after removing tag".to_string(),
166            });
167        }
168
169        // Check if there's additional info (indicated by /)
170        if let Some(slash_pos) = content.find('/') {
171            let instruction_code = &content[..slash_pos];
172            let additional_info = &content[slash_pos + 1..];
173
174            Self::new(instruction_code, Some(additional_info.to_string()))
175        } else {
176            Self::new(content, None)
177        }
178    }
179
180    fn to_swift_string(&self) -> String {
181        match &self.additional_info {
182            Some(info) => format!(":23E:{}/{}", self.instruction_code, info),
183            None => format!(":23E:{}", self.instruction_code),
184        }
185    }
186
187    fn validate(&self) -> ValidationResult {
188        let mut errors = Vec::new();
189
190        // Validate instruction code length
191        if self.instruction_code.len() != 4 {
192            errors.push(ValidationError::LengthValidation {
193                field_tag: "23E".to_string(),
194                expected: "4 characters".to_string(),
195                actual: self.instruction_code.len(),
196            });
197        }
198
199        // Validate instruction code characters (alphanumeric)
200        if !self
201            .instruction_code
202            .chars()
203            .all(|c| c.is_alphanumeric() && c.is_ascii())
204        {
205            errors.push(ValidationError::FormatValidation {
206                field_tag: "23E".to_string(),
207                message: "Instruction code must contain only alphanumeric characters".to_string(),
208            });
209        }
210
211        // Validate against known instruction codes
212        if !VALID_INSTRUCTION_CODES.contains(&self.instruction_code.as_str()) {
213            errors.push(ValidationError::ValueValidation {
214                field_tag: "23E".to_string(),
215                message: format!("Invalid instruction code: {}", self.instruction_code),
216            });
217        }
218
219        // Validate additional info if present
220        if let Some(ref info) = self.additional_info {
221            if info.len() > 30 {
222                errors.push(ValidationError::LengthValidation {
223                    field_tag: "23E".to_string(),
224                    expected: "max 30 characters".to_string(),
225                    actual: info.len(),
226                });
227            }
228
229            if info.is_empty() {
230                errors.push(ValidationError::ValueValidation {
231                    field_tag: "23E".to_string(),
232                    message: "Additional information cannot be empty if specified".to_string(),
233                });
234            }
235
236            // Validate characters (printable ASCII)
237            if !info.chars().all(|c| c.is_ascii() && !c.is_control()) {
238                errors.push(ValidationError::FormatValidation {
239                    field_tag: "23E".to_string(),
240                    message: "Additional information contains invalid characters".to_string(),
241                });
242            }
243        }
244
245        ValidationResult {
246            is_valid: errors.is_empty(),
247            errors,
248            warnings: Vec::new(),
249        }
250    }
251
252    fn format_spec() -> &'static str {
253        "4!c[/30x]"
254    }
255}
256
257impl Field23E {
258    /// Create a new Field23E with validation
259    pub fn new(
260        instruction_code: impl Into<String>,
261        additional_info: Option<String>,
262    ) -> crate::Result<Self> {
263        let code = instruction_code.into().trim().to_uppercase();
264
265        if code.is_empty() {
266            return Err(crate::ParseError::InvalidFieldFormat {
267                field_tag: "23E".to_string(),
268                message: "Instruction code cannot be empty".to_string(),
269            });
270        }
271
272        if code.len() != 4 {
273            return Err(crate::ParseError::InvalidFieldFormat {
274                field_tag: "23E".to_string(),
275                message: "Instruction code must be exactly 4 characters".to_string(),
276            });
277        }
278
279        // Validate characters (alphanumeric)
280        if !code.chars().all(|c| c.is_alphanumeric() && c.is_ascii()) {
281            return Err(crate::ParseError::InvalidFieldFormat {
282                field_tag: "23E".to_string(),
283                message: "Instruction code must contain only alphanumeric characters".to_string(),
284            });
285        }
286
287        // Validate against known instruction codes
288        if !VALID_INSTRUCTION_CODES.contains(&code.as_str()) {
289            return Err(crate::ParseError::InvalidFieldFormat {
290                field_tag: "23E".to_string(),
291                message: format!("Invalid instruction code: {}", code),
292            });
293        }
294
295        // Validate additional info if present
296        if let Some(ref info) = additional_info {
297            if info.len() > 30 {
298                return Err(crate::ParseError::InvalidFieldFormat {
299                    field_tag: "23E".to_string(),
300                    message: "Additional information too long (max 30 characters)".to_string(),
301                });
302            }
303
304            if info.is_empty() {
305                return Err(crate::ParseError::InvalidFieldFormat {
306                    field_tag: "23E".to_string(),
307                    message: "Additional information cannot be empty if specified".to_string(),
308                });
309            }
310
311            // Validate characters (printable ASCII)
312            if !info.chars().all(|c| c.is_ascii() && !c.is_control()) {
313                return Err(crate::ParseError::InvalidFieldFormat {
314                    field_tag: "23E".to_string(),
315                    message: "Additional information contains invalid characters".to_string(),
316                });
317            }
318        }
319
320        Ok(Field23E {
321            instruction_code: code,
322            additional_info,
323        })
324    }
325
326    /// Get the instruction code
327    ///
328    /// Returns the 4-character instruction code that specifies
329    /// how the transaction should be processed.
330    ///
331    /// # Returns
332    /// A string slice containing the instruction code in uppercase
333    ///
334    /// # Example
335    /// ```rust
336    /// # use swift_mt_message::fields::Field23E;
337    /// let field = Field23E::new("CHQB", None).unwrap();
338    /// assert_eq!(field.code(), "CHQB");
339    /// ```
340    pub fn code(&self) -> &str {
341        &self.instruction_code
342    }
343
344    /// Get the additional information
345    ///
346    /// Returns the optional additional information that provides
347    /// supplementary details about the instruction.
348    ///
349    /// # Returns
350    /// An optional string slice containing the additional information
351    ///
352    /// # Example
353    /// ```rust
354    /// # use swift_mt_message::fields::Field23E;
355    /// let field = Field23E::new("HOLD", Some("COMPLIANCE CHECK".to_string())).unwrap();
356    /// assert_eq!(field.additional_info(), Some("COMPLIANCE CHECK"));
357    /// ```
358    pub fn additional_info(&self) -> Option<&str> {
359        self.additional_info.as_deref()
360    }
361
362    /// Check if this is a valid instruction code
363    ///
364    /// Determines if the instruction code is recognized by SWIFT standards.
365    ///
366    /// # Returns
367    /// `true` if the instruction code is valid
368    ///
369    /// # Example
370    /// ```rust
371    /// # use swift_mt_message::fields::Field23E;
372    /// let field = Field23E::new("CHQB", None).unwrap();
373    /// assert!(field.is_valid_code());
374    /// ```
375    pub fn is_valid_code(&self) -> bool {
376        VALID_INSTRUCTION_CODES.contains(&self.instruction_code.as_str())
377    }
378
379    /// Check if this is a communication instruction
380    ///
381    /// Determines if the instruction code requires communication
382    /// with parties before processing the payment.
383    ///
384    /// # Returns
385    /// `true` if the instruction requires communication
386    ///
387    /// # Example
388    /// ```rust
389    /// # use swift_mt_message::fields::Field23E;
390    /// let phone_instruction = Field23E::new("PHOB", None).unwrap();
391    /// assert!(phone_instruction.is_communication_instruction());
392    ///
393    /// let payment_instruction = Field23E::new("CHQB", None).unwrap();
394    /// assert!(!payment_instruction.is_communication_instruction());
395    /// ```
396    pub fn is_communication_instruction(&self) -> bool {
397        matches!(
398            self.instruction_code.as_str(),
399            "PHOB" | "PHOI" | "PHON" | "TELB" | "TELE" | "TELI"
400        )
401    }
402
403    /// Check if this is a timing instruction
404    ///
405    /// Determines if the instruction code affects the timing
406    /// or scheduling of the payment processing.
407    ///
408    /// # Returns
409    /// `true` if the instruction affects timing
410    ///
411    /// # Example
412    /// ```rust
413    /// # use swift_mt_message::fields::Field23E;
414    /// let timing_instruction = Field23E::new("SDVA", None).unwrap();
415    /// assert!(timing_instruction.is_timing_instruction());
416    ///
417    /// let hold_instruction = Field23E::new("HOLD", None).unwrap();
418    /// assert!(hold_instruction.is_timing_instruction());
419    /// ```
420    pub fn is_timing_instruction(&self) -> bool {
421        matches!(self.instruction_code.as_str(), "SDVA" | "HOLD")
422    }
423
424    /// Check if this is a payment method instruction
425    ///
426    /// Determines if the instruction code specifies a particular
427    /// payment method or instrument.
428    ///
429    /// # Returns
430    /// `true` if the instruction specifies a payment method
431    ///
432    /// # Example
433    /// ```rust
434    /// # use swift_mt_message::fields::Field23E;
435    /// let method_instruction = Field23E::new("CHQB", None).unwrap();
436    /// assert!(method_instruction.is_payment_method_instruction());
437    ///
438    /// let reimbursement = Field23E::new("REPA", None).unwrap();
439    /// assert!(reimbursement.is_payment_method_instruction());
440    /// ```
441    pub fn is_payment_method_instruction(&self) -> bool {
442        matches!(self.instruction_code.as_str(), "CHQB" | "REPA")
443    }
444
445    /// Check if this instruction requires manual intervention
446    ///
447    /// Determines if the instruction code typically requires
448    /// manual processing or intervention by bank staff.
449    ///
450    /// # Returns
451    /// `true` if manual intervention is likely required
452    ///
453    /// # Example
454    /// ```rust
455    /// # use swift_mt_message::fields::Field23E;
456    /// let manual_instruction = Field23E::new("HOLD", None).unwrap();
457    /// assert!(manual_instruction.requires_manual_intervention());
458    ///
459    /// let auto_instruction = Field23E::new("SDVA", None).unwrap();
460    /// assert!(!auto_instruction.requires_manual_intervention());
461    /// ```
462    pub fn requires_manual_intervention(&self) -> bool {
463        matches!(
464            self.instruction_code.as_str(),
465            "HOLD" | "PHOB" | "PHOI" | "PHON" | "TELB" | "TELE" | "TELI" | "CHQB"
466        )
467    }
468
469    /// Get the processing priority impact
470    ///
471    /// Returns how this instruction affects processing priority.
472    /// Positive values increase priority, negative values decrease it.
473    ///
474    /// # Returns
475    /// Priority adjustment (-2 to +2)
476    ///
477    /// # Example
478    /// ```rust
479    /// # use swift_mt_message::fields::Field23E;
480    /// let urgent = Field23E::new("SDVA", None).unwrap();
481    /// assert_eq!(urgent.priority_impact(), 2);
482    ///
483    /// let hold = Field23E::new("HOLD", None).unwrap();
484    /// assert_eq!(hold.priority_impact(), -2);
485    /// ```
486    pub fn priority_impact(&self) -> i8 {
487        match self.instruction_code.as_str() {
488            "SDVA" => 2,                             // Same day value increases priority
489            "PHON" | "TELE" => 1, // Communication to all parties increases priority
490            "INTC" => 0,          // Intracompany has neutral impact
491            "PHOB" | "PHOI" | "TELB" | "TELI" => -1, // Specific communication decreases priority
492            "CHQB" | "REPA" => -1, // Physical instruments decrease priority
493            "HOLD" => -2,         // Hold significantly decreases priority
494            _ => 0,               // Unknown codes have neutral impact
495        }
496    }
497
498    /// Get human-readable description of the instruction code
499    ///
500    /// Returns a descriptive string explaining what this instruction code
501    /// represents and its typical usage in payment processing.
502    ///
503    /// # Returns
504    /// A descriptive string
505    ///
506    /// # Example
507    /// ```rust
508    /// # use swift_mt_message::fields::Field23E;
509    /// let field = Field23E::new("CHQB", None).unwrap();
510    /// println!("{}", field.description());
511    /// ```
512    pub fn description(&self) -> &'static str {
513        match self.instruction_code.as_str() {
514            "CHQB" => "Pay by cheque/banker's draft - Physical payment instrument required",
515            "HOLD" => "Hold payment until further notice - Suspend processing pending instructions",
516            "INTC" => "Intracompany payment - Internal company transfer between related entities",
517            "PHOB" => {
518                "Phone ordering customer before payment - Contact beneficiary before processing"
519            }
520            "PHOI" => "Phone intermediary bank before payment - Contact intermediary institution",
521            "PHON" => {
522                "Phone all parties before payment - Contact all relevant parties for verification"
523            }
524            "REPA" => "Reimbursement payment - Payment for reimbursement or expense purposes",
525            "SDVA" => "Same day value - Ensure same-day value dating for the payment",
526            "TELB" => "Telex beneficiary before payment - Send telex notification to beneficiary",
527            "TELE" => "Telex all parties before payment - Send telex notifications to all parties",
528            "TELI" => {
529                "Telex intermediary bank before payment - Send telex to intermediary institution"
530            }
531            _ => "Unknown instruction code - Non-standard or institution-specific instruction",
532        }
533    }
534
535    /// Get the instruction category
536    ///
537    /// Returns the category that this instruction code belongs to,
538    /// helping to group related instructions.
539    ///
540    /// # Returns
541    /// Instruction category as a string
542    ///
543    /// # Example
544    /// ```rust
545    /// # use swift_mt_message::fields::Field23E;
546    /// let field = Field23E::new("PHOB", None).unwrap();
547    /// assert_eq!(field.instruction_category(), "Communication");
548    /// ```
549    pub fn instruction_category(&self) -> &'static str {
550        match self.instruction_code.as_str() {
551            "PHOB" | "PHOI" | "PHON" | "TELB" | "TELE" | "TELI" => "Communication",
552            "SDVA" | "HOLD" => "Timing",
553            "CHQB" | "REPA" => "Payment Method",
554            "INTC" => "Internal Transfer",
555            _ => "Other",
556        }
557    }
558
559    /// Check if additional information is recommended
560    ///
561    /// Determines if this instruction code typically benefits from
562    /// additional information to clarify processing requirements.
563    ///
564    /// # Returns
565    /// `true` if additional information is recommended
566    ///
567    /// # Example
568    /// ```rust
569    /// # use swift_mt_message::fields::Field23E;
570    /// let hold = Field23E::new("HOLD", None).unwrap();
571    /// assert!(hold.recommends_additional_info());
572    ///
573    /// let same_day = Field23E::new("SDVA", None).unwrap();
574    /// assert!(!same_day.recommends_additional_info());
575    /// ```
576    pub fn recommends_additional_info(&self) -> bool {
577        matches!(
578            self.instruction_code.as_str(),
579            "HOLD" | "PHOB" | "PHOI" | "PHON" | "TELB" | "TELE" | "TELI" | "REPA"
580        )
581    }
582
583    /// Validate against Field 23B business rules
584    pub fn validate_with_field_23b(&self, field_23b_code: &str) -> crate::Result<()> {
585        match field_23b_code {
586            "SPRI" => {
587                // If 23B = SPRI, 23E can only contain SDVA, TELB, PHOB, INTC
588                if !["SDVA", "TELB", "PHOB", "INTC"].contains(&self.instruction_code.as_str()) {
589                    return Err(crate::ParseError::InvalidFieldFormat {
590                        field_tag: "23E".to_string(),
591                        message: format!(
592                            "When Field 23B is SPRI, Field 23E can only be SDVA, TELB, PHOB, or INTC. Got: {}",
593                            self.instruction_code
594                        ),
595                    });
596                }
597            }
598            "SSTD" | "SPAY" => {
599                // If 23B = SSTD/SPAY, 23E must not be used
600                return Err(crate::ParseError::InvalidFieldFormat {
601                    field_tag: "23E".to_string(),
602                    message: "Field 23E must not be present when Field 23B is SSTD or SPAY"
603                        .to_string(),
604                });
605            }
606            _ => {
607                // For other 23B values, 23E can contain any valid instruction code
608            }
609        }
610
611        Ok(())
612    }
613
614    /// Get comprehensive instruction details
615    ///
616    /// Returns a detailed description including the instruction code,
617    /// category, description, and additional information if present.
618    ///
619    /// # Returns
620    /// Formatted string with comprehensive details
621    ///
622    /// # Example
623    /// ```rust
624    /// # use swift_mt_message::fields::Field23E;
625    /// let field = Field23E::new("HOLD", Some("COMPLIANCE CHECK".to_string())).unwrap();
626    /// println!("{}", field.comprehensive_description());
627    /// ```
628    pub fn comprehensive_description(&self) -> String {
629        let base = format!(
630            "{} ({}): {}",
631            self.instruction_code,
632            self.instruction_category(),
633            self.description()
634        );
635
636        if let Some(ref info) = self.additional_info {
637            format!("{} - Additional Info: {}", base, info)
638        } else {
639            base
640        }
641    }
642}
643
644impl std::fmt::Display for Field23E {
645    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
646        match &self.additional_info {
647            Some(info) => write!(f, "{}/{}", self.instruction_code, info),
648            None => write!(f, "{}", self.instruction_code),
649        }
650    }
651}
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656
657    #[test]
658    fn test_field23e_creation_simple() {
659        let field = Field23E::new("CHQB", None).unwrap();
660        assert_eq!(field.instruction_code, "CHQB");
661        assert_eq!(field.additional_info, None);
662        assert_eq!(field.code(), "CHQB");
663    }
664
665    #[test]
666    fn test_field23e_creation_with_info() {
667        let field = Field23E::new("HOLD", Some("COMPLIANCE CHECK".to_string())).unwrap();
668        assert_eq!(field.instruction_code, "HOLD");
669        assert_eq!(field.additional_info, Some("COMPLIANCE CHECK".to_string()));
670        assert_eq!(field.additional_info(), Some("COMPLIANCE CHECK"));
671    }
672
673    #[test]
674    fn test_field23e_parse_simple() {
675        let field = Field23E::parse("INTC").unwrap();
676        assert_eq!(field.instruction_code, "INTC");
677        assert_eq!(field.additional_info, None);
678    }
679
680    #[test]
681    fn test_field23e_parse_with_info() {
682        let field = Field23E::parse("REPA/WEEKLY PAYMENT").unwrap();
683        assert_eq!(field.instruction_code, "REPA");
684        assert_eq!(field.additional_info, Some("WEEKLY PAYMENT".to_string()));
685    }
686
687    #[test]
688    fn test_field23e_parse_with_tag_prefix() {
689        let field = Field23E::parse(":23E:HOLD/INFO").unwrap();
690        assert_eq!(field.instruction_code, "HOLD");
691        assert_eq!(field.additional_info, Some("INFO".to_string()));
692
693        let field = Field23E::parse("23E:SDVA").unwrap();
694        assert_eq!(field.instruction_code, "SDVA");
695        assert_eq!(field.additional_info, None);
696    }
697
698    #[test]
699    fn test_field23e_case_normalization() {
700        let field = Field23E::new("phob", None).unwrap();
701        assert_eq!(field.instruction_code, "PHOB");
702    }
703
704    #[test]
705    fn test_field23e_invalid_code() {
706        let result = Field23E::new("INVL", None); // Invalid code
707        assert!(result.is_err());
708
709        let result = Field23E::new("ABC", None); // Too short
710        assert!(result.is_err());
711
712        let result = Field23E::new("ABCDE", None); // Too long
713        assert!(result.is_err());
714    }
715
716    #[test]
717    fn test_field23e_invalid_additional_info() {
718        let result = Field23E::new("HOLD", Some("A".repeat(31))); // Too long
719        assert!(result.is_err());
720
721        let result = Field23E::new("HOLD", Some("".to_string())); // Empty
722        assert!(result.is_err());
723    }
724
725    #[test]
726    fn test_field23e_business_rules() {
727        // SPRI with valid codes
728        let field = Field23E::new("SDVA", None).unwrap();
729        assert!(field.validate_with_field_23b("SPRI").is_ok());
730
731        let field = Field23E::new("TELB", None).unwrap();
732        assert!(field.validate_with_field_23b("SPRI").is_ok());
733
734        // SPRI with invalid code
735        let field = Field23E::new("CHQB", None).unwrap();
736        assert!(field.validate_with_field_23b("SPRI").is_err());
737
738        // SSTD/SPAY should not allow 23E
739        let field = Field23E::new("HOLD", None).unwrap();
740        assert!(field.validate_with_field_23b("SSTD").is_err());
741        assert!(field.validate_with_field_23b("SPAY").is_err());
742
743        // Other 23B values allow any valid code
744        let field = Field23E::new("CHQB", None).unwrap();
745        assert!(field.validate_with_field_23b("CRED").is_ok());
746    }
747
748    #[test]
749    fn test_field23e_to_swift_string() {
750        let field = Field23E::new("TELI", None).unwrap();
751        assert_eq!(field.to_swift_string(), ":23E:TELI");
752
753        let field = Field23E::new("PHON", Some("CALL BEFORE".to_string())).unwrap();
754        assert_eq!(field.to_swift_string(), ":23E:PHON/CALL BEFORE");
755    }
756
757    #[test]
758    fn test_field23e_validation() {
759        let field = Field23E::new("TELE", None).unwrap();
760        let result = field.validate();
761        assert!(result.is_valid);
762
763        let invalid_field = Field23E {
764            instruction_code: "INVALID".to_string(),
765            additional_info: None,
766        };
767        let result = invalid_field.validate();
768        assert!(!result.is_valid);
769    }
770
771    #[test]
772    fn test_field23e_format_spec() {
773        assert_eq!(Field23E::format_spec(), "4!c[/30x]");
774    }
775
776    #[test]
777    fn test_field23e_display() {
778        let field = Field23E::new("PHOI", None).unwrap();
779        assert_eq!(format!("{}", field), "PHOI");
780
781        let field = Field23E::new("SDVA", Some("SAME DAY".to_string())).unwrap();
782        assert_eq!(format!("{}", field), "SDVA/SAME DAY");
783    }
784
785    #[test]
786    fn test_field23e_descriptions() {
787        let field = Field23E::new("CHQB", None).unwrap();
788        assert_eq!(
789            field.description(),
790            "Pay by cheque/banker's draft - Physical payment instrument required"
791        );
792
793        let field = Field23E::new("HOLD", None).unwrap();
794        assert_eq!(
795            field.description(),
796            "Hold payment until further notice - Suspend processing pending instructions"
797        );
798
799        let field = Field23E::new("SDVA", None).unwrap();
800        assert_eq!(
801            field.description(),
802            "Same day value - Ensure same-day value dating for the payment"
803        );
804    }
805
806    #[test]
807    fn test_field23e_is_valid_code() {
808        let field = Field23E::new("CHQB", None).unwrap();
809        assert!(field.is_valid_code());
810
811        let field = Field23E {
812            instruction_code: "XXXX".to_string(),
813            additional_info: None,
814        };
815        assert!(!field.is_valid_code());
816    }
817
818    #[test]
819    fn test_field23e_communication_instructions() {
820        let communication_codes = ["PHOB", "PHOI", "PHON", "TELB", "TELE", "TELI"];
821        for code in communication_codes {
822            let field = Field23E::new(code, None).unwrap();
823            assert!(
824                field.is_communication_instruction(),
825                "Code {} should be communication instruction",
826                code
827            );
828            assert!(
829                !field.is_timing_instruction(),
830                "Code {} should not be timing instruction",
831                code
832            );
833            assert!(
834                !field.is_payment_method_instruction(),
835                "Code {} should not be payment method instruction",
836                code
837            );
838        }
839
840        let non_communication_codes = ["CHQB", "HOLD", "INTC", "REPA", "SDVA"];
841        for code in non_communication_codes {
842            let field = Field23E::new(code, None).unwrap();
843            assert!(
844                !field.is_communication_instruction(),
845                "Code {} should not be communication instruction",
846                code
847            );
848        }
849    }
850
851    #[test]
852    fn test_field23e_timing_instructions() {
853        let timing_codes = ["SDVA", "HOLD"];
854        for code in timing_codes {
855            let field = Field23E::new(code, None).unwrap();
856            assert!(
857                field.is_timing_instruction(),
858                "Code {} should be timing instruction",
859                code
860            );
861            assert!(
862                !field.is_communication_instruction(),
863                "Code {} should not be communication instruction",
864                code
865            );
866        }
867
868        let non_timing_codes = ["CHQB", "PHOB", "INTC", "REPA"];
869        for code in non_timing_codes {
870            let field = Field23E::new(code, None).unwrap();
871            assert!(
872                !field.is_timing_instruction(),
873                "Code {} should not be timing instruction",
874                code
875            );
876        }
877    }
878
879    #[test]
880    fn test_field23e_payment_method_instructions() {
881        let payment_method_codes = ["CHQB", "REPA"];
882        for code in payment_method_codes {
883            let field = Field23E::new(code, None).unwrap();
884            assert!(
885                field.is_payment_method_instruction(),
886                "Code {} should be payment method instruction",
887                code
888            );
889            assert!(
890                !field.is_communication_instruction(),
891                "Code {} should not be communication instruction",
892                code
893            );
894            assert!(
895                !field.is_timing_instruction(),
896                "Code {} should not be timing instruction",
897                code
898            );
899        }
900
901        let non_payment_method_codes = ["HOLD", "PHOB", "INTC", "SDVA"];
902        for code in non_payment_method_codes {
903            let field = Field23E::new(code, None).unwrap();
904            assert!(
905                !field.is_payment_method_instruction(),
906                "Code {} should not be payment method instruction",
907                code
908            );
909        }
910    }
911
912    #[test]
913    fn test_field23e_manual_intervention_requirements() {
914        let manual_codes = [
915            "HOLD", "PHOB", "PHOI", "PHON", "TELB", "TELE", "TELI", "CHQB",
916        ];
917        for code in manual_codes {
918            let field = Field23E::new(code, None).unwrap();
919            assert!(
920                field.requires_manual_intervention(),
921                "Code {} should require manual intervention",
922                code
923            );
924        }
925
926        let automatic_codes = ["SDVA", "INTC", "REPA"];
927        for code in automatic_codes {
928            let field = Field23E::new(code, None).unwrap();
929            assert!(
930                !field.requires_manual_intervention(),
931                "Code {} should not require manual intervention",
932                code
933            );
934        }
935    }
936
937    #[test]
938    fn test_field23e_priority_impact() {
939        let test_cases = [
940            ("SDVA", 2),  // Same day value increases priority
941            ("PHON", 1),  // Communication to all parties increases priority
942            ("TELE", 1),  // Communication to all parties increases priority
943            ("INTC", 0),  // Intracompany has neutral impact
944            ("PHOB", -1), // Specific communication decreases priority
945            ("PHOI", -1), // Specific communication decreases priority
946            ("TELB", -1), // Specific communication decreases priority
947            ("TELI", -1), // Specific communication decreases priority
948            ("CHQB", -1), // Physical instruments decrease priority
949            ("REPA", -1), // Physical instruments decrease priority
950            ("HOLD", -2), // Hold significantly decreases priority
951        ];
952
953        for (code, expected_impact) in test_cases {
954            let field = Field23E::new(code, None).unwrap();
955            assert_eq!(
956                field.priority_impact(),
957                expected_impact,
958                "Priority impact mismatch for code {}",
959                code
960            );
961        }
962    }
963
964    #[test]
965    fn test_field23e_instruction_categories() {
966        let test_cases = [
967            ("PHOB", "Communication"),
968            ("PHOI", "Communication"),
969            ("PHON", "Communication"),
970            ("TELB", "Communication"),
971            ("TELE", "Communication"),
972            ("TELI", "Communication"),
973            ("SDVA", "Timing"),
974            ("HOLD", "Timing"),
975            ("CHQB", "Payment Method"),
976            ("REPA", "Payment Method"),
977            ("INTC", "Internal Transfer"),
978        ];
979
980        for (code, expected_category) in test_cases {
981            let field = Field23E::new(code, None).unwrap();
982            assert_eq!(
983                field.instruction_category(),
984                expected_category,
985                "Category mismatch for code {}",
986                code
987            );
988        }
989    }
990
991    #[test]
992    fn test_field23e_additional_info_recommendations() {
993        let recommends_info = [
994            "HOLD", "PHOB", "PHOI", "PHON", "TELB", "TELE", "TELI", "REPA",
995        ];
996        for code in recommends_info {
997            let field = Field23E::new(code, None).unwrap();
998            assert!(
999                field.recommends_additional_info(),
1000                "Code {} should recommend additional info",
1001                code
1002            );
1003        }
1004
1005        let no_info_needed = ["SDVA", "INTC", "CHQB"];
1006        for code in no_info_needed {
1007            let field = Field23E::new(code, None).unwrap();
1008            assert!(
1009                !field.recommends_additional_info(),
1010                "Code {} should not recommend additional info",
1011                code
1012            );
1013        }
1014    }
1015
1016    #[test]
1017    fn test_field23e_comprehensive_description() {
1018        // Test without additional info
1019        let field = Field23E::new("CHQB", None).unwrap();
1020        let desc = field.comprehensive_description();
1021        assert!(desc.contains("CHQB"));
1022        assert!(desc.contains("Payment Method"));
1023        assert!(desc.contains("Physical payment instrument required"));
1024
1025        // Test with additional info
1026        let field = Field23E::new("HOLD", Some("COMPLIANCE CHECK".to_string())).unwrap();
1027        let desc = field.comprehensive_description();
1028        assert!(desc.contains("HOLD"));
1029        assert!(desc.contains("Timing"));
1030        assert!(desc.contains("Suspend processing pending instructions"));
1031        assert!(desc.contains("Additional Info: COMPLIANCE CHECK"));
1032    }
1033
1034    #[test]
1035    fn test_field23e_enhanced_descriptions() {
1036        let test_cases = [
1037            (
1038                "CHQB",
1039                "Pay by cheque/banker's draft - Physical payment instrument required",
1040            ),
1041            (
1042                "HOLD",
1043                "Hold payment until further notice - Suspend processing pending instructions",
1044            ),
1045            (
1046                "INTC",
1047                "Intracompany payment - Internal company transfer between related entities",
1048            ),
1049            (
1050                "PHOB",
1051                "Phone ordering customer before payment - Contact beneficiary before processing",
1052            ),
1053            (
1054                "PHOI",
1055                "Phone intermediary bank before payment - Contact intermediary institution",
1056            ),
1057            (
1058                "PHON",
1059                "Phone all parties before payment - Contact all relevant parties for verification",
1060            ),
1061            (
1062                "REPA",
1063                "Reimbursement payment - Payment for reimbursement or expense purposes",
1064            ),
1065            (
1066                "SDVA",
1067                "Same day value - Ensure same-day value dating for the payment",
1068            ),
1069            (
1070                "TELB",
1071                "Telex beneficiary before payment - Send telex notification to beneficiary",
1072            ),
1073            (
1074                "TELE",
1075                "Telex all parties before payment - Send telex notifications to all parties",
1076            ),
1077            (
1078                "TELI",
1079                "Telex intermediary bank before payment - Send telex to intermediary institution",
1080            ),
1081        ];
1082
1083        for (code, expected_desc) in test_cases {
1084            let field = Field23E::new(code, None).unwrap();
1085            assert_eq!(
1086                field.description(),
1087                expected_desc,
1088                "Description mismatch for code {}",
1089                code
1090            );
1091        }
1092    }
1093
1094    #[test]
1095    fn test_field23e_business_logic_combinations() {
1096        // Test SDVA: timing instruction, high priority
1097        let sdva = Field23E::new("SDVA", None).unwrap();
1098        assert!(sdva.is_timing_instruction());
1099        assert_eq!(sdva.priority_impact(), 2);
1100        assert!(!sdva.requires_manual_intervention());
1101        assert!(!sdva.recommends_additional_info());
1102        assert_eq!(sdva.instruction_category(), "Timing");
1103
1104        // Test HOLD: timing instruction, low priority, manual intervention
1105        let hold = Field23E::new("HOLD", None).unwrap();
1106        assert!(hold.is_timing_instruction());
1107        assert_eq!(hold.priority_impact(), -2);
1108        assert!(hold.requires_manual_intervention());
1109        assert!(hold.recommends_additional_info());
1110        assert_eq!(hold.instruction_category(), "Timing");
1111
1112        // Test PHON: communication instruction, slight priority increase
1113        let phon = Field23E::new("PHON", None).unwrap();
1114        assert!(phon.is_communication_instruction());
1115        assert_eq!(phon.priority_impact(), 1);
1116        assert!(phon.requires_manual_intervention());
1117        assert!(phon.recommends_additional_info());
1118        assert_eq!(phon.instruction_category(), "Communication");
1119
1120        // Test CHQB: payment method, low priority, manual intervention
1121        let chqb = Field23E::new("CHQB", None).unwrap();
1122        assert!(chqb.is_payment_method_instruction());
1123        assert_eq!(chqb.priority_impact(), -1);
1124        assert!(chqb.requires_manual_intervention());
1125        assert!(!chqb.recommends_additional_info());
1126        assert_eq!(chqb.instruction_category(), "Payment Method");
1127    }
1128
1129    #[test]
1130    fn test_field23e_serialization_with_enhanced_fields() {
1131        let field = Field23E::new("HOLD", Some("COMPLIANCE CHECK".to_string())).unwrap();
1132
1133        // Test JSON serialization
1134        let json = serde_json::to_string(&field).unwrap();
1135        let deserialized: Field23E = serde_json::from_str(&json).unwrap();
1136
1137        assert_eq!(field, deserialized);
1138        assert_eq!(field.code(), deserialized.code());
1139        assert_eq!(field.additional_info(), deserialized.additional_info());
1140        assert_eq!(
1141            field.instruction_category(),
1142            deserialized.instruction_category()
1143        );
1144        assert_eq!(field.priority_impact(), deserialized.priority_impact());
1145    }
1146
1147    #[test]
1148    fn test_field23e_real_world_scenarios() {
1149        // Scenario 1: High-value payment requiring verification
1150        let high_value = Field23E::new("PHON", Some("CALL ALL PARTIES".to_string())).unwrap();
1151        assert!(high_value.is_communication_instruction());
1152        assert!(high_value.requires_manual_intervention());
1153        assert_eq!(high_value.priority_impact(), 1);
1154
1155        // Scenario 2: Same-day urgent payment
1156        let urgent = Field23E::new("SDVA", None).unwrap();
1157        assert!(urgent.is_timing_instruction());
1158        assert!(!urgent.requires_manual_intervention());
1159        assert_eq!(urgent.priority_impact(), 2);
1160
1161        // Scenario 3: Compliance hold with details
1162        let compliance = Field23E::new("HOLD", Some("AML REVIEW REQUIRED".to_string())).unwrap();
1163        assert!(compliance.is_timing_instruction());
1164        assert!(compliance.requires_manual_intervention());
1165        assert_eq!(compliance.priority_impact(), -2);
1166        assert!(compliance.recommends_additional_info());
1167
1168        // Scenario 4: Internal company transfer
1169        let internal = Field23E::new("INTC", None).unwrap();
1170        assert_eq!(internal.instruction_category(), "Internal Transfer");
1171        assert!(!internal.requires_manual_intervention());
1172        assert_eq!(internal.priority_impact(), 0);
1173    }
1174
1175    #[test]
1176    fn test_field23e_edge_cases_enhanced() {
1177        // Test all instruction categories are covered
1178        let all_codes = [
1179            "CHQB", "HOLD", "INTC", "PHOB", "PHOI", "PHON", "REPA", "SDVA", "TELB", "TELE", "TELI",
1180        ];
1181        for code in all_codes {
1182            let field = Field23E::new(code, None).unwrap();
1183
1184            // Every code should have a category
1185            assert!(!field.instruction_category().is_empty());
1186
1187            // Every code should have a description
1188            assert!(!field.description().is_empty());
1189
1190            // Priority impact should be in valid range
1191            assert!(field.priority_impact() >= -2 && field.priority_impact() <= 2);
1192
1193            // Priority impact should be reasonable
1194            assert!(field.priority_impact() >= -2 && field.priority_impact() <= 2);
1195        }
1196    }
1197
1198    #[test]
1199    fn test_field23e_comprehensive_validation() {
1200        // Test all valid instruction codes
1201        for &code in VALID_INSTRUCTION_CODES {
1202            let field = Field23E::new(code, None).unwrap();
1203            let validation = field.validate();
1204            assert!(validation.is_valid, "Code {} should be valid", code);
1205            assert!(field.is_valid_code());
1206
1207            // Test with additional info
1208            let field_with_info = Field23E::new(code, Some("TEST INFO".to_string())).unwrap();
1209            let validation_with_info = field_with_info.validate();
1210            assert!(
1211                validation_with_info.is_valid,
1212                "Code {} with info should be valid",
1213                code
1214            );
1215        }
1216    }
1217}