swift_mt_message/messages/
mt935.rs

1use crate::errors::SwiftValidationError;
2use crate::fields::*;
3use crate::parser::utils::*;
4use serde::{Deserialize, Serialize};
5
6/// **MT935: Rate Change Advice**
7///
8/// Advises changes in interest rates affecting accounts or agreements.
9///
10/// **Usage:** Rate change notifications, interest rate updates
11/// **Category:** Category 9 (Cash Management & Customer Status)
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct MT935 {
14    /// Transaction Reference Number (Field 20)
15    #[serde(rename = "20")]
16    pub field_20: Field20,
17
18    /// Rate change sequences (1-10 occurrences)
19    #[serde(rename = "#")]
20    pub rate_changes: Vec<MT935RateChange>,
21
22    /// Sender to Receiver Information (Field 72)
23    #[serde(rename = "72", skip_serializing_if = "Option::is_none")]
24    pub field_72: Option<Field72>,
25}
26
27/// Rate change sequence for MT935
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29pub struct MT935RateChange {
30    /// Further Identification (Field 23)
31    #[serde(rename = "23", skip_serializing_if = "Option::is_none")]
32    pub field_23: Option<Field23>,
33
34    /// Account Identification (Field 25)
35    #[serde(rename = "25", skip_serializing_if = "Option::is_none")]
36    pub field_25: Option<Field25NoOption>,
37
38    /// Effective Date of New Rate (Field 30)
39    #[serde(rename = "30")]
40    pub field_30: Field30,
41
42    /// New Interest Rate (Field 37H)
43    #[serde(rename = "37H")]
44    pub field_37h: Vec<Field37H>,
45}
46
47impl MT935 {
48    /// Parse message from Block 4 content
49    pub fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
50        let mut parser = crate::parser::MessageParser::new(block4, "935");
51
52        // Parse mandatory field 20
53        let field_20 = parser.parse_field::<Field20>("20")?;
54
55        // Enable duplicate field handling for repetitive sequences
56        parser = parser.with_duplicates(true);
57
58        // Parse rate change sequences (1-10 occurrences)
59        let mut rate_changes = Vec::new();
60
61        while (parser.detect_field("23") || parser.detect_field("25")) && rate_changes.len() < 10 {
62            // Parse optional Field 23 - Further Identification (mutually exclusive with field_25)
63            let field_23 = parser.parse_optional_field::<Field23>("23")?;
64
65            // Parse optional Field 25 - Account Identification (mutually exclusive with field_23)
66            let field_25 = parser.parse_optional_field::<Field25NoOption>("25")?;
67
68            // Parse mandatory Field 30 - Effective Date of New Rate
69            let field_30 = parser.parse_field::<Field30>("30")?;
70
71            // Parse field 37H (New Interest Rate) - can be multiple per sequence
72            let mut field_37h = Vec::new();
73            while let Ok(rate) = parser.parse_field::<Field37H>("37H") {
74                field_37h.push(rate);
75                // Keep parsing 37H until we hit the next sequence marker or end
76                if !parser.detect_field("37H") {
77                    break;
78                }
79            }
80
81            // At least one field 37H is required per sequence
82            if field_37h.is_empty() {
83                return Err(crate::errors::ParseError::InvalidFormat {
84                    message: format!(
85                        "MT935: At least one field 37H is required for sequence {}",
86                        rate_changes.len() + 1
87                    ),
88                });
89            }
90
91            rate_changes.push(MT935RateChange {
92                field_23,
93                field_25,
94                field_30,
95                field_37h,
96            });
97        }
98
99        // Disable duplicates mode after parsing sequences
100        parser = parser.with_duplicates(false);
101
102        // At least one sequence is required
103        if rate_changes.is_empty() {
104            return Err(crate::errors::ParseError::InvalidFormat {
105                message: "MT935: At least one rate change sequence is required".to_string(),
106            });
107        }
108
109        // Parse optional field 72
110        let field_72 = parser.parse_optional_field::<Field72>("72")?;
111
112        Ok(MT935 {
113            field_20,
114            rate_changes,
115            field_72,
116        })
117    }
118
119    // ========================================================================
120    // NETWORK VALIDATION RULES (SR 2025 MT935)
121    // ========================================================================
122
123    /// Valid function codes for field 23 (Further Identification)
124    const VALID_23_FUNCTION_CODES: &'static [&'static str] = &[
125        "BASE",
126        "CALL",
127        "COMMERCIAL",
128        "CURRENT",
129        "DEPOSIT",
130        "NOTICE",
131        "PRIME",
132    ];
133
134    // ========================================================================
135    // HELPER METHODS
136    // ========================================================================
137
138    /// Check if field 23 is present in a sequence
139    fn has_field_23(seq: &MT935RateChange) -> bool {
140        seq.field_23.is_some()
141    }
142
143    /// Check if field 25 is present in a sequence
144    fn has_field_25(seq: &MT935RateChange) -> bool {
145        seq.field_25.is_some()
146    }
147
148    // ========================================================================
149    // VALIDATION RULES (C1-C2 and Field-Specific)
150    // ========================================================================
151
152    /// C1: Repetitive Sequence Occurrence (Error code: T10)
153    /// The repetitive sequence must appear at least once, but not more than ten times
154    fn validate_c1_sequence_occurrence(&self) -> Option<SwiftValidationError> {
155        let num_sequences = self.rate_changes.len();
156
157        if num_sequences == 0 {
158            return Some(SwiftValidationError::content_error(
159                "T10",
160                "RateChangeSequence",
161                "0",
162                "The repetitive sequence must appear at least once",
163                "The repetitive sequence (fields 23/25, 30, 37H) must appear at least once",
164            ));
165        }
166
167        if num_sequences > 10 {
168            return Some(SwiftValidationError::content_error(
169                "T10",
170                "RateChangeSequence",
171                &num_sequences.to_string(),
172                &format!(
173                    "The repetitive sequence must not appear more than ten times, found {}",
174                    num_sequences
175                ),
176                "The repetitive sequence (fields 23/25, 30, 37H) must not appear more than ten times",
177            ));
178        }
179
180        None
181    }
182
183    /// C2: Further Identification and Account Identification Mutual Exclusivity (Error code: C83)
184    /// Either field 23 or field 25, but not both, must be present in any repetitive sequence
185    fn validate_c2_field_23_25_mutual_exclusivity(&self) -> Vec<SwiftValidationError> {
186        let mut errors = Vec::new();
187
188        for (idx, seq) in self.rate_changes.iter().enumerate() {
189            let has_23 = Self::has_field_23(seq);
190            let has_25 = Self::has_field_25(seq);
191
192            if has_23 && has_25 {
193                // Both present - NOT ALLOWED
194                errors.push(SwiftValidationError::relation_error(
195                    "C83",
196                    "23/25",
197                    vec!["23".to_string(), "25".to_string()],
198                    &format!(
199                        "Sequence {}: Both field 23 and field 25 are present. Either field 23 or field 25, but not both, must be present",
200                        idx + 1
201                    ),
202                    "Either field 23 or field 25, but not both, must be present in any repetitive sequence",
203                ));
204            } else if !has_23 && !has_25 {
205                // Neither present - NOT ALLOWED
206                errors.push(SwiftValidationError::relation_error(
207                    "C83",
208                    "23/25",
209                    vec!["23".to_string(), "25".to_string()],
210                    &format!(
211                        "Sequence {}: Neither field 23 nor field 25 is present. Either field 23 or field 25 must be present",
212                        idx + 1
213                    ),
214                    "Either field 23 or field 25, but not both, must be present in any repetitive sequence",
215                ));
216            }
217        }
218
219        errors
220    }
221
222    /// Validate field 23 (Further Identification) format and content
223    /// Field 23 must be formatted as: 3!a[2!n]11x (Currency)(Number of Days)(Function)
224    fn validate_field_23(&self) -> Vec<SwiftValidationError> {
225        let mut errors = Vec::new();
226
227        for (idx, seq) in self.rate_changes.iter().enumerate() {
228            if let Some(ref field_23) = seq.field_23 {
229                // Reconstruct the full value as it appears in SWIFT format
230                let mut value = field_23.function_code.clone();
231                if let Some(days) = field_23.days {
232                    value.push_str(&format!("{:02}", days));
233                }
234                value.push_str(&field_23.reference);
235
236                // Minimum length check: 3 (currency) + at least one function character
237                if value.len() < 4 {
238                    errors.push(SwiftValidationError::format_error(
239                        "T26",
240                        "23",
241                        &value,
242                        "3!a[2!n]11x",
243                        &format!(
244                            "Sequence {}: Field 23 must be at least 4 characters (currency code + function)",
245                            idx + 1
246                        ),
247                    ));
248                    continue;
249                }
250
251                // Extract currency (first 3 characters)
252                let currency = &value[..3];
253
254                // Validate currency is alphabetic
255                if !currency.chars().all(|c| c.is_ascii_alphabetic()) {
256                    errors.push(SwiftValidationError::format_error(
257                        "T26",
258                        "23",
259                        &value,
260                        "3!a[2!n]11x",
261                        &format!(
262                            "Sequence {}: Currency code '{}' must be 3 alphabetic characters",
263                            idx + 1,
264                            currency
265                        ),
266                    ));
267                }
268
269                // Extract remaining part (could be [2!n]function or just function)
270                let remaining = &value[3..];
271
272                // Check if next 2 characters are digits (Number of Days)
273                let (num_days, function_start) =
274                    if remaining.len() >= 2 && remaining[..2].chars().all(|c| c.is_ascii_digit()) {
275                        (Some(&remaining[..2]), 2)
276                    } else {
277                        (None, 0)
278                    };
279
280                // Extract function code
281                let function = &remaining[function_start..];
282
283                // Validate function code
284                if !Self::VALID_23_FUNCTION_CODES.contains(&function) {
285                    errors.push(SwiftValidationError::content_error(
286                        "T26",
287                        "23",
288                        function,
289                        &format!(
290                            "Sequence {}: Function code '{}' is not valid. Valid codes: {}",
291                            idx + 1,
292                            function,
293                            Self::VALID_23_FUNCTION_CODES.join(", ")
294                        ),
295                        &format!(
296                            "Function code must be one of: {}",
297                            Self::VALID_23_FUNCTION_CODES.join(", ")
298                        ),
299                    ));
300                }
301
302                // Validate Number of Days only allowed with NOTICE
303                if let Some(days) = num_days
304                    && function != "NOTICE"
305                {
306                    errors.push(SwiftValidationError::content_error(
307                            "T26",
308                            "23",
309                            &value,
310                            &format!(
311                                "Sequence {}: Number of Days '{}' is only allowed when Function is NOTICE, but found '{}'",
312                                idx + 1, days, function
313                            ),
314                            "Number of Days must only be used when Function is NOTICE",
315                        ));
316                }
317            }
318        }
319
320        errors
321    }
322
323    /// Validate field 37H (New Interest Rate) content rules
324    /// - Indicator must be C or D (Error code: T51)
325    /// - Sign must not be used if Rate is zero (Error code: T14)
326    fn validate_field_37h(&self) -> Vec<SwiftValidationError> {
327        let mut errors = Vec::new();
328
329        for (seq_idx, seq) in self.rate_changes.iter().enumerate() {
330            for (field_idx, field_37h) in seq.field_37h.iter().enumerate() {
331                let indicator = field_37h.rate_indicator;
332                let is_negative = field_37h.is_negative;
333                let rate = field_37h.rate;
334
335                // T51: Validate indicator is C or D
336                if indicator != 'C' && indicator != 'D' {
337                    errors.push(SwiftValidationError::format_error(
338                        "T51",
339                        "37H",
340                        &indicator.to_string(),
341                        "C or D",
342                        &format!(
343                            "Sequence {}, Rate {}: Indicator '{}' is not valid. Must be C (Credit) or D (Debit)",
344                            seq_idx + 1,
345                            field_idx + 1,
346                            indicator
347                        ),
348                    ));
349                }
350
351                // T14: Sign must not be used if rate is zero
352                if rate.abs() < 0.00001 && is_negative.is_some() {
353                    errors.push(SwiftValidationError::content_error(
354                        "T14",
355                        "37H",
356                        &rate.to_string(),
357                        &format!(
358                            "Sequence {}, Rate {}: Sign must not be used when rate is zero",
359                            seq_idx + 1,
360                            field_idx + 1
361                        ),
362                        "Sign (N for negative) must not be used if Rate is zero",
363                    ));
364                }
365            }
366        }
367
368        errors
369    }
370
371    /// Main validation method - validates all network rules
372    /// Returns array of validation errors, respects stop_on_first_error flag
373    pub fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
374        let mut all_errors = Vec::new();
375
376        // C1: Repetitive Sequence Occurrence
377        if let Some(error) = self.validate_c1_sequence_occurrence() {
378            all_errors.push(error);
379            if stop_on_first_error {
380                return all_errors;
381            }
382        }
383
384        // C2: Field 23/25 Mutual Exclusivity
385        let c2_errors = self.validate_c2_field_23_25_mutual_exclusivity();
386        all_errors.extend(c2_errors);
387        if stop_on_first_error && !all_errors.is_empty() {
388            return all_errors;
389        }
390
391        // Field 23 Validation
392        let f23_errors = self.validate_field_23();
393        all_errors.extend(f23_errors);
394        if stop_on_first_error && !all_errors.is_empty() {
395            return all_errors;
396        }
397
398        // Field 37H Validation
399        let f37h_errors = self.validate_field_37h();
400        all_errors.extend(f37h_errors);
401
402        all_errors
403    }
404}
405
406// Implement the SwiftMessageBody trait for MT935
407impl crate::traits::SwiftMessageBody for MT935 {
408    fn message_type() -> &'static str {
409        "935"
410    }
411
412    fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
413        Self::parse_from_block4(block4)
414    }
415
416    fn to_mt_string(&self) -> String {
417        use crate::traits::SwiftField;
418        let mut result = String::new();
419
420        append_field(&mut result, &self.field_20);
421
422        // Rate change sequences
423        for rate_change in &self.rate_changes {
424            append_optional_field(&mut result, &rate_change.field_23);
425            append_optional_field(&mut result, &rate_change.field_25);
426            append_field(&mut result, &rate_change.field_30);
427
428            // Manually append vec field
429            for field_37h in &rate_change.field_37h {
430                result.push_str(&field_37h.to_swift_string());
431                result.push_str("\r\n");
432            }
433        }
434
435        append_optional_field(&mut result, &self.field_72);
436
437        finalize_mt_string(result, false)
438    }
439
440    fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
441        // Call the existing public method implementation
442        MT935::validate_network_rules(self, stop_on_first_error)
443    }
444}