cbpr-usage-rules 0.1.2__tar.gz → 0.2.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.2 → cbpr_usage_rules-0.2.0}/CHANGELOG.md +19 -0
  2. {cbpr_usage_rules-0.1.2/src/cbpr_usage_rules.egg-info → cbpr_usage_rules-0.2.0}/PKG-INFO +21 -4
  3. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/README.md +20 -3
  4. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/__init__.py +1 -1
  5. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/cli.py +54 -2
  6. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/registry.py +55 -6
  7. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2025/camt_052.py +1 -29
  8. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2025/camt_054.py +1 -13
  9. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2025/pacs_002.py +1 -5
  10. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2025/pacs_004.py +1 -27
  11. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2025/pacs_008.py +1 -9
  12. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2025/pacs_008_stp.py +1 -13
  13. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2025/pacs_009.py +1 -13
  14. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2025/pacs_009_adv.py +1 -9
  15. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2025/pacs_009_cov.py +1 -9
  16. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2025/pain_001.py +1 -26
  17. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2026/camt_052.py +0 -14
  18. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2026/camt_054.py +1 -14
  19. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2026/pacs_002.py +1 -10
  20. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2026/pacs_004.py +1 -27
  21. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2026/pacs_008.py +1 -13
  22. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2026/pacs_008_stp.py +1 -13
  23. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2026/pacs_009.py +1 -10
  24. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2026/pacs_009_adv.py +1 -10
  25. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2026/pacs_009_cov.py +1 -9
  26. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2026/pain_001.py +1 -19
  27. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0/src/cbpr_usage_rules.egg-info}/PKG-INFO +21 -4
  28. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/LICENSE +0 -0
  29. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/MANIFEST.in +0 -0
  30. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/pyproject.toml +0 -0
  31. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/setup.cfg +0 -0
  32. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/engine.py +0 -0
  33. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/helpers.py +0 -0
  34. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/loader.py +0 -0
  35. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/message.py +0 -0
  36. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/models.py +0 -0
  37. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/py.typed +0 -0
  38. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/reference/__init__.py +0 -0
  39. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/reference/countries.py +0 -0
  40. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/reference/currencies.py +0 -0
  41. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/__init__.py +0 -0
  42. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2025/__init__.py +0 -0
  43. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/rules/y2026/__init__.py +0 -0
  44. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/schema.py +0 -0
  45. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/validators/__init__.py +0 -0
  46. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/validators/bic.py +0 -0
  47. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/validators/country.py +0 -0
  48. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/validators/currency.py +0 -0
  49. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/validators/iban.py +0 -0
  50. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_rules/validators/lei.py +0 -0
  51. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_usage_rules.egg-info/SOURCES.txt +0 -0
  52. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_usage_rules.egg-info/dependency_links.txt +0 -0
  53. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_usage_rules.egg-info/entry_points.txt +0 -0
  54. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_usage_rules.egg-info/requires.txt +0 -0
  55. {cbpr_usage_rules-0.1.2 → cbpr_usage_rules-0.2.0}/src/cbpr_usage_rules.egg-info/top_level.txt +0 -0
@@ -4,6 +4,25 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
5
5
  to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.2.0]
8
+
9
+ ### Added
10
+ - `--format {text,compact,json}` CLI option. The new `compact` format prints one
11
+ finding per line, compiler/linter-style (`file:line: severity [rule] message | at
12
+ xpath`) with a stable `VALID:`/`INVALID:` summary line — easy for tools and coding
13
+ agents to parse. `--json` is now an alias for `--format json`; the default rich
14
+ text output is unchanged.
15
+
16
+ ## [0.1.3]
17
+
18
+ ### Changed
19
+ - Country (`VAL-CTRY`) and currency (`VAL-CCY`) validation are now single universal
20
+ rules, like IBAN/LEI. `VAL-CTRY` checks every `<Ctry>` element; `VAL-CCY` checks
21
+ every `Ccy` attribute and `<Ccy>` element, document-wide, for all message types.
22
+ This closes gaps where country was unvalidated (e.g. pacs.008 2025, the pacs.009
23
+ family) and currency was unvalidated (pacs.002 2025), and replaces the ~13
24
+ per-module `VAL-CTRY` and ~19 per-module `VAL-CCY` rules.
25
+
7
26
  ## [0.1.2]
8
27
 
9
28
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cbpr-usage-rules
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Validate ISO 20022 CBPR+ XML messages against SWIFT usage (business) rules.
5
5
  Author: Pete Houghton
6
6
  License: MIT License
@@ -112,8 +112,11 @@ result = cbpr_rules.validate_string(xml_text, year=2026)
112
112
  # Human-readable (default). Exit code is non-zero if there are violations.
113
113
  cbpr-validate payment.xml --year 2025
114
114
 
115
- # JSON output
116
- cbpr-validate payment.xml --year 2025 --json
115
+ # Compact, one finding per line (compiler-style) — for tools and coding agents
116
+ cbpr-validate payment.xml --year 2025 --format compact
117
+
118
+ # JSON output (full machine-readable result)
119
+ cbpr-validate payment.xml --year 2025 --json # same as --format json
117
120
 
118
121
  # Read from stdin
119
122
  cat payment.xml | cbpr-validate --year 2026
@@ -127,6 +130,20 @@ cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
127
130
 
128
131
  Each reported violation shows the rule, **why** this instance failed (`Problem:`),
129
132
  the **offending XML** from your file (`Found:`), and its line + xpath (`At:`).
133
+
134
+ **For tools and coding agents**, prefer `--format compact` (one finding per line,
135
+ parseable like a compiler/linter) or `--json` (the full structured result):
136
+
137
+ ```text
138
+ $ cbpr-validate payment.xml --year 2025 --format compact
139
+ payment.xml:7: violation [pacs.008:R2] From vs Instructing Agent: ABCDGB2LXXX != GGGGGB2LXXX | at /AppHdr/Fr/FIId/FinInstnId/BICFI
140
+ payment.xml:63: violation [pacs.008:VAL-IBAN] invalid IBAN: 'GB29NWBK60161331926818' | at /Document/.../DbtrAcct/Id/IBAN
141
+ INVALID: 5 violations (87 rules)
142
+ ```
143
+
144
+ Each line is `file:line: severity [rule] message | at xpath`, and the final
145
+ `VALID:` / `INVALID:` line is a stable summary to grep for. With `--xsd`, schema
146
+ errors appear as `schema-error` lines and are counted in the summary.
130
147
  Advisory rules are summarised as a count by default; pass `--advisory` to list them.
131
148
 
132
149
  ---
@@ -141,7 +158,7 @@ Both `validate_file` and `validate_string` return a dictionary:
141
158
  "message_type": "pacs.008", # the rule set that was applied
