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.
Files changed (48) hide show
  1. cbpr_rules/__init__.py +21 -0
  2. cbpr_rules/cli.py +176 -0
  3. cbpr_rules/engine.py +100 -0
  4. cbpr_rules/helpers.py +420 -0
  5. cbpr_rules/loader.py +77 -0
  6. cbpr_rules/message.py +170 -0
  7. cbpr_rules/models.py +83 -0
  8. cbpr_rules/py.typed +0 -0
  9. cbpr_rules/reference/__init__.py +9 -0
  10. cbpr_rules/reference/countries.py +28 -0
  11. cbpr_rules/reference/currencies.py +25 -0
  12. cbpr_rules/registry.py +107 -0
  13. cbpr_rules/rules/__init__.py +1 -0
  14. cbpr_rules/rules/y2025/__init__.py +1 -0
  15. cbpr_rules/rules/y2025/camt_052.py +224 -0
  16. cbpr_rules/rules/y2025/camt_054.py +176 -0
  17. cbpr_rules/rules/y2025/pacs_002.py +212 -0
  18. cbpr_rules/rules/y2025/pacs_004.py +831 -0
  19. cbpr_rules/rules/y2025/pacs_008.py +375 -0
  20. cbpr_rules/rules/y2025/pacs_008_stp.py +367 -0
  21. cbpr_rules/rules/y2025/pacs_009.py +273 -0
  22. cbpr_rules/rules/y2025/pacs_009_adv.py +255 -0
  23. cbpr_rules/rules/y2025/pacs_009_cov.py +358 -0
  24. cbpr_rules/rules/y2025/pain_001.py +306 -0
  25. cbpr_rules/rules/y2026/__init__.py +1 -0
  26. cbpr_rules/rules/y2026/camt_052.py +191 -0
  27. cbpr_rules/rules/y2026/camt_054.py +182 -0
  28. cbpr_rules/rules/y2026/pacs_002.py +208 -0
  29. cbpr_rules/rules/y2026/pacs_004.py +491 -0
  30. cbpr_rules/rules/y2026/pacs_008.py +377 -0
  31. cbpr_rules/rules/y2026/pacs_008_stp.py +369 -0
  32. cbpr_rules/rules/y2026/pacs_009.py +260 -0
  33. cbpr_rules/rules/y2026/pacs_009_adv.py +256 -0
  34. cbpr_rules/rules/y2026/pacs_009_cov.py +324 -0
  35. cbpr_rules/rules/y2026/pain_001.py +272 -0
  36. cbpr_rules/schema.py +97 -0
  37. cbpr_rules/validators/__init__.py +16 -0
  38. cbpr_rules/validators/bic.py +21 -0
  39. cbpr_rules/validators/country.py +11 -0
  40. cbpr_rules/validators/currency.py +11 -0
  41. cbpr_rules/validators/iban.py +26 -0
  42. cbpr_rules/validators/lei.py +17 -0
  43. cbpr_usage_rules-0.1.0.dist-info/METADATA +335 -0
  44. cbpr_usage_rules-0.1.0.dist-info/RECORD +48 -0
  45. cbpr_usage_rules-0.1.0.dist-info/WHEEL +5 -0
  46. cbpr_usage_rules-0.1.0.dist-info/entry_points.txt +2 -0
  47. cbpr_usage_rules-0.1.0.dist-info/licenses/LICENSE +21 -0
  48. cbpr_usage_rules-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,255 @@
