cbpr-usage-rules 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cbpr_rules/__init__.py +21 -0
- cbpr_rules/cli.py +176 -0
- cbpr_rules/engine.py +100 -0
- cbpr_rules/helpers.py +420 -0
- cbpr_rules/loader.py +77 -0
- cbpr_rules/message.py +170 -0
- cbpr_rules/models.py +83 -0
- cbpr_rules/py.typed +0 -0
- cbpr_rules/reference/__init__.py +9 -0
- cbpr_rules/reference/countries.py +28 -0
- cbpr_rules/reference/currencies.py +25 -0
- cbpr_rules/registry.py +107 -0
- cbpr_rules/rules/__init__.py +1 -0
- cbpr_rules/rules/y2025/__init__.py +1 -0
- cbpr_rules/rules/y2025/camt_052.py +224 -0
- cbpr_rules/rules/y2025/camt_054.py +176 -0
- cbpr_rules/rules/y2025/pacs_002.py +212 -0
- cbpr_rules/rules/y2025/pacs_004.py +831 -0
- cbpr_rules/rules/y2025/pacs_008.py +375 -0
- cbpr_rules/rules/y2025/pacs_008_stp.py +367 -0
- cbpr_rules/rules/y2025/pacs_009.py +273 -0
- cbpr_rules/rules/y2025/pacs_009_adv.py +255 -0
- cbpr_rules/rules/y2025/pacs_009_cov.py +358 -0
- cbpr_rules/rules/y2025/pain_001.py +306 -0
- cbpr_rules/rules/y2026/__init__.py +1 -0
- cbpr_rules/rules/y2026/camt_052.py +191 -0
- cbpr_rules/rules/y2026/camt_054.py +182 -0
- cbpr_rules/rules/y2026/pacs_002.py +208 -0
- cbpr_rules/rules/y2026/pacs_004.py +491 -0
- cbpr_rules/rules/y2026/pacs_008.py +377 -0
- cbpr_rules/rules/y2026/pacs_008_stp.py +369 -0
- cbpr_rules/rules/y2026/pacs_009.py +260 -0
- cbpr_rules/rules/y2026/pacs_009_adv.py +256 -0
- cbpr_rules/rules/y2026/pacs_009_cov.py +324 -0
- cbpr_rules/rules/y2026/pain_001.py +272 -0
- cbpr_rules/schema.py +97 -0
- cbpr_rules/validators/__init__.py +16 -0
- cbpr_rules/validators/bic.py +21 -0
- cbpr_rules/validators/country.py +11 -0
- cbpr_rules/validators/currency.py +11 -0
- cbpr_rules/validators/iban.py +26 -0
- cbpr_rules/validators/lei.py +17 -0
- cbpr_usage_rules-0.1.0.dist-info/METADATA +335 -0
- cbpr_usage_rules-0.1.0.dist-info/RECORD +48 -0
- cbpr_usage_rules-0.1.0.dist-info/WHEEL +5 -0
- cbpr_usage_rules-0.1.0.dist-info/entry_points.txt +2 -0
- cbpr_usage_rules-0.1.0.dist-info/licenses/LICENSE +21 -0
- cbpr_usage_rules-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""CBPR+ SR2026 usage rules for camt.054.001.08 (BankToCustomerDebitCreditNotification).
|
|
2
|
+
|
|
3
|
+
Rule numbers, names and descriptions are taken from the published usage
|
|
4
|
+
guideline's Rules sheet; XML paths are the short ISO 20022 tags from its
|
|
5
|
+
Full_View / XML Path column. The module mirrors the reference module
|
|
6
|
+
``cbpr_rules.rules.y2025.pacs_008``: combinator-built checks are registered via
|
|
7
|
+
the local ``reg`` helper, cross-field / bespoke logic is written as
|
|
8
|
+
``fn(msg, report)`` and decorated with ``@rule``, and non-mechanizable textual
|
|
9
|
+
rules are surfaced via ``advisory``.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from ...registry import advisory, rule
|
|
14
|
+
from ...validators import is_valid_bic, is_valid_country, is_valid_currency, is_valid_lei
|
|
15
|
+
from ...helpers import (
|
|
16
|
+
business_msg_id_carries_group_id,
|
|
17
|
+
each_value_valid,
|
|
18
|
+
header_msg_def_id_matches,
|
|
19
|
+
not_matching_pattern,
|
|
20
|
+
value_not_in,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
MT = "camt.054"
|
|
24
|
+
YEAR = 2026
|
|
25
|
+
ROOT = "/Document/BkToCstmrDbtCdtNtfctn"
|
|
26
|
+
NTFCTN = ROOT + "/Ntfctn"
|
|
27
|
+
ENTRY = NTFCTN + "/Ntry"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def reg(number: str, name: str, description: str, check) -> None:
|
|
31
|
+
"""Register a combinator-built check as a rule."""
|
|
32
|
+
rule(MT, YEAR, number, name, description)(check)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _values_match(msg, report, path_a, path_b, label):
|
|
36
|
+
a_nodes = msg.find(path_a)
|
|
37
|
+
if not a_nodes:
|
|
38
|
+
return
|
|
39
|
+
b_vals = {msg.text_of(n) for n in msg.find(path_b)}
|
|
40
|
+
if not b_vals:
|
|
41
|
+
return
|
|
42
|
+
a_vals = {msg.text_of(n) for n in a_nodes}
|
|
43
|
+
if a_vals != b_vals:
|
|
44
|
+
report(a_nodes[0], detail=label)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Formal rules
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
@rule(MT, YEAR, "R1", "CBPR_Copy_Duplicate_FormalRule",
|
|
52
|
+
"If Copy Duplicate indicator is used in the Business Application Header, it "
|
|
53
|
+
"must be identical to the Copy Duplicate indicator in the business document "
|
|
54
|
+
"(if the latter is present).")
|
|
55
|
+
def _r1(msg, report):
|
|
56
|
+
bah = "/AppHdr/CpyDplct"
|
|
57
|
+
doc = NTFCTN + "/CpyDplctInd"
|
|
58
|
+
if msg.present(bah) and msg.present(doc):
|
|
59
|
+
_values_match(msg, report, bah, doc,
|
|
60
|
+
"BAH CopyDuplicate must equal Notification CopyDuplicateIndicator")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@rule(MT, YEAR, "R2", "CBPR_BusinessMessageIdentifier_FormalRule",
|
|
64
|
+
"The Business Message Identifier must match the Message Identification in "
|
|
65
|
+
"the Group Header.")
|
|
66
|
+
def _r2(msg, report):
|
|
67
|
+
_values_match(msg, report, "/AppHdr/BizMsgIdr", ROOT + "/GrpHdr/MsgId",
|
|
68
|
+
"BusinessMessageIdentifier must equal GroupHeader MessageIdentification")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
reg("R8", "CBPR_PageNumber_FormalRule",
|
|
72
|
+
"The page number must be greater than zero (>0).",
|
|
73
|
+
value_not_in(NTFCTN + "/NtfctnPgntn/PgNb",
|
|
74
|
+
["0", "00", "000", "0000", "00000"]))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@rule(MT, YEAR, "R10", "CBPR_BookingDate_ValueDate_FormalRule",
|
|
78
|
+
"Either the BookingDate or the ValueDate, or both, must be present when the "
|
|
79
|
+
"Status is 'BOOK'.")
|
|
80
|
+
def _r10(msg, report):
|
|
81
|
+
for entry in msg.each(ENTRY):
|
|
82
|
+
codes = msg.values("Sts/Cd", entry)
|
|
83
|
+
if not codes or not all(c == "BOOK" for c in codes):
|
|
84
|
+
continue
|
|
85
|
+
if msg.absent("BookgDt", entry) and msg.absent("ValDt", entry):
|
|
86
|
+
report(entry, detail="BookingDate or ValueDate required when Status is BOOK")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
reg("R13", "CBPR_Original_Instruction_Identification_FormalRule",
|
|
90
|
+
"This element must not start or end with a slash '/' and must not contain two "
|
|
91
|
+
"consecutive slashes '//'.",
|
|
92
|
+
not_matching_pattern(ENTRY + "/NtryDtls/TxDtls/Refs/InstrId",
|
|
93
|
+
r"(/.*)|(.*/)|(.*//.*)"))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Algorithmic field validation (brief-required VAL-* checks), applied to the
|
|
98
|
+
# fields where these data types appear in camt.054.
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
reg("VAL-CCY", "CBPR_Valid_Entry_Amount_Currency",
|
|
101
|
+
"Entry Amount currency must be a valid ISO 4217 code.",
|
|
102
|
+
lambda msg, report: [
|
|
103
|
+
report(el, detail=f"invalid currency '{ccy}'")
|
|
104
|
+
for el, ccy in msg.attr_nodes(ENTRY + "/Amt", "Ccy")
|
|
105
|
+
if ccy and not is_valid_currency(ccy)
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
reg("VAL-BIC", "CBPR_Valid_Account_Servicer_BIC",
|
|
109
|
+
"Account Servicer BICFI must be a structurally valid BIC.",
|
|
110
|
+
each_value_valid(NTFCTN + "/Acct/Svcr/FinInstnId/BICFI", is_valid_bic, "BIC"))
|
|
111
|
+
|
|
112
|
+
reg("VAL-LEI", "CBPR_Valid_Account_Servicer_LEI",
|
|
113
|
+
"Account Servicer LEI must be a structurally valid ISO 17442 LEI.",
|
|
114
|
+
each_value_valid(NTFCTN + "/Acct/Svcr/FinInstnId/LEI", is_valid_lei, "LEI"))
|
|
115
|
+
|
|
116
|
+
reg("VAL-CTRY", "CBPR_Valid_Account_Servicer_Country",
|
|
117
|
+
"Account Servicer postal address Country must be a valid ISO 3166 code.",
|
|
118
|
+
each_value_valid(NTFCTN + "/Acct/Svcr/FinInstnId/PstlAdr/Ctry",
|
|
119
|
+
is_valid_country, "country"))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Mechanizable textual rules promoted from advisory to enforced. Both are
|
|
124
|
+
# conservative cross-schema (BAH vs Document) combinators that skip when the
|
|
125
|
+
# BAH, Document, or the inspected element is absent.
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
reg("R4", "CBPR_Business_Message_Identifier_TextualRule",
|
|
128
|
+
"The Business Message Identifier is the unique identifier of the Business "
|
|
129
|
+
"Message instance that is being transported with this header, as defined by "
|
|
130
|
+
"the sending application or system. Must contain the Message Identification "
|
|
131
|
+
"element from the Group Header of the underlying message, where available "
|
|
132
|
+
"(as is typically the case with pacs, pain, and camt messages, for example). "
|
|
133
|
+
"If Message Identification is not available in the underlying message, then "
|
|
134
|
+
"this element must contain the unique identifier of the Business Message "
|
|
135
|
+
"instance.",
|
|
136
|
+
business_msg_id_carries_group_id())
|
|
137
|
+
|
|
138
|
+
reg("R5", "CBPR_Message_Definition_Identifier_TextualRule",
|
|
139
|
+
"The Message Definition Identifier of the Business Message instance that is "
|
|
140
|
+
"being transported with this header. In general, it must be formatted exactly "
|
|
141
|
+
"as it appears in the namespace of the Business Message instance.",
|
|
142
|
+
header_msg_def_id_matches())
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Advisory textual rules (not mechanically enforceable - surfaced as guidance)
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
_ADVISORY = {
|
|
149
|
+
"R3": ("CBPR_Related_Business_Application_Header_TextualRule",
|
|
150
|
+
"If used, the Related BAH must transport the exact same information as "
|
|
151
|
+
"in the BAH of the related message."),
|
|
152
|
+
"R6": ("CBPR_Related_BAH_Business_Service_TextualRule",
|
|
153
|
+
"If related BAH is present, it should transport the element Business "
|
|
154
|
+
"Service."),
|
|
155
|
+
"R7": ("CBPR_Additional_Information_TextualRule",
|
|
156
|
+
"May be used to indicate type of Notification. Where this is used, all "
|
|
157
|
+
"transactions within this message are of the same type. Codes are for "
|
|
158
|
+
"example: /LBOX/ - Lock box /BULK/ - Bulk reporting (batch transaction "
|
|
159
|
+
"with underlying transactions) /RTRN/ - Return report /CRED/ - "
|
|
160
|
+
"Notification with Credit entries ONLY. /CRED/ will be provided by the "
|
|
161
|
+
"Bank to the Corporate in case of Instant Payment receivable."),
|
|
162
|
+
"R9": ("CBPR_Copy_Duplicate_Indicator_TextualRule",
|
|
163
|
+
"If Applicable, for Copy or Duplicate, the electronic sequence and "
|
|
164
|
+
"legal sequence must be the same as the original report."),
|
|
165
|
+
"R11": ("CBPR_Reversal_Indicator_TextualRule",
|
|
166
|
+
"Value is TRUE or FALSE. Should only be shown if TRUE."),
|
|
167
|
+
"R12": ("CBPR_Charges_TextualRule",
|
|
168
|
+
"Charges applied to Entry level amount only for a batch booked "
|
|
169
|
+
"amount. When batch booked Entry has underlying transactions and "
|
|
170
|
+
"charges are applicable, Entry level AmountDetails is used for "
|
|
171
|
+
"totaling the underlying transaction amounts and charges."),
|
|
172
|
+
"R14": ("CBPR_UETR_TextualRule",
|
|
173
|
+
"If the underlying transaction contains/owns a UETR then it should be "
|
|
174
|
+
"reported in the camt.054 message."),
|
|
175
|
+
"R15": ("CBPR_Charges_TextualRule",
|
|
176
|
+
"Charges against the amount reported at Entry level (single, batch or "
|
|
177
|
+
"aggregate amount booking). When batch booked Entry has underlying "
|
|
178
|
+
"transactions with charges, the charges will be shown against each "
|
|
179
|
+
"entry detail amount."),
|
|
180
|
+
}
|
|
181
|
+
for _num, (_name, _desc) in _ADVISORY.items():
|
|
182
|
+
advisory(MT, YEAR, _num, _name, _desc)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""CBPR+ SR2026 usage rules for pacs.002.001.10 (FIToFIPaymentStatusReport).
|
|
2
|
+
|
|
3
|
+
Rule numbers, names and descriptions are taken from the published usage
|
|
4
|
+
guideline's Rules sheet; XML paths are the short ISO 20022 tags from its
|
|
5
|
+
Full_View / XML Path column. Structure mirrors the reference module
|
|
6
|
+
``rules/y2025/pacs_008.py``.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re as _re
|
|
11
|
+
|
|
12
|
+
from ...registry import advisory, rule
|
|
13
|
+
from ...validators import is_valid_bic, is_valid_currency
|
|
14
|
+
from ...helpers import (
|
|
15
|
+
business_msg_id_carries_group_id,
|
|
16
|
+
header_msg_def_id_matches,
|
|
17
|
+
no_postal_address_duplication,
|
|
18
|
+
not_matching_pattern,
|
|
19
|
+
requires_if_present,
|
|
20
|
+
required_when_absent,
|
|
21
|
+
same_value,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
MT = "pacs.002"
|
|
25
|
+
YEAR = 2026
|
|
26
|
+
ROOT = "/Document/FIToFIPmtStsRpt"
|
|
27
|
+
TX = ROOT + "/TxInfAndSts"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def reg(number: str, name: str, description: str, check) -> None:
|
|
31
|
+
"""Register a combinator-built check as a rule."""
|
|
32
|
+
rule(MT, YEAR, number, name, description)(check)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Cross-schema BIC / identifier matching (BAH vs Document)
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def _values_match(msg, report, path_a, path_b, label):
|
|
40
|
+
a_nodes = msg.find(path_a)
|
|
41
|
+
if not a_nodes:
|
|
42
|
+
return
|
|
43
|
+
b_vals = {msg.text_of(n) for n in msg.find(path_b)}
|
|
44
|
+
if not b_vals:
|
|
45
|
+
return
|
|
46
|
+
a_vals = {msg.text_of(n) for n in a_nodes}
|
|
47
|
+
if a_vals != b_vals:
|
|
48
|
+
report(a_nodes[0], detail=label)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@rule(MT, YEAR, "R1", "CBPR_To_Instructed_Agent_BIC_1_FormalRule",
|
|
52
|
+
'BAH "To" BIC must match "Instructed Agent" BIC, except where BAH '
|
|
53
|
+
"CopyDuplicate = COPY or = CODU.")
|
|
54
|
+
def _r1(msg, report):
|
|
55
|
+
if any(v in {"COPY", "CODU"} for v in msg.values("/AppHdr/CpyDplct")):
|
|
56
|
+
return
|
|
57
|
+
_values_match(msg, report,
|
|
58
|
+
"/AppHdr/To/FIId/FinInstnId/BICFI",
|
|
59
|
+
TX + "/InstdAgt/FinInstnId/BICFI",
|
|
60
|
+
"To BIC must equal Instructed Agent BIC")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@rule(MT, YEAR, "R2", "CBPR_To_Instructed_Agent_BIC_2_FormalRule",
|
|
64
|
+
'BAH "To" BIC must match "Instructed Agent" BIC if CopyDuplicate is absent.')
|
|
65
|
+
def _r2(msg, report):
|
|
66
|
+
if not msg.absent("/AppHdr/CpyDplct"):
|
|
67
|
+
return
|
|
68
|
+
_values_match(msg, report,
|
|
69
|
+
"/AppHdr/To/FIId/FinInstnId/BICFI",
|
|
70
|
+
TX + "/InstdAgt/FinInstnId/BICFI",
|
|
71
|
+
"To BIC must equal Instructed Agent BIC")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
reg("R3", "CBPR_BusinessMessageIdentifier_FormalRule",
|
|
75
|
+
"The Business Message Identifier must match the Message Identification in "
|
|
76
|
+
"the Group Header.",
|
|
77
|
+
same_value("/AppHdr/BizMsgIdr", ROOT + "/GrpHdr/MsgId"))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
reg("R4", "CBPR_From_Instructing_Agent_BIC_FormalRule",
|
|
81
|
+
'BAH "From" BIC must match "Instructing Agent" BIC.',
|
|
82
|
+
same_value("/AppHdr/Fr/FIId/FinInstnId/BICFI",
|
|
83
|
+
TX + "/InstgAgt/FinInstnId/BICFI"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Transaction status / reject handling
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
@rule(MT, YEAR, "R9", "CBPR_Transaction_Status_Reject_Reason_FormalRule",
|
|
91
|
+
'If TransactionStatus/Code equals RJCT, then "Status Reason Information/'
|
|
92
|
+
'Reason" is mandatory.')
|
|
93
|
+
def _r9(msg, report):
|
|
94
|
+
for tx in msg.each(TX):
|
|
95
|
+
if "RJCT" in msg.values("TxSts", tx) and msg.absent("StsRsnInf/Rsn", tx):
|
|
96
|
+
report(tx, detail="StatusReasonInformation/Reason required when TransactionStatus is RJCT")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@rule(MT, YEAR, "R10", "CBPR_Transaction_Status_Reject_Effective_Sett_Date_FormalRule",
|
|
100
|
+
"If TransactionStatus is 'RJCT' then Effective Interbank Settlement Date "
|
|
101
|
+
"is not allowed.")
|
|
102
|
+
def _r10(msg, report):
|
|
103
|
+
for tx in msg.each(TX):
|
|
104
|
+
if "RJCT" in msg.values("TxSts", tx) and msg.present("FctvIntrBkSttlmDt", tx):
|
|
105
|
+
report(tx, detail="EffectiveInterbankSettlementDate not allowed when TransactionStatus is RJCT")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@rule(MT, YEAR, "R12", "CBPR_OriginalMessageNameIdentification_FormalRule",
|
|
109
|
+
"This element must be populated with either pacs.008.001.xx, "
|
|
110
|
+
"pacs.009.001.xx, pacs.004.001.xx, pacs.003.001.xx or pacs.010.001.xx.")
|
|
111
|
+
def _r12(msg, report):
|
|
112
|
+
rx = _re.compile(r"pacs\.00[8349]\.001\.[0-9]{2}|pacs\.010\.001\.[0-9]{2}")
|
|
113
|
+
for node in msg.find(TX + "/OrgnlGrpInf/OrgnlMsgNmId"):
|
|
114
|
+
val = msg.text_of(node)
|
|
115
|
+
if val and not rx.fullmatch(val):
|
|
116
|
+
report(node, detail=f"'{val}' is not an allowed OriginalMessageNameIdentification")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
reg("R14", "CBPR_Original_Instruction_Identification_FormalRule",
|
|
120
|
+
"This field must not start or end with a slash '/' and must not contain two "
|
|
121
|
+
"consecutive slashes '//'.",
|
|
122
|
+
not_matching_pattern(TX + "/OrgnlInstrId", r"(/.*)|(.*/)|(.*//.*)"))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Status Reason Information / Originator party rules
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
_ORGTR = TX + "/StsRsnInf/Orgtr"
|
|
129
|
+
|
|
130
|
+
reg("R19", "CBPR_Party_Name_Postal_Address_FormalRule",
|
|
131
|
+
"If Postal Address is present then Name is mandatory. Recommendation: If "
|
|
132
|
+
"present, the BIC (AnyBIC) will always take precedence in case of "
|
|
133
|
+
"conflicting information.",
|
|
134
|
+
requires_if_present(_ORGTR, "PstlAdr", "Nm"))
|
|
135
|
+
|
|
136
|
+
reg("R20", "CBPR_Party_Name_Any_BIC_FormalRule",
|
|
137
|
+
"If AnyBIC is absent, then Name is mandatory.",
|
|
138
|
+
required_when_absent(_ORGTR, "Id/OrgId/AnyBIC", ["Nm"]))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# Algorithmic field validation (project brief) - only fields present here.
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
reg("VAL-CCY", "CBPR_Valid_Original_Interbank_Settlement_Currency",
|
|
145
|
+
"Original Interbank Settlement Amount currency must be a valid ISO 4217 code.",
|
|
146
|
+
lambda msg, report: [
|
|
147
|
+
report(el, detail=f"invalid currency '{ccy}'")
|
|
148
|
+
for el, ccy in msg.attr_nodes(TX + "/OrgnlTxRef/IntrBkSttlmAmt", "Ccy")
|
|
149
|
+
if ccy and not is_valid_currency(ccy)
|
|
150
|
+
])
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@rule(MT, YEAR, "VAL-BIC", "CBPR_Valid_Agent_BIC",
|
|
154
|
+
"Every Instructing/Instructed Agent BICFI must be a structurally valid BIC.")
|
|
155
|
+
def _val_bic(msg, report):
|
|
156
|
+
for path in (TX + "/InstgAgt/FinInstnId/BICFI", TX + "/InstdAgt/FinInstnId/BICFI"):
|
|
157
|
+
for node in msg.find(path):
|
|
158
|
+
val = msg.text_of(node)
|
|
159
|
+
if val and not is_valid_bic(val):
|
|
160
|
+
report(node, detail=f"invalid BIC: '{val}'")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Mechanizable textual rules promoted to enforced checks (conservative: each
|
|
165
|
+
# combinator skips when its inputs are absent, so valid messages never fail).
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
reg("R6", "CBPR_Business_Message_Identifier_TextualRule",
|
|
168
|
+
"The Business Message Identifier is the unique identifier of the Business "
|
|
169
|
+
"Message instance that is being transported with this header, as defined by "
|
|
170
|
+
"the sending application or system. Must contain the Message Identification "
|
|
171
|
+
"element from the Group Header of the underlying message, where available.",
|
|
172
|
+
business_msg_id_carries_group_id())
|
|
173
|
+
|
|
174
|
+
reg("R7", "CBPR_Message_Definition_Identifier_TextualRule",
|
|
175
|
+
"The Message Definition Identifier of the Business Message instance that is "
|
|
176
|
+
"being transported with this header. In general, it must be formatted exactly "
|
|
177
|
+
"as it appears in the namespace of the Business Message instance.",
|
|
178
|
+
header_msg_def_id_matches())
|
|
179
|
+
|
|
180
|
+
reg("R21", "CBPR_Duplication_Postal_Address_TextualRule",
|
|
181
|
+
"Data present in structured elements within the Postal Address must not, "
|
|
182
|
+
"under any circumstances be repeated in AddressLine.",
|
|
183
|
+
no_postal_address_duplication())
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# Advisory textual rules (not mechanically enforceable - surfaced as guidance)
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
_ADVISORY = {
|
|
190
|
+
"R5": ("CBPR_Related_Business_Application_Header_TextualRule",
|
|
191
|
+
"If used, the Related BAH must transport the exact same information as in the BAH of the related message."),
|
|
192
|
+
"R8": ("CBPR_Related_BAH_Business_Service_TextualRule",
|
|
193
|
+
"If related BAH is present, it should transport the element Business Service."),
|
|
194
|
+
"R11": ("CBPR_Original_Message_Identification_TextualRule",
|
|
195
|
+
"Original Message Identification must transport the Message Identification of the underlying payment (e.g. pacs.008/pacs.009/pacs.004)."),
|
|
196
|
+
"R13": ("CBPR_Original_Instruction_Identification_TextualRule",
|
|
197
|
+
"Should transport the Instruction Identification of the underlying payment message for example pacs.008/pacs.009 or the same Original Instruction Identification if present in pacs.004."),
|
|
198
|
+
"R15": ("CBPR_Original_End_To_End_Identification_TextualRule",
|
|
199
|
+
"Should transport the EndToEnd Identification of the underlying payment message for example pacs.008/pacs.009 or the same Original EndToEnd Identification as in the pacs.004."),
|
|
200
|
+
"R16": ("CBPR_Original_Transaction_Identification_TextualRule",
|
|
201
|
+
"Should transport the Transaction Identification of the underlying payment message for example pacs.008/pacs.009 when present, or the same Original Transaction Identification if present in pacs.004."),
|
|
202
|
+
"R17": ("CBPR_Original_UETR_TextualRule",
|
|
203
|
+
"Must transport the UETR of the underlying pacs.008/pacs.009."),
|
|
204
|
+
"R18": ("CBPR_Originator_Identification_TextualRule",
|
|
205
|
+
"If AnyBIC is present, in addition to any other optional elements, in case of conflicting information it will always take precedence."),
|
|
206
|
+
}
|
|
207
|
+
for _num, (_name, _desc) in _ADVISORY.items():
|
|
208
|
+
advisory(MT, YEAR, _num, _name, _desc)
|