cbpr-usage-rules 0.1.0__tar.gz

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 (55) hide show
  1. cbpr_usage_rules-0.1.0/CHANGELOG.md +28 -0
  2. cbpr_usage_rules-0.1.0/LICENSE +21 -0
  3. cbpr_usage_rules-0.1.0/MANIFEST.in +4 -0
  4. cbpr_usage_rules-0.1.0/PKG-INFO +335 -0
  5. cbpr_usage_rules-0.1.0/README.md +280 -0
  6. cbpr_usage_rules-0.1.0/pyproject.toml +56 -0
  7. cbpr_usage_rules-0.1.0/setup.cfg +4 -0
  8. cbpr_usage_rules-0.1.0/src/cbpr_rules/__init__.py +21 -0
  9. cbpr_usage_rules-0.1.0/src/cbpr_rules/cli.py +176 -0
  10. cbpr_usage_rules-0.1.0/src/cbpr_rules/engine.py +100 -0
  11. cbpr_usage_rules-0.1.0/src/cbpr_rules/helpers.py +420 -0
  12. cbpr_usage_rules-0.1.0/src/cbpr_rules/loader.py +77 -0
  13. cbpr_usage_rules-0.1.0/src/cbpr_rules/message.py +170 -0
  14. cbpr_usage_rules-0.1.0/src/cbpr_rules/models.py +83 -0
  15. cbpr_usage_rules-0.1.0/src/cbpr_rules/py.typed +0 -0
  16. cbpr_usage_rules-0.1.0/src/cbpr_rules/reference/__init__.py +9 -0
  17. cbpr_usage_rules-0.1.0/src/cbpr_rules/reference/countries.py +28 -0
  18. cbpr_usage_rules-0.1.0/src/cbpr_rules/reference/currencies.py +25 -0
  19. cbpr_usage_rules-0.1.0/src/cbpr_rules/registry.py +107 -0
  20. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/__init__.py +1 -0
  21. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2025/__init__.py +1 -0
  22. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2025/camt_052.py +224 -0
  23. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2025/camt_054.py +176 -0
  24. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2025/pacs_002.py +212 -0
  25. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2025/pacs_004.py +831 -0
  26. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2025/pacs_008.py +375 -0
  27. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2025/pacs_008_stp.py +367 -0
  28. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2025/pacs_009.py +273 -0
  29. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2025/pacs_009_adv.py +255 -0
  30. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2025/pacs_009_cov.py +358 -0
  31. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2025/pain_001.py +306 -0
  32. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2026/__init__.py +1 -0
  33. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2026/camt_052.py +191 -0
  34. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2026/camt_054.py +182 -0
  35. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2026/pacs_002.py +208 -0
  36. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2026/pacs_004.py +491 -0
  37. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2026/pacs_008.py +377 -0
  38. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2026/pacs_008_stp.py +369 -0
  39. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2026/pacs_009.py +260 -0
  40. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2026/pacs_009_adv.py +256 -0
  41. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2026/pacs_009_cov.py +324 -0
  42. cbpr_usage_rules-0.1.0/src/cbpr_rules/rules/y2026/pain_001.py +272 -0
  43. cbpr_usage_rules-0.1.0/src/cbpr_rules/schema.py +97 -0
  44. cbpr_usage_rules-0.1.0/src/cbpr_rules/validators/__init__.py +16 -0
  45. cbpr_usage_rules-0.1.0/src/cbpr_rules/validators/bic.py +21 -0
  46. cbpr_usage_rules-0.1.0/src/cbpr_rules/validators/country.py +11 -0
  47. cbpr_usage_rules-0.1.0/src/cbpr_rules/validators/currency.py +11 -0
  48. cbpr_usage_rules-0.1.0/src/cbpr_rules/validators/iban.py +26 -0
  49. cbpr_usage_rules-0.1.0/src/cbpr_rules/validators/lei.py +17 -0
  50. cbpr_usage_rules-0.1.0/src/cbpr_usage_rules.egg-info/PKG-INFO +335 -0
  51. cbpr_usage_rules-0.1.0/src/cbpr_usage_rules.egg-info/SOURCES.txt +53 -0
  52. cbpr_usage_rules-0.1.0/src/cbpr_usage_rules.egg-info/dependency_links.txt +1 -0
  53. cbpr_usage_rules-0.1.0/src/cbpr_usage_rules.egg-info/entry_points.txt +2 -0
  54. cbpr_usage_rules-0.1.0/src/cbpr_usage_rules.egg-info/requires.txt +7 -0
  55. cbpr_usage_rules-0.1.0/src/cbpr_usage_rules.egg-info/top_level.txt +1 -0
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
5
+ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+ - Initial package: validation engine, wrapper-tolerant loader, public API
11
+ (`validate_file`, `validate_string`, `list_rules`, `available`), and the
12
+ `cbpr-validate` CLI (text + JSON output).
13
+ - Algorithmic validators: IBAN, LEI, BIC, ISO 3166-1 country, ISO 4217 currency.
14
+ - CBPR+ SR2025 / SR2026 usage rules per message type.
15
+ - Violations report `detail` (why this instance failed) and `found` (the
16
+ offending XML); the CLI wraps output and summarises advisory rules unless
17
+ `--advisory` is passed.
18
+ - Promoted mechanizable textual rules from advisory to enforced: header↔message
19
+ consistency (`MsgDefIdr`, `BizMsgIdr`), Postal Address / AddressLine
20
+ duplication, `AnyBIC` presence exclusivity, Structured Remittance length, and
21
+ conditional charge / amount-sum checks. Contextual rules (referring to a
22
+ related/underlying message, network, jurisdiction, or recommendation) remain
23
+ advisory.
24
+ - Optional XSD schema validation: pass `xsd=` (a path or list) to
25
+ `validate_file`/`validate_string`, or `--xsd PATH` (repeatable) on the CLI. XSDs
26
+ are auto-matched to the Document or AppHdr by `targetNamespace` and reported in a
27
+ separate `xsd` result block / CLI section. XSD files are not bundled. The CLI
28
+ exit code is non-zero on usage-rule OR schema failure.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Peter Houghton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ recursive-include src/cbpr_rules py.typed
@@ -0,0 +1,335 @@
1
+ Metadata-Version: 2.4
2
+ Name: cbpr-usage-rules
3
+ Version: 0.1.0
4
+ Summary: Validate ISO 20022 CBPR+ XML messages against SWIFT usage (business) rules.
5
+ Author: Pete Houghton
6
+ License: MIT License
7
+
8
+ Copyright (c) 2021 Peter Houghton
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/phoughton/iso20022-cbpr-ur
29
+ Project-URL: Repository, https://github.com/phoughton/iso20022-cbpr-ur
30
+ Project-URL: Issues, https://github.com/phoughton/iso20022-cbpr-ur/issues
31
+ Keywords: iso20022,cbpr,swift,payments,validation,pacs,pain,camt
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Financial and Insurance Industry
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.9
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3.13
43
+ Classifier: Topic :: Office/Business :: Financial
44
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
45
+ Requires-Python: >=3.9
46
+ Description-Content-Type: text/markdown
47
+ License-File: LICENSE
48
+ Requires-Dist: lxml>=4.9
49
+ Provides-Extra: dev
50
+ Requires-Dist: pytest>=7; extra == "dev"
51
+ Requires-Dist: openpyxl>=3.1; extra == "dev"
52
+ Requires-Dist: build; extra == "dev"
53
+ Requires-Dist: twine; extra == "dev"
54
+ Dynamic: license-file
55
+
56
+ # cbpr-usage-rules
57
+
58
+ Validate **ISO 20022 CBPR+** XML messages (the SWIFT cross-border payments
59
+ profile) against the **Usage / Business Rules** that apply on top of the XSD
60
+ schemas. These are the rules that the schema alone cannot express: cross-field
61
+ constraints, cross-schema checks between the Business Application Header (BAH)
62
+ and the message Document, conditional presence, code-list restrictions, and
63
+ field-format algorithms (IBAN, LEI, BIC, country, currency).
64
+
65
+ Rules are versioned per year (currently **2025** and **2026**) and organised by
66
+ message type (pacs.008, pacs.009 and its COV/ADV variants, pacs.002, pacs.004,
67
+ pain.001, camt.052, camt.054, and the STP variants).
68
+
69
+ > **This package is AI generated.** The rule logic was derived, with review,
70
+ > from the published CBPR+ usage-guideline spreadsheets. Treat it as an aid, not
71
+ > as a substitute for the official SWIFT specifications. Verify against the
72
+ > source material before relying on results in production.
73
+
74
+ ---
75
+
76
+ ## Installation
77
+
78
+ ```bash
79
+ pip install cbpr-usage-rules
80
+ ```
81
+
82
+ Requires Python 3.9+ and depends only on `lxml`.
83
+
84
+ ---
85
+
86
+ ## Quick start
87
+
88
+ ### As a library
89
+
90
+ ```python
91
+ import cbpr_rules
92
+
93
+ result = cbpr_rules.validate_file("payment.xml", year=2025)
94
+
95
+ if result["valid"]:
96
+ print("Compliant")
97
+ else:
98
+ for v in result["violations"]:
99
+ print(f"{v['rule_number']} line {v['line']} {v['xpath']}")
100
+ print(f" {v['description']}")
101
+ ```
102
+
103
+ `validate_string` is the same but takes XML text:
104
+
105
+ ```python
106
+ result = cbpr_rules.validate_string(xml_text, year=2026)
107
+ ```
108
+
109
+ ### From the command line
110
+
111
+ ```bash
112
+ # Human-readable (default). Exit code is non-zero if there are violations.
113
+ cbpr-validate payment.xml --year 2025
114
+
115
+ # JSON output
116
+ cbpr-validate payment.xml --year 2025 --json
117
+
118
+ # Read from stdin
119
+ cat payment.xml | cbpr-validate --year 2026
120
+
121
+ # Also list the advisory (non-enforced) rules in full
122
+ cbpr-validate payment.xml --year 2025 --advisory
123
+
124
+ # Additionally schema-validate against an XSD (repeatable, results shown separately)
125
+ cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
126
+ ```
127
+
128
+ Each reported violation shows the rule, **why** this instance failed (`Problem:`),
129
+ the **offending XML** from your file (`Found:`), and its line + xpath (`At:`).
130
+ Advisory rules are summarised as a count by default; pass `--advisory` to list them.
131
+
132
+ ---
133
+
134
+ ## The result object
135
+
136
+ Both `validate_file` and `validate_string` return a dictionary:
137
+
138
+ ```python
139
+ {
140
+ "valid": False, # True only if there are no VIOLATION-severity findings
141
+ "message_type": "pacs.008", # the rule set that was applied
142
+ "detected_message_type": "pacs.008",# auto-detected from the Document namespace
143
+ "year": 2025,
144
+ "rules_evaluated": 84,
145
+ "violations": [
146
+ {
147
+ "rule_number": "pacs.008:R41", # unique within the message type
148
+ "name": "CBPR_Interbank_Settlement_Currency_FormalRule",
149
+ "description": "The codes XAU, XAG, XPD and XPT are not allowed ...",
150
+ "detail": "commodity currency 'XAU' not allowed", # why this instance was flagged
151
+ "found": "<IntrBkSttlmAmt Ccy=\"XAU\">1000.00</IntrBkSttlmAmt>", # the offending XML
152
+ "xpath": "/RequestPayload/Document/FIToFICstmrCdtTrf/CdtTrfTxInf/IntrBkSttlmAmt",
153
+ "line": 39, # 1-based line in the source XML
154
+ "severity": "violation"
155
+ }
156
+ ],
157
+ "advisory": [ # textual guidance that cannot be mechanically enforced
158
+ {"rule_number": "pacs.008:R4", "name": "...", "description": "..."}
159
+ ]
160
+ }
161
+ ```
162
+
163
+ Every violation carries the four required pieces of information: the **xpath**,
164
+ the **line number**, a **description**, and a **unique rule number**.
165
+
166
+ ---
167
+
168
+ ## Mid-level usage
169
+
170
+ ### Choosing the year
171
+
172
+ Pass `year=2025` or `year=2026`. Rule loading is per-year and lazy — only the
173
+ requested year's modules are imported. You can validate against more than one
174
+ year in the same process simply by calling with different `year` values.
175
+
176
+ ### Message type detection and variants
177
+
178
+ The message type is auto-detected from the `<Document>` namespace, so you
179
+ normally don't pass it. Business variants that share a base namespace — **STP**
180
+ (pacs.008/pacs.009), **COV** and **ADV** (pacs.009) — cannot be told apart from
181
+ the XML alone, so select them explicitly when you want their stricter rule sets:
182
+
183
+ ```python
184
+ cbpr_rules.validate_file("cover.xml", year=2025, msgtype="pacs.009_cov")
185
+ ```
186
+
187
+ ```bash
188
+ cbpr-validate cover.xml --year 2025 --type pacs.009_cov
189
+ ```
190
+
191
+ ### Wrapper tags
192
+
193
+ Messages arrive inside different envelopes (`<RequestPayload>`, `<DataPDU>`,
194
+ SWIFT SAA wrappers, and so on). The validator locates the `AppHdr` and
195
+ `Document` elements wherever they sit in the tree and ignores the surrounding
196
+ wrapper, so the same input validates identically regardless of envelope. Element
197
+ matching is by local name, so namespace prefixes never matter.
198
+
199
+ ### Severity
200
+
201
+ - `violation` — a usage rule is broken; this makes `valid` false.
202
+ - `info` — advisory guidance surfaced for awareness; it never fails validation
203
+ and appears in the separate `advisory` list (textual rules that cannot be
204
+ mechanically checked).
205
+
206
+ ---
207
+
208
+ ## In-depth usage
209
+
210
+ ### Discovering the rule set
211
+
212
+ ```python
213
+ cbpr_rules.available(2025) # -> ['camt.052', 'pacs.008', ...]
214
+ cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
215
+ ```
216
+
217
+ ```bash
218
+ cbpr-validate --year 2025 --list-types
219
+ cbpr-validate --year 2025 --type pacs.008 --list
220
+ ```
221
+
222
+ ### Algorithmic field validation
223
+
224
+ Beyond the published rules, the following formats are validated with their
225
+ standard algorithms wherever the corresponding fields appear:
226
+
227
+ | Field | Standard | Check |
228
+ |----------|---------------------|-----------------------------------------|
229
+ | IBAN | ISO 13616 | structure + mod-97 check digits |
230
+ | LEI | ISO 17442 | structure + ISO 7064 mod-97-10 check |
231
+ | BIC | ISO 9362 | 8/11-char structure + valid country |
232
+ | Country | ISO 3166-1 alpha-2 | membership |
233
+ | Currency | ISO 4217 | membership |
234
+
235
+ The reference code lists are vendored, so validation never makes a network call.
236
+
237
+ Some of these check-digit algorithms and reference code lists were derived from
238
+ Wikipedia (e.g. [IBAN](https://en.wikipedia.org/wiki/International_Bank_Account_Number),
239
+ [LEI](https://en.wikipedia.org/wiki/Legal_Entity_Identifier), and the
240
+ [ISO 3166 country codes](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes)).
241
+ Verify against the authoritative ISO/registry sources before relying on them in
242
+ production.
243
+
244
+ ### Optional XSD schema validation
245
+
246
+ You can additionally validate a message against one or more **XSD schemas** as a
247
+ *separate* second result set. XSD files are **not** bundled with the package —
248
+ supply your own path(s):
249
+
250
+ ```python
251
+ result = cbpr_rules.validate_file("payment.xml", year=2025, xsd="pacs.008.001.08.xsd")
252
+ # or several: xsd=["head.001.001.02.xsd", "pacs.008.001.08.xsd"]
253
+ ```
254
+
255
+ ```bash
256
+ cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
257
+ ```
258
+
259
+ Each XSD is auto-matched by its `targetNamespace`: a message schema validates the
260
+ `Document`, a head.001 schema validates the `AppHdr`. When (and only when) an XSD
261
+ is supplied, the result gains a separate top-level `xsd` block, and the CLI prints
262
+ a distinct **XSD SCHEMA VALIDATION** section:
263
+
264
+ ```python
265
+ "xsd": {
266
+ "checked": True,
267
+ "schema_valid": False, # all supplied schemas passed?
268
+ "schemas": [
269
+ {
270
+ "file": "pacs.008.001.08.xsd",
271
+ "target_namespace": "urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08",
272
+ "validated_element": "Document", # or "AppHdr" / "root"
273
+ "valid": False,
274
+ "errors": [{"message": "...", "line": 82, "xpath": "/Document/.../CdtrAgt"}]
275
+ }
276
+ ]
277
+ }
278
+ ```
279
+
280
+ Usage-rule results and schema results stay separate: the top-level `valid` reflects
281
+ only the usage rules, while schema validity is `xsd.schema_valid`. The **CLI exit
282
+ code is non-zero if either** the usage rules or the schema fail. With no `--xsd`,
283
+ nothing about XSD appears in the output.
284
+
285
+ ### Handling errors
286
+
287
+ `validate_file` / `validate_string` raise `cbpr_rules.engine.ValidationError`
288
+ for unparseable XML or when the message type cannot be determined and was not
289
+ supplied. The CLI turns these into a message on stderr and exit code 2.
290
+
291
+ ### CLI exit codes
292
+
293
+ | Code | Meaning |
294
+ |------|----------------------------------|
295
+ | 0 | Valid (no violations) |
296
+ | 1 | Invalid (one or more violations) |
297
+ | 2 | Usage error / could not validate |
298
+
299
+ ---
300
+
301
+ ## How the rules are organised
302
+
303
+ Each `(year, message type)` has a hand-authored Python module under
304
+ `cbpr_rules/rules/y<year>/`. Rules are built from a small library of reusable
305
+ combinators (presence, conditional presence, mutual exclusion, value matching,
306
+ length, code lists, address grace-period rules) plus bespoke functions for
307
+ cross-field and cross-schema logic. The XSD files in the source material are
308
+ informational only and are not reimplemented here.
309
+
310
+ ### Enforced vs advisory
311
+
312
+ Every published rule is registered. A rule is **enforced** (can produce a
313
+ violation) when it can be checked deterministically from the message alone —
314
+ this includes the formal pseudo-code rules and the mechanizable textual rules,
315
+ such as:
316
+
317
+ - header ↔ message consistency (`MsgDefIdr` matches the Document definition;
318
+ `BizMsgIdr` carries the GroupHeader `MsgId`);
319
+ - no structured Postal Address value duplicated in an `AddressLine`;
320
+ - `AnyBIC` present ⇒ `Name`/`PostalAddress` not allowed;
321
+ - Structured Remittance ≤ 9,000 characters;
322
+ - charge rules where instructed and settlement amounts share a currency and
323
+ differ, and amount-total-equals-sum checks.
324
+
325
+ A rule stays **advisory** (`severity: info`, surfaced as guidance, never failing
326
+ validation) when it cannot be verified from the message in isolation — for
327
+ example anything that refers to a *related or underlying message* not present
328
+ (`Original_*`, related-BAH, COV/ADV UETR/E2E), depends on the SWIFT network or a
329
+ jurisdiction, or expresses a recommendation / bilaterally-agreed practice.
330
+ Checks are deliberately **conservative**: each skips when its inputs are absent
331
+ or ambiguous, so a compliant message is never failed spuriously.
332
+
333
+ ## Licence
334
+
335
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,280 @@
1
+ # cbpr-usage-rules
2
+
3
+ Validate **ISO 20022 CBPR+** XML messages (the SWIFT cross-border payments
4
+ profile) against the **Usage / Business Rules** that apply on top of the XSD
5
+ schemas. These are the rules that the schema alone cannot express: cross-field
6
+ constraints, cross-schema checks between the Business Application Header (BAH)
7
+ and the message Document, conditional presence, code-list restrictions, and
8
+ field-format algorithms (IBAN, LEI, BIC, country, currency).
9
+
10
+ Rules are versioned per year (currently **2025** and **2026**) and organised by
11
+ message type (pacs.008, pacs.009 and its COV/ADV variants, pacs.002, pacs.004,
12
+ pain.001, camt.052, camt.054, and the STP variants).
13
+
14
+ > **This package is AI generated.** The rule logic was derived, with review,
15
+ > from the published CBPR+ usage-guideline spreadsheets. Treat it as an aid, not
16
+ > as a substitute for the official SWIFT specifications. Verify against the
17
+ > source material before relying on results in production.
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install cbpr-usage-rules
25
+ ```
26
+
27
+ Requires Python 3.9+ and depends only on `lxml`.
28
+
29
+ ---
30
+
31
+ ## Quick start
32
+
33
+ ### As a library
34
+
35
+ ```python
36
+ import cbpr_rules
37
+
38
+ result = cbpr_rules.validate_file("payment.xml", year=2025)
39
+
40
+ if result["valid"]:
41
+ print("Compliant")
42
+ else:
43
+ for v in result["violations"]:
44
+ print(f"{v['rule_number']} line {v['line']} {v['xpath']}")
45
+ print(f" {v['description']}")
46
+ ```
47
+
48
+ `validate_string` is the same but takes XML text:
49
+
50
+ ```python
51
+ result = cbpr_rules.validate_string(xml_text, year=2026)
52
+ ```
53
+
54
+ ### From the command line
55
+
56
+ ```bash
57
+ # Human-readable (default). Exit code is non-zero if there are violations.
58
+ cbpr-validate payment.xml --year 2025
59
+
60
+ # JSON output
61
+ cbpr-validate payment.xml --year 2025 --json
62
+
63
+ # Read from stdin
64
+ cat payment.xml | cbpr-validate --year 2026
65
+
66
+ # Also list the advisory (non-enforced) rules in full
67
+ cbpr-validate payment.xml --year 2025 --advisory
68
+
69
+ # Additionally schema-validate against an XSD (repeatable, results shown separately)
70
+ cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
71
+ ```
72
+
73
+ Each reported violation shows the rule, **why** this instance failed (`Problem:`),
74
+ the **offending XML** from your file (`Found:`), and its line + xpath (`At:`).
75
+ Advisory rules are summarised as a count by default; pass `--advisory` to list them.
76
+
77
+ ---
78
+
79
+ ## The result object
80
+
81
+ Both `validate_file` and `validate_string` return a dictionary:
82
+
83
+ ```python
84
+ {
85
+ "valid": False, # True only if there are no VIOLATION-severity findings
86
+ "message_type": "pacs.008", # the rule set that was applied
87
+ "detected_message_type": "pacs.008",# auto-detected from the Document namespace
88
+ "year": 2025,
89
+ "rules_evaluated": 84,
90
+ "violations": [
91
+ {
92
+ "rule_number": "pacs.008:R41", # unique within the message type
93
+ "name": "CBPR_Interbank_Settlement_Currency_FormalRule",
94
+ "description": "The codes XAU, XAG, XPD and XPT are not allowed ...",
95
+ "detail": "commodity currency 'XAU' not allowed", # why this instance was flagged
96
+ "found": "<IntrBkSttlmAmt Ccy=\"XAU\">1000.00</IntrBkSttlmAmt>", # the offending XML
97
+ "xpath": "/RequestPayload/Document/FIToFICstmrCdtTrf/CdtTrfTxInf/IntrBkSttlmAmt",
98
+ "line": 39, # 1-based line in the source XML
99
+ "severity": "violation"
100
+ }
101
+ ],
102
+ "advisory": [ # textual guidance that cannot be mechanically enforced
103
+ {"rule_number": "pacs.008:R4", "name": "...", "description": "..."}
104
+ ]
105
+ }
106
+ ```
107
+
108
+ Every violation carries the four required pieces of information: the **xpath**,
109
+ the **line number**, a **description**, and a **unique rule number**.
110
+
111
+ ---
112
+
113
+ ## Mid-level usage
114
+
115
+ ### Choosing the year
116
+
117
+ Pass `year=2025` or `year=2026`. Rule loading is per-year and lazy — only the
118
+ requested year's modules are imported. You can validate against more than one
119
+ year in the same process simply by calling with different `year` values.
120
+
121
+ ### Message type detection and variants
122
+
123
+ The message type is auto-detected from the `<Document>` namespace, so you
124
+ normally don't pass it. Business variants that share a base namespace — **STP**
125
+ (pacs.008/pacs.009), **COV** and **ADV** (pacs.009) — cannot be told apart from
126
+ the XML alone, so select them explicitly when you want their stricter rule sets:
127
+
128
+ ```python
129
+ cbpr_rules.validate_file("cover.xml", year=2025, msgtype="pacs.009_cov")
130
+ ```
131
+
132
+ ```bash
133
+ cbpr-validate cover.xml --year 2025 --type pacs.009_cov
134
+ ```
135
+
136
+ ### Wrapper tags
137
+
138
+ Messages arrive inside different envelopes (`<RequestPayload>`, `<DataPDU>`,
139
+ SWIFT SAA wrappers, and so on). The validator locates the `AppHdr` and
140
+ `Document` elements wherever they sit in the tree and ignores the surrounding
141
+ wrapper, so the same input validates identically regardless of envelope. Element
142
+ matching is by local name, so namespace prefixes never matter.
143
+
144
+ ### Severity
145
+
146
+ - `violation` — a usage rule is broken; this makes `valid` false.
147
+ - `info` — advisory guidance surfaced for awareness; it never fails validation
148
+ and appears in the separate `advisory` list (textual rules that cannot be
149
+ mechanically checked).
150
+
151
+ ---
152
+
153
+ ## In-depth usage
154
+
155
+ ### Discovering the rule set
156
+
157
+ ```python
158
+ cbpr_rules.available(2025) # -> ['camt.052', 'pacs.008', ...]
159
+ cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
160
+ ```
161
+
162
+ ```bash
163
+ cbpr-validate --year 2025 --list-types
164
+ cbpr-validate --year 2025 --type pacs.008 --list
165
+ ```
166
+
167
+ ### Algorithmic field validation
168
+
169
+ Beyond the published rules, the following formats are validated with their
170
+ standard algorithms wherever the corresponding fields appear:
171
+
172
+ | Field | Standard | Check |
173
+ |----------|---------------------|-----------------------------------------|
174
+ | IBAN | ISO 13616 | structure + mod-97 check digits |
175
+ | LEI | ISO 17442 | structure + ISO 7064 mod-97-10 check |
176
+ | BIC | ISO 9362 | 8/11-char structure + valid country |
177
+ | Country | ISO 3166-1 alpha-2 | membership |
178
+ | Currency | ISO 4217 | membership |
179
+
180
+ The reference code lists are vendored, so validation never makes a network call.
181
+
182
+ Some of these check-digit algorithms and reference code lists were derived from
183
+ Wikipedia (e.g. [IBAN](https://en.wikipedia.org/wiki/International_Bank_Account_Number),
184
+ [LEI](https://en.wikipedia.org/wiki/Legal_Entity_Identifier), and the
185
+ [ISO 3166 country codes](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes)).
186
+ Verify against the authoritative ISO/registry sources before relying on them in
187
+ production.
188
+
189
+ ### Optional XSD schema validation
190
+
191
+ You can additionally validate a message against one or more **XSD schemas** as a
192
+ *separate* second result set. XSD files are **not** bundled with the package —
193
+ supply your own path(s):
194
+
195
+ ```python
196
+ result = cbpr_rules.validate_file("payment.xml", year=2025, xsd="pacs.008.001.08.xsd")
197
+ # or several: xsd=["head.001.001.02.xsd", "pacs.008.001.08.xsd"]
198
+ ```
199
+
200
+ ```bash
201
+ cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
202
+ ```
203
+
204
+ Each XSD is auto-matched by its `targetNamespace`: a message schema validates the
205
+ `Document`, a head.001 schema validates the `AppHdr`. When (and only when) an XSD
206
+ is supplied, the result gains a separate top-level `xsd` block, and the CLI prints
207
+ a distinct **XSD SCHEMA VALIDATION** section:
208
+
209
+ ```python
210
+ "xsd": {
211
+ "checked": True,
212
+ "schema_valid": False, # all supplied schemas passed?
213
+ "schemas": [
214
+ {
215
+ "file": "pacs.008.001.08.xsd",
216
+ "target_namespace": "urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08",
217
+ "validated_element": "Document", # or "AppHdr" / "root"
218
+ "valid": False,
219
+ "errors": [{"message": "...", "line": 82, "xpath": "/Document/.../CdtrAgt"}]
220
+ }
221
+ ]
222
+ }
223
+ ```
224
+
225
+ Usage-rule results and schema results stay separate: the top-level `valid` reflects
226
+ only the usage rules, while schema validity is `xsd.schema_valid`. The **CLI exit
227
+ code is non-zero if either** the usage rules or the schema fail. With no `--xsd`,
228
+ nothing about XSD appears in the output.
229
+
230
+ ### Handling errors
231
+
232
+ `validate_file` / `validate_string` raise `cbpr_rules.engine.ValidationError`
233
+ for unparseable XML or when the message type cannot be determined and was not
234
+ supplied. The CLI turns these into a message on stderr and exit code 2.
235
+
236
+ ### CLI exit codes
237
+
238
+ | Code | Meaning |
239
+ |------|----------------------------------|
240
+ | 0 | Valid (no violations) |
241
+ | 1 | Invalid (one or more violations) |
242
+ | 2 | Usage error / could not validate |
243
+
244
+ ---
245
+
246
+ ## How the rules are organised
247
+
248
+ Each `(year, message type)` has a hand-authored Python module under
249
+ `cbpr_rules/rules/y<year>/`. Rules are built from a small library of reusable
250
+ combinators (presence, conditional presence, mutual exclusion, value matching,
251
+ length, code lists, address grace-period rules) plus bespoke functions for
252
+ cross-field and cross-schema logic. The XSD files in the source material are
253
+ informational only and are not reimplemented here.
254
+
255
+ ### Enforced vs advisory
256
+
257
+ Every published rule is registered. A rule is **enforced** (can produce a
258
+ violation) when it can be checked deterministically from the message alone —
259
+ this includes the formal pseudo-code rules and the mechanizable textual rules,
260
+ such as:
261
+
262
+ - header ↔ message consistency (`MsgDefIdr` matches the Document definition;
263
+ `BizMsgIdr` carries the GroupHeader `MsgId`);
264
+ - no structured Postal Address value duplicated in an `AddressLine`;
265
+ - `AnyBIC` present ⇒ `Name`/`PostalAddress` not allowed;
266
+ - Structured Remittance ≤ 9,000 characters;
267
+ - charge rules where instructed and settlement amounts share a currency and
268
+ differ, and amount-total-equals-sum checks.
269
+
270
+ A rule stays **advisory** (`severity: info`, surfaced as guidance, never failing
271
+ validation) when it cannot be verified from the message in isolation — for
272
+ example anything that refers to a *related or underlying message* not present
273
+ (`Original_*`, related-BAH, COV/ADV UETR/E2E), depends on the SWIFT network or a
274
+ jurisdiction, or expresses a recommendation / bilaterally-agreed practice.
275
+ Checks are deliberately **conservative**: each skips when its inputs are absent
276
+ or ambiguous, so a compliant message is never failed spuriously.
277
+
278
+ ## Licence
279
+
280
+ MIT — see [LICENSE](LICENSE).