swift_mt_message/messages/
mt192.rs

1use crate::errors::SwiftValidationError;
2use crate::fields::*;
3use crate::parser::utils::*;
4use serde::{Deserialize, Serialize};
5
6/// **MT192: Request for Cancellation**
7///
8/// Request to cancel a previously sent payment message before execution.
9///
10/// **Usage:** Payment cancellation requests, transaction reversal
11/// **Category:** Category 1 (Customer Payments & Cheques)
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct MT192 {
14    /// Sender's Reference (Field 20)
15    #[serde(rename = "20")]
16    pub field_20: Field20,
17
18    /// Related Reference (Field 21)
19    #[serde(rename = "21")]
20    pub field_21: Field21NoOption,
21
22    /// MT and Date (Field 11S)
23    #[serde(rename = "11S")]
24    pub field_11s: Field11S,
25
26    /// Narrative (Field 79)
27    #[serde(rename = "79", skip_serializing_if = "Option::is_none")]
28    pub field_79: Option<Field79>,
29}
30
31impl MT192 {
32    /// Parse message from Block 4 content
33    pub fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
34        let mut parser = crate::parser::MessageParser::new(block4, "192");
35
36        // Parse mandatory fields in order: 20, 21, 11S
37        let field_20 = parser.parse_field::<Field20>("20")?;
38        let field_21 = parser.parse_field::<Field21NoOption>("21")?;
39        let field_11s = parser.parse_field::<Field11S>("11S")?;
40
41        // Parse optional field 79
42        let field_79 = parser.parse_optional_field::<Field79>("79")?;
43
44        Ok(MT192 {
45            field_20,
46            field_21,
47            field_11s,
48            field_79,
49        })
50    }
51
52    /// Parse from generic SWIFT input (tries to detect blocks)
53    pub fn parse(input: &str) -> Result<Self, crate::errors::ParseError> {
54        let block4 = extract_block4(input)?;
55        Self::parse_from_block4(&block4)
56    }
57
58    // ========================================================================
59    // NETWORK VALIDATION RULES (SR 2025 MT192)
60    // ========================================================================
61
62    /// Field 79 valid cancellation reason codes for MT192
63    const MT192_VALID_79_CODES: &'static [&'static str] = &[
64        "AGNT", // Incorrect Agent
65        "AM09", // Wrong Amount
66        "COVR", // Cover Cancelled or Returned
67        "CURR", // Incorrect Currency
68        "CUST", // Requested by Customer
69        "CUTA", // Cancel upon Unable to Apply
70        "DUPL", // Duplicate Payment
71        "FRAD", // Fraudulent Origin
72        "TECH", // Technical Problem
73        "UPAY", // Undue Payment
74    ];
75
76    // ========================================================================
77    // HELPER METHODS
78    // ========================================================================
79
80    /// Check if Field 79 is present
81    fn has_field_79(&self) -> bool {
82        self.field_79.is_some()
83    }
84
85    /// Extract cancellation reason code from Field 79 if present
86    /// Returns the 4-character code from the first line if formatted as /CODE/...
87    fn get_field_79_cancellation_code(&self) -> Option<String> {
88        if let Some(ref field_79) = self.field_79 {
89            // Get the first line from the information vector
90            let first_line = field_79.information.first()?;
91            if first_line.starts_with('/') {
92                let parts: Vec<&str> = first_line.split('/').collect();
93                if parts.len() >= 2 && parts[1].len() == 4 {
94                    return Some(parts[1].to_string());
95                }
96            }
97        }
98        None
99    }
100
101    // ========================================================================
102    // VALIDATION RULES (C1)
103    // ========================================================================
104
105    /// C1: Field 79 or Copy of Mandatory Fields Requirement (Error code: C25)
106    /// Field 79 or a copy of at least the mandatory fields of the original message
107    /// or both must be present.
108    ///
109    /// **Implementation Note**: This implementation only validates that Field 79 is present
110    /// since we don't currently support parsing the "copy of mandatory fields" section.
111    /// A full implementation would check for either Field 79 OR copied fields OR both.
112    fn validate_c1_field_79_or_copy(&self) -> Option<SwiftValidationError> {
113        // Check if Field 79 is present
114        // Note: In a complete implementation, we would also check for copied fields
115        // from the original message, but this is not currently supported
116        if !self.has_field_79() {
117            return Some(SwiftValidationError::content_error(
118                "C25",
119                "79",
120                "",
121                "Field 79 (Narrative Description) or a copy of at least the mandatory fields of the original message must be present",
122                "Either field 79 or a copy of at least the mandatory fields of the original message or both must be present to identify the transaction to be cancelled",
123            ));
124        }
125
126        None
127    }
128
129    /// Validate Field 79 cancellation reason codes (if present)
130    /// While not explicitly a network validation rule, this validates the cancellation
131    /// reason codes are from the allowed list when present in Field 79.
132    fn validate_field_79_codes(&self) -> Vec<SwiftValidationError> {
133        let mut errors = Vec::new();
134
135        if let Some(code) = self.get_field_79_cancellation_code()
136            && !Self::MT192_VALID_79_CODES.contains(&code.as_str())
137        {
138            errors.push(SwiftValidationError::content_error(
139                    "T47",
140                    "79",
141                    &code,
142                    &format!(
143                        "Cancellation reason code '{}' is not valid for MT192. Valid codes: {}",
144                        code,
145                        Self::MT192_VALID_79_CODES.join(", ")
146                    ),
147                    "Cancellation reason must be one of the allowed codes when using /CODE/ format in field 79",
148                ));
149        }
150
151        errors
152    }
153
154    /// Main validation method - validates all network rules
155    /// Returns array of validation errors, respects stop_on_first_error flag
156    pub fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
157        let mut all_errors = Vec::new();
158
159        // C1: Field 79 or Copy of Mandatory Fields Requirement
160        if let Some(error) = self.validate_c1_field_79_or_copy() {
161            all_errors.push(error);
162            if stop_on_first_error {
163                return all_errors;
164            }
165        }
166
167        // Validate Field 79 cancellation codes (if present)
168        let f79_errors = self.validate_field_79_codes();
169        all_errors.extend(f79_errors);
170
171        all_errors
172    }
173}
174
175// Implement the SwiftMessageBody trait for MT192
176impl crate::traits::SwiftMessageBody for MT192 {
177    fn message_type() -> &'static str {
178        "192"
179    }
180
181    fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
182        Self::parse_from_block4(block4)
183    }
184
185    fn to_mt_string(&self) -> String {
186        let mut result = String::new();
187
188        append_field(&mut result, &self.field_20);
189        append_field(&mut result, &self.field_21);
190        append_field(&mut result, &self.field_11s);
191        append_optional_field(&mut result, &self.field_79);
192
193        finalize_mt_string(result, false)
194    }
195
196    fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
197        // Call the existing public method implementation
198        MT192::validate_network_rules(self, stop_on_first_error)
199    }
200}