swift_mt_message/
swift_message.rs

1//! Complete SWIFT message with headers and body
2
3use crate::{
4    Result, ValidationError, ValidationResult,
5    errors::ParseError,
6    headers::{ApplicationHeader, BasicHeader, Trailer, UserHeader},
7    messages,
8    parser::extract_base_tag,
9    traits::SwiftMessageBody,
10};
11use serde::{Deserialize, Serialize};
12use std::any::Any;
13
14/// Complete SWIFT message with headers and body
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct SwiftMessage<T: SwiftMessageBody> {
17    /// Basic Header (Block 1)
18    pub basic_header: BasicHeader,
19
20    /// Application Header (Block 2)
21    pub application_header: ApplicationHeader,
22
23    /// User Header (Block 3) - Optional
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub user_header: Option<UserHeader>,
26
27    /// Trailer (Block 5) - Optional
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub trailer: Option<Trailer>,
30
31    /// Message type identifier
32    pub message_type: String,
33
34    /// Parsed message body with typed fields
35    pub fields: T,
36}
37
38impl<T: SwiftMessageBody> SwiftMessage<T> {
39    /// Check if this message contains reject codes (MT103 specific)
40    ///
41    /// Reject messages are identified by checking:
42    /// 1. Field 20 (Sender's Reference) for "REJT" prefix
43    /// 2. Block 3 field 108 (MUR - Message User Reference) for "REJT"
44    /// 3. Field 72 (Sender to Receiver Information) containing `/REJT/` code
45    pub fn has_reject_codes(&self) -> bool {
46        // Check Block 3 field 108 (MUR - Message User Reference)
47        if let Some(ref user_header) = self.user_header
48            && let Some(ref mur) = user_header.message_user_reference
49            && mur.to_uppercase().contains("REJT")
50        {
51            return true;
52        }
53
54        if let Some(mt103_fields) =
55            (&self.fields as &dyn Any).downcast_ref::<crate::messages::MT103>()
56        {
57            return mt103_fields.has_reject_codes();
58        } else if let Some(mt202_fields) =
59            (&self.fields as &dyn Any).downcast_ref::<crate::messages::MT202>()
60        {
61            return mt202_fields.has_reject_codes();
62        } else if let Some(mt205_fields) =
63            (&self.fields as &dyn Any).downcast_ref::<crate::messages::MT205>()
64        {
65            return mt205_fields.has_reject_codes();
66        }
67
68        false
69    }
70
71    /// Check if this message contains return codes (MT103 specific)
72    ///
73    /// Return messages are identified by checking:
74    /// 1. Field 20 (Sender's Reference) for "RETN" prefix
75    /// 2. Block 3 field 108 (MUR - Message User Reference) for "RETN"
76    /// 3. Field 72 (Sender to Receiver Information) containing `/RETN/` code
77    pub fn has_return_codes(&self) -> bool {
78        // Check Block 3 field 108 (MUR - Message User Reference)
79        if let Some(ref user_header) = self.user_header
80            && let Some(ref mur) = user_header.message_user_reference
81            && mur.to_uppercase().contains("RETN")
82        {
83            return true;
84        }
85
86        if let Some(mt103_fields) =
87            (&self.fields as &dyn Any).downcast_ref::<crate::messages::MT103>()
88        {
89            return mt103_fields.has_return_codes();
90        } else if let Some(mt202_fields) =
91            (&self.fields as &dyn Any).downcast_ref::<crate::messages::MT202>()
92        {
93            return mt202_fields.has_return_codes();
94        } else if let Some(mt205_fields) =
95            (&self.fields as &dyn Any).downcast_ref::<crate::messages::MT205>()
96        {
97            return mt205_fields.has_return_codes();
98        }
99
100        false
101    }
102
103    pub fn is_cover_message(&self) -> bool {
104        if let Some(mt202_fields) =
105            (&self.fields as &dyn Any).downcast_ref::<crate::messages::MT202>()
106        {
107            return mt202_fields.is_cover_message();
108        }
109        if let Some(mt205_fields) =
110            (&self.fields as &dyn Any).downcast_ref::<crate::messages::MT205>()
111        {
112            return mt205_fields.is_cover_message();
113        }
114
115        false
116    }
117
118    pub fn is_stp_message(&self) -> bool {
119        if let Some(mt103_fields) =
120            (&self.fields as &dyn Any).downcast_ref::<crate::messages::MT103>()
121        {
122            return mt103_fields.is_stp_compliant();
123        }
124
125        false
126    }
127
128    /// Validate message against business rules using JSONLogic
129    /// This validation method has access to both headers and message fields,
130    /// allowing for comprehensive validation of MT103 and other message types.
131    pub fn validate(&self) -> ValidationResult {
132        // Check if the message type has validation rules
133        let validation_rules = match T::message_type() {
134            "101" => messages::MT101::validate(),
135            "103" => messages::MT103::validate(),
136            "104" => messages::MT104::validate(),
137            "107" => messages::MT107::validate(),
138            "110" => messages::MT110::validate(),
139            "111" => messages::MT111::validate(),
140            "112" => messages::MT112::validate(),
141            "190" => messages::MT190::validate(),
142            "200" => messages::MT200::validate(),
143            "202" => messages::MT202::validate(),
144            "204" => messages::MT204::validate(),
145            "205" => messages::MT205::validate(),
146            "210" => messages::MT210::validate(),
147            "290" => messages::MT290::validate(),
148            "900" => messages::MT900::validate(),
149            "910" => messages::MT910::validate(),
150            "920" => messages::MT920::validate(),
151            "935" => messages::MT935::validate(),
152            "940" => messages::MT940::validate(),
153            "941" => messages::MT941::validate(),
154            "942" => messages::MT942::validate(),
155            "950" => messages::MT950::validate(),
156            "192" => messages::MT192::validate(),
157            "196" => messages::MT196::validate(),
158            "292" => messages::MT292::validate(),
159            "296" => messages::MT296::validate(),
160            "199" => messages::MT199::validate(),
161            "299" => messages::MT299::validate(),
162            _ => {
163                return ValidationResult::with_error(ValidationError::BusinessRuleValidation {
164                    rule_name: "UNSUPPORTED_MESSAGE_TYPE".to_string(),
165                    message: format!(
166                        "No validation rules defined for message type {}",
167                        T::message_type()
168                    ),
169                });
170            }
171        };
172
173        // Parse the validation rules JSON
174        let rules_json: serde_json::Value = match serde_json::from_str(validation_rules) {
175            Ok(json) => json,
176            Err(e) => {
177                return ValidationResult::with_error(ValidationError::BusinessRuleValidation {
178                    rule_name: "JSON_PARSE".to_string(),
179                    message: format!("Failed to parse validation rules JSON: {e}"),
180                });
181            }
182        };
183
184        // Extract rules array from the JSON
185        let rules = match rules_json.get("rules").and_then(|r| r.as_array()) {
186            Some(rules) => rules,
187            None => {
188                return ValidationResult::with_error(ValidationError::BusinessRuleValidation {
189                    rule_name: "RULES_FORMAT".to_string(),
190                    message: "Validation rules must contain a 'rules' array".to_string(),
191                });
192            }
193        };
194
195        // Get constants if they exist
196        let constants = rules_json
197            .get("constants")
198            .and_then(|c| c.as_object())
199            .cloned()
200            .unwrap_or_default();
201
202        // Create comprehensive data context with headers and fields
203        let context_value = match self.create_validation_context(&constants) {
204            Ok(context) => {
205                // Debug: Always show validation context in debug mode
206                if std::env::var("TEST_DEBUG").is_ok()
207                    && let Ok(context_str) = serde_json::to_string_pretty(&context)
208                {
209                    eprintln!("\n=== VALIDATION CONTEXT for {} ===", T::message_type());
210                    eprintln!("{}", context_str);
211                    eprintln!("=== END VALIDATION CONTEXT ===\n");
212                }
213                context
214            }
215            Err(e) => {
216                return ValidationResult::with_error(ValidationError::BusinessRuleValidation {
217                    rule_name: "CONTEXT_CREATION".to_string(),
218                    message: format!("Failed to create validation context: {e}"),
219                });
220            }
221        };
222
223        // Validate each rule using datalogic-rs
224        let mut errors = Vec::new();
225        let mut warnings = Vec::new();
226
227        for (rule_index, rule) in rules.iter().enumerate() {
228            let rule_id = rule
229                .get("id")
230                .and_then(|id| id.as_str())
231                .map(|s| s.to_string())
232                .unwrap_or_else(|| format!("RULE_{rule_index}"));
233
234            let rule_description = rule
235                .get("description")
236                .and_then(|desc| desc.as_str())
237                .unwrap_or("No description");
238
239            if let Some(condition) = rule.get("condition") {
240                // Create DataLogic instance for evaluation
241                let dl = datalogic_rs::DataLogic::new();
242                match dl.evaluate_json(condition, &context_value, None) {
243                    Ok(result) => {
244                        match result.as_bool() {
245                            Some(true) => {
246                                // Rule passed
247                                continue;
248                            }
249                            Some(false) => {
250                                // Rule failed
251                                errors.push(ValidationError::BusinessRuleValidation {
252                                    rule_name: rule_id.clone(),
253                                    message: format!(
254                                        "Business rule validation failed: {rule_id} - {rule_description}"
255                                    ),
256                                });
257                            }
258                            None => {
259                                // Rule returned non-boolean value
260                                warnings.push(format!(
261                                    "Rule {rule_id} returned non-boolean value: {result:?}"
262                                ));
263                            }
264                        }
265                    }
266                    Err(e) => {
267                        // JSONLogic evaluation error
268                        errors.push(ValidationError::BusinessRuleValidation {
269                            rule_name: rule_id.clone(),
270                            message: format!("JSONLogic evaluation error for rule {rule_id}: {e}"),
271                        });
272                    }
273                }
274            } else {
275                warnings.push(format!("Rule {rule_id} has no condition"));
276            }
277        }
278
279        ValidationResult {
280            is_valid: errors.is_empty(),
281            errors,
282            warnings,
283        }
284    }
285
286    /// Create a comprehensive validation context that includes headers, fields, and constants
287    fn create_validation_context(
288        &self,
289        constants: &serde_json::Map<String, serde_json::Value>,
290    ) -> Result<serde_json::Value> {
291        // Serialize the entire message (including headers) to JSON for data context
292        let full_message_data = match serde_json::to_value(self) {
293            Ok(data) => data,
294            Err(e) => {
295                return Err(ParseError::SerializationError {
296                    message: format!("Failed to serialize complete message: {e}"),
297                });
298            }
299        };
300
301        // Create a comprehensive data context
302        let mut data_context = serde_json::Map::new();
303
304        // Add the complete message data
305        if let serde_json::Value::Object(msg_obj) = full_message_data {
306            for (key, value) in msg_obj {
307                data_context.insert(key, value);
308            }
309        }
310
311        // Add constants to data context
312        for (key, value) in constants {
313            data_context.insert(key.clone(), value.clone());
314        }
315
316        // Extract sender and receiver BIC from headers for enhanced validation context
317        let (sender_country, receiver_country) = self.extract_country_codes_from_bics();
318
319        // Add enhanced message context including BIC-derived information
320        data_context.insert("message_context".to_string(), serde_json::json!({
321            "message_type": self.message_type,
322            "sender_country": sender_country,
323            "receiver_country": receiver_country,
324            "sender_bic": self.basic_header.logical_terminal,
325            "receiver_bic": &self.application_header.destination_address,
326            "message_priority": &self.application_header.priority,
327            "delivery_monitoring": self.application_header.delivery_monitoring.as_ref().unwrap_or(&"3".to_string()),
328        }));
329
330        Ok(serde_json::Value::Object(data_context))
331    }
332
333    /// Extract country codes from BIC codes in the headers
334    fn extract_country_codes_from_bics(&self) -> (String, String) {
335        // Extract sender country from basic header BIC (positions 4-5)
336        let sender_country = if self.basic_header.logical_terminal.len() >= 6 {
337            self.basic_header.logical_terminal[4..6].to_string()
338        } else {
339            "XX".to_string() // Unknown country
340        };
341
342        // Extract receiver country from application header destination BIC
343        let receiver_country = if self.application_header.destination_address.len() >= 6 {
344            self.application_header.destination_address[4..6].to_string()
345        } else {
346            "XX".to_string()
347        };
348
349        (sender_country, receiver_country)
350    }
351
352    pub fn to_mt_message(&self) -> String {
353        // Pre-allocate capacity based on typical message size
354        // Headers ~200 chars + fields vary but typically 20-100 chars each
355        let estimated_size = 200 + self.fields.to_fields().len() * 50;
356        let mut swift_message = String::with_capacity(estimated_size);
357
358        // Block 1: Basic Header
359        let block1 = &self.basic_header.to_string();
360        swift_message.push_str(&format!("{{1:{block1}}}\n"));
361
362        // Block 2: Application Header
363        let block2 = &self.application_header.to_string();
364        swift_message.push_str(&format!("{{2:{block2}}}\n"));
365
366        // Block 3: User Header (if present)
367        if let Some(ref user_header) = self.user_header {
368            let block3 = &user_header.to_string();
369            swift_message.push_str(&format!("{{3:{block3}}}\n"));
370        }
371
372        // Block 4: Text Block with fields
373        let mut block4 = String::new();
374
375        // Get optional field tags for this message type to determine which fields can be skipped
376        let optional_fields: std::collections::HashSet<String> = T::optional_fields()
377            .into_iter()
378            .map(|s| s.to_string())
379            .collect();
380
381        // Use to_ordered_fields for proper sequence ordering
382        let ordered_fields = self.fields.to_ordered_fields();
383
384        // Output fields in the correct order
385        for (field_tag, field_value) in ordered_fields {
386            // Skip empty optional fields
387            if optional_fields.contains(&field_tag) && field_value.trim().is_empty() {
388                continue;
389            }
390
391            // field_value already includes the field tag prefix from to_swift_string()
392            // but we need to check if it starts with ':' to avoid double prefixing
393            if field_value.starts_with(':') {
394                // Value already has field tag prefix, use as-is
395                block4.push_str(&format!("\n{field_value}"));
396            } else {
397                // Value doesn't have field tag prefix, add it
398                block4.push_str(&format!(
399                    "\n:{}:{field_value}",
400                    extract_base_tag(&field_tag)
401                ));
402            }
403        }
404
405        swift_message.push_str(&format!("{{4:{block4}\n-}}\n"));
406
407        // Block 5: Trailer (if present)
408        if let Some(ref trailer) = self.trailer {
409            let block5 = &trailer.to_string();
410            swift_message.push_str(&format!("{{5:{block5}}}\n"));
411        }
412
413        swift_message
414    }
415}