swift_mt_message/messages/
mt103.rs

1use crate::errors::SwiftValidationError;
2use crate::fields::*;
3use crate::parser::utils::*;
4use std::collections::HashSet;
5
6use serde::{Deserialize, Serialize};
7
8/// **MT103: Single Customer Credit Transfer**
9///
10/// Customer payment instruction from ordering to beneficiary customer via financial institutions.
11/// Most common SWIFT payment message for cross-border transfers.
12///
13/// **Usage:** Customer credit transfers, STP payments
14/// **Category:** Category 1 (Customer Payments)
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub struct MT103 {
17    /// Transaction reference (Field 20)
18    #[serde(rename = "20")]
19    pub field_20: Field20,
20
21    /// Bank operation code (Field 23B)
22    #[serde(rename = "23B")]
23    pub field_23b: Field23B,
24
25    /// Value date, currency, amount (Field 32A)
26    #[serde(rename = "32A")]
27    pub field_32a: Field32A,
28
29    /// Ordering customer (Field 50)
30    #[serde(flatten)]
31    pub field_50: Field50OrderingCustomerAFK,
32
33    /// Beneficiary customer (Field 59)
34    #[serde(flatten)]
35    pub field_59: Field59,
36
37    /// Details of charges (Field 71A)
38    #[serde(rename = "71A")]
39    pub field_71a: Field71A,
40
41    /// Time indication (Field 13C)
42    #[serde(rename = "13C")]
43    pub field_13c: Option<Vec<Field13C>>,
44
45    /// Instruction codes (Field 23E)
46    #[serde(rename = "23E")]
47    pub field_23e: Option<Vec<Field23E>>,
48
49    /// Transaction type code (Field 26T)
50    #[serde(rename = "26T")]
51    pub field_26t: Option<Field26T>,
52
53    /// Instructed amount (Field 33B)
54    #[serde(rename = "33B")]
55    pub field_33b: Option<Field33B>,
56
57    /// Exchange rate (Field 36)
58    #[serde(rename = "36")]
59    pub field_36: Option<Field36>,
60
61    /// Instructing institution (Field 51A)
62    #[serde(rename = "51A")]
63    pub field_51a: Option<Field51A>,
64
65    /// Ordering institution (Field 52)
66    #[serde(flatten)]
67    pub field_52: Option<Field52OrderingInstitution>,
68
69    /// Sender's correspondent (Field 53)
70    #[serde(flatten)]
71    pub field_53: Option<Field53SenderCorrespondent>,
72
73    /// Receiver's correspondent (Field 54)
74    #[serde(flatten)]
75    pub field_54: Option<Field54ReceiverCorrespondent>,
76
77    /// Third reimbursement institution (Field 55)
78    #[serde(flatten)]
79    pub field_55: Option<Field55ThirdReimbursementInstitution>,
80
81    /// Intermediary institution (Field 56)
82    #[serde(flatten)]
83    pub field_56: Option<Field56Intermediary>,
84
85    /// Account with institution (Field 57)
86    #[serde(flatten)]
87    pub field_57: Option<Field57AccountWithInstitution>,
88
89    /// Remittance information (Field 70)
90    #[serde(rename = "70")]
91    pub field_70: Option<Field70>,
92
93    /// Sender's charges (Field 71F)
94    #[serde(rename = "71F")]
95    pub field_71f: Option<Vec<Field71F>>,
96
97    /// Receiver's charges (Field 71G)
98    #[serde(rename = "71G")]
99    pub field_71g: Option<Field71G>,
100
101    /// Sender to receiver information (Field 72)
102    #[serde(rename = "72")]
103    pub field_72: Option<Field72>,
104
105    /// Regulatory reporting (Field 77B)
106    #[serde(rename = "77B")]
107    pub field_77b: Option<Field77B>,
108
109    /// Envelope contents (Field 77T)
110    #[serde(rename = "77T")]
111    pub field_77t: Option<Field77T>,
112}
113
114// Additional methods for MT103
115impl MT103 {
116    /// Parse from SWIFT MT text format
117    pub fn parse(input: &str) -> Result<Self, crate::errors::ParseError> {
118        let block4 = extract_block4(input)?;
119        <Self as crate::traits::SwiftMessageBody>::parse_from_block4(&block4)
120    }
121
122    /// Convert to SWIFT MT text format
123    pub fn to_mt_string(&self) -> String {
124        let mut result = String::new();
125
126        // Add mandatory fields in order
127        append_field(&mut result, &self.field_20);
128        append_vec_field(&mut result, &self.field_13c);
129        append_field(&mut result, &self.field_23b);
130        append_vec_field(&mut result, &self.field_23e);
131        append_optional_field(&mut result, &self.field_26t);
132        append_field(&mut result, &self.field_32a);
133        append_optional_field(&mut result, &self.field_33b);
134        append_optional_field(&mut result, &self.field_36);
135        append_field(&mut result, &self.field_50);
136        append_optional_field(&mut result, &self.field_51a);
137        append_optional_field(&mut result, &self.field_52);
138        append_optional_field(&mut result, &self.field_53);
139        append_optional_field(&mut result, &self.field_54);
140        append_optional_field(&mut result, &self.field_55);
141        append_optional_field(&mut result, &self.field_56);
142        append_optional_field(&mut result, &self.field_57);
143        append_field(&mut result, &self.field_59);
144        append_optional_field(&mut result, &self.field_70);
145        append_field(&mut result, &self.field_71a);
146        append_vec_field(&mut result, &self.field_71f);
147        append_optional_field(&mut result, &self.field_71g);
148        append_optional_field(&mut result, &self.field_72);
149        append_optional_field(&mut result, &self.field_77b);
150        append_optional_field(&mut result, &self.field_77t);
151
152        result.push('-');
153        result
154    }
155
156    /// Check if this MT103 message contains reject codes
157    pub fn has_reject_codes(&self) -> bool {
158        // Check field 72 for reject codes like /REJT/
159        if let Some(ref field_72) = self.field_72 {
160            for line in &field_72.information {
161                if line.contains("/REJT/") || line.contains("/RETN/") {
162                    return true;
163                }
164            }
165        }
166        false
167    }
168
169    /// Check if this MT103 message contains return codes
170    pub fn has_return_codes(&self) -> bool {
171        // Check field 72 for return codes
172        if let Some(ref field_72) = self.field_72 {
173            for line in &field_72.information {
174                if line.contains("/RETN/") {
175                    return true;
176                }
177            }
178        }
179        false
180    }
181
182    /// Check if this MT103 message is STP compliant
183    pub fn is_stp_compliant(&self) -> bool {
184        // Check if this is an STP message (SPRI, SSTD, or SPAY)
185        let bank_op_code = &self.field_23b.instruction_code;
186        if !["SPRI", "SSTD", "SPAY"].contains(&bank_op_code.as_str()) {
187            // Not an STP message type, so it's compliant by default
188            return true;
189        }
190
191        // C3: If 23B is SPRI, field 23E may contain only SDVA, TELB, PHOB, INTC
192        // If 23B is SSTD or SPAY, field 23E must not be used
193        if bank_op_code == "SPRI" {
194            if let Some(ref field_23e_vec) = self.field_23e {
195                let allowed_codes = ["SDVA", "TELB", "PHOB", "INTC"];
196                for field_23e in field_23e_vec {
197                    if !allowed_codes.contains(&field_23e.instruction_code.as_str()) {
198                        return false;
199                    }
200                }
201            }
202        } else if ["SSTD", "SPAY"].contains(&bank_op_code.as_str()) && self.field_23e.is_some() {
203            return false;
204        }
205
206        // C10: If 23B is SPRI, field 56 is not allowed
207        // If 23B is SSTD or SPAY, field 56 may be present but only option A or C
208        if bank_op_code == "SPRI" && self.field_56.is_some() {
209            return false;
210        }
211
212        // Additional STP validation rules could be added here
213        // For now, return true if basic checks pass
214        true
215    }
216
217    // ========================================================================
218    // NETWORK VALIDATION RULES (SR 2025 MT103 STP & REMIT)
219    // ========================================================================
220
221    /// Field 23B valid bank operation codes for MT103
222    const MT103_VALID_23B_CODES: &'static [&'static str] =
223        &["CRED", "CRTS", "SPAY", "SPRI", "SSTD"];
224
225    /// Field 23E valid instruction codes for MT103 (combined STP and REMIT)
226    const MT103_VALID_23E_CODES: &'static [&'static str] = &[
227        "CHQB", "CORT", "HOLD", "INTC", "PHOB", "PHOI", "PHON", "REPA", "SDVA", "TELB", "TELE",
228        "TELI",
229    ];
230
231    /// Field 23E codes that allow additional information
232    const CODES_WITH_ADDITIONAL_INFO: &'static [&'static str] = &[
233        "PHON", "PHOB", "PHOI", "TELE", "TELB", "TELI", "HOLD", "REPA",
234    ];
235
236    /// Field 23E valid codes for REMIT when 23B is SPRI
237    const REMIT_SPRI_ALLOWED_23E: &'static [&'static str] = &["SDVA", "TELB", "PHOB", "INTC"];
238
239    /// Field 23E invalid code combinations
240    const INVALID_23E_COMBINATIONS: &'static [(&'static str, &'static [&'static str])] = &[
241        ("SDVA", &["HOLD", "CHQB"]),
242        ("INTC", &["HOLD", "CHQB"]),
243        ("REPA", &["HOLD", "CHQB", "CORT"]),
244        ("CORT", &["HOLD", "CHQB"]),
245        ("HOLD", &["CHQB"]),
246        ("PHOB", &["TELB"]),
247        ("PHON", &["TELE"]),
248        ("PHOI", &["TELI"]),
249    ];
250
251    /// Field 23E code ordering for validation (D98)
252    const FIELD_23E_CODE_ORDER: &'static [&'static str] = &[
253        "SDVA", "INTC", "REPA", "CORT", "HOLD", "CHQB", "PHOB", "TELB", "PHON", "TELE", "PHOI",
254        "TELI",
255    ];
256
257    // ========================================================================
258    // HELPER METHODS
259    // ========================================================================
260
261    /// Check if field 56a is present
262    fn has_field_56(&self) -> bool {
263        self.field_56.is_some()
264    }
265
266    /// Check if field 57a is present
267    fn has_field_57(&self) -> bool {
268        self.field_57.is_some()
269    }
270
271    /// Check if field 53a is present
272    fn has_field_53(&self) -> bool {
273        self.field_53.is_some()
274    }
275
276    /// Check if field 54a is present
277    fn has_field_54(&self) -> bool {
278        self.field_54.is_some()
279    }
280
281    /// Check if field 55a is present
282    fn has_field_55(&self) -> bool {
283        self.field_55.is_some()
284    }
285
286    /// Check if field 71F is present (any occurrence)
287    fn has_field_71f(&self) -> bool {
288        self.field_71f.is_some() && !self.field_71f.as_ref().unwrap().is_empty()
289    }
290
291    /// Check if field 71G is present
292    fn has_field_71g(&self) -> bool {
293        self.field_71g.is_some()
294    }
295
296    // ========================================================================
297    // VALIDATION RULES (C1-C18, T36, T48, etc.)
298    // ========================================================================
299
300    /// C1: Currency/Instructed Amount and Exchange Rate (Error code: D75)
301    /// If field 33B is present and currency differs from 32A, field 36 must be present
302    fn validate_c1_currency_exchange(&self) -> Option<SwiftValidationError> {
303        if let Some(ref field_33b) = self.field_33b {
304            let currency_32a = &self.field_32a.currency;
305            let currency_33b = &field_33b.currency;
306
307            if currency_32a != currency_33b {
308                // Currencies differ - field 36 is mandatory
309                if self.field_36.is_none() {
310                    return Some(SwiftValidationError::content_error(
311                        "D75",
312                        "36",
313                        "",
314                        "Field 36 (Exchange Rate) is mandatory when field 33B is present and currency code differs from field 32A",
315                        "If field 33B is present and the currency code is different from the currency code in field 32A, field 36 must be present",
316                    ));
317                }
318            } else {
319                // Currencies are the same - field 36 is not allowed
320                if self.field_36.is_some() {
321                    return Some(SwiftValidationError::content_error(
322                        "D75",
323                        "36",
324                        "",
325                        "Field 36 (Exchange Rate) is not allowed when field 33B currency code is the same as field 32A",
326                        "If field 33B is present and the currency code is equal to the currency code in field 32A, field 36 must not be present",
327                    ));
328                }
329            }
330        } else {
331            // Field 33B not present - field 36 is not allowed
332            if self.field_36.is_some() {
333                return Some(SwiftValidationError::content_error(
334                    "D75",
335                    "36",
336                    "",
337                    "Field 36 (Exchange Rate) is not allowed when field 33B is not present",
338                    "Field 36 is only allowed when field 33B is present",
339                ));
340            }
341        }
342
343        None
344    }
345
346    /// C3: Field 23B and 23E Code Dependencies (Error codes: E01, E02)
347    /// Restricts field 23E codes based on field 23B value
348    fn validate_c3_bank_op_instruction_codes(&self) -> Vec<SwiftValidationError> {
349        let mut errors = Vec::new();
350        let bank_op_code = &self.field_23b.instruction_code;
351
352        if bank_op_code == "SPRI" {
353            // For SPRI: field 23E may contain only SDVA or INTC (STP) or SDVA, TELB, PHOB, INTC (REMIT)
354            // We'll allow the broader REMIT set to cover both cases
355            if let Some(ref field_23e_vec) = self.field_23e {
356                for field_23e in field_23e_vec {
357                    let code = &field_23e.instruction_code;
358                    if !Self::REMIT_SPRI_ALLOWED_23E.contains(&code.as_str()) {
359                        errors.push(SwiftValidationError::content_error(
360                            "E01",
361                            "23E",
362                            code,
363                            &format!(
364                                "When field 23B is SPRI, field 23E may only contain codes: {}. Code '{}' is not allowed",
365                                Self::REMIT_SPRI_ALLOWED_23E.join(", "),
366                                code
367                            ),
368                            "If field 23B contains SPRI, field 23E may contain only SDVA, TELB, PHOB, or INTC",
369                        ));
370                    }
371                }
372            }
373        } else if bank_op_code == "SSTD" || bank_op_code == "SPAY" {
374            // For SSTD or SPAY: field 23E must not be used
375            if self.field_23e.is_some() {
376                errors.push(SwiftValidationError::content_error(
377                    "E02",
378                    "23E",
379                    "",
380                    &format!(
381                        "When field 23B is {} or {}, field 23E must not be used",
382                        "SSTD", "SPAY"
383                    ),
384                    "If field 23B contains one of the codes SSTD or SPAY, field 23E must not be used",
385                ));
386            }
387        }
388
389        errors
390    }
391
392    /// C4: Third Reimbursement Institution Dependencies (Error code: E06)
393    /// If field 55a is present, both fields 53a and 54a must also be present
394    fn validate_c4_third_reimbursement(&self) -> Option<SwiftValidationError> {
395        if self.has_field_55() && (!self.has_field_53() || !self.has_field_54()) {
396            return Some(SwiftValidationError::content_error(
397                "E06",
398                "55a",
399                "",
400                "Fields 53a (Sender's Correspondent) and 54a (Receiver's Correspondent) are mandatory when field 55a (Third Reimbursement Institution) is present",
401                "If field 55a is present, both fields 53a and 54a must also be present",
402            ));
403        }
404
405        None
406    }
407
408    /// C5 (C9): Intermediary and Account With Institution (Error code: C81)
409    /// If field 56a is present, field 57a must also be present
410    fn validate_c5_intermediary(&self) -> Option<SwiftValidationError> {
411        if self.has_field_56() && !self.has_field_57() {
412            return Some(SwiftValidationError::content_error(
413                "C81",
414                "57a",
415                "",
416                "Field 57a (Account With Institution) is mandatory when field 56a (Intermediary) is present",
417                "If field 56a is present, field 57a must also be present",
418            ));
419        }
420
421        None
422    }
423
424    /// C6 (C10): Field 23B SPRI and Field 56A (Error codes: E16, E17)
425    /// If field 23B is SPRI, field 56a must not be present
426    /// If field 23B is SSTD or SPAY, field 56a may be used with option A or C only
427    fn validate_c6_field_56_restrictions(&self) -> Option<SwiftValidationError> {
428        let bank_op_code = &self.field_23b.instruction_code;
429
430        if bank_op_code == "SPRI" && self.has_field_56() {
431            return Some(SwiftValidationError::content_error(
432                "E16",
433                "56a",
434                "",
435                "Field 56a (Intermediary Institution) must not be present when field 23B is SPRI",
436                "If field 23B contains the code SPRI, field 56a must not be present",
437            ));
438        }
439
440        None
441    }
442
443    /// C7 (C14): Details of Charges and Sender's/Receiver's Charges (Error codes: E13, D50, E15)
444    /// Complex rules for fields 71A, 71F, and 71G
445    fn validate_c7_charges(&self) -> Vec<SwiftValidationError> {
446        let mut errors = Vec::new();
447        let charges_code = &self.field_71a.code;
448
449        match charges_code.as_str() {
450            "OUR" => {
451                // If 71A is OUR, field 71F is not allowed, field 71G is optional
452                if self.has_field_71f() {
453                    errors.push(SwiftValidationError::content_error(
454                        "E13",
455                        "71F",
456                        "",
457                        "Field 71F (Sender's Charges) is not allowed when field 71A is OUR",
458                        "If field 71A contains OUR, then field 71F is not allowed",
459                    ));
460                }
461            }
462            "SHA" => {
463                // If 71A is SHA, field 71F is optional, field 71G is not allowed
464                if self.has_field_71g() {
465                    errors.push(SwiftValidationError::content_error(
466                        "D50",
467                        "71G",
468                        "",
469                        "Field 71G (Receiver's Charges) is not allowed when field 71A is SHA",
470                        "If field 71A contains SHA, then field 71G is not allowed",
471                    ));
472                }
473            }
474            "BEN" => {
475                // If 71A is BEN, at least one occurrence of 71F is mandatory, 71G is not allowed
476                if !self.has_field_71f() {
477                    errors.push(SwiftValidationError::content_error(
478                        "E15",
479                        "71F",
480                        "",
481                        "At least one occurrence of field 71F (Sender's Charges) is mandatory when field 71A is BEN",
482                        "If field 71A contains BEN, then at least one occurrence of field 71F is mandatory",
483                    ));
484                }
485                if self.has_field_71g() {
486                    errors.push(SwiftValidationError::content_error(
487                        "E15",
488                        "71G",
489                        "",
490                        "Field 71G (Receiver's Charges) is not allowed when field 71A is BEN",
491                        "If field 71A contains BEN, then field 71G is not allowed",
492                    ));
493                }
494            }
495            _ => {}
496        }
497
498        errors
499    }
500
501    /// C8 (C15): Sender's/Receiver's Charges and Field 33B (Error code: D51)
502    /// If either field 71F or field 71G is present, then field 33B is mandatory
503    fn validate_c8_charges_instructed_amount(&self) -> Option<SwiftValidationError> {
504        if (self.has_field_71f() || self.has_field_71g()) && self.field_33b.is_none() {
505            return Some(SwiftValidationError::content_error(
506                "D51",
507                "33B",
508                "",
509                "Field 33B (Currency/Instructed Amount) is mandatory when field 71F or 71G is present",
510                "If either field 71F (at least one occurrence) or field 71G is present, then field 33B is mandatory",
511            ));
512        }
513
514        None
515    }
516
517    /// C9 (C18): Currency Codes in Fields 71G and 32A (Error code: C02)
518    /// The currency code in fields 71G and 32A must be the same
519    fn validate_c9_receiver_charges_currency(&self) -> Option<SwiftValidationError> {
520        if let Some(ref field_71g) = self.field_71g {
521            let currency_32a = &self.field_32a.currency;
522            let currency_71g = &field_71g.currency;
523
524            if currency_32a != currency_71g {
525                return Some(SwiftValidationError::content_error(
526                    "C02",
527                    "71G",
528                    currency_71g,
529                    &format!(
530                        "Currency code in field 71G ({}) must be the same as in field 32A ({})",
531                        currency_71g, currency_32a
532                    ),
533                    "The currency code in fields 71G and 32A must be the same",
534                ));
535            }
536        }
537
538        None
539    }
540
541    /// C13: Field 59a Account Restriction for Cheque (Error code: E18)
542    /// If any field 23E contains CHQB, subfield 1 (Account) in field 59a is not allowed
543    fn validate_c13_chqb_beneficiary_account(&self) -> Option<SwiftValidationError> {
544        if let Some(ref field_23e_vec) = self.field_23e {
545            let has_chqb = field_23e_vec.iter().any(|f| f.instruction_code == "CHQB");
546
547            if has_chqb {
548                // Check if field 59 has account - this depends on the variant
549                // Field59F has party_identifier instead of account, and account is not restricted for F variant
550                let has_account = match &self.field_59 {
551                    Field59::NoOption(f) => f.account.is_some(),
552                    Field59::A(f) => f.account.is_some(),
553                    Field59::F(_) => false, // Option F uses party_identifier, not account
554                };
555
556                if has_account {
557                    return Some(SwiftValidationError::content_error(
558                        "E18",
559                        "59a",
560                        "",
561                        "Subfield 1 (Account) in field 59a (Beneficiary Customer) is not allowed when field 23E contains code CHQB",
562                        "If any field 23E contains the code CHQB, subfield 1 (Account) in field 59a Beneficiary Customer is not allowed",
563                    ));
564                }
565            }
566        }
567
568        None
569    }
570
571    /// C16: Field 23E TELI/PHOI Restriction (Error code: E44)
572    /// If field 56a is not present, no field 23E may contain TELI or PHOI
573    fn validate_c16_teli_phoi_restriction(&self) -> Vec<SwiftValidationError> {
574        let mut errors = Vec::new();
575
576        if !self.has_field_56()
577            && let Some(ref field_23e_vec) = self.field_23e
578        {
579            for field_23e in field_23e_vec {
580                let code = &field_23e.instruction_code;
581                if code == "TELI" || code == "PHOI" {
582                    errors.push(SwiftValidationError::content_error(
583                        "E44",
584                        "23E",
585                        code,
586                        &format!(
587                            "Field 23E code '{}' is not allowed when field 56a is not present",
588                            code
589                        ),
590                        "If field 56a is not present, no field 23E may contain TELI or PHOI",
591                    ));
592                }
593            }
594        }
595
596        errors
597    }
598
599    /// C17: Field 23E TELE/PHON Restriction (Error code: E45)
600    /// If field 57a is not present, no field 23E may contain TELE or PHON
601    fn validate_c17_tele_phon_restriction(&self) -> Vec<SwiftValidationError> {
602        let mut errors = Vec::new();
603
604        if !self.has_field_57()
605            && let Some(ref field_23e_vec) = self.field_23e
606        {
607            for field_23e in field_23e_vec {
608                let code = &field_23e.instruction_code;
609                if code == "TELE" || code == "PHON" {
610                    errors.push(SwiftValidationError::content_error(
611                        "E45",
612                        "23E",
613                        code,
614                        &format!(
615                            "Field 23E code '{}' is not allowed when field 57a is not present",
616                            code
617                        ),
618                        "If field 57a is not present, no field 23E may contain TELE or PHON",
619                    ));
620                }
621            }
622        }
623
624        errors
625    }
626
627    /// Validate Field 23B bank operation code (Error code: T36)
628    fn validate_field_23b(&self) -> Option<SwiftValidationError> {
629        let code = &self.field_23b.instruction_code;
630
631        if !Self::MT103_VALID_23B_CODES.contains(&code.as_str()) {
632            return Some(SwiftValidationError::format_error(
633                "T36",
634                "23B",
635                code,
636                &format!("One of: {}", Self::MT103_VALID_23B_CODES.join(", ")),
637                &format!(
638                    "Bank operation code '{}' is not valid for MT103. Valid codes: {}",
639                    code,
640                    Self::MT103_VALID_23B_CODES.join(", ")
641                ),
642            ));
643        }
644
645        None
646    }
647
648    /// Validate Field 23E instruction codes (Error codes: T48, D97, D98, D67, E46)
649    /// Complex validation for instruction code combinations and restrictions
650    fn validate_field_23e(&self) -> Vec<SwiftValidationError> {
651        let mut errors = Vec::new();
652
653        if let Some(ref field_23e_vec) = self.field_23e {
654            let mut seen_codes = HashSet::new();
655            let mut code_positions: Vec<(String, usize)> = Vec::new();
656
657            for field_23e in field_23e_vec {
658                let code = &field_23e.instruction_code;
659
660                // T48: Validate instruction code is in allowed list
661                if !Self::MT103_VALID_23E_CODES.contains(&code.as_str()) {
662                    errors.push(SwiftValidationError::format_error(
663                        "T48",
664                        "23E",
665                        code,
666                        &format!("One of: {}", Self::MT103_VALID_23E_CODES.join(", ")),
667                        &format!(
668                            "Instruction code '{}' is not valid for MT103. Valid codes: {}",
669                            code,
670                            Self::MT103_VALID_23E_CODES.join(", ")
671                        ),
672                    ));
673                }
674
675                // D97: Additional information only allowed for specific codes
676                if field_23e.additional_info.is_some()
677                    && !Self::CODES_WITH_ADDITIONAL_INFO.contains(&code.as_str())
678                {
679                    errors.push(SwiftValidationError::content_error(
680                        "D97",
681                        "23E",
682                        code,
683                        &format!(
684                            "Additional information is only allowed for codes: {}. Code '{}' does not allow additional information",
685                            Self::CODES_WITH_ADDITIONAL_INFO.join(", "),
686                            code
687                        ),
688                        "Additional information in field 23E is only allowed for codes: PHON, PHOB, PHOI, TELE, TELB, TELI, HOLD, REPA",
689                    ));
690                }
691
692                // E46: Same code must not be present more than once
693                if seen_codes.contains(code) {
694                    errors.push(SwiftValidationError::relation_error(
695                        "E46",
696                        "23E",
697                        vec![],
698                        &format!(
699                            "Instruction code '{}' appears more than once. Same code must not be repeated",
700                            code
701                        ),
702                        "When field 23E is repeated, the same code must not be present more than once",
703                    ));
704                }
705                seen_codes.insert(code.clone());
706
707                // Track position for ordering check
708                if let Some(pos) = Self::FIELD_23E_CODE_ORDER.iter().position(|&c| c == code) {
709                    code_positions.push((code.clone(), pos));
710                }
711            }
712
713            // D98: Check code ordering
714            for i in 1..code_positions.len() {
715                if code_positions[i].1 < code_positions[i - 1].1 {
716                    errors.push(SwiftValidationError::content_error(
717                        "D98",
718                        "23E",
719                        &code_positions[i].0,
720                        &format!(
721                            "Instruction codes must appear in the following order: {}. Code '{}' appears out of order",
722                            Self::FIELD_23E_CODE_ORDER.join(", "),
723                            code_positions[i].0
724                        ),
725                        "When field 23E is repeated, codes must appear in specified order",
726                    ));
727                    break;
728                }
729            }
730
731            // D67: Check for invalid combinations
732            for field_23e in field_23e_vec {
733                let code = &field_23e.instruction_code;
734
735                for &(base_code, forbidden_codes) in Self::INVALID_23E_COMBINATIONS {
736                    if code == base_code {
737                        for other_field in field_23e_vec {
738                            let other_code = &other_field.instruction_code;
739                            if forbidden_codes.contains(&other_code.as_str()) {
740                                errors.push(SwiftValidationError::content_error(
741                                    "D67",
742                                    "23E",
743                                    code,
744                                    &format!(
745                                        "Instruction code '{}' cannot be combined with code '{}'. Invalid combination",
746                                        code, other_code
747                                    ),
748                                    &format!(
749                                        "Code '{}' cannot be combined with: {}",
750                                        base_code,
751                                        forbidden_codes.join(", ")
752                                    ),
753                                ));
754                            }
755                        }
756                    }
757                }
758            }
759        }
760
761        errors
762    }
763
764    /// Main validation method - validates all network rules
765    /// Returns array of validation errors, respects stop_on_first_error flag
766    pub fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
767        let mut all_errors = Vec::new();
768
769        // Field 23B Validation
770        if let Some(error) = self.validate_field_23b() {
771            all_errors.push(error);
772            if stop_on_first_error {
773                return all_errors;
774            }
775        }
776
777        // Field 23E Validation
778        let f23e_errors = self.validate_field_23e();
779        all_errors.extend(f23e_errors);
780        if stop_on_first_error && !all_errors.is_empty() {
781            return all_errors;
782        }
783
784        // C1: Currency/Instructed Amount and Exchange Rate
785        if let Some(error) = self.validate_c1_currency_exchange() {
786            all_errors.push(error);
787            if stop_on_first_error {
788                return all_errors;
789            }
790        }
791
792        // C3: Field 23B and 23E Code Dependencies
793        let c3_errors = self.validate_c3_bank_op_instruction_codes();
794        all_errors.extend(c3_errors);
795        if stop_on_first_error && !all_errors.is_empty() {
796            return all_errors;
797        }
798
799        // C4: Third Reimbursement Institution Dependencies
800        if let Some(error) = self.validate_c4_third_reimbursement() {
801            all_errors.push(error);
802            if stop_on_first_error {
803                return all_errors;
804            }
805        }
806
807        // C5 (C9): Intermediary and Account With Institution
808        if let Some(error) = self.validate_c5_intermediary() {
809            all_errors.push(error);
810            if stop_on_first_error {
811                return all_errors;
812            }
813        }
814
815        // C6 (C10): Field 23B SPRI and Field 56A
816        if let Some(error) = self.validate_c6_field_56_restrictions() {
817            all_errors.push(error);
818            if stop_on_first_error {
819                return all_errors;
820            }
821        }
822
823        // C7 (C14): Details of Charges
824        let c7_errors = self.validate_c7_charges();
825        all_errors.extend(c7_errors);
826        if stop_on_first_error && !all_errors.is_empty() {
827            return all_errors;
828        }
829
830        // C8 (C15): Charges and Instructed Amount
831        if let Some(error) = self.validate_c8_charges_instructed_amount() {
832            all_errors.push(error);
833            if stop_on_first_error {
834                return all_errors;
835            }
836        }
837
838        // C9 (C18): Receiver's Charges Currency
839        if let Some(error) = self.validate_c9_receiver_charges_currency() {
840            all_errors.push(error);
841            if stop_on_first_error {
842                return all_errors;
843            }
844        }
845
846        // C13: CHQB Beneficiary Account Restriction
847        if let Some(error) = self.validate_c13_chqb_beneficiary_account() {
848            all_errors.push(error);
849            if stop_on_first_error {
850                return all_errors;
851            }
852        }
853
854        // C16: TELI/PHOI Restriction
855        let c16_errors = self.validate_c16_teli_phoi_restriction();
856        all_errors.extend(c16_errors);
857        if stop_on_first_error && !all_errors.is_empty() {
858            return all_errors;
859        }
860
861        // C17: TELE/PHON Restriction
862        let c17_errors = self.validate_c17_tele_phon_restriction();
863        all_errors.extend(c17_errors);
864
865        all_errors
866    }
867}
868
869impl crate::traits::SwiftMessageBody for MT103 {
870    fn message_type() -> &'static str {
871        "103"
872    }
873
874    fn parse_from_block4(block4: &str) -> Result<Self, crate::errors::ParseError> {
875        let mut parser = crate::parser::MessageParser::new(block4, "103");
876
877        // Parse mandatory field 20
878        let field_20 = parser.parse_field::<Field20>("20")?;
879
880        // Parse optional repeating Field13C
881        parser = parser.with_duplicates(true);
882        let mut field_13c = Vec::new();
883        while let Ok(field) = parser.parse_field::<Field13C>("13C") {
884            field_13c.push(field);
885        }
886        parser = parser.with_duplicates(false);
887
888        // Parse mandatory field 23B
889        let field_23b = parser.parse_field::<Field23B>("23B")?;
890
891        // Parse optional repeating Field23E
892        parser = parser.with_duplicates(true);
893        let mut field_23e = Vec::new();
894        while let Ok(field) = parser.parse_field::<Field23E>("23E") {
895            field_23e.push(field);
896        }
897        parser = parser.with_duplicates(false);
898
899        // Parse optional field 26T
900        let field_26t = parser.parse_optional_field::<Field26T>("26T")?;
901
902        // Parse mandatory field 32A
903        let field_32a = parser.parse_field::<Field32A>("32A")?;
904
905        // Parse optional fields 33B and 36
906        let field_33b = parser.parse_optional_field::<Field33B>("33B")?;
907        let field_36 = parser.parse_optional_field::<Field36>("36")?;
908
909        // Parse mandatory field 50
910        let field_50 = parser.parse_variant_field::<Field50OrderingCustomerAFK>("50")?;
911
912        // Parse optional fields that come before field 59
913        let field_51a = parser.parse_optional_field::<Field51A>("51A")?;
914        let field_52 = parser.parse_optional_variant_field::<Field52OrderingInstitution>("52")?;
915        let field_53 = parser.parse_optional_variant_field::<Field53SenderCorrespondent>("53")?;
916        let field_54 = parser.parse_optional_variant_field::<Field54ReceiverCorrespondent>("54")?;
917        let field_55 =
918            parser.parse_optional_variant_field::<Field55ThirdReimbursementInstitution>("55")?;
919        let field_56 = parser.parse_optional_variant_field::<Field56Intermediary>("56")?;
920        let field_57 =
921            parser.parse_optional_variant_field::<Field57AccountWithInstitution>("57")?;
922
923        // Parse mandatory field 59 (after optional routing fields)
924        let field_59 = parser.parse_variant_field::<Field59>("59")?;
925
926        // Parse optional field 70
927        let field_70 = parser.parse_optional_field::<Field70>("70")?;
928
929        // Parse mandatory field 71A
930        let field_71a = parser.parse_field::<Field71A>("71A")?;
931
932        // Parse optional repeating Field71F
933        parser = parser.with_duplicates(true);
934        let mut field_71f = Vec::new();
935        while let Ok(field) = parser.parse_field::<Field71F>("71F") {
936            field_71f.push(field);
937        }
938        parser = parser.with_duplicates(false);
939
940        // Parse remaining optional fields
941        let field_71g = parser.parse_optional_field::<Field71G>("71G")?;
942        let field_72 = parser.parse_optional_field::<Field72>("72")?;
943        let field_77b = parser.parse_optional_field::<Field77B>("77B")?;
944        let field_77t = parser.parse_optional_field::<Field77T>("77T")?;
945
946        // Verify all content is consumed
947        verify_parser_complete(&parser)?;
948
949        Ok(Self {
950            field_20,
951            field_23b,
952            field_32a,
953            field_50,
954            field_59,
955            field_71a,
956            field_13c: if field_13c.is_empty() {
957                None
958            } else {
959                Some(field_13c)
960            },
961            field_23e: if field_23e.is_empty() {
962                None
963            } else {
964                Some(field_23e)
965            },
966            field_26t,
967            field_33b,
968            field_36,
969            field_51a,
970            field_52,
971            field_53,
972            field_54,
973            field_55,
974            field_56,
975            field_57,
976            field_70,
977            field_71f: if field_71f.is_empty() {
978                None
979            } else {
980                Some(field_71f)
981            },
982            field_71g,
983            field_72,
984            field_77b,
985            field_77t,
986        })
987    }
988
989    fn to_mt_string(&self) -> String {
990        // Call the existing public method implementation
991        MT103::to_mt_string(self)
992    }
993
994    fn validate_network_rules(&self, stop_on_first_error: bool) -> Vec<SwiftValidationError> {
995        // Call the existing public method implementation
996        MT103::validate_network_rules(self, stop_on_first_error)
997    }
998}
999
1000#[cfg(test)]
1001mod tests {
1002    use super::*;
1003
1004    #[test]
1005    fn test_mt103_parse() {
1006        let mt103_text = r#":20:123456789012345
1007:23B:CRED
1008:32A:241201USD1000000,00
1009:50K:/12345678901234567890
1010JOHN DOE
1011123 MAIN STREET
1012NEW YORK, NY 10001
1013:59:/98765432109876543210
1014JANE SMITH
1015456 OAK AVENUE
1016LOS ANGELES, CA 90001
1017:71A:OUR
1018-"#;
1019        let result = <MT103 as crate::traits::SwiftMessageBody>::parse_from_block4(mt103_text);
1020        assert!(result.is_ok());
1021        let mt103 = result.unwrap();
1022        assert_eq!(mt103.field_20.reference, "123456789012345");
1023        assert_eq!(mt103.field_23b.instruction_code, "CRED");
1024        assert_eq!(mt103.field_71a.code, "OUR");
1025    }
1026
1027    #[test]
1028    fn test_mt103_stp_compliance() {
1029        let mt103_text = r#":20:123456789012345
1030:23B:SPRI
1031:32A:241201USD1000000,00
1032:50K:/12345678901234567890
1033JOHN DOE
1034123 MAIN STREET
1035NEW YORK, NY 10001
1036:59:/98765432109876543210
1037JANE SMITH
1038456 OAK AVENUE
1039LOS ANGELES, CA 90001
1040:71A:OUR
1041-"#;
1042        let result = <MT103 as crate::traits::SwiftMessageBody>::parse_from_block4(mt103_text);
1043        assert!(result.is_ok());
1044        let mt103 = result.unwrap();
1045
1046        // SPRI message without field 56 should be STP compliant
1047        assert!(mt103.is_stp_compliant());
1048    }
1049}