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            "191" => messages::MT191::validate(),
143            "200" => messages::MT200::validate(),
144            "202" => messages::MT202::validate(),
145            "204" => messages::MT204::validate(),
146            "205" => messages::MT205::validate(),
147            "210" => messages::MT210::validate(),
148            "290" => messages::MT290::validate(),
149            "291" => messages::MT291::validate(),
150            "900" => messages::MT900::validate(),
151            "910" => messages::MT910::validate(),
152            "920" => messages::MT920::validate(),
153            "935" => messages::MT935::validate(),
154            "940" => messages::MT940::validate(),
155            "941" => messages::MT941::validate(),
156            "942" => messages::MT942::validate(),
157            "950" => messages::MT950::validate(),
158            "192" => messages::MT192::validate(),
159            "196" => messages::MT196::validate(),
160            "292" => messages::MT292::validate(),
161            "296" => messages::MT296::validate(),
162            "199" => messages::MT199::validate(),
163            "299" => messages::MT299::validate(),
164            _ => {
165                return ValidationResult::with_error(ValidationError::BusinessRuleValidation {
166                    rule_name: "UNSUPPORTED_MESSAGE_TYPE".to_string(),
167                    message: format!(
168                        "No validation rules defined for message type {}",
169                        T::message_type()
170                    ),
171                });
172            }
173        };
174
175        // Parse the validation rules JSON
176        let rules_json: serde_json::Value = match serde_json::from_str(validation_rules) {
177            Ok(json) => json,
178            Err(e) => {
179                return ValidationResult::with_error(ValidationError::BusinessRuleValidation {
180                    rule_name: "JSON_PARSE".to_string(),
181                    message: format!("Failed to parse validation rules JSON: {e}"),
182                });
183            }
184        };
185
186        // Extract rules array from the JSON
187        let rules = match rules_json.get("rules").and_then(|r| r.as_array()) {
188            Some(rules) => rules,
189            None => {
190                return ValidationResult::with_error(ValidationError::BusinessRuleValidation {
191                    rule_name: "RULES_FORMAT".to_string(),
192                    message: "Validation rules must contain a 'rules' array".to_string(),
193                });
194            }
195        };
196
197        // Get constants if they exist
198        let constants = rules_json
199            .get("constants")
200            .and_then(|c| c.as_object())
201            .cloned()
202            .unwrap_or_default();
203
204        // Create comprehensive data context with headers and fields
205        let context_value = match self.create_validation_context(&constants) {
206            Ok(context) => {
207                // Debug: Always show validation context in debug mode
208                if std::env::var("TEST_DEBUG").is_ok()
209                    && let Ok(context_str) = serde_json::to_string_pretty(&context)
210                {
211                    eprintln!("\n=== VALIDATION CONTEXT for {} ===", T::message_type());
212                    eprintln!("{}", context_str);
213                    eprintln!("=== END VALIDATION CONTEXT ===\n");
214                }
215                context
216            }
217            Err(e) => {
218                return ValidationResult::with_error(ValidationError::BusinessRuleValidation {
219                    rule_name: "CONTEXT_CREATION".to_string(),
220                    message: format!("Failed to create validation context: {e}"),
221                });
222            }
223        };
224
225        // Validate each rule using datalogic-rs
226        let mut errors = Vec::new();
227        let mut warnings = Vec::new();
228
229        for (rule_index, rule) in rules.iter().enumerate() {
230            let rule_id = rule
231                .get("id")
232                .and_then(|id| id.as_str())
233                .map(|s| s.to_string())
234                .unwrap_or_else(|| format!("RULE_{rule_index}"));
235
236            let rule_description = rule
237                .get("description")
238                .and_then(|desc| desc.as_str())
239                .unwrap_or("No description");
240
241            if let Some(condition) = rule.get("condition") {
242                // Create DataLogic instance for evaluation
243                let dl = datalogic_rs::DataLogic::new();
244                match dl.evaluate_json(condition, &context_value) {
245                    Ok(result) => {
246                        match result.as_bool() {
247                            Some(true) => {
248                                // Rule passed
249                                continue;
250                            }
251                            Some(false) => {
252                                // Rule failed
253                                errors.push(ValidationError::BusinessRuleValidation {
254                                    rule_name: rule_id.clone(),
255                                    message: format!(
256                                        "Business rule validation failed: {rule_id} - {rule_description}"
257                                    ),
258                                });
259                            }
260                            None => {
261                                // Rule returned non-boolean value
262                                warnings.push(format!(
263                                    "Rule {rule_id} returned non-boolean value: {result:?}"
264                                ));
265                            }
266                        }
267                    }
268                    Err(e) => {
269                        // JSONLogic evaluation error
270                        errors.push(ValidationError::BusinessRuleValidation {
271                            rule_name: rule_id.clone(),
272                            message: format!("JSONLogic evaluation error for rule {rule_id}: {e}"),
273                        });
274                    }
275                }
276            } else {
277                warnings.push(format!("Rule {rule_id} has no condition"));
278            }
279        }
280
281        ValidationResult {
282            is_valid: errors.is_empty(),
283            errors,
284            warnings,
285        }
286    }
287
288    /// Create a comprehensive validation context that includes headers, fields, and constants
289    fn create_validation_context(
290        &self,
291        constants: &serde_json::Map<String, serde_json::Value>,
292    ) -> Result<serde_json::Value> {
293        // Serialize the entire message (including headers) to JSON for data context
294        let full_message_data = match serde_json::to_value(self) {
295            Ok(data) => data,
296            Err(e) => {
297                return Err(ParseError::SerializationError {
298                    message: format!("Failed to serialize complete message: {e}"),
299                });
300            }
301        };
302
303        // Create a comprehensive data context
304        let mut data_context = serde_json::Map::new();
305
306        // Add the complete message data
307        if let serde_json::Value::Object(msg_obj) = full_message_data {
308            for (key, value) in msg_obj {
309                data_context.insert(key, value);
310            }
311        }
312
313        // Add constants to data context
314        for (key, value) in constants {
315            data_context.insert(key.clone(), value.clone());
316        }
317
318        // Extract sender and receiver BIC from headers for enhanced validation context
319        let (sender_country, receiver_country) = self.extract_country_codes_from_bics();
320
321        // Add enhanced message context including BIC-derived information
322        data_context.insert("message_context".to_string(), serde_json::json!({
323            "message_type": self.message_type,
324            "sender_country": sender_country,
325            "receiver_country": receiver_country,
326            "sender_bic": self.basic_header.logical_terminal,
327            "receiver_bic": &self.application_header.destination_address,
328            "message_priority": &self.application_header.priority,
329            "delivery_monitoring": self.application_header.delivery_monitoring.as_ref().unwrap_or(&"3".to_string()),
330        }));
331
332        Ok(serde_json::Value::Object(data_context))
333    }
334
335    /// Extract country codes from BIC codes in the headers
336    fn extract_country_codes_from_bics(&self) -> (String, String) {
337        // Extract sender country from basic header BIC (positions 4-5)
338        let sender_country = if self.basic_header.logical_terminal.len() >= 6 {
339            self.basic_header.logical_terminal[4..6].to_string()
340        } else {
341            "XX".to_string() // Unknown country
342        };
343
344        // Extract receiver country from application header destination BIC
345        let receiver_country = if self.application_header.destination_address.len() >= 6 {
346            self.application_header.destination_address[4..6].to_string()
347        } else {
348            "XX".to_string()
349        };
350
351        (sender_country, receiver_country)
352    }
353
354    pub fn to_mt_message(&self) -> String {
355        // Pre-allocate capacity based on typical message size
356        // Headers ~200 chars + fields vary but typically 20-100 chars each
357        let estimated_size = 200 + self.fields.to_fields().len() * 50;
358        let mut swift_message = String::with_capacity(estimated_size);
359
360        // Block 1: Basic Header
361        let block1 = &self.basic_header.to_string();
362        swift_message.push_str(&format!("{{1:{block1}}}\n"));
363
364        // Block 2: Application Header
365        let block2 = &self.application_header.to_string();
366        swift_message.push_str(&format!("{{2:{block2}}}\n"));
367
368        // Block 3: User Header (if present)
369        if let Some(ref user_header) = self.user_header {
370            let block3 = &user_header.to_string();
371            swift_message.push_str(&format!("{{3:{block3}}}\n"));
372        }
373
374        // Block 4: Text Block with fields
375        let mut block4 = String::new();
376
377        // Get optional field tags for this message type to determine which fields can be skipped
378        let optional_fields: std::collections::HashSet<String> = T::optional_fields()
379            .into_iter()
380            .map(|s| s.to_string())
381            .collect();
382
383        // Use to_ordered_fields for proper sequence ordering
384        let ordered_fields = self.fields.to_ordered_fields();
385
386        // Output fields in the correct order
387        for (field_tag, field_value) in ordered_fields {
388            // Skip empty optional fields
389            if optional_fields.contains(&field_tag) && field_value.trim().is_empty() {
390                continue;
391            }
392
393            // field_value already includes the field tag prefix from to_swift_string()
394            // but we need to check if it starts with ':' to avoid double prefixing
395            if field_value.starts_with(':') {
396                // Value already has field tag prefix, use as-is
397                block4.push_str(&format!("\n{field_value}"));
398            } else {
399                // Value doesn't have field tag prefix, add it
400                block4.push_str(&format!(
401                    "\n:{}:{field_value}",
402                    extract_base_tag(&field_tag)
403                ));
404            }
405        }
406
407        swift_message.push_str(&format!("{{4:{block4}\n-}}\n"));
408
409        // Block 5: Trailer (if present)
410        if let Some(ref trailer) = self.trailer {
411            let block5 = &trailer.to_string();
412            swift_message.push_str(&format!("{{5:{block5}}}\n"));
413        }
414
415        swift_message
416    }
417}