142
159
  "detected_message_type": "pacs.008",# auto-detected from the Document namespace
143
160
  "year": 2025,
144
- "rules_evaluated": 86,
161
+ "rules_evaluated": 87,
145
162
  "violations": [
146
163
  {
147
164
  "rule_number": "pacs.008:R41", # unique within the message type
@@ -57,8 +57,11 @@ result = cbpr_rules.validate_string(xml_text, year=2026)
57
57
  # Human-readable (default). Exit code is non-zero if there are violations.
58
58
  cbpr-validate payment.xml --year 2025
59
59
 
60
- # JSON output
61
- cbpr-validate payment.xml --year 2025 --json
60
+ # Compact, one finding per line (compiler-style) — for tools and coding agents
61
+ cbpr-validate payment.xml --year 2025 --format compact
62
+
63
+ # JSON output (full machine-readable result)
64
+ cbpr-validate payment.xml --year 2025 --json # same as --format json
62
65
 
63
66
  # Read from stdin
64
67
  cat payment.xml | cbpr-validate --year 2026
@@ -72,6 +75,20 @@ cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
72
75
 
73
76
  Each reported violation shows the rule, **why** this instance failed (`Problem:`),
74
77
  the **offending XML** from your file (`Found:`), and its line + xpath (`At:`).
78
+
79
+ **For tools and coding agents**, prefer `--format compact` (one finding per line,
80
+ parseable like a compiler/linter) or `--json` (the full structured result):
81
+
82
+ ```text
83
+ $ cbpr-validate payment.xml --year 2025 --format compact
84
+ payment.xml:7: violation [pacs.008:R2] From vs Instructing Agent: ABCDGB2LXXX != GGGGGB2LXXX | at /AppHdr/Fr/FIId/FinInstnId/BICFI
85
+ payment.xml:63: violation [pacs.008:VAL-IBAN] invalid IBAN: 'GB29NWBK60161331926818' | at /Document/.../DbtrAcct/Id/IBAN
86
+ INVALID: 5 violations (87 rules)
87
+ ```
88
+
89
+ Each line is `file:line: severity [rule] message | at xpath`, and the final
90
+ `VALID:` / `INVALID:` line is a stable summary to grep for. With `--xsd`, schema
91
+ errors appear as `schema-error` lines and are counted in the summary.
75
92
  Advisory rules are summarised as a count by default; pass `--advisory` to list them.
76
93
 
77
94
  ---
@@ -86,7 +103,7 @@ Both `validate_file` and `validate_string` return a dictionary:
86
103
  "message_type": "pacs.008", # the rule set that was applied
87
104
  "detected_message_type": "pacs.008",# auto-detected from the Document namespace
88
105
  "year": 2025,
89
- "rules_evaluated": 86,
106
+ "rules_evaluated": 87,
90
107
  "violations": [
91
108
  {
92
109
  "rule_number": "pacs.008:R41", # unique within the message type
@@ -4,7 +4,7 @@ This package is AI generated. See the README for details and caveats.
4
4
  """
5
5
  from __future__ import annotations
6
6
 
7
- __version__ = "0.1.2"
7
+ __version__ = "0.2.0"
8
8
 
9
9
  from .models import Rule, Severity, Violation
10
10
  from .engine import validate_file, validate_string, list_rules, available
@@ -26,7 +26,18 @@ def _build_parser() -> argparse.ArgumentParser:
26
26
  help="Message type override, e.g. pacs.008 or pacs.008_stp "
27
27
  "(auto-detected from the Document namespace if omitted).",
28
28
  )
29
- p.add_argument("--json", action="store_true", help="Emit JSON instead of text.")
29
+ p.add_argument(
30
+ "--format",
31
+ choices=("text", "compact", "json"),
32
+ default="text",
33
+ help="Output format: text (rich, default), compact (one finding per line, "
34
+ "compiler-style — for tools/agents), or json (full machine-readable).",
35
+ )
36
+ p.add_argument(
37
+ "--json",
38
+ action="store_true",
39
+ help="Alias for --format json.",
40
+ )
30
41
  p.add_argument(
31
42
  "--xsd",
32
43
  action="append",
@@ -125,6 +136,43 @@ def _format_text(result: dict, show_advisory: bool = False) -> str:
125
136
  return "\n".join(lines)
126
137
 
127
138
 
139
+ def _format_compact(result: dict, source: str) -> str:
140
+ """One finding per line, compiler/linter-style, for tools and coding agents.
141
+
142
+ ``<source>:<line>: <severity> [<rule>] <message> | at <xpath>`` per violation,
143
+ followed by a single greppable ``VALID:``/``INVALID:`` summary line. Advisory
144
+ (non-enforced) rules are omitted; use --json or text --advisory for those.
145
+ """
146
+ lines: List[str] = []
147
+ for v in result["violations"]:
148
+ line = v["line"] if v["line"] is not None else "?"
149
+ message = " ".join((v.get("detail") or v["description"] or "").split())
150
+ suffix = f" | at {v['xpath']}" if v.get("xpath") else ""
151
+ lines.append(f"{source}:{line}: {v['severity']} [{v['rule_number']}] {message}{suffix}")
152
+
153
+ xsd = result.get("xsd")
154
+ schema_errors = 0
155
+ if xsd is not None:
156
+ for s in xsd["schemas"]:
157
+ if s.get("load_error"):
158
+ schema_errors += 1
159
+ lines.append(f"{s['file']}: schema-error could not load schema: {s['load_error']}")
160
+ for e in s["errors"]:
161
+ schema_errors += 1
162
+ eline = e["line"] if e.get("line") is not None else "?"
163
+ msg = " ".join((e.get("message") or "").split())
164
+ lines.append(f"{s['file']}:{eline}: schema-error {msg}")
165
+
166
+ n = len(result["violations"])
167
+ verdict = "VALID" if result["valid"] else "INVALID"
168
+ parts = [f"{n} violation{'s' if n != 1 else ''}"]
169
+ if xsd is not None:
170
+ parts.append(f"{schema_errors} schema error{'s' if schema_errors != 1 else ''}")
171
+ summary = f"{verdict}: {', '.join(parts)} ({result['rules_evaluated']} rules)"
172
+ lines.append(summary)
173
+ return "\n".join(lines)
174
+
175
+
128
176
  def main(argv: Optional[List[str]] = None) -> int:
129
177
  args = _build_parser().parse_args(argv)
130
178
 
@@ -153,6 +201,7 @@ def main(argv: Optional[List[str]] = None) -> int:
153
201
  print("error: --year is required to validate", file=sys.stderr)
154
202
  return 2
155
203
 
204
+ source = args.file or "<stdin>"
156
205
  try:
157
206
  if args.file:
158
207
  result = validate_file(args.file, args.year, args.msgtype, xsd=args.xsd)
@@ -163,8 +212,11 @@ def main(argv: Optional[List[str]] = None) -> int:
163
212
  print(f"error: {exc}", file=sys.stderr)
164
213
  return 2
165
214
 
166
- if args.json:
215
+ fmt = "json" if args.json else args.format
216
+ if fmt == "json":
167
217
  print(json.dumps(result, indent=2))
218
+ elif fmt == "compact":
219
+ print(_format_compact(result, source))
168
220
  else:
169
221
  print(_format_text(result, show_advisory=args.advisory))
170
222
 
@@ -11,7 +11,7 @@ import pkgutil
11
11
  from typing import Callable, Dict, List, Tuple
12
12
 
13
13
  from .models import Rule, Severity, Violation
14
- from .validators import is_valid_iban, is_valid_lei
14
+ from .validators import is_valid_country, is_valid_currency, is_valid_iban, is_valid_lei
15
15
 
16
16
  # (year, message_type) -> list[Rule]
17
17
  _REGISTRY: Dict[Tuple[int, str], List[Rule]] = {}
@@ -99,24 +99,32 @@ def _discover(year: int) -> None:
99
99
 
100
100
 
101
101
  # Cross-cutting datatype validations applied to every message type. In ISO 20022
102
- # these elements are self-validating datatypes wherever they appear, so a single
102
+ # these are self-validating datatypes wherever they appear, so a single
103
103
  # document-wide scan covers all message types (and all party positions - Debtor,
104
104
  # Creditor, Agent, Intermediary) correctly:
105
105
  # <IBAN> is IBAN2007Identifier (ISO 7064 mod-97);
106
- # <LEI> is LEIIdentifier (ISO 17442, 18 alphanumerics + 2 ISO 7064 check digits).
106
+ # <LEI> is LEIIdentifier (ISO 17442, 18 alphanumerics + 2 ISO 7064 check digits);
107
+ # <Ctry> is CountryCode (ISO 3166-1 alpha-2).
107
108
  # (number, element, validator, name, description)
108
- _UNIVERSAL = [
109
+ _UNIVERSAL_ELEMENT = [
109
110
  ("VAL-IBAN", "IBAN", is_valid_iban, "CBPR_Valid_IBAN",
110
111
  "Every IBAN must be structurally valid and pass the ISO 7064 mod-97 check."),
111
112
  ("VAL-LEI", "LEI", is_valid_lei, "CBPR_Valid_LEI",
112
113
  "Every LEI must be a structurally valid ISO 17442 identifier with correct check digits."),
114
+ ("VAL-CTRY", "Ctry", is_valid_country, "CBPR_Valid_Country",
115
+ "Every Country must be a valid ISO 3166-1 alpha-2 country code."),
113
116
  ]
114
117
 
118
+ # Currency is carried as the ``Ccy`` XML attribute on amount elements
119
+ # (ActiveCurrencyAndAmount etc.) and occasionally as a ``<Ccy>`` element (account
120
+ # currency). Both must be a valid ISO 4217 code.
121
+ _CCY_DESC = "Every currency must be a valid ISO 4217 currency code."
122
+
115
123
 
116
124
  def _universal_rules(msgtype: str) -> List[Rule]:
117
- """Build the cross-cutting datatype rules (IBAN, LEI) for a message type."""
125
+ """Build the cross-cutting datatype rules (IBAN, LEI, country, currency)."""
118
126
  rules: List[Rule] = []
119
- for number, element, validator, name, description in _UNIVERSAL:
127
+ for number, element, validator, name, description in _UNIVERSAL_ELEMENT:
120
128
  rule_id = f"{msgtype}:{number}"
121
129
 
122
130
  def check(msg, _el=element, _val=validator, _id=rule_id, _name=name, _desc=description):
@@ -138,9 +146,50 @@ def _universal_rules(msgtype: str) -> List[Rule]:
138
146
  return out
139
147
 
140
148
  rules.append(Rule(rule_id, name, description, Severity.VIOLATION, check))
149
+
150
+ rules.append(_currency_rule(msgtype))
141
151
  return rules
142
152
 
143
153
 
154
+ def _currency_rule(msgtype: str) -> Rule:
155
+ rule_id = f"{msgtype}:VAL-CCY"
156
+
157
+ def check(msg) -> List[Violation]:
158
+ out: List[Violation] = []
159
+
160
+ def flag(el, code):
161
+ out.append(
162
+ Violation(
163
+ rule_number=rule_id,
164
+ name="CBPR_Valid_Currency",
165
+ description=_CCY_DESC,
166
+ detail=f"invalid currency '{code}'",
167
+ found=msg.snippet_of(el),
168
+ xpath=msg.xpath_of(el),
169
+ line=msg.line_of(el),
170
+ )
171
+ )
172
+
173
+ # Every ``Ccy`` attribute, anywhere in the message.
174
+ for root in (msg.bah, msg.document):
175
+ if root is None:
176
+ continue
177
+ for el in root.iter():
178
+ if not isinstance(el.tag, str):
179
+ continue
180
+ code = el.get("Ccy")
181
+ if code and not is_valid_currency(code):
182
+ flag(el, code)
183
+ # Standalone ``<Ccy>`` elements (e.g. account currency).
184
+ for el in msg.iter_local("Ccy"):
185
+ code = msg.text_of(el)
186
+ if code and not is_valid_currency(code):
187
+ flag(el, code)
188
+ return out
189
+
190
+ return Rule(rule_id, "CBPR_Valid_Currency", _CCY_DESC, Severity.VIOLATION, check)
191
+
192
+
144
193
  def load_rules(year: int, msgtype: str) -> List[Rule]:
145
194
  _discover(int(year))
146
195
  rules = list(_REGISTRY.get(_key(year, msgtype), []))
@@ -11,7 +11,7 @@ rules are surfaced as advisory guidance. Algorithmic field validations
11
11
  from __future__ import annotations
12
12
 
13
13
  from ...registry import advisory, rule
14
- from ...validators import is_valid_bic, is_valid_country, is_valid_currency
14
+ from ...validators import is_valid_bic
15
15
  from ...helpers import (
16
16
  amount_equals_sum,
17
17
  business_msg_id_carries_group_id,
@@ -117,34 +117,6 @@ reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
117
117
  _val_bic)
118
118
 
119
119
 
120
- def _val_ctry(msg, report):
121
- for node in msg.find(RPT + "/Acct/Svcr/FinInstnId/PstlAdr/Ctry"):
122
- val = msg.text_of(node)
123
- if val and not is_valid_country(val):
124
- report(node, detail=f"invalid country '{val}'")
125
- for node in msg.find(RPT + "/Acct/Ownr/PstlAdr/Ctry"):
126
- val = msg.text_of(node)
127
- if val and not is_valid_country(val):
128
- report(node, detail=f"invalid country '{val}'")
129
-
130
-
131
- reg("VAL-CTRY", "CBPR_Valid_Country",
132
- "Every PostalAddress Country must be a valid ISO 3166 alpha-2 code.",
133
- _val_ctry)
134
-
135
-
136
- def _val_ccy(msg, report):
137
- for p in (RPT + "/Bal/Amt", RPT + "/Ntry/Amt"):
138
- for el, ccy in msg.attr_nodes(p, "Ccy"):
139
- if ccy and not is_valid_currency(ccy):
140
- report(el, detail=f"invalid currency '{ccy}'")
141
-
142
-
143
- reg("VAL-CCY", "CBPR_Valid_Amount_Currency",
144
- "Balance and Entry amount currency must be a valid ISO 4217 code.",
145
- _val_ccy)
146
-
147
-
148
120
  # ---------------------------------------------------------------------------
149
121
  # Advisory textual rules (not mechanically enforceable - surfaced as guidance)
150
122
  # ---------------------------------------------------------------------------
@@ -8,7 +8,7 @@ mechanizable textual rules are enforced, the rest are surfaced as advisories.
8
8
  from __future__ import annotations
9
9
 
10
10
  from ...registry import advisory, rule
11
- from ...validators import is_valid_bic, is_valid_country, is_valid_currency
11
+ from ...validators import is_valid_bic
12
12
  from ...helpers import (
13
13
  business_msg_id_carries_group_id,
14
14
  code_in,
@@ -76,22 +76,10 @@ reg("R4", "CBPR_Message_Definition_Identifier_TextualRule",
76
76
  # ---------------------------------------------------------------------------
77
77
  # Algorithmic field validations (brief), only for fields present in camt.054
78
78
  # ---------------------------------------------------------------------------
79
- reg("VAL-CCY", "CBPR_Valid_Entry_Currency",
80
- "Entry Amount currency must be a valid ISO 4217 code.",
81
- lambda msg, report: [
82
- report(el, detail=f"invalid currency '{ccy}'")
83
- for el, ccy in msg.attr_nodes(ENTRY + "/Amt", "Ccy")
84
- if ccy and not is_valid_currency(ccy)
85
- ])
86
-
87
79
  reg("VAL-BIC", "CBPR_Valid_Account_Servicer_BIC",
88
80
  "Account Servicer BICFI must be a structurally valid BIC.",
89
81
  each_value_valid(NTFCTN + "/Acct/Svcr/FinInstnId/BICFI", is_valid_bic, "BIC"))
90
82
 
91
- reg("VAL-CTRY", "CBPR_Valid_Account_Owner_Country",
92
- "Account Owner postal address Country must be a valid ISO 3166 code.",
93
- each_value_valid(NTFCTN + "/Acct/Ownr/PstlAdr/Ctry", is_valid_country, "country"))
94
-
95
83
 
96
84
  # ---------------------------------------------------------------------------
97
85
  # Advisory textual / guideline rules (not mechanically enforceable)
@@ -12,7 +12,7 @@ Paths are the short ISO 20022 tags from the guideline's XML Path column.
12
12
  from __future__ import annotations
13
13
 
14
14
  from ...registry import advisory, rule
15
- from ...validators import is_valid_bic, is_valid_country
15
+ from ...validators import is_valid_bic
16
16
  from ...helpers import (
17
17
  address_hybrid,
18
18
  address_lines_max_length,
@@ -172,10 +172,6 @@ reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
172
172
  "Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
173
173
  each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"))
174
174
 
175
- reg("VAL-CTRY", "CBPR_Valid_Originator_Country",
176
- "Originator Postal Address Country must be a valid ISO 3166 country code.",
177
- each_value_valid(ORGTR + "/PstlAdr/Ctry", is_valid_country, "country"))
178
-
179
175
 
180
176
  # ---------------------------------------------------------------------------
181
177
  # Advisory textual rules (not mechanically enforceable - surfaced as guidance)
@@ -12,7 +12,7 @@ from __future__ import annotations
12
12
  import re
13
13
 
14
14
  from ...registry import advisory, rule
15
- from ...validators import is_valid_bic, is_valid_country, is_valid_currency
15
+ from ...validators import is_valid_bic
16
16
  from ...helpers import (
17
17
  address_hybrid,
18
18
  address_lines_max_length,
@@ -691,23 +691,6 @@ def _run_all(*checks):
691
691
  return check
692
692
 
693
693
 
694
- def _valid_ccy(path):
695
- def check(msg, report):
696
- for el, ccy in msg.attr_nodes(path, "Ccy"):
697
- if ccy and not is_valid_currency(ccy):
698
- report(el, detail=f"invalid currency '{ccy}'")
699
- return check
700
-
701
-
702
- reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
703
- "Settlement amount currencies must be valid ISO 4217 codes.",
704
- _run_all(
705
- _valid_ccy(TX + "/OrgnlIntrBkSttlmAmt"),
706
- _valid_ccy(TX + "/RtrdIntrBkSttlmAmt"),
707
- _valid_ccy(TX + "/RtrdInstdAmt"),
708
- _valid_ccy(OTR + "/IntrBkSttlmAmt"),
709
- ))
710
-
711
694
  reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
712
695
  "Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
713
696
  _run_all(
@@ -715,15 +698,6 @@ reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
715
698
  each_value_valid(TX + "/InstdAgt/FinInstnId/BICFI", is_valid_bic, "BIC"),
716
699
  ))
717
700
 
718
- reg("VAL-CTRY", "CBPR_Valid_Country",
719
- "Every Country code must be a valid ISO 3166 country code.",
720
- _run_all(
721
- each_value_valid(RC + "/Dbtr/Pty/PstlAdr/Ctry", is_valid_country, "country"),
722
- each_value_valid(RC + "/Cdtr/Pty/PstlAdr/Ctry", is_valid_country, "country"),
723
- each_value_valid(OTR + "/Dbtr/Pty/PstlAdr/Ctry", is_valid_country, "country"),
724
- each_value_valid(OTR + "/Cdtr/Pty/PstlAdr/Ctry", is_valid_country, "country"),
725
- ))
726
-
727
701
  # ---------------------------------------------------------------------------
728
702
  # Advisory textual / guideline rules (not mechanically enforceable)
729
703
  # ---------------------------------------------------------------------------
@@ -11,7 +11,7 @@ XML paths are the short ISO 20022 tags from its Full_View / XML Path column.
11
11
  from __future__ import annotations
12
12
 
13
13
  from ...registry import advisory, rule
14
- from ...validators import is_valid_bic, is_valid_currency
14
+ from ...validators import is_valid_bic
15
15
  from ...helpers import (
16
16
  address_hybrid,
17
17
  address_lines_max_length,
@@ -302,14 +302,6 @@ reg("R44", "CBPR_DEBT_Rule_1_TextualRule",
302
302
 
303
303
  # Specific validations required by the brief (algorithmic), applied to the
304
304
  # fields where these data types appear in pacs.008.
305
- reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
306
- "Interbank Settlement Amount currency must be a valid ISO 4217 code.",
307
- lambda msg, report: [
308
- report(el, detail=f"invalid currency '{ccy}'")
309
- for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
310
- if ccy and not is_valid_currency(ccy)
311
- ])
312
-
313
305
  reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
314
306
  "Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
315
307
  each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"))
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
  import re as _re
14
14
 
15
15
  from ...registry import advisory, rule
16
- from ...validators import is_valid_bic, is_valid_country, is_valid_currency
16
+ from ...validators import is_valid_bic
17
17
  from ...helpers import (
18
18
  address_hybrid,
19
19
  address_lines_max_length,
@@ -251,22 +251,10 @@ reg("R8", "CBPR_Business_Service_Usage_TextualRule",
251
251
  code_in("/AppHdr/BizSvc", ["swift.cbprplus.stp.03"]))
252
252
 
253
253
 
254
- reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
255
- "Interbank Settlement Amount currency must be a valid ISO 4217 code.",
256
- lambda msg, report: [
257
- report(el, detail=f"invalid currency '{ccy}'")
258
- for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
259
- if ccy and not is_valid_currency(ccy)
260
- ])
261
-
262
254
  reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
263
255
  "Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
264
256
  each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"))
265
257
 
266
- reg("VAL-CTRY", "CBPR_Valid_Country",
267
- "Every Postal Address Country must be a valid ISO 3166 country code.",
268
- each_value_valid(TX + "/Dbtr/PstlAdr/Ctry", is_valid_country, "country"))
269
-
270
258
 
271
259
  # ---------------------------------------------------------------------------
272
260
  # Promoted from advisory: mechanizable textual rules now enforced.
@@ -11,7 +11,7 @@ Name/PostalAddress follow the *agent* (present-together) rule via FinInstnId.
11
11
  from __future__ import annotations
12
12
 
13
13
  from ...registry import advisory, rule
14
- from ...validators import is_valid_bic, is_valid_country, is_valid_currency
14
+ from ...validators import is_valid_bic
15
15
  from ...helpers import (
16
16
  address_hybrid,
17
17
  address_lines_max_length,
@@ -199,22 +199,10 @@ reg("R28", "CBPR_Duplication_Postal_Address_TextualRule",
199
199
  no_postal_address_duplication())
200
200
 
201
201
  # Algorithmic validations required by the brief, for fields present in pacs.009.
202
- reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
203
- "Interbank Settlement Amount currency must be a valid ISO 4217 code.",
204
- lambda msg, report: [
205
- report(el, detail=f"invalid currency '{ccy}'")
206
- for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
207
- if ccy and not is_valid_currency(ccy)
208
- ])
209
-
210
202
  reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
211
203
  "Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
212
204
  each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"))
213
205
 
214
- reg("VAL-CTRY", "CBPR_Valid_Postal_Country",
215
- "Every Postal Address Country must be a valid ISO 3166 country code.",
216
- each_value_valid(TX + "/CdtrAgt/FinInstnId/PstlAdr/Ctry", is_valid_country, "country"))
217
-
218
206
 
219
207
  # ---------------------------------------------------------------------------
220
208
  # Advisory textual rules (not mechanically enforceable - surfaced as guidance)
@@ -12,7 +12,7 @@ Name/PostalAddress + grace-period rules (not the customer party rules).
12
12
  from __future__ import annotations
13
13
 
14
14
  from ...registry import advisory, rule
15
- from ...validators import is_valid_bic, is_valid_currency
15
+ from ...validators import is_valid_bic
16
16
  from ...helpers import (
17
17
  address_hybrid,
18
18
  address_lines_max_length,
@@ -198,14 +198,6 @@ reg("R20", "CBPR_Duplication_Postal_Address_TextualRule",
198
198
  # ---------------------------------------------------------------------------
199
199
  # Specific algorithmic validations required by the brief (fields present here).
200
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
201
  reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
210
202
  "Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
211
203
  each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"))
@@ -10,7 +10,7 @@ are the short ISO 20022 tags from its Full_View / XML Path column.
10
10
  from __future__ import annotations
11
11
 
12
12
  from ...registry import advisory, rule
13
- from ...validators import is_valid_bic, is_valid_currency
13
+ from ...validators import is_valid_bic
14
14
  from ...helpers import (
15
15
  address_hybrid,
16
16
  address_lines_max_length,
@@ -235,14 +235,6 @@ reg("R8", "CBPR_Business_Service_Usage_TextualRule",
235
235
  code_in("/AppHdr/BizSvc", ["swift.cbprplus.cov.03"]))
236
236
 
237
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
238
  reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
247
239
  "Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
248
240
  each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"))
@@ -9,7 +9,7 @@ module (``pacs_008``): combinator-built rules go through ``reg``/``_agent_block`
9
9
  from __future__ import annotations
10
10
 
11
11
  from ...registry import advisory, rule
12
- from ...validators import is_valid_bic, is_valid_country, is_valid_currency
12
+ from ...validators import is_valid_bic
13
13
  from ...helpers import (
14
14
  address_hybrid,
15
15
  address_lines_max_length,
@@ -141,14 +141,6 @@ reg("R69", "CBPR_Structured_Remittance_Information_TextualRule",
141
141
  # ---------------------------------------------------------------------------
142
142
  # Algorithmic field validation (brief), only for fields present in pain.001.
143
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
144
  reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
153
145
  "Every Financial Institution BICFI must be a structurally valid BIC.",
154
146
  lambda msg, report: [
@@ -164,23 +156,6 @@ reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
164
156
  if msg.text_of(node) and not is_valid_bic(msg.text_of(node))
165
157
  ])
166
158
 
167
- reg("VAL-CTRY", "CBPR_Valid_Country",
168
- "Every Country must be a valid ISO 3166-1 alpha-2 code.",
169
- lambda msg, report: [
170
- report(node, detail=f"invalid country '{msg.text_of(node)}'")
171
- for path in (
172
- PMTINF + "/Dbtr/PstlAdr/Ctry",
173
- PMTINF + "/DbtrAgt/FinInstnId/PstlAdr/Ctry",
174
- TX + "/Cdtr/PstlAdr/Ctry",
175
- TX + "/CdtrAgt/FinInstnId/PstlAdr/Ctry",
176
- TX + "/IntrmyAgt1/FinInstnId/PstlAdr/Ctry",
177
- TX + "/IntrmyAgt2/FinInstnId/PstlAdr/Ctry",
178
- TX + "/IntrmyAgt3/FinInstnId/PstlAdr/Ctry",
179
- )
180
- for node in msg.find(path)
181
- if msg.text_of(node) and not is_valid_country(msg.text_of(node))
182
- ])
183
-
184
159
 
185
160
  # ---------------------------------------------------------------------------
186
161
  # Advisory textual rules (not mechanically enforceable - surfaced as guidance)
@@ -11,8 +11,6 @@ from __future__ import annotations
11
11
  from ...registry import advisory, rule
12
12
  from ...validators import (
13
13
  is_valid_bic,
14
- is_valid_country,
15
- is_valid_currency,
16
14
  )
17
15
  from ...helpers import (
18
16
  amount_equals_sum,
@@ -89,22 +87,10 @@ reg("R13", "CBPR_Original_Instruction_Identification_FormalRule",
89
87
  # ---------------------------------------------------------------------------
90
88
  # Algorithmic field validations (brief), for fields present in camt.052
91
89
  # ---------------------------------------------------------------------------
92
- reg("VAL-CCY", "CBPR_Valid_Balance_Currency",
93
- "Balance Amount currency must be a valid ISO 4217 code.",
94
- lambda msg, report: [
95
- report(el, detail=f"invalid currency '{ccy}'")
96
- for el, ccy in msg.attr_nodes(RPT + "/Bal/Amt", "Ccy")
97
- if ccy and not is_valid_currency(ccy)
98
- ])
99
-
100
90
  reg("VAL-BIC", "CBPR_Valid_Account_Servicer_BIC",
101
91
  "Account Servicer BICFI must be a structurally valid BIC.",
102
92
  each_value_valid(RPT + "/Acct/Svcr/FinInstnId/BICFI", is_valid_bic, "BIC"))
103
93
 
104
- reg("VAL-CTRY", "CBPR_Valid_Account_Owner_Country",
105
- "Account Owner Postal Address Country must be a valid ISO 3166 code.",
106
- each_value_valid(RPT + "/Acct/Ownr/PstlAdr/Ctry", is_valid_country, "Country"))
107
-
108
94
 
109
95
  # ---------------------------------------------------------------------------
110
96
  # Mechanizable textual rules promoted from advisory to enforced.
@@ -11,7 +11,7 @@ rules are surfaced via ``advisory``.
11
11
  from __future__ import annotations
12
12
 
13
13
  from ...registry import advisory, rule
14
- from ...validators import is_valid_bic, is_valid_country, is_valid_currency
14
+ from ...validators import is_valid_bic
15
15
  from ...helpers import (
16
16
  business_msg_id_carries_group_id,
17
17
  each_value_valid,
@@ -97,23 +97,10 @@ reg("R13", "CBPR_Original_Instruction_Identification_FormalRule",
97
97
  # Algorithmic field validation (brief-required VAL-* checks), applied to the
98
98
  # fields where these data types appear in camt.054.
99
99
  # ---------------------------------------------------------------------------
100
- reg("VAL-CCY", "CBPR_Valid_Entry_Amount_Currency",
101
- "Entry Amount currency must be a valid ISO 4217 code.",
102
- lambda msg, report: [
103
- report(el, detail=f"invalid currency '{ccy}'")
104
- for el, ccy in msg.attr_nodes(ENTRY + "/Amt", "Ccy")
105
- if ccy and not is_valid_currency(ccy)
106
- ])
107
-
108
100
  reg("VAL-BIC", "CBPR_Valid_Account_Servicer_BIC",
109
101
  "Account Servicer BICFI must be a structurally valid BIC.",
110
102
  each_value_valid(NTFCTN + "/Acct/Svcr/FinInstnId/BICFI", is_valid_bic, "BIC"))
111
103
 
112
- reg("VAL-CTRY", "CBPR_Valid_Account_Servicer_Country",
113
- "Account Servicer postal address Country must be a valid ISO 3166 code.",
114
- each_value_valid(NTFCTN + "/Acct/Svcr/FinInstnId/PstlAdr/Ctry",
115
- is_valid_country, "country"))
116
-
117
104
 
118
105
  # ---------------------------------------------------------------------------
119
106
  # Mechanizable textual rules promoted from advisory to enforced. Both are
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  import re as _re
11
11
 
12
12
  from ...registry import advisory, rule
13
- from ...validators import is_valid_bic, is_valid_currency
13
+ from ...validators import is_valid_bic
14
14
  from ...helpers import (
15
15
  business_msg_id_carries_group_id,
16
16
  header_msg_def_id_matches,
@@ -141,15 +141,6 @@ reg("R20", "CBPR_Party_Name_Any_BIC_FormalRule",
141
141
  # ---------------------------------------------------------------------------
142
142
  # Algorithmic field validation (project brief) - only fields present here.
143
143
  # ---------------------------------------------------------------------------
144
- reg("VAL-CCY", "CBPR_Valid_Original_Interbank_Settlement_Currency",
145
- "Original Interbank Settlement 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 + "/OrgnlTxRef/IntrBkSttlmAmt", "Ccy")
149
- if ccy and not is_valid_currency(ccy)
150
- ])
151
-
152
-
153
144
  @rule(MT, YEAR, "VAL-BIC", "CBPR_Valid_Agent_BIC",
154
145
  "Every Instructing/Instructed Agent BICFI must be a structurally valid BIC.")
155
146
  def _val_bic(msg, report):
@@ -10,7 +10,7 @@ Structure mirrors the reference module ``y2025/pacs_008.py``.
10
10
  from __future__ import annotations
11
11
 
12
12
  from ...registry import advisory, rule
13
- from ...validators import is_valid_bic, is_valid_country, is_valid_currency
13
+ from ...validators import is_valid_bic
14
14
  from ...helpers import (
15
15
  bic_presence_exclusive,
16
16
  business_msg_id_carries_group_id,
@@ -258,23 +258,6 @@ def _run_all(*checks):
258
258
  return check
259
259
 
260
260
 
261
- def _valid_ccy(path: str):
262
- def check(msg, report):
263
- for el, ccy in msg.attr_nodes(path, "Ccy"):
264
- if ccy and not is_valid_currency(ccy):
265
- report(el, detail=f"invalid currency '{ccy}'")
266
- return check
267
-
268
-
269
- reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
270
- "Settlement amount currencies must be valid ISO 4217 codes.",
271
- _run_all(
272
- _valid_ccy(TX + "/OrgnlIntrBkSttlmAmt"),
273
- _valid_ccy(TX + "/RtrdIntrBkSttlmAmt"),
274
- _valid_ccy(TX + "/RtrdInstdAmt"),
275
- _valid_ccy(OTR + "/IntrBkSttlmAmt"),
276
- ))
277
-
278
261
  reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
279
262
  "Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
280
263
  _run_all(
@@ -282,15 +265,6 @@ reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
282
265
  each_value_valid(TX + "/InstdAgt/FinInstnId/BICFI", is_valid_bic, "BIC"),
283
266
  ))
284
267
 
285
- reg("VAL-CTRY", "CBPR_Valid_Country",
286
- "Every Country code must be a valid ISO 3166 country code.",
287
- _run_all(
288
- each_value_valid(RC + "/Dbtr/Pty/PstlAdr/Ctry", is_valid_country, "country"),
289
- each_value_valid(RC + "/Cdtr/Pty/PstlAdr/Ctry", is_valid_country, "country"),
290
- each_value_valid(OTR + "/Dbtr/Pty/PstlAdr/Ctry", is_valid_country, "country"),
291
- each_value_valid(OTR + "/Cdtr/Pty/PstlAdr/Ctry", is_valid_country, "country"),
292
- ))
293
-
294
268
 
295
269
  # ---------------------------------------------------------------------------
296
270
  # Mechanizable textual rules + advisory textual rules.
@@ -9,7 +9,7 @@ registered as advisories.
9
9
  from __future__ import annotations
10
10
 
11
11
  from ...registry import advisory, rule
12
- from ...validators import is_valid_bic, is_valid_country, is_valid_currency
12
+ from ...validators import is_valid_bic
13
13
  from ...helpers import (
14
14
  bic_presence_exclusive,
15
15
  business_msg_id_carries_group_id,
@@ -307,22 +307,10 @@ reg("R63", "CBPR_Structured_RemittanceInformation_TextualRule",
307
307
  # ---------------------------------------------------------------------------
308
308
  # Algorithmic field validation required by the project brief
309
309
  # ---------------------------------------------------------------------------
310
- reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
311
- "Interbank Settlement Amount currency must be a valid ISO 4217 code.",
312
- lambda msg, report: [
313
- report(el, detail=f"invalid currency '{ccy}'")
314
- for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
315
- if ccy and not is_valid_currency(ccy)
316
- ])
317
-
318
310
  reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
319
311
  "Every BICFI in the message must be a structurally valid BIC.",
320
312
  each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"))
321
313
 
322
- reg("VAL-CTRY", "CBPR_Valid_Country",
323
- "Every Country code must be a valid ISO 3166 code.",
324
- each_value_valid(TX + "/Dbtr/PstlAdr/Ctry", is_valid_country, "Country"))
325
-
326
314
 
327
315
  # ---------------------------------------------------------------------------
328
316
  # Advisory textual rules (not mechanically enforceable - surfaced as guidance)
@@ -11,7 +11,7 @@ from __future__ import annotations
11
11
  import re
12
12
 
13
13
  from ...registry import advisory, rule
14
- from ...validators import is_valid_bic, is_valid_country, is_valid_currency
14
+ from ...validators import is_valid_bic
15
15
  from ...helpers import (
16
16
  bic_presence_exclusive,
17
17
  business_msg_id_carries_group_id,
@@ -309,22 +309,10 @@ def _r20(msg, report):
309
309
  # ---------------------------------------------------------------------------
310
310
  # Algorithmic field validation (VAL-*) - only for fields present in this message
311
311
  # ---------------------------------------------------------------------------
312
- reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
313
- "Interbank Settlement Amount currency must be a valid ISO 4217 code.",
314
- lambda msg, report: [
315
- report(el, detail=f"invalid currency '{ccy}'")
316
- for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
317
- if ccy and not is_valid_currency(ccy)
318
- ])
319
-
320
312
  reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
321
313
  "Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
322
314
  each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"))
323
315
 
324
- reg("VAL-CTRY", "CBPR_Valid_Country",
325
- "Every PostalAddress Country must be a valid ISO 3166 alpha-2 code.",
326
- each_value_valid(TX + "/Dbtr/PstlAdr/Ctry", is_valid_country, "country"))
327
-
328
316
 
329
317
  # ---------------------------------------------------------------------------
330
318
  # Advisory textual rules (not mechanically enforceable - surfaced as guidance)
@@ -9,7 +9,7 @@ otherwise surfaced as advisories.
9
9
  from __future__ import annotations
10
10
 
11
11
  from ...registry import advisory, rule
12
- from ...validators import is_valid_bic, is_valid_currency
12
+ from ...validators import is_valid_bic
13
13
  from ...helpers import (
14
14
  business_msg_id_carries_group_id,
15
15
  header_msg_def_id_matches,
@@ -161,15 +161,6 @@ for _num, _path in _AGENT_NAME_ADR.items():
161
161
  # ---------------------------------------------------------------------------
162
162
  # Algorithmic field validation (brief), only for fields present in pacs.009.
163
163
  # ---------------------------------------------------------------------------
164
- reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
165
- "Interbank Settlement Amount currency must be a valid ISO 4217 code.",
166
- lambda msg, report: [
167
- report(el, detail=f"invalid currency '{ccy}'")
168
- for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
169
- if ccy and not is_valid_currency(ccy)
170
- ])
171
-
172
-
173
164
  @rule(MT, YEAR, "VAL-BIC", "CBPR_Valid_Agent_BIC",
174
165
  "Every Agent BICFI in the message must be a structurally valid BIC.")
175
166
  def _val_bic(msg, report):
@@ -9,7 +9,7 @@ otherwise surfaced as advisories.
9
9
  from __future__ import annotations
10
10
 
11
11
  from ...registry import advisory, rule
12
- from ...validators import is_valid_bic, is_valid_currency
12
+ from ...validators import is_valid_bic
13
13
  from ...helpers import (
14
14
  not_matching_pattern,
15
15
  presence_together,
@@ -167,15 +167,6 @@ for _num, _path in _AGENT_NAME_ADR.items():
167
167
  # ---------------------------------------------------------------------------
168
168
  # Algorithmic field validation (brief), only for fields present in pacs.009 ADV.
169
169
  # ---------------------------------------------------------------------------
170
- reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
171
- "Interbank Settlement Amount currency must be a valid ISO 4217 code.",
172
- lambda msg, report: [
173
- report(el, detail=f"invalid currency '{ccy}'")
174
- for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
175
- if ccy and not is_valid_currency(ccy)
176
- ])
177
-
178
-
179
170
  @rule(MT, YEAR, "VAL-BIC", "CBPR_Valid_Agent_BIC",
180
171
  "Every Agent BICFI in the message must be a structurally valid BIC.")
181
172
  def _val_bic(msg, report):
@@ -10,7 +10,7 @@ are the short ISO 20022 tags from its Full_View / XML Path column.
10
10
  from __future__ import annotations
11
11
 
12
12
  from ...registry import advisory, rule
13
- from ...validators import is_valid_bic, is_valid_currency
13
+ from ...validators import is_valid_bic
14
14
  from ...helpers import (
15
15
  bic_presence_exclusive,
16
16
  business_msg_id_carries_group_id,
@@ -210,14 +210,6 @@ _party_name_adr("R59", UND + "/UltmtCdtr")
210
210
  # ---------------------------------------------------------------------------
211
211
  # Algorithmic field validation (brief), for fields present in pacs.009 COV.
212
212
  # ---------------------------------------------------------------------------
213
- reg("VAL-CCY", "CBPR_Valid_Settlement_Currency",
214
- "Interbank Settlement Amount currency must be a valid ISO 4217 code.",
215
- lambda msg, report: [
216
- report(el, detail=f"invalid currency '{ccy}'")
217
- for el, ccy in msg.attr_nodes(TX + "/IntrBkSttlmAmt", "Ccy")
218
- if ccy and not is_valid_currency(ccy)
219
- ])
220
-
221
213
  reg("VAL-BIC", "CBPR_Valid_Agent_BIC",
222
214
  "Instructing/Instructed Agent BICFI must be a structurally valid BIC.",
223
215
  each_value_valid(TX + "/InstgAgt/FinInstnId/BICFI", is_valid_bic, "BIC"))
@@ -9,7 +9,7 @@ small helpers, and cross-field / cross-schema logic is written as bespoke
9
9
  from __future__ import annotations
10
10
 
11
11
  from ...registry import advisory, rule
12
- from ...validators import is_valid_bic, is_valid_country, is_valid_currency
12
+ from ...validators import is_valid_bic
13
13
  from ...helpers import (
14
14
  business_msg_id_carries_group_id,
15
15
  header_msg_def_id_matches,
@@ -182,14 +182,6 @@ _CTRY_PATHS = [
182
182
  ]
183
183
 
184
184
 
185
- @rule(MT, YEAR, "VAL-CCY", "CBPR_Valid_Instructed_Amount_Currency",
186
- "Instructed Amount currency must be a valid ISO 4217 code.")
187
- def _val_ccy(msg, report):
188
- for el, ccy in msg.attr_nodes(TX + "/Amt/InstdAmt", "Ccy"):
189
- if ccy and not is_valid_currency(ccy):
190
- report(el, detail=f"invalid currency '{ccy}'")
191
-
192
-
193
185
  @rule(MT, YEAR, "VAL-BIC", "CBPR_Valid_Agent_BIC",
194
186
  "Every Financial Institution BICFI must be a structurally valid BIC.")
195
187
  def _val_bic(msg, report):
@@ -200,16 +192,6 @@ def _val_bic(msg, report):
200
192
  report(node, detail=f"invalid BIC: '{val}'")
201
193
 
202
194
 
203
- @rule(MT, YEAR, "VAL-CTRY", "CBPR_Valid_Country",
204
- "Every Country code in a Postal Address must be a valid ISO 3166 code.")
205
- def _val_ctry(msg, report):
206
- for path in _CTRY_PATHS:
207
- for node in msg.find(path):
208
- val = msg.text_of(node)
209
- if val and not is_valid_country(val):
210
- report(node, detail=f"invalid country: '{val}'")
211
-
212
-
213
195
  # ---------------------------------------------------------------------------
214
196
  # Promoted from advisory: mechanizable header / address / remittance rules.
215
197
  # Each combinator is conservative: it skips when its inputs are absent or
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cbpr-usage-rules
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Validate ISO 20022 CBPR+ XML messages against SWIFT usage (business) rules.
5
5
  Author: Pete Houghton
6
6
  License: MIT License
@@ -112,8 +112,11 @@ result = cbpr_rules.validate_string(xml_text, year=2026)
112
112
  # Human-readable (default). Exit code is non-zero if there are violations.
113
113
  cbpr-validate payment.xml --year 2025
114
114
 
115
- # JSON output
116
- cbpr-validate payment.xml --year 2025 --json
115
+ # Compact, one finding per line (compiler-style) — for tools and coding agents
116
+ cbpr-validate payment.xml --year 2025 --format compact
117
+
118
+ # JSON output (full machine-readable result)
119
+ cbpr-validate payment.xml --year 2025 --json # same as --format json
117
120
 
118
121
  # Read from stdin
119
122
  cat payment.xml | cbpr-validate --year 2026
@@ -127,6 +130,20 @@ cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
127
130
 
128
131
  Each reported violation shows the rule, **why** this instance failed (`Problem:`),
129
132
  the **offending XML** from your file (`Found:`), and its line + xpath (`At:`).
133
+
134
+ **For tools and coding agents**, prefer `--format compact` (one finding per line,
135
+ parseable like a compiler/linter) or `--json` (the full structured result):
136
+
137
+ ```text
138
+ $ cbpr-validate payment.xml --year 2025 --format compact
139
+ payment.xml:7: violation [pacs.008:R2] From vs Instructing Agent: ABCDGB2LXXX != GGGGGB2LXXX | at /AppHdr/Fr/FIId/FinInstnId/BICFI
140
+ payment.xml:63: violation [pacs.008:VAL-IBAN] invalid IBAN: 'GB29NWBK60161331926818' | at /Document/.../DbtrAcct/Id/IBAN
141
+ INVALID: 5 violations (87 rules)
142
+ ```
143
+
144
+ Each line is `file:line: severity [rule] message | at xpath`, and the final
145
+ `VALID:` / `INVALID:` line is a stable summary to grep for. With `--xsd`, schema
146
+ errors appear as `schema-error` lines and are counted in the summary.
130
147
  Advisory rules are summarised as a count by default; pass `--advisory` to list them.
131
148
 
132
149
  ---
@@ -141,7 +158,7 @@ Both `validate_file` and `validate_string` return a dictionary:
141
158
  "message_type": "pacs.008", # the rule set that was applied
142
159
  "detected_message_type": "pacs.008",# auto-detected from the Document namespace
143
160
  "year": 2025,
144
- "rules_evaluated": 86,
161
+ "rules_evaluated": 87,
145
162
  "violations": [
146
163
  {
147
164
  "rule_number": "pacs.008:R41", # unique within the message type