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        result.push('-');
305        result
306    }
307
308    // ========================================================================
309    // NETWORK VALIDATION RULES (SR 2025 MT101)
310    // ========================================================================
311
312    /// Field 23E valid instruction codes for MT101
313    const MT101_VALID_23E_CODES: &'static [&'static str] = &[
314        "CHQB", "CMSW", "CMTO", "CMZB", "CORT", "EQUI", "INTC", "NETS", "OTHR", "PHON", "REPA",
315        "RTGS", "URGP",
316    ];
317
318    /// Field 23E codes that allow additional information
319    const CODES_WITH_ADDITIONAL_INFO: &'static [&'static str] = &["CMTO", "PHON", "OTHR", "REPA"];
320
321    /// Field 23E invalid code combinations
322    const INVALID_23E_COMBINATIONS: &'static [(&'static str, &'static [&'static str])] = &[
323        (
324            "CHQB",
325            &[
326                "CMSW", "CMTO", "CMZB", "CORT", "NETS", "PHON", "REPA", "RTGS", "URGP",
327            ],
328        ),
329        ("CMSW", &["CMTO", "CMZB"]),
330        ("CMTO", &["CMZB"]),
331        ("CORT", &["CMSW", "CMTO", "CMZB", "REPA"]),
332        ("EQUI", &["CMSW", "CMTO", "CMZB"]),
333        ("NETS", &["RTGS"]),
334    ];
335
336    // ========================================================================
337    // HELPER METHODS
338    // ========================================================================
339
340    /// Check if ordering customer (F/G/H) is present in Sequence A
341    fn has_ordering_customer_in_seq_a(&self) -> bool {
342        self.ordering_customer.is_some()
343    }
344
345    /// Check if ordering customer (F/G/H) is present in all Sequence B transactions
346    fn has_ordering_customer_in_all_seq_b(&self) -> bool {
347        !self.transactions.is_empty()
348            && self
349                .transactions
350                .iter()
351                .all(|tx| tx.ordering_customer_tx.is_some())
352    }
353
354    /// Check if ordering customer (F/G/H) is present in any Sequence B transaction
355    fn has_ordering_customer_in_any_seq_b(&self) -> bool {
356        self.transactions
357            .iter()
358            .any(|tx| tx.ordering_customer_tx.is_some())
359    }
360
361    /// Check if instructing party (C/L) is present in Sequence A
362    fn has_instructing_party_in_seq_a(&self) -> bool {
363        self.instructing_party.is_some()
364    }
365
366    /// Check if instructing party (C/L) is present in any Sequence B transaction
367    fn has_instructing_party_in_any_seq_b(&self) -> bool {
368        self.transactions
369            .iter()
370            .any(|tx| tx.instructing_party_tx.is_some())
371    }
372
373    /// Check if account servicing institution is present in Sequence A
374    fn has_account_servicing_in_seq_a(&self) -> bool {
375        self.field_52a.is_some()
376    }
377
378    /// Check if account servicing institution is present in any Sequence B transaction
379    fn has_account_servicing_in_any_seq_b(&self) -> bool {
380        self.transactions.iter().any(|tx| tx.field_52.is_some())
381    }
382
383    // ========================================================================
384    // VALIDATION RULES (C1-C9)
385    // ========================================================================
386
387    /// C1: Exchange Rate and F/X Deal Reference (Error code: D54)
388    /// If field 36 is present, field 21F must be present
389    fn validate_c1_fx_deal_reference(&self) -> Vec<SwiftValidationError> {
390        let mut errors = Vec::new();
391
392        for (idx, transaction) in self.transactions.iter().enumerate() {
393            if transaction.field_36.is_some() && transaction.field_21f.is_none() {
394                errors.push(SwiftValidationError::content_error(
395                    "D54",
396                    "21F",
397                    "",
398                    &format!(
399                        "Transaction {}: Field 21F (F/X Deal Reference) is mandatory when field 36 (Exchange Rate) is present",
400                        idx + 1
401                    ),
402                    "If an exchange rate is given in field 36, the corresponding forex deal must be referenced in field 21F",
403                ));
404            }
405        }
406
407        errors
408    }
409
410    /// C2: Field 33B, 32B Amount, and Field 36 (Error code: D60)
411    /// Dependencies between fields 33B, 32B amount, and 36
412    fn validate_c2_amount_exchange(&self) -> Vec<SwiftValidationError> {
413        let mut errors = Vec::new();
414
415        for (idx, transaction) in self.transactions.iter().enumerate() {
416            if let Some(ref _field_33b) = transaction.field_33b {
417                // Check if amount in field_32b is zero
418                let amount_is_zero = transaction.field_32b.amount.abs() < 0.01;
419
420                if amount_is_zero {
421                    // Field 33B present AND 32B amount = 0 → field 36 NOT allowed
422                    if transaction.field_36.is_some() {
423                        errors.push(SwiftValidationError::content_error(
424                            "D60",
425                            "36",
426                            "",
427                            &format!(
428                                "Transaction {}: Field 36 (Exchange Rate) is not allowed when field 33B is present and amount in field 32B is zero",
429                                idx + 1
430                            ),
431                            "When field 33B is present and amount in field 32B equals zero, field 36 must not be present",
432                        ));
433                    }
434                } else {
435                    // Field 33B present AND 32B amount ≠ 0 → field 36 MANDATORY
436                    if transaction.field_36.is_none() {
437                        errors.push(SwiftValidationError::content_error(
438                            "D60",
439                            "36",
440                            "",
441                            &format!(
442                                "Transaction {}: Field 36 (Exchange Rate) is mandatory when field 33B is present and amount in field 32B is not zero",
443                                idx + 1
444                            ),
445                            "When field 33B is present and amount in field 32B is not equal to zero, field 36 must be present",
446                        ));
447                    }
448                }
449            } else {
450                // Field 33B NOT present → field 36 NOT allowed
451                if transaction.field_36.is_some() {
452                    errors.push(SwiftValidationError::content_error(
453                        "D60",
454                        "36",
455                        "",
456                        &format!(
457                            "Transaction {}: Field 36 (Exchange Rate) is not allowed when field 33B is not present",
458                            idx + 1
459                        ),
460                        "Field 36 is only allowed when field 33B is present",
461                    ));
462                }
463            }
464        }
465
466        errors
467    }
468
469    /// C3: Ordering Customer - Single vs Multiple Debit Accounts (Error code: D61)
470    /// Field 50a (F/G/H) must be in EITHER Seq A OR every Seq B, never both, never neither
471    fn validate_c3_ordering_customer(&self) -> Option<SwiftValidationError> {
472        let in_seq_a = self.has_ordering_customer_in_seq_a();
473        let in_all_seq_b = self.has_ordering_customer_in_all_seq_b();
474        let in_any_seq_b = self.has_ordering_customer_in_any_seq_b();
475
476        if in_seq_a && in_any_seq_b {
477            // Present in both sequences - NOT ALLOWED
478            return Some(SwiftValidationError::content_error(
479                "D61",
480                "50a",
481                "",
482                "Field 50a (Ordering Customer F/G/H) must not be present in both Sequence A and Sequence B",
483                "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",
484            ));
485        }
486
487        if !in_seq_a && !in_all_seq_b {
488            // Not present in Seq A, and not in all Seq B transactions
489            if in_any_seq_b {
490                // Present in some but not all Seq B transactions
491                return Some(SwiftValidationError::content_error(
492                    "D61",
493                    "50a",
494                    "",
495                    "Field 50a (Ordering Customer F/G/H) must be present in every Sequence B transaction when using multiple debit accounts",
496                    "When field 50a (F/G/H) is not in Sequence A, it must be present in every occurrence of Sequence B",
497                ));
498            } else {
499                // Not present anywhere - NOT ALLOWED
500                return Some(SwiftValidationError::content_error(
501                    "D61",
502                    "50a",
503                    "",
504                    "Field 50a (Ordering Customer F/G/H) must be present in either Sequence A or in every Sequence B transaction",
505                    "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)",
506                ));
507            }
508        }
509
510        None
511    }
512
513    /// C4: Instructing Party Field (Error code: D62)
514    /// Field 50a (C/L) may be in Seq A OR Seq B, but not both
515    fn validate_c4_instructing_party(&self) -> Option<SwiftValidationError> {
516        let in_seq_a = self.has_instructing_party_in_seq_a();
517        let in_any_seq_b = self.has_instructing_party_in_any_seq_b();
518
519        if in_seq_a && in_any_seq_b {
520            return Some(SwiftValidationError::content_error(
521                "D62",
522                "50a",
523                "",
524                "Field 50a (Instructing Party C/L) must not be present in both Sequence A and Sequence B",
525                "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",
526            ));
527        }
528
529        None
530    }
531
532    /// C5: Currency Codes in Fields 33B and 32B (Error code: D68)
533    /// If field 33B is present, its currency code must differ from field 32B currency code
534    fn validate_c5_currency_codes(&self) -> Vec<SwiftValidationError> {
535        let mut errors = Vec::new();
536
537        for (idx, transaction) in self.transactions.iter().enumerate() {
538            if let Some(ref field_33b) = transaction.field_33b {
539                let currency_32b = &transaction.field_32b.currency;
540                let currency_33b = &field_33b.currency;
541
542                if currency_32b == currency_33b {
543                    errors.push(SwiftValidationError::content_error(
544                        "D68",
545                        "33B",
546                        currency_33b,
547                        &format!(
548                            "Transaction {}: Currency code in field 33B ({}) must be different from currency code in field 32B ({})",
549                            idx + 1, currency_33b, currency_32b
550                        ),
551                        "When field 33B is present, its currency code must differ from the currency code in field 32B within the same transaction",
552                    ));
553                }
554            }
555        }
556
557        errors
558    }
559
560    /// C6: Account Servicing Institution Field (Error code: D64)
561    /// Field 52a may be in Seq A OR Seq B, but not both
562    fn validate_c6_account_servicing(&self) -> Option<SwiftValidationError> {
563        let in_seq_a = self.has_account_servicing_in_seq_a();
564        let in_any_seq_b = self.has_account_servicing_in_any_seq_b();
565
566        if in_seq_a && in_any_seq_b {
567            return Some(SwiftValidationError::content_error(
568                "D64",
569                "52a",
570                "",
571                "Field 52a (Account Servicing Institution) must not be present in both Sequence A and Sequence B",
572                "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",
573            ));
574        }
575
576        None
577    }
578
579    /// C7: Intermediary and Account With Institution (Error code: D65)
580    /// If field 56a is present, field 57a must also be present
581    fn validate_c7_intermediary(&self) -> Vec<SwiftValidationError> {
582        let mut errors = Vec::new();
583
584        for (idx, transaction) in self.transactions.iter().enumerate() {
585            if transaction.field_56.is_some() && transaction.field_57.is_none() {
586                errors.push(SwiftValidationError::content_error(
587                    "D65",
588                    "57a",
589                    "",
590                    &format!(
591                        "Transaction {}: Field 57a (Account With Institution) is mandatory when field 56a (Intermediary) is present",
592                        idx + 1
593                    ),
594                    "If field 56a is present, field 57a must also be present",
595                ));
596            }
597        }
598
599        errors
600    }
601
602    /// C8: Customer Specified Reference and Currency Consistency (Error code: D98)
603    /// If field 21R is present, all transactions must have the same currency in field 32B
604    fn validate_c8_currency_consistency(&self) -> Option<SwiftValidationError> {
605        self.field_21r.as_ref()?;
606
607        if self.transactions.is_empty() {
608            return None;
609        }
610
611        // Get the currency from the first transaction
612        let first_currency = &self.transactions[0].field_32b.currency;
613
614        // Check if all transactions have the same currency
615        for (idx, transaction) in self.transactions.iter().enumerate().skip(1) {
616            if &transaction.field_32b.currency != first_currency {
617                return Some(SwiftValidationError::content_error(
618                    "D98",
619                    "32B",
620                    &transaction.field_32b.currency,
621                    &format!(
622                        "Transaction {}: Currency code in field 32B ({}) must be the same as in other transactions ({}) when field 21R is present",
623                        idx + 1,
624                        transaction.field_32b.currency,
625                        first_currency
626                    ),
627                    "When field 21R is present in Sequence A, the currency code in field 32B must be the same in all occurrences of Sequence B",
628                ));
629            }
630        }
631
632        None
633    }
634
635    /// C9: Fields 33B, 21F, 32B and 23E Dependencies (Error code: E54)
636    /// Complex dependencies for zero-amount transactions
637    fn validate_c9_zero_amount(&self) -> Vec<SwiftValidationError> {
638        let mut errors = Vec::new();
639
640        for (idx, transaction) in self.transactions.iter().enumerate() {
641            // Check if amount in field_32b is zero
642            let amount_is_zero = transaction.field_32b.amount.abs() < 0.01;
643
644            if amount_is_zero {
645                // Check if field 23E has EQUI code
646                let has_equi = transaction
647                    .field_23e
648                    .as_ref()
649                    .is_some_and(|codes| codes.iter().any(|code| code.instruction_code == "EQUI"));
650
651                if has_equi {
652                    // Amount = 0 AND 23E = EQUI → 33B MANDATORY, 21F OPTIONAL
653                    if transaction.field_33b.is_none() {
654                        errors.push(SwiftValidationError::relation_error(
655                            "E54",
656                            "33B",
657                            vec!["32B".to_string(), "23E".to_string()],
658                            &format!(
659                                "Transaction {}: Field 33B is mandatory when amount in field 32B is zero and field 23E contains code EQUI",
660                                idx + 1
661                            ),
662                            "When amount in field 32B equals zero and field 23E is present with code EQUI, field 33B is mandatory",
663                        ));
664                    }
665                } else {
666                    // Amount = 0 AND (23E ≠ EQUI OR no 23E) → 33B and 21F NOT ALLOWED
667                    if transaction.field_33b.is_some() {
668                        errors.push(SwiftValidationError::relation_error(
669                            "E54",
670                            "33B",
671                            vec!["32B".to_string(), "23E".to_string()],
672                            &format!(
673                                "Transaction {}: Field 33B is not allowed when amount in field 32B is zero and field 23E does not contain code EQUI",
674                                idx + 1
675                            ),
676                            "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",
677                        ));
678                    }
679                    if transaction.field_21f.is_some() {
680                        errors.push(SwiftValidationError::relation_error(
681                            "E54",
682                            "21F",
683                            vec!["32B".to_string(), "23E".to_string()],
684                            &format!(
685                                "Transaction {}: Field 21F is not allowed when amount in field 32B is zero and field 23E does not contain code EQUI",
686                                idx + 1
687                            ),
688                            "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",
689                        ));
690                    }
691                }
692            }
693        }
694
695        errors
696    }
697
698    /// Validate Field 23E instruction codes (Error codes: T47, D66, D67, E46)
699    /// Complex validation for instruction code combinations and restrictions
700    fn validate_field_23e(&self) -> Vec<SwiftValidationError> {
701        let mut errors = Vec::new();
702
703        for (idx, transaction) in self.transactions.iter().enumerate() {
704            if let Some(ref field_23e_vec) = transaction.field_23e {
705                let mut seen_codes = HashSet::new();
706
707                for field_23e in field_23e_vec {
708                    let code = &field_23e.instruction_code;
709
710                    // T47: Validate instruction code is in allowed list
711                    if !Self::MT101_VALID_23E_CODES.contains(&code.as_str()) {
712                        errors.push(SwiftValidationError::format_error(
713                            "T47",
714                            "23E",
715                            code,
716                            &format!("One of: {}", Self::MT101_VALID_23E_CODES.join(", ")),
717                            &format!(
718                                "Transaction {}: Instruction code '{}' is not valid for MT101. Valid codes: {}",
719                                idx + 1,
720                                code,
721                                Self::MT101_VALID_23E_CODES.join(", ")
722                            ),
723                        ));
724                    }
725
726                    // D66: Additional information only allowed for specific codes
727                    if field_23e.additional_info.is_some()
728                        && !Self::CODES_WITH_ADDITIONAL_INFO.contains(&code.as_str())
729                    {
730                        errors.push(SwiftValidationError::content_error(
731                            "D66",
732                            "23E",
733                            code,
734                            &format!(
735                                "Transaction {}: Additional information is only allowed for codes: {}. Code '{}' does not allow additional information",
736                                idx + 1,
737                                Self::CODES_WITH_ADDITIONAL_INFO.join(", "),
738                                code
739                            ),
740                            "Additional information in field 23E is only allowed for codes: CMTO, PHON, OTHR, REPA",
741                        ));
742                    }
743
744                    // E46: Same code must not be present more than once (except OTHR)
745                    if code != "OTHR" {
746                        if seen_codes.contains(code) {
747                            errors.push(SwiftValidationError::relation_error(
748                                "E46",
749                                "23E",
750                                vec![],
751                                &format!(
752                                    "Transaction {}: Instruction code '{}' appears more than once. Same code must not be repeated except OTHR",
753                                    idx + 1, code
754                                ),
755                                "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",
756                            ));
757                        }
758                        seen_codes.insert(code.clone());
759                    }
760                }
761
762                // D67: Check for invalid combinations
763                for field_23e in field_23e_vec {
764                    let code = &field_23e.instruction_code;
765
766                    for &(base_code, forbidden_codes) in Self::INVALID_23E_COMBINATIONS {
767                        if code == base_code {
768                            // Check if any forbidden code is present
769                            for other_field in field_23e_vec {
770                                let other_code = &other_field.instruction_code;
771                                if forbidden_codes.contains(&other_code.as_str()) {
772                                    errors.push(SwiftValidationError::content_error(
773                                        "D67",
774                                        "23E",
775                                        code,
776                                        &format!(
777                                            "Transaction {}: Instruction code '{}' cannot be combined with code '{}'. Invalid combination",
778                                            idx + 1, code, other_code
779                                        ),
780                                        &format!(
781                                            "Code '{}' cannot be combined with: {}",
782                                            base_code,
783                                            forbidden_codes.join(", ")
784                                        ),
785                                    ));
786                                }
787                            }
788                        }
789                    }
790                }
791            }
792        }
793
794        errors
795    }
796
797    /// Main validation method - validates all network rules
798    /// Returns array of validation errors, respects stop_on_first_error flag
799    pub fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
800        let mut all_errors = Vec::new();
801
802        // C1: Exchange Rate and F/X Deal Reference
803        let c1_errors = self.validate_c1_fx_deal_reference();
804        all_errors.extend(c1_errors);
805        if stop_on_first_error && !all_errors.is_empty() {
806            return all_errors;
807        }
808
809        // C2: Field 33B, 32B, and 36 Dependencies
810        let c2_errors = self.validate_c2_amount_exchange();
811        all_errors.extend(c2_errors);
812        if stop_on_first_error && !all_errors.is_empty() {
813            return all_errors;
814        }
815
816        // C3: Ordering Customer Placement
817        if let Some(error) = self.validate_c3_ordering_customer() {
818            all_errors.push(error);
819            if stop_on_first_error {
820                return all_errors;
821            }
822        }
823
824        // C4: Instructing Party Placement
825        if let Some(error) = self.validate_c4_instructing_party() {
826            all_errors.push(error);
827            if stop_on_first_error {
828                return all_errors;
829            }
830        }
831
832        // C5: Currency Code Mismatch
833        let c5_errors = self.validate_c5_currency_codes();
834        all_errors.extend(c5_errors);
835        if stop_on_first_error && !all_errors.is_empty() {
836            return all_errors;
837        }
838
839        // C6: Account Servicing Institution
840        if let Some(error) = self.validate_c6_account_servicing() {
841            all_errors.push(error);
842            if stop_on_first_error {
843                return all_errors;
844            }
845        }
846
847        // C7: Intermediary & Account With
848        let c7_errors = self.validate_c7_intermediary();
849        all_errors.extend(c7_errors);
850        if stop_on_first_error && !all_errors.is_empty() {
851            return all_errors;
852        }
853
854        // C8: Currency Consistency
855        if let Some(error) = self.validate_c8_currency_consistency() {
856            all_errors.push(error);
857            if stop_on_first_error {
858                return all_errors;
859            }
860        }
861
862        // C9: Zero Amount Dependencies
863        let c9_errors = self.validate_c9_zero_amount();
864        all_errors.extend(c9_errors);
865        if stop_on_first_error && !all_errors.is_empty() {
866            return all_errors;
867        }
868
869        // Field 23E Validation
870        let f23e_errors = self.validate_field_23e();
871        all_errors.extend(f23e_errors);
872
873        all_errors
874    }
875}
876
877impl crate::traits::SwiftMessageBody for MT101 {
878    fn message_type() -> &'static str {
879        "101"
880    }
881
882    fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
883        // Call the existing public method implementation
884        MT101::parse_from_block4(block4)
885    }
886
887    fn to_mt_string(&self) -> String {
888        // Call the existing public method implementation
889        MT101::to_mt_string(self)
890    }
891
892    fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
893        // Call the existing public method implementation
894        MT101::validate_network_rules(self, stop_on_first_error)
895    }
896}