1
+ """CBPR+ SR2025 usage rules for pacs.009.001.08 ADV (FinancialInstitutionCreditTransfer - Advice).
2
+
3
+ Structure mirrors the reference module ``pacs_008``: each Rules-sheet R-index is
4
+ registered with its real number, name (the CBPR_..._FormalRule / _TextualRule
5
+ token) and description, implemented with a shared combinator from ``helpers`` or
6
+ a bespoke ``fn(msg, report)`` for cross-field / cross-schema logic.
7
+
8
+ In the FI credit transfer, the Debtor and Creditor are themselves financial
9
+ institutions, so they carry a ``FinInstnId`` block and use the agent-style
10
+ Name/PostalAddress + grace-period rules (not the customer party rules).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from ...registry import advisory, rule
15
+ from ...validators import is_valid_bic, is_valid_currency
16
+ from ...helpers import (
17
+ address_hybrid,
18
+ address_lines_max_length,
19
+ business_msg_id_carries_group_id,
20
+ code_in,
21
+ each_value_valid,
22
+ header_msg_def_id_matches,
23
+ no_postal_address_duplication,
24
+ not_matching_pattern,
25
+ presence_together,
26
+ required_when_absent,
27
+ )
28
+
29
+ MT = "pacs.009_adv"
30
+ YEAR = 2025
31
+ ROOT = "/Document/FICdtTrf"
32
+ TX = ROOT + "/CdtTrfTxInf"
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_GRACE_STRUCT = (
37
+ "If Postal Address is used, and if Address Line is absent, then Town Name "
38
+ "and Country must be present."
39
+ )
40
+ D_GRACE_HYBRID = (
41
+ "If Address Line is present and any other Postal Address element(s) are "
42
+ "present, then Town Name and Country are mandatory in Postal Address and a "
43
+ "maximum of two occurrences of Address Line are allowed."
44
+ )
45
+ D_GRACE_UNSTRUCT = (
46
+ "If Postal Address is present and if no other element than Address Line is "
47
+ "present then every occurrence of Address Line must not exceed 35 characters."
48
+ )
49
+
50
+
51
+ def reg(number: str, name: str, description: str, check) -> None:
52
+ """Register a combinator-built check as a rule."""
53
+ rule(MT, YEAR, number, name, description)(check)
54
+
55
+
56
+ def _agent_block(fin_inst_path: str, n_name, n_struct, n_hybrid, n_unstruct) -> None:
57
+ """The four rules that recur for each agent: Name+Address + grace period."""
58
+ pstl = fin_inst_path + "/PstlAdr"
59
+ reg(n_name, "CBPR_Agent_Name_Postal_Address_FormalRule", D_AGENT_NAME_ADR,
60
+ presence_together(fin_inst_path, "Nm", "PstlAdr"))
61
+ reg(n_struct, "CBPR_GracePeriod_Structured_FormalRule", D_GRACE_STRUCT,
62
+ required_when_absent(pstl, "AdrLine", ["TwnNm", "Ctry"]))
63
+ reg(n_hybrid, "CBPR_GracePeriod_Hybrid_FormalRule", D_GRACE_HYBRID,
64
+ address_hybrid(pstl))
65
+ reg(n_unstruct, "CBPR_GracePeriod_Unstructured_FormalRule", D_GRACE_UNSTRUCT,
66
+ address_lines_max_length(pstl, 35))
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Bespoke cross-field / cross-schema rules
71
+ # ---------------------------------------------------------------------------
72
+
73
+ def _values_match(msg, report, path_a, path_b, label):
74
+ a_nodes = msg.find(path_a)
75
+ if not a_nodes:
76
+ return
77
+ b_vals = {msg.text_of(n) for n in msg.find(path_b)}
78
+ if not b_vals:
79
+ return
80
+ a_vals = {msg.text_of(n) for n in a_nodes}
81
+ if a_vals != b_vals:
82
+ report(a_nodes[0], detail=label)
83
+
84
+
85
+ @rule(MT, YEAR, "R1", "CBPR_Priority_Instruction_Priority_FormalRule",
86
+ 'If "Priority" is used in the BAH for pacs messages, the value should be '
87
+ 'identical to the one in the "Payment Type Information/InstructionPriority" '
88
+ "if present.")
89
+ def _r1(msg, report):
90
+ if msg.present("/AppHdr/Prty") and msg.present(TX + "/PmtTpInf/InstrPrty"):
91
+ _values_match(msg, report, "/AppHdr/Prty", TX + "/PmtTpInf/InstrPrty",
92
+ "BAH Priority must equal InstructionPriority")
93
+
94
+
95
+ _BIC_PAIRS = [
96
+ ("/AppHdr/Fr/FIId/FinInstnId/BICFI", TX + "/InstgAgt/FinInstnId/BICFI", "From vs Instructing Agent"),
97
+ ("/AppHdr/To/FIId/FinInstnId/BICFI", TX + "/InstdAgt/FinInstnId/BICFI", "To vs Instructed Agent"),
98
+ ]
99
+
100
+
101
+ @rule(MT, YEAR, "R2", "CBPR_From_To_Instructing_Instructed_Agent_BIC_1_FormalRule",
102
+ 'BAH "From" BIC must match "Instructing Agent" BIC, except where BAH '
103
+ 'CopyDuplicate = COPY or = CODU. BAH "To" BIC must match "Instructed Agent" '
104
+ "BIC, except where BAH CopyDuplicate = COPY or = CODU.")
105
+ def _r2(msg, report):
106
+ if any(v in {"COPY", "CODU"} for v in msg.values("/AppHdr/CpyDplct")):
107
+ return
108
+ for a, b, label in _BIC_PAIRS:
109
+ _values_match(msg, report, a, b, label)
110
+
111
+
112
+ @rule(MT, YEAR, "R3", "CBPR_From_To_Instructing_Instructed_Agent_BIC_2_FormalRule",
113
+ 'BAH "From" BIC must match "Instructing Agent" BIC if CopyDuplicate is '
114
+ 'absent. BAH "To" BIC must match "Instructed Agent" BIC if CopyDuplicate is '
115
+ "absent.")
116
+ def _r3(msg, report):
117
+ if not msg.absent("/AppHdr/CpyDplct"):
118
+ return
119
+ for a, b, label in _BIC_PAIRS:
120
+ _values_match(msg, report, a, b, label)
121
+
122
+
123
+ # Reimbursement agents (Group Header / Settlement Information)
124
+ _agent_block(ROOT + "/GrpHdr/SttlmInf/InstgRmbrsmntAgt/FinInstnId", "R17", "R18", "R19", "R21")
125
+ _agent_block(ROOT + "/GrpHdr/SttlmInf/InstdRmbrsmntAgt/FinInstnId", "R22", "R23", "R24", "R25")
126
+
127
+
128
+ @rule(MT, YEAR, "R26", "CBPR_Instruction_Identification_FormalRule",
129
+ "This field must not start or end with a slash '/' and must not contain "
130
+ "two consecutive slashes '//'.",
131
+ )
132
+ def _r26(msg, report):
133
+ not_matching_pattern(TX + "/PmtId/InstrId", r"(/.*)|(.*/)|(.*//.*)")(msg, report)
134
+
135
+
136
+ @rule(MT, YEAR, "R28", "CBPR_End_To_End_Identification_FormalRule",
137
+ "In the E2E identification, the below restrictions apply to the first 16 "
138
+ 'characters: - The first one and the 16th one cannot be "/" and - The '
139
+ 'string of 16 characters cannot contain "//".')
140
+ def _r28(msg, report):
141
+ # forbidden if matches '/.*' OR '.{15}/.*' OR '.{0,14}//.*'
142
+ import re as _re
143
+ pats = [_re.compile(r"/.*"), _re.compile(r".{15}/.*"), _re.compile(r".{0,14}//.*")]
144
+ for node in msg.find(TX + "/PmtId/EndToEndId"):
145
+ val = msg.text_of(node)
146
+ if val and any(p.fullmatch(val) for p in pats):
147
+ report(node, detail="EndToEndId violates first-16-character slash restrictions")
148
+
149
+
150
+ reg("R31", "CBPR_Interbank_Settlement_Currency_FormalRule",
151
+ "The codes XAU, XAG, XPD and XPT are not allowed, as these are codes are "
152
+ "only used for commodities.",
153
+ lambda msg, report: [
154
+ report(el, detail=f"commodity currency '{ccy}' not allowed")
155
+ for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
156
+ if ccy in {"XAU", "XAG", "XPD", "XPT"}
157
+ ])
158
+
159
+
160
+ # Transaction-chain agents and FI parties (each: name/address + grace period).
161
+ _agent_block(TX + "/PrvsInstgAgt1/FinInstnId", "R32", "R33", "R34", "R35")
162
+ _agent_block(TX + "/PrvsInstgAgt2/FinInstnId", "R36", "R37", "R38", "R39")
163
+ _agent_block(TX + "/PrvsInstgAgt3/FinInstnId", "R40", "R41", "R42", "R43")
164
+ _agent_block(TX + "/IntrmyAgt1/FinInstnId", "R44", "R45", "R46", "R47")
165
+ _agent_block(TX + "/IntrmyAgt2/FinInstnId", "R48", "R49", "R50", "R51")
166
+ _agent_block(TX + "/IntrmyAgt3/FinInstnId", "R52", "R53", "R54", "R55")
167
+ _agent_block(TX + "/Dbtr/FinInstnId", "R56", "R57", "R58", "R59")
168
+ _agent_block(TX + "/DbtrAgt/FinInstnId", "R60", "R61", "R62", "R63")
169
+ _agent_block(TX + "/CdtrAgt/FinInstnId", "R64", "R65", "R66", "R67")
170
+ _agent_block(TX + "/Cdtr/FinInstnId", "R68", "R69", "R70", "R71")
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # Mechanizable textual rules + algorithmic field validation
175
+ # ---------------------------------------------------------------------------
176
+ reg("R8", "CBPR_Business_Service_Usage_TextualRule",
177
+ 'The value "swift.cbprplus.adv.03" must be used.',
178
+ code_in("/AppHdr/BizSvc", ["swift.cbprplus.adv.03"]))
179
+
180
+ # Promoted from advisory: cross-schema header consistency + address duplication.
181
+ reg("R5", "CBPR_Business_Message_Identifier_TextualRule",
182
+ "The Business Message Identifier is the unique identifier of the Business Message instance. "
183
+ "Must contain the Message Identification element from the Group Header of the underlying "
184
+ "message, where available.",
185
+ business_msg_id_carries_group_id())
186
+
187
+ reg("R6", "CBPR_Message_Definition_Identifier_TextualRule",
188
+ "The Message Definition Identifier must be formatted exactly as it appears in the namespace "
189
+ "of the Business Message instance.",
190
+ header_msg_def_id_matches())
191
+
192
+ reg("R20", "CBPR_Duplication_Postal_Address_TextualRule",
193
+ "Data present in structured elements within the Postal Address must not, under any "
194
+ "circumstances be repeated in AddressLine.",
195
+ no_postal_address_duplication())
196
+
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # Specific algorithmic validations required by the brief (fields present here).
200
+ # ---------------------------------------------------------------------------
201
+ reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
202
+ "Interbank Settlement Amount currency must be a valid ISO 4217 code.",
203
+ lambda msg, report: [
204
+ report(el, detail=f"invalid currency '{ccy}'")
205
+ for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
206
+ if ccy and not is_valid_currency(ccy)
207
+ ])
208
+
209
+ reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
210
+ "Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
211
+ each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"))
212
+
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Advisory textual rules (not mechanically enforceable - surfaced as guidance)
216
+ # ---------------------------------------------------------------------------
217
+ _ADVISORY = {
218
+ "R4": ("CBPR_Character_Set_Usage_TextualRule",
219
+ "For further description on the usage of the field, please refer to the CBPR Plus UHB."),
220
+ "R7": ("CBPR_Business_Service_TextualRule",
221
+ "Business Service may be used by SWIFT to support differentiated processing on "
222
+ "SWIFT-administered services such as FINplus."),
223
+ "R9": ("CBPR_Market_Practice_TextualRule",
224
+ "Market Practice may be used by SWIFT on SWIFT-administered services."),
225
+ "R10": ("CBPR_Related_Business_Application_Header_TextualRule",
226
+ "If used, the Related BAH must transport the exact same information as in the BAH of the "
227
+ "related message."),
228
+ "R11": ("CBPR_Business_Service_TextualRule",
229
+ "This field may be used by SWIFT to support differentiated processing on SWIFT-administered "
230
+ "services such as FINplus."),
231
+ "R12": ("CBPR_Related_BAH_Business_Service_TextualRule",
232
+ "If related BAH is present, it should transport the element Business Service."),
233
+ "R13": ("CBPR_Agent_National_only_TextualRule",
234
+ "Whenever Debtor Agent, Creditor Agent and all agents in between are located within the "
235
+ "same country, the clearing code only may be used."),
236
+ "R14": ("CBPR_Agent_Option_1_TextualRule",
237
+ "BICFI, complemented optionally with a LEI (preferred option)."),
238
+ "R15": ("CBPR_Agent_Option_2_TextualRule",
239
+ "(Clearing Code OR LEI) AND (Name AND (Unstructured postal address OR Structured postal "
240
+ "address with minimum Town Name and Country OR Hybrid postal address with minimum Town Name "
241
+ "and Country))."),
242
+ "R16": ("CBPR_Agent_Option_3_TextualRule",
243
+ "Name AND (Unstructured OR Structured postal address with minimum Town Name and Country OR "
244
+ "Hybrid postal address with minimum Town Name and Country)."),
245
+ "R27": ("CBPR_E2E_ADV_TextualRule",
246
+ "In the pacs.009 ADV, the E2E identification is provided by the Debtor (Agent)."),
247
+ "R29": ("CBPR_Local_Instrument_TextualRule",
248
+ "The preferred option is coded information."),
249
+ "R30": ("CBPR_Category_Purpose_TextualRule",
250
+ "The preferred option is coded information."),
251
+ "R72": ("CBPR_Purpose_Guideline",
252
+ "The preferred option is coded information."),
253
+ }
254
+ for _num, (_name, _desc) in _ADVISORY.items():
255
+ advisory(MT, YEAR, _num, _name, _desc)
@@ -0,0 +1,358 @@
1
+ """CBPR+ SR2025 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
+ address_hybrid,
16
+ address_lines_max_length,
17
+ bic_presence_exclusive,
18
+ business_msg_id_carries_group_id,
19
+ code_in,
20
+ each_value_valid,
21
+ header_msg_def_id_matches,
22
+ mutually_exclusive,
23
+ no_postal_address_duplication,
24
+ not_matching_pattern,
25
+ presence_together,
26
+ required_when_absent,
27
+ requires_if_present,
28
+ structured_remittance_max_total,
29
+ )
30
+
31
+ MT = "pacs.009_cov"
32
+ YEAR = 2025
33
+ ROOT = "/Document/FICdtTrf"
34
+ TX = ROOT + "/CdtTrfTxInf"
35
+ UND = TX + "/UndrlygCstmrCdtTrf"
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_GRACE_STRUCT = (
45
+ "If Postal Address is used, and if Address Line is absent, then Town Name "
46
+ "and Country must be present."
47
+ )
48
+ D_GRACE_HYBRID = (
49
+ "If Address Line is present and any other Postal Address element(s) are "
50
+ "present, then Town Name and Country are mandatory in Postal Address and a "
51
+ "maximum of two occurrences of Address Line are allowed."
52
+ )
53
+ D_GRACE_UNSTRUCT = (
54
+ "If Postal Address is present and if no other element than Address Line is "
55
+ "present then every occurrence of Address Line must not exceed 35 characters."
56
+ )
57
+
58
+
59
+ def reg(number: str, name: str, description: str, check) -> None:
60
+ """Register a combinator-built check as a rule."""
61
+ rule(MT, YEAR, number, name, description)(check)
62
+
63
+
64
+ def _agent_block(fin_inst_path: str, n_name, n_struct, n_hybrid, n_unstruct) -> None:
65
+ """The four rules that recur for each agent: Name+Address + grace period."""
66
+ pstl = fin_inst_path + "/PstlAdr"
67
+ reg(n_name, "CBPR_Agent_Name_Postal_Address_FormalRule", D_AGENT_NAME_ADR,
68
+ presence_together(fin_inst_path, "Nm", "PstlAdr"))
69
+ reg(n_struct, "CBPR_GracePeriod_Structured_FormalRule", D_GRACE_STRUCT,
70
+ required_when_absent(pstl, "AdrLine", ["TwnNm", "Ctry"]))
71
+ reg(n_hybrid, "CBPR_GracePeriod_Hybrid_FormalRule", D_GRACE_HYBRID,
72
+ address_hybrid(pstl))
73
+ reg(n_unstruct, "CBPR_GracePeriod_Unstructured_FormalRule", D_GRACE_UNSTRUCT,
74
+ address_lines_max_length(pstl, 35))
75
+
76
+
77
+ def _party_grace_block(party_path: str, n_struct, n_hybrid, n_unstruct) -> None:
78
+ pstl = party_path + "/PstlAdr"
79
+ reg(n_struct, "CBPR_GracePeriod_Structured_FormalRule", D_GRACE_STRUCT,
80
+ required_when_absent(pstl, "AdrLine", ["TwnNm", "Ctry"]))
81
+ reg(n_hybrid, "CBPR_GracePeriod_Hybrid_FormalRule", D_GRACE_HYBRID,
82
+ address_hybrid(pstl))
83
+ reg(n_unstruct, "CBPR_GracePeriod_Unstructured_FormalRule", D_GRACE_UNSTRUCT,
84
+ address_lines_max_length(pstl, 35))
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Bespoke cross-field / cross-schema rules
89
+ # ---------------------------------------------------------------------------
90
+
91
+ def _values_match(msg, report, path_a, path_b, label):
92
+ a_nodes = msg.find(path_a)
93
+ if not a_nodes:
94
+ return
95
+ b_vals = {msg.text_of(n) for n in msg.find(path_b)}
96
+ if not b_vals:
97
+ return
98
+ a_vals = {msg.text_of(n) for n in a_nodes}
99
+ if a_vals != b_vals:
100
+ report(a_nodes[0], detail=label)
101
+
102
+
103
+ @rule(MT, YEAR, "R1", "CBPR_Priority_Instruction_Priority_FormalRule",
104
+ 'If "Priority" is used in the BAH for pacs messages, the value should be '
105
+ 'identical to the one in the "Payment Type Information/InstructionPriority" if present.')
106
+ def _r1(msg, report):
107
+ if msg.present("/AppHdr/Prty") and msg.present(TX + "/PmtTpInf/InstrPrty"):
108
+ _values_match(msg, report, "/AppHdr/Prty", TX + "/PmtTpInf/InstrPrty",
109
+ "BAH Priority must equal InstructionPriority")
110
+
111
+
112
+ _BIC_PAIRS = [
113
+ ("/AppHdr/Fr/FIId/FinInstnId/BICFI", TX + "/InstgAgt/FinInstnId/BICFI", "From vs Instructing Agent"),
114
+ ("/AppHdr/To/FIId/FinInstnId/BICFI", TX + "/InstdAgt/FinInstnId/BICFI", "To vs Instructed Agent"),
115
+ ]
116
+
117
+
118
+ @rule(MT, YEAR, "R2", "CBPR_From_To_Instructing_Instructed_Agent_BIC_1_FormalRule",
119
+ 'BAH "From" BIC must match "Instructing Agent" BIC, except where BAH '
120
+ 'CopyDuplicate = COPY or = CODU. BAH "To" BIC must match "Instructed Agent" '
121
+ 'BIC, except where BAH CopyDuplicate = COPY or = CODU.')
122
+ def _r2(msg, report):
123
+ if any(v in {"COPY", "CODU"} for v in msg.values("/AppHdr/CpyDplct")):
124
+ return
125
+ for a, b, label in _BIC_PAIRS:
126
+ _values_match(msg, report, a, b, label)
127
+
128
+
129
+ @rule(MT, YEAR, "R3", "CBPR_From_To_Instructing_Instructed_Agent_BIC_2_FormalRule",
130
+ 'BAH "From" BIC must match "Instructing Agent" BIC if CopyDuplicate is '
131
+ 'absent. BAH "To" BIC must match "Instructed Agent" BIC if CopyDuplicate is absent.')
132
+ def _r3(msg, report):
133
+ if not msg.absent("/AppHdr/CpyDplct"):
134
+ return
135
+ for a, b, label in _BIC_PAIRS:
136
+ _values_match(msg, report, a, b, label)
137
+
138
+
139
+ # R12: remittance mutually exclusive (under the underlying customer credit transfer)
140
+ reg("R12", "CBPR_Remittance_Mutually_Exclusive_FormalRule",
141
+ "Either Structured or Unstructured Remittance can be present.",
142
+ mutually_exclusive(UND + "/RmtInf", ["Ustrd", "Strd"]))
143
+
144
+
145
+ @rule(MT, YEAR, "R13", "CBPR_Instruction_For_Creditor_Presence_Code_FormalRule",
146
+ 'Each code can only be used once for element "Instruction For Creditor Agent".')
147
+ def _r13(msg, report):
148
+ for tx in msg.each(TX):
149
+ codes = msg.values("InstrForCdtrAgt/Cd", tx)
150
+ if len(codes) != len(set(codes)):
151
+ report(tx, detail="duplicate InstructionForCreditorAgent code")
152
+
153
+
154
+ reg("R14", "CBPR_Instruction_Identification_FormalRule",
155
+ "This field must not start or end with a slash '/' and must not contain two "
156
+ "consecutive slashes '//'.",
157
+ not_matching_pattern(TX + "/PmtId/InstrId", r"(/.*)|(.*/)|(.*//.*)"))
158
+
159
+
160
+ @rule(MT, YEAR, "R16", "CBPR_End_To_End_Identification_FormalRule",
161
+ "In the E2E identification, the below restrictions apply to the first 16 "
162
+ 'characters: - The first one and the 16th one cannot be "/" and - The '
163
+ 'string of 16 characters cannot contain "//"')
164
+ def _r16(msg, report):
165
+ import re as _re
166
+ pats = [_re.compile(p) for p in (r"/.*", r".{15}/.*", r".{0,14}//.*")]
167
+ for node in msg.find(TX + "/PmtId/EndToEndId"):
168
+ val = msg.text_of(node)
169
+ if val and any(p.fullmatch(val) for p in pats):
170
+ report(node, detail="EndToEndId first 16 characters violate slash restrictions")
171
+
172
+
173
+ reg("R20", "CBPR_Interbank_Settlement_Currency_FormalRule",
174
+ "The codes XAU, XAG, XPD and XPT are not allowed, as these are codes are "
175
+ "only used for commodities.",
176
+ lambda msg, report: [
177
+ report(el, detail=f"commodity currency '{ccy}' not allowed")
178
+ for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
179
+ if ccy in {"XAU", "XAG", "XPD", "XPT"}
180
+ ])
181
+
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # Agent blocks - cover (interbank) chain
185
+ # ---------------------------------------------------------------------------
186
+ _agent_block(TX + "/PrvsInstgAgt1/FinInstnId", "R25", "R26", "R27", "R29")
187
+ _agent_block(TX + "/PrvsInstgAgt2/FinInstnId", "R30", "R31", "R32", "R33")
188
+ _agent_block(TX + "/PrvsInstgAgt3/FinInstnId", "R34", "R35", "R36", "R37")
189
+ _agent_block(TX + "/IntrmyAgt1/FinInstnId", "R38", "R39", "R40", "R41")
190
+ _agent_block(TX + "/IntrmyAgt2/FinInstnId", "R42", "R43", "R44", "R45")
191
+ _agent_block(TX + "/IntrmyAgt3/FinInstnId", "R46", "R47", "R48", "R49")
192
+ _agent_block(TX + "/Dbtr/FinInstnId", "R50", "R51", "R52", "R53")
193
+ _agent_block(TX + "/DbtrAgt/FinInstnId", "R54", "R55", "R56", "R57")
194
+ _agent_block(TX + "/CdtrAgt/FinInstnId", "R58", "R59", "R60", "R61")
195
+ _agent_block(TX + "/Cdtr/FinInstnId", "R62", "R63", "R64", "R65")
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # Underlying customer credit transfer - parties
199
+ # ---------------------------------------------------------------------------
200
+ reg("R70", "CBPR_Party_Name_Postal_Address_FormalRule", D_PARTY_NAME_ADR,
201
+ requires_if_present(UND + "/UltmtDbtr", "PstlAdr", "Nm"))
202
+ reg("R71", "CBPR_Party_Name_Postal_Address_FormalRule", D_PARTY_NAME_ADR,
203
+ requires_if_present(UND + "/InitgPty", "PstlAdr", "Nm"))
204
+ reg("R72", "CBPR_Party_Name_Any_BIC_FormalRule", D_PARTY_ANY_BIC,
205
+ required_when_absent(UND + "/Dbtr", "Id/OrgId/AnyBIC", ["Nm"]))
206
+ reg("R76", "CBPR_Party_Name_Postal_Address_FormalRule", D_PARTY_NAME_ADR,
207
+ requires_if_present(UND + "/Dbtr", "PstlAdr", "Nm"))
208
+ _party_grace_block(UND + "/Dbtr", "R78", "R79", "R80")
209
+
210
+ # Underlying agent chain
211
+ _agent_block(UND + "/DbtrAgt/FinInstnId", "R81", "R82", "R83", "R84")
212
+ _agent_block(UND + "/PrvsInstgAgt1/FinInstnId", "R85", "R86", "R87", "R88")
213
+ _agent_block(UND + "/PrvsInstgAgt2/FinInstnId", "R89", "R90", "R91", "R92")
214
+ _agent_block(UND + "/PrvsInstgAgt3/FinInstnId", "R93", "R94", "R95", "R96")
215
+ _agent_block(UND + "/IntrmyAgt1/FinInstnId", "R97", "R98", "R99", "R100")
216
+ _agent_block(UND + "/IntrmyAgt2/FinInstnId", "R101", "R102", "R103", "R104")
217
+ _agent_block(UND + "/IntrmyAgt3/FinInstnId", "R105", "R106", "R107", "R108")
218
+ _agent_block(UND + "/CdtrAgt/FinInstnId", "R109", "R110", "R111", "R112")
219
+
220
+ reg("R113", "CBPR_Party_Name_Any_BIC_FormalRule", D_PARTY_ANY_BIC,
221
+ required_when_absent(UND + "/Cdtr", "Id/OrgId/AnyBIC", ["Nm"]))
222
+ reg("R117", "CBPR_Party_Name_Postal_Address_FormalRule", D_PARTY_NAME_ADR,
223
+ requires_if_present(UND + "/Cdtr", "PstlAdr", "Nm"))
224
+ _party_grace_block(UND + "/Cdtr", "R119", "R120", "R121")
225
+
226
+ reg("R124", "CBPR_Name_Postal_Address_FormalRule", D_PARTY_NAME_ADR,
227
+ requires_if_present(UND + "/UltmtCdtr", "PstlAdr", "Nm"))
228
+
229
+
230
+ # ---------------------------------------------------------------------------
231
+ # Mechanizable textual rules + algorithmic field validation
232
+ # ---------------------------------------------------------------------------
233
+ reg("R8", "CBPR_Business_Service_Usage_TextualRule",
234
+ 'The value "swift.cbprplus.cov.03" must be used.',
235
+ code_in("/AppHdr/BizSvc", ["swift.cbprplus.cov.03"]))
236
+
237
+ # Algorithmic validations (brief), for fields present in pacs.009 COV.
238
+ reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
239
+ "Interbank Settlement Amount currency must be a valid ISO 4217 code.",
240
+ lambda msg, report: [
241
+ report(el, detail=f"invalid currency '{ccy}'")
242
+ for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
243
+ if ccy and not is_valid_currency(ccy)
244
+ ])
245
+
246
+ reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
247
+ "Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
248
+ each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"))
249
+
250
+
251
+ # ---------------------------------------------------------------------------
252
+ # Promoted from advisory: mechanizable cross-schema / cross-field checks.
253
+ # Each combinator is conservative (skips when inputs are absent/ambiguous).
254
+ # ---------------------------------------------------------------------------
255
+ reg("R5", "CBPR_Business_Message_Identifier_TextualRule",
256
+ "The Business Message Identifier is the unique identifier of the Business Message instance "
257
+ "that is being transported with this header, as defined by the sending application or system. "
258
+ "Must contain the Message Identification element from the Group Header of the underlying message, "
259
+ "where available.",
260
+ business_msg_id_carries_group_id())
261
+
262
+ reg("R6", "CBPR_Message_Definition_Identifier_TextualRule",
263
+ "The Message Definition Identifier of the Business Message instance that is being transported "
264
+ "with this header. In general, it must be formatted exactly as it appears in the namespace of "
265
+ "the Business Message instance.",
266
+ header_msg_def_id_matches())
267
+
268
+ reg("R28", "CBPR_Duplication_Postal_Address_TextualRule",
269
+ "Data present in structured elements within the Postal Address must not, under any circumstances "
270
+ "be repeated in AddressLine.",
271
+ no_postal_address_duplication())
272
+
273
+ reg("R77", "CBPR_Debtor_BIC_Presence_TextualRule",
274
+ "If Any BIC is present, then (Name and Postal Address) is NOT allowed (other elements remain "
275
+ "optional) - However, in case of conflicting information, AnyBIC will always take precedence.",
276
+ bic_presence_exclusive(UND + "/Dbtr"))
277
+
278
+ reg("R118", "CBPR_Creditor_BIC_Presence_TextualRule",
279
+ "If Any BIC is present, then (Name and Postal Address) is NOT allowed (other elements remain "
280
+ "optional) - However, in case of conflicting information, AnyBIC will always take precedence.",
281
+ bic_presence_exclusive(UND + "/Cdtr"))
282
+
283
+ reg("R125", "CBPR_RemittanceInformation_TextualRule",
284
+ "1. Use of Structured Remittance must be bilaterally or multilaterally agreed. 2. Structured "
285
+ "Remittance can be repeated, however the total business data for all occurrences (excluding tags) "
286
+ "must not exceed 9,000 characters.",
287
+ structured_remittance_max_total(UND + "/RmtInf/Strd", 9000))
288
+
289
+
290
+ # ---------------------------------------------------------------------------
291
+ # Advisory textual rules (not mechanically enforceable - surfaced as guidance)
292
+ # ---------------------------------------------------------------------------
293
+ _ADVISORY = {
294
+ "R4": ("CBPR_Character_Set_Usage_TextualRule",
295
+ "For further description on the usage of the field, pls refer to the CBPR Plus UHB."),
296
+ "R7": ("CBPR_Business_Service_TextualRule",
297
+ "This field may be used by SWIFT to support differentiated processing on SWIFT-administered "
298
+ "services such as FINplus."),
299
+ "R9": ("CBPR_Market_Practice_TextualRule",
300
+ "This field may be used by SWIFT on SWIFT-administered services. For a description of reserved "
301
+ "values, please refer to the Service Description for your service."),
302
+ "R10": ("CBPR_Related_Business_Application_Header_TextualRule",
303
+ "If used, the Related BAH must transport the exact same information as in the BAH of the related message."),
304
+ "R11": ("CBPR_Related_BAH_Business_Service_TextualRule",
305
+ "If related BAH is present, it should transport the element Business Service."),
306
+ "R15": ("CBPR_E2E_COV_TextualRule",
307
+ "In the pacs.009 COV, the E2E identification should transport the instruction identification "
308
+ "of the underlying pacs.008."),
309
+ "R17": ("CBPR_UETR_COV_TextualRule",
310
+ "In the pacs.009 COV, the UETR should transport the UETR of the underlying pacs.008."),
311
+ "R18": ("CBPR_Local_Instrument_Guideline",
312
+ "The preferred option is coded information."),
313
+ "R19": ("CBPR_Category_Purpose_Guideline",
314
+ "The preferred option is coded information."),
315
+ "R21": ("CBPR_Agent_National_only_TextualRule",
316
+ "Whenever Debtor Agent, Creditor Agent and all agents in between are located within the same "
317
+ "country, the clearing code only may be used."),
318
+ "R22": ("CBPR_Agent_Option_1_TextualRule",
319
+ "BICFI, complemented optionally with a LEI (preferred option)."),
320
+ "R23": ("CBPR_Agent_Option_2_TextualRule",
321
+ "(Clearing Code OR LEI) AND (Name AND (Unstructured postal address OR [Structured postal address "
322
+ "with minimum Town Name and Country] OR [Hybrid postal address with minimum Town Name and Country])."),
323
+ "R24": ("CBPR_Agent_Option_3_TextualRule",
324
+ "Name AND (Unstructured OR [Structured postal address with minimum Town Name and Country] OR "
325
+ "[Hybrid postal address with minimum Town Name and Country])."),
326
+ "R66": ("CBPR_Purpose_Guideline",
327
+ "The preferred option is coded information."),
328
+ "R67": ("CBPR_UltimateDebtor_Option_3_Jurisdictions_only_TextualRule",
329
+ "For Jurisdictional transactions, Name and/or Identification (Private or Organisation)."),
330
+ "R68": ("CBPR_Ultimate_Debtor_Option_1_TextualRule",
331
+ "Name AND [(Structured Postal Address) OR (Hybrid Postal Address) with minimum Town Name & Country]."),
332
+ "R69": ("CBPR_Ultimate_Debtor_Option_2_TextualRule",
333
+ "Name AND [(Structured Postal Address) OR (Hybrid Postal Address) with minimum Town Name & Country] "
334
+ "AND (Identification: Private or Organisation)."),
335
+ "R73": ("CBPR_Debtor_Option_3_Jurisdictions_only_TextualRule",
336
+ "For Jurisdictional transactions, Debtor/Name is mandatory with either Debtor Account OR Debtor "
337
+ "Identification."),
338
+ "R74": ("CBPR_Debtor_Option_1_TextualRule",
339
+ "Organisation Identification/AnyBIC AND (Account Number OR Organisation Identification/Other)."),
340
+ "R75": ("CBPR_Debtor_Option_2_TextualRule",
341
+ "Name AND (Unstructured OR [Structured Address with minimum Town Name & Country] OR [Hybrid postal "
342
+ "address with minimum Town Name and Country]) AND (Account Number OR Identification: Private or Organisation)."),
343
+ "R114": ("CBPR_Creditor_Option_3_Jurisdictions_only_TextualRule",
344
+ "For Jurisdictional transactions, Creditor/Name is mandatory with either Creditor Account OR "
345
+ "Creditor Identification."),
346
+ "R115": ("CBPR_Creditor_Option_1_TextualRule",
347
+ "Organisation Identification/AnyBIC AND (Account Number OR Organisation Identification/Other)."),
348
+ "R116": ("CBPR_Creditor_Option_2_TextualRule",
349
+ "Name AND (Unstructured OR [Structured Address with minimum Town Name & Country] OR [Hybrid postal "
350
+ "address with minimum Town Name and Country]) AND (Account Number OR Identification: Private or Organisation)."),
351
+ "R122": ("CBPR_Ultimate_Creditor_Option_1_TextualRule",
352
+ "Name AND Structured Address, with minimum Country (other elements are optional, eg "
353
+ "Identification: Private or Organisation)."),
354
+ "R123": ("CBPR_UltimateCreditor_Option_2_Jurisdictions_only_TextualRule",
355
+ "For Jurisdictional transactions, Name and/or Identification (Private or Organisation)."),
356
+ }
357
+ for _num, (_name, _desc) in _ADVISORY.items():
358
+ advisory(MT, YEAR, _num, _name, _desc)