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,306 @@
1
+ """CBPR+ SR2025 usage rules for pain.001.001.09 (CustomerCreditTransferInitiation).
2
+
3
+ Authored against the published usage guideline's Rules sheet. Rule numbers, names
4
+ and descriptions are taken verbatim from the sheet; XML paths are the short ISO
5
+ 20022 tags from the Full_View / XML Path column. Structure mirrors the reference
6
+ module (``pacs_008``): combinator-built rules go through ``reg``/``_agent_block``/
7
+ ``_party_block``; bespoke cross-field logic uses ``@rule`` directly.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from ...registry import advisory, rule
12
+ from ...validators import is_valid_bic, is_valid_country, is_valid_currency, is_valid_lei
13
+ from ...helpers import (
14
+ address_hybrid,
15
+ address_lines_max_length,
16
+ business_msg_id_carries_group_id,
17
+ code_in,
18
+ each_value_valid,
19
+ header_msg_def_id_matches,
20
+ mutually_exclusive,
21
+ no_postal_address_duplication,
22
+ required_when_absent,
23
+ structured_remittance_max_total,
24
+ )
25
+
26
+ MT = "pain.001"
27
+ YEAR = 2025
28
+ ROOT = "/Document/CstmrCdtTrfInitn"
29
+ PMTINF = ROOT + "/PmtInf"
30
+ TX = PMTINF + "/CdtTrfTxInf"
31
+
32
+ # Repeated grace-period rule descriptions (identical across the locations).
33
+ D_GRACE_STRUCT = (
34
+ "If Postal Address is used, and if Address Line is absent, then Town Name "
35
+ "and Country must be present."
36
+ )
37
+ D_GRACE_HYBRID = (
38
+ "If Address Line is present and any other Postal Address element(s) are "
39
+ "present, then Town Name and Country are mandatory in Postal Address and a "
40
+ "maximum of two occurrences of Address Line are allowed."
41
+ )
42
+ D_GRACE_UNSTRUCT = (
43
+ "If Postal Address is present and if no other element than Address Line is "
44
+ "present then every occurrence of Address Line must not exceed 35 characters."
45
+ )
46
+
47
+
48
+ def reg(number: str, name: str, description: str, check) -> None:
49
+ """Register a combinator-built check as a rule."""
50
+ rule(MT, YEAR, number, name, description)(check)
51
+
52
+
53
+ def _grace_block(pstl_path: str, n_struct: str, n_hybrid: str, n_unstruct: str) -> None:
54
+ """The three grace-period rules that recur for each postal address."""
55
+ reg(n_struct, "CBPR_GracePeriod_Structured_FormalRule", D_GRACE_STRUCT,
56
+ required_when_absent(pstl_path, "AdrLine", ["TwnNm", "Ctry"]))
57
+ reg(n_hybrid, "CBPR_GracePeriod_Hybrid_FormalRule", D_GRACE_HYBRID,
58
+ address_hybrid(pstl_path))
59
+ reg(n_unstruct, "CBPR_GracePeriod_Unstructured_FormalRule", D_GRACE_UNSTRUCT,
60
+ address_lines_max_length(pstl_path, 35))
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Formal rules
65
+ # ---------------------------------------------------------------------------
66
+ reg("R16", "CBPR_Remittance_Mutually_Exclusive_FormalRule",
67
+ "Either Structured or Unstructured Remittance can be present, not both "
68
+ "together. Both may be absent.",
69
+ mutually_exclusive(TX, ["RmtInf/Ustrd", "RmtInf/Strd"]))
70
+
71
+ # Debtor postal address (party level on Payment Information).
72
+ _grace_block(PMTINF + "/Dbtr/PstlAdr", "R23", "R24", "R25")
73
+ # Debtor Agent postal address.
74
+ _grace_block(PMTINF + "/DbtrAgt/FinInstnId/PstlAdr", "R31", "R32", "R33")
75
+ # Transaction-chain agents and parties (per CreditTransferTransactionInformation).
76
+ _grace_block(TX + "/IntrmyAgt1/FinInstnId/PstlAdr", "R46", "R47", "R48")
77
+ _grace_block(TX + "/IntrmyAgt2/FinInstnId/PstlAdr", "R50", "R51", "R52")
78
+ _grace_block(TX + "/IntrmyAgt3/FinInstnId/PstlAdr", "R53", "R54", "R55")
79
+ _grace_block(TX + "/CdtrAgt/FinInstnId/PstlAdr", "R56", "R57", "R58")
80
+ _grace_block(TX + "/Cdtr/PstlAdr", "R59", "R60", "R61")
81
+
82
+
83
+ @rule(MT, YEAR, "R64", "CBPR_Instruction_for_Creditor_Agent1_TextualRule",
84
+ "If CHQB is present Then HOLD is not allowed Else HOLD is optional.")
85
+ def _r64(msg, report):
86
+ for tx in msg.each(TX):
87
+ codes = set(msg.values("InstrForCdtrAgt/Cd", tx))
88
+ if "CHQB" in codes and "HOLD" in codes:
89
+ report(tx, detail="HOLD not allowed when CHQB present")
90
+
91
+
92
+ @rule(MT, YEAR, "R65", "CBPR_Instruction_for_Creditor_Agent2_TextualRule",
93
+ "If PHOB is present Then TELB is not allowed Else TELB is optional.")
94
+ def _r65(msg, report):
95
+ for tx in msg.each(TX):
96
+ codes = set(msg.values("InstrForCdtrAgt/Cd", tx))
97
+ if "PHOB" in codes and "TELB" in codes:
98
+ report(tx, detail="TELB not allowed when PHOB present")
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # Mechanizable textual rule (fixed value)
103
+ # ---------------------------------------------------------------------------
104
+ reg("R5", "CBPR_Business_Service_Usage_TextualRule",
105
+ 'The value "swift.cbprplus.03" must be used.',
106
+ code_in("/AppHdr/BizSvc", ["swift.cbprplus.03"]))
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Mechanizable rules promoted from advisory (cross-schema / cross-field).
110
+ # Each combinator is conservative: it no-ops when its inputs are absent, so a
111
+ # valid message can never be made to fail spuriously.
112
+ # ---------------------------------------------------------------------------
113
+ reg("R2", "CBPR_Business_Message_Identifier_TextualRule",
114
+ "The Business Message Identifier is the unique identifier of the Business "
115
+ "Message instance that is being transported with this header, as defined by "
116
+ "the sending application or system.",
117
+ business_msg_id_carries_group_id())
118
+
119
+ reg("R3", "CBPR_Message_Definition_Identifier_TextualRule",
120
+ "The Message Definition Identifier of the Business Message instance that is "
121
+ "being transported with this header. In general, it must be formatted exactly "
122
+ "as it appears in the namespace of the Business Message instance.",
123
+ header_msg_def_id_matches())
124
+
125
+ reg("R8", "CBPR_Business_Message_Identifier_TextualRule",
126
+ "The Business Message Identifier is the unique identifier of the Business "
127
+ "Message instance that is being transported with this header.",
128
+ business_msg_id_carries_group_id())
129
+
130
+ reg("R13", "CBPR_Duplication_Postal_Address_TextualRule",
131
+ "Data present in structured elements within the Postal Address must not, "
132
+ "under any circumstances be repeated in AddressLine.",
133
+ no_postal_address_duplication())
134
+
135
+ reg("R69", "CBPR_Structured_Remittance_Information_TextualRule",
136
+ "Use of Structured Remittance Information must be bilaterally agreed. The "
137
+ "total business data for all occurrences must not exceed 9,000 characters.",
138
+ structured_remittance_max_total(TX + "/RmtInf/Strd", 9000))
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Algorithmic field validation (brief), only for fields present in pain.001.
143
+ # ---------------------------------------------------------------------------
144
+ reg("VAL-CCY", "CBPR_Valid_Instructed_Amount_Currency",
145
+ "Instructed Amount currency must be a valid ISO 4217 code.",
146
+ lambda msg, report: [
147
+ report(el, detail=f"invalid currency '{ccy}'")
148
+ for el, ccy in msg.attr_nodes(TX + "/Amt/InstdAmt", "Ccy")
149
+ if ccy and not is_valid_currency(ccy)
150
+ ])
151
+
152
+ reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
153
+ "Every Financial Institution BICFI must be a structurally valid BIC.",
154
+ lambda msg, report: [
155
+ report(node, detail=f"invalid BIC: '{msg.text_of(node)}'")
156
+ for path in (
157
+ PMTINF + "/DbtrAgt/FinInstnId/BICFI",
158
+ TX + "/CdtrAgt/FinInstnId/BICFI",
159
+ TX + "/IntrmyAgt1/FinInstnId/BICFI",
160
+ TX + "/IntrmyAgt2/FinInstnId/BICFI",
161
+ TX + "/IntrmyAgt3/FinInstnId/BICFI",
162
+ )
163
+ for node in msg.find(path)
164
+ if msg.text_of(node) and not is_valid_bic(msg.text_of(node))
165
+ ])
166
+
167
+ reg("VAL-LEI", "CBPR_Valid_LEI",
168
+ "Every LEI must be a structurally valid ISO 17442 LEI.",
169
+ lambda msg, report: [
170
+ report(node, detail=f"invalid LEI: '{msg.text_of(node)}'")
171
+ for path in (
172
+ PMTINF + "/Dbtr/Id/OrgId/LEI",
173
+ PMTINF + "/DbtrAgt/FinInstnId/LEI",
174
+ TX + "/Cdtr/Id/OrgId/LEI",
175
+ TX + "/CdtrAgt/FinInstnId/LEI",
176
+ )
177
+ for node in msg.find(path)
178
+ if msg.text_of(node) and not is_valid_lei(msg.text_of(node))
179
+ ])
180
+
181
+ reg("VAL-CTRY", "CBPR_Valid_Country",
182
+ "Every Country must be a valid ISO 3166-1 alpha-2 code.",
183
+ lambda msg, report: [
184
+ report(node, detail=f"invalid country '{msg.text_of(node)}'")
185
+ for path in (
186
+ PMTINF + "/Dbtr/PstlAdr/Ctry",
187
+ PMTINF + "/DbtrAgt/FinInstnId/PstlAdr/Ctry",
188
+ TX + "/Cdtr/PstlAdr/Ctry",
189
+ TX + "/CdtrAgt/FinInstnId/PstlAdr/Ctry",
190
+ TX + "/IntrmyAgt1/FinInstnId/PstlAdr/Ctry",
191
+ TX + "/IntrmyAgt2/FinInstnId/PstlAdr/Ctry",
192
+ TX + "/IntrmyAgt3/FinInstnId/PstlAdr/Ctry",
193
+ )
194
+ for node in msg.find(path)
195
+ if msg.text_of(node) and not is_valid_country(msg.text_of(node))
196
+ ])
197
+
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Advisory textual rules (not mechanically enforceable - surfaced as guidance)
201
+ # ---------------------------------------------------------------------------
202
+ _ADVISORY = {
203
+ "R1": ("CBPR_Character_Set_Usage_TextualRule",
204
+ "For further description on the usage of the field, pls refer to the CBPR Plus UHB."),
205
+ "R4": ("CBPR_Business_Service_TextualRule",
206
+ "This field may be used by SWIFT to support differentiated processing on "
207
+ "SWIFT-administered services such as FINplus."),
208
+ "R6": ("CBPR_Market_Practice_TextualRule",
209
+ "This field may be used by SWIFT on SWIFT-administered services. A "
210
+ "user-specific value may be used, but please contact your Service Administrator."),
211
+ "R7": ("CBPR_Related_Business_Application_Header_TextualRule",
212
+ "If used, the Related BAH must transport the exact same information as in the "
213
+ "BAH of the related message."),
214
+ "R9": ("CBPR_Related_BAH_Business_Service_TextualRule",
215
+ "If related BAH is present, it should transport the element Business Service."),
216
+ "R10": ("CGI_Message_Identification_TextualRule",
217
+ "Forwarding Agent should respect the Message ID provided by the Initiating "
218
+ "Party of the pain.001 and pain.002."),
219
+ "R11": ("CGI_ISO_Date_Time_TextualRule",
220
+ "Preferred representation is Local time with UTC offset format "
221
+ "(YYYY-MM-DDThh:mm:ss.sss+/-hh:mm). Otherwise use UTC time format."),
222
+ "R12": ("CBPR_Party_Name_TextualRule",
223
+ "If Postal Address is provided, then Name is mandatory. Name without address "
224
+ "is allowed."),
225
+ "R14": ("CGI_Initiating_Party_Id_TextualRule",
226
+ "Multiple IDs are allowed to support overpopulation. Can be used for "
227
+ "dedicated bank services, e.g. reporting in the pain.002 or online authorisation."),
228
+ "R15": ("CBPR_Forwarding_Agent_TextualRule",
229
+ "If the Debtor Agent is not the receiver of the pain.001, the Initiating "
230
+ "Party must populate the Forward Agent BIC."),
231
+ "R17": ("CBPR_CHK_TextualRule",
232
+ "Required in case of cheque relay payment."),
233
+ "R18": ("CBPR_Payment_Type_Information_TextualRule",
234
+ "Required on Transaction Level, unless bilaterally determined (bank value "
235
+ "added service)."),
236
+ "R19": ("CGI_Instruction_Priority_TextualRule",
237
+ "Based on whether priority processing vs. normal processing is offered by the bank."),
238
+ "R20": ("CBPR_ISO_Code_TextualRule",
239
+ "Unless the country-specifics require a non-ISO code, preference is to provide Code."),
240
+ "R21": ("CGI_ISO_Code_TextualRule",
241
+ "Unless the country-specifics require a non-ISO code, preference is to provide Code."),
242
+ "R22": ("CBPR_Party_Name_TextualRule", "Required"),
243
+ "R26": ("CGI_Account_Currency_TextualRule", "Recommended."),
244
+ "R27": ("CBPR_BIC_TextualRule", "Should be provided as the preferred option."),
245
+ "R28": ("CBPR_Clearing_System_Member_Identification_TextualRule",
246
+ "To be provided as second preference, unless BICFI, or Name and Address are "
247
+ "provided as FI identification."),
248
+ "R29": ("CBPR_Clearing_System_Id_TextualRule",
249
+ "Required if Clearing System Identification is provided."),
250
+ "R30": ("CBPR_Agent_Name_TextualRule",
251
+ "To be provided, unless BICFI, or Member ID are provided as FI identification, "
252
+ "and if Postal Address is used."),
253
+ "R34": ("CBPR_Ultimate_Debtor_TextualRule",
254
+ "Usage based on business need and bank service. If an ultimate debtor is "
255
+ "involved in the payment, being different from the debtor, it must be provided."),
256
+ "R35": ("CBPR_Country_Of_Residence_TextualRule",
257
+ "Country of Residence (where the party physically lives) should be used only "
258
+ "if different from PostalAdress/Country."),
259
+ "R36": ("CBPR_Charge_Bearer_TextualRule",
260
+ "Should be provided only on Transaction Level, unless bilaterally determined "
261
+ "(bank value added service)."),
262
+ "R37": ("CGI_Instruction_Identification_TextualRule",
263
+ "If provided, this Id is returned to the ordering party in account statement reporting."),
264
+ "R38": ("CBPR_Payment_Type_Information_TextualRul",
265
+ "Required on Transaction Level, unless bilaterally determined (bank value "
266
+ "added service)."),
267
+ "R39": ("CBPR_Charge_Bearer_TextualRule",
268
+ "Should be provided only on Transaction Level."),
269
+ "R40": ("CBPR_Cheque_Number_TextualRule",
270
+ "Required only for Customer Cheques."),
271
+ "R41": ("CBPR_Cheque_From_TextualRule",
272
+ "Populated only if info different from Debtor/Ultimate Debtor; assumes "
273
+ "Ultimate Debtor takes precedence over Debtor if populated."),
274
+ "R42": ("CBPR_Delivery_Method_TextualRule",
275
+ "Populated to advise how cheque/draft is to be delivered."),
276
+ "R43": ("CBPR_Deliver_To_TextualRule",
277
+ "Populated only if info different from Creditor/Ultimate Creditor; assumes "
278
+ "Ultimate Creditor takes precedence over Creditor if populated."),
279
+ "R44": ("CBPR_Cheque_Maturity_Date_TextualRule",
280
+ "If the instrument has a maturity date."),
281
+ "R45": ("CGI_Ultimate_Debtor_TextualRule",
282
+ "Usage based on business need and bank service. If an ultimate debtor is "
283
+ "involved in the payment, being different from the debtor, it must be provided."),
284
+ "R49": ("CBPR_BIC_TextualRule",
285
+ "Recommended to be provided as the preferred option."),
286
+ "R62": ("CBPR_CHK_Creditor_Account_TextualRule",
287
+ "Creditor Account is not required for cheque payments."),
288
+ "R63": ("CGI_Ultimate_Creditor_TextualRule",
289
+ "Based on business need and bank service. If an ultimate creditor is involved "
290
+ "in the payment, being different from the creditor, it must be provided."),
291
+ "R66": ("CBPR_Instruction_For_Debtor_Agent_TextualRule",
292
+ "Depending on local payment instrument and bilaterally determined bank service."),
293
+ "R67": ("CBPR_Purpose_Guideline",
294
+ "The preferred option is coded information."),
295
+ "R68": ("CGI_Remittance_Location_Details_Postal_Address_TextualRule",
296
+ "Optional, but not recommended to provide this unstructured address data "
297
+ "(max. 1 occurrence with 70 char). If provided, it must not include Country."),
298
+ "R70": ("CBPR_Issuer_TextualRule",
299
+ "Value of 'ISO' reserved for ISO 11649 international creditor's reference (if "
300
+ "used it must be bilaterally agreed)."),
301
+ "R71": ("CBPR_Reference_TextualRule",
302
+ "If Creditor Reference Information is used (bilateral agreement), Reference "
303
+ "must be included."),
304
+ }
305
+ for _num, (_name, _desc) in _ADVISORY.items():
306
+ advisory(MT, YEAR, _num, _name, _desc)
@@ -0,0 +1 @@
1
+ """CBPR+ SR2026 usage rules."""
@@ -0,0 +1,191 @@
1
+ """CBPR+ SR2026 usage rules for camt.052.001.08 (BankToCustomerAccountReport).
2
+
3
+ Rules and text are taken from the published usage guideline's Rules sheet; XML
4
+ paths are the short ISO 20022 tags translated from its Full_View column. The
5
+ module mirrors the reference module ``y2025/pacs_008`` - combinator-built rules
6
+ go through the local ``reg`` helper; cross-field / bespoke logic is a plain
7
+ ``fn(msg, report)`` registered with ``@rule``.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from ...registry import advisory, rule
12
+ from ...validators import (
13
+ is_valid_bic,
14
+ is_valid_country,
15
+ is_valid_currency,
16
+ is_valid_lei,
17
+ )
18
+ from ...helpers import (
19
+ amount_equals_sum,
20
+ business_msg_id_carries_group_id,
21
+ each_value_valid,
22
+ header_msg_def_id_matches,
23
+ not_matching_pattern,
24
+ requires_if_present,
25
+ structured_remittance_max_total,
26
+ value_not_in,
27
+ )
28
+
29
+ MT = "camt.052"
30
+ YEAR = 2026
31
+ ROOT = "/Document/BkToCstmrAcctRpt"
32
+ RPT = ROOT + "/Rpt"
33
+ TXDTLS = RPT + "/Ntry/NtryDtls/TxDtls"
34
+ INTRST = TXDTLS + "/Intrst"
35
+ RMTINF_STRD = TXDTLS + "/RmtInf/Strd"
36
+
37
+
38
+ def reg(number: str, name: str, description: str, check) -> None:
39
+ """Register a combinator-built check as a rule."""
40
+ rule(MT, YEAR, number, name, description)(check)
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Formal rules
45
+ # ---------------------------------------------------------------------------
46
+
47
+ @rule(MT, YEAR, "R1", "CBPR_Copy_Duplicate_FormalRule",
48
+ "If Copy Duplicate indicator is used in the Business Application Header, "
49
+ "it must be identical to the Copy Duplicate indicator in the business "
50
+ "document (if the latter is present).")
51
+ def _r1(msg, report):
52
+ bah = msg.find("/AppHdr/CpyDplct")
53
+ doc = msg.find(RPT + "/CpyDplctInd")
54
+ if not bah or not doc:
55
+ return
56
+ bah_vals = {msg.text_of(n) for n in bah}
57
+ doc_vals = {msg.text_of(n) for n in doc}
58
+ if bah_vals != doc_vals:
59
+ report(bah[0], detail="BAH CopyDuplicate must equal Report CopyDuplicateIndicator")
60
+
61
+
62
+ @rule(MT, YEAR, "R2", "CBPR_BusinessMessageIdentifier_FormalRule",
63
+ "The Business Message Identifier must match the Message Identification in "
64
+ "the Group Header.")
65
+ def _r2(msg, report):
66
+ bah = msg.find("/AppHdr/BizMsgIdr")
67
+ grp = msg.find(ROOT + "/GrpHdr/MsgId")
68
+ if not bah or not grp:
69
+ return
70
+ if {msg.text_of(n) for n in bah} != {msg.text_of(n) for n in grp}:
71
+ report(bah[0], detail="BusinessMessageIdentifier must equal GroupHeader MessageIdentification")
72
+
73
+
74
+ reg("R7", "CBPR_PageNumber_FormalRule",
75
+ "The page number must be greater than zero (>0).",
76
+ value_not_in(RPT + "/RptPgntn/PgNb", ["0", "00", "000", "0000", "00000"]))
77
+
78
+
79
+ reg("R9", "CBPR_Party_Name_Postal_Address_FormalRule",
80
+ "If Postal Address is present then Name is mandatory.",
81
+ requires_if_present(RPT + "/Acct/Ownr", "PstlAdr", "Nm"))
82
+
83
+
84
+ reg("R13", "CBPR_Original_Instruction_Identification_FormalRule",
85
+ "This element must not start or end with a slash '/' and must not contain "
86
+ "two consecutive slashes '//'.",
87
+ not_matching_pattern(TXDTLS + "/Refs/InstrId", r"(/.*)|(.*/)|(.*//.*)"))
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Algorithmic field validations (brief), for fields present in camt.052
92
+ # ---------------------------------------------------------------------------
93
+ reg("VAL-CCY", "CBPR_Valid_Balance_Currency",
94
+ "Balance Amount currency must be a valid ISO 4217 code.",
95
+ lambda msg, report: [
96
+ report(el, detail=f"invalid currency '{ccy}'")
97
+ for el, ccy in msg.attr_nodes(RPT + "/Bal/Amt", "Ccy")
98
+ if ccy and not is_valid_currency(ccy)
99
+ ])
100
+
101
+ reg("VAL-BIC", "CBPR_Valid_Account_Servicer_BIC",
102
+ "Account Servicer BICFI must be a structurally valid BIC.",
103
+ each_value_valid(RPT + "/Acct/Svcr/FinInstnId/BICFI", is_valid_bic, "BIC"))
104
+
105
+ reg("VAL-LEI", "CBPR_Valid_Account_Servicer_LEI",
106
+ "Account Servicer LEI must be a structurally valid LEI.",
107
+ each_value_valid(RPT + "/Acct/Svcr/FinInstnId/LEI", is_valid_lei, "LEI"))
108
+
109
+ reg("VAL-CTRY", "CBPR_Valid_Account_Owner_Country",
110
+ "Account Owner Postal Address Country must be a valid ISO 3166 code.",
111
+ each_value_valid(RPT + "/Acct/Ownr/PstlAdr/Ctry", is_valid_country, "Country"))
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Mechanizable textual rules promoted from advisory to enforced.
116
+ # Each promoted check is conservative (skips when inputs are absent/ambiguous),
117
+ # so a previously-valid message can never be made to fail spuriously.
118
+ # ---------------------------------------------------------------------------
119
+
120
+ # R4: BizMsgIdr must carry the underlying GroupHeader MessageIdentification.
121
+ reg("R4", "CBPR_Business_Message_Identifier_TextualRule",
122
+ "The Business Message Identifier is the unique identifier of the Business "
123
+ "Message instance that is being transported with this header, as defined by "
124
+ "the sending application or system. Must contain the Message Identification "
125
+ "element from the Group Header of the underlying message, where available "
126
+ "(as is typically the case with pacs, pain, and camt messages, for example). "
127
+ "If Message Identification is not available in the underlying message, then "
128
+ "this element must contain the unique identifier of the Business Message "
129
+ "instance.",
130
+ business_msg_id_carries_group_id())
131
+
132
+ # R5: MsgDefIdr must match the Document message-definition id (namespace suffix).
133
+ reg("R5", "CBPR_Message_Definition_Identifier_TextualRule",
134
+ "The Message Definition Identifier of the Business Message instance that is "
135
+ "being transported with this header. In general, it must be formatted "
136
+ "exactly as it appears in the namespace of the Business Message instance.",
137
+ header_msg_def_id_matches())
138
+
139
+ # R12 / R16: Total Interest And Tax Amount must equal the sum of record amounts.
140
+ reg("R12", "CBPR_Interest_TextualRule",
141
+ "Total Charges And Tax Amount must equal the sum of the individual record "
142
+ "amounts.",
143
+ amount_equals_sum(INTRST + "/TtlIntrstAndTaxAmt", INTRST + "/Rcrd/Amt"))
144
+
145
+ reg("R16", "CBPR_Interest_TextualRule",
146
+ "Total Charges And Tax Amount must equal the sum of the individual recode "
147
+ "amounts",
148
+ amount_equals_sum(INTRST + "/TtlIntrstAndTaxAmt", INTRST + "/Rcrd/Amt"))
149
+
150
+ # R17: Structured Remittance, when repeated, must not exceed 9,000 characters
151
+ # in total (excluding tags). The bilateral-agreement clause remains guidance.
152
+ reg("R17", "CBPR_Remittance_Rules_TextualRule",
153
+ "1. Use of Structured Remittance must be bilaterally or multilaterally "
154
+ "agreed 2. Structured Remittance can be repeated, however the total "
155
+ "business data for all occurrences (excluding tags) must not exceed 9,000 "
156
+ "characters.",
157
+ structured_remittance_max_total(RMTINF_STRD, 9000))
158
+
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # Advisory textual rules (not mechanically enforceable - surfaced as guidance)
162
+ # ---------------------------------------------------------------------------
163
+ _ADVISORY = {
164
+ "R3": ("CBPR_Related_Business_Application_Header_TextualRule",
165
+ "If used, the Related BAH must transport the exact same information "
166
+ "as in the BAH of the related message."),
167
+ "R6": ("CBPR_Related_BAH_Business_Service_TextualRule",
168
+ "If related BAH is present, it should transport the element "
169
+ "Business Service."),
170
+ "R8": ("CBPR_Copy_Duplicate_Indicator_TextualRule",
171
+ "If Applicable, for Copy or Duplicate, the electronic sequence and "
172
+ "legal sequence must be the same as the original report."),
173
+ "R10": ("CBPR_Intraday_Balance_Recommendation_TextualRule",
174
+ "As increased local regulations are requesting that intraday "
175
+ "balance positioning be provided by the account servicing "
176
+ "institutions and not be calculated by the account owner, every "
177
+ "camt.052 message which include Entry <Ntry> items, Intraday "
178
+ "Booked (ITBD ) and IntraDay Available (ITAV) balances should be "
179
+ "sent."),
180
+ "R11": ("CBPR_Charges_TextualRule",
181
+ "Total Charges And Tax Amount must equal the sum of the individual "
182
+ "record amounts."),
183
+ "R14": ("CBPR_UETR_TextualRule",
184
+ "If the underlying transaction contains/owns a UETR then it should "
185
+ "be reported in the camt.052 message."),
186
+ "R15": ("CBPR_Charges_TextualRule",
187
+ "Total Charges And Tax Amount must equal the sum of the individual "
188
+ "recode amounts"),
189
+ }
190
+ for _num, (_name, _desc) in _ADVISORY.items():
191
+ advisory(MT, YEAR, _num, _name, _desc)