swift_mt_message/messages/
mt103.rs

1use crate::fields::*;
2use serde::{Deserialize, Serialize};
3use swift_mt_message_macros::{SwiftMessage, serde_swift_fields};
4
5/// Message status information for MT103 messages
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct MessageStatus {
8    /// Whether the message is STP compliant
9    pub is_stp_compliant: bool,
10    /// Whether the message is a REMIT message
11    pub is_remit: bool,
12    /// Whether the message contains reject codes
13    pub has_reject_codes: bool,
14    /// Whether the message contains return codes
15    pub has_return_codes: bool,
16    /// The processing variant (Standard, STP, REMIT, REJECT, RETURN)
17    pub processing_variant: String,
18}
19
20/// MT103: Customer Credit Transfer (Standard and STP variants)
21///
22/// Unified structure supporting both standard MT103 and MT103 STP variants.
23/// Use `is_stp_compliant()` to check if the message meets STP requirements.
24#[serde_swift_fields]
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, SwiftMessage)]
26#[validation_rules(MT103_VALIDATION_RULES)]
27pub struct MT103 {
28    // Mandatory Fields
29    #[field("20", mandatory)]
30    pub field_20: GenericReferenceField,
31
32    #[field("23B", mandatory)]
33    pub field_23b: GenericTextField,
34
35    #[field("32A", mandatory)]
36    pub field_32a: Field32A,
37
38    #[field("50", mandatory)]
39    pub field_50: Field50,
40
41    #[field("59", mandatory)]
42    pub field_59: Field59,
43
44    #[field("71A", mandatory)]
45    pub field_71a: GenericTextField,
46
47    // Optional Fields
48    #[field("13C", optional)]
49    pub field_13c: Option<Field13C>,
50
51    #[field("23E", optional)]
52    pub field_23e: Option<Field23E>,
53
54    #[field("26T", optional)]
55    pub field_26t: Option<GenericTextField>,
56
57    #[field("33B", optional)]
58    pub field_33b: Option<Field33B>,
59
60    #[field("36", optional)]
61    pub field_36: Option<Field36>,
62
63    #[field("51A", optional)]
64    pub field_51a: Option<GenericBicField>,
65
66    #[field("52A", optional)]
67    pub field_52a: Option<GenericBicField>,
68
69    #[field("52D", optional)]
70    pub field_52d: Option<GenericNameAddressField>,
71
72    #[field("53A", optional)]
73    pub field_53a: Option<GenericBicField>,
74
75    #[field("53B", optional)]
76    pub field_53b: Option<GenericPartyField>,
77
78    #[field("53D", optional)]
79    pub field_53d: Option<GenericNameAddressField>,
80
81    #[field("54A", optional)]
82    pub field_54a: Option<GenericBicField>,
83
84    #[field("54B", optional)]
85    pub field_54b: Option<GenericPartyField>,
86
87    #[field("54D", optional)]
88    pub field_54d: Option<GenericNameAddressField>,
89
90    #[field("55A", optional)]
91    pub field_55a: Option<GenericBicField>,
92
93    #[field("55B", optional)]
94    pub field_55b: Option<GenericPartyField>,
95
96    #[field("55D", optional)]
97    pub field_55d: Option<GenericNameAddressField>,
98
99    #[field("56A", optional)]
100    pub field_56a: Option<GenericBicField>,
101
102    #[field("56C", optional)]
103    pub field_56c: Option<GenericAccountField>,
104
105    #[field("56D", optional)]
106    pub field_56d: Option<GenericNameAddressField>,
107
108    #[field("57A", optional)]
109    pub field_57a: Option<GenericBicField>,
110
111    #[field("57B", optional)]
112    pub field_57b: Option<GenericPartyField>,
113
114    #[field("57C", optional)]
115    pub field_57c: Option<GenericAccountField>,
116
117    #[field("57D", optional)]
118    pub field_57d: Option<GenericNameAddressField>,
119
120    #[field("70", optional)]
121    pub field_70: Option<GenericMultiLine4x35>,
122
123    #[field("71F", optional)]
124    pub field_71f: Option<Field71F>,
125
126    #[field("71G", optional)]
127    pub field_71g: Option<Field71G>,
128
129    #[field("72", optional)]
130    pub field_72: Option<GenericMultiLine6x35>,
131
132    #[field("77B", optional)]
133    pub field_77b: Option<GenericMultiLine3x35>,
134
135    #[field("77T", optional)]
136    pub field_77t: Option<Field77T>,
137}
138
139impl MT103 {
140    /// Check if this MT103 message is compliant with STP (Straight Through Processing) requirements
141    ///
142    /// STP compliance requires:
143    /// - Field 51A must not be present
144    /// - Field 52: Only option A allowed (no D)
145    /// - Field 53: Only options A/B allowed (no D)
146    /// - Field 54: Only option A allowed (no B/D)
147    /// - Field 23E: Limited to CORT, INTC, SDVA, REPA
148    /// - Field 56a: Not allowed if 23B is SPRI
149    /// - Field 59: Account information mandatory
150    /// - Additional conditional rules (C4, C6)
151    pub fn is_stp_compliant(&self) -> bool {
152        // Check field 51A - must not be present in STP
153        if self.field_51a.is_some() {
154            return false;
155        }
156
157        // Check field 52 - only option A allowed in STP
158        if self.field_52d.is_some() {
159            return false;
160        }
161
162        // Check field 53 - only options A/B allowed in STP
163        if self.field_53d.is_some() {
164            return false;
165        }
166
167        // Check field 54 - only option A allowed in STP
168        if self.field_54b.is_some() || self.field_54d.is_some() {
169            return false;
170        }
171
172        // Check field 23E - restricted instruction codes in STP
173        if let Some(ref field_23e) = self.field_23e {
174            let stp_allowed_codes = ["CORT", "INTC", "SDVA", "REPA"];
175            if !stp_allowed_codes.contains(&field_23e.instruction_code.as_str()) {
176                return false;
177            }
178        }
179
180        // Check C6_STP: If 23B is SPRI → 56a must not be present
181        if self.field_23b.value == "SPRI"
182            && (self.field_56a.is_some() || self.field_56c.is_some() || self.field_56d.is_some())
183        {
184            return false;
185        }
186
187        // Check C4_STP: If 55a present → 53a and 54a are mandatory
188        if self.field_55a.is_some() || self.field_55b.is_some() || self.field_55d.is_some() {
189            if self.field_53a.is_none() && self.field_53b.is_none() {
190                return false;
191            }
192            if self.field_54a.is_none() {
193                return false;
194            }
195        }
196
197        // Check field 59 - account information should be present for STP
198        // This is a soft requirement - in practice, field 59 without account (NoOption)
199        // might still be acceptable in some STP scenarios
200        match &self.field_59 {
201            Field59::A(_) => {} // Has account - good for STP
202            Field59::F(_) => {} // Has party identifier - acceptable for STP
203            Field59::NoOption(_) => {
204                // This might be acceptable in some STP scenarios
205                // We'll allow it but note that proper STP usually requires account info
206            }
207        }
208
209        true
210    }
211
212    /// Check if this MT103 message is a REMIT message with enhanced remittance information
213    ///
214    /// REMIT messages are distinguished by:
215    /// - Field 77T must be present and contain structured remittance information
216    /// - Field 70 is typically not used (replaced by 77T)
217    /// - Enhanced remittance data for regulatory compliance
218    pub fn is_remit_message(&self) -> bool {
219        // The key distinguishing feature of REMIT is the presence of field 77T
220        // with structured remittance information
221        match &self.field_77t {
222            Some(field_77t) => {
223                // Check if 77T contains actual remittance data (not just empty)
224                !field_77t.envelope_identifier.trim().is_empty()
225                    && !field_77t.envelope_type.trim().is_empty()
226                    && !field_77t.envelope_format.trim().is_empty()
227            }
228            None => false,
229        }
230    }
231
232    /// Check if this MT103 message contains reject codes
233    ///
234    /// Reject messages are identified by checking:
235    /// 1. Field 20 (Sender's Reference) for "REJT" prefix
236    /// 2. Block 3 field 108 (MUR - Message User Reference) for "REJT"
237    /// 3. Field 72 (Sender to Receiver Information) containing `/REJT/` code
238    pub fn has_reject_codes(&self) -> bool {
239        // Check field 20 (sender's reference)
240        if self.field_20.value.to_uppercase().contains("REJT") {
241            return true;
242        }
243
244        // Check field 72 for structured reject codes
245        if let Some(field_72) = &self.field_72 {
246            let content = field_72.lines.join(" ").to_uppercase();
247            if content.contains("/REJT/") || content.contains("REJT") {
248                return true;
249            }
250        }
251
252        false
253    }
254
255    /// Check if this MT103 message contains return codes
256    ///
257    /// Return messages are identified by checking:
258    /// 1. Field 20 (Sender's Reference) for "RETN" prefix
259    /// 2. Block 3 field 108 (MUR - Message User Reference) for "RETN"
260    /// 3. Field 72 (Sender to Receiver Information) containing `/RETN/` code
261    pub fn has_return_codes(&self) -> bool {
262        // Check field 20 (sender's reference)
263        if self.field_20.value.to_uppercase().contains("RETN") {
264            return true;
265        }
266
267        // Check field 72 for structured return codes
268        if let Some(field_72) = &self.field_72 {
269            let content = field_72.lines.join(" ").to_uppercase();
270            if content.contains("/RETN/") || content.contains("RETN") {
271                return true;
272            }
273        }
274
275        false
276    }
277}
278
279/// Comprehensive MT103 validation rules covering both standard and STP variants
280const MT103_VALIDATION_RULES: &str = r#"{
281  "rules": [
282    {
283      "id": "C1",
284      "description": "If 33B is present and its currency differs from 32A, then 36 must be present; otherwise, 36 must not be present",
285      "condition": {
286        "if": [
287          {"!!": {"var": "fields.33B"}},
288          {
289            "if": [
290              {"!=": [{"var": "fields.33B.currency"}, {"var": "fields.32A.currency"}]},
291              {"!!": {"var": "fields.36"}},
292              {"!": {"var": "fields.36"}}
293            ]
294          },
295          {"!": {"var": "fields.36"}}
296        ]
297      }
298    },
299    {
300      "id": "C2", 
301      "description": "33B is mandatory if both Sender and Receiver BICs are in EU/EEA country codes list",
302      "condition": {
303        "if": [
304          {"and": [
305            {"in": [{"var": "basic_header.sender_bic.country_code"}, {"var": "EU_EEA_COUNTRIES"}]},
306            {"in": [{"var": "application_header.receiver_bic.country_code"}, {"var": "EU_EEA_COUNTRIES"}]}
307          ]},
308          {"!!": {"var": "fields.33B"}},
309          true
310        ]
311      }
312    },
313    {
314      "id": "C3",
315      "description": "Bank operation code and instruction code compatibility rules",
316      "condition": {
317        "and": [
318          {
319            "if": [
320              {"==": [{"var": "fields.23B.value"}, "SPRI"]},
321              {"and": [
322                {"!!": {"var": "fields.23E"}},
323                {"in": [{"var": "fields.23E.instruction_code"}, ["SDVA", "INTC"]]}
324              ]},
325              true
326            ]
327          },
328          {
329            "if": [
330              {"in": [{"var": "fields.23B.value"}, ["SSTD", "SPAY"]]},
331              {"!": {"var": "fields.23E"}},
332              true
333            ]
334          }
335        ]
336      }
337    },
338    {
339      "id": "C4",
340      "description": "If 55a is present, then 53a and 54a become mandatory",
341      "condition": {
342        "if": [
343          {"or": [
344            {"!!": {"var": "fields.55A"}},
345            {"!!": {"var": "fields.55B"}},
346            {"!!": {"var": "fields.55D"}}
347          ]},
348          {"and": [
349            {"or": [
350              {"!!": {"var": "fields.53A"}},
351              {"!!": {"var": "fields.53B"}},
352              {"!!": {"var": "fields.53D"}}
353            ]},
354            {"or": [
355              {"!!": {"var": "fields.54A"}},
356              {"!!": {"var": "fields.54B"}},
357              {"!!": {"var": "fields.54D"}}
358            ]}
359          ]},
360          true
361        ]
362      }
363    },
364    {
365      "id": "C5",
366      "description": "If 56a is present, 57a becomes mandatory",
367      "condition": {
368        "if": [
369          {"or": [
370            {"!!": {"var": "fields.56A"}},
371            {"!!": {"var": "fields.56C"}},
372            {"!!": {"var": "fields.56D"}}
373          ]},
374          {"or": [
375            {"!!": {"var": "fields.57A"}},
376            {"!!": {"var": "fields.57B"}},
377            {"!!": {"var": "fields.57C"}},
378            {"!!": {"var": "fields.57D"}}
379          ]},
380          true
381        ]
382      }
383    },
384    {
385      "id": "C7",
386      "description": "Charge allocation rules: If 71A = OUR → 71F not allowed, 71G optional; If 71A = SHA → 71F optional, 71G not allowed; If 71A = BEN → 71F mandatory, 71G not allowed",
387      "condition": {
388        "and": [
389          {
390            "if": [
391              {"==": [{"var": "fields.71A.value"}, "OUR"]},
392              {"!": {"var": "fields.71F"}},
393              true
394            ]
395          },
396          {
397            "if": [
398              {"==": [{"var": "fields.71A.value"}, "SHA"]},
399              {"!": {"var": "fields.71G"}},
400              true
401            ]
402          },
403          {
404            "if": [
405              {"==": [{"var": "fields.71A.value"}, "BEN"]},
406              {"and": [
407                {"!!": {"var": "fields.71F"}},
408                {"!": {"var": "fields.71G"}}
409              ]},
410              true
411            ]
412          }
413        ]
414      }
415    },
416    {
417      "id": "C8",
418      "description": "If either 71F or 71G is present, 33B becomes mandatory",
419      "condition": {
420        "if": [
421          {"or": [
422            {"!!": {"var": "fields.71F"}},
423            {"!!": {"var": "fields.71G"}}
424          ]},
425          {"!!": {"var": "fields.33B"}},
426          true
427        ]
428      }
429    },
430    {
431      "id": "C9",
432      "description": "Currency codes in 71G and 32A must match",
433      "condition": {
434        "if": [
435          {"!!": {"var": "fields.71G"}},
436          {"==": [{"var": "fields.71G.currency"}, {"var": "fields.32A.currency"}]},
437          true
438        ]
439      }
440    },
441    {
442      "id": "MANDATORY_FIELDS",
443      "description": "All mandatory fields must be present and valid",
444      "condition": {
445        "and": [
446          {"!!": {"var": "fields.20"}},
447          {"!=": [{"var": "fields.20.value"}, ""]},
448          {"!!": {"var": "fields.23B"}},
449          {"in": [{"var": "fields.23B.value"}, {"var": "VALID_BANK_OPERATION_CODES"}]},
450          {"!!": {"var": "fields.32A"}},
451          {">": [{"var": "fields.32A.amount"}, 0]},
452          {"!!": {"var": "fields.50"}},
453          {"!!": {"var": "fields.59"}},
454          {"!!": {"var": "fields.71A"}},
455          {"in": [{"var": "fields.71A.value"}, {"var": "VALID_CHARGE_CODES"}]}
456        ]
457      }
458    },
459    {
460      "id": "INSTRUCTION_CODE_VALIDATION",
461      "description": "23E instruction codes must be valid when present",
462      "condition": {
463        "if": [
464          {"!!": {"var": "fields.23E"}},
465          {"in": [{"var": "fields.23E.instruction_code"}, {"var": "VALID_INSTRUCTION_CODES"}]},
466          true
467        ]
468      }
469    },
470    {
471      "id": "AMOUNT_CONSISTENCY",
472      "description": "All amounts must be positive and properly formatted",
473      "condition": {
474        "and": [
475          {">": [{"var": "fields.32A.amount"}, 0]},
476          {
477            "if": [
478              {"!!": {"var": "fields.33B"}},
479              {">": [{"var": "fields.33B.amount"}, 0]},
480              true
481            ]
482          },
483          {
484            "if": [
485              {"!!": {"var": "fields.71F"}},
486              {">": [{"var": "fields.71F.amount"}, 0]},
487              true
488            ]
489          },
490          {
491            "if": [
492              {"!!": {"var": "fields.71G"}},
493              {">": [{"var": "fields.71G.amount"}, 0]},
494              true
495            ]
496          }
497        ]
498      }
499    },
500    {
501      "id": "CURRENCY_CODE_VALIDATION",
502      "description": "All currency codes must be valid ISO 4217 3-letter codes",
503      "condition": {
504        "and": [
505          {"!=": [{"var": "fields.32A.currency"}, ""]},
506          {
507            "if": [
508              {"!!": {"var": "fields.33B"}},
509              {"!=": [{"var": "fields.33B.currency"}, ""]},
510              true
511            ]
512          },
513          {
514            "if": [
515              {"!!": {"var": "fields.71F"}},
516              {"!=": [{"var": "fields.71F.currency"}, ""]},
517              true
518            ]
519          },
520          {
521            "if": [
522              {"!!": {"var": "fields.71G"}},
523              {"!=": [{"var": "fields.71G.currency"}, ""]},
524              true
525            ]
526          }
527        ]
528      }
529    },
530    {
531      "id": "REFERENCE_FORMAT",
532      "description": "Reference fields must not contain invalid patterns",
533      "condition": {
534        "and": [
535          {"!=": [{"var": "fields.20.value"}, ""]},
536          {"!": {"in": ["//", {"var": "fields.20.value"}]}}
537        ]
538      }
539    },
540    {
541      "id": "BIC_VALIDATION",
542      "description": "All BIC codes must be properly formatted (non-empty)",
543      "condition": {
544        "and": [
545          {"!=": [{"var": "basic_header.sender_bic.raw"}, ""]},
546          {"!=": [{"var": "application_header.receiver_bic.raw"}, ""]},
547          {
548            "if": [
549              {"!!": {"var": "fields.52A"}},
550              {"!=": [{"var": "fields.52A.bic.raw"}, ""]},
551              true
552            ]
553          },
554          {
555            "if": [
556              {"!!": {"var": "fields.53A"}},
557              {"!=": [{"var": "fields.53A.bic.raw"}, ""]},
558              true
559            ]
560          },
561          {
562            "if": [
563              {"!!": {"var": "fields.57A"}},
564              {"!=": [{"var": "fields.57A.bic.raw"}, ""]},
565              true
566            ]
567          }
568        ]
569      }
570    },
571    {
572      "id": "REMIT_77T",
573      "description": "REMIT: If 77T is present, it must contain valid structured remittance information",
574      "condition": {
575        "if": [
576          {"!!": {"var": "fields.77T"}},
577          {"and": [
578            {"!=": [{"var": "fields.77T.envelope_type"}, ""]},
579            {"!=": [{"var": "fields.77T.envelope_format"}, ""]},
580            {"!=": [{"var": "fields.77T.envelope_identifier"}, ""]}
581          ]},
582          true
583        ]
584      }
585    },
586    {
587      "id": "REMIT_FIELD_COMPATIBILITY",
588      "description": "REMIT: Field 70 should not be used when 77T is present (77T replaces 70 in REMIT)",
589      "condition": {
590        "if": [
591          {"!!": {"var": "fields.77T"}},
592          {"!": {"var": "fields.70"}},
593          true
594        ]
595      }
596    }
597  ],
598  "constants": {
599    "EU_EEA_COUNTRIES": ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "IS", "LI", "NO"],
600    "VALID_BANK_OPERATION_CODES": ["CRED", "CRTS", "SPAY", "SPRI", "SSTD"],
601    "VALID_CHARGE_CODES": ["OUR", "SHA", "BEN"],
602    "VALID_INSTRUCTION_CODES": ["CORT", "INTC", "REPA", "SDVA", "CHQB", "PHOB", "PHOI", "PHON", "TELE", "TELI", "TELB"],
603    "VALID_INSTRUCTION_CODES_STP": ["CORT", "INTC", "SDVA", "REPA"]
604  }
605}"#;