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,256 @@
|
|
|
1
|
+
"""CBPR+ SR2026 usage rules for pacs.009.001.08 ADV (FinancialInstitutionCreditTransfer, Advice).
|
|
2
|
+
|
|
3
|
+
Authored against the published usage guideline's Rules sheet. Each R-index is
|
|
4
|
+
registered with its real rule number, name token and description; formal rules
|
|
5
|
+
use shared combinators where the shape matches, bespoke ``fn(msg, report)`` for
|
|
6
|
+
cross-field / cross-schema logic. Textual rules are enforced where mechanizable,
|
|
7
|
+
otherwise surfaced as advisories.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from ...registry import advisory, rule
|
|
12
|
+
from ...validators import is_valid_bic, is_valid_currency
|
|
13
|
+
from ...helpers import (
|
|
14
|
+
not_matching_pattern,
|
|
15
|
+
presence_together,
|
|
16
|
+
header_msg_def_id_matches,
|
|
17
|
+
business_msg_id_carries_group_id,
|
|
18
|
+
no_postal_address_duplication,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
MT = "pacs.009_adv"
|
|
22
|
+
YEAR = 2026
|
|
23
|
+
ROOT = "/Document/FICdtTrf"
|
|
24
|
+
TX = ROOT + "/CdtTrfTxInf"
|
|
25
|
+
|
|
26
|
+
D_AGENT_NAME_ADR = "Name and Address must always be present together."
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def reg(number: str, name: str, description: str, check) -> None:
|
|
30
|
+
"""Register a combinator-built check as a rule."""
|
|
31
|
+
rule(MT, YEAR, number, name, description)(check)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _values_match(msg, report, path_a, path_b, label):
|
|
35
|
+
a_nodes = msg.find(path_a)
|
|
36
|
+
if not a_nodes:
|
|
37
|
+
return
|
|
38
|
+
b_vals = {msg.text_of(n) for n in msg.find(path_b)}
|
|
39
|
+
if not b_vals:
|
|
40
|
+
return
|
|
41
|
+
a_vals = {msg.text_of(n) for n in a_nodes}
|
|
42
|
+
if a_vals != b_vals:
|
|
43
|
+
report(a_nodes[0], detail=label)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Bespoke cross-field / cross-schema rules
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
@rule(MT, YEAR, "R1", "CBPR_BusinessMessageIdentifier_FormalRule",
|
|
50
|
+
"The Business Message Identifier must match the Message Identification in "
|
|
51
|
+
"the Group Header.")
|
|
52
|
+
def _r1(msg, report):
|
|
53
|
+
_values_match(msg, report, "/AppHdr/BizMsgIdr", ROOT + "/GrpHdr/MsgId",
|
|
54
|
+
"BusinessMessageIdentifier must equal GroupHeader/MessageIdentification")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@rule(MT, YEAR, "R2", "CBPR_Priority_Instruction_Priority_FormalRule",
|
|
58
|
+
'If "Priority" is used in the BAH for pacs messages, the value should be '
|
|
59
|
+
'identical to the one in the "Payment Type Information/InstructionPriority" '
|
|
60
|
+
"if present.")
|
|
61
|
+
def _r2(msg, report):
|
|
62
|
+
if msg.present("/AppHdr/Prty") and msg.present(TX + "/PmtTpInf/InstrPrty"):
|
|
63
|
+
_values_match(msg, report, "/AppHdr/Prty", TX + "/PmtTpInf/InstrPrty",
|
|
64
|
+
"BAH Priority must equal InstructionPriority")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@rule(MT, YEAR, "R3", "CBPR_To_Instructed_Agent_BIC_2_FormalRule",
|
|
68
|
+
'BAH "To" BIC must match "Instructed Agent" BIC if CopyDuplicate is absent.')
|
|
69
|
+
def _r3(msg, report):
|
|
70
|
+
if not msg.absent("/AppHdr/CpyDplct"):
|
|
71
|
+
return
|
|
72
|
+
_values_match(msg, report, "/AppHdr/To/FIId/FinInstnId/BICFI",
|
|
73
|
+
TX + "/InstdAgt/FinInstnId/BICFI", "To vs Instructed Agent")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@rule(MT, YEAR, "R4", "CBPR_To_Instructed_Agent_BIC_1_FormalRule",
|
|
77
|
+
'BAH "To" BIC must match "Instructed Agent" BIC, except where BAH '
|
|
78
|
+
"CopyDuplicate = COPY or = CODU")
|
|
79
|
+
def _r4(msg, report):
|
|
80
|
+
if any(v in {"COPY", "CODU"} for v in msg.values("/AppHdr/CpyDplct")):
|
|
81
|
+
return
|
|
82
|
+
_values_match(msg, report, "/AppHdr/To/FIId/FinInstnId/BICFI",
|
|
83
|
+
TX + "/InstdAgt/FinInstnId/BICFI", "To vs Instructed Agent")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@rule(MT, YEAR, "R5", "CBPR_From_Instructing_Agent_BIC_FormalRule",
|
|
87
|
+
'BAH "From" BIC must match "Instructing Agent" BIC')
|
|
88
|
+
def _r5(msg, report):
|
|
89
|
+
_values_match(msg, report, "/AppHdr/Fr/FIId/FinInstnId/BICFI",
|
|
90
|
+
TX + "/InstgAgt/FinInstnId/BICFI", "From vs Instructing Agent")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@rule(MT, YEAR, "R10", "CBPR_GPI_ServiceLevel_Code_FormalRule",
|
|
94
|
+
"The GPI ServiceLevel Code in pacs.009 ADV must be 'G004'.")
|
|
95
|
+
def _r10(msg, report):
|
|
96
|
+
for tx in msg.each(TX):
|
|
97
|
+
if msg.absent("PmtTpInf/SvcLvl", tx):
|
|
98
|
+
continue
|
|
99
|
+
for code in msg.find("PmtTpInf/SvcLvl/Cd", tx):
|
|
100
|
+
if msg.text_of(code) in {"G001", "G002", "G003", "G005", "G006", "G007", "G009"}:
|
|
101
|
+
report(code, detail="ServiceLevel/Code must be 'G004'")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Reimbursement agents (Group Header / Settlement Information): FinInstnId agents.
|
|
105
|
+
reg("R15", "CBPR_Agent_Name_Postal_Address_FormalRule", D_AGENT_NAME_ADR,
|
|
106
|
+
presence_together(ROOT + "/GrpHdr/SttlmInf/InstgRmbrsmntAgt/FinInstnId", "Nm", "PstlAdr"))
|
|
107
|
+
reg("R17", "CBPR_Agent_Name_Postal_Address_FormalRule", D_AGENT_NAME_ADR,
|
|
108
|
+
presence_together(ROOT + "/GrpHdr/SttlmInf/InstdRmbrsmntAgt/FinInstnId", "Nm", "PstlAdr"))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@rule(MT, YEAR, "R18", "CBPR_Instruction_For_Creditor_Presence_Code_FormalRule",
|
|
112
|
+
"Each code can only be used once for element Instruction For Creditor Agent.")
|
|
113
|
+
def _r18(msg, report):
|
|
114
|
+
for tx in msg.each(TX):
|
|
115
|
+
codes = msg.values("InstrForCdtrAgt/Cd", tx)
|
|
116
|
+
if len(codes) != len(set(codes)):
|
|
117
|
+
report(tx, detail="duplicate InstructionForCreditorAgent code")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
reg("R19", "CBPR_Instruction_Identification_FormalRule",
|
|
121
|
+
"This element must not start or end with a slash '/' and must not contain "
|
|
122
|
+
"two consecutive slashes '//'.",
|
|
123
|
+
not_matching_pattern(TX + "/PmtId/InstrId", r"(/.*)|(.*/)|(.*//.*)"))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@rule(MT, YEAR, "R20", "CBPR_End_To_End_Identification_FormalRule",
|
|
127
|
+
"In the E2E identification, the below restrictions apply to the first 16 "
|
|
128
|
+
"characters: - The first one and the 16th one cannot be “/” and - "
|
|
129
|
+
"The string of 16 characters cannot contain “//”")
|
|
130
|
+
def _r20(msg, report):
|
|
131
|
+
import re as _re
|
|
132
|
+
pats = [_re.compile(r"/.*"), _re.compile(r".{15}/.*"), _re.compile(r".{0,14}//.*")]
|
|
133
|
+
for node in msg.find(TX + "/PmtId/EndToEndId"):
|
|
134
|
+
val = msg.text_of(node)
|
|
135
|
+
if val and any(p.fullmatch(val) for p in pats):
|
|
136
|
+
report(node, detail="EndToEndIdentification matches a forbidden pattern")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
reg("R21", "CBPR_Interbank_Settlement_Currency_FormalRule",
|
|
140
|
+
"The codes XAU, XAG, XPD and XPT are not allowed, as these are codes are "
|
|
141
|
+
"only used for commodities.",
|
|
142
|
+
lambda msg, report: [
|
|
143
|
+
report(el, detail=f"commodity currency '{ccy}' not allowed")
|
|
144
|
+
for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
|
|
145
|
+
if ccy in {"XAU", "XAG", "XPD", "XPT"}
|
|
146
|
+
])
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Transaction-chain agents / FI parties: Name + Postal Address presence-together.
|
|
150
|
+
_AGENT_NAME_ADR = {
|
|
151
|
+
"R22": TX + "/PrvsInstgAgt1/FinInstnId",
|
|
152
|
+
"R23": TX + "/PrvsInstgAgt2/FinInstnId",
|
|
153
|
+
"R24": TX + "/PrvsInstgAgt3/FinInstnId",
|
|
154
|
+
"R25": TX + "/IntrmyAgt1/FinInstnId",
|
|
155
|
+
"R26": TX + "/IntrmyAgt2/FinInstnId",
|
|
156
|
+
"R27": TX + "/IntrmyAgt3/FinInstnId",
|
|
157
|
+
"R28": TX + "/Dbtr/FinInstnId",
|
|
158
|
+
"R29": TX + "/DbtrAgt/FinInstnId",
|
|
159
|
+
"R30": TX + "/CdtrAgt/FinInstnId",
|
|
160
|
+
"R31": TX + "/Cdtr/FinInstnId",
|
|
161
|
+
}
|
|
162
|
+
for _num, _path in _AGENT_NAME_ADR.items():
|
|
163
|
+
reg(_num, "CBPR_Agent_Name_Postal_Address_FormalRule", D_AGENT_NAME_ADR,
|
|
164
|
+
presence_together(_path, "Nm", "PstlAdr"))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Algorithmic field validation (brief), only for fields present in pacs.009 ADV.
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
|
|
171
|
+
"Interbank Settlement Amount currency must be a valid ISO 4217 code.",
|
|
172
|
+
lambda msg, report: [
|
|
173
|
+
report(el, detail=f"invalid currency '{ccy}'")
|
|
174
|
+
for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
|
|
175
|
+
if ccy and not is_valid_currency(ccy)
|
|
176
|
+
])
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@rule(MT, YEAR, "VAL-BIC", "CBPR_Valid_Agent_BIC",
|
|
180
|
+
"Every Agent BICFI in the message must be a structurally valid BIC.")
|
|
181
|
+
def _val_bic(msg, report):
|
|
182
|
+
paths = [
|
|
183
|
+
"/AppHdr/Fr/FIId/FinInstnId/BICFI",
|
|
184
|
+
"/AppHdr/To/FIId/FinInstnId/BICFI",
|
|
185
|
+
ROOT + "/GrpHdr/SttlmInf/InstgRmbrsmntAgt/FinInstnId/BICFI",
|
|
186
|
+
ROOT + "/GrpHdr/SttlmInf/InstdRmbrsmntAgt/FinInstnId/BICFI",
|
|
187
|
+
TX + "/InstgAgt/FinInstnId/BICFI",
|
|
188
|
+
TX + "/InstdAgt/FinInstnId/BICFI",
|
|
189
|
+
TX + "/PrvsInstgAgt1/FinInstnId/BICFI",
|
|
190
|
+
TX + "/PrvsInstgAgt2/FinInstnId/BICFI",
|
|
191
|
+
TX + "/PrvsInstgAgt3/FinInstnId/BICFI",
|
|
192
|
+
TX + "/IntrmyAgt1/FinInstnId/BICFI",
|
|
193
|
+
TX + "/IntrmyAgt2/FinInstnId/BICFI",
|
|
194
|
+
TX + "/IntrmyAgt3/FinInstnId/BICFI",
|
|
195
|
+
TX + "/Dbtr/FinInstnId/BICFI",
|
|
196
|
+
TX + "/DbtrAgt/FinInstnId/BICFI",
|
|
197
|
+
TX + "/CdtrAgt/FinInstnId/BICFI",
|
|
198
|
+
TX + "/Cdtr/FinInstnId/BICFI",
|
|
199
|
+
]
|
|
200
|
+
for p in paths:
|
|
201
|
+
for node in msg.find(p):
|
|
202
|
+
val = msg.text_of(node)
|
|
203
|
+
if val and not is_valid_bic(val):
|
|
204
|
+
report(node, detail=f"invalid BIC: '{val}'")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Promoted from advisory: mechanizable header / postal-address checks.
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
reg("R7", "CBPR_Business_Message_Identifier_TextualRule",
|
|
211
|
+
"The Business Message Identifier is the unique identifier of the "
|
|
212
|
+
"Business Message instance that is being transported with this header, "
|
|
213
|
+
"as defined by the sending application or system. Must contain the "
|
|
214
|
+
"Message Identification element from the Group Header.",
|
|
215
|
+
business_msg_id_carries_group_id())
|
|
216
|
+
|
|
217
|
+
reg("R8", "CBPR_Message_Definition_Identifier_TextualRule",
|
|
218
|
+
"The Message Definition Identifier of the Business Message instance "
|
|
219
|
+
"that is being transported with this header. In general, it must be "
|
|
220
|
+
"formatted exactly as it appears in the namespace of the Business "
|
|
221
|
+
"Message instance.",
|
|
222
|
+
header_msg_def_id_matches())
|
|
223
|
+
|
|
224
|
+
reg("R16", "CBPR_Duplication_Postal_Address_TextualRule",
|
|
225
|
+
"Data present in structured elements within the Postal Address must "
|
|
226
|
+
"not, under any circumstances be repeated in AddressLine.",
|
|
227
|
+
no_postal_address_duplication())
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# Advisory textual rules (not mechanically enforceable).
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
_ADVISORY = {
|
|
234
|
+
"R6": ("CBPR_Related_Business_Application_Header_TextualRule",
|
|
235
|
+
"If used, the Related BAH must transport the exact same information as "
|
|
236
|
+
"in the BAH of the related message."),
|
|
237
|
+
"R9": ("CBPR_Related_BAH_Business_Service_TextualRule",
|
|
238
|
+
"If related BAH is present, it should transport the element Business "
|
|
239
|
+
"Service."),
|
|
240
|
+
"R11": ("CBPR_Agent_Option_3_TextualRule",
|
|
241
|
+
"Name AND ([Structured postal address with minimum Town Name and "
|
|
242
|
+
"Country] OR [Hybrid postal address with minimum Town Name and "
|
|
243
|
+
"Country]). It is recommended to also add the post code when available."),
|
|
244
|
+
"R12": ("CBPR_Agent_Option_2_TextualRule",
|
|
245
|
+
"(Clearing Code OR LEI) AND (Name AND ([Structured postal address "
|
|
246
|
+
"with minimum Town Name and Country] OR [Hybrid postal address with "
|
|
247
|
+
"minimum Town Name and Country]). It is recommended to also add the "
|
|
248
|
+
"post code when available."),
|
|
249
|
+
"R13": ("CBPR_Agent_Option_1_TextualRule",
|
|
250
|
+
"BICFI, complemented optionally with a LEI (preferred option)"),
|
|
251
|
+
"R14": ("CBPR_Agent_National_only_TextualRule",
|
|
252
|
+
"Whenever Debtor Agent, Creditor Agent and all agents in between are "
|
|
253
|
+
"located within the same country, the clearing code only may be used."),
|
|
254
|
+
}
|
|
255
|
+
for _num, (_name, _desc) in _ADVISORY.items():
|
|
256
|
+
advisory(MT, YEAR, _num, _name, _desc)
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""CBPR+ SR2026 usage rules for pacs.009.001.08 COV (FinancialInstitutionCreditTransfer, cover).
|
|
2
|
+
|
|
3
|
+
Authored following the pacs.008 reference module: each rule is registered with
|
|
4
|
+
its source rule number, name and description, implemented either with a shared
|
|
5
|
+
combinator from ``helpers`` or a bespoke ``fn(msg, report)``.
|
|
6
|
+
|
|
7
|
+
Rule numbers/text are from the published usage guideline's Rules sheet; XML paths
|
|
8
|
+
are the short ISO 20022 tags from its Full_View / XML Path column.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ...registry import advisory, rule
|
|
13
|
+
from ...validators import is_valid_bic, is_valid_currency
|
|
14
|
+
from ...helpers import (
|
|
15
|
+
bic_presence_exclusive,
|
|
16
|
+
business_msg_id_carries_group_id,
|
|
17
|
+
each_value_valid,
|
|
18
|
+
header_msg_def_id_matches,
|
|
19
|
+
mutually_exclusive,
|
|
20
|
+
no_postal_address_duplication,
|
|
21
|
+
not_matching_pattern,
|
|
22
|
+
presence_together,
|
|
23
|
+
required_when_absent,
|
|
24
|
+
requires_if_present,
|
|
25
|
+
structured_remittance_max_total,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
MT = "pacs.009_cov"
|
|
29
|
+
YEAR = 2026
|
|
30
|
+
ROOT = "/Document/FICdtTrf"
|
|
31
|
+
TX = ROOT + "/CdtTrfTxInf"
|
|
32
|
+
UND = TX + "/UndrlygCstmrCdtTrf"
|
|
33
|
+
|
|
34
|
+
# Repeated rule descriptions (identical across the locations they apply to).
|
|
35
|
+
D_AGENT_NAME_ADR = "Name and Address must always be present together."
|
|
36
|
+
D_PARTY_NAME_ADR = "If Postal Address is present then Name is mandatory."
|
|
37
|
+
D_PARTY_ANY_BIC = (
|
|
38
|
+
"If AnyBIC is absent then Name is mandatory and it is recommended to also "
|
|
39
|
+
"provide the Postal Address."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def reg(number: str, name: str, description: str, check) -> None:
|
|
44
|
+
"""Register a combinator-built check as a rule."""
|
|
45
|
+
rule(MT, YEAR, number, name, description)(check)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _agent_name_adr(number: str, fin_inst_path: str) -> None:
|
|
49
|
+
reg(number, "CBPR_Agent_Name_Postal_Address_FormalRule", D_AGENT_NAME_ADR,
|
|
50
|
+
presence_together(fin_inst_path, "Nm", "PstlAdr"))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _party_name_adr(number: str, party_path: str) -> None:
|
|
54
|
+
reg(number, "CBPR_Party_Name_Postal_Address_FormalRule", D_PARTY_NAME_ADR,
|
|
55
|
+
requires_if_present(party_path, "PstlAdr", "Nm"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _party_any_bic(number: str, party_path: str) -> None:
|
|
59
|
+
reg(number, "CBPR_Party_Name_Any_BIC_FormalRule", D_PARTY_ANY_BIC,
|
|
60
|
+
required_when_absent(party_path, "Id/OrgId/AnyBIC", ["Nm"]))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Bespoke cross-field / cross-schema rules
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def _values_match(msg, report, path_a, path_b, label):
|
|
68
|
+
a_nodes = msg.find(path_a)
|
|
69
|
+
if not a_nodes:
|
|
70
|
+
return
|
|
71
|
+
b_vals = {msg.text_of(n) for n in msg.find(path_b)}
|
|
72
|
+
if not b_vals:
|
|
73
|
+
return
|
|
74
|
+
a_vals = {msg.text_of(n) for n in a_nodes}
|
|
75
|
+
if a_vals != b_vals:
|
|
76
|
+
report(a_nodes[0], detail=label)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@rule(MT, YEAR, "R1", "CBPR_BusinessMessageIdentifier_FormalRule",
|
|
80
|
+
"The Business Message Identifier must match the Message Identification in "
|
|
81
|
+
"the Group Header.")
|
|
82
|
+
def _r1(msg, report):
|
|
83
|
+
_values_match(msg, report, "/AppHdr/BizMsgIdr", ROOT + "/GrpHdr/MsgId",
|
|
84
|
+
"BusinessMessageIdentifier must equal GroupHeader MessageIdentification")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@rule(MT, YEAR, "R2", "CBPR_Priority_Instruction_Priority_FormalRule",
|
|
88
|
+
'If "Priority" is used in the BAH for pacs messages, the value should be '
|
|
89
|
+
'identical to the one in the "Payment Type Information/InstructionPriority" if present.')
|
|
90
|
+
def _r2(msg, report):
|
|
91
|
+
if msg.present("/AppHdr/Prty") and msg.present(TX + "/PmtTpInf/InstrPrty"):
|
|
92
|
+
_values_match(msg, report, "/AppHdr/Prty", TX + "/PmtTpInf/InstrPrty",
|
|
93
|
+
"BAH Priority must equal InstructionPriority")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
_TO_PAIR = ("/AppHdr/To/FIId/FinInstnId/BICFI", TX + "/InstdAgt/FinInstnId/BICFI",
|
|
97
|
+
"To vs Instructed Agent")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@rule(MT, YEAR, "R3", "CBPR_To_Instructed_Agent_BIC_1_FormalRule",
|
|
101
|
+
'BAH "To" BIC must match "Instructed Agent" BIC, except where BAH '
|
|
102
|
+
"CopyDuplicate = COPY or = CODU")
|
|
103
|
+
def _r3(msg, report):
|
|
104
|
+
if any(v in {"COPY", "CODU"} for v in msg.values("/AppHdr/CpyDplct")):
|
|
105
|
+
return
|
|
106
|
+
_values_match(msg, report, *_TO_PAIR)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@rule(MT, YEAR, "R4", "CBPR_To_Instructed_Agent_BIC_2_FormalRule",
|
|
110
|
+
'BAH "To" BIC must match "Instructed Agent" BIC if CopyDuplicate is absent.')
|
|
111
|
+
def _r4(msg, report):
|
|
112
|
+
if not msg.absent("/AppHdr/CpyDplct"):
|
|
113
|
+
return
|
|
114
|
+
_values_match(msg, report, *_TO_PAIR)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@rule(MT, YEAR, "R5", "CBPR_From_Instructing_Agent_BIC_FormalRule",
|
|
118
|
+
'BAH "From" BIC must match "Instructing Agent" BIC')
|
|
119
|
+
def _r5(msg, report):
|
|
120
|
+
_values_match(msg, report, "/AppHdr/Fr/FIId/FinInstnId/BICFI",
|
|
121
|
+
TX + "/InstgAgt/FinInstnId/BICFI", "From vs Instructing Agent")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# R10: remittance mutually exclusive (under the underlying customer credit transfer)
|
|
125
|
+
reg("R10", "CBPR_Remittance_Mutually_Exclusive_FormalRule",
|
|
126
|
+
"Either Structured or Unstructured Remittance can be present.",
|
|
127
|
+
mutually_exclusive(UND + "/RmtInf", ["Ustrd", "Strd"]))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
reg("R11", "CBPR_GPI_ServiceLevel_Code_FormalRule",
|
|
131
|
+
"The GPI ServiceLevel Code in pacs.009 COV must be 'G001'.",
|
|
132
|
+
not_matching_pattern(TX + "/PmtTpInf/SvcLvl/Cd",
|
|
133
|
+
r"(G002|G003|G004|G005|G006|G007|G009)"))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@rule(MT, YEAR, "R12", "CBPR_Instruction_For_Creditor_Presence_Code_FormalRule",
|
|
137
|
+
'Each code can only be used once for element "Instruction For Creditor Agent".')
|
|
138
|
+
def _r12(msg, report):
|
|
139
|
+
for tx in msg.each(TX):
|
|
140
|
+
codes = msg.values("InstrForCdtrAgt/Cd", tx)
|
|
141
|
+
if len(codes) != len(set(codes)):
|
|
142
|
+
report(tx, detail="duplicate InstructionForCreditorAgent code")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
reg("R13", "CBPR_Instruction_Identification_FormalRule",
|
|
146
|
+
"This element must not start or end with a slash '/' and must not contain "
|
|
147
|
+
"two consecutive slashes '//'.",
|
|
148
|
+
not_matching_pattern(TX + "/PmtId/InstrId", r"(/.*)|(.*/)|(.*//.*)"))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@rule(MT, YEAR, "R15", "CBPR_End_To_End_Identification_FormalRule",
|
|
152
|
+
"In the E2E identification, the below restrictions apply to the first 16 "
|
|
153
|
+
'characters: - The first one and the 16th one cannot be "/" and - The '
|
|
154
|
+
'string of 16 characters cannot contain "//"')
|
|
155
|
+
def _r15(msg, report):
|
|
156
|
+
import re as _re
|
|
157
|
+
pats = [_re.compile(p) for p in (r"/.*", r".{15}/.*", r".{0,14}//.*")]
|
|
158
|
+
for node in msg.find(TX + "/PmtId/EndToEndId"):
|
|
159
|
+
val = msg.text_of(node)
|
|
160
|
+
if val and any(p.fullmatch(val) for p in pats):
|
|
161
|
+
report(node, detail="EndToEndId first 16 characters violate slash restrictions")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
reg("R17", "CBPR_Interbank_Settlement_Currency_FormalRule",
|
|
165
|
+
"The codes XAU, XAG, XPD and XPT are not allowed, as these are codes are "
|
|
166
|
+
"only used for commodities.",
|
|
167
|
+
lambda msg, report: [
|
|
168
|
+
report(el, detail=f"commodity currency '{ccy}' not allowed")
|
|
169
|
+
for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
|
|
170
|
+
if ccy in {"XAU", "XAG", "XPD", "XPT"}
|
|
171
|
+
])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# Agent name/address rules - cover (interbank) chain
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
_agent_name_adr("R22", TX + "/PrvsInstgAgt1/FinInstnId")
|
|
178
|
+
_agent_name_adr("R24", TX + "/PrvsInstgAgt2/FinInstnId")
|
|
179
|
+
_agent_name_adr("R25", TX + "/PrvsInstgAgt3/FinInstnId")
|
|
180
|
+
_agent_name_adr("R26", TX + "/IntrmyAgt1/FinInstnId")
|
|
181
|
+
_agent_name_adr("R27", TX + "/IntrmyAgt2/FinInstnId")
|
|
182
|
+
_agent_name_adr("R28", TX + "/IntrmyAgt3/FinInstnId")
|
|
183
|
+
_agent_name_adr("R29", TX + "/Dbtr/FinInstnId")
|
|
184
|
+
_agent_name_adr("R30", TX + "/DbtrAgt/FinInstnId")
|
|
185
|
+
_agent_name_adr("R31", TX + "/CdtrAgt/FinInstnId")
|
|
186
|
+
_agent_name_adr("R32", TX + "/Cdtr/FinInstnId")
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# Underlying customer credit transfer - parties + agents
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
_party_name_adr("R33", UND + "/UltmtDbtr")
|
|
192
|
+
_party_name_adr("R37", UND + "/InitgPty")
|
|
193
|
+
_party_any_bic("R38", UND + "/Dbtr")
|
|
194
|
+
_party_name_adr("R43", UND + "/Dbtr")
|
|
195
|
+
|
|
196
|
+
_agent_name_adr("R44", UND + "/DbtrAgt/FinInstnId")
|
|
197
|
+
_agent_name_adr("R45", UND + "/PrvsInstgAgt1/FinInstnId")
|
|
198
|
+
_agent_name_adr("R46", UND + "/PrvsInstgAgt2/FinInstnId")
|
|
199
|
+
_agent_name_adr("R47", UND + "/PrvsInstgAgt3/FinInstnId")
|
|
200
|
+
_agent_name_adr("R48", UND + "/IntrmyAgt1/FinInstnId")
|
|
201
|
+
_agent_name_adr("R49", UND + "/IntrmyAgt2/FinInstnId")
|
|
202
|
+
_agent_name_adr("R50", UND + "/IntrmyAgt3/FinInstnId")
|
|
203
|
+
_agent_name_adr("R51", UND + "/CdtrAgt/FinInstnId")
|
|
204
|
+
|
|
205
|
+
_party_name_adr("R53", UND + "/Cdtr")
|
|
206
|
+
_party_any_bic("R54", UND + "/Cdtr")
|
|
207
|
+
_party_name_adr("R59", UND + "/UltmtCdtr")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# Algorithmic field validation (brief), for fields present in pacs.009 COV.
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
|
|
214
|
+
"Interbank Settlement Amount currency must be a valid ISO 4217 code.",
|
|
215
|
+
lambda msg, report: [
|
|
216
|
+
report(el, detail=f"invalid currency '{ccy}'")
|
|
217
|
+
for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
|
|
218
|
+
if ccy and not is_valid_currency(ccy)
|
|
219
|
+
])
|
|
220
|
+
|
|
221
|
+
reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
|
|
222
|
+
"Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
|
|
223
|
+
each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
# Advisory textual rules (not mechanically enforceable - surfaced as guidance)
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
_ADVISORY = {
|
|
230
|
+
"R6": ("CBPR_Related_Business_Application_Header_TextualRule",
|
|
231
|
+
"If used, the Related BAH must transport the exact same information as in the BAH of the related message."),
|
|
232
|
+
"R9": ("CBPR_Related_BAH_Business_Service_TextualRule",
|
|
233
|
+
"If related BAH is present, it should transport the element Business Service."),
|
|
234
|
+
"R14": ("CBPR_E2E_COV_TextualRule",
|
|
235
|
+
"In the pacs.009 COV, the E2E identification should transport the instruction identification "
|
|
236
|
+
"of the underlying pacs.008."),
|
|
237
|
+
"R16": ("CBPR_UETR_COV_TextualRule",
|
|
238
|
+
"In the pacs.009 COV, the UETR should transport the UETR of the underlying pacs.008."),
|
|
239
|
+
"R18": ("CBPR_Agent_Option_3_TextualRule",
|
|
240
|
+
"Name AND ([Structured postal address with minimum Town Name and Country] OR [Hybrid postal "
|
|
241
|
+
"address with minimum Town Name and Country]). It is recommended to also add the post code when available."),
|
|
242
|
+
"R19": ("CBPR_Agent_Option_2_TextualRule",
|
|
243
|
+
"(Clearing Code OR LEI) AND (Name AND ([Structured postal address with minimum Town Name and "
|
|
244
|
+
"Country] OR [Hybrid postal address with minimum Town Name and Country]). It is recommended to "
|
|
245
|
+
"also add the post code when available."),
|
|
246
|
+
"R20": ("CBPR_Agent_Option_1_TextualRule",
|
|
247
|
+
"BICFI, complemented optionally with a LEI (preferred option)"),
|
|
248
|
+
"R21": ("CBPR_Agent_National_only_TextualRule",
|
|
249
|
+
"Whenever Debtor Agent, Creditor Agent and all agents in between are located within the same "
|
|
250
|
+
"country, the clearing code only may be used."),
|
|
251
|
+
"R34": ("CBPR_Ultimate_Debtor_Option_1_TextualRule",
|
|
252
|
+
"Name AND [(Structured Postal Address) OR (Hybrid Postal Address) with minimum Town Name & "
|
|
253
|
+
"Country - it is recommended to add Post code when available)]"),
|
|
254
|
+
"R35": ("CBPR_UltimateDebtor_Option_3_Jurisdictions_only_TextualRule",
|
|
255
|
+
"For Jurisdictional transactions, Name and/or Identification (Private or Organisation) (that is "
|
|
256
|
+
"within a country or for regions under same legislations - e.g. EEA)."),
|
|
257
|
+
"R36": ("CBPR_Ultimate_Debtor_Option_2_TextualRule",
|
|
258
|
+
"Name AND [(Structured Postal Address) OR (Hybrid Postal Address) with minimum Town Name & "
|
|
259
|
+
"Country - it is recommended to add Post code when available] AND (Identification: Private or Organisation)"),
|
|
260
|
+
"R39": ("CBPR_Debtor_Option_2_TextualRule",
|
|
261
|
+
"Name AND ([Structured Address with minimum Town Name & Country (+ recommended to add Post code "
|
|
262
|
+
"when available)] OR [Hybrid postal address with minimum Town Name and Country (+ recommended to "
|
|
263
|
+
"add Post code when available)] AND (Account Number OR Identification: Private or Organisation)."),
|
|
264
|
+
"R41": ("CBPR_Debtor_Option_1_TextualRule",
|
|
265
|
+
"Organisation Identification/AnyBIC AND (Account Number OR Organisation Identification/Other)"),
|
|
266
|
+
"R42": ("CBPR_Debtor_Option_3_Jurisdictions_only_TextualRule",
|
|
267
|
+
"For Jurisdictional transactions, Debtor/Name is mandatory with either Debtor Account OR Debtor "
|
|
268
|
+
"Identification (that is within a country or for regions under same legislations - e.g. EEA)."),
|
|
269
|
+
"R55": ("CBPR_Creditor_Option_3_Jurisdictions_only_TextualRule",
|
|
270
|
+
"For Jurisdictional transactions, Creditor/Name is mandatory with either Creditor Account OR "
|
|
271
|
+
"Creditor Identification (that is within a country or for regions under same legislations - e.g. EEA)."),
|
|
272
|
+
"R56": ("CBPR_Creditor_Option_2_TextualRule",
|
|
273
|
+
"Name AND ([Structured Address with minimum Town Name & Country (+ recommended to add Post code "
|
|
274
|
+
"when available)] OR [Hybrid postal address with minimum Town Name and Country (+ recommended to "
|
|
275
|
+
"add Post code when available)) AND (Account Number OR Identification: Private or Organisation)."),
|
|
276
|
+
"R57": ("CBPR_Creditor_Option_1_TextualRule",
|
|
277
|
+
"Organisation Identification/AnyBIC AND (Account Number OR Organisation Identification/Other)"),
|
|
278
|
+
"R58": ("CBPR_UltimateCreditor_Option_2_Jurisdictions_only_TextualRule",
|
|
279
|
+
"For Jurisdictional transactions, Name and/or Identification (Private or Organisation) (that is "
|
|
280
|
+
"within a country or for regions under same legislations - e.g. EEA)."),
|
|
281
|
+
"R60": ("CBPR_Ultimate_Creditor_Option_1_TextualRule",
|
|
282
|
+
"Name AND [(Structured Postal Address) OR (Hybrid Postal Address) with minimum Town Name & "
|
|
283
|
+
"Country - it is recommended to add Post code when available)]. Other elements are optional, eg "
|
|
284
|
+
"Identification: Private or Organisation)"),
|
|
285
|
+
}
|
|
286
|
+
for _num, (_name, _desc) in _ADVISORY.items():
|
|
287
|
+
advisory(MT, YEAR, _num, _name, _desc)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
# Promoted from advisory to enforced (mechanizable, conservative checks).
|
|
292
|
+
# ---------------------------------------------------------------------------
|
|
293
|
+
reg("R7", "CBPR_Business_Message_Identifier_FormalRule",
|
|
294
|
+
"The Business Message Identifier is the unique identifier of the Business Message instance "
|
|
295
|
+
"that is being transported with this header, as defined by the sending application or system. "
|
|
296
|
+
"Must contain the Message Identification element from the Group Header of the underlying message, "
|
|
297
|
+
"where available.",
|
|
298
|
+
business_msg_id_carries_group_id())
|
|
299
|
+
|
|
300
|
+
reg("R8", "CBPR_Message_Definition_Identifier_FormalRule",
|
|
301
|
+
"The Message Definition Identifier of the Business Message instance that is being transported "
|
|
302
|
+
"with this header. In general, it must be formatted exactly as it appears in the namespace of "
|
|
303
|
+
"the Business Message instance.",
|
|
304
|
+
header_msg_def_id_matches())
|
|
305
|
+
|
|
306
|
+
reg("R23", "CBPR_Duplication_Postal_Address_FormalRule",
|
|
307
|
+
"Data present in structured elements within the Postal Address must not, under any circumstances "
|
|
308
|
+
"be repeated in AddressLine.",
|
|
309
|
+
no_postal_address_duplication())
|
|
310
|
+
|
|
311
|
+
reg("R40", "CBPR_Debtor_BIC_Presence_FormalRule",
|
|
312
|
+
"If Any BIC is present, then (Name and Postal Address) is NOT allowed (other elements remain "
|
|
313
|
+
"optional) - However, in case of conflicting information, AnyBIC will always take precedence.",
|
|
314
|
+
bic_presence_exclusive(UND + "/Dbtr"))
|
|
315
|
+
|
|
316
|
+
reg("R52", "CBPR_Creditor_BIC_Presence_FormalRule",
|
|
317
|
+
"If Any BIC is present, then (Name and Postal Address) is NOT allowed (other elements remain "
|
|
318
|
+
"optional) - However, in case of conflicting information, AnyBIC will always take precedence.",
|
|
319
|
+
bic_presence_exclusive(UND + "/Cdtr"))
|
|
320
|
+
|
|
321
|
+
reg("R61", "CBPR_Structured_RemittanceInformation_FormalRule",
|
|
322
|
+
"Structured can be repeated, however the total business data for all occurrences (excluding "
|
|
323
|
+
"tags) must not exceed 9,000 characters.",
|
|
324
|
+
structured_remittance_max_total(UND + "/RmtInf/Strd", 9000))
|