swift_mt_message/messages/
mt101.rs

1use crate::errors::SwiftValidationError;
2use crate::fields::*;
3use crate::parser::utils::*;
4use serde::{Deserialize, Serialize};
5use std::collections::HashSet;
6
7/// Sequence B - Transaction details
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9pub struct MT101Transaction {
10    /// Transaction reference (Field 21)
11    #[serde(rename = "21")]
12    pub field_21: Field21NoOption,
13
14    /// F/X deal reference (Field 21F)
15    #[serde(rename = "21F")]
16    pub field_21f: Option<Field21F>,
17
18    /// Instruction codes (Field 23E)
19    #[serde(rename = "23E")]
20    pub field_23e: Option<Vec<Field23E>>,
21
22    /// Currency and amount (Field 32B)
23    #[serde(rename = "32B")]
24    pub field_32b: Field32B,
25
26    /// Instructing party (Field 50C/L)
27    #[serde(flatten)]
28    pub instructing_party_tx: Option<Field50InstructingParty>,
29
30    /// Ordering customer (Field 50F/G/H)
31    #[serde(flatten)]
32    pub ordering_customer_tx: Option<Field50OrderingCustomerFGH>,
33
34    /// Account servicing institution (Field 52)
35    #[serde(flatten)]
36    pub field_52: Option<Field52AccountServicingInstitution>,
37
38    /// Intermediary (Field 56)
39    #[serde(flatten)]
40    pub field_56: Option<Field56Intermediary>,
41
42    /// Account with institution (Field 57)
43    #[serde(flatten)]
44    pub field_57: Option<Field57AccountWithInstitution>,
45
46    /// Beneficiary customer (Field 59)
47    #[serde(flatten)]
48    pub field_59: Field59,
49
50    /// Remittance information (Field 70)
51    #[serde(rename = "70")]
52    pub field_70: Option<Field70>,
53
54    /// Regulatory reporting (Field 77B)
55    #[serde(rename = "77B")]
56    pub field_77b: Option<Field77B>,
57
58    /// Original amount (Field 33B)
59    #[serde(rename = "33B")]
60    pub field_33b: Option<Field33B>,
61
62    /// Details of charges (Field 71A)
63    #[serde(rename = "71A")]
64    pub field_71a: Field71A,
65
66    /// Charges account (Field 25A)
67    #[serde(rename = "25A")]
68    pub field_25a: Option<Field25A>,
69
70    /// Exchange rate (Field 36)
71    #[serde(rename = "36")]
72    pub field_36: Option<Field36>,
73}
74
75/// **MT101: Request for Transfer**
76///
77/// Batch payment instruction from ordering customer to account servicing institution.
78/// Contains one or more transfer instructions for beneficiary payments.
79///
80/// **Usage:** Batch payments, salary payments, vendor payments
81/// **Category:** Category 1 (Customer Payments)
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83pub struct MT101 {
84    /// Sender's reference (Field 20)
85    #[serde(rename = "20")]
86    pub field_20: Field20,
87
88    /// Customer specified reference (Field 21R)
89    #[serde(rename = "21R")]
90    pub field_21r: Option<Field21R>,
91
92    /// Message index/total (Field 28D)
93    #[serde(rename = "28D")]
94    pub field_28d: Field28D,
95
96    /// Instructing party (Field 50C/L)
97    #[serde(flatten)]
98    pub instructing_party: Option<Field50InstructingParty>,
99
100    /// Ordering customer (Field 50F/G/H)
101    #[serde(flatten)]
102    pub ordering_customer: Option<Field50OrderingCustomerFGH>,
103
104    /// Account servicing institution (Field 52)
105    #[serde(flatten)]
106    pub field_52a: Option<Field52AccountServicingInstitution>,
107
108    /// Sending institution (Field 51A)
109    #[serde(rename = "51A")]
110    pub field_51a: Option<Field51A>,
111
112    /// Requested execution date (Field 30)
113    #[serde(rename = "30")]
114    pub field_30: Field30,
115
116    /// Account identification (Field 25)
117    #[serde(rename = "25")]
118    pub field_25: Option<Field25NoOption>,
119
120    /// Transaction details (Sequence B)
121    #[serde(rename = "#")]
122    pub transactions: Vec<MT101Transaction>,
123}
124
125impl MT101 {
126    /// Parse message from Block 4 content
127    pub fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
128        let mut parser = crate::parser::MessageParser::new(block4, "101");
129
130        // Parse mandatory and optional fields in sequence A
131        let field_20 = parser.parse_field::<Field20>("20")?;
132        let field_21r = parser.parse_optional_field::<Field21R>("21R")?;
133        let field_28d = parser.parse_field::<Field28D>("28D")?;
134
135        // Parse optional ordering customer and instructing party (can appear in either order)
136        // Field 50 can be either instructing party (C/L) or ordering customer (F/G/H)
137        // Check which variant is present and parse accordingly
138        let (instructing_party, ordering_customer) = {
139            let mut instructing = None;
140            let mut ordering = None;
141
142            // Detect which Field 50 variant is present
143            if let Some(variant) = parser.detect_variant_optional("50") {
144                match variant.as_str() {
145                    "C" | "L" => {
146                        // Instructing party variants
147                        instructing =
148                            parser.parse_optional_variant_field::<Field50InstructingParty>("50")?;
149                    }
150                    "F" | "G" | "H" => {
151                        // Ordering customer variants
152                        ordering = parser
153                            .parse_optional_variant_field::<Field50OrderingCustomerFGH>("50")?;
154                    }
155                    _ => {
156                        // Unknown variant - try instructing party first, then ordering customer
157                        if let Ok(Some(field)) =
158                            parser.parse_optional_variant_field::<Field50InstructingParty>("50")
159                        {
160                            instructing = Some(field);
161                        } else {
162                            ordering = parser
163                                .parse_optional_variant_field::<Field50OrderingCustomerFGH>("50")?;
164                        }
165                    }
166                }
167            }
168
169            (instructing, ordering)
170        };
171
172        let field_52a =
173            parser.parse_optional_variant_field::<Field52AccountServicingInstitution>("52")?;
174        let field_51a = parser.parse_optional_field::<Field51A>("51A")?;
175        let field_30 = parser.parse_field::<Field30>("30")?;
176        let field_25 = parser.parse_optional_field::<Field25NoOption>("25")?;
177
178        // Parse transactions - this is a repeating sequence B
179        let mut transactions = Vec::new();
180
181        // Enable duplicates for repeating fields
182        parser = parser.with_duplicates(true);
183
184        // Parse each transaction - they start with field 21
185        while parser.detect_field("21") {
186            let field_21 = parser.parse_field::<Field21NoOption>("21")?;
187            let field_21f = parser.parse_optional_field::<Field21F>("21F")?;
188
189            // Field 23E can appear multiple times within a transaction
190            // Only parse consecutive 23E fields (stop when we hit any other field)
191            let field_23e = if parser.detect_field("23E") {
192                let mut codes = Vec::new();
193                while parser.detect_field("23E") {
194                    if let Ok(field) = parser.parse_field::<Field23E>("23E") {
195                        codes.push(field);
196                    } else {
197                        break;
198                    }
199                }
200                if !codes.is_empty() { Some(codes) } else { None }
201            } else {
202                None
203            };
204
205            let field_32b = parser.parse_field::<Field32B>("32B")?;
206
207            // Transaction-level optional ordering parties
208            let instructing_party_tx =
209                parser.parse_optional_variant_field::<Field50InstructingParty>("50")?;
210            let ordering_customer_tx =
211                parser.parse_optional_variant_field::<Field50OrderingCustomerFGH>("50")?;
212
213            let field_52 =
214                parser.parse_optional_variant_field::<Field52AccountServicingInstitution>("52")?;
215            let field_56 = parser.parse_optional_variant_field::<Field56Intermediary>("56")?;
216            let field_57 =
217                parser.parse_optional_variant_field::<Field57AccountWithInstitution>("57")?;
218            let field_59 = parser.parse_variant_field::<Field59>("59")?;
219            let field_70 = parser.parse_optional_field::<Field70>("70")?;
220            let field_77b = parser.parse_optional_field::<Field77B>("77B")?;
221            let field_33b = parser.parse_optional_field::<Field33B>("33B")?;
222            let field_71a = parser.parse_field::<Field71A>("71A")?; // Mandatory
223            let field_25a = parser.parse_optional_field::<Field25A>("25A")?;
224            let field_36 = parser.parse_optional_field::<Field36>("36")?;
225
226            transactions.push(MT101Transaction {
227                field_21,
228                field_21f,
229                field_23e,
230                field_32b,
231                instructing_party_tx,
232                ordering_customer_tx,
233                field_52,
234                field_56,
235                field_57,
236                field_59,
237                field_70,
238                field_77b,
239                field_33b,
240                field_71a,
241                field_25a,
242                field_36,
243            });
244        }
245
246        // Verify all content is consumed
247        verify_parser_complete(&parser)?;
248
249        Ok(Self {
250            field_20,
251            field_21r,
252            field_28d,
253            instructing_party,
254            ordering_customer,
255            field_52a,
256            field_51a,
257            field_30,
258            field_25,
259            transactions,
260        })
261    }
262
263    /// Parse from generic SWIFT input (tries to detect blocks)
264    pub fn parse(input: &str) -> Result<Self, crate::errors::ParseError> {
265        let block4 = extract_block4(input)?;
266        Self::parse_from_block4(&block4)
267    }
268
269    /// Convert to SWIFT MT text format
270    pub fn to_mt_string(&self) -> String {
271        let mut result = String::new();
272
273        // Add mandatory fields in sequence A
274        append_field(&mut result, &self.field_20);
275        append_optional_field(&mut result, &self.field_21r);
276        append_field(&mut result, &self.field_28d);
277        append_optional_field(&mut result, &self.instructing_party);
278        append_optional_field(&mut result, &self.ordering_customer);
279        append_optional_field(&mut result, &self.field_52a);
280        append_optional_field(&mut result, &self.field_51a);
281        append_field(&mut result, &self.field_30);
282        append_optional_field(&mut result, &self.field_25);
283
284        // Add transactions (sequence B)
285        for transaction in &self.transactions {
286            append_field(&mut result, &transaction.field_21);
287            append_optional_field(&mut result, &transaction.field_21f);
288            append_vec_field(&mut result, &transaction.field_23e);
289            append_field(&mut result, &transaction.field_32b);
290            append_optional_field(&mut result, &transaction.instructing_party_tx);
291            append_optional_field(&mut result, &transaction.ordering_customer_tx);
292            append_optional_field(&mut result, &transaction.field_52);
293            append_optional_field(&mut result, &transaction.field_56);
294            append_optional_field(&mut result, &transaction.field_57);
295            append_field(&mut result, &transaction.field_59);
296            append_optional_field(&mut result, &transaction.field_70);
297            append_optional_field(&mut result, &transaction.field_77b);
298            append_optional_field(&mut result, &transaction.field_33b);
299            append_field(&mut result, &transaction.field_71a);
300            append_optional_field(&mut result, &transaction.field_25a);
301            append_optional_field(&mut result, &transaction.field_36);
302        }
303
304        finalize_mt_string(result, false)
305    }
306
307    // ========================================================================
308    // NETWORK VALIDATION RULES (SR 2025 MT101)
309    // ========================================================================
310
311    /// Field 23E valid instruction codes for MT101
312    const MT101_VALID_23E_CODES: &'static [&'static str] = &[
313        "CHQB", "CMSW", "CMTO", "CMZB", "CORT", "EQUI", "INTC", "NETS", "OTHR", "PHON", "REPA",
314        "RTGS", "URGP",
315    ];
316
317    /// Field 23E codes that allow additional information
318    const CODES_WITH_ADDITIONAL_INFO: &'static [&'static str] = &["CMTO", "PHON", "OTHR", "REPA"];
319
320    /// Field 23E invalid code combinations
321    const INVALID_23E_COMBINATIONS: &'static [(&'static str, &'static [&'static str])] = &[
322        (
323            "CHQB",
324            &[
325                "CMSW", "CMTO", "CMZB", "CORT", "NETS", "PHON", "REPA", "RTGS", "URGP",
326            ],
327        ),
328        ("CMSW", &["CMTO", "CMZB"]),
329        ("CMTO", &["CMZB"]),
330        ("CORT", &["CMSW", "CMTO", "CMZB", "REPA"]),
331        ("EQUI", &["CMSW", "CMTO", "CMZB"]),
332        ("NETS", &["RTGS"]),
333    ];
334
335    // ========================================================================
336    // HELPER METHODS
337    // ========================================================================
338
339    /// Check if ordering customer (F/G/H) is present in Sequence A
340    fn has_ordering_customer_in_seq_a(&self) -> bool {
341        self.ordering_customer.is_some()
342    }
343
344    /// Check if ordering customer (F/G/H) is present in all Sequence B transactions
345    fn has_ordering_customer_in_all_seq_b(&self) -> bool {
346        !self.transactions.is_empty()
347            && self
348                .transactions
349                .iter()
350                .all(|tx| tx.ordering_customer_tx.is_some())
351    }
352
353    /// Check if ordering customer (F/G/H) is present in any Sequence B transaction
354    fn has_ordering_customer_in_any_seq_b(&self) -> bool {
355        self.transactions
356            .iter()
357            .any(|tx| tx.ordering_customer_tx.is_some())
358    }
359
360    /// Check if instructing party (C/L) is present in Sequence A
361    fn has_instructing_party_in_seq_a(&self) -> bool {
362        self.instructing_party.is_some()
363    }
364
365    /// Check if instructing party (C/L) is present in any Sequence B transaction
366    fn has_instructing_party_in_any_seq_b(&self) -> bool {
367        self.transactions
368            .iter()
369            .any(|tx| tx.instructing_party_tx.is_some())
370    }
371
372    /// Check if account servicing institution is present in Sequence A
373    fn has_account_servicing_in_seq_a(&self) -> bool {
374        self.field_52a.is_some()
375    }
376
377    /// Check if account servicing institution is present in any Sequence B transaction
378    fn has_account_servicing_in_any_seq_b(&self) -> bool {
379        self.transactions.iter().any(|tx| tx.field_52.is_some())
380    }
381
382    // ========================================================================
383    // VALIDATION RULES (C1-C9)
384    // ========================================================================
385
386    /// C1: Exchange Rate and F/X Deal Reference (Error code: D54)
387    /// If field 36 is present, field 21F must be present
388    fn validate_c1_fx_deal_reference(&self) -> Vec<SwiftValidationError> {
389        let mut errors = Vec::new();
390
391        for (idx, transaction) in self.transactions.iter().enumerate() {
392            if transaction.field_36.is_some() && transaction.field_21f.is_none() {
393                errors.push(SwiftValidationError::content_error(
394                    "D54",
395                    "21F",
396                    "",
397                    &format!(
398                        "Transaction {}: Field 21F (F/X Deal Reference) is mandatory when field 36 (Exchange Rate) is present",
399                        idx + 1
400                    ),
401                    "If an exchange rate is given in field 36, the corresponding forex deal must be referenced in field 21F",
402                ));
403            }
404        }
405
406        errors
407    }
408
409    /// C2: Field 33B, 32B Amount, and Field 36 (Error code: D60)
410    /// Dependencies between fields 33B, 32B amount, and 36
411    fn validate_c2_amount_exchange(&self) -> Vec<SwiftValidationError> {
412        let mut errors = Vec::new();
413
414        for (idx, transaction) in self.transactions.iter().enumerate() {
415            if let Some(ref _field_33b) = transaction.field_33b {
416                // Check if amount in field_32b is zero
417                let amount_is_zero = transaction.field_32b.amount.abs() < 0.01;
418
419                if amount_is_zero {
420                    // Field 33B present AND 32B amount = 0 → field 36 NOT allowed
421                    if transaction.field_36.is_some() {
422                        errors.push(SwiftValidationError::content_error(
423                            "D60",
424                            "36",
425                            "",
426                            &format!(
427                                "Transaction {}: Field 36 (Exchange Rate) is not allowed when field 33B is present and amount in field 32B is zero",
428                                idx + 1
429                            ),
430                            "When field 33B is present and amount in field 32B equals zero, field 36 must not be present",
431                        ));
432                    }
433                } else {
434                    // Field 33B present AND 32B amount ≠ 0 → field 36 MANDATORY
435                    if transaction.field_36.is_none() {
436                        errors.push(SwiftValidationError::content_error(
437                            "D60",
438                            "36",
439                            "",
440                            &format!(
441                                "Transaction {}: Field 36 (Exchange Rate) is mandatory when field 33B is present and amount in field 32B is not zero",
442                                idx + 1
443                            ),
444                            "When field 33B is present and amount in field 32B is not equal to zero, field 36 must be present",
445                        ));
446                    }
447                }
448            } else {
449                // Field 33B NOT present → field 36 NOT allowed
450                if transaction.field_36.is_some() {
451                    errors.push(SwiftValidationError::content_error(
452                        "D60",
453                        "36",
454                        "",
455                        &format!(
456                            "Transaction {}: Field 36 (Exchange Rate) is not allowed when field 33B is not present",
457                            idx + 1
458                        ),
459                        "Field 36 is only allowed when field 33B is present",
460                    ));
461                }
462            }
463        }
464
465        errors
466    }
467
468    /// C3: Ordering Customer - Single vs Multiple Debit Accounts (Error code: D61)
469    /// Field 50a (F/G/H) must be in EITHER Seq A OR every Seq B, never both, never neither
470    fn validate_c3_ordering_customer(&self) -> Option<SwiftValidationError> {
471        let in_seq_a = self.has_ordering_customer_in_seq_a();
472        let in_all_seq_b = self.has_ordering_customer_in_all_seq_b();
473        let in_any_seq_b = self.has_ordering_customer_in_any_seq_b();
474
475        if in_seq_a && in_any_seq_b {
476            // Present in both sequences - NOT ALLOWED
477            return Some(SwiftValidationError::content_error(
478                "D61",
479                "50a",
480                "",
481                "Field 50a (Ordering Customer F/G/H) must not be present in both Sequence A and Sequence B",
482                "If single debit account, field 50a (F/G/H) must be in Sequence A only. If multiple debit accounts, field 50a (F/G/H) must be in every Sequence B transaction only",
483            ));
484        }
485
486        if !in_seq_a && !in_all_seq_b {
487            // Not present in Seq A, and not in all Seq B transactions
488            if in_any_seq_b {
489                // Present in some but not all Seq B transactions
490                return Some(SwiftValidationError::content_error(
491                    "D61",
492                    "50a",
493                    "",
494                    "Field 50a (Ordering Customer F/G/H) must be present in every Sequence B transaction when using multiple debit accounts",
495                    "When field 50a (F/G/H) is not in Sequence A, it must be present in every occurrence of Sequence B",
496                ));
497            } else {
498                // Not present anywhere - NOT ALLOWED
499                return Some(SwiftValidationError::content_error(
500                    "D61",
501                    "50a",
502                    "",
503                    "Field 50a (Ordering Customer F/G/H) must be present in either Sequence A or in every Sequence B transaction",
504                    "Field 50a (F/G/H) must be present in either Sequence A (single debit account) or in each occurrence of Sequence B (multiple debit accounts)",
505                ));
506            }
507        }
508
509        None
510    }
511
512    /// C4: Instructing Party Field (Error code: D62)
513    /// Field 50a (C/L) may be in Seq A OR Seq B, but not both
514    fn validate_c4_instructing_party(&self) -> Option<SwiftValidationError> {
515        let in_seq_a = self.has_instructing_party_in_seq_a();
516        let in_any_seq_b = self.has_instructing_party_in_any_seq_b();
517
518        if in_seq_a && in_any_seq_b {
519            return Some(SwiftValidationError::content_error(
520                "D62",
521                "50a",
522                "",
523                "Field 50a (Instructing Party C/L) must not be present in both Sequence A and Sequence B",
524                "Field 50a (C/L) may be present in either Sequence A or in one or more occurrences of Sequence B, but must not be present in both sequences",
525            ));
526        }
527
528        None
529    }
530
531    /// C5: Currency Codes in Fields 33B and 32B (Error code: D68)
532    /// If field 33B is present, its currency code must differ from field 32B currency code
533    fn validate_c5_currency_codes(&self) -> Vec<SwiftValidationError> {
534        let mut errors = Vec::new();
535
536        for (idx, transaction) in self.transactions.iter().enumerate() {
537            if let Some(ref field_33b) = transaction.field_33b {
538                let currency_32b = &transaction.field_32b.currency;
539                let currency_33b = &field_33b.currency;
540
541                if currency_32b == currency_33b {
542                    errors.push(SwiftValidationError::content_error(
543                        "D68",
544                        "33B",
545                        currency_33b,
546                        &format!(
547                            "Transaction {}: Currency code in field 33B ({}) must be different from currency code in field 32B ({})",
548                            idx + 1, currency_33b, currency_32b
549                        ),
550                        "When field 33B is present, its currency code must differ from the currency code in field 32B within the same transaction",
551                    ));
552                }
553            }
554        }
555
556        errors
557    }
558
559    /// C6: Account Servicing Institution Field (Error code: D64)
560    /// Field 52a may be in Seq A OR Seq B, but not both
561    fn validate_c6_account_servicing(&self) -> Option<SwiftValidationError> {
562        let in_seq_a = self.has_account_servicing_in_seq_a();
563        let in_any_seq_b = self.has_account_servicing_in_any_seq_b();
564
565        if in_seq_a && in_any_seq_b {
566            return Some(SwiftValidationError::content_error(
567                "D64",
568                "52a",
569                "",
570                "Field 52a (Account Servicing Institution) must not be present in both Sequence A and Sequence B",
571                "Field 52a may be present in either Sequence A or in one or more occurrences of Sequence B, but must not be present in both sequences",
572            ));
573        }
574
575        None
576    }
577
578    /// C7: Intermediary and Account With Institution (Error code: D65)
579    /// If field 56a is present, field 57a must also be present
580    fn validate_c7_intermediary(&self) -> Vec<SwiftValidationError> {
581        let mut errors = Vec::new();
582
583        for (idx, transaction) in self.transactions.iter().enumerate() {
584            if transaction.field_56.is_some() && transaction.field_57.is_none() {
585                errors.push(SwiftValidationError::content_error(
586                    "D65",
587                    "57a",
588                    "",
589                    &format!(
590                        "Transaction {}: Field 57a (Account With Institution) is mandatory when field 56a (Intermediary) is present",
591                        idx + 1
592                    ),
593                    "If field 56a is present, field 57a must also be present",
594                ));
595            }
596        }
597
598        errors
599    }
600
601    /// C8: Customer Specified Reference and Currency Consistency (Error code: D98)
602    /// If field 21R is present, all transactions must have the same currency in field 32B
603    fn validate_c8_currency_consistency(&self) -> Option<SwiftValidationError> {
604        self.field_21r.as_ref()?;
605
606        if self.transactions.is_empty() {
607            return None;
608        }
609
610        // Get the currency from the first transaction
611        let first_currency = &self.transactions[0].field_32b.currency;
612
613        // Check if all transactions have the same currency
614        for (idx, transaction) in self.transactions.iter().enumerate().skip(1) {
615            if &transaction.field_32b.currency != first_currency {
616                return Some(SwiftValidationError::content_error(
617                    "D98",
618                    "32B",
619                    &transaction.field_32b.currency,
620                    &format!(
621                        "Transaction {}: Currency code in field 32B ({}) must be the same as in other transactions ({}) when field 21R is present",
622                        idx + 1,
623                        transaction.field_32b.currency,
624                        first_currency
625                    ),
626                    "When field 21R is present in Sequence A, the currency code in field 32B must be the same in all occurrences of Sequence B",
627                ));
628            }
629        }
630
631        None
632    }
633
634    /// C9: Fields 33B, 21F, 32B and 23E Dependencies (Error code: E54)
635    /// Complex dependencies for zero-amount transactions
636    fn validate_c9_zero_amount(&self) -> Vec<SwiftValidationError> {
637        let mut errors = Vec::new();
638
639        for (idx, transaction) in self.transactions.iter().enumerate() {
640            // Check if amount in field_32b is zero
641            let amount_is_zero = transaction.field_32b.amount.abs() < 0.01;
642
643            if amount_is_zero {
644                // Check if field 23E has EQUI code
645                let has_equi = transaction
646                    .field_23e
647                    .as_ref()
648                    .is_some_and(|codes| codes.iter().any(|code| code.instruction_code == "EQUI"));
649
650                if has_equi {
651                    // Amount = 0 AND 23E = EQUI → 33B MANDATORY, 21F OPTIONAL
652                    if transaction.field_33b.is_none() {
653                        errors.push(SwiftValidationError::relation_error(
654                            "E54",
655                            "33B",
656                            vec!["32B".to_string(), "23E".to_string()],
657                            &format!(
658                                "Transaction {}: Field 33B is mandatory when amount in field 32B is zero and field 23E contains code EQUI",
659                                idx + 1
660                            ),
661                            "When amount in field 32B equals zero and field 23E is present with code EQUI, field 33B is mandatory",
662                        ));
663                    }
664                } else {
665                    // Amount = 0 AND (23E ≠ EQUI OR no 23E) → 33B and 21F NOT ALLOWED
666                    if transaction.field_33b.is_some() {
667                        errors.push(SwiftValidationError::relation_error(
668                            "E54",
669                            "33B",
670                            vec!["32B".to_string(), "23E".to_string()],
671                            &format!(
672                                "Transaction {}: Field 33B is not allowed when amount in field 32B is zero and field 23E does not contain code EQUI",
673                                idx + 1
674                            ),
675                            "When amount in field 32B equals zero and field 23E is not present or does not contain code EQUI, field 33B must not be present",
676                        ));
677                    }
678                    if transaction.field_21f.is_some() {
679                        errors.push(SwiftValidationError::relation_error(
680                            "E54",
681                            "21F",
682                            vec!["32B".to_string(), "23E".to_string()],
683                            &format!(
684                                "Transaction {}: Field 21F is not allowed when amount in field 32B is zero and field 23E does not contain code EQUI",
685                                idx + 1
686                            ),
687                            "When amount in field 32B equals zero and field 23E is not present or does not contain code EQUI, field 21F must not be present",
688                        ));
689                    }
690                }
691            }
692        }
693
694        errors
695    }
696
697    /// Validate Field 23E instruction codes (Error codes: T47, D66, D67, E46)
698    /// Complex validation for instruction code combinations and restrictions
699    fn validate_field_23e(&self) -> Vec<SwiftValidationError> {
700        let mut errors = Vec::new();
701
702        for (idx, transaction) in self.transactions.iter().enumerate() {
703            if let Some(ref field_23e_vec) = transaction.field_23e {
704                let mut seen_codes = HashSet::new();
705
706                for field_23e in field_23e_vec {
707                    let code = &field_23e.instruction_code;
708
709                    // T47: Validate instruction code is in allowed list
710                    if !Self::MT101_VALID_23E_CODES.contains(&code.as_str()) {
711                        errors.push(SwiftValidationError::format_error(
712                            "T47",
713                            "23E",
714                            code,
715                            &format!("One of: {}", Self::MT101_VALID_23E_CODES.join(", ")),
716                            &format!(
717                                "Transaction {}: Instruction code '{}' is not valid for MT101. Valid codes: {}",
718                                idx + 1,
719                                code,
720                                Self::MT101_VALID_23E_CODES.join(", ")
721                            ),
722                        ));
723                    }
724
725                    // D66: Additional information only allowed for specific codes
726                    if field_23e.additional_info.is_some()
727                        && !Self::CODES_WITH_ADDITIONAL_INFO.contains(&code.as_str())
728                    {
729                        errors.push(SwiftValidationError::content_error(
730                            "D66",
731                            "23E",
732                            code,
733                            &format!(
734                                "Transaction {}: Additional information is only allowed for codes: {}. Code '{}' does not allow additional information",
735                                idx + 1,
736                                Self::CODES_WITH_ADDITIONAL_INFO.join(", "),
737                                code
738                            ),
739                            "Additional information in field 23E is only allowed for codes: CMTO, PHON, OTHR, REPA",
740                        ));
741                    }
742
743                    // E46: Same code must not be present more than once (except OTHR)
744                    if code != "OTHR" {
745                        if seen_codes.contains(code) {
746                            errors.push(SwiftValidationError::relation_error(
747                                "E46",
748                                "23E",
749                                vec![],
750                                &format!(
751                                    "Transaction {}: Instruction code '{}' appears more than once. Same code must not be repeated except OTHR",
752                                    idx + 1, code
753                                ),
754                                "When field 23E is repeated in Sequence B, the same code must not be present more than once, except for code OTHR which may be repeated",
755                            ));
756                        }
757                        seen_codes.insert(code.clone());
758                    }
759                }
760
761                // D67: Check for invalid combinations
762                for field_23e in field_23e_vec {
763                    let code = &field_23e.instruction_code;
764
765                    for &(base_code, forbidden_codes) in Self::INVALID_23E_COMBINATIONS {
766                        if code == base_code {
767                            // Check if any forbidden code is present
768                            for other_field in field_23e_vec {
769                                let other_code = &other_field.instruction_code;
770                                if forbidden_codes.contains(&other_code.as_str()) {
771                                    errors.push(SwiftValidationError::content_error(
772                                        "D67",
773                                        "23E",
774                                        code,
775                                        &format!(
776                                            "Transaction {}: Instruction code '{}' cannot be combined with code '{}'. Invalid combination",
777                                            idx + 1, code, other_code
778                                        ),
779                                        &format!(
780                                            "Code '{}' cannot be combined with: {}",
781                                            base_code,
782                                            forbidden_codes.join(", ")
783                                        ),
784                                    ));
785                                }
786                            }
787                        }
788                    }
789                }
790            }
791        }
792
793        errors
794    }
795
796    /// Main validation method - validates all network rules
797    /// Returns array of validation errors, respects stop_on_first_error flag
798    pub fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
799        let mut all_errors = Vec::new();
800
801        // C1: Exchange Rate and F/X Deal Reference
802        let c1_errors = self.validate_c1_fx_deal_reference();
803        all_errors.extend(c1_errors);
804        if stop_on_first_error && !all_errors.is_empty() {
805            return all_errors;
806        }
807
808        // C2: Field 33B, 32B, and 36 Dependencies
809        let c2_errors = self.validate_c2_amount_exchange();
810        all_errors.extend(c2_errors);
811        if stop_on_first_error && !all_errors.is_empty() {
812            return all_errors;
813        }
814
815        // C3: Ordering Customer Placement
816        if let Some(error) = self.validate_c3_ordering_customer() {
817            all_errors.push(error);
818            if stop_on_first_error {
819                return all_errors;
820            }
821        }
822
823        // C4: Instructing Party Placement
824        if let Some(error) = self.validate_c4_instructing_party() {
825            all_errors.push(error);
826            if stop_on_first_error {
827                return all_errors;
828            }
829        }
830
831        // C5: Currency Code Mismatch
832        let c5_errors = self.validate_c5_currency_codes();
833        all_errors.extend(c5_errors);
834        if stop_on_first_error && !all_errors.is_empty() {
835            return all_errors;
836        }
837
838        // C6: Account Servicing Institution
839        if let Some(error) = self.validate_c6_account_servicing() {
840            all_errors.push(error);
841            if stop_on_first_error {
842                return all_errors;
843            }
844        }
845
846        // C7: Intermediary & Account With
847        let c7_errors = self.validate_c7_intermediary();
848        all_errors.extend(c7_errors);
849        if stop_on_first_error && !all_errors.is_empty() {
850            return all_errors;
851        }
852
853        // C8: Currency Consistency
854        if let Some(error) = self.validate_c8_currency_consistency() {
855            all_errors.push(error);
856            if stop_on_first_error {
857                return all_errors;
858            }
859        }
860
861        // C9: Zero Amount Dependencies
862        let c9_errors = self.validate_c9_zero_amount();
863        all_errors.extend(c9_errors);
864        if stop_on_first_error && !all_errors.is_empty() {
865            return all_errors;
866        }
867
868        // Field 23E Validation
869        let f23e_errors = self.validate_field_23e();
870        all_errors.extend(f23e_errors);
871
872        all_errors
873    }
874}
875
876impl crate::traits::SwiftMessageBody for MT101 {
877    fn message_type() -> &'static str {
878        "101"
879    }
880
881    fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
882        // Call the existing public method implementation
883        MT101::parse_from_block4(block4)
884    }
885
886    fn to_mt_string(&self) -> String {
887        // Call the existing public method implementation
888        MT101::to_mt_string(self)
889    }
890
891    fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
892        // Call the existing public method implementation
893        MT101::validate_network_rules(self, stop_on_first_error)
894    }
895}