1use crate::fields::*;
2use serde::{Deserialize, Serialize};
3use swift_mt_message_macros::{SwiftMessage, serde_swift_fields};
4
5#[serde_swift_fields]
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, SwiftMessage)]
91#[validation_rules(MT103_VALIDATION_RULES)]
92pub struct MT103 {
93 #[field("20")]
95 pub field_20: Field20,
96
97 #[field("13C")]
98 pub field_13c: Option<Vec<Field13C>>,
99
100 #[field("23B")]
101 pub field_23b: Field23B,
102
103 #[field("23E")]
104 pub field_23e: Option<Vec<Field23E>>,
105
106 #[field("26T")]
107 pub field_26t: Option<Field26T>,
108
109 #[field("32A")]
110 pub field_32a: Field32A,
111
112 #[field("33B")]
113 pub field_33b: Option<Field33B>,
114
115 #[field("36")]
116 pub field_36: Option<Field36>,
117
118 #[field("50")]
119 pub field_50: Field50OrderingCustomerAFK,
120
121 #[field("51A")]
122 pub field_51a: Option<Field51A>,
123
124 #[field("52")]
125 pub field_52: Option<Field52OrderingInstitution>,
126
127 #[field("53")]
128 pub field_53: Option<Field53SenderCorrespondent>,
129
130 #[field("54")]
131 pub field_54: Option<Field54ReceiverCorrespondent>,
132
133 #[field("55")]
134 pub field_55: Option<Field55ThirdReimbursementInstitution>,
135
136 #[field("56")]
137 pub field_56: Option<Field56Intermediary>,
138
139 #[field("57")]
140 pub field_57: Option<Field57AccountWithInstitution>,
141
142 #[field("59")]
143 pub field_59: Field59,
144
145 #[field("70")]
146 pub field_70: Option<Field70>,
147
148 #[field("71A")]
149 pub field_71a: Field71A,
150
151 #[field("71F")]
152 pub field_71f: Option<Vec<Field71F>>,
153
154 #[field("71G")]
155 pub field_71g: Option<Field71G>,
156
157 #[field("72")]
158 pub field_72: Option<Field72>,
159
160 #[field("77B")]
161 pub field_77b: Option<Field77B>,
162
163 #[field("77T")]
164 pub field_77t: Option<Field77T>,
165}
166
167impl MT103 {
168 pub fn is_stp_compliant(&self) -> bool {
181 let bank_op_code = &self.field_23b.instruction_code;
183 if !["SPRI", "SSTD", "SPAY"].contains(&bank_op_code.as_str()) {
184 return true;
186 }
187
188 if bank_op_code == "SPRI" {
191 if let Some(ref field_23e_vec) = self.field_23e {
192 let allowed_codes = ["SDVA", "TELB", "PHOB", "INTC"];
193 for field_23e in field_23e_vec {
194 if !allowed_codes.contains(&field_23e.instruction_code.as_str()) {
195 return false;
196 }
197 }
198 }
199 } else if ["SSTD", "SPAY"].contains(&bank_op_code.as_str())
200 && self.field_23e.is_some()
201 && !self.field_23e.as_ref().unwrap().is_empty()
202 {
203 return false;
204 }
205
206 if let Some(ref field_53) = self.field_53 {
208 if let Field53SenderCorrespondent::D(_) = field_53 {
209 return false;
210 }
211
212 if let Field53SenderCorrespondent::B(field_53b) = field_53
214 && field_53b.party_identifier.is_none()
215 {
216 return false;
217 }
218 }
219
220 if let Some(ref field_54) = self.field_54 {
222 match field_54 {
223 Field54ReceiverCorrespondent::A(_) => {}
224 _ => return false,
225 }
226 }
227
228 if let Some(ref field_55) = self.field_55 {
230 match field_55 {
231 Field55ThirdReimbursementInstitution::A(_) => {}
232 _ => return false,
233 }
234 }
235
236 if bank_op_code == "SPRI" {
239 if self.field_56.is_some() {
240 return false;
241 }
242 } else if ["SSTD", "SPAY"].contains(&bank_op_code.as_str())
243 && let Some(ref field_56) = self.field_56
244 {
245 match field_56 {
246 Field56Intermediary::A(_) | Field56Intermediary::C(_) => {}
247 _ => return false,
248 }
249 }
250
251 if let Some(ref field_57) = self.field_57 {
254 match field_57 {
255 Field57AccountWithInstitution::A(_) | Field57AccountWithInstitution::C(_) => {}
256 Field57AccountWithInstitution::D(field_57d) => {
257 if field_57d.party_identifier.is_none() {
258 return false;
259 }
260 }
261 _ => return false,
262 }
263 }
264
265 match &self.field_59 {
267 Field59::NoOption(field_59) => {
268 if field_59.account.is_none() {
269 return false;
270 }
271 }
272 Field59::A(field_59a) => {
273 if field_59a.account.is_none() {
274 return false;
275 }
276 }
277 Field59::F(field_59f) => {
278 if field_59f.party_identifier.is_none() {
279 return false;
280 }
281 }
282 }
283
284 if self.field_55.is_some() && (self.field_53.is_none() || self.field_54.is_none()) {
287 return false;
288 }
289
290 true
291 }
292
293 pub fn is_remit_message(&self) -> bool {
300 match &self.field_77t {
303 Some(field_77t) => {
304 !field_77t.envelope_content.trim().is_empty()
306 }
307 None => false,
308 }
309 }
310
311 pub fn has_reject_codes(&self) -> bool {
318 if self.field_20.reference.to_uppercase().contains("REJT") {
320 return true;
321 }
322
323 if let Some(field_72) = &self.field_72 {
325 let content = field_72.information.join(" ").to_uppercase();
326 if content.contains("/REJT/") || content.contains("REJT") {
327 return true;
328 }
329 }
330
331 false
332 }
333
334 pub fn has_return_codes(&self) -> bool {
341 if self.field_20.reference.to_uppercase().contains("RETN") {
343 return true;
344 }
345
346 if let Some(field_72) = &self.field_72 {
348 let content = field_72.information.join(" ").to_uppercase();
349 if content.contains("/RETN/") || content.contains("RETN") {
350 return true;
351 }
352 }
353
354 false
355 }
356}
357
358const MT103_VALIDATION_RULES: &str = r#"{
360 "rules": [
361 {
362 "id": "C1",
363 "description": "If field 33B is present and the currency code is different from the currency code in field 32A, field 36 must be present, otherwise field 36 is not allowed",
364 "condition": {
365 "if": [
366 {"exists": ["fields", "33B"]},
367 {
368 "if": [
369 {"!=": [{"var": "fields.33B.currency"}, {"var": "fields.32A.currency"}]},
370 {"exists": ["fields", "36"]},
371 {"!": {"exists": ["fields", "36"]}}
372 ]
373 },
374 {"!": {"exists": ["fields", "36"]}}
375 ]
376 }
377 },
378 {
379 "id": "C2",
380 "description": "If the country codes of the Sender's and the Receiver's BICs are within EU/EEA list, then field 33B is mandatory",
381 "condition": {
382 "if": [
383 {"and": [
384 {"in": [{"var": "basic_header.sender_bic.country_code"}, {"var": "EU_EEA_COUNTRIES"}]},
385 {"in": [{"var": "application_header.receiver_bic.country_code"}, {"var": "EU_EEA_COUNTRIES"}]}
386 ]},
387 {"exists": ["fields", "33B"]},
388 true
389 ]
390 }
391 },
392 {
393 "id": "C3",
394 "description": "If field 23B contains SPRI, field 23E may contain only SDVA, TELB, PHOB, INTC. If field 23B contains SSTD or SPAY, field 23E must not be used",
395 "condition": {
396 "and": [
397 {
398 "if": [
399 {"==": [{"var": "fields.23B.instruction_code"}, "SPRI"]},
400 {
401 "if": [
402 {"exists": ["fields", "23E"]},
403 {
404 "all": [
405 {"var": "fields.23E"},
406 {
407 "in": [{"var": "instruction_code"}, ["SDVA", "TELB", "PHOB", "INTC"]]
408 }
409 ]
410 },
411 true
412 ]
413 },
414 true
415 ]
416 },
417 {
418 "if": [
419 {"in": [{"var": "fields.23B.instruction_code"}, ["SSTD", "SPAY"]]},
420 {"!": {"exists": ["fields", "23E"]}},
421 true
422 ]
423 }
424 ]
425 }
426 },
427 {
428 "id": "C4",
429 "description": "If field 23B contains SPRI, SSTD or SPAY, field 53a must not be used with option D",
430 "condition": {
431 "if": [
432 {"in": [{"var": "fields.23B.instruction_code"}, ["SPRI", "SSTD", "SPAY"]]},
433 {
434 "if": [
435 {"exists": ["fields", "53", "D"]},
436 false,
437 true
438 ]
439 },
440 true
441 ]
442 }
443 },
444 {
445 "id": "C5",
446 "description": "If field 23B contains SPRI, SSTD or SPAY and field 53a is present with option B, Party Identifier must be present",
447 "condition": {
448 "if": [
449 {"and": [
450 {"in": [{"var": "fields.23B.instruction_code"}, ["SPRI", "SSTD", "SPAY"]]},
451 {"exists": ["fields", "53", "B"]}
452 ]},
453 {"exists": ["fields", "53", "B", "party_identifier"]},
454 true
455 ]
456 }
457 },
458 {
459 "id": "C6",
460 "description": "If field 23B contains SPRI, SSTD or SPAY, field 54a may be used with option A only",
461 "condition": {
462 "if": [
463 {"in": [{"var": "fields.23B.instruction_code"}, ["SPRI", "SSTD", "SPAY"]]},
464 {
465 "if": [
466 {"or": [
467 {"exists": ["fields", "54", "A"]},
468 {"!": {"exists": ["fields", "54"]}}
469 ]},
470 true,
471 false
472 ]
473 },
474 true
475 ]
476 }
477 },
478 {
479 "id": "C7",
480 "description": "If field 55a is present, then both fields 53a and 54a must also be present",
481 "condition": {
482 "if": [
483 {"exists": ["fields", "55"]},
484 {"and": [
485 {"exists": ["fields", "53"]},
486 {"exists": ["fields", "54"]}
487 ]},
488 true
489 ]
490 }
491 },
492 {
493 "id": "C8",
494 "description": "If field 23B contains SPRI, SSTD or SPAY, field 55a may be used with option A only",
495 "condition": {
496 "if": [
497 {"in": [{"var": "fields.23B.instruction_code"}, ["SPRI", "SSTD", "SPAY"]]},
498 {
499 "if": [
500 {"or": [
501 {"exists": ["fields", "55", "A"]},
502 {"!": {"exists": ["fields", "55"]}}
503 ]},
504 true,
505 false
506 ]
507 },
508 true
509 ]
510 }
511 },
512 {
513 "id": "C9",
514 "description": "If field 56a is present, field 57a must also be present",
515 "condition": {
516 "if": [
517 {"exists": ["fields", "56"]},
518 {"exists": ["fields", "57"]},
519 true
520 ]
521 }
522 },
523 {
524 "id": "C10",
525 "description": "If field 23B contains SPRI, field 56a must not be present. If field 23B contains SSTD or SPAY, field 56a may be used with option A or C only",
526 "condition": {
527 "and": [
528 {
529 "if": [
530 {"==": [{"var": "fields.23B.instruction_code"}, "SPRI"]},
531 {"!": {"exists": ["fields", "56"]}},
532 true
533 ]
534 },
535 {
536 "if": [
537 {"and": [
538 {"in": [{"var": "fields.23B.instruction_code"}, ["SSTD", "SPAY"]]},
539 {"or": [
540 {"exists": ["fields", "56", "A"]},
541 {"exists": ["fields", "56", "C"]},
542 {"exists": ["fields", "56", "D"]}
543 ]}
544 ]},
545 {"or": [
546 {"exists": ["fields", "56", "A"]},
547 {"exists": ["fields", "56", "C"]}
548 ]},
549 true
550 ]
551 }
552 ]
553 }
554 },
555 {
556 "id": "C11",
557 "description": "If field 23B contains SPRI, SSTD or SPAY, field 57a may be used with option A, C or D. In option D, Party Identifier is mandatory",
558 "condition": {
559 "if": [
560 {"and": [
561 {"in": [{"var": "fields.23B.instruction_code"}, ["SPRI", "SSTD", "SPAY"]]},
562 {"or": [
563 {"exists": ["fields", "57", "A"]},
564 {"exists": ["fields", "57", "B"]},
565 {"exists": ["fields", "57", "C"]},
566 {"exists": ["fields", "57", "D"]}
567 ]}
568 ]},
569 {"and": [
570 {"or": [
571 {"exists": ["fields", "57", "A"]},
572 {"exists": ["fields", "57", "C"]},
573 {"exists": ["fields", "57", "D"]}
574 ]},
575 {
576 "if": [
577 {"exists": ["fields", "57", "D"]},
578 {"exists": ["fields", "57", "D", "party_identifier"]},
579 true
580 ]
581 }
582 ]},
583 true
584 ]
585 }
586 },
587 {
588 "id": "C12",
589 "description": "If field 23B contains SPRI, SSTD or SPAY, Account in field 59a is mandatory",
590 "condition": {
591 "if": [
592 {"in": [{"var": "fields.23B.instruction_code"}, ["SPRI", "SSTD", "SPAY"]]},
593 {"or": [
594 {"exists": ["fields", "59", "A", "account"]},
595 {"exists": ["fields", "59", "NoOption", "account"]},
596 {"exists": ["fields", "59", "F", "party_identifier"]}
597 ]},
598 true
599 ]
600 }
601 },
602 {
603 "id": "C13",
604 "description": "If any field 23E contains CHQB, Account in field 59a is not allowed",
605 "condition": {
606 "if": [
607 {"and": [
608 {"exists": ["fields", "23E"]},
609 {
610 "some": [
611 {"var": "fields.23E"},
612 {"==": [{"var": "instruction_code"}, "CHQB"]}
613 ]
614 }
615 ]},
616 {"and": [
617 {"!": {"exists": ["fields", "59", "A", "account"]}},
618 {"!": {"exists": ["fields", "59", "NoOption", "account"]}}
619 ]},
620 true
621 ]
622 }
623 },
624 {
625 "id": "C14",
626 "description": "If field 71A contains OUR, then field 71F is not allowed and field 71G is optional. If field 71A contains SHA, then field 71F is optional and field 71G is not allowed. If field 71A contains BEN, then at least one occurrence of field 71F is mandatory and field 71G is not allowed",
627 "condition": {
628 "and": [
629 {
630 "if": [
631 {"==": [{"var": "fields.71A.code"}, "OUR"]},
632 {"!": {"exists": ["fields", "71F"]}},
633 true
634 ]
635 },
636 {
637 "if": [
638 {"==": [{"var": "fields.71A.code"}, "SHA"]},
639 {"!": {"exists": ["fields", "71G"]}},
640 true
641 ]
642 },
643 {
644 "if": [
645 {"==": [{"var": "fields.71A.code"}, "BEN"]},
646 {"and": [
647 {"exists": ["fields", "71F"]},
648 {">": [{"var": "fields.71F.length"}, 0]},
649 {"!": {"exists": ["fields", "71G"]}}
650 ]},
651 true
652 ]
653 }
654 ]
655 }
656 },
657 {
658 "id": "C15",
659 "description": "If either field 71F (at least one occurrence) or field 71G is present, then field 33B is mandatory",
660 "condition": {
661 "if": [
662 {"or": [
663 {"exists": ["fields", "71F"]},
664 {"exists": ["fields", "71G"]}
665 ]},
666 {"exists": ["fields", "33B"]},
667 true
668 ]
669 }
670 },
671 {
672 "id": "C16",
673 "description": "If field 56a is not present, no field 23E may contain TELI or PHOI",
674 "condition": {
675 "if": [
676 {"!": {"exists": ["fields", "56"]}},
677 {
678 "if": [
679 {"exists": ["fields", "23E"]},
680 {
681 "none": [
682 {"var": "fields.23E"},
683 {"in": [{"var": "instruction_code"}, ["TELI", "PHOI"]]}
684 ]
685 },
686 true
687 ]
688 },
689 true
690 ]
691 }
692 },
693 {
694 "id": "C17",
695 "description": "If field 57a is not present, no field 23E may contain TELE or PHON",
696 "condition": {
697 "if": [
698 {"!": {"exists": ["fields", "57"]}},
699 {
700 "if": [
701 {"exists": ["fields", "23E"]},
702 {
703 "none": [
704 {"var": "fields.23E"},
705 {"in": [{"var": "instruction_code"}, ["TELE", "PHON"]]}
706 ]
707 },
708 true
709 ]
710 },
711 true
712 ]
713 }
714 },
715 {
716 "id": "C18",
717 "description": "The currency code in the fields 71G and 32A must be the same",
718 "condition": {
719 "if": [
720 {"exists": ["fields", "71G"]},
721 {"==": [{"var": "fields.71G.currency"}, {"var": "fields.32A.currency"}]},
722 true
723 ]
724 }
725 },
726 {
727 "id": "INSTRUCTION_CODE_VALIDATION",
728 "description": "23E instruction codes must be valid when present",
729 "condition": {
730 "if": [
731 {"exists": ["fields", "23E"]},
732 {
733 "all": [
734 {"var": "fields.23E"},
735 {"in": [{"var": "instruction_code"}, ["CORT", "INTC", "REPA", "SDVA", "CHQB", "PHOB", "PHOI", "PHON", "TELE", "TELI", "TELB"]]}
736 ]
737 },
738 true
739 ]
740 }
741 },
742 {
743 "id": "AMOUNT_CONSISTENCY",
744 "description": "All amounts must be positive and properly formatted",
745 "condition": {
746 "and": [
747 {">": [{"var": "fields.32A.amount"}, 0]},
748 {
749 "if": [
750 {"exists": ["fields", "33B"]},
751 {">": [{"var": "fields.33B.amount"}, 0]},
752 true
753 ]
754 },
755 {
756 "if": [
757 {"exists": ["fields", "71F"]},
758 {
759 "all": [
760 {"var": "fields.71F"},
761 {">": [{"var": "amount"}, 0]}
762 ]
763 },
764 true
765 ]
766 },
767 {
768 "if": [
769 {"exists": ["fields", "71G"]},
770 {">": [{"var": "fields.71G.amount"}, 0]},
771 true
772 ]
773 }
774 ]
775 }
776 },
777 {
778 "id": "CURRENCY_CODE_VALIDATION",
779 "description": "All currency codes must be valid ISO 4217 3-letter codes",
780 "condition": {
781 "and": [
782 {"!=": [{"var": "fields.32A.currency"}, ""]},
783 {
784 "if": [
785 {"exists": ["fields", "33B"]},
786 {"!=": [{"var": "fields.33B.currency"}, ""]},
787 true
788 ]
789 },
790 {
791 "if": [
792 {"exists": ["fields", "71F"]},
793 {
794 "all": [
795 {"var": "fields.71F"},
796 {"!=": [{"var": "currency"}, ""]}
797 ]
798 },
799 true
800 ]
801 },
802 {
803 "if": [
804 {"exists": ["fields", "71G"]},
805 {"!=": [{"var": "fields.71G.currency"}, ""]},
806 true
807 ]
808 }
809 ]
810 }
811 },
812 {
813 "id": "REFERENCE_FORMAT",
814 "description": "Reference fields must not contain invalid patterns",
815 "condition": {
816 "and": [
817 {"!=": [{"var": "fields.20.reference"}, ""]},
818 {"!": {"in": ["//", {"var": "fields.20.reference"}]}}
819 ]
820 }
821 },
822 {
823 "id": "BIC_VALIDATION",
824 "description": "All BIC codes must be properly formatted (non-empty)",
825 "condition": {
826 "and": [
827 {"!=": [{"var": "basic_header.sender_bic"}, ""]},
828 {"!=": [{"var": "application_header.receiver_bic"}, ""]},
829 {
830 "if": [
831 {"exists": ["fields", "52", "A"]},
832 {"!=": [{"var": "fields.52.A.bic"}, ""]},
833 true
834 ]
835 },
836 {
837 "if": [
838 {"exists": ["fields", "53", "A"]},
839 {"!=": [{"var": "fields.53.A.bic"}, ""]},
840 true
841 ]
842 },
843 {
844 "if": [
845 {"exists": ["fields", "54", "A"]},
846 {"!=": [{"var": "fields.54.A.bic"}, ""]},
847 true
848 ]
849 },
850 {
851 "if": [
852 {"exists": ["fields", "55", "A"]},
853 {"!=": [{"var": "fields.55.A.bic"}, ""]},
854 true
855 ]
856 },
857 {
858 "if": [
859 {"exists": ["fields", "56", "A"]},
860 {"!=": [{"var": "fields.56.A.bic"}, ""]},
861 true
862 ]
863 },
864 {
865 "if": [
866 {"exists": ["fields", "57", "A"]},
867 {"!=": [{"var": "fields.57.A.bic"}, ""]},
868 true
869 ]
870 }
871 ]
872 }
873 },
874 {
875 "id": "REMIT_77T",
876 "description": "REMIT: If 77T is present, it must contain valid structured remittance information",
877 "condition": {
878 "if": [
879 {"exists": ["fields", "77T"]},
880 {"and": [
881 {"!=": [{"var": "fields.77T.envelope_content"}, ""]}
882 ]},
883 true
884 ]
885 }
886 }
887 ],
888 "constants": {
889 "EU_EEA_COUNTRIES": ["AD", "AT", "BE", "BG", "BV", "CH", "CY", "CZ", "DE", "DK", "ES", "EE", "FI", "FR", "GB", "GF", "GI", "GP", "GR", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MQ", "MT", "NL", "NO", "PL", "PM", "PT", "RE", "RO", "SE", "SI", "SJ", "SK", "SM", "TF", "VA"],
890 "VALID_BANK_OPERATION_CODES": ["CRED", "CRTS", "SPAY", "SPRI", "SSTD"],
891 "VALID_CHARGE_CODES": ["OUR", "SHA", "BEN"],
892 "VALID_INSTRUCTION_CODES": ["CORT", "INTC", "REPA", "SDVA", "CHQB", "PHOB", "PHOI", "PHON", "TELE", "TELI", "TELB"]
893 }
894}"#;