swift_mt_message/
swift_message.rs

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