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