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,491 @@
|
|
|
1
|
+
"""CBPR+ SR2026 usage rules for pacs.004.001.09 (PaymentReturn).
|
|
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
|
+
reuse the shared combinators from ``helpers`` where they match a known shape;
|
|
6
|
+
cross-field / cross-schema logic is written as bespoke ``fn(msg, report)``.
|
|
7
|
+
|
|
8
|
+
Structure mirrors the reference module ``y2025/pacs_008.py``.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ...registry import advisory, rule
|
|
13
|
+
from ...validators import is_valid_bic, is_valid_country, is_valid_currency, is_valid_lei
|
|
14
|
+
from ...helpers import (
|
|
15
|
+
bic_presence_exclusive,
|
|
16
|
+
business_msg_id_carries_group_id,
|
|
17
|
+
charges_required_when_amounts_differ,
|
|
18
|
+
each_value_valid,
|
|
19
|
+
header_msg_def_id_matches,
|
|
20
|
+
mutually_exclusive,
|
|
21
|
+
no_postal_address_duplication,
|
|
22
|
+
not_matching_pattern,
|
|
23
|
+
presence_together,
|
|
24
|
+
required_when_absent,
|
|
25
|
+
requires_if_present,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from decimal import Decimal, InvalidOperation as _InvalidOperation
|
|
29
|
+
|
|
30
|
+
MT = "pacs.004"
|
|
31
|
+
YEAR = 2026
|
|
32
|
+
ROOT = "/Document/PmtRtr"
|
|
33
|
+
TX = ROOT + "/TxInf"
|
|
34
|
+
OTR = TX + "/OrgnlTxRef"
|
|
35
|
+
RC = TX + "/RtrChain"
|
|
36
|
+
|
|
37
|
+
# Repeated rule descriptions (identical across the locations they apply to).
|
|
38
|
+
D_AGENT_NAME_ADR = "Name and Address must always be present together."
|
|
39
|
+
D_PARTY_NAME_ADR = "If Postal Address is present then Name is mandatory."
|
|
40
|
+
D_PARTY_ANY_BIC = (
|
|
41
|
+
"If AnyBIC is absent then Name is mandatory and it is recommended to also "
|
|
42
|
+
"provide the Postal Address."
|
|
43
|
+
)
|
|
44
|
+
D_PARTY_ANY_BIC_2 = "If AnyBIC is absent, then Name is mandatory."
|
|
45
|
+
D_COMMODITY = (
|
|
46
|
+
"The codes XAU, XAG, XPD and XPT are not allowed, as these are codes are "
|
|
47
|
+
"only used for commodities."
|
|
48
|
+
)
|
|
49
|
+
_COMMODITY = ["XAU", "XAG", "XPD", "XPT"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def reg(number: str, name: str, description: str, check) -> None:
|
|
53
|
+
"""Register a combinator-built check as a rule."""
|
|
54
|
+
rule(MT, YEAR, number, name, description)(check)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _agent_name_adr(number: str, fin_inst_path: str) -> None:
|
|
58
|
+
reg(number, "CBPR_Agent_Name_Postal_Address_FormalRule", D_AGENT_NAME_ADR,
|
|
59
|
+
presence_together(fin_inst_path, "Nm", "PstlAdr"))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _party_name_adr(number: str, party_path: str) -> None:
|
|
63
|
+
reg(number, "CBPR_Party_Name_Postal_Address_FormalRule", D_PARTY_NAME_ADR,
|
|
64
|
+
requires_if_present(party_path, "PstlAdr", "Nm"))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _party_any_bic(number: str, party_path: str, desc: str) -> None:
|
|
68
|
+
reg(number, "CBPR_Party_Name_Any_BIC_FormalRule", desc,
|
|
69
|
+
required_when_absent(party_path, "Id/OrgId/AnyBIC", ["Nm"]))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Bespoke cross-field / cross-schema rules
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def _values_match(msg, report, path_a, path_b, label):
|
|
77
|
+
a_nodes = msg.find(path_a)
|
|
78
|
+
if not a_nodes:
|
|
79
|
+
return
|
|
80
|
+
b_vals = {msg.text_of(n) for n in msg.find(path_b)}
|
|
81
|
+
if not b_vals:
|
|
82
|
+
return
|
|
83
|
+
a_vals = {msg.text_of(n) for n in a_nodes}
|
|
84
|
+
if a_vals != b_vals:
|
|
85
|
+
report(a_nodes[0], detail=label)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@rule(MT, YEAR, "R1", "CBPR_BusinessMessageIdentifier_FormalRule",
|
|
89
|
+
"The Business Message Identifier must match the Message Identification in "
|
|
90
|
+
"the Group Header.")
|
|
91
|
+
def _r1(msg, report):
|
|
92
|
+
_values_match(msg, report, "/AppHdr/BizMsgIdr", ROOT + "/GrpHdr/MsgId",
|
|
93
|
+
"BusinessMessageIdentifier must equal GroupHeader MessageIdentification")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@rule(MT, YEAR, "R2", "CBPR_To_Instructed_Agent_BIC_1_FormalRule",
|
|
97
|
+
'BAH "To" BIC must match "Instructed Agent" BIC, except where BAH '
|
|
98
|
+
"CopyDuplicate = COPY or = CODU")
|
|
99
|
+
def _r2(msg, report):
|
|
100
|
+
if any(v in {"COPY", "CODU"} for v in msg.values("/AppHdr/CpyDplct")):
|
|
101
|
+
return
|
|
102
|
+
_values_match(msg, report, "/AppHdr/To/FIId/FinInstnId/BICFI",
|
|
103
|
+
TX + "/InstdAgt/FinInstnId/BICFI", "To vs Instructed Agent")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@rule(MT, YEAR, "R3", "CBPR_To_Instructed_Agent_BIC_2_FormalRule",
|
|
107
|
+
'BAH "To" BIC must match "Instructed Agent" BIC if CopyDuplicate is absent.')
|
|
108
|
+
def _r3(msg, report):
|
|
109
|
+
if not msg.absent("/AppHdr/CpyDplct"):
|
|
110
|
+
return
|
|
111
|
+
_values_match(msg, report, "/AppHdr/To/FIId/FinInstnId/BICFI",
|
|
112
|
+
TX + "/InstdAgt/FinInstnId/BICFI", "To vs Instructed Agent")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@rule(MT, YEAR, "R4", "CBPR_From_Instructing_Agent_BIC_FormalRule",
|
|
116
|
+
'BAH "From" BIC must match "Instructing Agent" BIC')
|
|
117
|
+
def _r4(msg, report):
|
|
118
|
+
_values_match(msg, report, "/AppHdr/Fr/FIId/FinInstnId/BICFI",
|
|
119
|
+
TX + "/InstgAgt/FinInstnId/BICFI", "From vs Instructing Agent")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@rule(MT, YEAR, "R9", "CBPR_Interbank_Settlement_Date_FormalRule",
|
|
123
|
+
"If TransactionInformation/OriginalInterbankSettlementDate is present, then "
|
|
124
|
+
"OriginalTransactionReference/InterbankSettlementDate must not be used.")
|
|
125
|
+
def _r9(msg, report):
|
|
126
|
+
for tx in msg.each(TX):
|
|
127
|
+
if msg.present("OrgnlIntrBkSttlmDt", tx) and msg.present("OrgnlTxRef/IntrBkSttlmDt", tx):
|
|
128
|
+
report(tx, detail="OrgnlTxRef/InterbankSettlementDate must be absent when "
|
|
129
|
+
"OriginalInterbankSettlementDate is present")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@rule(MT, YEAR, "R10", "CBPR_Interbank_Settlement_Amount_FormalRule",
|
|
133
|
+
"If TransactionInformation/OriginalInterbankSettlementAmount is present, then "
|
|
134
|
+
"OriginalTransactionReference/InterbankSettlementAmount must not be used.")
|
|
135
|
+
def _r10(msg, report):
|
|
136
|
+
for tx in msg.each(TX):
|
|
137
|
+
if msg.present("OrgnlIntrBkSttlmAmt", tx) and msg.present("OrgnlTxRef/IntrBkSttlmAmt", tx):
|
|
138
|
+
report(tx, detail="OrgnlTxRef/InterbankSettlementAmount must be absent when "
|
|
139
|
+
"OriginalInterbankSettlementAmount is present")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@rule(MT, YEAR, "R11", "CBPR_CRED_FormalRule",
|
|
143
|
+
"Charge information is mandatory if CRED is present - if no charges are "
|
|
144
|
+
'taken, Zero must be used in "Amount" (any agent in the payment chain).')
|
|
145
|
+
def _r11(msg, report):
|
|
146
|
+
for tx in msg.each(TX):
|
|
147
|
+
cb = msg.values("ChrgBr", tx)
|
|
148
|
+
if cb and all(v == "CRED" for v in cb) and msg.absent("ChrgsInf", tx):
|
|
149
|
+
report(tx, detail="ChargesInformation required when ChargeBearer is CRED")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# R13 is a positive XML pattern (value MUST match); implement bespoke.
|
|
153
|
+
import re as _re # noqa: E402
|
|
154
|
+
|
|
155
|
+
_R13_RX = _re.compile(
|
|
156
|
+
r"pacs\.00[289]\.001\.[0-9]{2}|camt\.05[34]\.001\.[0-9]{2}|"
|
|
157
|
+
r"MT103|MT202|MT205|MT900|MT910|MT940|MT950"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@rule(MT, YEAR, "R13", "CBPR_Original_Message_Name_Identification_FormalRule",
|
|
162
|
+
"This element should be populated with either pacs.002.001.xx or "
|
|
163
|
+
"pacs.008.001.xx or pacs.009.001.xx or camt.053.001.xx or camt.054.001.xx or "
|
|
164
|
+
"MT103 or MT202 or MT205 or MT 900 or MT910 or MT940 or MT950 when present.")
|
|
165
|
+
def _r13(msg, report):
|
|
166
|
+
for node in msg.find(TX + "/OrgnlGrpInf/OrgnlMsgNmId"):
|
|
167
|
+
val = msg.text_of(node)
|
|
168
|
+
if val and not _R13_RX.fullmatch(val):
|
|
169
|
+
report(node, detail=f"OriginalMessageNameIdentification '{val}' not an allowed value")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
reg("R14", "CBPR_Original_Instruction_Identification_FormalRule",
|
|
173
|
+
"This element must not start or end with a slash '/' and must not contain two "
|
|
174
|
+
"consecutive slashes '//'.",
|
|
175
|
+
not_matching_pattern(TX + "/OrgnlInstrId", r"(/.*)|(.*/)|(.*//.*)"))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _commodity_ccy(path: str):
|
|
179
|
+
def check(msg, report):
|
|
180
|
+
for el, ccy in msg.attr_nodes(path, "Ccy"):
|
|
181
|
+
if ccy in _COMMODITY:
|
|
182
|
+
report(el, detail=f"commodity currency '{ccy}' not allowed")
|
|
183
|
+
return check
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
reg("R20", "CBPR_Interbank_Settlement_Currency_FormalRule", D_COMMODITY,
|
|
187
|
+
_commodity_ccy(TX + "/OrgnlIntrBkSttlmAmt"))
|
|
188
|
+
|
|
189
|
+
reg("R23", "CBPR_Returned_Interbank_Settlement_Currency_FormalRule", D_COMMODITY,
|
|
190
|
+
_commodity_ccy(TX + "/RtrdIntrBkSttlmAmt"))
|
|
191
|
+
|
|
192
|
+
reg("R65", "CBPR_Interbank_Settlement_Currency_FormalRule", D_COMMODITY,
|
|
193
|
+
_commodity_ccy(OTR + "/IntrBkSttlmAmt"))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# Agent Name/Postal Address rules (presence_together on each FinInstnId)
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
_agent_name_adr("R29", TX + "/ChrgsInf/Agt/FinInstnId")
|
|
200
|
+
_agent_name_adr("R41", RC + "/Dbtr/Agt/FinInstnId")
|
|
201
|
+
_agent_name_adr("R43", RC + "/DbtrAgt/FinInstnId")
|
|
202
|
+
_agent_name_adr("R44", RC + "/PrvsInstgAgt1/FinInstnId")
|
|
203
|
+
_agent_name_adr("R45", RC + "/PrvsInstgAgt2/FinInstnId")
|
|
204
|
+
_agent_name_adr("R46", RC + "/PrvsInstgAgt3/FinInstnId")
|
|
205
|
+
_agent_name_adr("R47", RC + "/IntrmyAgt1/FinInstnId")
|
|
206
|
+
_agent_name_adr("R48", RC + "/IntrmyAgt2/FinInstnId")
|
|
207
|
+
_agent_name_adr("R49", RC + "/IntrmyAgt3/FinInstnId")
|
|
208
|
+
_agent_name_adr("R50", RC + "/CdtrAgt/FinInstnId")
|
|
209
|
+
_agent_name_adr("R57", RC + "/Cdtr/Agt/FinInstnId")
|
|
210
|
+
_agent_name_adr("R67", OTR + "/SttlmInf/InstgRmbrsmntAgt/FinInstnId")
|
|
211
|
+
_agent_name_adr("R68", OTR + "/SttlmInf/InstdRmbrsmntAgt/FinInstnId")
|
|
212
|
+
_agent_name_adr("R69", OTR + "/SttlmInf/ThrdRmbrsmntAgt/FinInstnId")
|
|
213
|
+
_agent_name_adr("R77", OTR + "/Dbtr/Agt/FinInstnId")
|
|
214
|
+
_agent_name_adr("R78", OTR + "/DbtrAgt/FinInstnId")
|
|
215
|
+
_agent_name_adr("R80", OTR + "/CdtrAgt/FinInstnId")
|
|
216
|
+
_agent_name_adr("R85", OTR + "/Cdtr/Agt/FinInstnId")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# Party Name / Postal Address rules (if PostalAddress then Name)
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
_party_name_adr("R34", RC + "/UltmtDbtr/Pty")
|
|
223
|
+
_party_name_adr("R36", RC + "/Dbtr/Pty")
|
|
224
|
+
_party_name_adr("R42", RC + "/InitgPty/Pty")
|
|
225
|
+
_party_name_adr("R53", RC + "/Cdtr/Pty")
|
|
226
|
+
_party_name_adr("R58", RC + "/UltmtCdtr/Pty")
|
|
227
|
+
_party_name_adr("R64", TX + "/RtrRsnInf/Orgtr")
|
|
228
|
+
_party_name_adr("R72", OTR + "/UltmtDbtr/Pty")
|
|
229
|
+
_party_name_adr("R75", OTR + "/Dbtr/Pty")
|
|
230
|
+
_party_name_adr("R81", OTR + "/Cdtr/Pty")
|
|
231
|
+
_party_name_adr("R88", OTR + "/UltmtCdtr/Pty")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
# Party Name / AnyBIC rules (if AnyBIC absent then Name)
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
_party_any_bic("R37", RC + "/Dbtr/Pty", D_PARTY_ANY_BIC)
|
|
238
|
+
_party_any_bic("R55", RC + "/Cdtr/Pty", D_PARTY_ANY_BIC)
|
|
239
|
+
_party_any_bic("R63", TX + "/RtrRsnInf/Orgtr", D_PARTY_ANY_BIC_2)
|
|
240
|
+
_party_any_bic("R74", OTR + "/Dbtr/Pty", D_PARTY_ANY_BIC_2)
|
|
241
|
+
_party_any_bic("R83", OTR + "/Cdtr/Pty", D_PARTY_ANY_BIC_2)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# R70: remittance mutually exclusive
|
|
245
|
+
reg("R70", "CBPR_Remittance_Mutually_Exclusive_FormalRule",
|
|
246
|
+
"Either Structured or Unstructured Remittance can be present.",
|
|
247
|
+
mutually_exclusive(OTR + "/RmtInf", ["Ustrd", "Strd"]))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# Algorithmic field validation (brief), only for fields present in pacs.004.
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
def _run_all(*checks):
|
|
254
|
+
"""Combine several builder-produced checks into one ``check(msg, report)``."""
|
|
255
|
+
def check(msg, report):
|
|
256
|
+
for c in checks:
|
|
257
|
+
c(msg, report)
|
|
258
|
+
return check
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _valid_ccy(path: str):
|
|
262
|
+
def check(msg, report):
|
|
263
|
+
for el, ccy in msg.attr_nodes(path, "Ccy"):
|
|
264
|
+
if ccy and not is_valid_currency(ccy):
|
|
265
|
+
report(el, detail=f"invalid currency '{ccy}'")
|
|
266
|
+
return check
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
|
|
270
|
+
"Settlement amount currencies must be valid ISO 4217 codes.",
|
|
271
|
+
_run_all(
|
|
272
|
+
_valid_ccy(TX + "/OrgnlIntrBkSttlmAmt"),
|
|
273
|
+
_valid_ccy(TX + "/RtrdIntrBkSttlmAmt"),
|
|
274
|
+
_valid_ccy(TX + "/RtrdInstdAmt"),
|
|
275
|
+
_valid_ccy(OTR + "/IntrBkSttlmAmt"),
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
|
|
279
|
+
"Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
|
|
280
|
+
_run_all(
|
|
281
|
+
each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"),
|
|
282
|
+
each_value_valid(TX + "/InstdAgt/FinInstnId/BICFI", is_valid_bic, "BIC"),
|
|
283
|
+
))
|
|
284
|
+
|
|
285
|
+
reg("VAL-LEI", "CBPR_Valid_Agent_LEI",
|
|
286
|
+
"Every Agent LEI must be a structurally valid LEI.",
|
|
287
|
+
_run_all(
|
|
288
|
+
each_value_valid(TX + "/InstgAgt/FinInstnId/LEI", is_valid_lei, "LEI"),
|
|
289
|
+
each_value_valid(TX + "/InstdAgt/FinInstnId/LEI", is_valid_lei, "LEI"),
|
|
290
|
+
))
|
|
291
|
+
|
|
292
|
+
reg("VAL-CTRY", "CBPR_Valid_Country",
|
|
293
|
+
"Every Country code must be a valid ISO 3166 country code.",
|
|
294
|
+
_run_all(
|
|
295
|
+
each_value_valid(RC + "/Dbtr/Pty/PstlAdr/Ctry", is_valid_country, "country"),
|
|
296
|
+
each_value_valid(RC + "/Cdtr/Pty/PstlAdr/Ctry", is_valid_country, "country"),
|
|
297
|
+
each_value_valid(OTR + "/Dbtr/Pty/PstlAdr/Ctry", is_valid_country, "country"),
|
|
298
|
+
each_value_valid(OTR + "/Cdtr/Pty/PstlAdr/Ctry", is_valid_country, "country"),
|
|
299
|
+
))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
# Mechanizable textual rules + advisory textual rules.
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# R71: total Structured remittance business data must not exceed 9,000 chars.
|
|
306
|
+
@rule(MT, YEAR, "R71", "CBPR_Structured_RemittanceInformation_TextualRule",
|
|
307
|
+
"Structured can be repeated, however the total business data for all "
|
|
308
|
+
"occurrences (excluding tags) must not exceed 9,000 characters.")
|
|
309
|
+
def _r71(msg, report):
|
|
310
|
+
strd = msg.find(OTR + "/RmtInf/Strd")
|
|
311
|
+
if not strd:
|
|
312
|
+
return
|
|
313
|
+
total = 0
|
|
314
|
+
for node in strd:
|
|
315
|
+
total += sum(len((t or "").strip()) for t in node.itertext())
|
|
316
|
+
if total > 9000:
|
|
317
|
+
report(strd[0], detail=f"Structured remittance business data {total} exceeds 9000 characters")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# Rules promoted from advisory to enforced (mechanizable, conservative).
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
# R6: Business Message Identifier must carry the Group Header MsgId.
|
|
324
|
+
reg("R6", "CBPR_Business_Message_Identifier_TextualRule",
|
|
325
|
+
"The Business Message Identifier is the unique identifier of the Business Message instance "
|
|
326
|
+
"that is being transported with this header, as defined by the sending application or system. "
|
|
327
|
+
"Must contain the Message Identification element from the Group Header of the underlying message, "
|
|
328
|
+
"where available.",
|
|
329
|
+
business_msg_id_carries_group_id())
|
|
330
|
+
|
|
331
|
+
# R7: Message Definition Identifier must match the Document message-definition id.
|
|
332
|
+
reg("R7", "CBPR_Message_Definition_Identifier_TextualRule",
|
|
333
|
+
"The Message Definition Identifier of the Business Message instance must in general be formatted "
|
|
334
|
+
"exactly as it appears in the namespace of the Business Message instance.",
|
|
335
|
+
header_msg_def_id_matches())
|
|
336
|
+
|
|
337
|
+
# R30: structured Postal Address values must not be duplicated in AddressLine.
|
|
338
|
+
reg("R30", "CBPR_Duplication_Postal_Address_TextualRule",
|
|
339
|
+
"Data present in structured elements within the Postal Address must not, under any circumstances "
|
|
340
|
+
"be repeated in AddressLine.",
|
|
341
|
+
no_postal_address_duplication())
|
|
342
|
+
|
|
343
|
+
# R40 / R52: if AnyBIC present then Name and Postal Address are not allowed.
|
|
344
|
+
reg("R40", "CBPR_Debtor_BIC_Presence_TextualRule",
|
|
345
|
+
"If Any BIC is present, then (Name and Postal Address) is NOT allowed. In case of conflicting "
|
|
346
|
+
"information, AnyBIC will always take precedence.",
|
|
347
|
+
bic_presence_exclusive(RC + "/Dbtr/Pty"))
|
|
348
|
+
|
|
349
|
+
reg("R52", "CBPR_Creditor_BIC_Presence_TextualRule",
|
|
350
|
+
"If Any BIC is present, then (Name and Postal Address) is NOT allowed. In case of conflicting "
|
|
351
|
+
"information, AnyBIC will always take precedence.",
|
|
352
|
+
bic_presence_exclusive(RC + "/Cdtr/Pty"))
|
|
353
|
+
|
|
354
|
+
# R22: if ReturnedInstructedAmount and ReturnedInterbankSettlementAmount share a
|
|
355
|
+
# currency and differ, Charge Information becomes mandatory.
|
|
356
|
+
reg("R22", "CBPR_Returned_Instructed_Rule_1_TextualRule",
|
|
357
|
+
"If ReturnedInstructedAmount and ReturnedInterbankSettlementAmount are expressed in the same "
|
|
358
|
+
"currency and differ, Charge Information becomes mandatory.",
|
|
359
|
+
charges_required_when_amounts_differ(TX, "RtrdInstdAmt", "RtrdIntrBkSttlmAmt", "ChrgsInf"))
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# R61: Partial Return - when a partial return is detectable (returned interbank
|
|
363
|
+
# amount < original interbank amount, same currency), ReturnReasonInformation/
|
|
364
|
+
# AdditionalInformation must equal "PART" and Reason must be populated.
|
|
365
|
+
@rule(MT, YEAR, "R61", "CBPR_Partial_Return_TextualRule",
|
|
366
|
+
'In case of Partial Return, the ReturnReasonInformation/AdditionalInformation must take the '
|
|
367
|
+
'fixed value "PART" and the ReturnReasonInformation/Reason must be populated with a code from '
|
|
368
|
+
"the External reason code list.")
|
|
369
|
+
def _r61(msg, report):
|
|
370
|
+
for tx in msg.each(TX):
|
|
371
|
+
rtrd = msg.find("RtrdIntrBkSttlmAmt", tx)
|
|
372
|
+
orig = msg.find("OrgnlIntrBkSttlmAmt", tx)
|
|
373
|
+
if not rtrd or not orig:
|
|
374
|
+
continue
|
|
375
|
+
r_ccy, o_ccy = rtrd[0].get("Ccy"), orig[0].get("Ccy")
|
|
376
|
+
if not r_ccy or r_ccy != o_ccy:
|
|
377
|
+
continue
|
|
378
|
+
try:
|
|
379
|
+
rv = Decimal(msg.text_of(rtrd[0]))
|
|
380
|
+
ov = Decimal(msg.text_of(orig[0]))
|
|
381
|
+
except (_InvalidOperation, ValueError):
|
|
382
|
+
continue
|
|
383
|
+
if rv >= ov:
|
|
384
|
+
continue # not a detectable partial return
|
|
385
|
+
addtl = [msg.text_of(n) for n in msg.find("RtrRsnInf/AddtlInf", tx)]
|
|
386
|
+
if not any(v == "PART" for v in addtl):
|
|
387
|
+
report(tx, detail='Partial Return requires ReturnReasonInformation/AdditionalInformation = "PART"')
|
|
388
|
+
if msg.absent("RtrRsnInf/Rsn", tx):
|
|
389
|
+
report(tx, detail="Partial Return requires ReturnReasonInformation/Reason to be populated")
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# Advisory textual rules (not mechanically enforceable - surfaced as guidance).
|
|
393
|
+
_ADVISORY = {
|
|
394
|
+
"R5": ("CBPR_Related_Business_Application_Header_TextualRule",
|
|
395
|
+
"If used, the Related BAH must transport the exact same information as in the BAH of the related message."),
|
|
396
|
+
"R8": ("CBPR_Related_BAH_Business_Service_TextualRule",
|
|
397
|
+
"If related BAH is present, it should transport the element Business Service."),
|
|
398
|
+
"R12": ("CBPR_Original_Message_Identification_TextualRule",
|
|
399
|
+
"Original Message Identification must transport the Message Identification of the underlying "
|
|
400
|
+
"payment (e.g. pacs.008/pacs.009)."),
|
|
401
|
+
"R15": ("CBPR_Original_Instruction_Identification_TextualRule",
|
|
402
|
+
"If present in underlying pacs.008/pacs.009, the Instruction Identification must be transported in pacs.004."),
|
|
403
|
+
"R16": ("CBPR_Original_End_To_End_Identification_TextualRule",
|
|
404
|
+
"If present in underlying pacs.008/pacs.009, the EndToEnd Identification must be transported in pacs.004."),
|
|
405
|
+
"R17": ("CBPR_Original_Transaction_Identification_TextualRule",
|
|
406
|
+
"If present in underlying pacs.008/pacs.009, the Transaction Identification must be transported in the pacs.004."),
|
|
407
|
+
"R18": ("CBPR_Original_UETR_TextualRule",
|
|
408
|
+
"Must transport the UETR of the underlying pacs.008/pacs.009."),
|
|
409
|
+
"R19": ("CBPR_Original_Clearing_System_Reference_TextualRule",
|
|
410
|
+
"If present in underlying pacs.008/pacs.009, the Clearing System Reference must be transported in the pacs.004."),
|
|
411
|
+
"R21": ("CBPR_Returned_Instructed_Rule_2_TextualRule",
|
|
412
|
+
"If ReturnedInstructedAmount and ReturnedInterbankSettlementAmount are NOT expressed in the same "
|
|
413
|
+
"currency: if ReturnedInstructedAmount is higher than ReturnedInterbankSettlementAmount when "
|
|
414
|
+
"converted, Charge Information becomes mandatory."),
|
|
415
|
+
"R24": ("CBPR_SHAR_TextualRule",
|
|
416
|
+
"If deduct taken then charge information is mandatory. It is optional for initiator (not taking deduct)."),
|
|
417
|
+
"R25": ("CBPR_Agent_Option_1_TextualRule",
|
|
418
|
+
"BICFI, complemented optionally with a LEI (preferred option)."),
|
|
419
|
+
"R26": ("CBPR_Agent_National_only_TextualRule",
|
|
420
|
+
"Whenever Debtor Agent, Creditor Agent and all agents in between are located within the same "
|
|
421
|
+
"country, the clearing code only may be used."),
|
|
422
|
+
"R27": ("CBPR_Agent_Option_3_TextualRule",
|
|
423
|
+
"Name AND ([Structured postal address with minimum Town Name and Country] OR [Hybrid postal "
|
|
424
|
+
"address with minimum Town Name and Country]). It is recommended to also add the post code when available."),
|
|
425
|
+
"R28": ("CBPR_Agent_Option_2_TextualRule",
|
|
426
|
+
"(Clearing Code OR LEI) AND (Name AND ([Structured postal address with minimum Town Name and "
|
|
427
|
+
"Country] OR [Hybrid postal address with minimum Town Name and Country])). It is recommended to "
|
|
428
|
+
"also add the post code when available."),
|
|
429
|
+
"R31": ("CBPR_Ultimate_Debtor_Option_1_TextualRule",
|
|
430
|
+
"Name AND [(Structured Postal Address) OR (Hybrid Postal Address) with minimum Town Name & "
|
|
431
|
+
"Country - it is recommended to add Post code when available]."),
|
|
432
|
+
"R32": ("CBPR_UltimateDebtor_Option_3_Jurisdictions_only_TextualRule",
|
|
433
|
+
"For Jurisdictional transactions, Name and/or Identification (Private or Organisation) within a "
|
|
434
|
+
"country or for regions under same legislation (e.g. EEA)."),
|
|
435
|
+
"R33": ("CBPR_Ultimate_Debtor_Option_2_TextualRule",
|
|
436
|
+
"Name AND [(Structured Postal Address) OR (Hybrid Postal Address) with minimum Town Name & "
|
|
437
|
+
"Country] AND (Identification: Private or Organisation)."),
|
|
438
|
+
"R35": ("CBPR_Debtor_Option_2_TextualRule",
|
|
439
|
+
"Name AND ([Structured Address with minimum Town Name & Country] OR [Hybrid postal address with "
|
|
440
|
+
"minimum Town Name and Country]) AND (Account Number OR Identification). Not relevant in the "
|
|
441
|
+
"current pacs.004 (Debtor Account is not present)."),
|
|
442
|
+
"R38": ("CBPR_Debtor_Option_1_TextualRule",
|
|
443
|
+
"Organisation Identification/AnyBIC AND (Account Number OR Organisation Identification/Other). "
|
|
444
|
+
"Not relevant in the current pacs.004 (Debtor Account is not present)."),
|
|
445
|
+
"R39": ("CBPR_Debtor_Option_3_Jurisdictions_only_TextualRule",
|
|
446
|
+
"For Jurisdictional transactions, Debtor/Name is mandatory with either Debtor Account OR Debtor "
|
|
447
|
+
"Identification within a country or for regions under same legislation (e.g. EEA)."),
|
|
448
|
+
"R51": ("CBPR_Creditor_Option_2_TextualRule",
|
|
449
|
+
"Name AND ([Structured Address with minimum Town Name & Country] OR [Hybrid postal address with "
|
|
450
|
+
"minimum Town Name and Country]) AND (Account Number OR Identification). Not relevant in the "
|
|
451
|
+
"current pacs.004 (Debtor Account is not present)."),
|
|
452
|
+
"R54": ("CBPR_Creditor_Option_3_Jurisdictions_only_TextualRule",
|
|
453
|
+
"For Jurisdictional transactions, Creditor/Name is mandatory with either Creditor Account OR "
|
|
454
|
+
"Creditor Identification within a country or for regions under same legislation (e.g. EEA)."),
|
|
455
|
+
"R56": ("CBPR_Creditor_Option_1_TextualRule",
|
|
456
|
+
"Organisation Identification/AnyBIC AND (Account Number OR Organisation Identification/Other). "
|
|
457
|
+
"Not relevant in the current pacs.004 (Debtor Account is not present)."),
|
|
458
|
+
"R59": ("CBPR_Ultimate_Creditor_Option_1_TextualRule",
|
|
459
|
+
"Name AND [(Structured Postal Address) OR (Hybrid Postal Address) with minimum Town Name & "
|
|
460
|
+
"Country - it is recommended to add Post code when available]."),
|
|
461
|
+
"R60": ("CBPR_UltimateCreditor_Option_2_Jurisdictions_only_TextualRule",
|
|
462
|
+
"For Jurisdictional transactions, Name and/or Identification (Private or Organisation) within a "
|
|
463
|
+
"country or for regions under same legislation (e.g. EEA)."),
|
|
464
|
+
"R62": ("CBPR_Originator_Identification_TextualRule",
|
|
465
|
+
"If AnyBIC is present, in addition to any other optional elements, in case of conflicting "
|
|
466
|
+
"information it will always take precedence."),
|
|
467
|
+
"R66": ("CBPR_Agent_Point_To_Point_On_SWIFT_TextualRule",
|
|
468
|
+
"If the transaction is exchanged on the SWIFT network, then BIC is mandatory and other elements "
|
|
469
|
+
"are optional, e.g. LEI."),
|
|
470
|
+
"R73": ("CBPR_Debtor_Option_1_TextualRule",
|
|
471
|
+
"Organisation Identification/AnyBIC AND (Account Number OR Organisation Identification/Other)."),
|
|
472
|
+
"R76": ("CBPR_Debtor_Option_2_TextualRule",
|
|
473
|
+
"Name AND ([Structured Address with minimum Town Name & Country]) AND (Account Number OR "
|
|
474
|
+
"Identification: Private or Organisation)."),
|
|
475
|
+
"R79": ("CBPR_Agent_National_only_TextualRule",
|
|
476
|
+
"Whenever Debtor Agent, Creditor Agent and all agents in between are located within the same "
|
|
477
|
+
"country, the clearing code only may be used."),
|
|
478
|
+
"R82": ("CBPR_Creditor_Option_2_TextualRule",
|
|
479
|
+
"Name AND ([Structured Address with minimum Town Name & Country]) AND (Account Number OR "
|
|
480
|
+
"Identification: Private or Organisation)."),
|
|
481
|
+
"R84": ("CBPR_Creditor_Option_1_TextualRule",
|
|
482
|
+
"Organisation Identification/AnyBIC AND (Account Number OR Organisation Identification/Other)."),
|
|
483
|
+
"R86": ("CBPR_UltimateCreditor_Option_2_Jurisdictions_only_TextualRule",
|
|
484
|
+
"For Jurisdictional transactions, Name and/or Identification (Private or Organisation) within a "
|
|
485
|
+
"country or for regions under same legislation (e.g. EEA)."),
|
|
486
|
+
"R87": ("CBPR_Ultimate_Creditor_Option_1_TextualRule",
|
|
487
|
+
"Name AND [(Structured Postal Address) OR (Hybrid Postal Address) with minimum Town Name & "
|
|
488
|
+
"Country - it is recommended to add Post code when available]. Other elements are optional."),
|
|
489
|
+
}
|
|
490
|
+
for _num, (_name, _desc) in _ADVISORY.items():
|
|
491
|
+
advisory(MT, YEAR, _num, _name, _desc)
|