1use crate::fields::*;
2use serde::{Deserialize, Serialize};
3use swift_mt_message_macros::{SwiftMessage, serde_swift_fields};
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct MessageStatus {
8 pub is_stp_compliant: bool,
10 pub is_remit: bool,
12 pub has_reject_codes: bool,
14 pub has_return_codes: bool,
16 pub processing_variant: String,
18}
19
20#[serde_swift_fields]
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, SwiftMessage)]
26#[validation_rules(MT103_VALIDATION_RULES)]
27pub struct MT103 {
28 #[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 #[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 pub fn is_stp_compliant(&self) -> bool {
152 if self.field_51a.is_some() {
154 return false;
155 }
156
157 if self.field_52d.is_some() {
159 return false;
160 }
161
162 if self.field_53d.is_some() {
164 return false;
165 }
166
167 if self.field_54b.is_some() || self.field_54d.is_some() {
169 return false;
170 }
171
172 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 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 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 match &self.field_59 {
201 Field59::A(_) => {} Field59::F(_) => {} Field59::NoOption(_) => {
204 }
207 }
208
209 true
210 }
211
212 pub fn is_remit_message(&self) -> bool {
219 match &self.field_77t {
222 Some(field_77t) => {
223 !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 pub fn has_reject_codes(&self) -> bool {
239 if self.field_20.value.to_uppercase().contains("REJT") {
241 return true;
242 }
243
244 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 pub fn has_return_codes(&self) -> bool {
262 if self.field_20.value.to_uppercase().contains("RETN") {
264 return true;
265 }
266
267 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
279const 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}"#;